11// Real API surface — talks to api.instanode.dev (via Vite proxy in dev,
22// same-origin in prod).
33//
4- // Mid-state, 2026-05-12: the §10.21 FIXTURE removal is in progress.
5- // - listStacks: cleaned (returns honest empty {items:[],total:0} on
6- // error). This is what /app/deployments consumes.
7- // - fetchTeam / updateTeam / listMembers / listInvitations / inviteMember:
8- // cleaned to honest derive-from-/auth/me or empty results.
9- // - getStack / getStackLogs / fetchActivity / fetchBilling 503 path /
10- // listInvoices 503 path / listVault: STILL use FIXTURE_* fallbacks.
11- // These are tracked under §10.21 to remove in a follow-up.
12- //
13- // The fixtures file is still imported below until that cleanup lands —
14- // removing each usage requires a per-page UX decision (empty state vs.
15- // error banner) that's easier to land in a focused PR.
16-
17- import {
18- FIXTURE_STACKS , FIXTURE_BUILD_LOGS , FIXTURE_BILLING , FIXTURE_INVOICES ,
19- FIXTURE_VAULT , FIXTURE_ACTIVITY ,
20- } from './fixtures'
21-
22- // fake() — tiny helper for the remaining FIXTURE-fallback paths. Returns
23- // the literal value as a resolved promise. Production code never calls
24- // it on the happy path — only inside catch blocks for surfaces that
25- // don't have a live API yet. Tracked for removal in §10.21.
26- function fake < T > ( value : T ) : Promise < T > { return Promise . resolve ( value ) }
4+ // §10.21 complete (2026-05-12): every FIXTURE_* fallback that previously
5+ // masked backend outages is gone. Surfaces with no live endpoint return
6+ // honest empty/null results; surfaces with a partial backend (billing 503,
7+ // invoices 503) now propagate errors so the consuming page renders a
8+ // real error banner instead of lying with mock data.
279
2810import type {
2911 Resource , DashboardStack , DashboardTeam , BillingDetails , Invoice ,
@@ -378,13 +360,27 @@ export async function listStacks(): Promise<{ ok: true; items: DashboardStack[];
378360 }
379361}
380362
381- export async function getStack ( slug : string ) : Promise < { ok : true ; stack : DashboardStack } > {
382- const s = FIXTURE_STACKS . find ( ( x ) => x . slug === slug ) ?? FIXTURE_STACKS [ 0 ]
383- return fake ( { ok : true as const , stack : s } )
363+ // §10.21: no live GET /api/v1/stacks/:slug yet. Derive the detail from
364+ // listStacks() so the dashboard stops fabricating stack metadata. Returns
365+ // `stack: null` honestly when the slug isn't found instead of silently
366+ // substituting the first FIXTURE_STACKS entry, which previously made the
367+ // dashboard render a fake "flashcards" stack for every unknown slug.
368+ export async function getStack ( slug : string ) : Promise < { ok : true ; stack : DashboardStack | null } > {
369+ try {
370+ const r = await listStacks ( )
371+ const stack = r . items . find ( ( x ) => x . slug === slug ) ?? null
372+ return { ok : true as const , stack }
373+ } catch {
374+ return { ok : true as const , stack : null }
375+ }
384376}
385377
378+ // §10.21: no live GET /api/v1/stacks/:slug/build-logs yet. Return an
379+ // honest empty buffer instead of canned build logs that don't match the
380+ // user's actual deploy. Real-time logs stream via streamSSE on
381+ // DeployDetailPage.
386382export async function getStackLogs ( slug : string ) {
387- return fake ( { ok : true as const , slug, lines : FIXTURE_BUILD_LOGS } )
383+ return { ok : true as const , slug, lines : [ ] as Array < { ts : string ; phase : string ; level : string ; message : string } > }
388384}
389385
390386// ─── Custom domains (LIVE) ──────────────────────────────────────────────
@@ -451,16 +447,13 @@ export async function deleteCustomDomain(stackSlug: string, id: string): Promise
451447
452448// ─── Billing (LIVE: every endpoint hits the agent API) ──────────────────
453449//
454- // fetchBilling — LIVE. Calls GET /api/v1/billing on the agent API,
455- // which returns the aggregated billing state (tier,
456- // subscription_status, next_renewal_at, amount_inr,
457- // payment_method, razorpay_*_id). Falls back to a
458- // whoami-derived shape when the endpoint isn't
459- // available (503 = Razorpay unconfigured, e.g. local
460- // dev) so the UI stays usable.
450+ // fetchBilling — LIVE. Calls GET /api/v1/billing on the agent API and
451+ // returns the aggregated billing state. Errors (including
452+ // 503 = Razorpay unconfigured) propagate so the page
453+ // renders a real error banner instead of mock data.
461454//
462- // listInvoices — LIVE. Calls GET /api/v1/billing/invoices on the agent
463- // API; falls back to FIXTURE_INVOICES on 503 .
455+ // listInvoices — LIVE. Calls GET /api/v1/billing/invoices. Errors
456+ // propagate (no fixture fallback) .
464457//
465458// createCheckout — LIVE. Calls POST /api/v1/billing/checkout, creates a
466459// real Razorpay subscription, and returns the hosted
@@ -505,40 +498,23 @@ function mapBillingState(r: BillingStateResp): BillingDetails {
505498}
506499
507500export async function fetchBilling ( ) : Promise < { ok : true ; plan : string ; billing : BillingDetails } > {
508- try {
509- const r = await call < BillingStateResp > ( '/api/v1/billing' )
510- return { ok : true as const , plan : r . tier , billing : mapBillingState ( r ) }
511- } catch ( e : any ) {
512- // 503 = Razorpay unconfigured in this env (e.g. local dev without
513- // RAZORPAY_KEY_ID). Fall back to the whoami-derived shape +
514- // FIXTURE_BILLING so the page still renders. Any other error
515- // propagates so the caller sees a real failure.
516- if ( e ?. status === 503 ) {
517- try {
518- const me = await fetchMe ( )
519- return { ok : true as const , plan : me . user . tier , billing : FIXTURE_BILLING }
520- } catch {
521- return { ok : true as const , plan : 'hobby' , billing : FIXTURE_BILLING }
522- }
523- }
524- throw e
525- }
501+ // §10.21: every error propagates. The previous 503 fallback returned
502+ // FIXTURE_BILLING (fake "active subscription, ****4242 visa, renews in
503+ // 9 days") whenever Razorpay was unconfigured, which lied to users in
504+ // local dev and any partial-outage state. BillingPage now catches the
505+ // APIError and renders a real error banner.
506+ const r = await call < BillingStateResp > ( '/api/v1/billing' )
507+ return { ok : true as const , plan : r . tier , billing : mapBillingState ( r ) }
526508}
527509
528510type InvoicesResp = { ok : boolean ; invoices ?: Invoice [ ] }
529511
530512export async function listInvoices ( ) : Promise < { ok : true ; invoices : Invoice [ ] } > {
531- try {
532- const r = await call < InvoicesResp > ( '/api/v1/billing/invoices' )
533- return { ok : true , invoices : r . invoices ?? [ ] }
534- } catch ( e : any ) {
535- // 503 = billing_not_configured (no Razorpay keys in this env). Fall
536- // back to the fixture list so the page renders something usable in
537- // local dev. Any other error propagates so the UI shows a real
538- // failure state.
539- if ( e ?. status === 503 ) return { ok : true , invoices : FIXTURE_INVOICES }
540- throw e
541- }
513+ // §10.21: errors propagate. The previous 503 fallback returned three
514+ // mock "paid" invoices that didn't correspond to any real payment;
515+ // BillingPage now surfaces the failure honestly.
516+ const r = await call < InvoicesResp > ( '/api/v1/billing/invoices' )
517+ return { ok : true , invoices : r . invoices ?? [ ] }
542518}
543519
544520export async function createCheckout (
@@ -560,6 +536,10 @@ export async function cancelSubscription(): Promise<{ ok: true }> {
560536type VaultListResp = { ok : boolean ; keys : string [ ] }
561537
562538export async function listVault ( env : string ) : Promise < { ok : true ; entries : VaultEntry [ ] } > {
539+ // §10.21: 401 still rethrows (AuthGate redirects to /login). Other
540+ // errors return an honest empty list — the page renders an empty state
541+ // rather than fabricating Stripe / OpenAI / Anthropic keys the user
542+ // has never stored.
563543 try {
564544 const r = await call < VaultListResp > ( `/api/v1/vault/${ encodeURIComponent ( env ) } ` )
565545 const entries : VaultEntry [ ] = ( r . keys ?? [ ] ) . map ( ( key ) => ( {
@@ -577,7 +557,7 @@ export async function listVault(env: string): Promise<{ ok: true; entries: Vault
577557 return { ok : true , entries }
578558 } catch ( e : any ) {
579559 if ( e ?. status === 401 ) throw e
580- return fake ( { ok : true as const , entries : FIXTURE_VAULT . filter ( ( v ) => v . env === env ) } )
560+ return { ok : true as const , entries : [ ] }
581561 }
582562}
583563
@@ -602,6 +582,12 @@ export async function deleteVaultSecret(env: string, key: string): Promise<void>
602582// provision / claim / rotate / delete / vault.put / etc.
603583// Falls back to synthesising from resource timestamps if the audit call fails.
604584export async function fetchActivity ( ) : Promise < { ok : true ; items : ActivityItem [ ] } > {
585+ // §10.21: 401 still rethrows. On any other failure we still try the
586+ // resource-synthesis fallback (honest data, just synthesised from the
587+ // live resource list). If that also fails we return an empty list
588+ // instead of FIXTURE_ACTIVITY — the page renders "no activity yet"
589+ // rather than fabricating "marcus rotated STRIPE_SECRET_KEY" rows
590+ // that never happened.
605591 try {
606592 type AuditResp = {
607593 ok : boolean
@@ -628,7 +614,8 @@ export async function fetchActivity(): Promise<{ ok: true; items: ActivityItem[]
628614 return { ok : true , items }
629615 } catch ( e : any ) {
630616 if ( e ?. status === 401 ) throw e
631- // Fall back to synthesising from resources so the dashboard still renders.
617+ // Fall back to synthesising from resources so the dashboard still
618+ // renders something honest (real resources, real timestamps).
632619 try {
633620 const r = await listResources ( )
634621 const items : ActivityItem [ ] = r . items . slice ( 0 , 8 ) . map ( ( res ) => ( {
@@ -641,7 +628,7 @@ export async function fetchActivity(): Promise<{ ok: true; items: ActivityItem[]
641628 } as unknown as ActivityItem ) )
642629 return { ok : true , items }
643630 } catch {
644- return fake ( { ok : true as const , items : FIXTURE_ACTIVITY } )
631+ return { ok : true as const , items : [ ] }
645632 }
646633 }
647634}
0 commit comments