Skip to content

fix(web): make the dashboard tell the truth about imported data (Phase 0) - #11

Merged
aafre merged 3 commits into
mainfrom
fix/phase-0-ui-honest
Aug 29, 2026
Merged

aafre merged 3 commits into
mainfrom
fix/phase-0-ui-honest

Conversation

@aafre

@aafre aafre commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Why

The dashboard rendered figures that did not match what the API sent. That made every manual test of the import path uninformative — you could not tell a real extraction defect from a display defect. Since the next milestone is importing my own real bank statements end to end, fixing the instrument comes before running the experiment.

What changed

# Defect Fix
1 API sends debit/credit; UI compared against outflow/inflow, so every row rendered as a green inflow and the direction filter did nothing Enum aligned in app.js (4 sites) and index.html (filter options)
2 Budgets read allocated_minor/spent_minor; API sends budget_minor/actual_minor/remaining_minor/over_budget Fields aligned; progress bar now computed from the real pair
3 Descriptions, merchants, categories, account names, filter values and issue messages interpolated into innerHTML unescaped escapeHtml() on all 23 render paths
4 Hardcoded Jun–Aug 2026 dataset stood in on any API failure, so a dead backend looked healthy and 3-month charts could mix real and invented figures FALLBACK_DATA deleted → EMPTY_MONTH + explicit error banner; month menu derived from the current date
5 amount_sign existed in the API but not the UI, so an all-positive credit-card export could only commit under as_written — booking every purchase as income Preview asks for the convention when every amount is an unsigned positive; commit blocked until answered
6 Commit was gated on excluded count alone and let blocking-error rows through to a server-side rejection Disabled with the reason shown; issue badges colour-coded by severity

Verification

Run against a live server with seeded demo data, not by reading the diff:

  • Salary renders is-inflow +£3,500.00; rent/groceries render is-outflow −£1,200.00
  • Direction filter splits 34 rows into 4 credits / 30 debits
  • Seeded eating_out budget: API budget_minor=80000, actual_minor=26000, remaining_minor=54000 → UI shows Budget £800.00, Spent £260.00, Remaining £540.00, bar 32.5%
  • A <script>alert(1)</script> payload in a CSV description renders as literal text; 0 injected script elements
  • All-positive CSV: sign selector appears, commit disabled with "Set the amount sign convention before committing"; selecting debit_positive flips all 5 rows to outflows and unblocks with "Ready to commit 5 transactions"
  • Killing the API mid-session shows the error banner and no figures
  • No console errors throughout

Gate: ruff check ✅ · ruff format --check ✅ · mypy src ✅ · 101 tests passed ✅ · node --check app.js ✅ · git diff --check ✅

Also checked: all 86 DOM ids used by app.js exist in index.html; every new CSS class has a rule; no hardcoded money values or periods remain in either web asset.

Also in this PR

  • README now states that no real PDF statement imports end to end yet, and documents the sign-convention requirement — rather than implying broad bank support.
  • docs/plans/2026-08-29-v0.2-currency-investments-plan.md — the v0.2 plan this is Phase 0 of: real-statement import as the core objective, GBP+INR in the core, then investments as holdings snapshots.

Known follow-up (not a blocker here)

amount_sign is applied to candidates but never persisted or echoed back — ImportBatchModel has no column and ImportBatchResponse has no field. So the UI's if (batch.amount_sign) pre-select can never fire, and there is no server-side record of which convention a committed batch used. The flow works in one sitting because the commit gate reads the live DOM value, and _apply_amount_sign re-derives direction from raw text rather than flipping it, so it stays idempotent. Fix belongs with the Phase 2 migration that adds statement_start/statement_end: one column, one response field.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RYgbBDgmuiRTGt5QNo6313

aafre and others added 2 commits August 29, 2026 21:43
The UI tested for 'outflow'/'inflow' but the API sends 'debit'/'credit',
causing every row to render as an inflow (green, positive) and the
Activity direction filter to do nothing.

Four sites in app.js fixed (candidate preview + activity ledger).
HTML direction filter option values updated to match.
…e 0)

The browser dashboard rendered figures that did not match what the API sent,
which made every manual test of the import path uninformative: you could not
tell a real extraction defect from a display defect.

- Budgets read `allocated_minor`/`spent_minor`; the API sends `budget_minor`,
  `actual_minor`, `remaining_minor` and `over_budget`. Every budget rendered as
  empty with a full bar.
- Descriptions, merchants, categories, account names, filter values and issue
  messages were interpolated into `innerHTML` unescaped. Statement text is
  attacker-influenced in the only way that matters here: it comes from a file.
  All 23 render paths now escape.
- A hardcoded June-August 2026 dataset stood in whenever an API call failed, so
  a dead backend looked like a healthy one and three-month charts could mix real
  and invented figures. Deleted. The API failing now says so, the month menu is
  derived from the current date, and an empty ledger renders as empty.
- `amount_sign` existed in the API but not the UI, so an all-positive
  credit-card export could only be committed under `as_written` - booking every
  purchase as income. The preview now asks for the convention when every amount
  is an unsigned positive, and blocks commit until it is answered.
- Commit was gated on excluded count alone and let rows with blocking errors
  through to a server-side rejection. It is now disabled with the reason shown.

Verified against a running server, not by reading the diff: salary renders as an
inflow and rent as an outflow, the direction filter splits 4 credits from 30
debits, a seeded 800.00 budget shows 260.00 spent and 540.00 remaining at 32.5%,
a `<script>` payload in a description renders as text with zero injected script
elements, an all-positive CSV blocks commit until the convention is set and then
flips all five rows to outflows, and a dead API shows the error banner with no
figures. Gate: ruff, ruff format, mypy, 101 tests, node --check.

README now states that no real PDF statement imports end to end yet, rather than
implying broad bank support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYgbBDgmuiRTGt5QNo6313
Section 1.1 listed the six dashboard defects to ignore while testing. All six are
fixed in this branch, so the table now describes a version of the app that no
longer exists - the worst kind of doc, because it tells a reader to disregard
behaviour that is now correct. The record of what was wrong belongs in this PR's
description, not in a living plan.

The run order kept the curl workaround for setting a batch's sign convention,
which the preview UI now asks for directly. Replaced with what the UI actually
does. Kept the one fact from the deleted table that is still true: OCR needs
Tesseract on PATH and it is not installed here, so image-only PDFs stay
untestable until it is.

Retitled: this is the v0.2 plan, not a guide to a single afternoon's testing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYgbBDgmuiRTGt5QNo6313
@aafre
aafre merged commit 8381e0d into main Aug 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant