Fibretrace Monet docs/Lovable sync 2026-08-19/Port plan

Lovable sync port-plan

origin/main HEAD: f0d02a2866a25dbd15e9a055cb786e41b3c58405 Status: clean git-first pull (baselineProvisional: false, degraded: false). No degraded-run warning needed this pull. 21 change-units resolved: 3 design-system rebrand units (2 independent files), 3 pure-UI PORT units with no BE dependency, 1 PARTIAL PORT unit, and 14 REVIEW units (11 of which also resolved GAP/PARTIAL against real Fibretrace REST).

Porting itself has NOT happened yet. Everything below is a plan for a later, deliberate port — this step only understands and plans.


Design-system rebrands (do first)

The design-system watchlist (apps/dashboard/src/index.css, tailwind.config.ts, apps/dashboard/src/components/ui/button.tsx) was checked in full this pull: index.css and tailwind.config.ts show no diff — the whole rebrand surface this window is button.tsx, tabs.tsx, and a brand-new table-pagination.tsx, all using Tailwind arbitrary-value utility classes rather than named tokens, so the “token deltas” below are exact class-string diffs, not tailwind.config.ts entries.

Three input units cover this same surface from different angles — design-system-button-tabs-pagination is the umbrella description, completeness-button-tsx and completeness-tabs-tsx are the per-file completeness-pass detail. They are merged below into 2 file-scoped units so button.tsx and tabs.tsx/table-pagination.tsx each land exactly once.

