Skip to Content

← All archived runs

Run: bid-pool-workbench

run.md

Run: bid-pool-workbench

  • issue: #465 # canonical home of the spec + state (labels, comments)
  • branch: feat/465-bid-pool-workbench
  • pr: #466

00_intake/stub.md

Stub: Bid pool (workbench view)

  • feature-slug: bid-pool-workbench
  • epic: expert-dashboard-data
  • personas: Expert
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: expert-workbench-foundation
  • sequence: 6 of 8

Problem

The workbench Bid pool table (apps/web/components/dashboard/expert/bid-pool-table.tsx) is dummy, even though the standalone /expert/bids page already renders the real bid pool via leadService.findBidPoolLeads (apps/web/components/expert/bid-pool-table.tsx, note the different path). The workbench version shows richer fields the real query doesn't yet return: urgency, a value range + valueSource (Customer budget | System estimate), timeToStart, timeToRespond, and a fit label.

Proposed change

  • Render the workbench Bid pool table from the existing real bid-pool query (reuse findBidPoolLeads with the expert's match fitScore + recommendation), so it stops being mock.
  • Add the workbench-only derivations/fields:
    • fit label from the existing fitScore bands.
    • timeToRespond from lead.bidDeadline.
    • timeToStart from lead.startDate.
    • value range + valueSourcenet-new: a customer-budget signal vs a system estimate. If no budget field exists, Define decides whether to add one or show estimate-only.
    • urgency — derived from bid deadline proximity / fit, or a small signal on the lead.
  • Show a compact top-N on the workbench (the full list stays at /expert/bids).

Acceptance criteria (rough)

  • Workbench Bid pool renders real open opportunities for the signed-in expert with fit, value, time-to-respond and time-to-start — no mock rows.
  • value/valueSource reflect a real budget when present and an estimate otherwise (clearly labelled), never invented.
  • Reuses the existing findBidPoolLeads path — no second bid-pool query.
  • Empty pool renders a clean state and links to /expert/bids.

Out of scope (this feature)

  • The full bid-pool listing/detail + proposal-submission flow at /expert/bids (already real, left alone).
  • Changing the match/scoring engine.

Notes for Define

  • Confirm whether a customer budget field exists on the lead; the mock distinguishes "Customer budget" vs "System estimate". If absent, the cleanest first version is estimate-only with the valueSource label honest about it.
  • Decide the workbench top-N + sort (urgency? fit? deadline?).
  • touches: packages/services/src/db/services/leads (reuse/extend findBidPoolLeads projection), apps/web/components/dashboard/expert/bid-pool-table.tsx. </content> </invoke>

01_define/output/spec.md

Spec: Bid pool (workbench view) on real data

  • slug: bid-pool-workbench
  • issue: #465
  • personas: Expert
  • touches: packages/services/src/db/models/lead.ts, packages/services/src/db/services/leads/index.ts, apps/web/components/dashboard/expert/bid-pool-table.tsx, apps/web/app/(app)/expert/dashboard/page.tsx
  • complexity: standard

Problem

The expert workbench (/expert/dashboard) Bid pool table is mock — it renders static rows from lib/mock/expert.ts even though the standalone /expert/bids page already shows the real bid pool via leadService.findBidPoolLeads (which returns the expert's fitScore, confidence and recommendation per match). The workbench is the expert's daily landing surface, so a fake bid pool there undercuts trust in the whole dashboard. This advances Build the Bridge → Q2 2026 Objective 3 (Validate technical infrastructure & payout flow): experts can only act on real opportunities — see fit, judge value, and respond before the deadline — if the workbench shows their actual open pool.

Proposed change

Render the workbench Bid pool from the existing findBidPoolLeads query (reuse it — no second bid-pool query path), scoped to the signed-in expert, and replace the mock with a compact top-5 view. The full list stays at /expert/bids.

Workbench-only derivations (all from real fields already returned, plus one net-new budget field):

  • fit — label from the existing fitScore bands, matching /expert/bids: >= 70 → "Strong fit", 40–69 → "Moderate fit", < 40 → "Weak fit". No score → no fit label.
  • timeToRespond — relative time from lead.bidDeadline (e.g. "2 days", "Today", "Overdue"); "—" when no deadline is set.
  • timeToStart — relative time from lead.startDate; "—" when unset.
  • urgency — derived from bid-deadline proximity: <= 2 days (or overdue) → "High", <= 7 days → "Medium", else / no deadline → "Low".
  • value + valueSourcenet-new: add an optional customer budget signal to the lead (budgetMin / budgetMax, currency-less integers in the tenant's currency). When present, show the formatted range labelled "Customer budget". When absent, show "—" — no invented estimate this run (a "System estimate" source is out of scope; see below).

Sort + size: top 5 by fit (highest fitScore) then nearest bidDeadline. Reuse findBidPoolLeads (extend its projection to include the new budget fields); the workbench requests the expert-scoped pool and applies this sort/limit — it does not introduce a parallel query.

Empty pool: render a clean empty state with a link to /expert/bids.

The new budget fields are additive and optional on the lead model; nothing currently writes them, so existing leads render "—" until a budget is captured (capture UI is out of scope).

Acceptance criteria

  • The workbench Bid pool renders real open opportunities for the signed-in expert (via findBidPoolLeads, expertUserId scoped) — no mock rows.
  • Each row shows fit (from fitScore bands), value + valueSource, time-to-respond, and time-to-start, all derived from real lead/match data.
  • value/valueSource shows the customer budget range labelled "Customer budget" when the lead has budgetMin/budgetMax, and "—" otherwise — never an invented number.
  • The table is a compact top 5, sorted by fit then nearest bid deadline; the full pool remains at /expert/bids (linked from the workbench).
  • No second bid-pool query is introduced — the workbench reuses findBidPoolLeads.
  • An empty pool renders a clean empty state that links to /expert/bids.
  • The mock bidPool rows are removed from the workbench data path (lib/mock/expert.ts / apps/web/components/dashboard/expert/bid-pool-table.tsx).

Out of scope

  • A "System estimate" value source / any pricing-estimate engine — there is no real estimate signal today, so value is customer-budget-or-nothing this run. valueSource carries only "Customer budget".
  • UI to capture the customer budget on a lead (the fields are added and read; populating them is a later run).
  • The full bid-pool listing/detail + proposal-submission flow at /expert/bids (already real — left alone).
  • Changing the match/scoring engine (fitScore, confidence, recommendation are consumed as-is).
  • The standalone apps/dashboards expert page — it stays mock by convention (dashboards app is self-contained mock data).

Open questions

  • none

02_build/output/notes.md

Build notes: bid-pool-workbench

  • branch: feat/465-bid-pool-workbench
  • commits:
    • feat: bid-pool-workbench — customer budget fields on lead
    • feat: bid-pool-workbench — render workbench bid pool from real data

What changed

  • packages/services/src/db/models/lead.ts — added optional, integer-validated budgetMin / budgetMax to both ILeadPopulated and the schema (the net-new customer-budget signal). They are additive and unset on existing leads.
  • packages/services/src/db/services/leads/index.ts — no change needed. findBidPoolLeads returns full lean docs (no .select() projection), so the new scalar fields flow through automatically and are typed via ILeadPopulatedBidPoolLeadPopulated. The workbench reuses this exact query — no second bid-pool path.
  • apps/web/components/dashboard/expert/bid-pool-table.tsx — rewritten from a mock-driven client component into a self-contained async server component (mirrors apps/web/components/expert/bid-pool-table.tsx): resolves tenant + signed-in expert, calls findBidPoolLeads({ expertUserId }), ranks top 5 by fit then nearest bid deadline, and renders Customer / Urgency / Value / Time to respond / Time to start / Fit. Pure derivation helpers: fitLabel (fitScore bands, matching /expert/bids), relativeTime (bidDeadline / startDate → "2 days" | "Today" | "Overdue" | "—"), urgencyFromDeadline, and customerBudget (Customer-budget range or "—", never invented). Header links to /expert/bids; empty pool renders a clean state linking there too. Exports BidPoolTableLoading for Suspense.
  • apps/web/app/(app)/expert/dashboard/page.tsx — dropped "use client" so the page (a server component) can render the async bid-pool table; wrapped it in <Suspense> so the DB query doesn't block the rest of the (instant, mock) workbench. Removed the bidPool={d.bidPool} prop.
  • apps/web/lib/mock/expert.ts — removed the mock bidPool array, the BidItem type, and the bidPool field from WorkbenchData (the workbench bid pool is now real).

Acceptance criteria status

  • Workbench renders real opportunities for the signed-in expert via findBidPoolLeads (expertUserId scoped) — no mock rows.
  • Each row shows fit, value + valueSource, time-to-respond, time-to-start — all from real lead/match data.
  • value/valueSource shows "Customer budget" range when budgetMin/budgetMax set, "—" otherwise — never an invented number (no "System estimate" source).
  • Compact top 5, sorted by fit then nearest bid deadline; full pool stays at /expert/bids (linked from header + empty state).
  • No second bid-pool query — reuses findBidPoolLeads.
  • Empty pool renders a clean empty state linking to /expert/bids.
  • Mock bidPool rows removed from the workbench data path (mock file + component).

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Ship reads them back from gh pr checks. No check is expected to fail.

Notes for review

  • The workbench fetches up to RANKING_LIMIT (50) matched leads to rank top-5 by fit, since fitScore is attached after the Mongo query and can't be sorted in the DB. Expert bid pools are bounded (matched leads only), so this is a safe cap; the accurate total is shown via pagination.total.
  • The new budget fields are read-only this run — no capture UI (explicitly out of scope), so leads render "—" for value until a budget is populated.

03_ship/output/changelog.md

Changelog: bid-pool-workbench

Persona: Expert

See your real opportunities on the expert workbench

Your go live workbench now opens to your real bid pool, not a sample:

  • The available work table shows the open opportunities matched to you, drawn live from the platform.
  • Each one shows how well it fits you, the customer's budget when they've shared one, and how long you have to respond and to start.
  • You see your top five matches at a glance, with the full bid pool a click away.

03_ship/output/investor-update.md

Experts now land on their real, fit-ranked opportunities

The expert workbench — the first screen an expert sees — now shows their real bid pool instead of a sample row. Each opportunity is ranked by how well it fits the expert and surfaces the customer's budget (where shared), time to respond, and time to start, so experts can act on the right work immediately. This advances Build the Bridge by strengthening the bid pool and proposals stage of the service journey, and moves our Q2 objective to Validate Technical Infrastructure & Payout Flow by putting experts in front of real, actionable leads on day one.

  • Experts open their workbench to their top five matched opportunities, drawn live from the platform — no mock data.
  • Every opportunity shows fit, customer budget when available, and the clock to respond and to start — the signals an expert needs to choose what to bid on.
  • Built on the existing bid-pool engine, so the workbench and the full bid pool stay consistent with no duplicate logic.

03_ship/output/pr.md

Ship: bid-pool-workbench

  • PR: #466 — https://github.com/sustentus/sustentus/pull/466
  • branch: feat/465-bid-pool-workbench
  • CI: format pending · lint pending · typecheck pending · preview build pass (all 6 Vercel previews green on build commit)
  • technical docs: no technical docs impact (the lead budget fields and the /expert/dashboard route are not documented at field/route level in apps/docs)
  • business docs: no business docs impact (business/roles + service-journey describe the bid-pool capability at the right abstraction; the workbench surface and the new backend budget field are not described, so nothing drifts)
  • release notes: both — investor draft (03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx

Review summary

  • Fixed on branchrelativeTime mislabel: Math.ceil of a sub-day negative fraction yields -0, which slipped past the days < 0 check, so a deadline that passed <24h ago showed "Today" instead of "Overdue". Now checks the raw ms sign first. (urgency was unaffected — -0 <= 2 already resolved to High.)
  • Accepted (not changed this run): the workbench re-derives fit bands as text labels while /expert/bids uses badge variants — same thresholds, different output, not worth a shared util yet; ranking top-5 in the component and fetching up to 50 to rank is intentional (fitScore is attached post-query, can't be Mongo-sorted) and bounded — a service-level sort/limit option is a future refinement, not in scope.
  • Refuted: a flagged TS2339 on lead.budgetMin/budgetMaxBidPoolLeadPopulated extends ILeadPopulated, which now carries both fields; types resolve.

Acceptance check (vs spec)

  • Workbench renders real opportunities for the signed-in expert via findBidPoolLeads (expertUserId scoped) — no mock rows
  • Each row shows fit, value + valueSource, time-to-respond, time-to-start — all from real lead/match data
  • value/valueSource shows the customer budget range labelled "Customer budget" when budgetMin/budgetMax set, "—" otherwise — never invented
  • Compact top 5, sorted by fit then nearest bid deadline; full pool stays at /expert/bids (linked)
  • No second bid-pool query — reuses findBidPoolLeads
  • Empty pool renders a clean empty state linking to /expert/bids
  • Mock bidPool rows removed from the workbench data path

Merge & deploy

  • merged: no — awaiting explicit human approval (gate:merge-approved)
  • deploy: pending merge