diff --git a/backend/src/index.test.ts b/backend/src/index.test.ts index 54cbe94..c4ce243 100644 --- a/backend/src/index.test.ts +++ b/backend/src/index.test.ts @@ -17,6 +17,7 @@ const eventHistoryMocks = vi.hoisted(() => ({ getGlobalEvents: vi.fn(), countAllEvents: vi.fn(), recordEvent: vi.fn(), + getStreamEventSummary: vi.fn(), })); vi.mock("./services/streamStore", () => streamStoreMocks); @@ -214,6 +215,7 @@ beforeEach(() => { eventHistoryMocks.getGlobalEvents.mockReset(); eventHistoryMocks.countAllEvents.mockReset(); eventHistoryMocks.getStreamHistory.mockReset(); + eventHistoryMocks.getStreamEventSummary.mockReset(); }); describe("GET /api/streams", () => { diff --git a/backend/src/index.ts b/backend/src/index.ts index a8b8d2e..00a751e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -779,6 +779,22 @@ app.get("/api/streams/:id/history", (req: Request, res: Response) => { res.json({ data, total, limit, offset }); }); +app.get("/api/streams/:id/history/summary", (req: Request, res: Response) => { + const parsedId = parseStreamId(req.params.id); + if (!parsedId.ok) { + sendValidationError(req, res, parsedId.issues); + return; + } + + const stream = getStream(parsedId.value); + if (!stream) { + sendApiError(req, res, 404, "Stream not found.", { code: "NOT_FOUND" }); + return; + } + + res.json({ data: getStreamEventSummary(parsedId.value) }); +}); + app.get("/api/streams/:id/snapshot", (req: Request, res: Response) => { const parsedId = parseStreamId(req.params.id); if (!parsedId.ok) { diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 04a766c..dd781e3 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -918,6 +918,53 @@ export const swaggerDocument = { }, }, }, + "/api/streams/{id}/history/summary": { + get: { + summary: "Get stream event count summary", + description: "Returns aggregated event counts per type for a stream. Useful for dashboard badges. Uses a single GROUP BY query; missing event types return 0.", + parameters: [ + { + name: "id", + in: "path", + required: true, + description: "The unique ID of the stream.", + schema: { type: "string" }, + }, + ], + responses: { + "200": { + description: "Event count summary.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + data: { + type: "object", + required: ["created", "claimed", "canceled", "start_time_updated"], + properties: { + created: { type: "integer", example: 1 }, + claimed: { type: "integer", example: 3 }, + canceled: { type: "integer", example: 0 }, + start_time_updated: { type: "integer", example: 1 }, + }, + }, + }, + }, + }, + }, + }, + "404": { + description: "Stream not found.", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + }, + }, + }, "/api/streams/{id}/snapshot": { get: { summary: "Get Stream Snapshot",