Worker Portal Design Reference

On this page

This page is the reference distillation of the worker-portal design handoff (design/canopy-web/). It records the architectural decisions the design depends on, the schemas the composition runtime must load and validate, and the contract plugin authors work against.

NOTE
The visual design system itself — Orchard color tokens, type scale, spacing, radii, motion, voice, and the reusable component primitives (PanelFrame, Overline, GoldRule, BigNumber, HeroStrip, …) — lives on its own page. See Orchard Design System. For the per-page layout reference of the production surfaces as they exist today, see Worker Portal Mockups.

The design artifacts

The worker-portal design package is a set of self-contained HTML design canvases (open in a browser to pan, zoom, and focus per artboard) backed by JSX component sources. Together they cover the full caseworker surface: the 12-panel dashboard kit and three jurisdiction compositions, case-detail section composability across three shell strategies, the IDP-aware sign-in, Jurisdiction Studio (onboarding wizard, live mode, promote-PR, plugin dev), worker-level dashboard customization, a full dark-mode sweep, empty/loading/ error panel states, the ⌘K command palette, the audit log (system + case scopes), the plugin marketplace, and first-impression states (splash, maintenance, expired, 404).

The package is the application of the design system, not a redefinition of it. The design system is the contract; everything else applies it.

Architectural decisions made during design

These decisions shaped the design and must hold for the design to work. Each is summarized here for context; the authoritative rationale lives in the ratifying ADR, which is xref’d rather than duplicated.

2.1 Composability is the core thesis

A jurisdiction should never fork canopy to make the portal theirs — it edits TOML in rulesets/{jurisdiction}/. The design enforces composability at three layers:

  • 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 and color.

Every plugin (panel or section) declares its slug, allowed programs, default span, permissions, and i18n catalogs in its manifest. The runtime renders whatever the jurisdiction’s TOML references; plugins not opted-in stay invisible.

Ratified by ADR-021 (compile-time-linked Askama plugins registered via #[canopy_plugin]
linkme, with a PluginSource trait so v2 WASM federation is additive).

2.2 Storage is hybrid-layered

Composition is resolved from layered overrides; the top layer wins.

Layer Storage Edited via Audience

User personal layout

DB (small JSONB delta)

"Customize my dashboard" view

Each worker

Role overrides

DB

Studio (live mode)

Jurisdiction admin

Jurisdiction live overrides

DB

Studio (live mode)

Jurisdiction admin

Jurisdiction baseline

TOML on disk, in git

Studio promote → PR

Maintainer via PR

System defaults

Bundled with binary

canopy core PRs

canopy team

Live → baseline promotion is a real PR (the Promote modal in Jurisdiction Studio). On merge, the live override clears because the baseline now equals what was live.

User overrides are a delta, not a full config — { hidden: […​], pinned: […​], rowOrder: […​] }. Reset-to-default deletes the row.

Required panels — a jurisdiction admin can mark a panel required = true in TOML; the user customize UI shows a lock badge and disables the hide button.

Ratified by ADR-022 (the DB-backed layers share one composition_documents table keyed by (jurisdiction_id, layer, scope_key, surface); override bodies replay against the TOML baseline in jurisdiction_live → role → user order). The user-layer delta shape is further specified by ADR-024 (the user_delta_v1 envelope of hidden_slugs / span_overrides / slug_order for dashboard surfaces).

2.3 Generic IDP interface

The IDP-aware sign-in is the canonical surface. Key behaviors:

  • Email-first discovery is the universal primary affordance. The user types an email; canopy routes to the matching IDP by domain or claim mapping.

  • N configured IDPs render as a list. Each entry carries a slug, label, provider type (Keycloak / SAML / OIDC / etc.), host (shown in mono as proof of inspectability), a chip color + 2-letter chip text, and an optional primary = true (gold "PRIMARY" badge + green border).

  • Local accounts is a toggle, not a default. If local_accounts = false, the "or local account" section vanishes entirely.

  • Zero-IDP state — with no IDPs configured the card shows a calm "Not yet configured" panel pointing at Studio → Identity. Never a broken page.

Ratified by ADR-019 (service identity, IDP integration, and on-behalf-of token flow).

2.4 Audit is JWS-signed and exportable

Every system action emits a signed event. Two scopes share one primitive:

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

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

The event shape the design assumes:

struct AuditEvent {
    id: String,           // evt_abc123
    ts: DateTime<Utc>,
    category: Category,   // case | config | auth | service | plugin | ievs | notice
    action: String,       // dotted: "dashboard.live_changed", "income.edited"
    actor: Actor,         // { kind: worker|system|applicant, name, role?, initials }
    target: Option<Target>, // { type, label, id, link? }
    summary: String,      // human-readable
    meta: serde_json::Value, // structured detail (diffs, before/after)
    signature: Vec<u8>,   // ed25519 over a canonical encoding
}

