Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
43e7476
docs: Phase 5 design addendum (Home v2, Spending, Bills, Budget, Rece…
coderconnoisseur Oct 21, 2026
d13376c
docs: Phase 5 implementation plan (5a detailed, 5b-5e outlined)
coderconnoisseur Oct 21, 2026
ff30d4f
feat: shared page header and card for the Phase 5 pages
coderconnoisseur Oct 21, 2026
ce1416e
feat: smooth monotone chart path and tick picking
coderconnoisseur Oct 21, 2026
877f444
feat: Home hero card in the Origin style
coderconnoisseur Oct 21, 2026
1b0c2b4
fix: Ask rail busy/focus/touch, palette outside the sheet, one-commit…
coderconnoisseur Oct 21, 2026
8568550
feat: day-by-day calendar with amounts and bill names
coderconnoisseur Oct 21, 2026
a60c4de
feat: Lumen found list with real actions; /ask?draft= prefills
coderconnoisseur Oct 22, 2026
9c13ab8
feat: Home v2 layout, budget and coming-up cards
coderconnoisseur Oct 22, 2026
09f27ca
fix: calendar amounts readable on phones
coderconnoisseur Oct 22, 2026
201a570
fix: e2e dev server builds into .next-e2e so it can't break npm run dev
coderconnoisseur Oct 22, 2026
94916a4
chore: include the e2e build's route types in tsconfig
coderconnoisseur Oct 22, 2026
1dbaf13
fix: Home reads reminders in the shape the API sends (crash on a date…
coderconnoisseur Oct 22, 2026
ca08865
fix: drop the Spending/Bills cards that read fields the backend never…
coderconnoisseur Oct 22, 2026
74995e9
feat: quiet Upload/Export and light upload and export dialogs
coderconnoisseur Oct 22, 2026
b9b9e49
docs: STORY 32, codebase guide and spec backlog for Phase 5a
coderconnoisseur Oct 22, 2026
79d84bb
fix: axis labels compare real edges; 3M/YTD labels name the month
coderconnoisseur Oct 22, 2026
43613a7
fix: an answer for an older chat can't take over the chat on screen
coderconnoisseur Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/api/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def _save_invoice(proposal_id: str, user_id: str, deps: AgentDeps) -> dict | Non
if not session.execute(update(Proposal).where(
Proposal.id == proposal_id, Proposal.user_id == user_id, Proposal.status == "pending")
.values(status="saving")).rowcount:
raise HTTPException(409, f"Proposal is already {p.status}")
raise HTTPException(409, "This proposal was already decided.")
payload = json.loads(p.payload or "{}")
try:
item = submit_invoice(deps.engine, user_id, payload, warnings_seen=True)
Expand Down Expand Up @@ -148,12 +148,13 @@ def _decide(proposal_id: str, user_id: str, decision: str, deps: AgentDeps) -> d
if p is None:
raise HTTPException(404, "Proposal not found")
# claim it first (UPDATE ... WHERE status = 'pending'): of two concurrent decisions only one can apply;
# anything raised below rolls the claim back with the rest of the transaction
# anything raised below rolls the claim back with the rest of the transaction. The 409 doesn't name p.status:
# it was read before the claim, so the loser of a race would still see "pending"
now = datetime.utcnow()
if not session.execute(update(Proposal).where(
Proposal.id == proposal_id, Proposal.user_id == user_id, Proposal.status == "pending")
.values(status=decision, decided_at=now).execution_options(synchronize_session=False)).rowcount:
raise HTTPException(409, f"Proposal is already {p.status}")
raise HTTPException(409, "This proposal was already decided.")
p.status, p.decided_at = decision, now
applied = None
if decision == "approved" and p.type == "set_budget":
Expand Down
7 changes: 5 additions & 2 deletions backend/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ def _own_thread(user_id: str, thread_id) -> ChatThread:


def _thread_for(user_id: str, thread_id: str | None, question: str) -> ChatThread:
"""The thread to save into. A new one is only added to the session: _save_exchange commits it together with its
messages, so a failed save leaves no empty titled thread behind."""
if thread_id:
return _own_thread(user_id, thread_id)
thread = ChatThread(id=str(uuid.uuid4()), user_id=user_id, title=_title(question) or "New chat")
db.session.add(thread)
db.session.commit()
return thread


Expand Down Expand Up @@ -193,8 +194,10 @@ def chat():
_save_exchange(user_id, thread.id, f"{query} [attached: {file.filename}]" if file else query,
result["response"])
except Exception as e:
# Losing history shouldn't cost the user their answer.
# Losing history shouldn't cost the user their answer. A new thread was rolled back with it: don't hand out its id.
db.session.rollback()
if not thread_id:
result.pop("thread_id", None)
logger.exception("Failed to save chat history for user=%s: %s", user_id, e)

return jsonify({"success": True, "data": result}), 200
Expand Down
3 changes: 2 additions & 1 deletion backend/tests/test_api_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def category():
approved = client.post(f"/api/agent/proposals/{pid}/approve", headers=bearer(u1)).json()
assert approved["proposal"]["status"] == "approved" and approved["applied"]["transactions_updated"] == 1
assert category() == "Shopping"
assert client.post(f"/api/agent/proposals/{pid}/reject", headers=bearer(u1)).status_code == 409
late = client.post(f"/api/agent/proposals/{pid}/reject", headers=bearer(u1))
assert late.status_code == 409 and late.json()["error"] == "This proposal was already decided."
with engine.connect() as conn:
actions = [r[0] for r in conn.execute(text(
"SELECT action FROM audit_events WHERE proposal_id = :p ORDER BY id"), {"p": pid})]
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_chat_attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_approve_save_invoice_records_once(env):
assert out["proposal"]["status"] == "approved"
with pytest.raises(HTTPException) as e:
_decide(pid, "user-1", "approved", deps)
assert e.value.status_code == 409
assert (e.value.status_code, e.value.detail) == (409, "This proposal was already decided.")
assert _count(engine, ReviewItem) == 1 # one submission (auto-approved: its transaction, else queued for review)
assert _count(engine, Transaction) == 1

Expand Down
15 changes: 15 additions & 0 deletions backend/tests/test_chat_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ def test_post_without_thread_creates_one_titled_from_question(chat):
assert not thread["title"].rstrip("…").endswith(" ")


def test_chat_save_failure_leaves_no_empty_thread(chat, monkeypatch):
import routes.chat as chat_routes

client, post, _, _ = chat

def boom(*a):
raise RuntimeError("db down")

monkeypatch.setattr(chat_routes, "_save_exchange", boom)
res = post(A, "Lost to a failed save")
assert res.status_code == 200 # the answer still comes back
assert "thread_id" not in res.get_json()["data"] # no id for a thread that was never saved
assert client.get("/chat/threads", headers=bearer(A)).get_json()["threads"] == []


def test_post_with_thread_appends(chat):
client, post, _, _ = chat
tid = post(A, "First").get_json()["data"]["thread_id"]
Expand Down
28 changes: 21 additions & 7 deletions docs/CODEBASE-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,26 @@ embeds the demo documents once per server and uploads the files in the backgroun
- **No layout shift from overlays.** `globals.css` sets `scrollbar-gutter: stable` and cancels Radix's scroll-lock
margin (`body[data-scroll-locked]`), so opening a menu or dialog never moves the page. Guarded by
`npm run test:e2e` (Playwright, local only), which drives the dev-only harness `app/dev/layout-shift` (404 in
production). Firefox is opt-in with `E2E_FIREFOX=1`. `html, body` use `overflow-x: clip`, never `hidden`:
production). Firefox is opt-in with `E2E_FIREFOX=1`. The e2e dev server builds into `.next-e2e` (`NEXT_DIST_DIR`),
so running tests never breaks a `npm run dev` in the same checkout. `html, body` use `overflow-x: clip`, never `hidden`:
`hidden` makes `<body>` a scroll container whose scrollbar disappears when a menu opens.
- **Home** (`app/(app)/home/`, UI revamp Phase 2): greeting and a serif brief over a sky band, then the cards.
The main column has Spent this period (a hand-drawn SVG: cumulative now vs the previous period, a forecast to month-end, bill pins,
1W/1M/3M/YTD), Lumen found, and Spending calendar + Recent transactions. The side column has Budget, Upcoming bills, the draft-budget promo and
Activity. `use-home-data.ts` fetches today's endpoints in parallel (transactions, reminders, forecast, review
queue). A failed call empties only its card. Skeletons show only on the first load, so changing the chart range
- **Page skeleton (UI revamp Phase 5).** Every redesigned page is built from `components/page/`: `PageHeader`
(26px title, quiet actions, serif brief, optional control row; `md:pr-12` leaves room for the floating account
menu), `PageCard` (title row with optional `›` link, `extra` and `action`; a skeleton at the card's final height,
so nothing moves when data arrives) and `VendorChip` (initials on a stable per-vendor hue, `vendorChip` in
`lib/home-math.ts`). Buttons on these pages use the `quiet` and `ink` variants, never the default blue. Charts use
`lib/chart.ts`: `smoothPath` (monotone cubic, so a running total never dips between days) and `pickTicks`
(axis labels that never overlap at any width). Approved mockups: `docs/superpowers/specs/2026-10-22-phase5/`.
- **Home** (`app/(app)/home/`, Home v2 from Phase 5a, modelled on Origin's app home): greeting, quiet Upload and
Export, and a serif brief over a sky band. Then a full-width hero: Spent in <Month> (total, change vs the previous
period by the same day, "heading for" from the forecast, smooth now-vs-before lines, bill markers on the axis,
1W/1M/3M/YTD, `+` opens upload). Below it, the main column has "<Month>, day by day" (amount per day, today
outlined, upcoming bill names on future days, always as tall as a six-week month) beside Recent transactions, then
Lumen found as a list whose links act ("Ask Lumen" opens `/ask?draft=` with the question typed but not sent). The
side column has Budget in <Month> (spent of total, % bar, top four categories by spend), Coming up (next four
bills), the draft-budget promo and Activity. `use-home-data.ts` fetches today's endpoints in parallel (transactions, reminders, forecast, review
queue). `/api/analytics/reminders` sends `title`/`predicted_date`/`metadata`; `toReminders` turns that into bills
once (vendor patterns only, rows without a date skipped). A failed call empties only its card. Skeletons show only on the first load, so changing the chart range
never blanks other cards. All the maths is pure, in `lib/home-math.ts` (unit tests: `e2e/home-math.spec.ts`).
**Phase 3 backend:** `GET /api/home/summary` (`api/home.py` over the pure `ai/home_signals.py`, no LLM) supplies
Lumen found and Activity:
Expand Down Expand Up @@ -324,7 +337,8 @@ embeds the demo documents once per server and uploads the files in the backgroun
comparable.
- **The page.** Opening Ask starts a fresh chat with suggestions (from Home's "Lumen found" via
`suggestedQuestions`, then `/chat/suggestions`). Past chats sit in `thread-rail.tsx` (a column on desktop, a Sheet
on small screens). `/ask?q=…` starts a new chat with that question once, then cleans the URL.
on small screens). `/ask?q=…` starts a new chat with that question once, then cleans the URL. `/ask?draft=…`
starts a new chat with the question typed into the composer but not sent (Lumen found, Bills "How to cancel").
- **Answers.** `POST /chat` returns the markdown answer plus its evidence: `sources` (passage `text` and the original's
`file_key`), `steps` (tool, summary, `latency_ms`) and `proposals`. The SQL the agent ran stays server-side. Saved
messages keep only the text, so older answers show citation numbers without passages.
Expand Down
22 changes: 22 additions & 0 deletions docs/direction/STORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,25 @@ with the numbers and the files that prove them. Newest last. Raw numbers live in
never on reload, a rail that doesn't move the conversation, and Caps-Lock-proof ⌘K. Screenshots caught two bugs the
tests then pinned: an unreadable blue selected thread and a 24 px sidebar overflow left over from Phase 1.

