# Personal Backlog — Architecture ## 1. Philosophy > **One deliverable.** The app ships as a single self-contained HTML file (`webapp/index-style-v2.html`). It talks to the filesystem either through a minimal localhost Python server **or** directly via the browser's File System Access API — no cloud, no accounts, no external CDNs at runtime. > > The user chooses the mode by how they open the app: run `python3 server/server.py` for universal browser support, or open the HTML file directly in Chrome/Edge for a zero-install experience. > > *The source code lives in `web/` as React 18 + JSX files and is compiled to the single-file artifact by `web/bundle.js`. The build step is only needed when changing source; users only ever touch the output.* ## 2. Deployment Topology ### Mode A — API Server (universal browsers) ``` ┌─────────────────────────────────────┐ │ Browser (Chrome / Firefox / Safari)│ │ ───────────────────────────────── │ │ Single self-contained HTML file │ │ ├─ inline CSS │ │ ├─ inline React 18 + app JS │ │ └─ inline SVG icons │ └──────────────┬──────────────────────┘ │ HTTP (localhost) ┌──────────────▼──────────────────────┐ │ Python File Server (~420 LoC) │ │ Python 3 stdlib, zero pip deps │ │ Serves HTML + REST API for disk │ └──────────────┬──────────────────────┘ │ read / write / watch ┌──────────┼──────────┐ ▼ ▼ ▼ backlog.md backups/ stats.jsonl (master) (rotating) (append-only) ``` ### Mode B — Direct File Access (Chrome/Edge, zero server) ``` ┌────────────────────────────────────────┐ │ Browser (Chrome / Edge) │ │ ──────────────────────────────────── │ │ HTML file opened via file:// or │ │ served by any static file server │ │ ├─ inline CSS │ │ ├─ inline React 18 + app JS │ │ └─ inline SVG icons │ └──────────────┬─────────────────────────┘ │ File System Access API │ (showDirectoryPicker) ┌──────────┼──────────┐ ▼ ▼ ▼ backlog.md backups/ stats.jsonl (master) (rotating) (append-only) ``` > **Note:** Firefox and Safari do not support the File System Access API. Opening the HTML file directly in those browsers starts the app in read-only mode with a warning banner. Use Mode A (Python server) for full functionality in any browser. ## 3. Technology Stack | Layer | Choice | Rationale | |-------|--------|-----------| | **Frontend runtime** | React 18 (production UMD, inlined) | Component model for complex UI; inlined so there are no runtime CDN requests. | | **Frontend source** | JSX + CSS in `web/` | Multi-file development ergonomics; compiled to single HTML by bundler. | | **Build tooling** | `bundle.js` + `@babel/core` | Pre-compiles JSX, inlines prod React builds, removes Babel. Dev-only dependency. | | **Styling** | External `styles.css` (inlined at build) | Edited as plain CSS; inlined into the bundle so the output file is self-contained. | | **Icons / graphics** | Inline SVG | Scalable, styleable with CSS, no network requests. | | **Server (API mode)** | Python 3 `http.server` | Ships with macOS/Linux/Windows; zero pip installs. | | **Server (Direct mode)** | File System Access API | Native browser API for Chrome/Edge; zero install. Not available in Firefox/Safari. | | **Master storage** | Markdown file (`backlog.md`) | Human-readable, diff-friendly, matches requirements exactly. | | **Backup storage** | Filesystem directory (`backups/`) | Simple rotation via filename sorting. | | **Stats storage** | JSONL file (`stats.jsonl`) | Append-only, no locking complexity, human-readable, trivial to parse. | | **Transport** | HTTP/1.1 + JSON | Universally supported, trivial to debug with curl. | ## 4. Frontend Architecture The source code is split across files in `web/` and compiled to a single self-contained HTML file in `webapp/`. Logical modules are organized into three layers so that UI components and business logic never know which storage backend is active. ### 4.1 Layered Module Map ``` ┌─────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER │ │ ───────────────── │ │ , , , , │ │ , , , │ │ │ │ React components — only read/write data through props │ │ and callbacks passed down from App. │ ├─────────────────────────────────────────────────────────────┤ │ DOMAIN / BUSINESS LOGIC LAYER │ │ ──────────────────────────── │ │ Parser, buildDataFromStorage, filter logic in App │ │ Parse markdown → data tree; serialize data tree → markdown │ │ Compute stats, enforce Parent Visibility Rule. │ ├─────────────────────────────────────────────────────────────┤ │ INFRASTRUCTURE LAYER │ │ ──────────────────── │ │ Storage, ApiBackend, DirectBackend, SyncPoller │ │ Route reads/writes to disk (API or File System API). │ └─────────────────────────────────────────────────────────────┘ ``` ### 4.2 Source File Reference | File | Layer | Responsibility | |------|-------|----------------| | `app.jsx` | App | Root `` component. All state lives here via React hooks. Storage init, save/load lifecycle, filter state, debounced autosave. | | `storage.jsx` | Infrastructure + Domain | `Parser` (parse/serialize markdown), `ApiBackend`, `DirectBackend`, `Storage` (detect + delegate), `SyncPoller`, `buildDataFromStorage`. | | `tree.jsx` | Presentation | Recursive `` component. Expand/collapse, drag-and-drop reorder, status glyphs, progress bars. | | `admin.jsx` | Presentation | `` — health card, storage stats, backup browser, stats chart, manual actions. | | `filter-panel.jsx` | Presentation | Left sidebar: status/priority/tag/date filters with multi-select. | | `tweaks-panel.jsx` | Presentation | Settings slide-out: density, accent hue, status glyph style. | | `dialogs.jsx` | Presentation | ``, ``, ``. | | `helpers.jsx` | Domain | `walkTree`, `findItem`, `countAll`, `countByStatus`, `useTweaks` hook, formatting utilities. | ### 4.3 State Management All state lives in the `` component via React hooks. No external state library. ```javascript const [data, setData] = useState(buildEmptyData); // data = { entries, history, meta, health, stats, backups } const [storageMode, setStorageMode] = useState('local'); // 'api' | 'direct' | 'local' const [filters, setFilters] = useState({ statuses, priorities, ... }); const [expandedMap, setExpandedMap] = useState({}); // id → bool, persisted to localStorage const [saveState, setSaveState] = useState({ status, lastSaved }); ``` Only `expandedMap` is persisted to `localStorage`. All backlog data is always sourced from `backlog.md`. ### 4.4 Rendering Strategy React 18 reconciliation. The full tree re-renders on data or filter changes; React diffs and patches only changed DOM nodes. Filters are applied in the render path — the canonical `data.entries` is never mutated by filtering. Responsive breakpoints: - Desktop: sidebar filter panel + main tree. - Mobile (< 768 px): filter panel becomes a collapsible drawer. ### 4.5 Dual-Mode Storage `Storage` is a thin router that picks the active backend at boot time. **Detection order:** 1. Probe `GET http://localhost:8080/api/health`. If it responds within 500 ms → use `ApiBackend`. 2. Else if `window.showDirectoryPicker` is available → use `DirectBackend`. 3. Else show a message: "Please run `python3 server.py` or open this page in Chrome/Edge." #### ApiBackend - `load()` → `GET /api/backlog` - `save(content)` → `POST /api/backlog` - `listBackups()` → `GET /api/backups` - `restoreBackup(name)` → `POST /api/backups/restore` - `getStats()` → `GET /api/stats` - `appendStats(event)` → `POST /api/stats` #### DirectBackend - On first launch the user is prompted to pick a **directory** via `showDirectoryPicker()`. - The directory handle is stored in IndexedDB (`pb-storage-v2` → `handles` → `root`). - On reload the handle is retrieved from IndexedDB; `queryPermission()` checks silently — no user gesture needed if permission is still active. If it has lapsed, a connect button is shown. - `load()` → `dirHandle.getFileHandle('backlog.md').getFile()` → read text. - `save(content)` → `dirHandle.getFileHandle('backlog.md', { create: true }).createWritable()` → write → close. - `listBackups()` → iterate `dirHandle.getDirectoryHandle('backups', { create: true })`. - `restoreBackup(name)` → copy backup file handle content over master. - `getStats()` / `appendStats()` → read/write `stats.jsonl` via the same directory handle. - Backup rotation runs in the frontend (same algorithm as server) after every successful save. ## 5. Backend Architecture ### 5.1 Server (`server.py`) A single Python file (~420 LoC) extending `http.server.BaseHTTPRequestHandler`. Zero external dependencies — stdlib only. Responsibilities: 1. Serve `index-style-v2.html` for `GET /`. 2. Handle REST API routes. 3. Perform atomic file writes. 4. Manage backup rotation. 5. Append stats events. 6. Compute SHA-256 integrity markers on write; verify on read for warning purposes only (never block loading). ### 5.2 REST API | Method | Path | Description | |--------|------|-------------| | `GET` | `/` | Serve `index-style-v2.html` | | `GET` | `/api/backlog?checksum=` | Return current markdown content + checksum. If `checksum` matches, return `304 Not Modified`. Checksum mismatch never blocks loading. | | `POST` | `/api/backlog` | Accept JSON `{ content }`. Write atomically, create backup, verify integrity marker is syntactically valid, return new checksum. | | `GET` | `/api/backups` | List backups: `[{ name, size, timestamp, valid }]`. | | `GET` | `/api/backups/` | Download a backup file. | | `POST` | `/api/backups/restore` | Body `{ name }`. Copy backup over master after checking backup is readable. | | `POST` | `/api/export` | Body `{ format: "json" }`. Return structured dump. | | `POST` | `/api/import` | Multipart upload. Validate, then replace master. | | `POST` | `/api/stats` | Body `{ event, payload }`. Append one line to `stats.jsonl`. | | `GET` | `/api/stats?from=&to=&aggregate=` | Read stats events and/or roll-ups. | | `GET` | `/api/health` | `{ status, lastSave, lastBackup, masterSize, backupCount }`. | ### 5.3 Atomic Write Sequence ``` Client POSTs new markdown content │ ▼ Server writes to backlog.md.tmp │ ▼ Server parses tmp file, verifies integrity marker is syntactically valid │ ▼ Server copies tmp → backups/backlog_YYYY-MM-DD_HH-mm-ss.md │ ▼ Server runs backup rotation (prune old files) │ ▼ Server renames tmp → backlog.md │ ▼ Server appends "save_completed" event to stats.jsonl │ ▼ Server returns { ok: true, checksum, saved } ``` ### 5.4 Backup Rotation Algorithm ```python def rotate_backups(backups_dir): files = sorted(glob("backlog_*.md"), key=extract_timestamp) # Keep everything for 7 days for f in files: if age(f) <= 7 days: continue # After 7 days: keep only the newest per calendar day day = extract_calendar_day(f) if f != newest_file_for_day(day): os.remove(f) # Hard cap: never delete the single most recent backup ``` ### 5.5 External Change Detection - **API mode**: polls `GET /api/backlog?checksum=` every 5 seconds. - `304 Not Modified` → nothing to do. - `200 + new content` → file changed externally. - **Direct mode**: calls `Storage.load()` every 5 seconds and compares checksums. In both modes: - If `Store.dirty === false`: auto-reload, show toast notification. - If `Store.dirty === true`: show conflict modal with options: - **Overwrite local** (discard unsaved edits, load disk version). - **Force save** (overwrite disk with local version). - **Download both** (save local as file, then reload disk version). ## 6. Data Layer Details ### 6.1 Master File (`backlog.md`) Exactly as specified in [Requirements §3.2](../requirements/requirements.md). The server and client both know how to parse and generate the three sections (`ENTRIES`, `HISTORY`, `INTEGRITY`). **Checksum algorithm:** ``` sha256( utf8_bytes( entries_section + "\n" + history_section ) ) ``` The integrity marker comment is excluded from the hash. **Checksum policy:** - On **save**, the writer computes the hash and appends the marker. This proves the file was written completely. - On **load**, the reader computes the hash and compares it to the stored marker. - If they match → green status, no banner. - If they mismatch → **yellow warning banner** ("Checksum mismatch — file was edited outside the app"). The file still loads normally. The next save will overwrite the marker with the correct value. - If the marker is missing → same yellow warning. ### 6.2 Stats File (`stats.jsonl`) One JSON object per line, newline-delimited: ```jsonl {"t":"2025-05-10T14:32:01Z","e":"item_created","d":{"id":"task-1","level":3,"project":"proj-1"}} {"t":"2025-05-10T14:35:00Z","e":"save_completed","d":{"size":12400,"ms":45}} ``` The server exposes a simple aggregator that reads the last 90 days of lines and computes roll-ups on demand. For large files, a memory-mapped or seek-from-end strategy can be used. ## 7. Safety Considerations This is a single-user local tool, not a production service. The only real threats are data loss and file corruption. | Concern | Mitigation | |---------|------------| | **Data loss** | Atomic writes (temp file + rename) + automatic backup on every save + rotating backup retention. | | **File corruption** | Integrity marker proves the file was written completely; parser validates structure before accepting. | | **Path traversal** | Backup names are generated by the server, never from user input. Restore validates the file exists and is readable. | | **Network exposure** | Server binds to `0.0.0.0` for convenience (access from phone/tablet on same LAN). No auth — this is intentional for a personal tool. | ## 8. Build & Distribution The repository contains: ``` personal-backlog/ ├── server/ │ └── server.py # Python REST API server (stdlib only) ├── web/ # V2 source (JSX + CSS + bundler) ├── webapp/ │ └── index-style-v2.html # Self-contained SPA (built artifact) ├── backlog.md # master data file (created on first save) ├── backups/ # created automatically └── stats.jsonl # created automatically ``` ### 8.1 Running the App **Mode A — API Server (all browsers):** ```bash python3 server/server.py --port 8080 --dir ~/my-backlog ``` Then open `http://localhost:8080`. **Mode B — Direct Access (Chrome/Edge, zero server):** ```bash open webapp/index-style-v2.html ``` The app will prompt you to pick a directory. Create a folder, select it, and the app will create `backlog.md`, `backups/`, and `stats.jsonl` inside it. **Building the bundle from source:** ```bash cd web && npm install node bundle.js index.html ../webapp/index-style-v2.html ``` ### 8.2 First-Time Setup - **API mode**: If `backlog.md` does not exist in the server `--dir`, the server creates a blank template with an empty `ENTRIES` section, an empty `HISTORY` table, and a valid `INTEGRITY` marker. - **Direct mode**: If the selected directory does not contain `backlog.md`, the frontend creates the same blank template via `DirectBackend`. ## 9. Performance Budget | Target | Limit | How | |--------|-------|-----| | Initial load | < 200 KB transferred | Single HTML, no external assets. | | Startup time | < 1 s for ≤ 1 MB backlog | Parse markdown in one pass; lazy-load admin charts. | | Save latency | < 300 ms | Atomic rename is instant; SHA-256 of 1 MB is ~5 ms. | | Render tree | < 50 ms for 500 items | Reuse DOM nodes where possible; virtual scrolling optional future enhancement. | | Poll overhead | Negligible | 304 responses are empty body; checksum is cached server-side. | ## 10. Future Extensibility (No-Regret Decisions) 1. **Dark mode** — CSS custom properties (`:root` variables) are used for all colors, so a theme switch is a one-line class toggle. 2. **Multi-user** — The server already validates checksums; adding a simple session token or Basic Auth header would be trivial if needed later. 3. **Firefox/Safari direct access** — If those browsers ever gain write access via File System Access API, `DirectBackend` will work without code changes.