Worker portal redesign — composability runtime + design-system extraction

On this page
NOTE

This plan tracks #460 under group epic &51 (19 active child issues, total weight 64; 2 deferred). Originated from a May 2026 design exploration; all load-bearing design context has been incorporated into this plan and the ratified ADR-021 + ADR-022. This file is the canonical source-of-truth for the redesign; older handoff artifacts are non-normative.

NOTE

2026-05-20 update: Stage 2 closes at 2 of 3 ADRs ratified. ADR-021 and ADR-022 shipped. The third (originally "ADR-023: promote-PR mechanism") was deferred during drafting — user feedback reframed the underlying question from "how does composition open PRs against canopy" to the broader "what is the unified config-backend abstraction across all canopy config domains (service config, secrets, jurisdiction policy, rulesets, theme, IDP, composition)". Filed separately as #507. Composition v1 ships using the existing filesystem rulesets/{juris}/ pattern; Studio promote affordance descoped from v1 (admins use their existing baseline-edit workflow external to canopy). When #507 ratifies the unified backend + adds a write-capable backend, the Studio promote affordance lands as a follow-up. #488 + #492 closed-deferred. #500 weight 5 → 3.

Status

Step Description Status

1

Design system extraction (1 MR). Extract 8 Askama macro primitives — panel_frame, overline, gold_rule, big_number, hero_strip, leaf_glyph, status_pill, money_cell — into a single services/canopy-web/templates/_primitives/orchard.html file. Reuses existing .skeleton + @keyframes pulse + .u-empty-state; adds one new .u-error-block utility. Stage 1.5 (#505) upgrades the four panel-state surfaces to first-class Askama-macro primitives. All primitives consume the --orchard-* token surface shipped via !295. Outcome: every existing canopy-web template can be rewritten to use these primitives without routing or business-logic changes. Low risk, high leverage. Independent of composability work. Stage-1 implementation plan: worker-portal-redesign-stage1-design-system.adoc.

Done (2026-05-21) — !349

1.5

Panel-state primitives upgrade (1 MR, follow-up to Stage 1, non-blocking). Upgrade the four CSS-only panel-state utility classes from Stage 1 to first-class Askama-macro primitives with proper props: empty_state (title + body + optional CTA), skeleton (discrete-enum height + width; emits the existing .skeleton class), skeleton_row (composed for table-shaped panels; columns enum 2-6), error_block (title + body + last-known timestamp + retry/status buttons). Macros wrap the existing classes (.u-empty-state, .skeleton, .u-error-block) — no class renames or deletions, continuing Stage 1 Decision 4. 19 of 20 in-tree .u-empty-state consumers migrated. Validating surface is cases/search.html + cases/_results.html + new cases/_results_error.html — handler returns the error fragment on Err(_) from canopy-persons; server-side branching with HTTP 200 (no htmx-response-targets extension dependency). 4 Playwright tests in tests/e2e/specs/panel-states.spec.ts + 18 new primitives_test wrappers. Stage 1.5 implementation plan.

Done (2026-05-21)

2

ADR ratification (2 MRs shipped; 3rd deferred). ADR-021 (composability runtime + plugin model — ratified 2026-05-20). ADR-022 (storage layering — ratified 2026-05-20). Originally ADR-023 (promote-PR mechanism) — deferred 2026-05-20 in favor of #507 (unified config backend ADR across canopy).

Done (2 of 3; ADR-023 deferred to #507)

3

Composability runtime + storage layering (2 MRs; 1 deferred). MR1 lands the forward-only migration creating composition_documents + composition_documents_archive per ADR-022 + the composition loader runtime (5-layer merge: system defaults → baseline TOML via RFC 7396 → DB layers via RFC 6902 in jurisdiction_live → role → user order) + PluginSource trait + CompileTimePluginSource + #[canopy_plugin] proc-macro + Plugin.toml schema + roles-only idp.toml schema + Georgia jurisdiction fixtures (shell-only since real plugin handlers land Stage 5). MR2 adds the live-override HTTP APIs (11 endpoints: GET/PUT/PATCH on live/role/user-me + DELETE live + POST live/archive) with atomic audit emission via publisher.publish_tx (Decision 9 — outbox row commits with the composition row), RFC 7232/6585-clean precondition handling, surgical cache invalidation, JSON-only OpenAPI spec (no Swagger UI in v1 due to strict CSP), and a JSON-aware session extractor pair that shares the HTML BFF’s refresh-token + fail-closed semantics. The promote-live-to-baseline endpoint (#492) was deferred to #507 when ADR-023 was reframed as a canopy-wide config-backend ADR. Stage-3 MR1 implementation plan: worker-portal-redesign-stage3-composition-runtime.adoc. Stage-3 MR2 implementation plan: worker-portal-redesign-stage3-mr2-live-override-apis.adoc.

Done (2026-05-22 — MR1 + MR2 both merged)

4

Identity rework (2 MRs). Generic IDP loader from rulesets/{juris}/idp.toml (email-discovery routing + OIDC/SAML 2.0 SSO initiation + local-accounts toggle). IDP-aware sign-in template carrying multiple IDP chips with chip_color + chip_icon per IDP entry; zero-IDP graceful state points jurisdiction admins at Studio → Identity.

Done (2026-05-22 — MR1 !353 + MR2 !354 both merged)

5

Core surfaces (4-5 MRs, one per surface). Worker dashboard (12-panel kit composition-driven). Supervisor dashboard. Analyst dashboard. Case detail with 3 shell strategies (scroll / card-grid / tabs) and 13 section types. Customize-my-dashboard (worker-level deltas). MR1 (495) lands the worker dashboard via the Stage 3 composition runtime: 12 panel plugins registered through [canopy_plugin] + linkme; each panel is its own #[derive(Template)] struct rendered to a String in Rust and embedded in worker.html via {{ panel.html|safe }}; Georgia baseline declares all 12 with row/span per ADR-021 breakpoints. 6 panels wire existing endpoints; 6 ship state = "empty" placeholders pending FU-1..FU-6 (#519-#524). MR2 (#496) adds supervisor + analyst dashboard surfaces via surface_for_role dispatch, 8 new panel plugins (5 supervisor + 5 of them with real upstream wiring: team-queue via /v1/applications?status=submitted, sanctions rollup via new /v1/tanf/sanctions/rollup, overpayment summary via new /v1/reporting/overpayments/summary, pending hearings client-filters /v1/appeals/queue), renames WorkerRole::QualityControl → ::Analyst, plumbs worker_role_slug through 11 templates, extracts shared _panel_grid.html macro, adds /team-queue full-page route. 9 of 14 FUs landed in-MR; 5 stay deferred with scope-corrected comments. MR3 (#498, ADR-024) ships Customize My Dashboard. MR4 (#497) ships case-detail composition over three MRs: MR4a wired the composition pipeline with a compat shim; MR4b shipped the scroll shell + 20 section plugins (13 design SECTION_REGISTRY + 7 issue-only stubs tracked at #562); MR4c shipped the card-grid shell + 27-action shell-aware redirect (target_section + safe_focus_section allowlist) and moved Georgia analyst to card_grid. MR-1 plan: worker-portal-redesign-stage5-worker-dashboard.adoc. MR-2 plan: worker-portal-redesign-stage5-supervisor-analyst-dashboards.adoc. MR-3 (#498) plan: worker-portal-redesign-stage5-customize-my-dashboard.adoc. MR-4 (#497) plan archived at plans/archive/worker-portal-redesign-stage5-case-detail.adoc.

Done (2026-05-24 — MR1 !355 + MR2 !356 + MR2.1 !357 + MR3 !359 + MR4a !363 + MR4b !364 + MR4c TBD all merged)

6

Maintainer surfaces (3 MRs). Jurisdiction Studio: onboarding wizard. Studio: live-mode composer (promote modal descoped from v1 pending #507). Studio: plugin developer view (Plugin Studio).

In progress

7

Polish (2-3 MRs). ⌘K command palette. Audit log (system + case scopes sharing one primitive). First-impression states (splash, maintenance, expired, 404).

In progress

Tracking issue: #460
Group epic: &51 (19 active child issues, total weight 64; 2 deferred)
Branch root: feat/worker-portal-redesign-{stage}-{surface} (one per MR)
Plan repo location: docs/modules/ROOT/pages/plans/worker-portal-redesign.adoc (this file, canonical)

Child issues

Stage Issue Title Weight

1

#485

Design system extraction (Askama partials + CSS utility classes)

3

1.5

#505

Upgrade panel-state utility classes to 4 Askama-macro primitives

3

2

#486

ADR-021 — composability runtime + plugin model (ratified)

2

2

#487

ADR-022 — composition override storage layering (ratified)

2

2

~#488~

~~ADR — promote-PR mechanism~~ deferred to #507

3

#489

DB migrations for composition override layers

3

3

#490

Composition loader (TOML parse + role filter + override merge)

5

3

#491

Live override APIs (read/write/archive endpoints)

3

3

~#492~

~~Promote-live-to-baseline (git-host PR generation)~~ deferred to #507

4

#493

Generic IDP loader from idp.toml

3

4

#494

IDP-aware sign-in template

2

5

#495

Worker dashboard (12-panel kit, composition-driven)

5

5

#496

Supervisor + analyst dashboards (role overrides)

3

5

#497

Case detail (3 shell strategies + 13 section types)

8

5

#498

Customize my dashboard (per-worker deltas)

3

6

#499

Studio onboarding wizard

3

6

#500

Studio live-mode composer (promote modal descoped pending #507)

3

6

#501

Plugin Studio (developer view)

5

7

#502

⌘K command palette

3

7

#503

Audit log unified surface (system + case scopes)

3

7

#504

First-impression states (splash / maintenance / expired / 404)

2

Context

What we have today

services/canopy-web/ ships an Askama + htmx + Alpine.js (CSP build) worker portal with:

  • Orchard design system tokens (light/dark/system theme via --orchard-* CSS custom properties)

  • 8 pages (dashboard, case search, case detail with 6 tabs, application process, renewal queue, applications list, notices list, appeals list, 404)

  • 30 caseworker action handlers across SNAP/TANF/Medicaid/CAPS/WIC (per #392, #393, #394 / MRs already shipped)

  • Program-specific action handlers with cross-program views and per-program upstream endpoints (ADR-019 service-class JWT)

  • JWS-signed determinations rendered via canopy_signing::SignableDetermination (ADR-002)

  • FTI auditor role + advisory naming (per the most-recent FTI work)

Already shipped (pre-epic, foundation for this work):

  • ✅ Palette revision (orchard tokens) — !295

  • --orchard-info CSS token

  • ✅ Light / dark / system theme support

  • ✅ Per-jurisdiction theme configuration (src/theme.rs + rulesets/{jurisdiction}/theme.toml)

Intended outcome

The worker portal becomes composition-driven: jurisdictions edit TOML in rulesets/{jurisdiction}/ rather than fork canopy. The same three layers (dashboard, case-detail, identity) follow the same composition + override pattern, with a five-layer storage model (user delta → role override → jurisdiction live override → TOML baseline → system defaults).

Scope

In scope (across Stages 1-7)

  • Design system extraction into reusable Askama partials + CSS utility classes (Stage 1) + four-state primitive upgrade (Stage 1.5)

  • Composability runtime: TOML composition loader, 5-layer merge (RFC 7396 baseline + RFC 6902 DB layers per ADR-022), plugin manifest validation per ADR-021

  • Storage layering: forward-only migration for composition_documents + composition_documents_archive; live override APIs (read/write/archive)

  • Generic IDP loader + IDP-aware sign-in

  • Worker / supervisor / analyst dashboards (composition-driven)

  • Case detail (3 shell strategies + 13 section types)

  • Customize-my-dashboard (per-worker deltas)

  • Jurisdiction Studio (onboarding wizard + live-mode composer + Plugin Studio)

  • ⌘K command palette

  • Audit log unified surface (system + case scopes)

  • First-impression states (splash, maintenance, expired, 404)

Out of scope

  • Plugin Marketplace federation — defer to v2 (in-process Askama partials in v1 per ADR-021 require canopy-core PRs to install plugins; federation arrives when WasmPluginSource is added)

  • Studio promote modal / promote-live-to-baseline endpoint — descoped from v1 pending #507 (unified canopy config-backend ADR). Admins promote live overrides to baseline via their jurisdiction’s existing workflow (PR / Salt / manual edit) external to canopy until #507 lands.

  • Applicant portal (ADR-008, Dioxus) — separate project

  • Mobile / tablet — not in scope for v1

  • Print viewsADR-010 Typst territory, separate surface

  • Worker-journey microspec — needs design iteration before build; file separate design-spec issues per journey (intake / IEVS resolution / sign-and-authorize / notice compose)

  • Determination sign-and-authorize ceremony — needs design before build; tracked as separate spec issue

  • Notice compose/preview UX — needs design before build; tracked as separate spec issue

  • Income tab interaction depth (IEVS resolution, add/edit/remove income, employer lookup) — section shape is designed; interaction depth is TBD

Design

NOTE

Stage 2 closed at 2 of 3 ADRs ratified 2026-05-20: ADR-021 (composability runtime + plugin model) and ADR-022 (storage layering). The third ADR (originally "ADR-023: promote-PR mechanism") was reframed during drafting and deferred to #507 (unified config backend across canopy domains). This Design section is the canonical narrative; the ADRs are the canonical contract.

Composability runtime + plugin model

Ratified in ADR-021. Key shape:

  • PluginSource trait abstracts plugin discovery. v1 ships only CompileTimePluginSource. v2 federation (WasmPluginSource etc) is additive.

  • Plugins are in-process Askama partials registered via #[canopy_plugin] macro + linkme distributed slice. Compile-time Plugin.toml ↔ Rust handler validation prevents manifest-handler drift.

  • Composition loader signature: load_composition(jurisdiction, role, user_id, surface) → Result<ComposedSurface, CompositionLoadError>.

  • Cache: invalidate-on-write, single-replica v1. Studio writes invalidate the in-process cache; multi-replica RabbitMQ fanout invalidation deferred post-UAT.

  • Role filtering applies after override merge — items the role can’t use are silently dropped, no aria-disabled clutter.

  • Closed-set errors: UnknownPlugin, SpanOutOfRange, RowOverflow, RoleNotFound.

Composition override storage layering

Ratified in ADR-022. Key shape:

  • Three DB-backed layers (user delta, role override, jurisdiction live) share one composition_documents table keyed by (jurisdiction_id, layer, scope_key, surface). scope_key is polymorphic: user UUID for user, role slug for role, 'jurisdiction' sentinel for jurisdiction_live.

  • Override body is an RFC 6902 JSON Patch op list. Studio "add one panel" maps to one {"op":"add","path":"/items/-",…} op (not a full-document rewrite). test ops support optimistic concurrency.

  • Composition loader fetches all DB layers in one indexed SQL query (WHERE jurisdiction_id AND surface AND (layer IN …)) and replays the patch lists against the jurisdiction TOML baseline in jurisdiction_live → role → user order.

  • Override lifecycle: live override stays after promote-merge as a no-op; Studio surfaces a "live matches baseline" hint with an explicit Archive button. No canopy-core-repo watcher in v1.

  • Uniform 1-year audit retention for override-layer events; every write emits a JWS-signed AuditEvent per ADR-014 (chain integrity extends across composition mutations).

Composability thesis (architectural)

Three layers all follow the same composition + override pattern:

  • Dashboard composition — which panels appear, in what rows, at what spans

  • Case-detail composition — which sections appear, in what order, with which shell strategy (scroll / card-grid / tabs)

  • Identity composition — which IDP(s), in what order, with what label/color

Every plugin (panel or section) declares its slug, allowed programs, default span, permissions, and i18n catalogs in its manifest. Runtime renders only what jurisdiction TOML references.

Case-detail shell strategy (locked 2026-05-19)

Shells are jurisdiction-selectable per role, not progressive enhancements at viewport breakpoints. Same jurisdiction can run different shells per role; any given user in any given context sees exactly one shell. No responsive shell-switching in v1.

Rationale (per design):

  • Desktop-first product — workers don’t review cases on phones

  • Runtime cost of two shells live: double templates, double htmx swap targets, ambiguous focus management when the viewport crosses a breakpoint mid-action

  • Escape hatch already exists: a jurisdiction that needs viewport flexibility picks tabs (the most viewport-tolerant shell)

TOML shape per role in rulesets/{jurisdiction}/composition/case-detail.toml:

[shell.eligibility_worker]
strategy = "scroll"

[shell.intake_screener]
strategy = "card_grid"

Section list and shell strategy are independent knobs. A jurisdiction on tabs can still reorder sections, mark some required = true, or exclude program-specific ones via the same TOML’s sections = […​] list. Section partials know nothing about which shell hosts them — section content shape is identical across shells; only the chrome changes per strategy.

Georgia migration default: existing 6-tab implementation becomes strategy = "tabs" with current section slugs in sections = […​]. scroll and card_grid ship as opt-in; Studio admin switches when ready. No auto-migration.

Storage layering (5 layers, top wins)

Layer Storage Edited via Audience

User personal layout

composition_documents (layer='user'), RFC 6902 op list

"Customize my dashboard"

Each worker

Role overrides

composition_documents (layer='role'), RFC 6902 op list

Studio live mode

Jurisdiction admin

Jurisdiction live overrides

composition_documents (layer='jurisdiction_live'), RFC 6902 op list

Studio live mode

Jurisdiction admin

Jurisdiction baseline

TOML on disk under rulesets/{juris}/composition/

Jurisdiction’s own workflow (PR / Salt / manual edit), external to canopy v1; Studio promote pending #507

Maintainer

System defaults

Compiled into canopy core binary (panel/section registry initial state)

canopy core PRs

canopy team

Schema + merge semantics ratified in ADR-022: one unified composition_documents table for the three DB-backed layers (NOT three per-layer tables); RFC 6902 JSON Patch for DB-backed layers; RFC 7396 JSON Merge Patch for the baseline-over-defaults overlay.

Generic IDP interface

  • Email-first discovery routes user to matching IDP by domain or claim mapping

  • idp.toml declares N IDPs with slug + label + provider type (Keycloak / SAML / OIDC) + host + chip styling

  • Local accounts as a toggle (localAccounts.enabled = false → section vanishes)

  • Zero-IDP graceful state pointing at Studio → Identity

Audit (one primitive, two scopes)

AuditEvent is JWS-signed (ADR-014 already shipped this for canopy-security):

  • System audit — admin-facing, jurisdiction-wide, filterable, exportable to CSV / signed PDF / compliance report

  • Case history — same data, filtered to one household, exportable as "Cite for hearing" → signed PDF

Plugin manifest

Plugin.toml declares [plugin] (slug + name + version + author + license + canopy_min), [plugin.exports] (panels + case_sections), per-panel config (display_name + icon + programs + default_span + allowed_spans + required_states), [data] (source + auth + cache_ttl + timeout + endpoints), [permissions] (required_roles + audit), [i18n] (default + catalogs). Full schema with v1 constraints is in ADR-021.

Compile-time validation via the #[canopy_plugin] macro: slug uniqueness across the registered set; semver on version + canopy_min (with canopy core’s CARGO_PKG_VERSION asserted to satisfy); allowed_spans ⊆ {1, 2, 3, 4, 6, 12} (12-column grid breakpoints only); programs ⊆ {snap, tanf, medicaid, caps, wic}; manifest-declared endpoints' URL parameters resolve to handler request type fields (prevents manifest-handler drift). Composition-time validation (per render): every slug in composition TOML resolves in the PluginRegistry; per-item spanallowed_spans; total row span ≤ 12.

Steps

(Each "Step" below is a discrete MR. Steps within a stage may parallelize. Stages 1 + 2 are gating; Stages 3-7 unlock incrementally.)

Stage 1 — Design system extraction (1 MR)

  • #485 Extract 8 Askama primitives (PanelFrame, Overline, GoldRule, BigNumber, HeroStrip, LeafGlyph, StatusPill, MoneyCell) into services/canopy-web/templates/_primitives/*.html

  • Reuses existing .skeleton + @keyframes pulse + .u-empty-state and adds one new .u-error-block utility (the four Stage-1.5 macros wrap these classes; no renames)

  • Outcome: existing canopy-web templates rewritable to use primitives without composability runtime

Stage 1.5 — Panel-state primitives upgrade (1 MR, follow-up to Stage 1)

  • #505 Upgrade the 4 panel-state utility classes to first-class Askama-macro primitives (EmptyState, Skeleton, SkeletonRow, ErrorBlock)

  • Rule: every panel renders all four states

  • Non-blocking — runs in parallel with Stage 2 / Stage 3

Stage 2 — ADR ratification (2 MRs shipped; 3rd deferred)

  • #486 ADR-021: Composability runtime + plugin model — ratified 2026-05-20

  • #487 ADR-022: Storage layering for composition overrides — ratified 2026-05-20

  • #488 ADR-023: Promote-PR mechanism — deferred 2026-05-20 to #507

No code in Stage 2. Pure architectural ratification.

NOTE

ADR numbering: epic &51 claimed ADR-021 + ADR-022 (both ratified 2026-05-20). ADR-023 (promote-PR) was deferred 2026-05-20 in favor of #507 (unified config backend across canopy). ADR-023’s number is released — the next-claiming ADR (likely #507 or #484 Phase 4) takes it.

Stage 3 — Composability runtime + storage layering (3 active MRs; 1 deferred)

  • #489 Forward-only migration (per ADR-016) landing the unified composition_documents table + composition_documents_archive per ADR-022 (one table for all three DB-backed layers, polymorphic scope_key)

  • #490 Composition loader: 5-layer merge (system defaults compiled into binary → jurisdiction baseline TOML via RFC 7396 → DB-backed layers via RFC 6902 in jurisdiction_live → role → user precedence) + role filter applied post-merge; integrated with the invalidate-on-write cache from ADR-021

  • #491 Live override APIs (read/write/archive endpoints accepting application/json-patch+json; If-Match ETag for full-list replace; JWS-signed AuditEvent per ADR-014 on every mutation)

  • Deferred: Promote-live-to-baseline (#492 closed-deferred to #507). Studio’s v1 "promote" affordance is admin-driven: admin uses jurisdiction’s existing baseline-edit workflow (PR / Salt / manual edit) external to canopy.

Stage 4 — Identity rework (2 MRs)

  • #493 Generic IDP loader from rulesets/{juris}/idp.toml (email-discovery routing; OIDC provider types keycloak + oidc-generic in v1; local-accounts toggle; zero-IDP graceful state). SAML federation stays upstream of the OIDC IdP per CRAIG-aligned design; full multi-shape claims (authentik / zitadel / kanidm) deferred to #515.

  • #494 IDP-aware sign-in template — email-first discovery + multiple IDP chips with per-IDP chip_color + chip_icon; CSP-clean (Alpine.js + htmx only); axe-core WCAG 2.1 AA

  • Plan: worker-portal-redesign-stage4-idp-loader-and-sign-in.adoc

  • Follow-ups filed: #512 (SAML spike), #513 (local accounts), #514 (introspection mode), #515 (multi-shape claims), #516 (multi-juris sign-in), #517 (identity verify xtask), #518 (env-var retirement)

Stage 5 — Core surfaces (4-5 MRs, one per surface)

  • Worker dashboard (12-panel kit, composition-driven)

  • Supervisor dashboard (jurisdiction-aware overrides)

  • Analyst dashboard (panel subset)

  • Case detail with 3 shell strategies + 13 section types

  • Customize-my-dashboard (per-worker deltas)

Stage 6 — Maintainer surfaces (3 MRs)

  • #499 Studio onboarding wizard — MR1a + MR1b + MR1c shipped 2026-05-25 (!367 / !368 / forthcoming). MR1a promoted rulesets/georgia/ to rulesets/default/ (reference ruleset). MR1b made canopy-core jurisdiction-agnostic: FilesystemJurisdictionRegistry + active_jurisdiction(state) + WorkerRole::StudioAdmin + role coverage across 40 Plugin.tomls. MR1c shipped the 5-step wizard at /studio/onboarding/step/{1..5} emitting a downloadable scaffold bundle (deep-copy of default/ + 3 overridden TOMLs + README documenting integrator’s required next steps). 3 FUs deferred: #568 multi-IdP, #569 custom theme palette, #570 IdP discovery probe.

  • #500 Studio live-mode composer (promote modal descoped from v1 pending #507; admin promotes via jurisdiction’s external workflow until then)

  • #501 Studio plugin developer view (Plugin Studio — authoring scope is one of the 3 open design questions on #486)

Stage 7 — Polish (2-3 MRs)

  • ⌘K command palette

  • Audit log unified surface (system + case scopes)

  • First-impression states (splash, maintenance, expired, 404)

Files Touched

Stage-by-stage rough touch list. Per-MR files-touched specifics live in each MR’s description; this is the planning-level inventory.

Stage 1 (design system extraction)

  • NEW: services/canopy-web/templates/_primitives/orchard.html (single file housing all 8 macros)

  • NEW: services/canopy-web/templates/_primitives/_smoke.html (smoke fixture for unit tests)

  • NEW: services/canopy-web/tests/primitives_test.rs (per-macro variant assertions)

  • MODIFIED: services/canopy-web/static/css/canopy-web.css — adds ~220 lines for the 8 primitives' classes + .u-error-block; reuses existing .skeleton, @keyframes pulse, and .u-empty-state; no inline style= per CSP. Status-pill kind backgrounds hardcoded for cross-jurisdiction consistency (matches existing .u-status-* convention)

  • Stage-1 implementation plan lives at worker-portal-redesign-stage1-design-system.adoc

Stage 1.5 (panel-state primitives upgrade)

  • NEW: macros for empty_state / skeleton / skeleton_row / error_block appended to _primitives/orchard.html

  • NEW: services/canopy-web/templates/cases/_results_error.html (htmx error fragment for the validating surface)

  • NEW: tests/e2e/specs/panel-states.spec.ts (Playwright spec exercising each of the four states)

  • MODIFIED: services/canopy-web/static/css/canopy-web.css — no changes (macros wrap existing .u-empty-state / .skeleton / .u-error-block)

  • MODIFIED: ≥ 1 in-tree panel template validating the four-state rule (Stage-1.5 plan picks cases/search.html to avoid the dashboard.spec.ts coupling Stage 1 deferred)

Stage 3 (composability runtime + storage)

  • NEW: services/canopy-web/migrations/<timestamp>_composition_documents.sql per ADR-022 schema

  • NEW: crates/canopy-plugin-macros/ (proc-macro crate; #[canopy_plugin] derive + compile-time Plugin.toml validation)

  • NEW: crates/canopy-composition/ (or canopy-web internal module — TBD by Stage 3 implementation plan): PluginSource trait + CompileTimePluginSource + load_composition + 5-layer merge

  • NEW: HTTP handlers in canopy-web for GET/PATCH/DELETE /v1/composition/{surface}/{layer}/…​ per ADR-022 write API contract

  • MODIFIED: services/canopy-web/src/api/…​ to wire the composition loader into existing case-detail + dashboard request paths

Stages 4-7

Per-MR file lists live in the MR description; this plan does not pre-enumerate them since the contracts are stable from Stages 1-3.

Branch + label hygiene

  • Branch root: feat/worker-portal-redesign-{stage}-{surface} (one per MR)

  • Labels: type::feature + priority::medium (or priority::low for v2 deferrals) + program::infrastructure (UI affects multiple programs) + service::web + workflow::ready (or workflow::needs-spec for design-iteration-required surfaces)

CHANGELOG entries

One === Changed entry per Stage MR. Format mirrors the canopy-test-lib port plan’s entries.

Verification

Per-MR

  • cargo xtask validate clean (fmt + clippy + nextest + check-docs)

  • cargo xtask docs plan-lint clean (this plan’s Status column stays canonical)

  • Pre-push validate + Playwright E2E (≥ 136 currently green)

  • cargo xtask coverage at or above the current DEFAULT_THRESHOLD floor (xtask/src/cmd/coverage.rs; unit-lane scoped since #1382)

Stage acceptance

  • Stage 1: every primitive exercised by the smoke-fixture template (_primitives/_smoke.html) + a per-variant assertion in tests/primitives_test.rs. Consumer-template migration happens per Stage 5-7 issues.

  • Stage 1.5: ≥ 1 in-tree panel template renders all four states (empty, loading, error, populated) using the four empty_state / skeleton / skeleton_row / error_block macros; all 20 .u-empty-state template consumers migrated to o::empty_state; underlying .u-empty-state / .skeleton / .u-error-block CSS classes retained (macros wrap them per Stage 1 Decision 4)

  • Stage 2: ADR-021 + ADR-022 merged + cross-referenced from this plan’s Design section (Stage 2 done at 2 of 3; ADR-023 deferred to #507)

  • Stage 3: forward-only migration applied cleanly; composition loader unit-tested with fixture TOML + fixture patch op lists covering all 5 layers; PatchFailed error surfaces test-op failures + missing paths with layer + op-index context

  • Stage 4: IDP loader unit-tested per provider type (keycloak + oidc-generic); sign-in template axe-core WCAG 2.1 AA clean; zero-IDP graceful state renders. SAML federation dropped from app layer (handled upstream of OIDC IdP); full multi-shape claims (authentik / zitadel / kanidm) deferred to #515.

  • Stage 5-7: per-surface acceptance defined in the surface’s own MR

Documentation Updates

  • CHANGELOG.adoc — one === Changed per Stage MR

  • This plan — Status column updated after each MR

  • ADRs (Stage 2 outputs)

  • Coding Conventions — adds composition-runtime conventions after Stage 3

  • Architecture — gains a "Worker portal composability" section after Stage 3

Pre-commit Q1-Q8 expectations (every MR)

  • Q1 — every stage MR adds tests for its new code (composability loader, override merge, IDP discovery, etc.)

  • Q2 — no unwrap outside tests, no unsafe, no #[allow]

  • Q3 — no test deletions or weakened assertions

  • Q4 — design deviations update this plan’s Design section + file separate design-iteration issues if material

  • Q5 — only the final MR of Stage 7 closes #460

  • Q6 — out-of-scope items stay deferred (marketplace, applicant portal, mobile, print, worker-journey microspec, sign-and-authorize ceremony, notice compose)

  • Q7 — per-stage CHANGELOG + this plan’s Status row + relevant doc updates

  • Q8 — zero new TODO/FIXME tokens

Risk + Rollback

  • Risk — Stage 3 (composability runtime). Real architectural commitment touching rendering pipeline, DB schema, override merge semantics, plugin lifecycle. Mitigation: ADR-first sequencing is non-negotiable (ADR-021 + ADR-022 ratified before any Stage-3 code).

  • Risk — Stage 6 (Studio). Largest design surface; live-mode composer + Plugin Studio + onboarding wizard. Mitigation: ship Stage 6 strictly after Stage 5; if Studio churns, core surfaces remain usable without it. (Studio promote modal already descoped from v1 pending #507 — reducing Stage 6 risk.)

  • Risk — design gaps surface mid-build (worker journeys, IEVS resolution depth, sign-and-authorize ceremony, notice compose). Mitigation: file separate design-iteration issues per journey; surface them in this plan’s "Out of scope" list and resolve via independent design+build cycles.

  • Risk — linkme platform portability (Stage 3). linkme distributed slices are linker-dependent; works reliably on canopy’s Linux Alpine production target but unproven on some edge targets. Mitigation: ADR-021 documents Option B (build.rs scan) as a portable fallback should linkme bite — same CompileTimePluginSource shape, different population mechanism.

  • Rollback: revert the offending MR. Each Stage’s MRs are independent; partial-stage rollback leaves the workspace consistent.

Open decisions revisited as Stages land

Architectural decisions resolved by ratified ADRs (no longer open):

  • ✅ DB override storage shape — unified composition_documents table (ADR-022 Decision 1)

  • ✅ Override merge semantics — RFC 6902 for DB layers, RFC 7396 for baseline-over-defaults (ADR-022 Decision 2)

  • ✅ Override lifecycle — explicit Studio Archive (ADR-022 Decision 3)

  • ✅ Audit retention — uniform 1-year for override-layer events (ADR-022 Decision 4)

  • ✅ Plugin sandboxing — in-process Askama partials behind PluginSource trait (ADR-021)

  • ✅ Plugin discovery — #[canopy_plugin] macro + linkme (ADR-021)

  • ✅ Composition cache — invalidate-on-write, single-replica v1 (ADR-021)

Still open (need decisions before the relevant stage starts):

  • Marketplace installs tracking (self-reported vs central registry) — only relevant once Plugin Marketplace federation is in scope (post-v1; tied to #507’s unified config backend)

  • Worker-journey microspec ownership (design vs engineering) — design-iteration issues to file per journey

Routed to design (open on #486):

  • Plugin Studio (#501) authoring scope in v1

  • Multi-jurisdiction plugin visibility (compile-time visibility vs jurisdiction opt-out)

  • i18n catalog fallback when user locale not shipped by a plugin

Edit this page · default