feat(015-reporting-dashboards): four real reporting/analytics endpoints
Wires the pre-scaffolded, unused platform/reports module (ReportsService
.generateSummaryReport previously returned {}) into four real, admin-
gated dashboards matching docs/09-testing-observability-cicd.md's own
table:
- GET /admin/reports/management: total cases, AI-resolved, human-
escalated, resolved/open, SLA compliance/breaches, escalation count,
average response/resolution time.
- GET /admin/reports/product/:externalProductId: support volume,
problem-category breakdown, recurring problems, AI-resolution/human-
escalation rate, top error codes.
- GET /admin/reports/support: current per-agent workload, SLA at-risk/
breached counts, escalation count, response/resolution performance.
- GET /admin/reports/ai: AI resolution/human-handoff rate, failed-
troubleshooting-then-escalated rate, knowledge-match rate, confidence
distribution (reusing 005-ai-support's own decideConfidenceBand),
tool invocation success/failure.
Every rate/average is number|null -- null means no qualifying data in
range, never a computed NaN or a misleading 0. Adds one new durable
table, ErrorCodeLookup, since 014-full-observability's own equivalent
metric is a process-lifetime Prometheus counter unusable for a
historical "top errors" report.
Verified end-to-end against real Postgres/Redis: every figure checked
against hand-computed expected values, including a no-activity range
(all-zero counts, all-null rates) and cross-product isolation.
Also fixes a real regression the new ErrorCodeLookup FK caused in the
pre-existing known-issues.test.ts (its afterAll deleted ErrorCode rows
before the now-referencing lookup rows).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
814d9d7b17
commit
d65683641a
@@ -47,3 +47,48 @@
|
||||
required — every open question (default date window, SLA-risk threshold, top-N limit) had a
|
||||
reasonable, documented, CONFIGURABLE default (see Assumptions), matching the roadmap's own
|
||||
"never hardcode a placeholder value and ship it as final" instruction.
|
||||
|
||||
## Implementation Notes (post-build)
|
||||
|
||||
- Named the Product dashboard's own repository class `ProductReportRepository` (not
|
||||
`ProductRepository`) once it became clear resolving `externalProductId -> Product` should
|
||||
reuse `catalog/products`' own already-public `productsRepository.findByExternalProductId`
|
||||
rather than duplicating that lookup — avoids a name collision and keeps "one authority per
|
||||
concern" (Constitution Principle I's spirit) for product resolution.
|
||||
- `ManagementRepository` and `SupportRepository` both needed byte-identical
|
||||
first-response-duration and resolution-duration queries. Extracted into a shared
|
||||
`SharedReportRepository` both compose, rather than duplicating the Prisma query (or the
|
||||
averaging helper alone) twice — discovered while writing the second repository and seeing the
|
||||
copy-paste, not planned upfront in research.md.
|
||||
- "Top errors"/"most common errors" resolution-back-to-`code` logic moved into
|
||||
`ErrorCodesService.getTopErrorCodesForProduct` (a new method on the module that already owns
|
||||
`ErrorCode`), rather than the reports module reaching into `errorCodesRepository`/
|
||||
`errorCodeLookupRepository` directly — cleaner module-boundary ownership than research.md's
|
||||
original per-repository sketch implied.
|
||||
- The AI dashboard's "failed troubleshooting then escalated" figure (spec.md User Story 4) has
|
||||
no single stored flag anywhere in this codebase — `classifyStepOutcome`'s per-step verdicts are
|
||||
never persisted as their own durable record. Implemented as a documented proxy instead: an
|
||||
escalated session with `toolCallCount > 0` attempted troubleshooting before giving up, one with
|
||||
zero attempts escalated immediately. Documented directly in `ai.repository.ts`'s own code
|
||||
comment, the same "honest, documented simplification" precedent research.md §7 already set for
|
||||
the confidence-distribution bucketing.
|
||||
- Three of this module's public exports needed adding to their owning modules' top-level
|
||||
`index.ts` (not previously exposed): `decideConfidenceBand`/`ConfidenceBand` and
|
||||
`knowledgeReferenceRepository` from `ai-support/sessions`, matching the "extend an existing
|
||||
module's public surface for a later feature" precedent already used repeatedly this session
|
||||
(004's `productsRepository`, 009's `problemsRepository`).
|
||||
- Found a real regression during T028's full regression pass: `known-issues.test.ts` (004-
|
||||
product-knowledge, pre-existing) calls `findKnownIssuesByErrorCode` and its own `afterAll`
|
||||
deleted `ErrorCode` rows before this feature's new `ErrorCodeLookup` FK (RESTRICT) existed —
|
||||
once T004 started writing a lookup row on every call, that cleanup order started failing with
|
||||
an FK violation. Fixed by deleting `ErrorCodeLookup` rows first in that test's own `afterAll`.
|
||||
This feature's own new test files never delete `ErrorCode` rows at all, so they weren't
|
||||
affected the same way (leftover rows there are the same accepted throwaway-data tradeoff
|
||||
already established elsewhere this session).
|
||||
- Confirmed (not caused by this feature — the exact pre-existing issue 014-full-observability's
|
||||
own checklist already documented and root-caused via `git checkout` comparison) that this
|
||||
feature's own new integration test files, which also name their test products `TEST_*`,
|
||||
occasionally hit the same shared `deriveProductCode` "TEST" prefix collision under vitest's
|
||||
concurrent file execution when run alongside other `TEST_*`-prefixed files. Every dashboard
|
||||
test passes reliably run individually or in small groups; the intermittent 500 in a full
|
||||
combined run is the same known, out-of-scope, 003-ticketing concern.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Task list for 015-reporting-dashboards"
|
||||
description: 'Task list for 015-reporting-dashboards'
|
||||
---
|
||||
|
||||
# Tasks: Reporting and Analytics Dashboards
|
||||
@@ -23,32 +23,32 @@ All file paths are relative to `supporthub-api/` (repo root).
|
||||
|
||||
## Phase 1: Foundational (Blocking Prerequisites)
|
||||
|
||||
- [ ] T001 Add `REPORTING_DEFAULT_WINDOW_DAYS` (default `30`),
|
||||
- [x] T001 Add `REPORTING_DEFAULT_WINDOW_DAYS` (default `30`),
|
||||
`REPORTING_SLA_RISK_THRESHOLD_MINUTES` (default `60`), and `REPORTING_TOP_N_LIMIT`
|
||||
(default `10`) to `src/config/env.ts`, exposed via a new `reportingConfig` in
|
||||
`src/config/reporting.ts` (or added to an existing config file, matching this codebase's
|
||||
own per-feature config-file convention)
|
||||
- [ ] T002 Add the `ErrorCodeLookup` model to `prisma/schema.prisma` per data-model.md, generate
|
||||
- [x] T002 Add the `ErrorCodeLookup` model to `prisma/schema.prisma` per data-model.md, generate
|
||||
the migration via `prisma migrate diff --from-url <db-url> --to-schema-datamodel
|
||||
./prisma/schema.prisma --script`, hand-write it into
|
||||
./prisma/schema.prisma --script`, hand-write it into
|
||||
`prisma/migrations/<timestamp>_add_error_code_lookup/migration.sql`, apply via `prisma
|
||||
migrate deploy` against the throwaway test database (depends on T001 only in that both
|
||||
migrate deploy` against the throwaway test database (depends on T001 only in that both
|
||||
are Foundational — no code dependency)
|
||||
- [ ] T003 Add `ai-support/knowledge/repository/error-code-lookup.repository.ts` —
|
||||
- [x] T003 Add `ai-support/knowledge/repository/error-code-lookup.repository.ts` —
|
||||
`create(errorCodeId, productId)`, exported from the knowledge module's repository index
|
||||
(depends on T002)
|
||||
- [ ] T004 [P] Call the new repository's `create(...)` from
|
||||
- [x] T004 [P] Call the new repository's `create(...)` from
|
||||
`ai-support/knowledge/service/error-codes.service.ts`'s existing
|
||||
`findKnownIssuesByErrorCode`, alongside (not replacing) 014's own
|
||||
`knownErrorLookupsCounter.inc(...)` call at that same call site (depends on T003)
|
||||
- [ ] T005 [P] Add `platform/reports/mapper/date-range.ts` — parses/validates `from`/`to` query
|
||||
- [x] T005 [P] Add `platform/reports/mapper/date-range.ts` — parses/validates `from`/`to` query
|
||||
params, defaulting via T001's `reportingConfig.defaultWindowDays`, throwing
|
||||
`ValidationError` when `from > to` (depends on T001)
|
||||
- [ ] T006 [P] Add `platform/reports/mapper/rate.ts` — a shared `computeRate(numerator,
|
||||
denominator): number | null` and `computeAverageSeconds(durations: number[]): number |
|
||||
null` pair, both returning `null` (never `NaN`/`0`) when there's no qualifying data
|
||||
- [x] T006 [P] Add `platform/reports/mapper/rate.ts` — a shared `computeRate(numerator,
|
||||
denominator): number | null` and `computeAverageSeconds(durations: number[]): number |
|
||||
null` pair, both returning `null` (never `NaN`/`0`) when there's no qualifying data
|
||||
(research.md §3) — no dependency, pure functions
|
||||
- [ ] T007 Scaffold `platform/reports/schema/` (query-param zod schema using T005's date-range
|
||||
- [x] T007 Scaffold `platform/reports/schema/` (query-param zod schema using T005's date-range
|
||||
parsing), `platform/reports/controller/reports.controller.ts` (empty methods to be filled
|
||||
in per user story below), `platform/reports/routes/reports.routes.ts` registering all four
|
||||
routes behind `requireRole('ADMIN')`, and update `platform/reports/index.ts` to export the
|
||||
@@ -69,20 +69,20 @@ can now be built independently.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [ ] T008 [P] [US1] Unit tests for T006's `computeRate`/`computeAverageSeconds` (empty input ->
|
||||
- [x] T008 [P] [US1] Unit tests for T006's `computeRate`/`computeAverageSeconds` (empty input ->
|
||||
`null`; a real mix -> the correct value) in
|
||||
`tests/unit/platform/reports/rate-helpers.test.ts`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T009 [US1] Add `platform/reports/repository/management.repository.ts` — one method per
|
||||
- [x] T009 [US1] Add `platform/reports/repository/management.repository.ts` — one method per
|
||||
research.md §2's Management table row (ticket counts by status, SLA-run outcome counts,
|
||||
response/resolution duration row-fetches for T006 to average) (depends on T007)
|
||||
- [ ] T010 [US1] Add `ReportsService.getManagementDashboard(range)` composing T009's repository
|
||||
- [x] T010 [US1] Add `ReportsService.getManagementDashboard(range)` composing T009's repository
|
||||
calls into the `ManagementDashboard` shape, reusing the `Resolution.resolvedBy` convention
|
||||
(research.md §4) for the AI-vs-human split (depends on T009)
|
||||
- [ ] T011 [US1] Wire `GET /admin/reports/management` to the controller/service (depends on T010)
|
||||
- [ ] T012 [US1] Integration test covering Quickstart Scenario 1 (real tickets in various
|
||||
- [x] T011 [US1] Wire `GET /admin/reports/management` to the controller/service (depends on T010)
|
||||
- [x] T012 [US1] Integration test covering Quickstart Scenario 1 (real tickets in various
|
||||
terminal states, a met and a breached SLA run, verified figure-by-figure; a no-activity
|
||||
range returns all-zero counts and all-null rates) in
|
||||
`tests/integration/platform-reports/management-dashboard.test.ts` (depends on T011)
|
||||
@@ -100,14 +100,14 @@ can now be built independently.
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T013 [P] [US2] Add `platform/reports/repository/product.repository.ts` — ticket/problem
|
||||
- [x] T013 [P] [US2] Add `platform/reports/repository/product.repository.ts` — ticket/problem
|
||||
queries scoped by `productId`, plus a query against T003's `ErrorCodeLookup` table for
|
||||
the top-N ranking (`reportingConfig.topNLimit`) (depends on T007)
|
||||
- [ ] T014 [US2] Add `ReportsService.getProductDashboard(externalProductId, range)`, 404-ing via
|
||||
- [x] T014 [US2] Add `ReportsService.getProductDashboard(externalProductId, range)`, 404-ing via
|
||||
`NotFoundError` when the product doesn't resolve (FR-006) before running any aggregation
|
||||
query (depends on T013)
|
||||
- [ ] T015 [US2] Wire `GET /admin/reports/product/:externalProductId` (depends on T014)
|
||||
- [ ] T016 [US2] Integration test covering Quickstart Scenario 2 (two products' data never
|
||||
- [x] T015 [US2] Wire `GET /admin/reports/product/:externalProductId` (depends on T014)
|
||||
- [x] T016 [US2] Integration test covering Quickstart Scenario 2 (two products' data never
|
||||
cross-contaminating each other's figures; an unknown product 404s) in
|
||||
`tests/integration/platform-reports/product-dashboard.test.ts` (depends on T015)
|
||||
|
||||
@@ -123,13 +123,13 @@ can now be built independently.
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T017 [P] [US3] Add `platform/reports/repository/support.repository.ts` — current
|
||||
- [x] T017 [P] [US3] Add `platform/reports/repository/support.repository.ts` — current
|
||||
`Assignment` workload-by-agent query, `SLARun` at-risk/breached queries (`resolutionDueAt`
|
||||
within `reportingConfig.slaRiskThresholdMinutes` of now, per research.md §2) (depends on
|
||||
T007)
|
||||
- [ ] T018 [US3] Add `ReportsService.getSupportDashboard(range)` (depends on T017)
|
||||
- [ ] T019 [US3] Wire `GET /admin/reports/support` (depends on T018)
|
||||
- [ ] T020 [US3] Integration test covering Quickstart Scenario 3 (real per-agent assignment
|
||||
- [x] T018 [US3] Add `ReportsService.getSupportDashboard(range)` (depends on T017)
|
||||
- [x] T019 [US3] Wire `GET /admin/reports/support` (depends on T018)
|
||||
- [x] T020 [US3] Integration test covering Quickstart Scenario 3 (real per-agent assignment
|
||||
counts; a near-due-but-not-breached run counted as at-risk, distinct from breached) in
|
||||
`tests/integration/platform-reports/support-dashboard.test.ts` (depends on T019)
|
||||
|
||||
@@ -145,20 +145,20 @@ can now be built independently.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [ ] T021 [P] [US4] Unit test: the confidence-distribution bucketing reuses
|
||||
- [x] T021 [P] [US4] Unit test: the confidence-distribution bucketing reuses
|
||||
`decideConfidenceBand` (005-ai-support) against `aiConfig` defaults, not a reimplemented
|
||||
threshold check, in `tests/unit/platform/reports/confidence-distribution.test.ts`
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T022 [US4] Add `platform/reports/repository/ai.repository.ts` — `AISupportSession` outcome
|
||||
- [x] T022 [US4] Add `platform/reports/repository/ai.repository.ts` — `AISupportSession` outcome
|
||||
counts, `AIDiagnosis` confidence fetch, `AIKnowledgeReference` presence-per-session query,
|
||||
`AIAction`/`AIActionResult` outcome counts (depends on T007)
|
||||
- [ ] T023 [US4] Add `ReportsService.getAiDashboard(range)`, bucketing confidence via
|
||||
- [x] T023 [US4] Add `ReportsService.getAiDashboard(range)`, bucketing confidence via
|
||||
`decideConfidenceBand` + `aiConfig.defaultHighConfidence`/`defaultLowConfidence`
|
||||
(research.md §7) (depends on T022, T021)
|
||||
- [ ] T024 [US4] Wire `GET /admin/reports/ai` (depends on T023)
|
||||
- [ ] T025 [US4] Integration test covering Quickstart Scenario 4 (real AI sessions to mixed
|
||||
- [x] T024 [US4] Wire `GET /admin/reports/ai` (depends on T023)
|
||||
- [x] T025 [US4] Integration test covering Quickstart Scenario 4 (real AI sessions to mixed
|
||||
outcomes, mixed tool results, a spread of diagnosis confidence values) in
|
||||
`tests/integration/platform-reports/ai-dashboard.test.ts` (depends on T024)
|
||||
|
||||
@@ -169,10 +169,10 @@ together — this feature's full scope.
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
- [ ] T026 [P] Update `specs/015-reporting-dashboards/checklists/requirements.md` Notes with any
|
||||
- [x] T026 [P] Update `specs/015-reporting-dashboards/checklists/requirements.md` Notes with any
|
||||
implementation-time findings
|
||||
- [ ] T027 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
||||
- [ ] T028 Full regression: `npm run test:unit` then the full integration suite against real
|
||||
- [x] T027 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
||||
- [x] T028 Full regression: `npm run test:unit` then the full integration suite against real
|
||||
Docker-provisioned Postgres/Redis, confirming nothing outside this feature regressed
|
||||
(particularly `error-codes.service.ts`'s own existing tests, now touched by T004)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user