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=" Bills_Tracker.xlsx isn’t in the data folder (Bills Dashboard
${esc(j.data_dir)}) yet. Upload it on the Statement Loader page.
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”.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.`));