Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .github/scripts/check_sri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Check the CDN scripts in index.html.

- A script WITH an `integrity` attribute must also have `crossorigin`, and its hash
must match the file the CDN serves today. A wrong hash makes the browser refuse to
run the script, so FinLens would break with no visible error. That fails the check.
- A script WITHOUT `integrity` is reported as a warning for now. Content Security
Policy + SRI for every CDN script is PR #22; once it's on master, change
MISSING_IS_ERROR to True so no new unpinned script can slip in.
"""
import base64, hashlib, pathlib, re, sys, urllib.request

MISSING_IS_ERROR = False

root = pathlib.Path(__file__).resolve().parents[2]
html = (root / "index.html").read_text(encoding="utf-8")
errors = 0

for tag in re.finditer(r"<script\b[^>]*\bsrc=\"(https?://[^\"]+)\"[^>]*>", html):
src, attrs = tag.group(1), tag.group(0)
m = re.search(r"integrity=\"([^\"]+)\"", attrs)
if not m:
level = "error" if MISSING_IS_ERROR else "warning"
errors += MISSING_IS_ERROR
print(f"::{level} file=index.html::No integrity attribute on {src}")
continue
if "crossorigin" not in attrs:
errors += 1
print(f"::error file=index.html::integrity without crossorigin on {src}")
algo, expected = m.group(1).split("-", 1)
req = urllib.request.Request(src, headers={"User-Agent": "finlens-ci"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
body = r.read()
except Exception as e: # a CDN outage should fail loudly, not pass
errors += 1
print(f"::error file=index.html::Could not download {src}: {e}")
continue
actual = base64.b64encode(hashlib.new(algo, body).digest()).decode()
if actual != expected:
errors += 1
print(f"::error file=index.html::SRI hash mismatch for {src}\n in index.html: {algo}-{expected}\n CDN serves: {algo}-{actual}")
else:
print(f"ok {src}")

sys.exit(1 if errors else 0)
31 changes: 31 additions & 0 deletions .github/scripts/check_syntax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Syntax-check every JavaScript file FinLens loads, plus the inline <script> blocks in index.html.

Uses `node --check`, which parses without running anything. FinLens has no build
step, so a syntax error here would otherwise only show up as a blank screen.
"""
import pathlib, re, subprocess, sys, tempfile

root = pathlib.Path(__file__).resolve().parents[2]
failed = 0

def check(path, label):
global failed
r = subprocess.run(["node", "--check", str(path)], capture_output=True, text=True)
if r.returncode:
failed += 1
print(f"::error file={label}::{r.stderr.strip()}")
else:
print(f"ok {label}")

for js in sorted((root / "js").glob("*.js")):
check(js, js.relative_to(root).as_posix())

html = (root / "index.html").read_text(encoding="utf-8")
inline = re.findall(r"<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)</script>", html)
with tempfile.TemporaryDirectory() as tmp:
for n, body in enumerate(inline, 1):
f = pathlib.Path(tmp) / f"inline-{n}.js"
f.write_text(body, encoding="utf-8")
check(f, f"index.html (inline script {n})")

sys.exit(1 if failed else 0)
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

# Runs on every pull request (any base branch, so stacked PRs are checked too)
# and on every push to master. Nothing here deploys or changes the repo.
on:
pull_request:
push:
branches: [master]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
syntax:
name: JavaScript syntax
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: python3 .github/scripts/check_syntax.py

sri:
name: CDN scripts (SRI)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: python3 .github/scripts/check_sri.py
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [2.0.0] — 2026-11-04

### Technical — CI
- **GitHub Actions checks on every pull request** (`.github/workflows/ci.yml`). Two jobs run on every PR, whatever branch it targets, and on every push to `master`. Nothing in the app changes.
- **JavaScript syntax** (`.github/scripts/check_syntax.py`) — runs `node --check` on every file in `js/` and on the inline `<script>` in `index.html`. With no build step, a syntax error otherwise only shows up as a blank screen.
- **CDN scripts (SRI)** (`.github/scripts/check_sri.py`) — for each CDN `<script>` with an `integrity` attribute, checks it also has `crossorigin` and that the hash matches the file the CDN serves today. A wrong hash makes the browser refuse to run the script. Scripts without `integrity` are a warning for now; after #22 (CSP + SRI) merges, set `MISSING_IS_ERROR = True`.
- Part of the **v2.0 — Public launch** milestone. Left open on purpose until the rename and custom domain are ready.

---

## [1.10.5] — 2026-11-02

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Until then, this section is the rule.

FinLens is a **single-origin, zero-build, vanilla JavaScript personal finance dashboard** that runs entirely in the browser. There is no server, no npm, no bundler, no framework, and no TypeScript. The entry point is `index.html`; everything else is a plain `.js` file in the `js/` folder.

**Current version:** v1.10.5 (updated in `index.html` footer, `README.md`, and `CHANGELOG.md` on every release).
**Current version:** v2.0.0 (updated in `index.html` footer, `README.md`, and `CHANGELOG.md` on every release).

