customer-action-itemsrun.md00_intake/stub.mdThe "your actions" items in the ProjectStatusStrip (label, urgent, dueInDays, urgency:
urgent | upcoming — e.g. "Approve integration access", "Confirm data mapping") are dummy. There is
no action-items model. The generic notification model is title/body/link/read only — it has no
due date, urgency, or completion semantics, and it is not project-scoped. These items represent work
the customer must do to keep delivery moving, and some gate go-live.
label, dueDate (→ derived dueInDays),
urgency (urgent | upcoming), a completed state, and an optional blocksGoLive flag.project-health-engine (sequenced before this).touches: packages/services/src/db/models (new action-item model),
packages/services/src/server,
apps/web/components/dashboard/customer/project-status-strip.tsx.01_define/output/spec.mdThe "your actions" list in the customer dashboard's ProjectStatusStrip (label, urgent,
dueInDays, urgency — e.g. "Approve integration access", "Confirm data mapping") is dummy data from
apps/web/lib/mock/customer.ts. There is no action-items model anywhere in the platform: the
generic notification model is title/body/link/read only — it has no due date, urgency, completion
semantics, or project scope. These items represent real work the customer must do to keep delivery
moving, and some gate go-live. This is sequence 7 of 9 in the customer-dashboard-data epic; with the
customer-project-foundation (sequence 1) read in place, this feature turns a mock surface into real,
queryable per-project data, advancing Build the Bridge → Q2 2026 Objective 3 (Validate technical
infrastructure & payout flow).
ActionItem collection (separate, indexed — not an
embedded sub-doc — because open items are queried per project frequently and written by multiple
personas, mirroring the shipped Blocker model). Each record references a project (lead) and
carries label, dueDate, urgency (urgent | upcoming), a status of open | completed
(with completed metadata: completedBy, completedAt), and a blocksGoLive flag. dueInDays is
derived at read time from dueDate (not stored). Indexed on { tenantId, lead, status } so
"open items for this project" is a cheap query.actionItemService in packages/services/src/db/services/action-item/
(the index.ts class + instance.ts singleton pattern), exported via @sustentus/services/server,
with: raise(leadId, { label, dueDate, urgency, blocksGoLive }), complete(actionItemId, byUserId)
(sets status = completed + records who/when), and listOpenByLead(leadId) — returns open items
with derived dueInDays, sorted by urgency then due date. listOpenByLead (and the blocksGoLive
flag it surfaces) is the read a future project-health-engine consumes.apps/web/app/(app)/projects/[id]), scoped to that leadId and available to the delivery team
(Expert/CSM/SDM): list the project's open items, raise a new one (label + due date + urgency +
optional blocksGoLive), and mark one complete. Backed by "use server" actions following the
existing (app)/.../actions.ts pattern (zod-validated, resolveActionContext / runActionBody).customerProjectService envelope (from
customer-project-foundation) with actions[] = the open items for the signed-in customer's active
project, and wire the ProjectStatusStrip "Your actions" list to them (replacing the mock). The
customer can complete / acknowledge an item from the dashboard via a "use server" action scoped
to their own active project; completing removes it from the list and records who/when. The actions
block is removed from apps/web/lib/mock/customer.ts.ActionItem model exists, tenant-scoped, attached to a project (lead), with label,
dueDate, urgency (urgent | upcoming), status (open | completed) + completed
metadata, and blocksGoLive, indexed on { tenantId, lead, status }.actionItemService exposes raise, complete, and listOpenByLead (open items with derived
dueInDays, sorted by urgency then due date), exported from @sustentus/services/server.listOpenByLead, and records who completed it and when.lead.customer); another customer's or tenant's items never appear.actions block is removed from apps/web/lib/mock/customer.ts; the envelope's actions[]
drives the strip and the remaining mock sections still render unchanged.project-health-engine — that engine is not yet specced/built;
this feature only supplies the queryable read (listOpenByLead + blocksGoLive) it will consume.02_build/output/notes.mdpackages/services/src/db/models/action-item.ts (new): tenant-scoped ActionItem model — lead,
label, dueDate, urgency (urgent|upcoming), status (open|completed) + completedAt/
completedBy, blocksGoLive, createdBy. Indexed { tenantId, lead, status }. Mirrors the shipped
Blocker model (same plugins: schema/softDelete/tenant). Exported via db/models/index.ts.packages/services/src/db/services/action-item/ (new): ActionItemService + actionItemService
singleton with raise, complete (optional leadId scope), and listOpenByLead (returns
serialized OpenActionItem[] with derived dueInDays, sorted urgency-then-due). Exported via
db/services/index.ts → flows through @sustentus/services/server.packages/services/src/db/services/customer-project/index.ts: extended the envelope with
actions: OpenActionItem[] (resolved from actionItemService.listOpenByLead for the active project)
and added getActiveProjectLeadId so the customer completion path can scope to the customer's own
project.apps/web/app/(app)/projects/[id]/actions.ts: added raiseActionItemAction +
completeActionItemAction (delivery-roles only), mirroring the blocker actions.apps/web/components/service-leads/lead-action-items/ (new): server LeadActionItems +
client ActionItemsPanel (raise form: label, due date, urgency, gates-go-live; complete per item) +
loading. Wired into projects/[id]/page.tsx alongside LeadBlockers, behind the delivery-role gate.apps/web/app/(app)/customer/dashboard/actions.ts (new): completeCustomerActionItemAction
(customer-only), scoped to the customer's active-project lead so another project's/tenant's item is
never completable.apps/web/components/dashboard/customer/project-status-strip.tsx: Action type now matches the
envelope (id, label, urgency, dueInDays, blocksGoLive); the "Your actions" card renders the
real items and lets the customer mark each done (server action + router.refresh). Empty state copy
is now "Nothing needs your attention right now".apps/web/app/(app)/customer/dashboard/page.tsx: passes project.actions + the completion action to
the strip.apps/web/lib/mock/customer.ts: removed the actions block; the rest of the mock is unchanged.ActionItem model — tenant-scoped, lead-attached, all fields + index present.actionItemService exposes raise/complete/listOpenByLead (derived dueInDays, sorted),
exported from @sustentus/services/server.listOpenByLead, and
records completedBy/completedAt.leadId-scoped.actions removed from the mock; the envelope's actions[] drives the strip; remaining mock
sections render unchanged.leadId (resolved from the signed-in customer's
active project) on top of the tenant scope — the team path relies on the delivery-role + tenant gate
like the blocker flow does.dueInDays is derived at read time (not stored), so it stays correct without a write; overdue items
render "Overdue by N days".project-health-engine is not built yet — this only supplies the queryable read (listOpenByLead
surfaces blocksGoLive); wiring into health is that feature's job (per the spec's Out of scope).03_release/output/changelog.mdLive entry: apps/help/app/changelog/2026-06-22-customer-action-items/page.mdx
Personas: customer, expert, csm, sdm
The "Your actions" strip on your dashboard now shows the real things your delivery team needs from you, so nothing that holds up go-live slips through the cracks:
03_release/output/investor-update.mdWho it's for: Customers, plus the delivery team (experts, CSMs, SDMs) What shipped: The delivery team raises action items on a project — with due dates and urgency — and the customer sees and completes them on their dashboard. Why it matters: Fewer projects stall on missed customer steps, advancing Build the bridge → Q2 objective 3, validate technical infrastructure & payout flow.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mddb/ already covers the new model + service)business/feature-role-matrix/projects (action-item rows), business/service-journey/delivery (customer action items section)/code-review (high effort) on the origin/main...HEAD diff. Backend (model/service/actions) came back clean — tenant + lead scoping verified (customer completion is leadId-scoped to their own active project; cross-customer/cross-tenant completion blocked), exports wire through @sustentus/services/server, sort/await/ObjectId guards all correct. Frontend clean — server action correctly passed as a prop to the client strip, no dangling mock actions reference, envelope/strip types align, dashboards app unaffected.
daysUntil derived dueInDays from the current instant with Math.ceil, giving an off-by-one ("due today" misrender, and a day's drift west of UTC) — fixed on branch: anchor both ends to start-of-UTC-day and round. (packages/services/src/db/services/action-item/index.ts)<input type="date">; matches the milestone/blocker date handling; out of scope for this round.ActionItem model — tenant-scoped, lead-attached, all fields + { tenantId, lead, status } index — verified in action-item.ts.actionItemService raise/complete/listOpenByLead (derived dueInDays, sorted urgency-then-due), exported from @sustentus/services/server — verified through db/index.ts barrel.LeadActionItems + raiseActionItemAction, delivery-role gated.actions[] → strip; dueInDays fix applied.complete sets status/completedAt/completedBy; listOpenByLead filters status: open.leadId-scoped.actions removed; envelope drives the strip; remaining mock sections unchanged.