Envelope budget: Budget page, per-month assignments stored on a Budget tab, rollover and ready-to-assign
build-container / build (push) Successful in 50s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 13:28:02 -04:00
1 parent 837f89e563
commit 4b93d6229d
8 files changed
+235 -5

No files matched your search

+22 -2
View File
@@ -7,6 +7,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse, parse_qs
import statement_loader as L
import budget as B
PORT = int(os.environ.get("PORT", "8765"))
BIND = os.environ.get("BIND", "127.0.0.1") # in Docker: 0.0.0.0 (publish the port to 127.0.0.1 only)
@@ -74,7 +75,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=[], income=[], categories=[], excluded=0, updated="", setup=True, data_dir=str(DATA))
if not L.WORKBOOK.exists(): return dict(tx=[], income=[], budget=[], 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)
@@ -87,6 +88,11 @@ def workbook_data():
if isinstance(r[0], dt.datetime) and r[1] and r[5] is not None:
c, t = pay.get(r[1], ("Other / Unsorted", "Spending"))
tx.append([f"{r[0]:%Y-%m-%d}", r[1], c, t, r[4] or "", round(float(r[5]), 2), r[6] or "", r[8] or ""])
budget = []
if "Budget" in wb.sheetnames:
for r in wb["Budget"].iter_rows(min_row=2, max_col=3, values_only=True):
if r[0] and r[1] and isinstance(r[2], (int, float)):
budget.append([r[0] if isinstance(r[0], str) else f"{r[0]:%Y-%m}", r[1], float(r[2])])
ex, income = 0, []
for r in wb["Excluded Transfers"].iter_rows(min_row=5, max_col=7, values_only=True):
if not isinstance(r[0], dt.datetime): continue
@@ -96,7 +102,7 @@ def workbook_data():
src = (m.group(1).strip().title() if m else (r[2] or "Other").title()) if r[6].startswith("Income") else "Interest"
income.append([f"{r[0]:%Y-%m-%d}", src, "Interest" if r[6].startswith("Interest") else "Paycheck", round(float(r[4]), 2), r[1] or ""])
wb.close()
_DATA["key"], _DATA["val"] = key, dict(tx=tx, income=income, categories=cats, excluded=ex, updated=f"{dt.datetime.fromtimestamp(L.WORKBOOK.stat().st_mtime):%b %-d, %Y %-I:%M %p}")
_DATA["key"], _DATA["val"] = key, dict(tx=tx, income=income, budget=budget, categories=cats, excluded=ex, updated=f"{dt.datetime.fromtimestamp(L.WORKBOOK.stat().st_mtime):%b %-d, %Y %-I:%M %p}")
return _DATA["val"]
def safe_name(name):
@@ -131,6 +137,16 @@ class H(BaseHTTPRequestHandler):
with LOCK:
try: return self._send(200, workbook_data())
except Exception as e: return self._send(500, {"error": str(e)})
if u.path == "/budget":
f = HERE / "app_budget.html"
return self._send(200, f.read_bytes().replace(b"__TOKEN__", TOKEN.encode()) if f.exists() else b"app_budget.html missing", "text/html; charset=utf-8")
if u.path == "/api/budget":
month = parse_qs(u.query).get("month", [B.this_month()])[0]
with LOCK:
try:
if not L.WORKBOOK.exists(): return self._send(200, {"setup": True})
return self._send(200, B.compute(workbook_data(), month))
except Exception as e: return self._send(500, {"error": str(e)})
if u.path == "/api/payees":
with LOCK:
try: return self._send(200, L.uncategorized())
@@ -152,6 +168,10 @@ class H(BaseHTTPRequestHandler):
DATA.mkdir(parents=True, exist_ok=True); p.write_bytes(data)
return self._send(200, {"saved": p.name})
body = self.rfile.read(n)
if u.path == "/api/budget":
req = json.loads(body or b"{}")
with LOCK: out = L.set_budget(req["month"], req["budgets"])
return self._send(200, {"message": f"Saved the {req['month']} budget ({out['rows']} envelopes). Backup: {out['backup']}."})
if u.path == "/api/set_categories":
with LOCK: out = L.set_categories(json.loads(body or b"[]"))
return self._send(200, {"message": f"Updated {out['updated']} payee(s). Backup: {out['backup']}."})