Ratified by ADR-014 (the JWS-signed audit hash chain; chain integrity extends across composition mutations and FTI events).

2.5 Empty / loading / error states are first-class

Every panel renders all four states (data plus three):

  • Empty is editorial and confident ("Nothing in your queue."), never chirpy. Sometimes a tiny CTA (Open Studio →).

  • Loading uses pulsing skeletons (the cy-pulse keyframe, 1.6s ease-in-out) shaped to the eventual content. Panel height stays constant so the data swap doesn’t reflow layout.

  • Error always names the failing service, shows the last-known sync timestamp when useful, and offers Retry + "Status page". It never blames the worker.

IMPORTANT
This is the rule, not an aspiration. Plugin panels must implement all four states — see hard rule 5 below.

2.6 The ⌘K palette is one keystroke from anywhere

Cross-entity search by default. Prefixes narrow scope:

Prefix Scope

(none)

Households, people, notices, appeals, activity

/

Command mode (run actions: "Re-run IEVS sync for current case", "Reset my layout", "Open Studio")

@

People only

#

Cases only

RBAC filters at query time — a worker sees the commands they can run, a supervisor sees their team, an admin sees everything. The palette is treated as a panel: same overline + gold rule + ID-mono language as everything else.

2.7 The accent gold has strict scope

#ecbf44 Orchard Gold is for brand and decoration only:

  • The center leaf of the canopy mark.

  • Section rule accents (the 14–40px gold lines).

  • Selected-row indicators (left rail + soft tint).

  • Overline labels (sparingly).

