Skip to content

Finances dashboard: vendors.yaml/obligations.yaml schema doesn't match parsing code (500 crash), hidden as an infinite "Loading finances…" spinner #1438

Description

@ichoosetoaccept

Summary

Two stacked bugs make /finances unusable out of the box:

  1. The backend throws a 500 on the shipped sample data (and on any real data filled in per the documented schema).
  2. The frontend can't tell a failed fetch from "still loading," so instead of showing an error it spins on "Loading finances…" forever.

Bug 1 — vendors.yaml/obligations.yaml schema doesn't match what the code parses

Files: LIFEOS/USER/TELOS/FINANCES/{vendors,obligations,schema}.yaml, PULSE/Observability/observability.ts (handleLifeFinances, VendorYaml, ObligationYaml)

Symptom: GET /api/life/finances500 {"error":"undefined is not an object (evaluating 'v.id.toLowerCase')"}

Root cause: schema.yaml documents (and the shipped sample templates use) this shape:

vendor:      { required: [name, category, direction], properties: [name, purpose, category, direction, match, notes] }
obligation:  { required: [vendor, category, amount, frequency], properties: [vendor, category, amount, frequency, due_day, account, autopay, notes] }

But observability.ts parses them against a disjoint shape:

interface VendorYaml {
  id: string; name?: string
  scope: "business" | "personal" | "mixed"
  cadence: "monthly" | "annual" | "quarterly" | "one_time" | "variable"
  source: "collector" | "manual" | "stripe" | "webhook"
  manual_monthly_usd?: number; manual_annual_usd?: number
  ...
}
interface ObligationYaml {
  id: string; name?: string; scope: "personal"
  cadence: ...; amount_usd: number; category: string
  ...
}

None of id, scope, cadence, source, amount_usd/manual_monthly_usd exist in the documented schema or the shipped template — vendor/amount/frequency do instead. So v.id is undefined for every vendor, and:

const knownLabels = new Set<string>([
  ...resolvedVendors.map(v => v.name.toLowerCase()),
  ...resolvedVendors.map(v => v.id.toLowerCase()),   // <-- throws
])

This means Finances cannot work for any user who fills in these files per their own documented schema — it's a permanent break between the shipped docs/template and the shipped code, not a missing-data edge case.

Bug 2 — frontend can't distinguish "failed" from "loading"

File: PULSE/Observability/src/app/finances/page.tsx

fetch("/api/life/finances")
  .then((r) => (r.ok ? r.json() : null))   // non-ok -> null, same as "not fetched yet"
  .then(setData)
  .catch((e) => setError(String(e)));      // never fires on HTTP errors

data === null is indistinguishable from the pre-fetch state, so the page spins on "Loading finances…" forever instead of showing its own already-built "Failed to load finances" card. growth/page.tsx gets this right via Promise.reject(new Error(...))finances/page.tsx doesn't.

Reproduction

  1. Fresh install, don't run the Finances interview (files still the shipped samples).
  2. Open Pulse dashboard → Finances tab → stuck on "Loading finances…".
  3. curl localhost:31337/api/life/finances shows the actual 500.

Proposed fix

  • Bug 2 (mechanical): Promise.reject(new Error(\HTTP ${r.status}`))on non-ok, matchinggrowth/page.tsx`.
  • Bug 1 (needs a decision): either update schema.yaml + templates to match the code's id/scope/cadence/amount_usd shape, or update the parser to read vendor/amount/frequency per the documented schema. They currently don't share a single field name. Stopgap patch (derives a slugified id from name/vendor when absent, stops the crash) verified locally, but real dollar amounts still won't populate until the schema is reconciled.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions