From 6f4ecd42f7dc9d11e58baaef4492aef82a7aa402 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:01:46 -0400 Subject: [PATCH] First-run setup: upload Bills_Tracker.xlsx and loader_rules.json from the web page when the data folder is empty Co-Authored-By: Claude Sonnet 5.5 --- app.py | 14 ++++++++++++-- app_dashboard.html | 1 + app_page.html | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index a3ebce4..081d05b 100644 --- a/app.py +++ b/app.py @@ -55,6 +55,9 @@ def coverage(sts): return dict(months=months, accounts=out) def state(): + if not L.WORKBOOK.exists(): + return dict(setup=True, data_dir=str(DATA), rules=(DATA / "loader_rules.json").exists(), + files=sorted(p.name for p in DATA.glob("*") if p.is_file())[:50]) res = L.run_preview(DATA) new = res["new"] rows = [dict(date=f"{r.txn.date:%Y-%m-%d}", account=r.txn.account, desc=r.txn.desc[:60], amount=r.txn.amount, kind=r.kind, @@ -71,6 +74,7 @@ _DATA = {} def workbook_data(): """Read-only snapshot of the Bill Log for the web dashboard (computed from raw cells, not cached formulas).""" from openpyxl import load_workbook + if not L.WORKBOOK.exists(): return dict(tx=[], categories=[], excluded=0, updated="", setup=True, data_dir=str(DATA)) key = L.WORKBOOK.stat().st_mtime_ns if _DATA.get("key") == key: return _DATA["val"] wb = load_workbook(L.WORKBOOK, read_only=True) @@ -90,7 +94,11 @@ def workbook_data(): def safe_name(name): name = re.sub(r"[^\w.\- ]", "_", Path(name).name) - if Path(name).suffix.lower() not in (".pdf", ".csv"): raise ValueError("Only PDF or CSV files can be added.") + if name == "Bills_Tracker.xlsx": + if L.WORKBOOK.exists(): raise ValueError("Bills_Tracker.xlsx already exists in the data folder; it won't be overwritten.") + return L.WORKBOOK + if name == "loader_rules.json": return DATA / name + if Path(name).suffix.lower() not in (".pdf", ".csv"): raise ValueError("Only PDF or CSV statements, Bills_Tracker.xlsx (first time only) or loader_rules.json can be added.") p = DATA / name; i = 1 while p.exists(): p = DATA / f"{Path(name).stem}_{i}{Path(name).suffix}"; i += 1 @@ -128,7 +136,9 @@ class H(BaseHTTPRequestHandler): if u.path == "/api/upload": name = parse_qs(u.query).get("name", [""])[0]; data = self.rfile.read(n) with LOCK: - p = safe_name(name); p.write_bytes(data) + p = safe_name(name) + if p.name == "loader_rules.json": json.loads(data) # must be valid JSON + DATA.mkdir(parents=True, exist_ok=True); p.write_bytes(data) return self._send(200, {"saved": p.name}) self.rfile.read(n) if u.path == "/api/import": diff --git a/app_dashboard.html b/app_dashboard.html index fc597b5..ad704b2 100644 --- a/app_dashboard.html +++ b/app_dashboard.html @@ -155,6 +155,7 @@ function renderTx(all){ } async function init(){ const j=await (await fetch("/api/data")).json();if(j.error){document.body.innerHTML="
"+esc(j.error)+"
";return} + if(j.setup){document.body.innerHTML=`

Bills Dashboard

Bills_Tracker.xlsx isn’t in the data folder (${esc(j.data_dir)}) yet. Upload it on the Statement Loader page.

`;return} D=j;$("#updated").textContent="Workbook saved "+j.updated; const yrs=[...new Set(j.tx.map(t=>+t[0].slice(0,4)))].sort().reverse(); $("#year").innerHTML=yrs.map(y=>``).join(""); diff --git a/app_page.html b/app_page.html index 4bed13d..4aa1c1b 100644 --- a/app_page.html +++ b/app_page.html @@ -35,7 +35,7 @@ th{font-size:12px;color:var(--mute);font-weight:600}td.num{text-align:right;font

1 · Add statements

-
Drop PDF or CSV statements here, or click to choose files
+
Drop PDF or CSV statements here, or click to choose files
Supported: Chase Visa PDF · Ally Bank PDF · CVFCU PDF · Capital One CSV download. Adding a different account?
Open statement_loader.py, add one line to ACCOUNTS (a name, a word printed on page 1 of its statement, and the day it closes), then ask Claude to write the parser for that bank’s layout. Any file the app can’t read is listed under “Needs attention”.
@@ -67,6 +67,8 @@ async function refresh(msg){ $("#coverage").innerHTML="Reading statements…"; const r=await fetch("/api/state");const s=await r.json(); if(s.error){alertBox("bad",esc(s.error));return} + if(s.setup){$("#coverage").innerHTML="";$("#preview").innerHTML="";$("#import").disabled=true; + $("#summary").innerHTML="";alertBox("warn",`First-time setup: Bills_Tracker.xlsx isn’t in the data folder (${esc(s.data_dir)}). Drop it into the box above (or copy it into that folder), then reload.`+(s.rules?"":" Also add loader_rules.json if you have one (your name, so transfers between your accounts are recognized)."));return} s.bad.forEach(b=>alertBox("bad",`${esc(b.file)}: the transactions I read add up to ${fmt(b.parsed)} but the statement says ${fmt(b.stated)}. It will not be imported until that matches.`)); s.skipped.forEach(b=>alertBox("warn",`${esc(b.file)}: ${esc(b.why)}`)); s.duplicates.forEach(b=>alertBox("warn",`${esc(b.file)} is a duplicate of ${esc(b.same_as)}; counted once.`));