Skip to content

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 new defaultSubTab for 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:
    1. Vendor header (logo chip + name + subtitle)
    2. Period picker row (picker + arrows + optional custom-range note)
    3. Summary KPI strip (4 cards: Period Total / vs Previous / Subscription / Avg per day)
    4. Spend over time chart (single-vendor daily bars, period-scoped)
    5. Current Subscription card (plan, fee, billing cycle, next bill estimate)
    6. Credits & one-time events (non-subscription line items, simple list)
    7. Line items table (full Anthropic line items, period-scoped, sortable/filterable, CSV export)
    8. 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:

  • VendorPageShell owns the period-picker state (range, same shape as BillingReports.jsx), fires the API call, handles loading/error states, and renders children with pre-computed slices.
  • AnthropicVendor is a thin assembler โ€” chooses which sections to include and in what order. Future GitHubVendor, 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:

jsonc
{
  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 slices

5. KPI strip semantics โ€‹

Four cards, mirroring Reports' Summary strip shape (role-cards-grid 4-col):

CardValueHintSource
Period Total$443.21Range labelsum of spendByDay
vs Previous+21%$366.20 (73d prior) โ€” prorated when current period is in-flightdailyTotal vs previousTotal with same proration logic as Reports
Subscription$200.00Plan name + cycleinvoice_actuals.subscription_charge_usd, latest invoice
Avg per day$6.07N days elapseddailyTotal / 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 (from attributed_service of the most recent subscription line item)
  • Monthly fee โ€” $200.00 (from invoice_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.39

Pulled 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--loading class pattern from BillingReports.jsx) rather than blanking to spinner.
  • First load: full-page spinner using the existing .loading-state pattern.
  • API error: whole-page replace with <ErrorState message=โ€ฆ> โ€” same as BillingReports.jsx.
  • Subscription card with no invoice: see ยง6.
  • Drift section empty: see ยง9.

11. Routing + nav (Billing.jsx changes) โ€‹

Resulting state of VENDORS_OPTIONS:

js
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, undocumented
  • GET /analytics/billing/report โ€” existing, undocumented
  • GET /analytics/billing/report/line-items.csv โ€” existing as of 63e16b7e, undocumented; will gain optional vendor query param in this PR
  • GET /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 getBillingVendor service function โ€” happy path + missing-subscription + missing-drift + zero-spend-vendor cases. BQ mocked via forge's _client injection pattern (same as existing service tests).
  • Component tests for AnthropicVendor covering: happy path render, loading state, error state, no-vendor-data empty state, picker change refetch, custom-range note visibility.
  • No backend change to LineItemsTable so 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_actuals data 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 โ€‹

Built with VitePress