Skip to content

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):

ConcernPathPattern in use
Admin pagesapps/web/src/app/features/admin/pages/One standalone component per page (dashboard-page.ts, users-page.ts, mandates-page.ts, …)
Admin CRUD schemasapps/web/src/app/features/admin/entities/One CrudSchema<T> per entity β€” users.ts, mandates.ts, organizations.ts, tasks.ts
Shared CRUD engineapps/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 / rolesapps/web/src/app/core/auth/roles.ts defines the Role union + MANAGER_ROLES; role.guard.ts gates routes
API clientapps/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
Backendapps/api/ (Go) + libs/auth/ (Go JWT verifier)New entities, new roles, and any AI proxy endpoint live here
Member appapps/web/src/app/features/member/Out of scope for this migration

Two consequences of the real layout that change the work:

  1. Role is server-validated. libs/auth/jwt.go validates the membership_role JWT claim against the same string union as roles.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.
  2. 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:orval or whatever the workspace target is β€” check orval.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 ​

Areav1v2Migration class
Storage versiondispatch_admin_v3dispatch_admin_v4Schema / API
Entitiesorgs, mandates, tasks, votes, users, skills+ nodes, referrals, candidatesNew domain models
User fieldsrole, function, org, balance, skills+ node (FK to node), + referredBy, + photo URL, role adds Principal PartnerModel extension
SidebarStatic dark sidebarIcon-rail collapsible with pin (localStorage dispatch_nav_pinned) and hover-tooltip on collapsed itemsLayout / component
TopbarSearch Β· bell Β· New+ viewer-scope switcher (Principal vs node-lead view), persisted to sessionStorage dispatch_admin_scopeComponent + guard
DashboardMandates + People & activity+ Member admin panel (KPIs Β· Nodes panel Β· sortable / multi-sort Member directory Β· referral relationships Β· Add-member CTA)New panel + features
Add-member flowSingle modalWizard with live Claude contract intelligence (parse pasted mandate letter β†’ extract rate, scope, member) + compensation waterfall previewNew flow + AI service
Users tableSortable single columnMulti-sort (additive sort with shift/cmd), member directory shows Node, Referrer, Category (PP/MP/AP)Table behavior
Member categoriesMembership role onlyDerived PP / MP / AP category (Principal Partner / Managing Partner / Associate Partner) + Dormant flag derived from activityDerived helpers
Account photosInitials avatarPhoto URL pulled at registration (Gravatar/pravatar in prototype, real Google Workspace photo in prod)Auth integration
Mandate formModalFull-page editor (editPage:true) with documents/file-dropComponent refactor
Mandates fieldrate, 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 @theme block
  • 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) ​