## 32. The owner runs the revamp and Home becomes Origin-like (UI revamp, review round + Phase 5a)
- **S:** Running Phases 0–4 locally, the owner found the app still felt like the old one: Home didn't resemble the
inspiration (Origin's app home), labels used a generic all-caps mono font, only one sidebar group was labelled, the
collapse button sat in an empty top bar, and an "Upload an invoice…" chip sent that sentence to the AI as a question.
`/spending` had cards that never loaded, and Home then crashed on real data.
- **T:** Fix what the owner flagged, agree a look for Home and the four inner pages from mockups first, then rebuild
Home on one shared page skeleton without breaking the zero layout-shift rule.
- **A:** Labelled every sidebar group, moved the collapse control into the sidebar header (no desktop top bar), and
switched labels to Funnel Sans in sentence case. Upload follow-ups became an upload button; other "do it yourself"
steps stay in the answer text. Mocked Home plus Spending, Bills, Budget and Receipts in the Origin style (four
mockups built in parallel by subagents) and wrote the Phase 5 spec and plan. Built `PageHeader`/`PageCard`, a
monotone curve and collision-free axis ticks, then the new Home: hero chart with "heading for", a day-by-day calendar
with amounts and bill names, Lumen found as a list whose links act (`/ask?draft=` types a question without sending
it), budget categories and Coming up. Upload and Export went quiet and their dialogs light. Ask fixes ran in
parallel in a separate worktree. Root causes found while testing on the owner's data: Home expected a bills shape the
API never sent (so bills never showed, and the new calendar crashed), three Spending cards read fields no backend
code produces (removed until 5b/5c rebuild them), and the e2e server shared the dev server's `.next` folder.
- **R:** Backend 979 passed (chat thread and its messages saved in one commit; a clear 409 on a lost approval race).
Frontend 128 e2e checks (up from 102): every Home card measured to the same height loading and loaded, ticks that
never overlap at 375 px, light dialogs, a draft that never sends itself, and the real reminders shape in the
fixtures. Screenshots caught three bugs the tests then pinned: a width observer that never attached, cut-off
calendar amounts on phones, and a doubled close button.
Loading
Loading