Gold is never used for functional warning UI (use warning amber #e08a2b), status pills, body text (fails contrast on white at any size), or new decorative ideas without design review.

NOTE
A second gold (#D0AE56, "DHS Logo Gold") exists only for rendering the actual Georgia DHS agency seal in co-branded materials. Treat it as out of scope for general UI.

Schemas implied by the design

These are the contracts the composition runtime loads and validates. The TOML blocks below are reproduced faithfully from the design handoff.

4.1 rulesets/{jurisdiction}/theme.toml

Branding plus layout plus the light/dark palettes. The full reference file ships with the design package (canopy-handoff/georgia-theme.toml); the live implementation is rulesets/georgia/theme.toml + src/theme.rs, with the token contract in the design system reference.

4.2 rulesets/{jurisdiction}/dashboards.toml

# Defines dashboards available in this jurisdiction.
# Multiple dashboards per role allowed; the user picks via a dashboard selector.

[dashboard.my_day]
title = "My day"
roles = ["eligibility_worker"]
default_for_role = true

[[dashboard.my_day.row]]
panels = [
  { type = "kpi_strip", span = 12 },
]

[[dashboard.my_day.row]]
panels = [
  { type = "my_worklist", span = 8 },
  { type = "quick_search", span = 4 },
]

[[dashboard.my_day.row]]
panels = [
  { type = "pending_verifications", span = 6, required = true },
  { type = "renewals_due", span = 6 },
]

[[dashboard.my_day.row]]
panels = [
  { type = "time_pressured", span = 5 },
  { type = "recent_activity", span = 7 },
]

Constraints:

  • Panel type must exist in the registry (catalog + installed plugins).

  • The sum of spans in a row must equal 12.

  • required = true disables user-level hiding.

  • Any role mentioned in roles must exist in roles.toml.

4.3 rulesets/{jurisdiction}/case-detail.toml

# Shell + section composition for case detail.
# Per-role shells allowed; default is "scroll".

[shell.eligibility_worker]
strategy = "scroll"            # scroll | card_grid | tabs

[shell.intake_screener]
strategy = "card_grid"

[shell.snap_only_worker]
strategy = "tabs"

[[section]]
slug = "household"
roles = ["*"]                  # all roles see it
required = true

[[section]]
slug = "income"
roles = ["*"]
required = true

[[section]]
slug = "determination"
roles = ["*"]
required = true

[[section]]
slug = "abawd"
roles = ["eligibility_worker"]
programs = ["snap"]            # only renders for SNAP cases

[[section]]
slug = "notices"
roles = ["*"]

[[section]]
slug = "appeals"
roles = ["*"]

[[section]]
slug = "activity"
roles = ["eligibility_worker", "supervisor"]

4.4 rulesets/{jurisdiction}/idp.toml

# Identity providers configured for this jurisdiction.
# Generic interface — anyone can use kanidm / authentik / keycloak / SAML / etc.

[options]
email_discovery = true         # show email-first input
local_accounts = false         # disable local accounts entirely
local_accounts_collapsed = true  # if enabled, render under an expander

[[idp]]
slug = "state-sso"
label = "State SSO"
provider = "keycloak"          # informational; the actual flow is OIDC
host = "auth.dhs.ga.gov"
chip_text = "KC"
chip_color = "#1e5146"
primary = true

[idp.config]
type = "oidc"
client_id = "canopy-ga"
discovery_url = "https://auth.dhs.ga.gov/realms/ga-dhs/.well-known/openid-configuration"
scopes = ["openid", "profile", "email", "groups"]

[[idp]]
slug = "ad-saml"
label = "Active Directory"
provider = "saml2"
host = "fed.fulton.gov"
chip_text = "AD"
chip_color = "#275f63"

[idp.config]
type = "saml2"
entity_id = "https://fed.fulton.gov/idp"
sso_url = "https://fed.fulton.gov/idp/SSO"
cert_path = "secrets/fulton-saml.pem"

# email_routing maps domain -> idp slug for the discovery flow
[email_routing]
"dhs.ga.gov" = "state-sso"
"fulton.gov" = "ad-saml"

4.5 Plugin manifest (Plugin.toml)

[plugin]
slug         = "child_support"
name         = "Child Support Enforcement"
version      = "0.2.1"
author       = "Georgia DHS · OCSE Integration Team"
license      = "AGPL-3.0-or-later"
canopy_min   = "1.4.0"

[plugin.exports]
panels        = ["child_support"]
case_sections = ["child_support"]

[panel.child_support]
display_name  = "Child support"
icon          = "scales"
description   = "OCSE enforcement actions, arrears balances, payment status."
programs      = ["tanf", "medicaid"]
default_span  = 6
allowed_spans = [4, 6, 8, 12]

[data]
# Implemented schema (ADR-021; drifted illustration corrected by #1218):
# `source` is a canopy service slug, `auth` ∈ {none, service_class, user_jwt},
# TTL/timeout are integer seconds/ms, and `endpoints` is a flat allow-list
# (EMPTY is legal for plugins that perform no upstream fetch). The manifest
# TTL is the plugin author's DEFAULT; deployments override per item via the
# composition layers, and cache_ttl_seconds = 0 disables caching.
source            = "canopy-exchange"
auth              = "service_class"
cache_ttl_seconds = 300
timeout_ms        = 2000
endpoints = [
    "/v1/child-support/balances/{household_id}",
    "/v1/child-support/actions?household_id={household_id}&limit=10",
]

[permissions]
required_roles = ["eligibility_worker", "case_manager", "supervisor"]
audit          = "ocse_read"

[i18n]
default  = "en-US"
catalogs = ["en-US", "es-US"]

Validation rules: the slug is unique in the catalog, the version is semver, the programs are known program slugs, allowed_spans ⊆ {1..12}, and every required role exists.

Hard rules for plugin authors

  1. Use --orchard-* CSS variables. Never hardcode hex.

  2. No new fonts. Inherit Montserrat + JetBrains Mono.

  3. Wrap content in PanelFrame with an overline label. Don’t add chrome at the panel-frame level.

  4. Declare programs + permissions in the manifest. The runtime enforces; don’t gate inside the plugin.

  5. Implement all four states (data, empty, loading, error). PR review will reject plugins that don’t.

  6. Ship en-US + at least one Spanish catalog. Required for publish to the marketplace.

  7. No images. Use the leaf glyph or text-only treatments. Plugins should add < 100KB.

  8. All write actions are CSRF-protected + audited. Use the canopy CSRF + audit helpers; don’t roll your own.

The CSRF + audit requirement in rule 8, and the manifest-declared permission enforcement in rule 4, follow the CLI/API/UI parity discipline in ADR-007 — every plugin action is a first-class, auditable API operation rather than UI-only behavior.

Open questions — resolution status

The handoff closed with five open questions for implementation to resolve. Most are now settled by the composition ADRs; the remainder are deferred post-UAT or marketplace-federation concerns.

Question Status Notes

DB override layers — table-per-layer or one config_documents table?

Resolved

One shared composition_documents table keyed by (jurisdiction_id, layer, scope_key, surface) per ADR-022; the user-layer delta shape is user_delta_v1 per ADR-024.

Where does git-host API access live for the promote-PR flow?

Open

Studio promote-merge lifecycle is explicit-archive (no canopy-core-repo watcher in v1) per ADR-022; the hosting boundary for the PR-creation call is still open.

Cross-jurisdiction directory service for federated workers (IDP email_routing)?

Open

Handoff leaned "probably no for v1"; email_routing stays jurisdiction-local.

Plugin marketplace installs count — who tracks it?

Open

A federation question, deferred with the marketplace surface itself.

Audit retention policy — per-jurisdiction override?

Partially resolved

ADR-022 fixes uniform 1-year retention for override-layer audit events; the broader 7-year case-action vs. 1-year config/auth split and any per-jurisdiction override remain a policy decision.

Edit this page · default