**What it does:** Imports CSV exports from Ally Bank (Checking + Savings) and Chase credit cards, parses and normalizes them into a unified transaction model, and renders a multi-tab financial dashboard with budgeting, debt tracking, savings forecasting, investment planning, and Claude-powered AI analysis.

Expand Down Expand Up @@ -798,7 +798,7 @@ Multiple Chase card files can be loaded simultaneously. Each gets a unique `acco

---

*Last updated: v1.10.5 — 2026-11-02 (no section changes: Import Logs race fix; v1.10.4: §8, §13 single `~` is literal; §21 excerpts escape `~`; v1.10.3: §13 refusals; §21 declined runs; v1.10.2: §13 summary states its dates; §21 per-month derivations; v1.10.1: §13 `AI_MAX_TOKENS`, thinking and cut-off answers; v1.10.0: §21 AI Eval added; §13 `callClaude()` raw mode; v1.9.6: §14 folder access lasts until the last tab closes, not one page load; v1.9.5: §7 chart guard; v1.9.4: views-db.js changelog reads CRLF checkouts; v1.9.3: §14 folder permission: re-request on click, Reconnect banner, browser copy wins; v1.9.2: §0 branch, PR and approval workflow added; v1.9.1: §4.5 rehydrate transactions loaded from JSON; v1.9.0: §20 Data & Reset added; v1.8.1: §10 re-upload dedup rewritten for the occurrence-suffix fix; v1.8.0: AI Analysis restored with API-key and output hardening; `renderMarkdown()` added; `samples/` added; §1–3, §13, §14, §17 corrected to match the code)*
*Last updated: v2.0.0 — 2026-11-04 (no section changes: CI checks on pull requests; v1.10.5 — 2026-11-02: no section changes: Import Logs race fix; v1.10.4: §8, §13 single `~` is literal; §21 excerpts escape `~`; v1.10.3: §13 refusals; §21 declined runs; v1.10.2: §13 summary states its dates; §21 per-month derivations; v1.10.1: §13 `AI_MAX_TOKENS`, thinking and cut-off answers; v1.10.0: §21 AI Eval added; §13 `callClaude()` raw mode; v1.9.6: §14 folder access lasts until the last tab closes, not one page load; v1.9.5: §7 chart guard; v1.9.4: views-db.js changelog reads CRLF checkouts; v1.9.3: §14 folder permission: re-request on click, Reconnect banner, browser copy wins; v1.9.2: §0 branch, PR and approval workflow added; v1.9.1: §4.5 rehydrate transactions loaded from JSON; v1.9.0: §20 Data & Reset added; v1.8.1: §10 re-upload dedup rewritten for the occurrence-suffix fix; v1.8.0: AI Analysis restored with API-key and output hardening; `renderMarkdown()` added; `samples/` added; §1–3, §13, §14, §17 corrected to match the code)*

---

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> A zero-install, browser-native finance dashboard. Drop in your bank CSVs, get instant clarity on your money.

**Version:** v1.10.5 &nbsp;·&nbsp; **Stack:** Vanilla JS, HTML, CSS &nbsp;·&nbsp; **Install:** None
**Version:** v2.0.0 &nbsp;·&nbsp; **Stack:** Vanilla JS, HTML, CSS &nbsp;·&nbsp; **Install:** None

---

Expand Down Expand Up @@ -260,4 +260,4 @@ See [CHANGELOG.md](CHANGELOG.md) for the full release history.

---

*FinLens v1.10.5 — Built for one person's finances, designed to be useful for anyone's.*
*FinLens v2.0.0 — Built for one person's finances, designed to be useful for anyone's.*
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="en">
<head>
Expand Down Expand Up @@ -326,7 +326,7 @@
<div style="margin-top:auto">
<a class="nav" data-view="settings">⚙️ Settings</a>
<div class="footer">
<div style="color:var(--text-3);font-size:11px">v1.10.5 · FinLens</div>
<div style="color:var(--text-3);font-size:11px">v2.0.0 · FinLens</div>
</div>
</div>
</aside>
Expand Down
2 changes: 1 addition & 1 deletion js/views-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function _renderGeneralSettings(root) {
</select>
</div>
</div>
<div class="muted" style="font-size:11.5px;margin-top:14px">FinLens v1.10.5 ·${State.transactions.length} transactions loaded${State.lastRefresh?' · Refreshed '+dayjs(State.lastRefresh).format('MMM D, YYYY h:mm A'):''}</div>`;
<div class="muted" style="font-size:11.5px;margin-top:14px">FinLens v2.0.0 ·${State.transactions.length} transactions loaded${State.lastRefresh?' · Refreshed '+dayjs(State.lastRefresh).format('MMM D, YYYY h:mm A'):''}</div>`;
root.appendChild(displayCard);

/* ---- Anthropic API Key ---- */
Expand Down
Loading