#ItemNotesSize
4.1.1Node { 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.2Referral { fromUserId, toUserId, date } model + ReferralService; derive referredBy on UserMirrors REFERRAL_SEEDS
4.1.3Candidate model β€” dropped. The existing pending_users flow (registrations awaiting MP merge) already covers the v2 CANDIDATE_SEED case.β€”β€”
4.1.4Extend 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.5Extend Mandate model: add documents: MandateDocument[] where MandateDocument = { name, size, url? }File upload contract needs backend coordinationM
4.1.6ScopeService (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 itM
4.1.7MemberCategory helper + isDormant helper as pure functions (or computed() on user)Mirrors v2 memberCategory, categoryLabel, isDormantS
4.1.8NavPinService: pinned: signal<boolean> synced to localStorage dispatch_nav_pinned; browser-only via afterNextRenderS

4.2 Layout β€” Admin shell ​

#ItemNotesSize
4.2.1<admin-sidebar> standalone component β€” icon-rail (collapsed default), expands on hover, locked open when pinned; pin button toggles NavPinServiceUse DaisyUI classes; bespoke styles already exist as .adm-sidebar / .adm-pin / .pin-on / .pin-off in bespoke.css β€” port the rulesM
4.2.2Tooltip on collapsed sidebar items (DaisyUI tooltip tooltip-right)S
4.2.3<admin-topbar>: page title + search input + <scope-switcher> + notifications bell + contextual New buttonM
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.5Role 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.6Update routes: add /dashboard/members, /dashboard/members/add (wizard), /nodes, /candidates (if backend exposes), keep all existingS

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.

#ItemNotesSize
4.3.1<member-admin-overview> β€” KPI strip: total members, active, dormant, awaiting verification, average competenceKPIs 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, actionsM
4.3.4Multi-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 sortMirror v2 sortTableBy(i, additive) / sortRows. Implement as a SortStack signal { col, dir }[] consumed by a computed() sorted view.M
4.3.5Referral relationships sub-view: graph or tree of referredBy → user; minimal v1 = flat "Recruited by X" badges; richer v2 = a small SVG tree per clusterStart with badges; promote to tree only if user asksS→M
4.3.6Add-member CTA β†’ opens /dashboard/members/add wizard (see Β§4.4)S
4.3.7Wire scope: every panel in Β§4.3 must respect ScopeService.viewScope β€” pure consumers, no scope logic of their ownS

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:

  1. Browser uploads PDF β†’ Go POST /api/v1/contracts/ingest.
  2. Go writes original.pdf to R2, calls Python sidecar POST /convert (multipart or signed R2 URL), Python returns markdown, Go writes text.md to R2, returns { contractId, pdfKey, markdownKey }.
  3. Wizard POSTs { contractId } to Go POST /api/v1/ai/contract-extract. Go fetches text.md from R2, calls Claude (Anthropic Go SDK), writes extract.json to R2, returns the structured object.
  4. 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.

#ItemNotesSize
4.4.1<add-member-wizard> standalone component, 3 steps: Member basics β†’ Contract & compensation β†’ Review & createM
4.4.2Step 1: Full name, workspace email, node, function, role (defaults Member Partner), referredBy (typeahead over existing users)S
4.4.3Step 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.4Step 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.5Step 3: read-only review + Create β†’ posts to UserService.create() + MandateService.create() with the R2 keys; links referral; opens new user's profileS
4.4.6aPython 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.6bGo 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.7Go 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.8ContractIntelService (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.9CompensationService (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.

#ItemNotesSize
4.8.1DB 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.2Go 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.3OpenAPI additions + Orval regenS
4.8.4Compensation 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:

GroupItems
OverviewDashboard
ManageOrganizations, Mandates, Tasks
Governance & PeopleVotes, Users
ReviewValidation 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)
#ItemNotesSize
4.9.1Update <admin-sidebar> to render the new group; update routes with redirects from old paths (/nodes β†’ /framework/nodes, /skills β†’ /framework/skills)S
4.9.2Compensation editor page (Β§4.8.4) wired at /framework/compensationcovered in 4.8.4

4.5 Users / mandates CRUD updates ​

#ItemNotesSize
4.5.1User form: add Node select (or "β€” (Principal / all nodes)" for cross-node users), add Membership role option Principal PartnerS
4.5.2Users table: add Node column (with nodeLabel(id) formatter); muted italic "all nodes" when nullS
4.5.3User profile drawer: show photo, category chip, referredBy, node, plus existing skills/competence blockS
4.5.4Mandate editor: convert from modal to full-page route /mandates/:id/edit; add <mandate-documents> file-drop component with drag-and-drop, size formatterM
4.5.5Avatar helper: avatar(user) returns photo <img> if photoUrl, else initials in colored chip β€” used in every table/profile that shows a memberS

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:

#ItemNotesSize
4.6.1Port .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.2Verify zero raw hex / Npx regressions (v2 keeps the adherence ruleset)S
4.6.3Verify both dispatch and dispatch-dark themes look correct on every new surfaceS

4.7 Tests & verification ​

#ItemNotesSize
4.7.1Unit tests for memberCategory, isDormant, clampCoef, voteApproval, CompensationService.waterfallS
4.7.2Unit test for ScopeService persistence + inScope filterS
4.7.3Component tests: scope switcher hidden for non-PP; sidebar pin persistence; multi-sort stacks correctlyM
4.7.4Smoke / e2e: full Add-member wizard with mocked ContractIntelService and seeded contract text β†’ user lands in directory with referral linkM

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>.

  1. Backend foundation β€” OpenAPI additions for Node, Referral; extend User (node, referredBy, photoUrl); extend Mandate (R2 keys for pdf/md/json); add 'Principal Partner' to the membership-role enum and to libs/auth/jwt.go and to Supabase JWT issuance. Issue a PP JWT to Yannick. Ship Go handlers (stubs OK initially). PR 1.
  2. Compensation policy backend (Β§4.8.1–4.8.3) β€” migration + Go endpoints + OpenAPI; seed placeholder policy v1 (100% member). PR 2.
  3. Orval regen + Angular foundation (Β§4.1) β€” regenerate @core/api/model, add roles.ts 'Principal Partner' + PRINCIPAL_VIEWER_ROLES, ship ScopeService + NavPinService. PR 3.
  4. 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.
  5. User / mandate schema extensions (Β§4.5.1–4.5.3, 4.5.5) β€” edit entities/users.ts and entities/mandates.ts CrudSchemas. PR 5.
  6. Member admin overview + nodes panel + directory (Β§4.3.1–4.3.4, 4.3.7). PR 6.
  7. Multi-sort + referral badges (Β§4.3.4 polish, Β§4.3.5) β€” can ride with PR 6 if small. Optional PR 7.
  8. 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.
  9. Go contract ingest endpoint + R2 wiring (Β§4.4.6b) β€” POST /api/v1/contracts/ingest, R2 client config, contract table migration, calls sidecar. PR 9.
  10. Mandate editor as full page + documents (Β§4.5.4) β€” uses the same R2 ingest path. PR 10.
  11. Go contract-extract endpoint (Β§4.4.7) β€” Claude call, writes extract.json. PR 11.
  12. Add-member wizard UI (Β§4.4.1–4.4.5, 4.4.8, 4.4.9) β€” wires ingest + extract + live CompensationService. PR 12.
  13. Compensation policy editor page (Β§4.8.4) under /framework/compensation. PR 13.
  14. 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 ​

#QuestionDecision
1Compensation waterfall formulaStored in DB and editable. Tables in Β§4.8. Seed placeholder policy = 100% member; real numbers backfilled via the editor (Β§4.8.4) once defined.
2Referral commissionNo referrer share at this time. Schema supports it (beneficiary enum includes referrer) so a future policy version can introduce one without migrations.
3PDF→MD + contract-extract pipelineGo 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.
4Candidate entityDropped. Existing pending_users covers it.
5Document storageCloudflare R2. PDF + MD + JSON per contract under contracts/{mandate_id}/.
6Principal Partner rolloutBackend-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.
7Admin UI for compensation policyYes, in this migration. Under new Framework & System sidebar group (Β§4.9).
8Sidebar groupingFramework & 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).