Skip to content

aggregator(feed): isFleetFeed never checks payg — one malformed line crashes usage() and /quota #2450

Closed

Description

@dezren39

Problem

isFleetFeed (pm-stack/aggregator/src/feed.ts:207-219) never validates
the optional payg section at all — it checks only version,
generatedAt, quota.pools being an array, and attention being an
array. But the live source serves feed.payg verbatim as the usage
view's PAYG lines, and both the attention-flag builder and the /quota
renderer dereference every line unguarded, so one malformed line in a
feed that passes the gate crashes the whole Usage & billing page —
inside usage() itself, before rendering even starts.

Evidence (verified this run, main 5a34fa70)

  • feed.ts:44: FleetFeed.payg?: PaygLine[] (shape at model.ts:1154-1167:
    string id/label/note, numeric-or-null accruedUsd/unbilledUsd,
    optional estimate { low, high, period }).
  • feed.ts:207-219: isFleetFeed contains no payg check of any
    kind — not array-ness, not entry shape.
  • sources/live.ts:808-815: in usage()'s feed branch (taken whenever
    feed.saas is non-empty), the section is passed through verbatim:
    payg: feed.payg ?? [] — no filter or per-entry guard.
  • sources/live.ts:832: usage() then calls
    buildAttentionFlags(feed.quota, partial) while building its return
    value; derive.ts:852-862 dereferences line.unbilledUsd /
    line.estimate.low|high per line — a null line throws here, inside
    usage(), so every route that awaits usage() fails.
  • render.ts:1684-1700: paygCardHtml maps lines with
    l.accruedUsd.toFixed(2) / l.estimate.low.toFixed(2) etc. — a
    string accruedUsd or a null line throws in the render too.
  • server.ts:486-487 (/quota), :492 (/quota/service/:id),
    :656-665 (/api/quota.json): all await source.usage() with no
    catch, so the throw becomes a 500 for the whole hub page and both
    JSON twins — not just a missing PAYG card.

Laptop Bun probe (~/git/fleet/probe-2026-10-29-laneA-feedpayg/, real
feed.ts + derive.ts + render.ts from main 5a34fa70,
md5-identical, Bun 1.4.2; usage view assembled exactly as
live.ts:811-815 assembles it, flags built exactly as live.ts:832):

payg isFleetFeed buildAttentionFlags (in usage()) renderQuota
valid line (control) true OK OK — line renders
[null] true throws TypeError: null is not an object (evaluating 'line.unbilledUsd') throws on l.accruedUsd
"oops" (non-array string) true OK (iterates chars, no flags) throws usage.payg.map is not a function
{...} (non-array object) true throws TypeError: {} is not iterable OK (empty card — but usage() already threw)
entry with string accruedUsd true OK throws l.accruedUsd.toFixed is not a function
[valid, null] mixed true throws on line.unbilledUsd throws — the valid line never renders

Why it matters

payg is the hub's PAYG dollars card (§1.2) — the metered-spill lines
the fleet's spend discipline reads. The collector emits these lines
from the quota records, so a single drifted or hand-edited feed line
takes down /quota, /quota/service/:id and /api/quota.json
entirely instead of dropping one line. This is the same failure class
as #2427 (attention entries), #2429 (quota pools), #2439
(chargesAttention), #2445 (charges) and #2446 (subscriptions) at the
same gate — the fix should stay consistent with whatever layer those
pin. This issue is the payg field specifically, with its own
consumers (buildAttentionFlags + paygCardHtml).

Acceptance criteria

Dedup

gh searches (open + closed) for payg, isFleetFeed and saas feed
returned no covering issue for the feed payg gate. #1119 is a
contract-test request for the /api/quota.json saas/payg schema —
related, but it does not track this verified crash at the feed gate.
Siblings at the same gate: #2427 (attention), #2429 (quota pools),
#2439 (chargesAttention), #2445 (charges), #2446 (subscriptions) —
each a distinct field and consumer.

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 30 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Lane E resumed task 5338: PR #4195 head 78ffd9a revalidated — MERGEABLE, Codex review on this head completed with no findings, targeted bun tests passed on laptop (feed+charges-feed-gate 49, quota-contract 9). The pm-aggregator test CI run 41392609663 has been stuck QUEUED since 05:12Z (~90 min, infra queue stall, not a failure). Merging per standing merge authority (PRs do not wait for CI when the change itself is verified).

  8. added a commit that references this issue on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Done: PR #4195 merged (merge commit 58ea183); issue #2450 closed by the merge. Codex clean on head 78ffd9a; targeted bun tests passed (49 + 9). Note: CI run 41392609663 was stuck QUEUED from 05:12Z and never started — infra queue stall, recorded in the fleet backlog.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions