Skip to Content

← All archived runs

Run: admin-sla-config

run.md

Run: admin-sla-config

  • issue: #455 # canonical home of the spec + state (labels, comments)
  • branch: claude/admin-sla-config-mk07y8
  • pr: #456

00_intake/stub.md

Stub: Admin dashboard — SLA configuration

  • feature-slug: admin-sla-config
  • epic: admin-dashboard-data
  • personas: Admin
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: admin-data-foundation
  • sequence: 3 of 7

Problem

SLA exists only as notification logic (packages/services/src/notifications/sla/) — there is no stored, editable SLA configuration. The dashboard's SlaConfigCard and the /admin/sla table/page show dummy stage timers. To measure delivery and trigger escalations, the admin needs real, per-tenant, per-stage SLA definitions. This is the one section the user agreed warrants a dedicated model (rather than tenant-settings) because its shape is structured.

Proposed change

  • Add a dedicated per-tenant SLA config model: one row per service stage (Request, BRD, Bidding, Delivery, Billing, CSAT) with owner role, SLA type, target/warning/breach (business days), impact, and escalation-enabled flag.
  • Wire the SlaConfigCard summary (stages covered / total, default-timers-set, overall status: Complete | Partial | Missing) to the real data.
  • Wire the /admin/sla page table to the real stage rows.
  • Build the config UI: admin can define/edit each stage's timers, owner, impact, and escalation toggle.

Acceptance criteria (rough)

  • An admin can create/edit SLA definitions per stage and they persist per tenant.
  • SlaConfigCard shows real stages-covered/total and a computed Complete/Partial/Missing status.
  • /admin/sla table renders the tenant's real SLA rows (no empty mock array).
  • Stage list aligns with the service-journey steps so the same stage vocabulary is reused.

Out of scope (this feature)

  • Actually enforcing SLAs / firing escalations from these timers (notification wiring stays as-is).
  • The escalation-rules card (admin-escalation-config) — separate concern.

Notes for Define

  • The stage vocabulary should match service-journey + the service-model card so admin-readiness-gaps can derive slaConfigured. Decide whether default timers live as a tenant-setting or on the model.
  • touches: packages/services/src/db/models (new SLA model), apps/web/app/(app)/admin/sla/page.tsx, apps/web/components/admin/dashboard/sla-config-card.tsx.

01_define/output/spec.md

Spec: Admin dashboard — SLA configuration

  • slug: admin-sla-config
  • issue: #455
  • personas: Admin
  • touches: packages/services/src/db/models/sla-definition.ts (new), packages/services/src/db/models/index.ts, packages/services/src/db/services/sla (new), apps/web/lib/admin-dashboard-data.ts, apps/web/app/(app)/admin/dashboard/page.tsx, apps/web/app/(app)/admin/sla/page.tsx (new), apps/web/components/admin/dashboard/sla-config-card.tsx, apps/web/lib/mock/admin.ts
  • complexity: standard

Problem

SLA exists only as notification logic (packages/services/src/notifications/sla/ — a single notifySlaBreach helper) with no stored, editable SLA configuration. On the admin "Platform setup" dashboard the SlaConfigCard and the (not-yet-built) /admin/sla page read dummy values from apps/web/lib/mock/admin.ts (slaConfig: { status, stagesCovered, totalStages, defaultTimersSet, stages: [] }), so every tenant sees the same fictional state and an admin cannot actually define delivery timers. This is feature 3 of 7 in the admin-dashboard-data epic, advancing Build the Bridge → Q2 2026 Objective 3 (Validate Technical Infrastructure & Payout Flow) by making each dashboard section operational from real, per-tenant data. Per the epic's storage decision, SLA is the one section that warrants a dedicated model (rather than the shared TenantSetting key/value store its siblings use) because its shape is structured — one configurable definition per service stage. The data foundation (admin-data-foundation, #438) already established the per-tenant server loader loadAdminDashboardData(); this feature plugs its real SLA datapoint into that seam.

