Admin Billing Portal โ Cloud Run + BQ Pipeline Wiring โ
Last Updated: 2026-05-14 Status: Overview + top-nav shipped (local, not yet pushed); Reports / Vendors / Settings deep content not built Branch: claude/billing-admin-bq-wiringLatest commit: ad2fbdccParent: 2026-05-10-bigquery-billing-phase-2.md โ this plan consumes the pipeline that one stood up
Task tracker โ
| ID | Task | Status | Notes / commit |
|---|---|---|---|
| 3.1 | Cloud Run analytics-api /analytics/billing/metrics | Done | 2cfb9ce3 โ granularity, per-vendor, drift, subscription, all-time |
| 3.2 | Overview rewrite (KPI strip, history, 3 donuts, footer) | Done | 2cfb9ce3 + d3c67c7e |
| 3.3 | Copilot $39 from invoice_actuals.subscription_charge | Done | 2cfb9ce3 โ replaces broken CF individual โ $19 detection |
| 3.4 | Cloud Scheduler migration | Done | Shipped separately in PR #519 |
| 5.1 | Top nav restructure + hash routing + dropdown tabs | Done | 42c01933 + ad2fbdcc (overflow-clip fix) |
| 5.2 | Reports tab content (KPI / chart / cards / tables) | Next | All Time / YTD / Monthly / Weekly / Custom โ see ยง5.2 |
| 5.3 | Vendors tab content (per-vendor deep pages + sub-tabs) | After 5.2 | Migrate per-vendor CF fetchers to analytics-api as part of this โ ยง5.3 |
| 5.4 | Settings tab content (drift, seeds, ingest status) | After 5.3 | Drift card moved off Overview, lives here โ ยง5.4 |
| 6.a | CSV export utility (shared across Reports + Vendors) | Parked | Client-side row-to-CSV โ surfaces in 5.2 first |
| 6.b | Open-in-BigQuery deep links | Parked | Generated BQ console URL with pre-filled SQL |
| 6.c | Backfill pricing seeds for pre-deployment months | Parked | Lets drift signals work for FebโApr 2026 GitHub history |
| 6.d | Confirm Anthropic invoice parser emitting rows | Parked | Parser landed PR #511; needs spot-check |
| 6.e | Retire Firebase Cloud Function getBillingData entirely | After 5.3 | Per [[prefer-cloud-run-over-functions]] โ depends on vendor migration |
| 6.f | Email digest ("your spend so far") | Parked (v3) | Out of scope for this PR |
| 7.a | Historical backfill via synthesized invoice line items | Done | New Dataform view invoice_synthetic_line_items, UNIONed into fact_cost_line_items; drift mart excludes synthetic from estimate side. Removes the API-side invoice-fallback fork. |
Legend: Done = committed locally on the branch. Next = first thing to pick up. After X = blocked on X. Parked = not in this PR.
Pipeline flow โ
Current configuration โ
All ingest runs as Cloud Run Jobs triggered by Cloud Scheduler at staggered 03:00โ03:30 America/Los_Angeles each day. Dataform runs last (03:30) so all raw rows are present before normalization + attribution. GCP and Anthropic don't have ingest jobs โ GCP uses Google's native Cloud Billing export (continuous), Anthropic writes per-request from the running app.
flowchart LR
%% โโ Ingest sources โโ
subgraph SCH["Cloud Scheduler ยท daily ยท America/Los_Angeles"]
direction TB
s1["03:00 ยท ingest-cloudflare"]
s2["03:10 ยท ingest-github"]
s3["03:20 ยท ingest-invoices"]
s4["03:30 ยท dataform-billing-daily"]
end
gcp_export["GCP Cloud Billing<br/>native export<br/><i>(continuous, Google-managed)</i>"]
anth_app["Anthropic per-request<br/>logger in services/api/assistant<br/><i>(continuous, app-driven)</i>"]
%% โโ Raw layer โโ
subgraph RAW["billing_raw โ sources"]
direction TB
r_cf["cloudflare_raw"]
r_gh["github_raw<br/>github_account_raw"]
r_inv["invoice_actuals<br/><i>(Gmail-parsed)</i>"]
r_anth["anthropic_request_usage"]
r_gcp["billing_export_gcp.<br/>gcp_billing_export_*"]
end
s1 --> r_cf
s2 --> r_gh
s3 --> r_inv
anth_app --> r_anth
gcp_export --> r_gcp
%% โโ Norm layer โโ
norm["billing_norm.fact_cost_line_items<br/><i>(incremental ยท daily 2-day reprocess)</i>"]
synth["billing_norm.invoice_synthetic_line_items<br/><i>(view ยท gap-fills pre-ingest history)</i>"]
r_cf --> norm
r_gh --> norm
r_anth --> norm
r_gcp --> norm
r_inv --> synth
synth -->|"UNION ALL"| norm
%% โโ Attrib seeds โโ
d_alloc[("dim_cost_allocation_rules")]
d_ghpr[("dim_github_plan_pricing")]
d_anthpr[("dim_anthropic_pricing")]
d_tax[("dim_taxonomy")]
d_ghpr -. "JOIN @ usage_start" .-> norm
d_anthpr -. "JOIN per-request" .-> norm
%% โโ Attributed fact โโ
attr["billing_attrib.fact_cost_attributed<br/><i>(incremental)</i>"]
norm --> attr
d_alloc -. "lowest-priority rule wins<br/>โ (env, app, attributed_service)" .-> attr
d_tax -.-> attr
s4 -. "compiles + runs<br/>norm + attrib" .-> norm
s4 -.-> attr
%% โโ Marts โโ
subgraph MARTS["billing_marts โ consumption views"]
direction TB
m_recon["invoice_reconciliation<br/><i>(view)</i>"]
m_unatt["unattributed_cost_daily<br/><i>(view)</i>"]
end
attr --> m_recon
r_inv --> m_recon
attr --> m_unatt
%% โโ Consumers โโ
api["Cloud Run ยท analytics-api<br/>GET /analytics/billing/metrics<br/>GET /analytics/billing/report"]
ui["Admin /admin/billing<br/>Overview ยท Reports ยท Vendors ยท Settings"]
attr --> api
m_recon --> api
r_inv --> api
api --> uiWhat we expect to build โ
Pipeline gap closed by ยง7.a (synthetic invoice-derived line items, now landed and shown above as part of the current configuration). Remaining work is admin-portal-side: the per-vendor deep pages (ยง5.3) and the diagnostics/settings surface (ยง5.4).
flowchart LR
api["analytics-api<br/><i>(existing)</i>"]:::existing
ui["Admin /admin/billing<br/><i>(existing shell)</i>"]:::existing
ov["Overview ยท ยง3.2<br/><i>(done)</i>"]:::existing
rp["Reports ยท ยง5.2<br/><i>(done)</i>"]:::existing
v["Vendors tab ยท ยง5.3<br/><i>per-vendor deep-dive<br/>Summary / Subscription /<br/>Usage / Line Items / Drift</i>"]:::proposed
st["Settings tab ยท ยง5.4<br/><i>drift signals ยท pricing seeds ยท<br/>ingest-job status ยท BQ troubleshooting</i>"]:::proposed
api --> ov
api --> rp
api --> v
api --> st
ov --> ui
rp --> ui
v --> ui
st --> ui
classDef existing stroke:#6b7280,fill:#1f2937,color:#e5e7eb
classDef proposed stroke-dasharray: 5 5,stroke:#a855f7,fill:#1f1132,color:#e5e7ebSynthetic backfill โ what landed: A new billing_norm.invoice_synthetic_line_items view splits each Gmail-parsed invoice into one row per (vendor, day, segment) for periods whose invoice_period_end is strictly before the vendor's earliest raw-ingest date. Segments are subscription / metered / tax / credit when the parser surfaced the breakdown, or a single invoice_total when only the grand total was parseable. UNIONed into fact_cost_line_items so attribution + downstream queries see synthetic rows identically to real ones. The drift mart (invoice_reconciliation) excludes synthetic from estimated_usd so historical drift signals don't self-compare invoice-against-its-own-synthesized-estimate. The "entire period before" gap filter guarantees no double-counting in months where raw ingest started mid-month.
1. Goal โ
Connect the admin billing surface to the BigQuery pipeline via Cloud Run (analytics-api), retire the on-demand Firebase Cloud Function path, and restructure the surface into a coherent IA:
- Overview โ status board, "am I spending OK?" in 5 seconds
- Reports โ time-period deep reports (All Time / YTD / Monthly / Weekly / Custom) with exports + raw-BQ deep-links
- Vendors โ per-vendor drilldown pages with horizontal sub-tabs
- Settings โ config + diagnostics (drift signals land here)
Bundle into a single PR (per [[bundle-related-work-into-fewer-prs]]) and target dev, draft-only (per [[open-prs-as-drafts]]).
2. IA decisions locked in โ
- Top nav:
Overview ยท Reports โพ ยท Vendors โพ ยท Settings(hybrid dropdown + breadcrumb)- Dropdowns on top nav; active sub-selection echoes as a breadcrumb (
Reports โบ Monthly โบ May 2026) where each segment is clickable
- Dropdowns on top nav; active sub-selection echoes as a breadcrumb (
- Report page layout: linear + dashboard combo
- Page header with
[โป refresh]/[Export CSV]/[Open in BQ โ] - KPI strip โ 12-month stacked bar โ 2-up dashboard (spend-by-day + donut) โ vendor breakdown table โ line items table (top 20)
- Page header with
- Vendor page layout: page header (vendor name + headline KPIs) โ horizontal tab strip
Summary / Subscription / Usage / Line Items / Drift
3. Done โ
3.1 Cloud Run analytics-api (services/api/analytics/src/) โ
GET /analytics/billing/metrics?granularity=weekly|monthly|yearlyreturns:- Totals:
totalCurrentMonth,totalLastMonth,totalYtd,totalAllTime - Per-vendor:
currentMonthByVendor,lastMonthByVendor,ytdByVendor,totalByVendor - Per-vendor/per-service:
currentMonthByVendorService(Copilot vs Actions, GCP service mix) - History:
monthlyHistory[](per-granularity) - Latest invoiced subscription fee:
latestSubscriptionByVendor - Drift signals:
driftSignals[]
- Totals:
- Auth:
verifyFirebaseToken + requireRole('admin') - Source-of-truth choices: monthly/yearly/total totals from
invoice_reconciliation(COALESCE invoiced/estimated with current-month CASE prefering estimated); per-line-item breakdown fromfact_cost_attributed; subscription fee frominvoice_actuals
3.2 Overview rewrite (apps/admin/src/admin/billing/Billing.jsx) โ
- Cost Summary KPIs:
Daily Average / Monthly / Yearly / Total - Spending History: full-width stacked bar with Weekly/Monthly/Yearly granularity toggle
- At a glance: 3-up donuts (Monthly / Yearly / Total vendor mix) โ replaces old drift section + Trend & Top Spend
- Providers grid (timeframe-labeled, navigates to per-vendor tabs)
- Quick links footer (
View this month's report โ,Run a custom BigQuery query โ) - Cut: MoM bar chart, Trend & Top Spend section, standalone Drift section (drift parked for Settings tab)
3.3 Copilot $39 fix โ
- Reads
invoice_actuals.subscription_charge_usdfor the latest invoice per vendor โ the real bill, not pipeline guesswork - Overrides
providers.github.usage.copilot.monthlyEstimatein admin client
3.4 Cloud Scheduler migration (PR #519, separate) โ
- Dataform cron moved from internal
workflowConfig.cronScheduleto Cloud Scheduler. Closed split-permission gotcha.
4. In progress / next โ
Nothing actively being edited. Next chunk picks up at ยง5.1.
5. Designed, not yet built โ
5.1 Top nav restructure (prerequisite for 5.2 + 5.3) โ
- Replace flat
Overview / Cloudflare / GCP/Firebase / GitHub / Railwaytabs withOverview ยท Reports โพ ยท Vendors โพ ยท Settings - Build a
<DropdownTab>component (hover/click opens menu; active sub-selection shows as chip on the tab label) - Build a
<Breadcrumb>row that renders below the tabs when a sub-selection is active - Route via hash:
#/admin/billing/reports/monthly/2026-05etc. so deep links work
5.2 Reports tab โ
- Sub-options:
All Time / YTD / Monthly / Weekly / Custom range - Layout per ยง2:
- KPI strip (4 metrics โ period-scoped totals + comparison)
- 12-month stacked bar (reuses
SpendingHistorywith locked granularity per report type) - 2-up dashboard cards:
Spend by day(area / sparkline) +Distribution(donut) - Vendor breakdown table โ sortable, columns: vendor, spend, % total, vs last period, trend
- Line items table โ top 20, link to "show all in BQ"
- Page header:
[Export CSV](client-side from current rows) +[Open in BigQuery โ](deep link with pre-filled SQL)
- Custom range needs a date-range picker โ punt to v2 if time-boxed
5.3 Vendors tab โ
- Sub-options:
GitHub / GCP / Cloudflare / Railway / Anthropic - Per-vendor page: vendor header (name + headline KPIs) then horizontal sub-tabs
- Summary: vendor-only 12mo chart, KPI strip, quick links into other sub-tabs
- Subscription: tier, seats, $/seat, next bill date, tier-change history
- Usage: vendor-specific metered (Actions minutes for GitHub, service mix for GCP, etc.)
- Line Items: full line-item table for the vendor
- Drift: vendor-specific drift signals (subset of the global drift feed)
- Migration work: as each Vendor page goes live, move that vendor's on-demand fetcher from
services/functions/firebase/modules/billing.jstoanalytics-api. Goal: retire the CF entirely. See [[prefer-cloud-run-over-functions]].
5.4 Settings tab โ
- Drift signals diagnostics panel (the pipeline-health view we cut from Overview)
- Pricing-seed visibility (read-only display of
dim_github_plan_pricingetc.) - Ingest-job status panel (last-run timestamps from
ops.pipeline_runs) - "BQ pipeline unreachable" troubleshooting affordances
6. Parked / future โ
- CSV export โ client-side row-to-CSV utility (shared across Reports + Vendors tables)
- Open-in-BigQuery deep links โ generate BQ console URL with
?p=<project>&page=queryresults&sq=<encoded SQL> - Backfill pricing seeds for pre-deployment months โ currently history is invoice-only for Feb-Apr 2026 GitHub; seeding rule-based estimates would let drift signals work historically
- Anthropic invoice parser โ landed (PR #511); confirm it's emitting rows
- Forecast / projected-EOM โ was on Overview as "vs last month" hint; cut for now, could resurface in Reports
- Per-vendor SLO tiles โ uptime / error rate on Vendor โ Summary? out of scope here
- Email digest โ weekly "your spend so far" email; v3
7. PR / shipping discipline โ
- Bundle 5.1 + 5.2 + 5.3 into the same PR (
claude/billing-admin-bq-wiring) per [[bundle-related-work-into-fewer-prs]] - Open as draft (per [[open-prs-as-drafts]]). User marks ready for review herself.
- PR base:
dev(per [[never-base-pr-on-main-without-explicit-ask]] / project default) - Don't push the branch until user explicitly approves (per [[no-unprompted-push]])
8. Open questions โ
- Custom date range picker UX โ calendar popover, or two inline inputs? Defer to first build iteration.
- Vendor sub-tab persistence โ should
Vendors โบ GitHub โบ Usagesurvive page reloads? Hash routing handles it, but need to wire. - "Total" math when pipeline has been running >1 year โ currently Total =
invoice_reconciliationrolled up. Confirm semantics when we cross the 12-month mark.
9. Related memory โ
- [[prefer-cloud-run-over-functions]] โ durable directive
- [[bundle-related-work-into-fewer-prs]] โ single-PR scope
- [[open-prs-as-drafts]] โ
--draftalways - [[billing-pipeline-state]] โ what the BQ pipeline currently captures
- [[operator-copilot-tier]] โ Pro+ at $39, seeded correctly