HouseBook — Family Finance Portal
A self-hosted household finance portal. Local-first, no cloud, no ads, no tracking. Single HTML file backed by a Python HTTP server and a SQLite database.
Getting Started
The portal is three files in one folder:
portal.html— the user interface (browser)server.py— Python HTTP server with REST API on port 8765hhfinance.db— SQLite database with all your data
- Open a terminal in the folder containing those three files.
- Run
python server.py(orpython3 server.pyon macOS/Linux). - The server prints all reachable IP addresses. Open
http://localhost:8765in your browser, or use a network IP for phone access. - Enter the 4-digit PIN (default: 1478). To change it, create a
pin.txtfile with your PIN on the first line and restart the server.
For access outside your home network, install Tailscale on both devices. The portal uses relative URLs and works from any hostname.
server.py is not running. The badge polls every 15 seconds and updates automatically.Session behaviour
The PIN sets a server-side session cookie (HttpOnly, 30-day expiry). Sessions are cleared when the server restarts — you will be asked for your PIN again after a restart. A hard browser refresh does not require re-entering the PIN as long as the cookie is valid. If the session expires while a form is open, the PIN overlay appears without destroying your form data — re-enter the PIN and continue.
📊 Dashboard ↑ Top
Summary of household finances for a chosen period. All figures convert to the selected display currency using the FX rates in Configuration.
Period selector
Options: This Month, Full Year, Last Month, Last Quarter, Last 3 Months, Last 6 Months, All Time, Custom Range. Defaults to All Time. Shared with Analytics and Insights.
Account chips
One chip per active account showing its native-currency balance. Drag to reorder (order persisted in DB). Click a chip to jump to that account's transactions. Balances are starting balance + all transactions.
Charts
- Spending by Category — pie chart of Expense transactions. Click a slice to jump to Transactions filtered by that category and period.
- Monthly Cash Flow — bar chart of income vs expense by month. Click a bar to jump to Transactions for that month.
📈 Analytics ↑ Top
Deeper charts: spending trend, income vs expense over time, top merchants. Period and currency shared globally with Dashboard and Insights. Recalculates from DB.transactions on every visit — always current.
💡 Insights ↑ Top
Automatically derived observations: unusual spending months, largest single expenses, category trends. Recalculates on every visit.
≡ All Transactions ↑ Top
Period filter
Options: This Month, Last Month, Last Quarter, Last 3 Months, Last 6 Months, Full Year (+ year picker), All Time, Custom Range. Defaults to All Time.
Other filters
| Filter | Behaviour |
|---|---|
| Account | Shows transactions for one account or all |
| Type | Expense / Income / Transfer / All |
| Currency | Filter to a single currency (totals update accordingly) |
| Category | Dropdown of all categories (Parent › Child). Selecting a parent shows all its sub-category transactions too |
| Search | Live text search across title, remarks, and category |
Totals header
Shows filtered transaction count, total income (green), and total expense (red), all in the selected display currency. Updates on every filter change and when the global display currency is changed.
Actions
- Edit — click any row to open the edit modal. Category picker shows only valid leaf categories (no parent categories)
- Duplicate — creates a copy of the transaction pre-filled in the Add modal
- Delete — immediate; no undo yet (Phase 1)
- ⇩ CSV — exports all filtered transactions to a CSV file
+ Add Transaction ↑ Top
All adds are server-first — the transaction is not shown in the list until the server confirms the POST. If the server is unreachable, the add fails with a clear error and the form stays filled.
Fields
| Field | Notes |
|---|---|
| Type | Expense / Income / Transfer. Changing type updates the category dropdown |
| Date | Defaults to today |
| Account | Active accounts only. Currency auto-derived |
| Category | Required for Expense and Income. Only leaf categories shown — no parent categories. Filtered by type (Expense pickers show Expense categories only) |
| Title | Free text. As you type (2+ characters), matching titles from past transactions are suggested via autocomplete |
| Amount | Positive number; sign applied by type |
| Remarks | Optional free text |
⇧ Import Transactions ↑ Top
Bulk-import transactions from a PDF bank statement or a CSV file. All parsing is done in-browser — no file is sent to any server.
PDF import (Suyool / Neo)
- Suyool — LBP or USD statements
- Neo (Bank Audi) — DD/MM/YYYY format, multi-line descriptions
- Select account and parser type
- Upload PDF (drag-and-drop or browse)
- Click Parse & Preview
- Review rows — filter by type, check/uncheck
- Click ⇧ Import Selected
CSV import
Select CSV File as the source. Required headers (in any order):
date, account, currency, type, category, title, amount, remarks
- date: YYYY-MM-DD
- type: Expense / Income / Transfer
- amount: positive number (sign is derived from type)
- remarks: optional, can be blank
Click Download Sample CSV for a ready-to-use template. The same preview, dedup check, and Import Selected flow applies to CSV as to PDF.
Duplicate detection
Before import, every row is compared against existing transactions. Matches on date + account + amount + (title or remarks) are skipped. The result card shows the count of skipped duplicates.
△ Monthly Budgets ↑ Top
Track current-month spending against monthly USD limits, sorted biggest-first.
- Click Edit Budgets to set limits or add new category budgets
- Category picker shows only Expense leaf categories
- Progress bars: green <80%, amber 80–100%, red over budget
- Stored in the database — synced across all devices on the same network
☐ By Category ↑ Top
Expense breakdown by category for a chosen period, sorted by amount descending.
- Period: This Month, Full Year, Last Month, Last Quarter, Last 3 Months, Last 6 Months, All Time, Custom Range
- Display currency: USD, EUR, LBP, AED, CAD
- Click any row to jump to Transactions filtered to that category and the same period
🏠 Accounts ↑ Top
Manage accounts. Click any account name (shown in blue) to view all its transactions (defaults to All Time).
Columns
| Column | Notes |
|---|---|
| Name | Clickable — opens that account's transactions with All Time filter |
| Currency | Native currency of the account |
| Type | Bank Account, Cash, Credit Card, Wallet, etc. |
| Starting Balance | Money that existed before tracking began |
| Current Balance | Starting balance + sum of all transactions in native currency |
| Description | Free-text note |
| Status | Active or Archived |
Archive vs delete
- Archive — hides account from Add/Edit forms but preserves all history
- Delete — only allowed for archived accounts with zero transactions
Editing
Renaming an account cascades to all transactions, recurring items, and income sources. Changing the currency does not update existing transactions (each transaction records the currency at the time it happened).
🏷 Categories ↑ Top
Manages the category hierarchy. Maximum two levels: a parent can have multiple sub-categories; sub-categories cannot have children.
Category types
| Type | Badge | Purpose |
|---|---|---|
| Expense | Expense | Shown in Expense and Transfer pickers; Budget page |
| Income | Income | Shown in Income pickers; Household Income category picker |
| Neutral | Neutral | For parent categories containing both Expense and Income sub-categories. Neutral parents are not assignable to transactions directly |
Storage standard
- Sub-categories stored as
Parent > Child(full path) - Top-level categories (no parent, no children) stored as plain name
- Parent categories (those with children) cannot be directly assigned to transactions
Uniqueness rules
- Top-level and parent category names must be unique among themselves
- Sub-categories under different parents may share a leaf name (e.g.
Children > clothesandCommon expenses > clothes) - Detaching a sub-category is blocked if a top-level category of the same name already exists
Actions
- Click name — navigates to Transactions filtered by this category (parent click shows all sub-category transactions)
- Edit — rename, change type, change or remove parent. Rename cascades to all transactions, budgets, recurring items, and income sources. Parent change cascades stored values in transactions
- Delete — if transactions reference this category, a replacement must be chosen first
⚙ Configuration ↑ Top
Household Name
Name and subtitle shown in the header. Stored in localStorage (per browser).
Exchange Rates
All rates are units per 1 USD. EUR, AED, and CAD are auto-fetched from exchangerate-api.com on each load — their fields are read-only in the UI. LBP and LOL must be entered manually and are saved to localStorage. Click Save LBP & LOL Rates to persist them. To reset to defaults, clear the values and save again.
PIN
Default: 1478. To change: create pin.txt in the same folder with your new PIN on the first line, then restart the server. The PIN is validated server-side; a session cookie (HttpOnly, 30-day) is issued on success. Sessions are cleared on server restart.
Backup
Creates a timestamped set of all app files in a backups/ folder. Files included: portal.html, mobile.html, server.py, hhfinance.db, documentation.html, housebook_server.log. Timestamp format: YYYYMMDD-HHMMSS. Recent Backups list shows the 10 most recent sets, sorted by the ISO timestamp inside the manifest JSON.
💰 Household Income ↑ Top
Define recurring expected income streams. This is a planning layer — the transactions table remains the system of record for actual income received.
Fields
| Field | Notes |
|---|---|
| Earner | Father / Mother / Joint / Other |
| Type | Salary, Bonus, Freelance, Rental, Investment, Gift, Refund, Other |
| Account | Required. Destination account. Currency auto-derived from account |
| Category | Required. Income-type leaf categories only |
| Amount + Currency | Expected gross; currency is read-only (from account) |
| Frequency | Weekly / Bi-weekly / Monthly / Quarterly / Annually / One-off |
| Pay Day | Day of month 1–31 |
| Reliability | Guaranteed / Likely / Variable / Uncertain — drives the summary card split |
Link to Repeating Items
Non-One-off sources auto-create a linked Repeating Item. The link is bidirectional — edits to either side sync back. Deleting an income source server-side cascades to the linked repeating item. One-off sources post a single Income transaction immediately with source='income-one-off'.
↻ Repeating Items ↑ Top
Templates for recurring Expense, Income, and Transfer transactions. Transactions are never posted automatically — use Generate Due to review and confirm.
Generate Due flow
- Click ⏱ Generate Due — shows all items where next due date ≤ today
- Adjust date and amount per item as needed
- Check/uncheck items to include or skip
- Click Post Selected — transactions created with
source='recurring'
Mobile Version
A lightweight, touch-optimised companion to the portal, served from the same server at /mobile. Designed for quick transaction entry and balance checks on a phone.
Overview ↑ Top
The mobile version is a single HTML file (mobile.html) served by the same server.py at port 8765. Access it at http://<server-ip>:8765/mobile or the short alias /m.
What it shares with the portal
- Same database — transactions added on mobile appear immediately in the portal after refresh
- Same PIN authentication — 24-hour session, required on every page load
- Same server reachability guard — writes blocked with a clear message if server is unreachable; 15-second health poll
- Same FX rates — reads from the portal's
localStoragekey on startup, then fetches live rates - Same category hierarchy —
Parent › Childdisplay, parent categories excluded from transaction pickers - Same pre-action auth guard (
_guard) — checks reachability and session before opening any form
What it does not have
- Analytics, Insights, By-Category, Budgets, Recurring Items, Household Income, Accounts management
- PDF import, category/account CRUD
Home ↑ Top
Summary card showing net worth, period metrics, and recent transactions.
Period selector
Chips: This Month, Last Month, Last 3 Months, This Year, All Time. Controls income/expense metrics. Account balances are always all-time cumulative regardless of period.
Balance display
- Net worth: sum of all active account balances converted to USD
- Account chips: one chip per active account — native currency balance + USD equivalent for non-USD accounts
FX rates
On startup: applies localStorage overrides (shared with portal), then fetches live EUR/AED/CAD rates from exchangerate-api.com. LBP uses the portal's manual rate. Dashboard re-renders after live rates arrive.
Recent transactions
10 most recent transactions. Tapping a row opens a read-only detail sheet with all fields. The sheet has an Edit button that opens the edit form (with auth guard).
Transactions ↑ Top
Full scrollable list, rendered in batches of 30 with a "Load more" button.
Filters
| Filter | Behaviour |
|---|---|
| Search | Live text search across title, category, remarks |
| Period chips | This Month, Last Month, Last 3 Months, This Year, All Time |
| Type chips | All, Expense, Income, Transfer |
Row actions (swipe left or ⋯ button)
- ✏️ Edit — opens the edit form (guarded)
- ⧉ Duplicate — opens Add form pre-filled
- 🗑 Delete — 5-second undo toast. For Transfer transactions, both legs are deleted together
Add / Edit Transaction ↑ Top
Tap the centre + tab. Edit via row action or the detail sheet. All saves are server-first — form stays filled on error.
Fields by type
| Type | Fields |
|---|---|
| Expense | Date, Account, Category (expense leaf only), Title, Amount, Remarks |
| Income | Date, Account, Category (income leaf only), Title, Amount, Remarks |
| Transfer | Date, From Account, To Account (both alphabetically sorted), Amount deducted, Amount received, Exchange Rate (auto), Title, Remarks |
Category picker excludes parent categories and filters by type. Transfer exchange rate is auto-calculated from account currencies; manual override supported via the Rate field.
Validation
Date must be a valid calendar date (e.g. 31 April is rejected). Title, Amount (positive), and Category (for Expense/Income) are required.
Settings ↑ Top
Via the ☰ header button on the Home view.
- Refresh data — re-fetches all transactions, accounts, and categories
- Server status badge — Connected / Disconnected; updates within 15 seconds of server state change
Architecture
Technical reference for file layout, server API, database schema, and operations.
File Inventory ↑ Top
| File | Purpose |
|---|---|
portal.html | Single-page web UI. All HTML, CSS, JavaScript inline. ~290 KB unminified. |
mobile.html | Mobile companion UI. Served at /mobile and /m. Touch-optimised, transaction entry and balance view only. |
server.py | Python HTTP server on port 8765 (all interfaces). Auth, DB access, backups, error logging. |
hhfinance.db | SQLite 3 database. Eight tables: accounts, categories, transactions, budgets, income_sources, recurring_items, meta, plus SQLite internal tables. |
documentation.html | This file. |
housebook_server.log | Rotating server log (1 MB × 3 files). Records startup, auth events, DB errors, and JS errors from the browser. Created on first request. Included in backups. |
pin.txt | Optional. PIN on first line. Absent → default PIN 1478. Restart server after changing. |
backups/ | Auto-created on first backup. Timestamped file sets + manifest JSON per set. |
CDN dependencies (requires internet on first load)
pdfjs-dist@3.11.174— PDF parsing for statement importschart.js@4.4.1— charts on Dashboard, Analytics, Insights
Server API ↑ Top
All endpoints respond with JSON. Require a valid session cookie except /api/ping, /api/auth, /api/log, and the HTML/favicon static files.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/ping | Health check — no auth; returns {ok:true, ts:...} |
| POST | /api/auth | Validate PIN; sets session cookie on success |
| POST | /api/log | Receive client-side error logs (no auth) |
| GET | /api/transactions | All transactions (JOINs categories for display value). No row limit. |
| POST | /api/transactions | Insert one or many. Auto-resolves category_id. Returns {ok:true, ids:[...]} |
| PUT | /api/transactions/:id | Update one transaction. Auto-resolves category_id. |
| DELETE | /api/transactions/:id | Delete one transaction |
| GET/POST/PUT/DELETE | /api/accounts[/:name] | CRUD accounts. PUT cascades name change to all referencing tables. |
| POST | /api/accounts/reorder | Save dashboard display order |
| GET/POST/PUT/DELETE | /api/categories[/:name] | CRUD categories. PUT cascades name/parent changes to all tables. |
| POST | /api/categories/:name/reassign | Bulk-reassign transactions from one category to another (used before delete) |
| GET/POST/PUT/DELETE | /api/income_sources[/:id] | CRUD income sources. DELETE cascades to linked recurring item. |
| GET/POST/PUT/DELETE | /api/recurring_items[/:id] | CRUD recurring items |
| GET/POST/DELETE | /api/budgets[/:category] | Budget envelopes |
| GET/POST | /api/backup | List or create backup sets |
Error handling
DB-locked errors return HTTP 503 with {"error":"database is locked..."} — client shows "Database busy" toast. Other server errors return HTTP 500. Network errors (server unreachable) are detected by the health poll and by failed fetch() calls; the badge switches to "Disconnected" and all writes are blocked.
Database Schema ↑ Top
SQLite 3. Auto-patched on every startup via idempotent ALTER TABLE statements — no manual migrations ever needed. See schema.sql for a standalone creation script. All timestamps are UTC ISO-8601 with a Z suffix.
accounts
id INTEGER PRIMARY KEY AUTOINCREMENT
name TEXT NOT NULL UNIQUE
currency TEXT NOT NULL DEFAULT 'USD'
description TEXT DEFAULT NULL -- NULL when not provided
is_active INTEGER DEFAULT 1 -- 1 = active, 0 = inactive
starting_balance REAL DEFAULT NULL
account_type TEXT DEFAULT 'Bank Account'
sort_order INTEGER DEFAULT NULL -- display order on dashboard
created_at TEXT DEFAULT (datetime('now'))
updated_at TEXT DEFAULT (datetime('now'))
categories
id INTEGER PRIMARY KEY AUTOINCREMENT
name TEXT NOT NULL -- leaf name only, not full path
type TEXT NOT NULL DEFAULT 'E' -- 'E' Expense | 'I' Income | 'N' Neutral
parent TEXT DEFAULT NULL -- parent category name; NULL = top-level
parent_id INTEGER DEFAULT NULL -- FK → categories.id (denormalised)
created_at TEXT DEFAULT (datetime('now'))
updated_at TEXT DEFAULT (datetime('now'))
-- No UNIQUE constraint on name alone — sub-categories under different parents
-- may share a leaf name. Uniqueness is effectively (name, parent).
transactions
id INTEGER PRIMARY KEY AUTOINCREMENT
date TEXT NOT NULL -- YYYY-MM-DD
account TEXT NOT NULL
currency TEXT NOT NULL DEFAULT 'USD'
type TEXT NOT NULL DEFAULT 'Expense' -- 'Expense' | 'Income' | 'Transfer'
category TEXT DEFAULT NULL -- 'Parent > Child' for sub-cats, leaf for top-level
category_id INTEGER DEFAULT NULL -- FK → categories.id; NULL for legacy rows
title TEXT NOT NULL
amount REAL NOT NULL -- negative = expense/outward, positive = income/inward
remarks TEXT DEFAULT NULL
source TEXT DEFAULT 'manualport'
-- 'manualport' added manually via portal
-- 'manualmob' added manually via mobile
-- 'recurring' posted by Generate Due
-- 'income-one-off' posted when a one-off income source is saved
-- 'import-neo' imported from Neo/Bank Audi PDF
-- 'import-suyool' imported from Suyool PDF
-- 'import-csv' imported from CSV file
-- 'manual' legacy value (pre-split)
created_at TEXT DEFAULT (datetime('now'))
updated_at TEXT DEFAULT (datetime('now'))
budgets
id INTEGER PRIMARY KEY AUTOINCREMENT
category TEXT NOT NULL UNIQUE -- full stored category path
category_id INTEGER DEFAULT NULL -- FK → categories.id
amount_usd REAL NOT NULL DEFAULT 0 -- monthly target in USD
created_at TEXT DEFAULT (datetime('now'))
updated_at TEXT DEFAULT (datetime('now'))
income_sources
id INTEGER PRIMARY KEY AUTOINCREMENT
name TEXT NOT NULL
earner TEXT DEFAULT 'Joint'
source_type TEXT DEFAULT 'Salary'
account TEXT DEFAULT NULL
amount REAL NOT NULL DEFAULT 0
currency TEXT NOT NULL DEFAULT 'USD'
frequency TEXT DEFAULT 'Monthly'
-- 'One-off' | 'Daily' | 'Weekly' | 'Bi-Weekly' | 'Monthly'
-- | 'Bi-Monthly' | 'Quarterly' | 'Semi-Annual' | 'Annual'
pay_day TEXT DEFAULT NULL -- day-of-month or description
start_date TEXT DEFAULT NULL -- YYYY-MM-DD
end_date TEXT DEFAULT NULL -- YYYY-MM-DD; NULL = no end
reliability TEXT DEFAULT 'Likely' -- 'Guaranteed' | 'Likely' | 'Variable'
category TEXT DEFAULT NULL -- full stored category path
category_id INTEGER DEFAULT NULL -- FK → categories.id
is_active INTEGER DEFAULT 1
notes TEXT DEFAULT NULL
recurring_item_id INTEGER DEFAULT NULL -- FK → recurring_items.id (auto-linked on save)
created_at TEXT DEFAULT (datetime('now'))
updated_at TEXT DEFAULT (datetime('now'))
recurring_items
id INTEGER PRIMARY KEY AUTOINCREMENT
name TEXT NOT NULL
type TEXT NOT NULL DEFAULT 'Expense' -- 'Expense' | 'Income' | 'Transfer'
account TEXT DEFAULT NULL
to_account TEXT DEFAULT NULL -- Transfer destination account
category TEXT DEFAULT NULL -- full stored category path
category_id INTEGER DEFAULT NULL -- FK → categories.id
amount REAL NOT NULL DEFAULT 0
currency TEXT NOT NULL DEFAULT 'USD'
frequency TEXT DEFAULT 'Monthly'
pay_day TEXT DEFAULT NULL
start_date TEXT DEFAULT NULL -- YYYY-MM-DD
end_date TEXT DEFAULT NULL -- YYYY-MM-DD; NULL = no end
last_generated_date TEXT DEFAULT NULL -- YYYY-MM-DD of most recent posted transaction
to_amount REAL DEFAULT NULL -- Transfer received amount (when different currency)
linked_income_id INTEGER DEFAULT NULL -- FK → income_sources.id
is_active INTEGER DEFAULT 1
notes TEXT DEFAULT NULL
created_at TEXT DEFAULT (datetime('now'))
updated_at TEXT DEFAULT (datetime('now'))
meta
key TEXT PRIMARY KEY
value TEXT
-- Known migration keys (all one-time, guarded so they only run once):
-- utc_migration_v1 timestamps shifted from local time to UTC
-- null_blank_remarks_v1 empty-string remarks converted to NULL
-- categories_name_unique_drop_v1 UNIQUE constraint removed from categories.name
-- category_fk_v1 category_id / parent_id FK columns added and back-filled
-- drop_color_v1 legacy categories.color column removed
NULL, not empty string, when not provided.Running the Portal ↑ Top
Requirements
- Python 3.8 or newer (no third-party packages needed)
- A modern browser (Chrome, Firefox, Safari, Edge — last 2 years)
- Internet access on first load for CDN libraries (cached afterward)
cd /path/to/housebook
python server.py
The server prints all reachable network IPs on startup. Navigate to http://localhost:8765 and enter your PIN.
Auto-patching
Every startup runs all schema migrations idempotently. New columns are added safely; existing data is never modified. Migration state tracked in the meta table.
Browser Storage ↑ Top
Only three small items in localStorage — everything else is in the DB:
| Key | Contents |
|---|---|
FX | Manually saved LBP and LOL exchange rates (JSON). EUR/AED/CAD are never written here — always dynamic. |
hhName / hhSub | Household name and subtitle from Configuration |
Backup & Restore ↑ Top
Via the portal
Settings → Backup creates a timestamped set in backups/. All five app files are copied. A manifest JSON is written per set for the Recent Backups list.
Manual
cp hhfinance.db backups/hhfinance_$(date +%Y%m%d).db
Restore
Stop the server. Replace hhfinance.db with a backup copy. Restart the server.
Export to CSV
From Transactions, use ⇩ CSV to export all filtered transactions.
Troubleshooting ↑ Top
Badge shows "Disconnected" / PIN screen won't proceed
server.py is not running or not reachable on port 8765. Start or restart it. The badge polls every 15 seconds and will recover automatically.
PIN accepted but data doesn't load
After successful PIN entry the server calls loadData(). If this fails (DB locked, network issue), an error toast appears. Check housebook_server.log for details.
"Database busy" toast
Another process has the SQLite file locked (e.g. DB Browser with the file open). Close the other tool and retry. The portal will not queue or lose your action — just retry after the lock clears.
Server crashes on startup
- Port 8765 in use — close any other
server.pyinstance - DB file locked — close any other tool that has it open
- Permission denied — ensure write access to the folder
Import shows "0 transactions parsed"
Check the diagnostic breakdown (dates found vs amounts found). If "no amounts" is high, the PDF text layer may not match the expected format. The preview table also shows per-row skip reasons.
Exchange rates don't update
Saved LBP and LOL values take precedence over defaults. EUR, AED, and CAD always come from the live API fetch and are never overridden.
Category shows blank when editing a transaction
The stored category value doesn't match any option in the dropdown. This can happen if the category was renamed or deleted after the transaction was created. Re-select the correct category and save.
History & Planning
Version changelog and upcoming work.
📝 Changelog
- Session management: PIN is now shown before a form opens (proactive guard via
withAuth()) rather than after submission — form data is never lost due to an expired session - Mid-action session renewal: if a 401 occurs while a form is open, the PIN overlay appears and re-authenticates without destroying the form; user sees "Session renewed — please retry your action" and can re-submit immediately
- Server connectivity: 15-second background health poll (
GET /api/ping, no auth required); badge switches to "Disconnected" within 15 seconds of the server going down; all writes blocked with clear message while disconnected - DB-locked errors now return HTTP 503 (previously caused a silent socket drop); client shows "Database busy — try again" toast; error logged to
housebook_server.log - All unhandled server exceptions caught, logged, and returned as JSON 500/503 — no more silent connection drops
- Parent categories (those with sub-categories) excluded from all transaction category pickers — only leaf sub-categories and standalone top-level categories can be assigned to transactions
- Category type Neutral added for parent categories that contain both Expense and Income sub-categories
- Categories type filter includes Neutral option; Neutral badge shown in table
- Category rename cascade handles all three cases: leaf rename, parent rename (
OldParent > *prefix), and parent change (detach/attach/re-parent) - Detaching a sub-category cascades all stored
Parent > Childvalues in transactions to plainChild - Period filters: All Time is now the default in Dashboard, Analytics, Insights, and Transactions
- Period filters: This Month added to all screens; wired to
Period._relRange - Transactions period selector unified with Dashboard/By-Category (This Month, Last Month, Last Quarter, Last 3 Months, Last 6 Months, Full Year, All Time, Custom Range)
- Transactions:
TT.init()now callsfilter()— previously bypassed period filter and showed all rows - Navigating to Transactions from Accounts or Categories no longer resets the period; defaults to All Time with the clicked account/category pre-filtered
- Add/Edit Transaction:
category_idnow included in POST/PUT payloads; server auto-resolves text to id - Recurring Items / Income Sources POST and PUT:
category_idresolved and stored server-side - Edit Transaction: save is server-first — modal stays open and shows error on failure; "Updated!" only shown after server confirms
- Add Transaction: transaction only added to memory after server POST confirms — no false "saved" appearance when server is down
- Backup: timestamp separator changed to
-(e.g.20260529-143000); sorted by manifesttimestamp_iso
- Category FK migration:
category_idinteger FK added to all tables;categories.parent_idadded (FK to self). Category renames no longer cascade text — FK stays valid automatically - Category storage standard: sub-categories always stored as
Parent > Child; top-level as plain name. No exceptions - GET
/api/transactionsJOINs categories —categoryresponse field is always the computed display value - UNIQUE constraint on
categories.nameremoved; sub-categories under different parents may share a leaf name - Category delete: transaction count matched against full stored path; reassign dropdown uses stored values
- Fix Missing Categories dropdown: deduplicated using
catStoredValue() /api/transactions: no row limit; fetches all rows by default/favicon.icoserved without auth
- Server + Portal: rotating error log (
housebook_server.log); front-end JS errors forwarded viaPOST /api/log - Categories: sortable headers; Delete with reassign; count badge; click name → filter transactions
- Transactions: category filter uses
fCatdropdown, not text search; prefix match for parent clicks - Budgets: fixed silent save failure for special-character category names; migrated to DB
- Repeating Items ↔ Household Income: bidirectional sync; delete cascades server-side
- Household Income: Account field mandatory; currency auto-derived from account
- Title autocomplete: past transaction titles suggested as you type
- Refresh button: spinning animation + green ✓ on success
- Display currency: dynamically built from USD, EUR, LBP plus any active account currencies — changing it updates all totals immediately
- Import: Neo Bank Audi parser added; source tags corrected; blank remarks stored as NULL
- Import: renamed to "Import Transactions"; CSV import added with sample template download
- PIN: server-enforced session cookie (HttpOnly, 30-day); default PIN 1478
- Backup:
housebook_server.logincluded in backup set
- New Repeating Items and Household Income pages
- Budgets migrated from localStorage to SQLite
- Dashboard chip reorder; Backup page; FX rate status labels
- Analytics period shared globally with Dashboard and Insights
🗺 Roadmap — Phase 1 (Next)
The portal is production-ready for personal use as of v3.6. Phase 1 focuses on UX polish to make the existing feature set feel finished. Items are independently shippable.
Quick wins
| Item | Description | Size |
|---|---|---|
| Undo toast | 5-second window before a delete is committed — tap Undo to cancel | S |
| Keyboard shortcuts | n = new transaction, / = focus search, Esc = close modal, arrow keys in pagination | S |
| Loading skeletons | Placeholder rows during data fetch instead of blank screens | S |
| Empty states | Helpful CTAs when a list is empty ("No transactions yet — add one or import") | S |
| Bulk select | Checkbox column in Transactions → bulk delete / bulk re-categorise | S |
| Sticky table headers | Headers stay visible while scrolling long transaction lists | S |
| Quick filter chips | "This month", "Last 7 days", "Unreviewed" chips instead of always opening the date picker | S |
| Duplicate detection | Warn before saving if a similar transaction exists within 3 days | S |
Information design
| Item | Description | Size |
|---|---|---|
| Dashboard v2 | Net worth trend line, cash flow waterfall, top spending merchants | M |
| Trend indicators | "Groceries this month: $420 ▲12% vs avg" on every category row | M |
| Smart category suggestions | Fuzzy match + frequency-based ranking as you type a transaction title | M |
| Per-account drill-down | Account page with its own mini-dashboard, not just a filtered transaction list | M |
Performance
| Item | Description | Size |
|---|---|---|
| Virtual scrolling | Render only visible rows in the transactions table (currently all ~26k rows rendered) | M |
| IndexedDB cache | Cache DB.transactions locally so reloads are instant | M |
| Lazy-load charts | Defer Chart.js (200KB+) until Analytics is first opened | S |
| Debounce search | Filter only after 200ms pause instead of on every keystroke | S |
HouseBook — local-first, no cloud, no ads, no tracking.
Version 3.6 — updated 2026-05-29.