Proposed change

  • New per-tenant SLA model at packages/services/src/db/models/sla-definition.ts, one row per service stage, exported from db/models/index.ts and using the established model conventions (schemaPlugin, softDeletePlugin, tenantPlugin; collection sla_definitions; unique index on { tenantId, stage }). Fields per row:
    • stage — one of the canonical service stages: Request | BRD | Bidding | Delivery | Billing | CSAT (the same vocabulary the service-model card and business/service-journey use, so admin-readiness-gaps can later derive slaConfigured from it).
    • ownerRole — the persona that owns the stage (admin | csm | sdm | expert | vendor | customer).
    • slaTypeResponse | Resolution.
    • targetDays, warningDays, breachDays — SLA thresholds in business days (non-negative integers; expected ordering target ≤ warning ≤ breach, validated on save).
    • impactLow | Medium | High | Critical.
    • escalationEnabled — boolean (records intent only; no escalation is fired this round).
  • Timers live per-stage on the row (the stub's deferred decision, resolved): there is no separate tenant-wide default timer set. A stage is "covered" when its row exists with timers saved.
  • New SLA service under packages/services/src/db/services/sla/ (mirroring the tenant-setting service shape: index.ts + instance.ts) exposing:
    • getSlaDefinitions(tenantId) → the tenant's saved stage rows (empty array when none).
    • upsertSlaDefinition(tenantId, stage, { ownerRole, slaType, targetDays, warningDays, breachDays, impact, escalationEnabled }) → create/update one stage row.
    • deleteSlaDefinition(tenantId, stage) → remove (soft-delete) a stage's definition.
    • getSlaConfigSummary(tenantId) → the card slice { status, stagesCovered, totalStages, defaultTimersSet } (see status rules below).
  • Status is derived from coverage (the stub's other deferred decision, resolved as "start empty, status by coverage"). totalStages = 6. stagesCovered = number of the 6 stages with a saved definition. status = Missing when 0 covered, Partial when 1–5, Complete when all 6. defaultTimersSet = whether the tenant has saved at least one SLA row (stagesCovered > 0). A brand-new tenant therefore reads Missing / no timers, never a mock value — consistent with how the sibling escalation card derives its empty state.
  • Wire the dashboard loader. loadAdminDashboardData() calls getSlaConfigSummary(tenantId) and returns the real slaConfig slice; app/(app)/admin/dashboard/page.tsx passes it into SlaConfigCard instead of d.slaConfig from the mock. The SlaConfigCard component already takes exactly these props (status, stagesCovered, totalStages, defaultTimersSet) and needs no shape change.
  • Build the /admin/sla page (new, server component scoped to the authenticated admin's tenant via the established resolveActionContext({ allowedRoles: ["admin"] }) pattern, with the same graceful no-access fallback the other admin pages use):
    • A table of the 6 service stages showing each stage's owner role, SLA type, target/warning/ breach (business days), impact, and escalation toggle — rendering the tenant's real rows; stages without a saved definition appear as an empty/"not configured" row, not absent.
    • A config form (client form backed by a server action, following the /admin/escalation and /admin/settings actions precedent) so the admin can define or edit each stage's timers, owner, SLA type, impact, and escalation flag, and save per tenant. Saving revalidatePath("/admin") so the dashboard card reflects the new coverage.
    • All copy in sentence case.
  • Drop the SLA mock dependency. Remove the slaConfig block from lib/mock/admin.ts (and any now unused fields) so nothing reads dummy SLA state.

Acceptance criteria

  • An admin can create/edit an SLA definition for each of the 6 service stages on /admin/sla (owner role, SLA type, target/warning/breach business days, impact, escalation toggle), and the values persist per tenant in the new sla_definitions collection.
  • The new SLA model enforces one definition per { tenantId, stage } and validates timer values as non-negative integers ordered target ≤ warning ≤ breach.
  • getSlaDefinitions, upsertSlaDefinition, deleteSlaDefinition, and getSlaConfigSummary exist on the new SLA service; getSlaDefinitions returns an empty array when nothing is stored.
  • The SlaConfigCard on the admin dashboard shows real stagesCovered / totalStages and a derived status: Missing at 0 covered, Partial at 1–5, Complete at all 6 — fed from the live data, not the mock.
  • defaultTimersSet reflects real state (true once at least one stage is saved); a tenant that has never configured SLA renders Missing with no timers set.
  • The /admin/sla table renders the tenant's real stage rows (no empty mock array); saving a stage updates the dashboard card after revalidation.
  • The stage list is exactly Request, BRD, Bidding, Delivery, Billing, CSAT, matching the service-model card and business/service-journey, so the same vocabulary is reusable by admin-readiness-gaps.
  • The slaConfig mock block is removed from lib/mock/admin.ts and no code reads it.

Out of scope

  • Enforcing SLAs / firing escalations from these timers — the notifications/sla wiring stays as-is; escalationEnabled records intent only.
  • The escalation-rules card and its config (admin-escalation-config) — separate concern.
  • The derived slaConfigured readiness tile and config-gap computation (admin-readiness-gaps).
  • A tenant-wide default timer template or per-stage override layering — timers are per-stage only.
  • Editing the canonical stage list itself (the 6 stages are fixed to the service-journey vocabulary).

Open questions

  • none

02_build/output/notes.md

Build notes: admin-sla-config

  • branch: claude/admin-sla-config-mk07y8
  • commits: feat: admin-sla-config — SLA model, service, /admin/sla page + dashboard wiring

What changed

  • packages/services/src/db/models/sla-definition.ts (new): per-tenant SLA model, one row per service stage. Canonical stage/owner/type/impact vocabularies are exported as const arrays (SLA_STAGES, SLA_OWNER_ROLES, SLA_TYPES, SLA_IMPACTS) so the UI and validation reuse one source. Collection sla_definitions; unique partial index on { tenantId, stage } (non-deleted only, so a stage can be reconfigured after a soft delete); per-field non-negative-integer validation on the day timers; schemaPlugin + softDeletePlugin + tenantPlugin.
  • packages/services/src/db/services/sla/{index,instance}.ts (new): SlaConfigService mirroring the tenant-setting service shape. getSlaDefinitions (canonical-order, [] when none), upsertSlaDefinition (ordering check target ≤ warning ≤ breach, upsert on { tenantId, stage }), deleteSlaDefinition (soft delete via archive), and getSlaConfigSummary (coverage-derived { status, stagesCovered, totalStages, defaultTimersSet }).
  • packages/services/src/db/models/index.ts + services/index.ts: export the new model + service (flows through @sustentus/services/server).
  • apps/web/lib/admin-dashboard-data.ts: loader now calls getSlaConfigSummary(tenantId) and returns a real sla slice on AdminDashboardData.
  • apps/web/app/(app)/admin/dashboard/page.tsx: SlaConfigCard is fed the real sla slice instead of d.slaConfig.
  • apps/web/app/(app)/admin/sla/ (new): server page.tsx (admin-scoped via resolveActionContext, graceful no-access fallback), actions.ts (saveSlaDefinition / removeSlaDefinition server actions with zod validation + revalidatePath), and _components/sla-config-manager.tsx (client table of all 6 stages + per-stage editor). Server-only union types reach the client via import type; option lists are passed as props so no server runtime leaks into the client bundle.
  • apps/web/lib/mock/admin.ts: removed the slaConfig block (no apps/web code reads it anymore).

Acceptance criteria status

  • Admin can create/edit an SLA definition per stage on /admin/sla and it persists per tenant — /admin/sla editor → saveSlaDefinitionupsertSlaDefinition into sla_definitions.
  • Model enforces one definition per { tenantId, stage } and validates non-negative integers ordered target ≤ warning ≤ breach — unique partial index + per-field validators + ordering check in the service and the action's zod refine.
  • getSlaDefinitions / upsertSlaDefinition / deleteSlaDefinition / getSlaConfigSummary exist; getSlaDefinitions returns [] when nothing is stored.
  • SlaConfigCard shows real stagesCovered / totalStages + derived status (Missing 0, Partial 1–5, Complete 6) — from the loader's sla slice.
  • defaultTimersSet reflects real state (true once ≥1 stage saved); never-configured tenant reads Missing / no timers.
  • /admin/sla table renders real rows; saving revalidates /admin/dashboard + /admin so the card updates.
  • Stage list is exactly Request, BRD, Bidding, Delivery, Billing, CSAT (SLA_STAGES).
  • slaConfig mock block removed from apps/web/lib/mock/admin.ts; nothing in apps/web reads it.

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Ship reads them back from gh pr checks.

Notes for review

  • apps/dashboards/ keeps its own standalone slaConfig mock + SLA page — that app is self-contained and out of this feature's scope (the spec's touches: is apps/web only).
  • Timer ordering is enforced in two places (service + action zod refine) by design; the model keeps per-field non-negative-integer validation. Cross-field ordering isn't a Mongoose path validator because the upsert goes through findOneAndUpdate.
  • The /admin/sla editor selects a stage (table row click or the stage dropdown) and edits it in place; Remove soft-deletes a configured stage. Coverage on the dashboard card updates on save.

03_ship/output/changelog.md

Changelog: admin-sla-config

Persona: admin

Configure delivery SLAs for your organisation

Admins can now set up service-level agreements from the platform setup dashboard:

  • Define the owner, target, warning, and breach timers (in business days), impact, and escalation toggle for each service stage — Request, BRD, Bidding, Delivery, Billing, and CSAT — from the new SLA configuration page, and your definitions are saved per organisation.
  • The SLA card on your setup dashboard now shows your real coverage — complete, partial, or missing — based on how many stages you have configured, instead of sample content.

This is part of making your platform setup dashboard reflect your organisation's real configuration.

03_ship/output/investor-update.md

Admins can now configure delivery SLAs for their organisation

We've made service-level agreements configurable per organisation — admins can define the delivery timers for each stage of the service journey, and the platform setup dashboard now reflects real SLA coverage instead of placeholder content. It continues turning the admin control surface into a working control panel, and puts the delivery commitments that govern how we measure service under explicit, per-organisation control.

  • Admins can set the owner, target, warning, and breach timers (in business days), impact, and escalation flag for each of the six service stages — Request, BRD, Bidding, Delivery, Billing, and CSAT — and the definitions persist per organisation.
  • The SLA card on the setup dashboard now shows real coverage — complete, partial, or missing — derived from how many stages are configured, rather than sample content.
  • The stage vocabulary matches our existing service model, so later delivery-measurement and escalation work can read these definitions without new infrastructure.

This advances the Build the Bridge initiative and our Q2 2026 objective to validate technical infrastructure & payout flow — establishing the per-organisation SLA definitions that delivery measurement and escalation will build on.

03_ship/output/pr.md

Ship: admin-sla-config

  • PR: #456 — https://github.com/sustentus/sustentus/pull/456
  • branch: claude/admin-sla-config-mk07y8
  • CI: format pass · lint pass · typecheck pass · pr-issue-link pass · vercel preview pass (re-confirm on final commit)
  • technical docs: no technical docs impact — no apps/docs/technical page enumerates admin routes, DB models, or service methods; a new model + service + admin route under existing packages doesn't change documented architecture (mirrors the escalation/commercial/people siblings' determination).
  • business docs: no business docs impact — admin platform-setup configuration is operational config capture, not a service-journey step or feature-role-matrix entity; platform-overview already describes per-tenant SLA timers generically, and this feature realises part of that without changing what the page describes.
  • release notes: both — investor draft (03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx.

Review summary

/code-review (high) over the diff vs origin/main — 6 candidates, 2 fixed, 4 accepted:

  • Fixed — SLA model unique partial index used $or + $exists:false (copied from the csat precedent), which is ambiguous across MongoDB versions. Simplified to partialFilterExpression: { isDeleted: false } — unambiguously valid and, since the soft-delete plugin defaults isDeleted:false on every insert, it covers every live row in this new collection.
  • Fixed/admin/sla editor coerced blank timer inputs to 0 via Number(""), silently saving a 0-day SLA and marking the stage covered. Added a non-empty guard in handleSave (explicit 0 still allowed).
  • Accepted — getSlaConfigSummary loads + sorts + de-dupes documents to produce a count; a countDocuments would be cheaper, but it's ≤6 docs once per dashboard render and countDocuments bypasses the soft-delete pre(/^find/) hook, so the current form is safer. Not worth the risk.
  • Accepted — timer-ordering check exists in both the action (for the fieldErrors mapping) and the service (backstop); minor duplication, deliberate.
  • Accepted — isDuplicateKeyError / Archiveable*Model mirror the established sibling-service pattern (9 services); a shared helper is a repo-wide refactor, out of scope here.
  • Accepted — edit-form state isn't re-synced from props after revalidation; benign (the form matches what the user typed; the table reads live props).

Acceptance check (vs spec)

  • Admin can create/edit an SLA definition per stage on /admin/sla (owner, type, target/warning/breach business days, impact, escalation) and it persists per tenant in sla_definitions — page + form + saveSlaDefinitionupsertSlaDefinition.
  • Model enforces one definition per { tenantId, stage } and validates non-negative-integer timers ordered target ≤ warning ≤ breach — unique partial index + per-field validators + service ordering check + action check.
  • getSlaDefinitions / upsertSlaDefinition / deleteSlaDefinition / getSlaConfigSummary exist; getSlaDefinitions returns [] when none stored.
  • SlaConfigCard shows real stagesCovered / totalStages + derived status (Missing 0, Partial 1–5, Complete 6).
  • defaultTimersSet reflects real state; never-configured tenant reads Missing / no timers.
  • /admin/sla table renders real rows; saving revalidates /admin/dashboard + /admin.
  • Stage list is exactly Request, BRD, Bidding, Delivery, Billing, CSAT (SLA_STAGES).
  • slaConfig mock block removed from apps/web/lib/mock/admin.ts; nothing in apps/web reads it.
  • Post-build fix: /admin/sla registered in route-policies.ts so the deny-by-default proxy no longer bounces it to /admin.

Merge & deploy

  • merged: <pending explicit approval>
  • deploy: <pending>