# Spec Delta: api-plugin-import — add SSE progress push ## ADDED Requirements ### Requirement: SSE progress push — `GET /imports/:id/progress` The package SHALL expose `GET /imports/:id/progress` returning `Content-Type: text/event-stream; charset=utf-8`, mounted under the existing `requireAuth()` middleware. Frames SHALL be driven directly off the existing `ImportService.bus` events (`import.progress`, `import.completed`, `import.failed`); NO polling loop over the store is permitted. Every frame SHALL carry the canonical `ImportProgress` JSON payload. Three SSE event kinds SHALL be emitted: `progress` (every `import.progress` tick), `complete` (terminal, on `import.completed`), and `failed` (terminal, on `import.failed`). The stream SHALL close cleanly after a terminal event. The handler SHALL respect the request's `AbortSignal` (client disconnect) by detaching the bus listener and incrementing `import.sse.disconnected{reason: "client"}`. Foreign-owner or missing-job lookups SHALL return HTTP 404 RFC 7807 problem+json with `code === IMPORT.NOT_FOUND` (anti-enumeration parity with `GET /:id`). Anonymous requests SHALL return HTTP 401 problem+json via the existing auth middleware. The package SHALL extend `getImportInstruments()` with three additional counters: `import.sse.connected` (Counter), `import.sse.events_sent` (Counter, labelled by `kind ∈ "progress" | "complete" | "failed"`), `import.sse.disconnected` (Counter, labelled by `reason ∈ "complete" | "failed" | "client"`). The counters SHALL be cached on the same bundle as the existing six instruments and SHALL rebind through `resetImportInstruments()`. #### Scenario: opens a text/event-stream and emits an initial progress frame - **WHEN** an authenticated caller `GET /imports/:id/progress` for a job they own - **THEN** the response is HTTP 200 with `content-type: text/event-stream; charset=utf-8` and the first frame is `event: progress\ndata: \n\n` #### Scenario: emits a progress frame for every bus progress event - **WHEN** the service emits `import.progress` while the client is connected - **THEN** the stream writes a frame whose first non-id line is `event: progress` and whose `data:` line decodes to an `ImportProgress` object with the matching `processedRows` #### Scenario: emits a complete frame and closes on import.completed - **WHEN** the service emits `import.completed` - **THEN** the stream writes a frame whose first non-id line is `event: complete` and then closes; subsequent reads from the stream return `done: true` #### Scenario: emits a failed frame and closes on import.failed - **WHEN** the service emits `import.failed` - **THEN** the stream writes a frame whose first non-id line is `event: failed` and then closes #### Scenario: returns 404 problem+json for a foreign-owner job - **WHEN** alice owns job `j1` and bob requests `GET /imports/j1/progress` - **THEN** the response is HTTP 404 with `content-type: application/problem+json` and a body whose `code === "IMPORT.NOT_FOUND"` #### Scenario: returns 401 problem+json when the caller is anonymous - **WHEN** an anonymous caller requests `GET /imports/:id/progress` - **THEN** the response is HTTP 401 with `content-type: application/problem+json` #### Scenario: detaches the listener and increments disconnect counter on client abort - **WHEN** the client aborts the request before a terminal event - **THEN** the handler unsubscribes from the bus and increments `import.sse.disconnected{reason:"client"}` #### Scenario: increments import.sse counters on connect, send, and disconnect - **WHEN** a connection opens, two progress events fire, and a terminal `complete` event closes the stream - **THEN** `import.sse.connected` increments by 1, `import.sse.events_sent{kind}` increments by 3 (2× `progress` + 1× `complete`), and `import.sse.disconnected{reason:"complete"}` increments by 1 **Verified by:** `src/routes/progress-sse/progress-sse.test.ts::"opens a text/event-stream and emits an initial progress frame"` AND `src/routes/progress-sse/progress-sse.test.ts::"emits a progress frame for every bus progress event"` AND `src/routes/progress-sse/progress-sse.test.ts::"emits a complete frame and closes on import.completed"` AND `src/routes/progress-sse/progress-sse.test.ts::"emits a failed frame and closes on import.failed"` AND `src/routes/progress-sse/progress-sse.test.ts::"returns 404 problem+json for a foreign-owner job"` AND `src/routes/progress-sse/progress-sse.test.ts::"returns 401 problem+json when the caller is anonymous"` AND `src/routes/progress-sse/progress-sse.test.ts::"detaches the listener and increments disconnect counter on client abort"` AND `src/routes/progress-sse/progress-sse.test.ts::"increments import.sse counters on connect, send, and disconnect"` AND `src/otel/instruments.test.ts::"bundle exposes the SSE counters"`