Anthropic vendor page โ design โ
Date: 2026-05-26 Status: Approved, ready for implementation plan Scope: Anthropic only (this branch). GitHub / GCP / Cloudflare / Railway vendor pages will reuse the shell in follow-up branches that fork from this one and merge back before PR. Tracker: Supersedes the ยง5.3 sketch in 2026-05-14-admin-billing-portal.md.
1. Goal โ
Replace the Vendors-tab placeholder/stub for Anthropic with a real per-vendor dashboard that answers "what is this vendor costing me, and is anything off?" at a glance. Establish the shell pattern (VendorPageShell + per-vendor section composition) that the other four vendors will adopt one at a time.
This is the first surface that consumes vendor-scoped BigQuery data without any Firebase Cloud Function in the path โ confirms the analytics-api can stand alone for a vendor and unblocks CF retirement work in subsequent branches.
2. IA + page layout (locked in) โ
- Top nav unchanged. Hash route is
#vendors/anthropic. Anthropic appears alphabetically first in the Vendors dropdown and becomes the newdefaultSubTabfor the Vendors tab. - Page shell is single-column scrolling, NOT horizontal sub-tabs. Same
SystemSection-stack idiom as Reports so users only learn one navigation pattern. - Period model: page-level period picker (same component as Reports'
PeriodPicker), default = vendor-active YTD (vendor's first-spend date through today), arrows shift, presets available. Custom range surfaces an inline italic note aligned right of the picker:โ Custom range: Apr 1 โ Apr 15, 2026. - Sections, top to bottom:
- Vendor header (logo chip + name + subtitle)
- Period picker row (picker + arrows + optional custom-range note)
- Summary KPI strip (4 cards: Period Total / vs Previous / Subscription / Avg per day)
- Spend over time chart (single-vendor daily bars, period-scoped)
- Current Subscription card (plan, fee, billing cycle, next bill estimate)
- Credits & one-time events (non-subscription line items, simple list)
- Line items table (full Anthropic line items, period-scoped, sortable/filterable, CSV export)
- Invoice drift (rolling 6 months invoice vs estimate)
3. Component architecture โ
New file layout under apps/admin/src/admin/billing/:
billing/
โโโ Billing.jsx (route wiring updates only)
โโโ BillingReports.jsx (LineItemsTable import path updated)
โโโ components/
โ โโโ LineItemsTable.jsx (MOVED from inside BillingReports.jsx, no behavioral change)
โโโ vendors/
โโโ VendorPageShell.jsx (header + period picker + section frame; future-vendor scaffold)
โโโ AnthropicVendor.jsx (assembles Anthropic-specific sections in order)
โโโ sections/
โโโ VendorSummaryStrip.jsx (4-card KPI strip)
โโโ VendorSpendChart.jsx (single-vendor daily bar chart, reuses SpendByDayChart styling)
โโโ SubscriptionCard.jsx (plan / fee / cycle / next bill estimate)
โโโ CreditsEventsTable.jsx (non-subscription line items, simple list)
โโโ VendorLineItemsSection.jsx (wraps the moved LineItemsTable with vendor pre-filter)
โโโ VendorDriftSection.jsx (invoice vs estimate, rolling 6mo, vendor-filtered)Responsibilities:
VendorPageShellowns the period-picker state (range, same shape asBillingReports.jsx), fires the API call, handles loading/error states, and renders children with pre-computed slices.AnthropicVendoris a thin assembler โ chooses which sections to include and in what order. FutureGitHubVendor,GcpVendor, etc. pick a different combination from the same library.- Each section component takes only the data it renders and produces zero side effects.
Existing stub components (GitHubBilling, GcpBilling, CloudflareBilling, RailwayBilling) are not touched in this branch โ they continue to render for their respective sub-tabs until their dedicated branches replace them.
4. Data wiring โ
4.1 New endpoint โ
GET /analytics/billing/vendor/:vendor?period=โฆ&date=โฆ&from=โฆ&to=โฆ
Single round-trip for everything the vendor page needs except line items (which reuse the existing Reports endpoint). Response shape:
{
bqAvailable: true,
vendor: "anthropic",
range: { from, to, label },
previousRange: { from, to, label }, // same-length window before, for vs Previous
vendorActiveSince: "2026-03-14", // first-spend date โ drives YTD default
kpis: {
total, // sum of fact_cost_attributed for vendor in range
previousTotal, // sum for previousRange
vsPrevious: { absolute, percent, firstPeriod },
avgPerDay, // total / elapsed days
daysInRange,
subscription: { // null when no invoice on file
amount, // latest invoice_actuals.subscription_charge_usd
plan, // attributed_service from latest subscription line item
billingCycle: "monthly",
lastInvoiceDate, // invoice_period_start of most recent invoice
nextBillEstimate, // lastInvoiceDate + cycle length
},
},
spendByDay: [ { date, total } ], // single-vendor daily series
creditsEvents: [ { date, service, sku, amount } ], // non-subscription line items
driftSignals: [ { invoice_period_start, estimated, invoiced, drift, driftPct } ],
}Server: new function getBillingVendor({ vendor, period, date, from, to }) in services/api/analytics/src/services/billingReport.service.js (or a new vendorReport.service.js if the file grows past ~500 lines). Reuses resolveRange(). Five parallel BQ queries for: spendByDay, KPIs (total + previousTotal), latest subscription, credits/events, drift signals. vendorActiveSince from a small SELECT MIN(DATE(usage_start)) WHERE vendor=@vendor.
4.2 Line items โ
Reuse GET /analytics/billing/report line items โ the page passes the same period and filters vendor=anthropic client-side. No new line-items endpoint.
For CSV export, the existing GET /analytics/billing/report/line-items.csv gains an optional vendor query param (~5 lines: extra WHERE vendor=@vendor clause when present). The vendor page's "Export CSV" hits the same endpoint with the vendor pinned.
4.3 Client wiring โ
analyticsApi.js
+ getBillingVendor({ vendor, period, date, from, to })
+ getBillingLineItemsCsv(...) // already exists, gain optional `vendor` param
VendorPageShell
โ owns range state
โ effect: getBillingVendor(...) in parallel with getBillingReport(...) for line items
โ children get pre-computed slices5. KPI strip semantics โ
Four cards, mirroring Reports' Summary strip shape (role-cards-grid 4-col):
| Card | Value | Hint | Source |
|---|---|---|---|
| Period Total | $443.21 | Range label | sum of spendByDay |
| vs Previous | +21% | $366.20 (73d prior) โ prorated when current period is in-flight | dailyTotal vs previousTotal with same proration logic as Reports |
| Subscription | $200.00 | Plan name + cycle | invoice_actuals.subscription_charge_usd, latest invoice |
| Avg per day | $6.07 | N days elapsed | dailyTotal / daysInRange |
A blue italic * Final reconciliation varies by vendor FYI sits in the Summary section header (right-aligned, styled via existing .billing-report__summary-fyi class) โ same wording / wiring as Reports.
6. Subscription card โ
Four labeled fields in a 4-col grid inside one SystemSection:
- Plan โ
Max plan - 20x(fromattributed_serviceof the most recent subscription line item) - Monthly fee โ
$200.00(frominvoice_actuals.subscription_charge_usd) - Billing cycle โ
Monthly(derived from invoice cadence; fallback to "Monthly" if undetermined) - Next bill (est.) โ
Jun 5, 2026(lastInvoiceDate + 1 month)
Edge cases:
- No invoice on file โ card renders
"No subscription on file."body, no fields. - Last invoice >35 days old โ fee renders with muted suffix
"(last invoice was Mar 5 โ may be out of date)".
7. Credits & one-time events โ
Simple 3-column rows inside one SystemSection (not a sortable table โ these are infrequent events, not analytical data):
DATE EVENT AMOUNT
2026-05-04 Gift Pro - 1 months $50.33
2026-05-11 Auto-recharge credits $12.62
2026-05-19 One-time credit purchase $8.39Pulled from creditsEvents[] in the API response (rows where attributed_service is not the subscription service). Sorted chronologically descending. Empty state: "No credit or one-time events in this period."
8. Line items section โ
Reuses LineItemsTable from BillingReports.jsx exactly as-is, fed a pre-filtered slice (report.lineItems.filter(li => li.vendor === 'anthropic')). Inherits all the sort/filter/wrap/CSV-export behavior we just shipped in commit 63e16b7e.
To enable extraction without modifying BillingReports.jsx, LineItemsTable will be moved to apps/admin/src/admin/billing/components/LineItemsTable.jsx and imported from both BillingReports.jsx and VendorLineItemsSection.jsx. No behavioral change.
9. Drift section โ
Rolling 6 months of invoice_reconciliation rows for the vendor, rendered as a compact 5-column table inside one SystemSection:
MONTH ESTIMATED INVOICED ฮ ฮ%
Apr 2026 $203.00 $205.00 +$2.00 +1.0%Only months with drift signal are shown (i.e. is_drift_alert = true from the source view โ same threshold as the existing global drift feed).
Empty state: "No drift in the last 6 months."
Note: The global drift feed eventually lives on the Settings tab per the prior plan. The per-vendor drift section here is independent โ vendor-scoped, no link required. When Settings ships, the per-vendor section can optionally cross-link to the full feed; not in v1 scope.
10. Edge cases + loading/error UX โ
- No vendor data ever (
vendorActiveSince === null): page renders the header + a single centered"No data for Anthropic yet."empty state. Period picker defaults to "Last 30 days" instead of vendor-active YTD. - Picker change in-flight: dim existing content (re-using the
.billing-report--loadingclass pattern fromBillingReports.jsx) rather than blanking to spinner. - First load: full-page spinner using the existing
.loading-statepattern. - API error: whole-page replace with
<ErrorState message=โฆ>โ same asBillingReports.jsx. - Subscription card with no invoice: see ยง6.
- Drift section empty: see ยง9.
11. Routing + nav (Billing.jsx changes) โ
Resulting state of VENDORS_OPTIONS:
const VENDORS_OPTIONS = [
{ id: 'anthropic', label: 'Anthropic', icon: <Sparkles size={14} /> },
{ id: 'cloudflare', label: 'Cloudflare', icon: <Cloud size={14} /> },
{ id: 'gcp', label: 'GCP/Firebase', icon: <Flame size={14} /> },
{ id: 'github', label: 'GitHub', icon: <Github size={14} /> },
{ id: 'railway', label: 'Railway', icon: <TrainFront size={14} /> },
]Vendors-tab defaultSubTab flips from github to anthropic. BillingVendors switch gains case 'anthropic': return <AnthropicVendor />. Existing case branches are untouched.
Hash routing (parseBillingHash) needs no change โ #vendors/anthropic already validates against SUB_IDS_BY_TAB.vendors, which is auto-derived from VENDORS_OPTIONS.
12. OpenAPI updates โ
New documentation lands in services/api/analytics/openapi.json for all four billing endpoints (backfilling existing drift):
GET /analytics/billing/metricsโ existing, undocumentedGET /analytics/billing/reportโ existing, undocumentedGET /analytics/billing/report/line-items.csvโ existing as of63e16b7e, undocumented; will gain optionalvendorquery param in this PRGET /analytics/billing/vendor/{vendor}โ new
Each entry specifies query params, auth (verifyFirebaseToken + requireRole('admin')), and response shape.
13. Testing โ
Per the project's npm run validate gate:
- Unit tests for
getBillingVendorservice function โ happy path + missing-subscription + missing-drift + zero-spend-vendor cases. BQ mocked via forge's_clientinjection pattern (same as existing service tests). - Component tests for
AnthropicVendorcovering: happy path render, loading state, error state, no-vendor-data empty state, picker change refetch, custom-range note visibility. - No backend change to
LineItemsTableso its existing test coverage is sufficient. - OpenAPI lint passes after backfill.
14. Out of scope (explicit) โ
- Migrating GitHub / GCP / Cloudflare / Railway off the Firebase CF โ separate per-vendor branches.
- Removing the Overview tab โ separate work, sequenced after vendor pages so we can decide which Overview signals to keep vs drop.
- Settings tab (drift diagnostics, pricing-seed visibility, ingest-job status) โ planned in ยง5.4 of the billing portal plan.
- Anthropic invoice parser enhancements โ current
invoice_actualsdata is sufficient for v1. - Per-vendor projected end-of-month KPI โ not in the 4-card strip (can be added later if requested).
15. Open questions โ
None as of approval. All design decisions resolved in brainstorming session.
16. References โ
- Reports page implementation โ shell pattern this design reuses
- billingReport.service.js โ service file the new vendor endpoint will sit alongside (or extract from, if it grows past ~500 lines)
- Billing portal plan ยง5.3 โ predecessor design; this spec supersedes it for Anthropic
- project_billing_pipeline_state โ current state of the BigQuery billing pipeline