1. Port button.tsx cva rebrand — merges design-system-button-tabs-pagination + completeness-button-tsx (priority: high)

  • Target files: apps/dashboard/src/components/ui/button.tsx
  • Token deltas:
    • Add a new tertiary variant: bg-[#F1F2F4] text-primary border-0 hover:bg-[#E4E6EA] (neutral pill, for modal CTAs).
    • Restyle the existing table variant: was bg-muted/50 border-0 → now a white pill: bg-white border border-neutral-200 rounded-[12px] text-neutral-700.
    • Bump base radius: rounded-[8px]rounded-[10px].
    • Unify CTA height to h-9 across default/sm/lg/icon sizes — monet currently mixes h-[42px]/h-8/h-10.
    • Switch the action variant from monet’s own green bg-action to upstream’s bg-primary.
  • Why: Malcolm: “these top modals you have added on nominations, ready to claim etc. make the CTAs our tertiary cta” — generalized by Lovable into a full neutral-pill pass across the table button variant, and picked up separately by the completeness scan, which also surfaced the base-radius/sizing/action-color drift that predates this window (open since 2026-06-28, f093043b).
  • Caution: this is the same button.tsx targeted by unit 2’s table-pagination.tsx nav buttons (which consume the table variant) — land this unit first, or at minimum in the same PR.

2. Port tabs.tsx pill restyle + create table-pagination.tsx — merges design-system-button-tabs-pagination + completeness-tabs-tsx (priority: high)

  • Target files: apps/dashboard/src/components/ui/tabs.tsx, apps/dashboard/src/components/ui/table-pagination.tsx (NEW FILE)
  • Token deltas:
    • tabs.tsx TabsList shell: bare rounded-md/py-4 → segmented h-10 p-1 bg-neutral-100/90 rounded-xl shell.
    • tabs.tsx TabsTrigger active state: gray/blue tint → white pill (bg-white shadow-sm text-neutral-900).
    • table-pagination.tsx (does not exist in monet at all): a shared pagination primitive — prev/next nav buttons (use Button variant="table", rounded-[12px]), numbered page pills, page-size select. Lovable tried and dropped a variant="mac" prop before settling on this same white/border-neutral-200 pill look. Used by 20+ Lovable pages: Claims, Facilities, SDU, Scanners, Team, admin/*, SettingsPartners, and more.
  • Why: Malcolm’s prompt asked to align the Active/Archived segmented pill’s height/visual weight with the adjacent search bar and action buttons — a sizing tune-up that landed as a broader tabs + pagination visual-language unification, applied platform-wide rather than to one screen.
  • Depends on: unit 1 landing first — table-pagination.tsx’s nav buttons render via Button variant="table", so the pill look is only correct once that variant is restyled.
  • Note: table-toolbar-and-shared-pagination-rollout (see Needs review) coordinates TableToolbar.tsx on top of this same table-pagination.tsx component — land this unit before that one.

Ready to port (EXISTS / no data)

PORT units that are pure UI/routing with no backing-table dependency at all — no apiSpec was needed for any of the three.

3. Move Partners to main nav, out of Settings — partners-nav-and-map-refactor (priority: medium)

  • Target files: apps/dashboard/src/components/AppSidebar.tsx, apps/dashboard/src/App.tsx, apps/dashboard/src/hooks/usePartners.ts, apps/dashboard/src/components/po/ImportPoCsvModal.tsx, apps/dashboard/src/pages/LinkToPo.tsx, apps/dashboard/src/pages/NewEvidenceRequest.tsx, apps/dashboard/src/components/ShareSessionModal.tsx, apps/dashboard/src/pages/RetailerSupplyChain.tsx
  • Resolved endpoint: none needed — data keeps flowing through the already-wired company_partners mapper / partnersMonetApi; this is pure routing.
  • portGuidance: Promote Partners from a nested Settings sub-item to a top-level /partners main-nav item; keep /settings/partners as a Navigate redirect for old links. Update AppSidebar.tsx’s nav array, move the route out of the SettingsLayout block in App.tsx, and fix ~8 hardcoded /settings/partners references across usePartners.ts notifications and the listed pages/components. Lovable’s PartnerMap.tsx data-format rework and company/country filter are OUT OF SCOPE — monet has no PartnerMap component or Map/Connected/Unclaimed tabs yet.
  • Why: Malcolm asked that clicking Partners in the main nav not jump the user into a different nav area (Settings) — “keep partners on the main nav, remove from settings area … make sense?”

4. Polish SDU page: API-key CTA label + CSV actions dropdown — sdu-page-refactor (priority: low)

  • Target files: apps/dashboard/src/pages/SDU.tsx
  • Resolved endpoint: none — no new data reads/writes, purely presentational reorg plus one copy change.
  • portGuidance: 5 sub-changes: (1) remove the KPI/telemetry banner, (2) rename the API-key CTA “Click here” → “Manage API keys”, (3) drop the unit-count from the page subtitle, (4) move the “Demo SDU” button into the toolbar’s extra slot, (5) merge FibreProductionTab’s “Sample CSV” + “Import CSV” buttons into a single MoreHorizontal dropdown (Export stays standalone).
  • Why: Malcolm’s Aug-19 instruction was a blanket “replicate all of today’s edits consistently across every persona/tier” inside Lovable itself — not a monet-port directive; it explains why this cosmetic polish landed, not that it specifically targets monet.

5. Refresh supply-chain page copy and align confidence chip to ConfidenceBadge — supply-chain-tier-explorer-polish (priority: low)

  • Target files: apps/dashboard/src/pages/RetailerSupplyChain.tsx
  • Resolved endpoint: none — reuses the already-existing shared ConfidenceBadge component (already wired elsewhere in monet).
  • portGuidance: (1) reword the chain description and the “You know your Tier 1…” headline, (2) swap the inline confidence-score chip for the shared ConfidenceBadge, (3) rename partner-invite nav targets from /settings/partners to /partners. Monet still ships the pre-refactor copy and a local ConfidenceChip; it never received the upstream SupplyChainTierExplorer/TierVisibilityTable component split at all — this unit does not attempt that split, only the copy/chip/link changes.
  • Depends on: unit 3 (partners-nav-and-map-refactor) — the /partners route this unit links to doesn’t exist until that unit lands.
  • Why: no Lovable MCP message matched this cluster. Inferred from diff: tightening supply-chain copy tone/clarity and converging the tier-explorer’s confidence chip onto the same component used across the rest of the app, as visual-consistency follow-through after a home-dashboard refresh.

Blocked / BE-gap

PORT/REVIEW units whose resolved apiSpec.integrationStatus is GAP or PARTIAL. Each carries the beSpecForGap to file with BE. Where a UI shell is safely portable now against apps/dashboard/src/lib/apiAdapter.ts’s standing empty-read / toast-guarded-write convention, that is called out explicitly per unit.

6. Port ClaimPosition HIG redesign and reservation-history logic — claim-position-page-rewrite (PORT, priority: high, status: PARTIAL)

  • Target files: apps/dashboard/src/pages/ClaimPosition.tsx, apps/dashboard/src/pages/ClaimPositionList.tsx, apps/dashboard/src/App.tsx, apps/dashboard/src/hooks/useProgrammes.ts, apps/dashboard/src/components/ExploreFibrePrograms.tsx
  • portGuidance: Do NOT rewrite apiAdapter.ts’s MAPPERS["fibre_programmes"]/["programme_reservations"] in place. Add a dedicated fibreProgramsMonetApi.ts on the existing services/monetBaseApi.ts instance (/api/monet/v1 is a real, separately-authed namespace, not a generic Supabase-shim table): getFibrePrograms, getFibreProgram(id), getClaimPositions (list), getClaimPosition(id) for the 6-stage funnel, and postReservationEnquiry({programId, additional_mt, notes}) — verify Sanctum session+XSRF plumbing before wiring the mutation live (reads use Bearer JWT; writes need cookie auth per DEV probe). Rewrite useProgrammes.ts/useProgrammeDetail to source Ceiling/Activated/Reserved/Available/Pending/Claimed from the funnel response instead of summing programme_reservations/production_claims rows client-side — this directly implements the ticket’s “Activated = claimed+available capped by reservation” redefinition, since the funnel is the server’s own authoritative computation. Keep AddReservationModal’s client-side expiry-preview math (?? 18 fallback) as presentational only until BE confirms default_reservation_term_months is returned; swap its fake insert for postReservationEnquiry and match ProgrammeDetail’s existing “enquiry sent, pending approval” async framing, not Lovable’s instant “Reservation added” toast — hard requirement, since the real endpoint is async+admin-approved. The reservation-history timeline, program_record_items panels, and PO-raised KPI have no backing at all: render them behind the standing apiAdapter.ts empty-read convention (empty timeline, no wording panels, PO KPI tile hidden or “—”) until BE ships the items below. Merge ClaimPositionList into ClaimPosition per the existing plan in .local/lovable/sync-20260818/port-plan.md unit 3 (delete ClaimPositionList.tsx + its /claim-position-list route). ExploreFibrePrograms.tsx is net-new — build it against getFibrePrograms but flag to the user that today’s endpoint only returns already-joined programmes; do not claim discovery support until BE confirms/adds an explore-scope filter.
  • beSpecForGap: Real BE covers the AGGREGATE side (6-stage capacity funnel + programme roster) via /api/monet/v1, not the per-row/detail side the UI needs:
    1. Reservation history rows — no GET exists. .from("programme_reservations").select("id, company_id, quantity_mt, notes, expires_at, created_at, updated_at, created_by_user_id").eq("programme_id", id).order("created_at",{ascending:false}). Needs GET /api/monet/v1/claim-positions/{program_id}/reservations (paginated). Vu’s collection proves the row-level resource exists server-side (PATCH /program-reservations/{id}/approve|reject) but no matching GET was captured.
    2. expires_at computation — client currently computes created_at + fibre_programmes.default_reservation_term_months months; real backend has no expires_at concept anywhere (confirmed 2026-08-18). BE decides: (a) return expires_at per approved reservation, server-computed, or (b) fold expiry exclusion into the funnel’s Reserved number server-side.
    3. default_reservation_term_months.from("fibre_programmes").select("default_reservation_term_months").eq("id", progId).maybeSingle(). Confirm GET /api/monet/v1/fibre-programs/{id} returns this column — never positively verified.
    4. program_record_items (wording_supported/wording_notyet sections) — zero backing anywhere. Net-new table program_record_items(id, programme_id, section enum, title, body, sort_order, visible, created_by, updated_at) + CRUD + reorder endpoints.
    5. PO-raised KPI — .from("purchase_orders").select("volume_mt").eq("company_id", cid).eq("programme_id", id).eq("archived", false). Separate PO domain, explicitly confirmed still uncovered. Needs a programme-scoped PO aggregate, e.g. GET /api/monet/v1/claim-positions/{program_id}/po-summary.
    6. ExploreFibreProgramsGET /api/monet/v1/fibre-programs is Tenant-scoped (“programs where caller’s company has a program_participants row”) — returns programs you’re ALREADY in, not a discovery feed. BE must confirm/add ?scope=explore or a public directory + join-request action.
  • mismatch: the real backend exposes only an aggregate, tenant-scoped, session-approved reservation workflow (summed 6-stage funnel + write-only “enquiry to sales” awaiting separate admin approve/reject) with no expires_at anywhere, whereas Lovable models reservations as flat, directly-insertable, per-row-queryable records with a client-computed expiry — so the reservation-history timeline, expiry chips, program_record_items, and PO-raised KPI have no real GET/POST counterpart, and the “list programmes” endpoint is scoped to programmes already joined, not a browse-unjoined feed.
  • Why: Prompt: “claim position page: Refactor the Claim position dashboard page to follow Apple HIG / macOS desktop layout principles while strictly adhering to our brand ty[pography]…” — a visual/UX modernization pass, with reservation-expiry and activated-capacity logic tightened as part of the same pass.

7. Scope the missing ActionSignals widget before porting — claims-hero-banner-redesign (REVIEW, priority: medium, status: GAP)

  • Cluster: “Your next actions” ActionSignals widget redesign (live claim-proposal + overdue signals, purchase_order_lines remaining-units tracking) on Home/PO sub-pages, plus the orphaned ClaimMomentHero hero banner on ReadyToClaim.
  • portGuidance: Port as UI-shell only, matching the existing pattern in usePurchaseOrders.ts (useProposedClaimsByPo already stubbed to {} with a “BE-blocked” comment at line 271-279). Add MAPPERS["purchase_orders"]/["purchase_order_verifications"] noop stubs (already in ALL_TABLES) and add the brand-new purchase_order_lines table to ALL_TABLES/apiAdapter.ts as a noop so the remaining-units query doesn’t crash. Add nullable linking_due_at to the PurchaseOrder interface. Do NOT port usePartners.ts as-is — rewrite it first to call useGetPartnersQuery/useGetPendingPartnersQuery from partnersMonetApi.ts (drop the requester/addressee/.or() model, no monet equivalent) before ActionSignals depends on it. Port ClaimMomentHero.tsx verbatim now — zero data risk; wire its props to ReadyToClaim.tsx’s already-computed kpis.ready/kpis.verifiedVol (ReadyToClaim.tsx:80-87).
  • beSpecForGap: 5 Lovable-only Supabase tables with zero Fibretrace REST equivalent: (1) purchase_orders (id, company_id, partner_company_id, po_reference, volume_mt, volume_claimed_mt, status enum, archived, notes, created_at, NEW linking_due_at); (2) purchase_order_lines (NEW — purchase_order_id, quantity, quantity_claimed, excluded); (3) production_claims (id, purchase_order_id, status[proposed|rejected|…], quantity_mt, notes, rejection_reason, rejected_at, proposed_by_user_id, created_at, company_id); (4) purchase_order_verifications (purchase_order_id FK); (5) company_partners invite sub-slice (requester/addressee/status/responded_at) — NOTE this one partially overlaps a real endpoint (GET /api/monet/v1/partners/pending-invitations already exists), only the bidirectional requester/addressee/accepted-history semantics are unbacked.
  • mismatch: the widget’s new logic reads 4 Lovable-only tables with zero REST backing anywhere (canvas, sat, backend-specs all confirm); separately the shared partner-invite signal calls usePartners.ts, which is dead/broken code (unsupported .or() query) even though the real pending-invitations endpoint already exists and is used correctly elsewhere (SettingsPartners.tsx).
  • Why: no Lovable MCP message matched this cluster directly (see apiSpec.feature note) — see full evidence in the API-resolution record above.

8. Reconcile Claims bulk-approval/table redesign gap before porting HIG polish — claims-table-bulk-approval-polish (REVIEW, priority: medium, status: GAP)

  • Cluster: BulkClaimApprovalModal.tsx rewrite (collapsible “Unchanged” bucket + select-all, sticky footer, inset alert banner, denser rows) + continued HIG polish on Claims.tsx (KPI stat cards, right-aligned toolbar, DropdownMenu row-action, clickable claim-ref chip, “mac” pagination variant). Two legacy snapshots (Claims.legacy.tsx, BulkClaimApprovalModal.legacy.tsx) are dead code, not imported anywhere — do not port them.
  • portGuidance: Treat both target files as pure UI/visual ports on top of the EXISTING Bucket-B empty-state stubs already in apiAdapter.ts (MAPPERS["production_claims"] line 235, MAPPERS["fibre_programmes"] lines 178-183, both no-op → []) — do not extend those mappers, there is no BE resource behind them. Gate Approve/bulk-confirm behind a disabled control + “backend pending” toast, matching the existing convention. useProgrammes.ts (lines 170-191, 369-389) computing available_to_claim_mt/my_reserved_mt/my_claimed_mt from the same stubbed tables is the correct real endpoint to back this once BE confirms GET /api/monet/v1/claim-positions/{program_id} returns real data for a real participant — track as a narrower follow-up, not part of this unit.
  • beSpecForGap: Full domain (purchase_orders, purchase_order_lines, purchase_order_verifications, production_claims, production_claim_lines, production_claim_sessions, purchase_order_evidence_packs, public_verification_tokens) with exact queries from usePurchaseOrders.ts/usePoProposalReview.ts/BulkClaimApprovalModal.tsx/Claims.tsx (commit f0d02a28) — see full column lists and 6 numbered query blocks in the working notes; BE should decide whether to extend /api/monet/v1/ (has claims create + claim-positions read, no PO sub-resource) with a full purchase-orders + propose/confirm + bulk-approve domain, or drop the two-party propose/confirm UX in favor of the simpler existing single-step /claims submit.
  • mismatch: Lovable models a multi-step, two-party PO-based claim workflow (PO + lines/verifications + production_claims.status proposed→confirmed + per-line allocation snapshots + client-side batch confirm loop) with zero Fibretrace backing; the only real claims endpoint (POST /api/monet/v1/claims) is single-step/single-party with no propose/review/approve stages and no list endpoint.
  • Why: micro-polish pass on the “Bulk review and claim” modal — alert-banner alignment, collapsible unchanged-items group with select-all, sticky footer — continuing days of HIG refinement after Malcolm caught and reverted a functional regression in nomination action rules.

9. Flag CU→Credit Units copy sweep as blocked on unported feature — credit-units-terminology-sweep (REVIEW, priority: low — no apiSpec, tracked here since it depends on other GAP units)

  • Lovable ran a 4-pass copy-only sweep (Aug 17-19) spelling out “CU”/“CP units” as “Credit Units” across Home, HomeManufacturer, OwnerDashboard, SDU, ReadyToClaim, and PurchaseOrders — even at Lovable HEAD the rename is itself incomplete (lib/claimUnits.ts, CuStat.tsx, fmtCu()/fmtCuLocal() still say “CU”). Not gated on BE by itself, but several of its target pages (ReadyToClaim, PurchaseOrders/Nominations, OwnerDashboard) are themselves GAP units below (units 12, 14, 15) — apply the copy sweep alongside whichever of those units ships, not standalone, so the wording doesn’t drift out of sync again.
  • Why: Malcolm kept spotting leftover “CU”/“CP units” abbreviations across several review passes and asked Lovable to spell them out for clarity — a plain copy fix, not a functional change.

10. Scope Ready-to-Claim page rebuild before porting HIG rewrite — ready-to-claim-rewrite (REVIEW, priority: medium, status: GAP)

  • Cluster: ReadyToClaim.tsx rewritten to an Apple HIG/macOS layout (net +185/-143), pre-rewrite version archived as ReadyToClaim.legacy.tsx. New layer: ClaimMomentHero banner, segmented pill filter nav (Ready/Requested/Confirmed/All) replacing the KPI-tile grid, icon-only single-record CTA, a redesigned over-capacity banner wired to a new AddReservationModal.
  • portGuidance: src/lib/claimUnits.ts and src/components/CuStat.tsx are safe verbatim ports now (props-only, no data accessors); ClaimMomentHero.tsx takes {ready, verifiedVolMt} as props from the page’s existing kpis memo — no new accessor. Reuse the EXISTING hooks already scaffolded (useMyPurchaseOrders, usePoPacksByPo, useProposedClaimsByPo, useConfirmEvidenceClaim, useProgrammes) rather than adding raw supabase.from() calls — they already degrade safely to []/no-op-toast. For AddReservationModal.tsx: do NOT wire .insert("reservation_requests") literally — the fake supabase client throws MutationNotImplementedError for any table’s insert, and reservation_requests isn’t even in apiAdapter.ts’s ALL_TABLES yet. Add a useCreateReservationRequest() mutation hook that toasts “Reservation requests - backend pending” and no-ops (same pattern as useProposeEvidenceClaim/useRejectEvidenceClaim).
  • beSpecForGap: 4 Lovable-only domains as one BE epic (“Programmes + Claims”): (1) NEW reservation_requests table (programme_id, company_id, requested_by_user_id, requested_by_email, quantity_cu, expires_at, status) + an admin review queue at /admin/reservation-requests; (2) fibre_programmes/programme_participants/programme_producers/programme_producer_facilities/programme_reservations — already documented BE-blocked (.local/backend-specs/integration-audit/BE-ASK.md:36,353-400 has draft DDL, reuse it); (3) purchase_orders/purchase_order_verifications/purchase_order_evidence_packs — already in apiAdapter.ts ALL_TABLES but never mapped, fall through to noop; (4) production_claims/production_claim_sessions confirm/reject lifecycle — already explicitly-disabled stubs in usePurchaseOrders.ts:319-353.
  • mismatch: Fibretrace REST has no concept of programmes, capacity reservations, purchase orders, or claim-confirmation at all — this is 4-5 entire Supabase-only domains the BE has never modeled, confirmed by both the frozen canvas inventory and apiAdapter.ts’s own noop/MutationNotImplementedError stubs.
  • Why: Prompt: “ready to claim page - same: Refactor the Ready to Claim page to follow Apple HIG / macOS desktop layout principles while preserving our brand colors and data.”

11. Review scope for Nominations/PO HIG rewrite before any port — nominations-purchase-orders-rewrite (REVIEW, priority: medium, status: GAP)

  • Cluster: “Nominations” (Purchase Orders) dashboard rewritten from Tabs+icon-button to HIG-style: segmented pill tabs (Active/Archived), ready/proposed statuses routed to Ready-to-Claim, icon-only DropdownMenu overflow actions, redesigned KPI tiles/reservation bar, plus ripple polish across CreatePoModal, ImportPoCsvModal, LinkVerificationsToPoModal, PoDetailsModal, PoLineAllocationTable, PoLinesTable, LinkToPo.tsx, PurchaseOrderDetail.tsx. Old page kept as PurchaseOrders.legacy.tsx for rollback. usePurchaseOrders.ts also renames notification copy PO→Nomination and adds an admin-only claim-revoke mutation.
  • portGuidance: Spec-capture only this pull — nothing here is BE-backed, so there is no endpoint to map yet. Extend the existing BE-blocked-stub convention (useProposeEvidenceClaim/useConfirmEvidenceClaim/useRejectEvidenceClaim: no-op + “backend pending” toast) to every PO write, including the new admin claim-revoke mutation. Fix first: apiAdapter.ts’s FilterBuilder (line 678) defines no .insert()/.update()/.delete(), so supabase.from('purchase_orders') writes currently throw TypeError instead of degrading gracefully — migrate those call sites onto disabled-mutation hooks rather than patch the adapter. Treat PurchaseOrderDetail.tsx, PoLinesTable, PoLineAllocationTable as empty-state shells; don’t invent a po_lines schema — the local Lovable clone predates this feature (needs a fresh git fetch to pull the actual new table/column shape before this becomes a complete BE spec).
  • beSpecForGap: purchase_orders (id, company_id, partner_company_id, po_reference, sku, programme_id, volume_mt, volume_claimed_mt, status, archived, notes, created_by_user_id, created_at) with insert/update/delete call sites; purchase_order_verifications (id, purchase_order_id, session_id, linked_by_company_id, linked_by_user_id, linked_at) with insert/delete for link/unlink sessions; purchase_order_evidence_packs (id, purchase_order_id, claim_id, volume_mt, is_partial, created_by_user_id); production_claims insert + NEW admin-only revoke update; notifications fire-and-forget insert for “Nomination shared/ready/claim proposal received”. useProposedClaimsByPo/usePoPacksByPo are already explicitly commented “BE-blocked” in usePurchaseOrders.ts, confirming this gap predates the HIG rewrite.
  • mismatch: the entire domain has zero Laravel endpoint anywhere — confirmed BE-blocked in integration-audit/audit.json and independently by apiAdapter.ts’s default no-op fallthrough for all four tables; the closest real resource (/user/notifications REST, canvas notoficationApi.ts) only supports GET/mark-read/bulk-accept-reject-export, not client POST of arbitrary rows; Fibretrace’s real /orders API is an unrelated domain (“Orders” == “Collections”).
  • Why: Malcolm/owner prompt (uncertain, exploratory): “nominations page: im not 100% sure on these changes, but lets try them… Refactor the Nominations dashboard page to match a lightweight [HIG] layout” — kept old version reachable as a rollback path.

12. Reconcile persona-split home dashboards before porting macOS refresh — home-dashboards-t0-macos-refresh (REVIEW, priority: high, status: PARTIAL)

  • Cluster: all four home/overview dashboards (Retailer Tier-0, Manufacturer, Producer, generic Home) get a coordinated macOS/Apple-HIG restyle: neutral-toned cards, rounded-2xl shadows, condensed KPI tiles, shared “next actions” panel with category tabs. Bundled in: /settings/partners/partners rename, a new “proposed” claim status, MT→CU unit display, a company-ux-resolving loading skeleton, and (Manufacturer only) brand-new linking/scan-coverage KPI tiles backed by facility/scanner-assignment/verification-session data.
  • portGuidance: Split in two. (1) Manufacturer KPI tiles for facilities + scanner coverage are backend-ready TODAY — reuse the already-wired MAPPERS['facilities'] (apiAdapter.ts:533) and scannersMonetApi.getScanners (GET /api/monet/v1/scanners), grouping scanners by current_facility.id client-side; do NOT wire scanner_facility_assignments (permanently superseded) and do not build a new endpoint. (2) Everything PO/claim/persona-driven (useIncomingPurchaseOrders, usePoLinesForPos, usePoPacksByPo, useProposedClaimsByPo, isRetailerTier0/isManufacturer/isProducer, the “next actions” panel, the “proposed” status) is backend-blocked — keep reading through supabase.from(...) so the noop stubs degrade to empty-state; file the beSpecForGap below rather than inventing an adapter now. useCompanyUxResolving needs no BE work: compose from useCurrentUser() + the already-wired useCompanyDetails() isLoading flags. MT→CU and the /settings/partners/partners rename are pure FE, no adapter change. All real endpoints above are under /api/monet/v1/ (monetBaseApi), a separate base from canvas’s /api/v1/ — do not inject monet endpoints into baseApi.ts.
  • beSpecForGap: purchase_orders (extended with linking_due_at, proposed status value); purchase_order_verifications; purchase_order_evidence_packs; production_claims (NEW proposed status + proposed_by_user_id/rejected_by_user_id/rejection_reason/confirmed_at/confirmed_by_user_id); persona-source tables programme_producers/programme_participants/company_details.company_type (isManufacturer is derived purely from purchase_orders.partner_company_id having rows, no separate table); full verification-session join (useVerificationSessions) — already documented BE-blocked in apiAdapter.ts (“no Fibretrace endpoints provide the full Lovable join hierarchy”).
  • mismatch: the facility/scanner/company half is cleanly backed (fat-object equivalents already exist), but the entire PO→claim(now incl. “proposed”)→evidence-pack pipeline plus the programme tables driving persona detection and the full verification-session join have zero Laravel endpoint — confirmed as literal {fetch: noop} stubs today, matching the 2026-05-29 BE-ASK.md gap list with no sat or canvas endpoint ever covering this domain.
  • Why: Malcolm asked for a clean Apple HIG/macOS desktop look across all home dashboards, homes-then-inner-pages scope, explicitly wanting the edits easy to revert — a coordinated visual restyle pass, not a refactor.

13. Rescope Owner Dashboard/Uptake HIG refactor — owner-dashboard-program-uptake (REVIEW, priority: high, status: GAP)

  • Cluster: Owner Dashboard + Program Uptake refactored to Apple HIG/macOS: tabular-nums, slate chart bars, CU-labeled KPI tiles, categorized/paginated “Next actions” inbox with urgency tabs, a demand-vs-supply warning driven by new purchase_orders/purchase_order_lines nomination data, a new “unlinked CU” participant column. OwnerProgramUptake also gained table pagination + shared ConfidenceBadge. OwnerProgramRecord/OwnerDocuments got minor spacing/pill restyles only.
  • portGuidance: Extend, never rewrite, apiAdapter.ts. Add purchase_order_lines to ALL_TABLES (currently absent) as a noop stub beside the already-stubbed purchase_orders/purchase_order_verifications/purchase_order_evidence_packs (never overridden). Port the HIG shell reading via unchanged supabase.from() calls so it degrades to hidden/empty state per the standing Bucket-B pattern (same treatment already applied to admin-owner-pages-misc and purchase-orders-shipping-data). Reuse ConfidenceBadge.tsx (already wired in AdminCompanies/Verifications/SettingsPartners) instead of OwnerProgramUptake’s local ConfidencePill. Build table-pagination.tsx on top of the existing shadcn Radix primitive at components/ui/pagination.tsx — this is the SAME shared component as unit 2 above; make one, reuse both places. Port claimUnits.ts as a pure display formatter (quantity_mt*1000=CU), no adapter change. File a BE ticket for a real purchase-orders/nomination resource rather than fat-object-collapsing onto claim-positions, which is aggregate-only with no per-PO/line breakdown.
  • beSpecForGap: purchase_orders (id, company_id FK, programme_id FK, status — enum unconfirmed post-2026-08-18 “Purchase Order → Nomination rename” + 2 Claim-Unit SQL migrations landed after the 2026-06-24 schema snapshot); purchase_order_lines (NEW, not present anywhere in the schema snapshot: purchase_order_id FK, quantity, quantity_claimed, cu_total, excluded). Derived client-side: demand = sum(quantity) over non-excluded lines of open POs per programme, compared against programme supply capacity (itself sourced from the equally-GAP programme_producers/production_records/claim-positions tables). Compounding, already-tracked GAPs for the same unit (sync-20260817 admin-owner-pages-misc): production_records, production_claims-as-LIST, programme_reservations-as-LIST, program_documents+assignments, program_certificates+assignments — all noop stubs already.
  • mismatch: Fibretrace has no purchase-order/nomination resource at all — the closest real backing is the single pre-aggregated GET /api/monet/v1/claim-positions/{program_id} 6-stage funnel per programme, not a queryable list of per-company/per-line PO demand rows the UI can sum and compare against supply.
  • Why: Malcolm asked to refactor Program Uptake to Apple HIG/macOS standards while preserving all existing data/charts/tables/functionality, and to reuse the AdminCompanies layout for a redesigned Owner Dashboard home with a CU-based next-actions inbox and a new nomination-demand signal.

14. Reconcile reservation-expiry logic with monet’s diverged reservation flow — reservation-term-expiry-negative-allowance (REVIEW, priority: medium, status: GAP)

  • Cluster: per-reservation expiry date (18-month default per programme, admin-overridable), excludes expired CU from usable/available capacity while keeping it visible as history; AddReservationModal’s submit switched from a simulated toast to a real insert into a NEW reservation_requests staging table (with computed expiry preview); a separate fix for a category-name truncation bug in CuEstimator’s select trigger (pure UI, portable now, no BE dependency).
  • portGuidance: Do not build a fake immediate insert. Reuse monetBaseApi.ts; add fibreProgramsMonetApi.ts with postReservationEnquiry({programId, additional_mt, notes})POST /claim-positions/{id}/reservations/enquiry (needs Sanctum session+XSRF, not just Bearer). Wire ClaimPosition.tsx’s “Add reservation” CTA and a ported AddReservationModal to that mutation with async “enquiry to sales” copy, not Lovable’s instant “Reservation added”. Leave MAPPERS["fibre_programmes"]/["programme_reservations"] stubbed — no expiry/history read exists to back them. Keep the ?? 18 client fallback for term months. Rewrite useProgrammes.ts’s capacity math off /claim-positions/{id}'s funnel once BE ships it, not a client-side per-row expiry filter. Hide expiry badges and the negative-allowance override until BE responds. The CuEstimator truncation fix has no data dependency — port it now.
  • beSpecForGap: Two parts. (1) NEW reservation_requests table: insert {programme_id, company_id, requested_by_user_id, requested_by_email, quantity_cu, expires_at} — reviewed via an implied /admin/reservation-requests queue (no GET/PATCH shape captured yet). (2) NEW expires_at column + ledger semantics on programme_reservations (already Bucket-B/BE-blocked): capacity roll-up read needs expires_at to exclude expired rows from live capacity while a separate per-programme history read must keep showing expired rows. Per Lovable’s own iteration, this must be a per-ADJUSTMENT ledger (“each individual adjustment logged separately, its own expiry date”), and quantity_mt/quantity_cu must accept negative values (reduction adjustments) that still render correctly in history.
  • mismatch: the real /api/monet/v1/ reservation surface has no expires_at/term concept anywhere, no tenant-readable per-row list of a company’s own programme_reservations (only a summed funnel), and no reservation_requests staging table; “Add reservation” on the real backend is a write-only enquiry that emails sales rather than persisting a row, and the only endpoints that DO persist a reservation are admin-only, session-authed, single reserved_mt overrides — not a signed, individually-expiring, negative-adjustment-capable ledger entry.
  • Why: Malcolm reconsidered reservation logic and asked for the individual Programme’s Timeline section to show the expiry date / history of reservations, sourced from the reservation term.

15. Scope certificates-modal HIG refactor against missing base feature — certificates-scope-modal-hig-refactor (REVIEW, priority: medium — no apiSpec, pure visual)

  • Lovable restyled ScopeCertificateModal.tsx to Apple HIG/macOS-sheet conventions (segmented holder picker replacing radio boxes, pill icon swap Award→Bookmark, new input/button classes, rounded-xl surfaces) plus matching toolbar/table/badge touch-ups in SettingsCertificates.tsx/OwnerCertificates.tsx. No data-logic or field changes — this can port now as a pure visual pass, unrelated to the already-tracked certificates-module GAP (state.json known_unported, company-wide certificates CRUD) which remains blocked separately.
  • Why: Malcolm’s explicit Tier-1 request: refactor the “add Certificates” modal to Apple HIG/macOS sheet principles while preserving all existing fields and brand styling tokens.

16. Reconcile Home.tsx loading-state fix with monet’s partial persona-hook migration — company-ux-loading-state-fix (REVIEW, priority: medium, status: PARTIAL)

  • Cluster: new useCompanyUxResolving() in useCompanyUx.ts (true while the company_details company-type query + usePersonaContext() are in flight); Home.tsx renders a skeleton while it’s true. Unrelated fix riding along: “Invite partner” link /settings/partners/partners.
  • portGuidance: Reuse usePersonaContext.ts as-is (already mirrors Lovable’s version, runs through the apiAdapter shim, hits the real GET /api/monet/v1/company for company_details). For useCompanyUxResolving(), call useCompany.ts’s useCompanyDetails() (same RTK Query cache) or replicate Lovable’s exact ["company-type", companyId] key, then gate with companyQuery.isPending || persona.isPending || !currentUser. IMPORTANT: monet’s current Home.tsx is a much older version with no useCompanyUx.ts, no HomeManufacturer/HomeProducer/HomeRetailerTier0 variant split, and imports usePersona (not usePersonaContext) — porting this cleanly needs either (a) first landing the larger unported Home.tsx variant-split refactor (unit 12 above), or (b) scoping down to wrap monet’s existing usePersona()/isProducer results with an equivalent isPending check, without importing Lovable’s full useCompanyUx.ts. For the “Invite partner” link: monet has no top-level /partners route yet — do not port the literal /partners path until unit 3 (partners-nav-and-map-refactor) lands.
  • beSpecForGap: 3 of the 4 accessors this feature composes on hit noop stubs: programme_producers/programme_participants (feeds isProducer, confirmed BE-blocked, apiAdapter.ts:230-231, DDL already proposed in BE-ASK.md:36,~356-417) and purchase_orders.partner_company_id/archived (feeds isManufacturer, no MAPPERS["purchase_orders"] override, falls to generic noop). company_details.company_type is real and already wired.
  • mismatch: useCompanyUxResolving()'s persona half of the gate will “resolve” almost immediately but with fabricated-empty data from the noop stubs — the loading-state UX fix works mechanically, but the persona classification it guards (“flash of the wrong dashboard”) isn’t backed by real data yet.
  • Why: no MCP match. Inferred: prevent the legacy/default dashboard from flashing briefly on refresh before the correct persona-specific home layout resolves.

17. Scope Verifications HIG chip/layout refactor before porting — verifications-scan-history-refactor (REVIEW, priority: medium, status: GAP)

  • Cluster: pure visual restyle of Verifications (scan-history): shadcn Tabs→segmented-pill tabs, variant="table" buttons, rounded-2xl/shadow-sm cards, hero KPI restyle, ConnectionStatusChip gains a compact prop, ConfidenceBadge swapped for an inline dot+pill, material_direction Badge swapped for an inline arrow-icon chip. One riding data-behavior change: a notification link field regression in useEvidenceRequests.ts (deep link → "/").
  • portGuidance: The visual restyle touches no API surface — port directly against the existing useVerificationSessions/useVerificationFacilities hooks, which already read real Fibretrace-backed tables unrelated to evidence requests. The link regression lives entirely in useEvidenceRequests.ts, which still imports the raw Lovable/Supabase demo client directly (not apiAdapter.ts) and is registered as a noop stub for both evidence_requests and notifications — i.e. zero live wiring today, so do NOT “fix” the link value against a real endpoint; there is none yet. Do not resurrect a client-push notification path — NotificationsContext.tsx:124-126 already correctly no-ops addNotification (“server is the source of truth”). Carry the Lovable diff verbatim into useEvidenceRequests.ts (disconnected demo code either way) and log the BE spec below.
  • beSpecForGap: evidence_requests (id, requester_company_id, recipient_company_id, programme_id, quantity_mt, scans_requested, sku, order_reference, lot, notes, status enum, created_at, responded_at, created_by_user_id) with full read/write query set; evidence_request_shares (evidence_request_id, session_id, shared_by_user_id); a client-writable notifications insert with an arbitrary link field (title, message, link: /evidence-requests/{id}) — this whole shape has no Fibretrace analog. BE must define /evidence-requests CRUD per BE-ASK.md:573-608 and synthesize the “New evidence request” notification server-side as part of that POST.
  • mismatch: Fibretrace has no evidence_requests/evidence_request_shares resource at all, and Fibretrace notifications are entirely server-generated read-only objects with no link field and no client-facing create endpoint — Lovable’s “client inserts an arbitrary notifications row with a deep link” pattern has no real-backend equivalent to even mismap onto.
  • Why: Malcolm: “verification records - again make these changes easy to revert: Refactor the Verification Records page to follow Apple HIG / macOS desktop layout principles” — iterative macOS-style polish, explicitly requested to stay easily revertible.

18. Coordinate TableToolbar redesign with Button ‘table’ variant port — table-toolbar-and-shared-pagination-rollout (REVIEW, priority: medium — no apiSpec, pure UI)

  • TableToolbar.tsx adds compact (36px control heights) and a deprecated no-op searchRight prop, moves search inside the right-hand action group so the whole toolbar is always right-aligned, switches Filters/Columns/Export buttons to variant="table", restyles the active-filter chip from filled to outline, widens button max-width, tightens bottom padding to sit flush above the new shared pagination row. Depends directly on unit 1 (button.tsx table variant) and unit 2 (table-pagination.tsx) landing first — this unit is the toolbar-side half of the same rollout.
  • Why: Malcolm asked Lovable to “use our defined table pagination here instead of what is currently being used” — TableToolbar was reshaped alongside the new shared TablePagination component so pagination lives outside the table body with consistent search+pagination placement across list pages.

19. Move Partners out of Settings into main nav; scope out admin-only nav additions — app-routing-sidebar-nav-reorg (REVIEW, priority: medium, status: GAP)

  • Cluster: the Partners relocation itself (/settings/partners/partners with redirect, removed from Settings nav) has NO mismatch — pure FE routing over the already-EXISTS partners endpoint, and should be folded into unit 3 above rather than duplicated. The GAP is in the rest of this bundle: sidebar “dot” badges become numeric pending-count badges on Ready-to-Claim/Link-to-PO (driven by usePoPacksByPo/useProposedClaimsByPo), a small admin-set siteSubText label beside the logo, and (out of scope for this unit, admin-only) new AdminHome/AdminGuide/AdminOverrides/AdminReservationRequests routes + an admin/programmesadmin/programs rename.
  • portGuidance: Partners move — see unit 3, zero API work. Badges: port the presentation change now (NavItem dot?: boolean | number, numeric pill capped “99+”) since it degrades safely, but leave count computation on the current stubs so badges always show 0/hidden until BE ships (tracked BE-ASK items 9 and 11). siteSubText: do not build a fake key/value table read — stub useSiteSubText to return "" (hides the label, safe default) and hand BE a 1-field ask: add site_sub_text: string | null to BasicConfigInterface/get-basic-config (read-only in canvas today) plus confirm/build the admin write endpoint (Lovable’s useSetSiteSubText has no Laravel write counterpart at all).
  • beSpecForGap: same purchase_orders/production_claims/purchase_order_evidence_packs shape as units 7/8/13 above (badge-count hooks hardcode return {} directly in usePurchaseOrders.ts:271-290); NEW generic app_settings key/value table (key, value jsonb, updated_at) suggested for siteSubText, OR extend BasicConfigInterface/get-basic-config directly (preferred — avoids a generic KV escape hatch on a fixed ~40-field config object).
  • mismatch: the two badge-driving hooks and their upstream PO data have zero Fibretrace REST equivalent, confirmed absent from canvas/sat and the most recent BE-delivered contract (monet-dashboard-api-v1.md, 2026-06-23); siteSubText similarly has no home in the real get-basic-config response.
  • Why: Malcolm’s stated prompt was “add pagination and search to the admin/programmes page and rename the page url programs” — purely about the admin Programmes page; the Partners nav relocation, sidebar badge-count change, and siteSubText addition all rode along in the same commit window without being the stated intent.

20. Scope the public-claim/CU-unit catch-up before porting — public-claim-fibre-creation-copy (REVIEW, priority: medium, status: GAP)

  • Cluster: public claim-verification page reshaped (QR code via qrcode.react, Section/KvTable layout, a new “Claim Units (CU)” concept replacing MT display); FibreCreation’s production-UOM dropdown tweaked + a multi-program dropdown selector replacing prev/next arrows; /settings/partners/partners link renames in two files. Collections/SignupStatus got only spacing/row-height touches.
  • portGuidance: Do not extend apiAdapter.ts for public_verification_tokens/production_claims/fibre_programmes — its mappers dispatch against the authenticated baseApi/session, wrong for a token-gated public route. Route PublicClaim.tsx OUTSIDE <ProtectedRoute> in App.tsx (same pattern as the already-wired /verify/:guidCollectionPreview.tsx, which degrades gracefully today since its backing tables are noop stubs). Until BE ships production_claims + fibre_programmes + a public token-resolution endpoint, port PublicClaim.tsx as a stub that always renders “not found” — do not wire real reads. When BE ships, add a new unauthenticated RTK service (publicClaimApi.ts, no baseApi auth headers) mirroring companiesPublicApi.ts. The CU/MT conversion and multi-programme dropdown are downstream of the same fibre_programmes GAP — no separate BE ask beyond the /programs endpoint already tracked.
  • beSpecForGap: public_verification_tokens (id, token UNIQUE, claim_id FK nullable, session_id FK nullable, pack_type, enabled, created_at); production_claims (id, programme_id, company_id, quantity_mt, status, created_at); fibre_programmes; companies (real BE surface exists for companies alone). All 4 unauthenticated queries chained by token→claim→programme→company. Suggested shape: ONE public GET /api/v2/public/claim-tokens/{token} resolving token → {claim_ref, programme_name, quantity_mt, created_at, pack_type, company_name, status}, plus the token-issuing side POST /api/v2/production-claims/{id}/public-token (authed, company-scoped).
  • mismatch: Fibretrace REST has no unauthenticated/public read surface at all — every real endpoint requires an authenticated session — whereas this feature is fundamentally a no-auth, token-gated public page; the two domain tables it depends on don’t exist as BE resources yet even for authenticated calls, so there is no data to expose publicly regardless of the auth question.
  • Why: Malcolm asked Lovable to “update the view public version so its aligned with the Fibre verification record” — the public claim page was reworked to mirror the internal EvidencePackDetail/Section-KvTable layout; the CU-unit and UOM-dropdown edits are incidental follow-on polish from that same alignment pass.

Needs review

All 14 REVIEW units are listed above under “Blocked / BE-gap” (11 of them, units 7-8, 10-14, 16-20) or “Ready to port” (none this pull — every REVIEW unit either resolved GAP/PARTIAL or, for units 9/15/18, has no apiSpec because it is copy/visual-only but still carries a cross-unit dependency worth a human nod before porting). The 3 without an apiSpec — unit 9 (credit-units-terminology-sweep), unit 15 (certificates-scope-modal-hig-refactor), unit 18 (table-toolbar-and-shared-pagination-rollout) — are the ones that most need a quick human decision rather than a BE ticket: they are low-risk to port but each depends on another unit’s sequencing (9 on 10/14/13’s pages, 18 on units 1-2’s design-system rebrand). Route all 14 through a human pass before execution; none of them get auto-ported by this plan.


Cursor

  • last_ported_sha auto-advanced to 2b52a6089ef7462bffb2cd845f256931d09be893 — the earliest not-yet-ported PORTABLE unit this pull is design-system-button-tabs-pagination, whose first commit is f3b4bddd347ffa3949aa80c8c7444afb0911da98; its verified linear PARENT (git rev-parse f3b4bddd~1 in the clone) is 2b52a6089ef7462bffb2cd845f256931d09be893. Everything chronologically before that parent is either verified ALREADY_DONE/SKIP (proven present in apps/dashboard) or BE-blocked and recorded in known_unported below — nothing is silently skipped, since the two BE-blocked units the cursor passes over this run (public-claim-fibre-creation-copy, reservation-term-expiry-negative-allowance) are now tracked in state.json’s known_unported list.
  • allPortableDone is false — only the 3 pure-UI port units + the 2 merged design-system rebrands are unconditionally ready this pull; claim-position-page-rewrite resolved PARTIAL and the remaining 14 review units resolved GAP/PARTIAL or need a scoping decision.
  • suggested_ported_sha = f0d02a2866a25dbd15e9a055cb786e41b3c58405 (this pull’s origin/main HEAD) — the full advance to accept once every READY unit (1-5) in this plan is actually ported.

After porting all READY units, accept the full advance:

python3 .claude/skills/monet-dashboard-lovable/scripts/state.py set-ported f0d02a2866a25dbd15e9a055cb786e41b3c58405