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
tertiaryvariant:bg-[#F1F2F4] text-primary border-0 hover:bg-[#E4E6EA](neutral pill, for modal CTAs). - Restyle the existing
tablevariant: wasbg-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-9acrossdefault/sm/lg/iconsizes — monet currently mixesh-[42px]/h-8/h-10. - Switch the
actionvariant from monet’s own greenbg-actionto upstream’sbg-primary.
- Add a new
- 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
tablebutton 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.tsxtargeted by unit 2’stable-pagination.tsxnav buttons (which consume thetablevariant) — 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.tsxTabsListshell: barerounded-md/py-4→ segmentedh-10 p-1 bg-neutral-100/90 rounded-xlshell.tabs.tsxTabsTriggeractive 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 (useButton variant="table",rounded-[12px]), numbered page pills, page-size select. Lovable tried and dropped avariant="mac"prop before settling on this same white/border-neutral-200pill 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 viaButton variant="table", so the pill look is only correct once that variant is restyled. - Note:
table-toolbar-and-shared-pagination-rollout(see Needs review) coordinatesTableToolbar.tsxon top of this sametable-pagination.tsxcomponent — 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_partnersmapper /partnersMonetApi; this is pure routing. - portGuidance: Promote Partners from a nested Settings sub-item to a top-level
/partnersmain-nav item; keep/settings/partnersas aNavigateredirect for old links. UpdateAppSidebar.tsx’s nav array, move the route out of theSettingsLayoutblock inApp.tsx, and fix ~8 hardcoded/settings/partnersreferences acrossusePartners.tsnotifications and the listed pages/components. Lovable’sPartnerMap.tsxdata-format rework and company/country filter are OUT OF SCOPE — monet has noPartnerMapcomponent 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 singleMoreHorizontaldropdown (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
ConfidenceBadgecomponent (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/partnersto/partners. Monet still ships the pre-refactor copy and a localConfidenceChip; it never received the upstreamSupplyChainTierExplorer/TierVisibilityTablecomponent 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/partnersroute 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’sMAPPERS["fibre_programmes"]/["programme_reservations"]in place. Add a dedicatedfibreProgramsMonetApi.tson the existingservices/monetBaseApi.tsinstance (/api/monet/v1is a real, separately-authed namespace, not a generic Supabase-shim table):getFibrePrograms,getFibreProgram(id),getClaimPositions(list),getClaimPosition(id)for the 6-stage funnel, andpostReservationEnquiry({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). RewriteuseProgrammes.ts/useProgrammeDetailto source Ceiling/Activated/Reserved/Available/Pending/Claimed from the funnel response instead of summingprogramme_reservations/production_claimsrows 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. KeepAddReservationModal’s client-side expiry-preview math (?? 18fallback) as presentational only until BE confirmsdefault_reservation_term_monthsis returned; swap its fake insert forpostReservationEnquiryand 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_itemspanels, and PO-raised KPI have no backing at all: render them behind the standingapiAdapter.tsempty-read convention (empty timeline, no wording panels, PO KPI tile hidden or “—”) until BE ships the items below. MergeClaimPositionListintoClaimPositionper the existing plan in.local/lovable/sync-20260818/port-plan.mdunit 3 (deleteClaimPositionList.tsx+ its/claim-position-listroute).ExploreFibrePrograms.tsxis net-new — build it againstgetFibreProgramsbut 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:- 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}). NeedsGET /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. expires_atcomputation — client currently computescreated_at + fibre_programmes.default_reservation_term_months months; real backend has noexpires_atconcept anywhere (confirmed 2026-08-18). BE decides: (a) returnexpires_atper approved reservation, server-computed, or (b) fold expiry exclusion into the funnel’s Reserved number server-side.default_reservation_term_months—.from("fibre_programmes").select("default_reservation_term_months").eq("id", progId).maybeSingle(). ConfirmGET /api/monet/v1/fibre-programs/{id}returns this column — never positively verified.program_record_items(wording_supported/wording_notyet sections) — zero backing anywhere. Net-new tableprogram_record_items(id, programme_id, section enum, title, body, sort_order, visible, created_by, updated_at)+ CRUD + reorder endpoints.- 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. ExploreFibrePrograms—GET /api/monet/v1/fibre-programsis Tenant-scoped (“programs where caller’s company has aprogram_participantsrow”) — returns programs you’re ALREADY in, not a discovery feed. BE must confirm/add?scope=exploreor a public directory + join-request action.
- Reservation history rows — no GET exists.
- 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_atanywhere, 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”
ActionSignalswidget redesign (live claim-proposal + overdue signals,purchase_order_linesremaining-units tracking) on Home/PO sub-pages, plus the orphanedClaimMomentHerohero banner on ReadyToClaim. - portGuidance: Port as UI-shell only, matching the existing pattern in
usePurchaseOrders.ts(useProposedClaimsByPoalready stubbed to{}with a “BE-blocked” comment at line 271-279). AddMAPPERS["purchase_orders"]/["purchase_order_verifications"]noop stubs (already inALL_TABLES) and add the brand-newpurchase_order_linestable toALL_TABLES/apiAdapter.tsas a noop so the remaining-units query doesn’t crash. Add nullablelinking_due_atto thePurchaseOrderinterface. Do NOT portusePartners.tsas-is — rewrite it first to calluseGetPartnersQuery/useGetPendingPartnersQueryfrompartnersMonetApi.ts(drop the requester/addressee/.or()model, no monet equivalent) beforeActionSignalsdepends on it. PortClaimMomentHero.tsxverbatim now — zero data risk; wire its props toReadyToClaim.tsx’s already-computedkpis.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, NEWlinking_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_partnersinvite sub-slice (requester/addressee/status/responded_at) — NOTE this one partially overlaps a real endpoint (GET /api/monet/v1/partners/pending-invitationsalready 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.featurenote) — 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.tsxrewrite (collapsible “Unchanged” bucket + select-all, sticky footer, inset alert banner, denser rows) + continued HIG polish onClaims.tsx(KPI stat cards, right-aligned toolbar,DropdownMenurow-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) computingavailable_to_claim_mt/my_reserved_mt/my_claimed_mtfrom the same stubbed tables is the correct real endpoint to back this once BE confirmsGET /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 fromusePurchaseOrders.ts/usePoProposalReview.ts/BulkClaimApprovalModal.tsx/Claims.tsx(commitf0d02a28) — see full column lists and 6 numbered query blocks in the working notes; BE should decide whether to extend/api/monet/v1/(hasclaimscreate +claim-positionsread, 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/claimssubmit. - mismatch: Lovable models a multi-step, two-party PO-based claim workflow (PO + lines/verifications +
production_claims.statusproposed→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.tsxrewritten to an Apple HIG/macOS layout (net +185/-143), pre-rewrite version archived asReadyToClaim.legacy.tsx. New layer:ClaimMomentHerobanner, 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 newAddReservationModal. - portGuidance:
src/lib/claimUnits.tsandsrc/components/CuStat.tsxare safe verbatim ports now (props-only, no data accessors);ClaimMomentHero.tsxtakes{ready, verifiedVolMt}as props from the page’s existingkpismemo — no new accessor. Reuse the EXISTING hooks already scaffolded (useMyPurchaseOrders,usePoPacksByPo,useProposedClaimsByPo,useConfirmEvidenceClaim,useProgrammes) rather than adding rawsupabase.from()calls — they already degrade safely to[]/no-op-toast. ForAddReservationModal.tsx: do NOT wire.insert("reservation_requests")literally — the fakesupabaseclient throwsMutationNotImplementedErrorfor any table’s insert, andreservation_requestsisn’t even inapiAdapter.ts’sALL_TABLESyet. Add auseCreateReservationRequest()mutation hook that toasts “Reservation requests - backend pending” and no-ops (same pattern asuseProposeEvidenceClaim/useRejectEvidenceClaim). - beSpecForGap: 4 Lovable-only domains as one BE epic (“Programmes + Claims”): (1) NEW
reservation_requeststable (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-400has draft DDL, reuse it); (3)purchase_orders/purchase_order_verifications/purchase_order_evidence_packs— already inapiAdapter.tsALL_TABLESbut never mapped, fall through to noop; (4)production_claims/production_claim_sessionsconfirm/reject lifecycle — already explicitly-disabled stubs inusePurchaseOrders.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/MutationNotImplementedErrorstubs. - 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
DropdownMenuoverflow actions, redesigned KPI tiles/reservation bar, plus ripple polish acrossCreatePoModal,ImportPoCsvModal,LinkVerificationsToPoModal,PoDetailsModal,PoLineAllocationTable,PoLinesTable,LinkToPo.tsx,PurchaseOrderDetail.tsx. Old page kept asPurchaseOrders.legacy.tsxfor rollback.usePurchaseOrders.tsalso 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’sFilterBuilder(line 678) defines no.insert()/.update()/.delete(), sosupabase.from('purchase_orders')writes currently throwTypeErrorinstead of degrading gracefully — migrate those call sites onto disabled-mutation hooks rather than patch the adapter. TreatPurchaseOrderDetail.tsx,PoLinesTable,PoLineAllocationTableas empty-state shells; don’t invent apo_linesschema — the local Lovable clone predates this feature (needs a freshgit fetchto 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_claimsinsert + NEW admin-only revoke update;notificationsfire-and-forget insert for “Nomination shared/ready/claim proposal received”.useProposedClaimsByPo/usePoPacksByPoare already explicitly commented “BE-blocked” inusePurchaseOrders.ts, confirming this gap predates the HIG rewrite. - mismatch: the entire domain has zero Laravel endpoint anywhere — confirmed BE-blocked in
integration-audit/audit.jsonand independently byapiAdapter.ts’s default no-op fallthrough for all four tables; the closest real resource (/user/notificationsREST, canvasnotoficationApi.ts) only supports GET/mark-read/bulk-accept-reject-export, not client POST of arbitrary rows; Fibretrace’s real/ordersAPI 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→/partnersrename, 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) andscannersMonetApi.getScanners(GET /api/monet/v1/scanners), grouping scanners bycurrent_facility.idclient-side; do NOT wirescanner_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 throughsupabase.from(...)so the noop stubs degrade to empty-state; file the beSpecForGap below rather than inventing an adapter now.useCompanyUxResolvingneeds no BE work: compose fromuseCurrentUser()+ the already-wireduseCompanyDetails()isLoading flags. MT→CU and the/settings/partners→/partnersrename 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 intobaseApi.ts. - beSpecForGap:
purchase_orders(extended withlinking_due_at,proposedstatus value);purchase_order_verifications;purchase_order_evidence_packs;production_claims(NEWproposedstatus +proposed_by_user_id/rejected_by_user_id/rejection_reason/confirmed_at/confirmed_by_user_id); persona-source tablesprogramme_producers/programme_participants/company_details.company_type(isManufacturer is derived purely frompurchase_orders.partner_company_idhaving rows, no separate table); full verification-session join (useVerificationSessions) — already documented BE-blocked inapiAdapter.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-29BE-ASK.mdgap 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_linesnomination data, a new “unlinked CU” participant column.OwnerProgramUptakealso gained table pagination + sharedConfidenceBadge.OwnerProgramRecord/OwnerDocumentsgot minor spacing/pill restyles only. - portGuidance: Extend, never rewrite,
apiAdapter.ts. Addpurchase_order_linestoALL_TABLES(currently absent) as a noop stub beside the already-stubbedpurchase_orders/purchase_order_verifications/purchase_order_evidence_packs(never overridden). Port the HIG shell reading via unchangedsupabase.from()calls so it degrades to hidden/empty state per the standing Bucket-B pattern (same treatment already applied toadmin-owner-pages-miscandpurchase-orders-shipping-data). ReuseConfidenceBadge.tsx(already wired in AdminCompanies/Verifications/SettingsPartners) instead ofOwnerProgramUptake’s localConfidencePill. Buildtable-pagination.tsxon top of the existing shadcn Radix primitive atcomponents/ui/pagination.tsx— this is the SAME shared component as unit 2 above; make one, reuse both places. PortclaimUnits.tsas 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 ontoclaim-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-GAPprogramme_producers/production_records/claim-positionstables). Compounding, already-tracked GAPs for the same unit (sync-20260817admin-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 NEWreservation_requestsstaging table (with computed expiry preview); a separate fix for a category-name truncation bug inCuEstimator’s select trigger (pure UI, portable now, no BE dependency). - portGuidance: Do not build a fake immediate insert. Reuse
monetBaseApi.ts; addfibreProgramsMonetApi.tswithpostReservationEnquiry({programId, additional_mt, notes})→POST /claim-positions/{id}/reservations/enquiry(needs Sanctum session+XSRF, not just Bearer). WireClaimPosition.tsx’s “Add reservation” CTA and a portedAddReservationModalto that mutation with async “enquiry to sales” copy, not Lovable’s instant “Reservation added”. LeaveMAPPERS["fibre_programmes"]/["programme_reservations"]stubbed — no expiry/history read exists to back them. Keep the?? 18client fallback for term months. RewriteuseProgrammes.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. TheCuEstimatortruncation fix has no data dependency — port it now. - beSpecForGap: Two parts. (1) NEW
reservation_requeststable: insert{programme_id, company_id, requested_by_user_id, requested_by_email, quantity_cu, expires_at}— reviewed via an implied/admin/reservation-requestsqueue (no GET/PATCH shape captured yet). (2) NEWexpires_atcolumn + ledger semantics onprogramme_reservations(already Bucket-B/BE-blocked): capacity roll-up read needsexpires_atto 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”), andquantity_mt/quantity_cumust accept negative values (reduction adjustments) that still render correctly in history. - mismatch: the real
/api/monet/v1/reservation surface has noexpires_at/term concept anywhere, no tenant-readable per-row list of a company’s ownprogramme_reservations(only a summed funnel), and noreservation_requestsstaging 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, singlereserved_mtoverrides — 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.tsxto 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 inSettingsCertificates.tsx/OwnerCertificates.tsx. No data-logic or field changes — this can port now as a pure visual pass, unrelated to the already-trackedcertificates-moduleGAP (state.jsonknown_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()inuseCompanyUx.ts(true while thecompany_detailscompany-type query +usePersonaContext()are in flight);Home.tsxrenders a skeleton while it’s true. Unrelated fix riding along: “Invite partner” link/settings/partners→/partners. - portGuidance: Reuse
usePersonaContext.tsas-is (already mirrors Lovable’s version, runs through the apiAdapter shim, hits the realGET /api/monet/v1/companyforcompany_details). ForuseCompanyUxResolving(), calluseCompany.ts’suseCompanyDetails()(same RTK Query cache) or replicate Lovable’s exact["company-type", companyId]key, then gate withcompanyQuery.isPending || persona.isPending || !currentUser. IMPORTANT: monet’s currentHome.tsxis a much older version with nouseCompanyUx.ts, no HomeManufacturer/HomeProducer/HomeRetailerTier0 variant split, and importsusePersona(notusePersonaContext) — 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 existingusePersona()/isProducerresults with an equivalentisPendingcheck, without importing Lovable’s fulluseCompanyUx.ts. For the “Invite partner” link: monet has no top-level/partnersroute yet — do not port the literal/partnerspath 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(feedsisProducer, confirmed BE-blocked, apiAdapter.ts:230-231, DDL already proposed inBE-ASK.md:36,~356-417) andpurchase_orders.partner_company_id/archived(feedsisManufacturer, noMAPPERS["purchase_orders"]override, falls to generic noop).company_details.company_typeis 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,ConnectionStatusChipgains a compact prop,ConfidenceBadgeswapped for an inline dot+pill,material_directionBadge swapped for an inline arrow-icon chip. One riding data-behavior change: a notificationlinkfield regression inuseEvidenceRequests.ts(deep link →"/"). - portGuidance: The visual restyle touches no API surface — port directly against the existing
useVerificationSessions/useVerificationFacilitieshooks, which already read real Fibretrace-backed tables unrelated to evidence requests. Thelinkregression lives entirely inuseEvidenceRequests.ts, which still imports the raw Lovable/Supabase demo client directly (notapiAdapter.ts) and is registered as a noop stub for bothevidence_requestsandnotifications— 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-126already correctly no-opsaddNotification(“server is the source of truth”). Carry the Lovable diff verbatim intouseEvidenceRequests.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-writablenotificationsinsert with an arbitrarylinkfield (title,message,link: /evidence-requests/{id}) — this whole shape has no Fibretrace analog. BE must define/evidence-requestsCRUD perBE-ASK.md:573-608and synthesize the “New evidence request” notification server-side as part of that POST. - mismatch: Fibretrace has no
evidence_requests/evidence_request_sharesresource at all, and Fibretrace notifications are entirely server-generated read-only objects with nolinkfield 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.tsxaddscompact(36px control heights) and a deprecated no-opsearchRightprop, moves search inside the right-hand action group so the whole toolbar is always right-aligned, switches Filters/Columns/Export buttons tovariant="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.tsxtablevariant) 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” —
TableToolbarwas reshaped alongside the new sharedTablePaginationcomponent 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→/partnerswith 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 byusePoPacksByPo/useProposedClaimsByPo), a small admin-setsiteSubTextlabel beside the logo, and (out of scope for this unit, admin-only) newAdminHome/AdminGuide/AdminOverrides/AdminReservationRequestsroutes + anadmin/programmes→admin/programsrename. - 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 — stubuseSiteSubTextto return""(hides the label, safe default) and hand BE a 1-field ask: addsite_sub_text: string | nulltoBasicConfigInterface/get-basic-config(read-only in canvas today) plus confirm/build the admin write endpoint (Lovable’suseSetSiteSubTexthas no Laravel write counterpart at all). - beSpecForGap: same
purchase_orders/production_claims/purchase_order_evidence_packsshape as units 7/8/13 above (badge-count hooks hardcodereturn {}directly inusePurchaseOrders.ts:271-290); NEW genericapp_settingskey/value table (key, value jsonb, updated_at) suggested forsiteSubText, OR extendBasicConfigInterface/get-basic-configdirectly (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);siteSubTextsimilarly has no home in the realget-basic-configresponse. - 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
siteSubTextaddition 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→/partnerslink renames in two files. Collections/SignupStatus got only spacing/row-height touches. - portGuidance: Do not extend
apiAdapter.tsforpublic_verification_tokens/production_claims/fibre_programmes— its mappers dispatch against the authenticatedbaseApi/session, wrong for a token-gated public route. RoutePublicClaim.tsxOUTSIDE<ProtectedRoute>inApp.tsx(same pattern as the already-wired/verify/:guid→CollectionPreview.tsx, which degrades gracefully today since its backing tables are noop stubs). Until BE ships production_claims + fibre_programmes + a public token-resolution endpoint, portPublicClaim.tsxas 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) mirroringcompaniesPublicApi.ts. The CU/MT conversion and multi-programme dropdown are downstream of the samefibre_programmesGAP — no separate BE ask beyond the/programsendpoint 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 publicGET /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 sidePOST /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_shaauto-advanced to2b52a6089ef7462bffb2cd845f256931d09be893— the earliest not-yet-ported PORTABLE unit this pull isdesign-system-button-tabs-pagination, whose first commit isf3b4bddd347ffa3949aa80c8c7444afb0911da98; its verified linear PARENT (git rev-parse f3b4bddd~1in the clone) is2b52a6089ef7462bffb2cd845f256931d09be893. Everything chronologically before that parent is either verified ALREADY_DONE/SKIP (proven present inapps/dashboard) or BE-blocked and recorded inknown_unportedbelow — 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 instate.json’sknown_unportedlist.allPortableDoneis false — only the 3 pure-UI port units + the 2 merged design-system rebrands are unconditionally ready this pull;claim-position-page-rewriteresolved PARTIAL and the remaining 14 review units resolved GAP/PARTIAL or need a scoping decision.suggested_ported_sha=f0d02a2866a25dbd15e9a055cb786e41b3c58405(this pull’sorigin/mainHEAD) — 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