Appearance
Dispatch v1 β v2 Design System β Migration Plan β
Date: 2026-06-18 Β· Target: production Angular app at apps/web/ in the seeg-dev/dispatch monorepo (Nx, Angular standalone + signals, zoneless, SSR/SSG hybrid). PRs land on master.
0. Where this lives in the actual repo β
The production app is already scaffolded in this monorepo. Migration work edits these locations (not greenfield):
| Concern | Path | Pattern in use |
|---|---|---|
| Admin pages | apps/web/src/app/features/admin/pages/ | One standalone component per page (dashboard-page.ts, users-page.ts, mandates-page.ts, β¦) |
| Admin CRUD schemas | apps/web/src/app/features/admin/entities/ | One CrudSchema<T> per entity β users.ts, mandates.ts, organizations.ts, tasks.ts |
| Shared CRUD engine | apps/web/src/app/shared/crud/ | CrudSchema<T> + CrudOps<T> drive tables/forms; adding a column / field = edit the schema (same pattern as v2 admin.js SCHEMA) |
| Auth / roles | apps/web/src/app/core/auth/ | roles.ts defines the Role union + MANAGER_ROLES; role.guard.ts gates routes |
| API client | apps/web/src/app/core/api/ | Generated by Orval from the OpenAPI spec (orval.config.ts); models live in @core/api/model β do not hand-edit |
| Backend | apps/api/ (Go) + libs/auth/ (Go JWT verifier) | New entities, new roles, and any AI proxy endpoint live here |
| Member app | apps/web/src/app/features/member/ | Out of scope for this migration |
Two consequences of the real layout that change the work:
Roleis server-validated.libs/auth/jwt.govalidates themembership_roleJWT claim against the same string union asroles.ts. Adding'Principal Partner'is a three-repo change: Supabase JWT issuance βlibs/auth/jwt.goβapps/web/.../roles.ts. Ship the backend half first so JWTs with the new role validate.- Models are generated, not handwritten. New entities (
Node,Referral,Candidate) and new fields (User.node,User.referredBy,Mandate.documents) start as OpenAPI schema additions on the Go API. After the Go side ships, run Orval (bun nx run web:orvalor whatever the workspace target is β checkorval.config.ts) to regenerate@core/api/model. Do not add hand-written TS interfaces that shadow the generated ones.
This means the suggested PR sequence in Β§5 starts with backend (OpenAPI + Go + JWT) for any item that touches a model or a role β not with Angular.
1. Scope & intent β
The Dispatch design-system repo ships two reference implementations:
- v1 β
design-system/Dispatch Design System - v1/β the original member app + admin we've been shipping against. - v2 β
design-system/Dispatch Design System - v2/β adds a Member admin surface, a Nodes model, a Principal Partner role, a viewer-scope switcher, a pinnable icon-rail sidebar, an AI-assisted Add-member flow with contract intelligence and a compensation waterfall, plus a polished admin look-and-feel.
This doc enumerates every delta between v1 and v2 and translates it into Angular component / service / route / model work for the production app. Theme and runtime are unchanged β v2 keeps Tailwind v4 + DaisyUI v5, the same dispatch / dispatch-dark themes, brand tokens, fonts, connection indicator, PWA scaffolding, and Angular handoff posture from v1. The change is functional and structural in the admin, not visual at the token level.
The member-facing desktop and mobile templates are visually unchanged between v1 and v2. All migration work below is in the admin surface.
2. Summary of v2 deltas β
| Area | v1 | v2 | Migration class |
|---|---|---|---|
| Storage version | dispatch_admin_v3 | dispatch_admin_v4 | Schema / API |
| Entities | orgs, mandates, tasks, votes, users, skills | + nodes, referrals, candidates | New domain models |
| User fields | role, function, org, balance, skills | + node (FK to node), + referredBy, + photo URL, role adds Principal Partner | Model extension |
| Sidebar | Static dark sidebar | Icon-rail collapsible with pin (localStorage dispatch_nav_pinned) and hover-tooltip on collapsed items | Layout / component |
| Topbar | Search Β· bell Β· New | + viewer-scope switcher (Principal vs node-lead view), persisted to sessionStorage dispatch_admin_scope | Component + guard |
| Dashboard | Mandates + People & activity | + Member admin panel (KPIs Β· Nodes panel Β· sortable / multi-sort Member directory Β· referral relationships Β· Add-member CTA) | New panel + features |
| Add-member flow | Single modal | Wizard with live Claude contract intelligence (parse pasted mandate letter β extract rate, scope, member) + compensation waterfall preview | New flow + AI service |
| Users table | Sortable single column | Multi-sort (additive sort with shift/cmd), member directory shows Node, Referrer, Category (PP/MP/AP) | Table behavior |
| Member categories | Membership role only | Derived PP / MP / AP category (Principal Partner / Managing Partner / Associate Partner) + Dormant flag derived from activity | Derived helpers |
| Account photos | Initials avatar | Photo URL pulled at registration (Gravatar/pravatar in prototype, real Google Workspace photo in prod) | Auth integration |
| Mandate form | Modal | Full-page editor (editPage:true) with documents/file-drop | Component refactor |
| Mandates field | rate, duration, assignee, scope | + documents[] (mandate documents / SOW upload) | Model extension |
What did not change: connection indicator, PWA, theme tokens, fonts, mobile shell, desktop shell, Dispatch AI chat behavior, validation queue, registration wizard, completion-derivation rules, 0.00β10.00 / 2-decimal competence rule.
3. Authoritative source β
For each delta below, the source of truth in the v2 repo is:
- Layout / HTML / sidebar / topbar:
design-system/Dispatch Design System - v2/templates/admin/index.html - Schema, entities, render functions, scope, AI flows:
design-system/Dispatch Design System - v2/templates/admin/admin.js - Tokens / theme: unchanged from v1 β
styles.css,src/app.css,app-shell.js@themeblock - Product narrative:
design-system/Dispatch Design System - v2/design.md(Β§5.3 Admin, Β§6 data model)
4. Angular work β per-area breakdown β
Sizing: S = β€Β½ day Β· M = Β½β2 days Β· L = 2β5 days Β· XL = β₯1 week.
4.1 Domain models & services (Foundation β do first) β
| # | Item | Notes | Size |
|---|---|---|---|
| 4.1.1 | Node { id, name, region, lead } model + NodeService (CRUD via HttpClient) | Backend endpoint TBD; seed mirrors NODE_SEED (Node 1 Montreal/Priya Nair, Node 2 Toronto/Marc Tremblay, β¦) | S |
| 4.1.2 | Referral { fromUserId, toUserId, date } model + ReferralService; derive referredBy on User | Mirrors REFERRAL_SEED | S |
| 4.1.3 | Candidate modelpending_users flow (registrations awaiting MP merge) already covers the v2 CANDIDATE_SEED case. | β | β |
| 4.1.4 | Extend User model: add node: number | null, referredBy: string | null, photoUrl: string; extend Role union with 'Principal Partner' | Note: node === null means cross-node (Principal / Admin) | S |
| 4.1.5 | Extend Mandate model: add documents: MandateDocument[] where MandateDocument = { name, size, url? } | File upload contract needs backend coordination | M |
| 4.1.6 | ScopeService (signals): viewScope: signal<'all' | number>; persists to sessionStorage under dispatch_admin_scope; hydrated in afterNextRender (SSR-safe) | Replaces v2 loadScope/setScope/inScope/scopedMembers; all list queries pipe through it | M |
| 4.1.7 | MemberCategory helper + isDormant helper as pure functions (or computed() on user) | Mirrors v2 memberCategory, categoryLabel, isDormant | S |
| 4.1.8 | NavPinService: pinned: signal<boolean> synced to localStorage dispatch_nav_pinned; browser-only via afterNextRender | S |
4.2 Layout β Admin shell β
| # | Item | Notes | Size |
|---|---|---|---|
| 4.2.1 | <admin-sidebar> standalone component β icon-rail (collapsed default), expands on hover, locked open when pinned; pin button toggles NavPinService | Use DaisyUI classes; bespoke styles already exist as .adm-sidebar / .adm-pin / .pin-on / .pin-off in bespoke.css β port the rules | M |
| 4.2.2 | Tooltip on collapsed sidebar items (DaisyUI tooltip tooltip-right) | S | |
| 4.2.3 | <admin-topbar>: page title + search input + <scope-switcher> + notifications bell + contextual New button | M | |
| 4.2.4 | <scope-switcher> β <select> with All nodes (Principal view) + each node "{name} β {region} Β· Node Director view"; bound to ScopeService; hidden unless auth.isPrincipalViewer() (Principal Partner / Admin / Board) | S | |
| 4.2.5 | Role guard principalOnly on /admin routes that require all-nodes view (none yet, but the guard is the right place to wire role-gating for future PP-only pages) | S | |
| 4.2.6 | Update routes: add /dashboard/members, /dashboard/members/add (wizard), /nodes, /candidates (if backend exposes), keep all existing | S |
4.3 Dashboard β Member admin panel (the big new feature) β
The v2 dashboard adds a Member admin segment alongside Mandates and People & activity. Build as a child route or signal-driven tab.
| # | Item | Notes | Size |
|---|---|---|---|
| 4.3.1 | <member-admin-overview> β KPI strip: total members, active, dormant, awaiting verification, average competence | KPIs derived from filtered scopedMembers() | S |
| 4.3.2 | <nodes-panel> β card per node showing name/region/lead, member count, active mandate count; click β filter directory to that node (sets ScopeService) | M | |
| 4.3.3 | <member-directory> β table over scopedMembers() with columns: avatar+name+email, Node, Referrer (with hover popover to referral chain), Category chip (PP/MP/AP), function, status, GACx balance, actions | M | |
| 4.3.4 | Multi-sort on the directory: shift/cmd-click on column header adds the column to a sort stack; visual indicator of sort order; replaces single-column sort | Mirror v2 sortTableBy(i, additive) / sortRows. Implement as a SortStack signal { col, dir }[] consumed by a computed() sorted view. | M |
| 4.3.5 | Referral relationships sub-view: graph or tree of referredBy β user; minimal v1 = flat "Recruited by X" badges; richer v2 = a small SVG tree per cluster | Start with badges; promote to tree only if user asks | SβM |
| 4.3.6 | Add-member CTA β opens /dashboard/members/add wizard (see Β§4.4) | S | |
| 4.3.7 | Wire scope: every panel in Β§4.3 must respect ScopeService.viewScope β pure consumers, no scope logic of their own | S |
4.4 Add-member wizard with Claude contract intelligence + compensation waterfall β
The most substantial new feature. v2 implements it inline; in Angular it becomes a route with three steps, backed by two backend services: Go (apps/api) for orchestration, R2 I/O, and the Claude call; a new Python sidecar (apps/pdf-convert) for PDFβMarkdown conversion. The split exists because best-in-class PDFβMD libraries live in Python and quality matters (incoming contracts vary in layout β tables, columns, signed-PDF artefacts).
Three artifacts per contract upload, all in R2 (contracts/{mandate_id}/):
original.pdfβ exactly what the user uploaded.text.mdβ markdown conversion. Produced deterministically by the Python service β no AI in this step.extract.jsonβ structured fields from Claude ({ org, mandateTitle, scope, hourlyRate, currency, language, dutyDescription, governingArticle }).
Re-opening a mandate's contract reads extract.json directly β no Claude re-call. Re-running extraction overwrites extract.json and bumps a version field on the contract row.
Flow:
- Browser uploads PDF β Go
POST /api/v1/contracts/ingest. - Go writes
original.pdfto R2, calls Python sidecarPOST /convert(multipart or signed R2 URL), Python returns markdown, Go writestext.mdto R2, returns{ contractId, pdfKey, markdownKey }. - Wizard POSTs
{ contractId }to GoPOST /api/v1/ai/contract-extract. Go fetchestext.mdfrom R2, calls Claude (Anthropic Go SDK), writesextract.jsonto R2, returns the structured object. - Wizard pre-fills step 2 from the response; user edits; on Create, the mandate row stores R2 keys for all three artifacts.
Python sidecar choice β shortlist for implementation:
- Docling (IBM) β best overall structure handling (tables, columns, headings). Sensible default.
- Marker β also state-of-art, ML-based, good for messy layouts. Heavier.
- pymupdf4llm β fast, no ML, lower quality on complex layouts. Fine fallback.
Pick at implementation time, but default to Docling unless we hit a performance ceiling. The service is a small FastAPI app with one endpoint:
POST /convert body: { pdfBytes | r2Key }
200: { markdown: string, pages: int, warnings: string[] }Deployment: new monorepo app apps/pdf-convert/ (FastAPI + Docling + Dockerfile + project.json for Nx). Deployed alongside apps/api; internal-only (no public route); Go calls it over the cluster network. No GPU required for Docling on signed text-based contracts; CPU is fine.
Service-to-service auth: shared static token in env (PDF_CONVERT_TOKEN) β sidecar rejects requests without it. Upgrade to mTLS or a signed JWT if the service ever leaves the cluster.
| # | Item | Notes | Size |
|---|---|---|---|
| 4.4.1 | <add-member-wizard> standalone component, 3 steps: Member basics β Contract & compensation β Review & create | M | |
| 4.4.2 | Step 1: Full name, workspace email, node, function, role (defaults Member Partner), referredBy (typeahead over existing users) | S | |
| 4.4.3 | Step 2 β Contract upload + intelligence: drop a PDF β Go ingest returns contractId β Go extract returns structured fields β form pre-fills. Editable. Use SAMPLE_CONTRACT in v2 admin.js as fixture. | L | |
| 4.4.4 | Step 2 β Compensation waterfall preview: CompensationService.waterfall(rate, ctx) calls Go which evaluates the active compensation_policy (see Β§4.8) and returns the stacked breakdown. No referrer share at this time (confirmed). | M | |
| 4.4.5 | Step 3: read-only review + Create β posts to UserService.create() + MandateService.create() with the R2 keys; links referral; opens new user's profile | S | |
| 4.4.6a | Python sidecar apps/pdf-convert/ β FastAPI app exposing POST /convert. Docling-backed PDFβMarkdown. Dockerfile, Nx project.json, shared-token auth. Internal-only. | M | |
| 4.4.6b | Go handler POST /api/v1/contracts/ingest β multipart upload, writes original.pdf to R2, calls Python sidecar with the PDF bytes (or signed R2 URL), writes returned text.md to R2, records a contract row ({id, pdf_key, md_key, status}), returns {contractId, pdfKey, markdownKey}. R2 client via AWS SDK v2 (R2 speaks S3 API). | M | |
| 4.4.7 | Go handler POST /api/v1/ai/contract-extract β fetches MD from R2 by contractId, calls Claude via the Anthropic Go SDK, writes extract.json to R2, updates the contract row with json_key, returns structured object. Idempotent β re-call overwrites JSON. | M | |
| 4.4.8 | ContractIntelService (Angular) β wraps the two HTTP calls (ingest then extract), exposes one extract(file) method returning the structured object; SSR-safe (browser-only via afterNextRender). | S | |
| 4.4.9 | CompensationService (Angular) β thin wrapper over Go POST /api/v1/compensation/preview so the active policy stays the single source of truth (no formula duplication in the client). | S |
4.8 Compensation policy (backend + Framework & System editor) β
The waterfall numbers live in the DB, are editable, and are versioned. Schema:
compensation_policy (
id uuid pk,
name text,
version int,
effective_from timestamptz,
effective_to timestamptz null,
active bool,
created_by uuid fk users
)
compensation_split_rule (
id uuid pk,
policy_id uuid fk compensation_policy,
beneficiary text check (beneficiary in
('member','cooperative','node_lead','referrer','reserve')),
percentage numeric(5,2), -- 0.00 .. 100.00
condition text null, -- future: role/node/skill predicate
order_index int -- waterfall display order
)Editing a policy creates a new version (immutable history); the active row is the one with active=true. Go endpoint GET /api/v1/compensation/active returns it; POST /api/v1/compensation/preview { rate, context } returns the computed waterfall.
| # | Item | Notes | Size |
|---|---|---|---|
| 4.8.1 | DB migration: compensation_policy + compensation_split_rule tables in Supabase. Seed placeholder policy v1: 100% to member so the wizard can ship with real plumbing before final numbers are agreed. | S | |
| 4.8.2 | Go API: GET /api/v1/compensation/active, POST /api/v1/compensation/preview, GET/POST/PUT /api/v1/compensation/policies (versioned writes β PUT creates a new version, never mutates) | M | |
| 4.8.3 | OpenAPI additions + Orval regen | S | |
| 4.8.4 | Compensation policy editor page under the new Framework & System sidebar group: list of versions (active highlighted), view rules per version, "New version" button β form for rules β save creates new version + flips active. Read-only history. | M |
4.9 Sidebar reorganisation β new "Framework & System" group β
Sidebar sections become:
| Group | Items |
|---|---|
| Overview | Dashboard |
| Manage | Organizations, Mandates, Tasks |
| Governance & People | Votes, Users |
| Review | Validation queue |
| Framework & System (new) | Compensation policies (new), Nodes (moved here from top-level), Skills catalogue (moved here from top-level) |
Routes:
/framework/compensation(list of policies) +/framework/compensation/:id(view/edit a version)/framework/nodes(moved from/nodes)/framework/skills(moved from/skills)
| # | Item | Notes | Size |
|---|---|---|---|
| 4.9.1 | Update <admin-sidebar> to render the new group; update routes with redirects from old paths (/nodes β /framework/nodes, /skills β /framework/skills) | S | |
| 4.9.2 | Compensation editor page (Β§4.8.4) wired at /framework/compensation | covered in 4.8.4 |
4.5 Users / mandates CRUD updates β
| # | Item | Notes | Size |
|---|---|---|---|
| 4.5.1 | User form: add Node select (or "β (Principal / all nodes)" for cross-node users), add Membership role option Principal Partner | S | |
| 4.5.2 | Users table: add Node column (with nodeLabel(id) formatter); muted italic "all nodes" when null | S | |
| 4.5.3 | User profile drawer: show photo, category chip, referredBy, node, plus existing skills/competence block | S | |
| 4.5.4 | Mandate editor: convert from modal to full-page route /mandates/:id/edit; add <mandate-documents> file-drop component with drag-and-drop, size formatter | M | |
| 4.5.5 | Avatar helper: avatar(user) returns photo <img> if photoUrl, else initials in colored chip β used in every table/profile that shows a member | S |
4.6 Visual / look-and-feel pass β
v1 STATUS flagged "Admin look-and-feel pass" as the open task; v2 implements it. Tokens are unchanged, so this is mostly applied use of existing tokens:
| # | Item | Notes | Size |
|---|---|---|---|
| 4.6.1 | Port .adm-* rules from v2 bespoke.css into the Angular app's global bespoke.css (already imported at root) | Diff v1 vs v2 bespoke.css and bring across new selectors (sidebar pin, scope switcher, member directory rows, nodes panel cards) | M |
| 4.6.2 | Verify zero raw hex / Npx regressions (v2 keeps the adherence ruleset) | S | |
| 4.6.3 | Verify both dispatch and dispatch-dark themes look correct on every new surface | S |
4.7 Tests & verification β
| # | Item | Notes | Size |
|---|---|---|---|
| 4.7.1 | Unit tests for memberCategory, isDormant, clampCoef, voteApproval, CompensationService.waterfall | S | |
| 4.7.2 | Unit test for ScopeService persistence + inScope filter | S | |
| 4.7.3 | Component tests: scope switcher hidden for non-PP; sidebar pin persistence; multi-sort stacks correctly | M | |
| 4.7.4 | Smoke / e2e: full Add-member wizard with mocked ContractIntelService and seeded contract text β user lands in directory with referral link | M |
5. Suggested migration sequence β
Backend-first for anything model- or role-shaped (Β§0 explains why). Each step is its own PR on master, branch named feature/gac-NN-<short-desc>.
- Backend foundation β OpenAPI additions for
Node,Referral; extendUser(node, referredBy, photoUrl); extendMandate(R2 keys forpdf/md/json); add'Principal Partner'to the membership-role enum and tolibs/auth/jwt.goand to Supabase JWT issuance. Issue a PP JWT to Yannick. Ship Go handlers (stubs OK initially). PR 1. - Compensation policy backend (Β§4.8.1β4.8.3) β migration + Go endpoints + OpenAPI; seed placeholder policy v1 (100% member). PR 2.
- Orval regen + Angular foundation (Β§4.1) β regenerate
@core/api/model, addroles.ts'Principal Partner'+PRINCIPAL_VIEWER_ROLES, shipScopeService+NavPinService. PR 3. - Admin layout shell + sidebar reorg (Β§4.2 + Β§4.9.1) β icon-rail sidebar with pin, topbar scope switcher (hidden unless principal viewer), new Framework & System group with redirects from old paths. PR 4.
- User / mandate schema extensions (Β§4.5.1β4.5.3, 4.5.5) β edit
entities/users.tsandentities/mandates.tsCrudSchemas. PR 5. - Member admin overview + nodes panel + directory (Β§4.3.1β4.3.4, 4.3.7). PR 6.
- Multi-sort + referral badges (Β§4.3.4 polish, Β§4.3.5) β can ride with PR 6 if small. Optional PR 7.
- Python sidecar
apps/pdf-convert/(Β§4.4.6a) β FastAPI + Docling + Dockerfile + Nx wiring + shared-token auth. Deployed to staging before PR 9 lands. PR 8. - Go contract ingest endpoint + R2 wiring (Β§4.4.6b) β
POST /api/v1/contracts/ingest, R2 client config,contracttable migration, calls sidecar. PR 9. - Mandate editor as full page + documents (Β§4.5.4) β uses the same R2 ingest path. PR 10.
- Go contract-extract endpoint (Β§4.4.7) β Claude call, writes
extract.json. PR 11. - Add-member wizard UI (Β§4.4.1β4.4.5, 4.4.8, 4.4.9) β wires ingest + extract + live
CompensationService. PR 12. - Compensation policy editor page (Β§4.8.4) under
/framework/compensation. PR 13. - Look-and-feel polish + tests (Β§4.6, Β§4.7). PR 14.
Linear status per [[linear-status-workflow]]: local done β In Review when the PR opens β Done only when merged to master.
6. Open questions β resolved 2026-06-18 β
| # | Question | Decision |
|---|---|---|
| 1 | Compensation waterfall formula | Stored in DB and editable. Tables in Β§4.8. Seed placeholder policy = 100% member; real numbers backfilled via the editor (Β§4.8.4) once defined. |
| 2 | Referral commission | No referrer share at this time. Schema supports it (beneficiary enum includes referrer) so a future policy version can introduce one without migrations. |
| 3 | PDFβMD + contract-extract pipeline | Go orchestrates; Python sidecar converts; Go calls Claude. New apps/pdf-convert/ (FastAPI + Docling) exposes POST /convert; Go POST /contracts/ingest calls it and stores R2; Go POST /ai/contract-extract does the Claude call. Three R2 artifacts per contract: original.pdf, text.md, extract.json (re-opens never re-call Claude). Python chosen over Go because best-in-class PDFβMD libs (Docling, Marker) are Python-only and conversion quality matters. |
| 4 | Candidate entity | Dropped. Existing pending_users covers it. |
| 5 | Document storage | Cloudflare R2. PDF + MD + JSON per contract under contracts/{mandate_id}/. |
| 6 | Principal Partner rollout | Backend-first (PR 1). Yannick gets the PP JWT. Damien stays Admin β Admin has all privileges, so PP rollout doesn't block Damien's access to anything. |
| 7 | Admin UI for compensation policy | Yes, in this migration. Under new Framework & System sidebar group (Β§4.9). |
| 8 | Sidebar grouping | Framework & System is a new group containing Compensation policies (new), Nodes (moved), Skills catalogue (moved). |
7. Out of scope (explicit non-goals) β
- Member-facing desktop / mobile shells β visually unchanged between v1 and v2; no work here.
- Connection indicator, PWA, fonts, theme tokens β unchanged.
- Validation queue, registration wizard, Dispatch AI chat β unchanged.
- Skills CSV import β still deferred ("for now ignore" per STATUS).
- Real Google Workspace OAuth β still simulated in the reference; the Angular app's existing auth posture is unaffected by this migration.
8. Linear ticketing suggestion β
One ticket per row in Β§4.1βΒ§4.7, parented under a new project "Admin v2 migration". Branch names per CLAUDE.md convention: feature/gac-NN-<short-desc> (e.g. feature/gac-NN-member-admin-overview). Status flow per [[linear-status-workflow]]: local done β In Review β merged to master β Done.
Source of truth for everything above: the diff between design-system/Dispatch Design System - v1/ and design-system/Dispatch Design System - v2/, with primary references to v2 design.md Β§5.3 + Β§6 and v2 templates/admin/admin.js (SCHEMA, NODE_SEED, REFERRAL_SEED, CANDIDATE_SEED, SAMPLE_CONTRACT, memberCategory, loadScope/setScope/inScope, toggleNavPin).