docs: initial wiki standard set (Home, Architecture, DevelopmentGuide, Operations, Roadmap)
commit
390b9ffe56
5 changed files with 515 additions and 0 deletions
146
Architecture.md
Normal file
146
Architecture.md
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
Small FastAPI app rendering Jinja2 templates with HTMX-driven partial
|
||||||
|
updates. SQLite is the only storage. SQLAlchemy 2.x is the ORM; Alembic
|
||||||
|
manages schema.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
```
|
||||||
|
src/quartermaster/
|
||||||
|
main.py FastAPI app factory
|
||||||
|
routes.py Budget configuration HTTP handlers
|
||||||
|
routes_month.py Monthly view HTTP handlers
|
||||||
|
service.py Budget queries, totals, target logic, zero-amount
|
||||||
|
month_service.py Snapshot, deviation, per-month CRUD, month zero
|
||||||
|
models.py SQLAlchemy models and Section enum
|
||||||
|
db.py Engine, session, PRAGMA foreign_keys=ON
|
||||||
|
config.py DB URL resolution
|
||||||
|
templates/ Jinja2 templates (base, index, month, partials)
|
||||||
|
static/app.css All styling
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
```
|
||||||
|
Section enum (budget + month):
|
||||||
|
income, fixed_bill, debt_minimum, food, subscription, other
|
||||||
|
```
|
||||||
|
|
||||||
|
Primary Debt Target is NOT a section. It is a singleton pointer that
|
||||||
|
selects one of the Debt Minimums rows as the current focus. Excluded
|
||||||
|
from the zero-amount math since it has no total of its own.
|
||||||
|
|
||||||
|
## Budget data model
|
||||||
|
|
||||||
|
```
|
||||||
|
entry
|
||||||
|
id, section, name, amount, created_at, updated_at
|
||||||
|
|
||||||
|
debt_target (singleton, id = 1)
|
||||||
|
id, debt_minimum_id FK entry(id) ON DELETE SET NULL, updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
Deleting the entry a target points at nulls the pointer; no cascade to
|
||||||
|
the entry itself.
|
||||||
|
|
||||||
|
## Month snapshot data model
|
||||||
|
|
||||||
|
```
|
||||||
|
month
|
||||||
|
id, year_month UNIQUE, created_at
|
||||||
|
|
||||||
|
month_entry
|
||||||
|
id, month_id FK month(id) ON DELETE CASCADE,
|
||||||
|
section, name, planned, applied,
|
||||||
|
origin_name NULL, origin_planned NULL,
|
||||||
|
source_entry_id FK entry(id) ON DELETE SET NULL,
|
||||||
|
created_at, updated_at
|
||||||
|
|
||||||
|
month_debt_target (singleton per month)
|
||||||
|
month_id PK FK month(id) ON DELETE CASCADE,
|
||||||
|
month_entry_id FK month_entry(id) ON DELETE SET NULL,
|
||||||
|
updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating a month copies every budget entry into `month_entry` and
|
||||||
|
preserves `origin_name` / `origin_planned`. `source_entry_id` links
|
||||||
|
back to the budget but is informational only; the snapshot is
|
||||||
|
self-contained so the month keeps working even after the referenced
|
||||||
|
budget row is deleted.
|
||||||
|
|
||||||
|
### Deviation states
|
||||||
|
|
||||||
|
Computed from `(name, planned)` vs `(origin_name, origin_planned)`:
|
||||||
|
|
||||||
|
* `unchanged` — both match. No tag.
|
||||||
|
* `edited` — name or planned differs. Orange tint + "modified" tag.
|
||||||
|
* `new_in_month` — no origin (row added after snapshot). Blue tint +
|
||||||
|
"new this month" tag.
|
||||||
|
|
||||||
|
### Per-month debt target
|
||||||
|
|
||||||
|
Copied from the budget's current debt target at creation time, resolved
|
||||||
|
through `source_entry_id`. Fully editable per month.
|
||||||
|
|
||||||
|
## Zero-amount calculation
|
||||||
|
|
||||||
|
`sum(income.amount) - sum(non_income.amount)` with
|
||||||
|
`debt_target` excluded. On the budget page there is one value; on the
|
||||||
|
month page there are two (Planned and Applied), each computed over the
|
||||||
|
relevant column.
|
||||||
|
|
||||||
|
Colour tone:
|
||||||
|
|
||||||
|
* zero → green (`tone-zero`)
|
||||||
|
* positive → amber (`tone-positive`): unassigned income
|
||||||
|
* negative → red (`tone-negative`): over-budget
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
### Budget (`src/quartermaster/routes.py`)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET / index page
|
||||||
|
POST /sections/{section}/entries add entry
|
||||||
|
DELETE /entries/{entry_id} remove entry
|
||||||
|
POST /debt-target set / clear debt target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Month (`src/quartermaster/routes_month.py`)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /month/{year_month} view or create-flow page
|
||||||
|
POST /month/{year_month}/create snapshot the budget
|
||||||
|
POST /month/{year_month}/sections/{section}/entries add month entry
|
||||||
|
POST /month/{year_month}/entries/{entry_id} update (name / planned / applied)
|
||||||
|
DELETE /month/{year_month}/entries/{entry_id} remove month entry
|
||||||
|
POST /month/{year_month}/target set / clear per-month target
|
||||||
|
```
|
||||||
|
|
||||||
|
All mutation routes return the section partial plus OOB swaps for:
|
||||||
|
|
||||||
|
* The debt target card when a Debt Minimums row was added, edited, or
|
||||||
|
deleted
|
||||||
|
* The Zero Amount widget on every entry change
|
||||||
|
|
||||||
|
OOB swap is cheaper than re-rendering the full page and keeps totals
|
||||||
|
in sync without a reload.
|
||||||
|
|
||||||
|
## HTMX conventions
|
||||||
|
|
||||||
|
* Each month entry row carries three inline inputs (`name`, `planned`,
|
||||||
|
`applied`) with `hx-post` and `hx-trigger="change"`. Each input
|
||||||
|
sends only its own field; the server accepts any subset.
|
||||||
|
* Section partials swap via `hx-target="#section-{section}"` +
|
||||||
|
`hx-swap="outerHTML"`.
|
||||||
|
* OOB swaps use `hx-swap-oob="outerHTML"` on the top-level element of
|
||||||
|
the partial so the same id can be re-rendered anywhere in the page.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
* Service tests hit an in-memory SQLite engine directly.
|
||||||
|
* Route tests use FastAPI's `TestClient` with a dependency override to
|
||||||
|
share the same in-memory engine across threads via `StaticPool`.
|
||||||
|
* Backup script tests shell out and assert on filenames and sqlite
|
||||||
|
round-trips.
|
||||||
148
DevelopmentGuide.md
Normal file
148
DevelopmentGuide.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Development Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
* Python 3.12+
|
||||||
|
* [uv](https://github.com/astral-sh/uv) for dependency management
|
||||||
|
* SQLite (the library, not the CLI; the backup script uses Python's
|
||||||
|
sqlite3 module so no `sqlite3` binary is required)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone ssh://git@forgejo.labbity.unbiasedgeek.com:2222/archeious/quartermaster.git
|
||||||
|
cd quartermaster
|
||||||
|
uv sync
|
||||||
|
uv run alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
The migration creates `./quartermaster.db` on first run. Override with
|
||||||
|
`QUARTERMASTER_DB_URL=sqlite:///path/to/other.db` if you want the data
|
||||||
|
file elsewhere.
|
||||||
|
|
||||||
|
## Run the app
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run uvicorn quartermaster.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://127.0.0.1:8000.
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests use an in-memory SQLite engine; no separate migration step
|
||||||
|
needed.
|
||||||
|
|
||||||
|
## Developing against a throwaway DB
|
||||||
|
|
||||||
|
Dev churn that needs a clean schema (autogenerating a new migration,
|
||||||
|
loading a scratch dataset) should target a throwaway path, never
|
||||||
|
`./quartermaster.db`. See [Operations](Operations) for why.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \
|
||||||
|
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
||||||
|
uv run alembic upgrade head
|
||||||
|
|
||||||
|
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \
|
||||||
|
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
||||||
|
uv run uvicorn quartermaster.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/quartermaster/
|
||||||
|
main.py FastAPI app factory
|
||||||
|
routes.py Budget configuration handlers
|
||||||
|
routes_month.py Monthly view handlers
|
||||||
|
service.py Budget service: entries, target, zero-amount
|
||||||
|
month_service.py Month service: snapshot, deviation, zero-amount
|
||||||
|
models.py SQLAlchemy models, Section enum
|
||||||
|
db.py Engine, session, PRAGMA foreign_keys=ON
|
||||||
|
config.py DB URL resolution
|
||||||
|
templates/
|
||||||
|
base.html
|
||||||
|
index.html Budget config page
|
||||||
|
month.html Month view page
|
||||||
|
month_create.html Create-flow for missing month
|
||||||
|
partials/
|
||||||
|
section.html Budget section card
|
||||||
|
target_card.html Budget debt target card
|
||||||
|
budget_zero.html Budget Zero Amount widget
|
||||||
|
month_section.html Month section card
|
||||||
|
month_target.html Month debt target card
|
||||||
|
month_zero.html Month Zero Amount widget (Planned/Applied)
|
||||||
|
month_nav.html Prev / next / picker / back-to-config
|
||||||
|
static/app.css
|
||||||
|
alembic/
|
||||||
|
env.py Includes backup-db.sh hook before migrations
|
||||||
|
versions/ Migration files
|
||||||
|
scripts/
|
||||||
|
backup-db.sh sqlite3.Connection.backup wrapper
|
||||||
|
tests/
|
||||||
|
test_service.py
|
||||||
|
test_routes.py
|
||||||
|
test_month_service.py
|
||||||
|
test_month_routes.py
|
||||||
|
test_zero_amount.py
|
||||||
|
test_backup_script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Issue-driven work
|
||||||
|
|
||||||
|
Every change ties to a Forgejo issue. Branch naming:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat/<issue>-<slug> new feature
|
||||||
|
fix/<issue>-<slug> bug fix
|
||||||
|
refactor/<issue>-<slug> internal change
|
||||||
|
chore/<issue>-<slug> tooling, deps, docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atomic commits
|
||||||
|
|
||||||
|
Each commit is one logical change. Typical PRs in this repo have 3-5
|
||||||
|
commits grouped by layer (data model, service, routes + templates,
|
||||||
|
tests, docs). Commit message subject under 72 chars, body under 80,
|
||||||
|
trailers include `Refs #<issue>` and the `Co-Authored-By` line for
|
||||||
|
Claude sessions.
|
||||||
|
|
||||||
|
### Merge flow
|
||||||
|
|
||||||
|
PRs merge via the Forgejo API (or web UI merge button), NOT `git merge`
|
||||||
|
locally. After the merge call:
|
||||||
|
|
||||||
|
1. Delete the remote branch.
|
||||||
|
2. `git checkout main && git pull --ff-only`.
|
||||||
|
3. `git branch -d <branch>` locally.
|
||||||
|
4. Close the tracking issue manually via the API (the `Closes #N`
|
||||||
|
keyword is unreliable on this Forgejo instance).
|
||||||
|
|
||||||
|
## Adding a schema change
|
||||||
|
|
||||||
|
1. Edit `src/quartermaster/models.py`.
|
||||||
|
2. `uv run alembic revision --autogenerate -m "short description"`.
|
||||||
|
The backup hook runs before alembic touches the DB.
|
||||||
|
3. Inspect the generated migration under `alembic/versions/` and edit
|
||||||
|
if needed (autogenerate misses server defaults, check constraints,
|
||||||
|
and index renames).
|
||||||
|
4. `uv run alembic upgrade head`. The backup hook runs again.
|
||||||
|
5. Write tests, update templates / routes.
|
||||||
|
|
||||||
|
## UI verification
|
||||||
|
|
||||||
|
Type checks and pytest do not verify UI behaviour. For any change that
|
||||||
|
affects the HTML, also:
|
||||||
|
|
||||||
|
1. Start the server against a throwaway DB.
|
||||||
|
2. Curl the affected endpoints and check for the expected classes and
|
||||||
|
text in the response bodies.
|
||||||
|
3. If practical, open the page in a browser and exercise the feature.
|
||||||
|
Claude cannot do this step from the CLI; ask the human.
|
||||||
39
Home.md
Normal file
39
Home.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Quartermaster
|
||||||
|
|
||||||
|
Household budget tracker. FastAPI + HTMX frontend, SQLite backend, managed
|
||||||
|
with `uv`. Built to practise zero-based budgeting: every income dollar
|
||||||
|
should be assigned to a category on the plan side, and reconciled against
|
||||||
|
real spending on the applied side.
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
* [Architecture](Architecture) — data model, routes, snapshot semantics,
|
||||||
|
zero-amount calculation
|
||||||
|
* [DevelopmentGuide](DevelopmentGuide) — setup, run, test, project
|
||||||
|
layout, conventions
|
||||||
|
* [Operations](Operations) — backup script, restore, DB safety rule,
|
||||||
|
alembic hook, throwaway-DB pattern
|
||||||
|
* [Roadmap](Roadmap) — shipped and deferred work
|
||||||
|
|
||||||
|
## At a glance
|
||||||
|
|
||||||
|
* `/` — the budget configuration (the plan). One card per section with
|
||||||
|
name + amount entries. Primary Debt Target points at a Debt Minimums
|
||||||
|
row.
|
||||||
|
* `/month/YYYY-MM` — a snapshot of the budget for a specific month with
|
||||||
|
an `applied` column per entry. Deviations from the snapshot are
|
||||||
|
visibly tagged.
|
||||||
|
* Top of every page: a Zero Amount header. Green when every dollar is
|
||||||
|
assigned, amber when there is unassigned income, red when over-budget.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Shipped: initial scaffold, monthly snapshot view with deviation tags,
|
||||||
|
database backup script with alembic auto-hook, zero-amount header. See
|
||||||
|
the [Roadmap](Roadmap) for what is next.
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Repo: `archeious/quartermaster` on the self-hosted Forgejo. All work is
|
||||||
|
issue-driven; branches follow `feat/<issue>-<slug>`; PRs merge to main
|
||||||
|
via the API; issues close manually on merge.
|
||||||
117
Operations.md
Normal file
117
Operations.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Operations
|
||||||
|
|
||||||
|
## Database safety rule
|
||||||
|
|
||||||
|
The SQLite database holds real user data. It is gitignored, so losing
|
||||||
|
it means the data is gone.
|
||||||
|
|
||||||
|
**Before any operation that interacts with the database at the schema
|
||||||
|
or destructive level**, run the backup script:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/backup-db.sh <reason>
|
||||||
|
```
|
||||||
|
|
||||||
|
That includes:
|
||||||
|
|
||||||
|
* `rm`, `mv`, truncate, or otherwise replacing `quartermaster.db`
|
||||||
|
* Ad-hoc `sqlite3` sessions that might `DROP`, `DELETE`, or `UPDATE`
|
||||||
|
* Running any script that mutates the DB outside the running app
|
||||||
|
* Running the app in "smoke test" / "exercise the flow" mode (the app
|
||||||
|
writes to the DB; use a throwaway DB for this)
|
||||||
|
|
||||||
|
Routine writes by the running web application on your normal data file
|
||||||
|
are NOT covered by this rule. Those are expected behaviour.
|
||||||
|
|
||||||
|
Alembic runs `scripts/backup-db.sh alembic` automatically before every
|
||||||
|
migration, downgrade, `alembic current`, or `alembic revision
|
||||||
|
--autogenerate`. You do not need to call it manually for ordinary
|
||||||
|
schema work.
|
||||||
|
|
||||||
|
## Backup script
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/backup-db.sh [reason]
|
||||||
|
```
|
||||||
|
|
||||||
|
* Uses `sqlite3.Connection.backup` (Python's online-backup API) so the
|
||||||
|
copy is consistent even if the app is writing. No `sqlite3` binary
|
||||||
|
is required.
|
||||||
|
* Resolves the DB path from `QUARTERMASTER_DB_URL` (sqlite:/// only) or
|
||||||
|
defaults to `./quartermaster.db`.
|
||||||
|
* Writes to `<dir-of-db>/backups/` by default. Override with
|
||||||
|
`QUARTERMASTER_BACKUP_DIR=/any/path`.
|
||||||
|
* Exits 0 when the DB file is missing (safe to call from hooks on a
|
||||||
|
fresh checkout).
|
||||||
|
* Exits 1 on a non-file sqlite URL (e.g. `postgres://...`) or an
|
||||||
|
unsupported URL.
|
||||||
|
|
||||||
|
Filename format:
|
||||||
|
|
||||||
|
```
|
||||||
|
quartermaster-YYYYMMDD-HHMMSS-<slug>.db
|
||||||
|
```
|
||||||
|
|
||||||
|
The slug is derived from the optional reason argument, sanitised to
|
||||||
|
`[A-Za-z0-9-]`. Default reason is `manual`.
|
||||||
|
|
||||||
|
## Retention
|
||||||
|
|
||||||
|
Forever. The dataset is small (kilobytes per month). Prune manually
|
||||||
|
from `backups/` if the directory ever grows uncomfortable.
|
||||||
|
|
||||||
|
## Restore
|
||||||
|
|
||||||
|
A backup file is a complete SQLite database. To restore:
|
||||||
|
|
||||||
|
1. Stop the app (`pkill -f uvicorn.*quartermaster` or Ctrl-C).
|
||||||
|
2. Back up the current state first in case you want to undo the restore:
|
||||||
|
`./scripts/backup-db.sh pre-restore`.
|
||||||
|
3. Copy the chosen backup over the live file:
|
||||||
|
`cp backups/quartermaster-20260417-103000-alembic.db quartermaster.db`.
|
||||||
|
4. Restart the app. Refresh the browser.
|
||||||
|
|
||||||
|
Alembic version tracking travels with the data, so if the backup was
|
||||||
|
made on an earlier schema you may need to `uv run alembic upgrade head`
|
||||||
|
after the restore (which will itself create a backup first).
|
||||||
|
|
||||||
|
## Throwaway-DB pattern for development
|
||||||
|
|
||||||
|
Never smoke-test or experiment against the live data file. Use a tmp
|
||||||
|
path:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db
|
||||||
|
export QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups
|
||||||
|
uv run alembic upgrade head
|
||||||
|
uv run uvicorn quartermaster.main:app --reload
|
||||||
|
# when done
|
||||||
|
unset QUARTERMASTER_DB_URL QUARTERMASTER_BACKUP_DIR
|
||||||
|
rm -rf /tmp/qm-dev.db /tmp/qm-dev-backups
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running in "production"
|
||||||
|
|
||||||
|
There is no prod. This is a local single-user app. Run the uvicorn
|
||||||
|
dev server and reach it at http://127.0.0.1:8000. For automatic
|
||||||
|
restart on crash, wrap the command in a `systemd --user` unit or a
|
||||||
|
supervisor of your choice; the app itself does nothing special.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "no such table" on first request
|
||||||
|
|
||||||
|
You forgot `uv run alembic upgrade head` after cloning. The backup
|
||||||
|
hook will print "no database at ..., nothing to back up" the first
|
||||||
|
time, which is expected.
|
||||||
|
|
||||||
|
### Alembic reports a revision it cannot locate
|
||||||
|
|
||||||
|
The DB is on a schema version whose migration file is not in the
|
||||||
|
current checkout. Check out the branch that introduced that revision
|
||||||
|
or downgrade the DB to a known-good revision before continuing.
|
||||||
|
|
||||||
|
### Backup script exits 1 on a postgres-style URL
|
||||||
|
|
||||||
|
Intentional. The script is sqlite-specific. Switch the URL back to
|
||||||
|
sqlite:///... or do the backup manually via your Postgres tooling.
|
||||||
65
Roadmap.md
Normal file
65
Roadmap.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## Shipped
|
||||||
|
|
||||||
|
| # | Title | Merged |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Initial scaffolding and single-month budget MVP | 2026-04-17 |
|
||||||
|
| 3 | Monthly budget view with snapshot-from-config and applied tracking | 2026-04-17 |
|
||||||
|
| 5 | DB backup script invoked before every schema or destructive DB operation | 2026-04-17 |
|
||||||
|
| 7 | Zero Amount header at the top of budget and month pages | 2026-04-17 |
|
||||||
|
|
||||||
|
## Deferred / on the horizon
|
||||||
|
|
||||||
|
Things we have explicitly called out as future work. Not yet issues;
|
||||||
|
file one when the time comes.
|
||||||
|
|
||||||
|
### Transaction log behind applied
|
||||||
|
|
||||||
|
Replace the hand-edited `applied` value with a log of dated
|
||||||
|
transactions per entry per month. `applied` becomes a computed sum.
|
||||||
|
Implies a new `month_transaction` table, a UI for entering
|
||||||
|
transactions, and a migration path that preserves existing applied
|
||||||
|
values as an opening balance.
|
||||||
|
|
||||||
|
### Month close-out / carryover
|
||||||
|
|
||||||
|
When a month ends, allow rolling remaining amounts into the next
|
||||||
|
month's planned values. Useful for categories where underspend rolls
|
||||||
|
forward (e.g. Food) and debatable for others (e.g. Rent). Likely a
|
||||||
|
per-section policy.
|
||||||
|
|
||||||
|
### Copy-forward month
|
||||||
|
|
||||||
|
A "new month" that snapshots a previous month rather than the budget
|
||||||
|
config. Useful when reality has drifted far enough from the plan that
|
||||||
|
last month is a better starting point.
|
||||||
|
|
||||||
|
### Cross-month summary / charts
|
||||||
|
|
||||||
|
A view that shows how zero, applied, and category totals have moved
|
||||||
|
over time. Would use the existing `month_entry` data, no new schema.
|
||||||
|
|
||||||
|
### CSV import / export
|
||||||
|
|
||||||
|
Bulk in for seeding a fresh install, bulk out for taxes or external
|
||||||
|
analysis. Month-scoped and budget-scoped variants.
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
Single-user, local-only today. If the app ever runs on a LAN or is
|
||||||
|
exposed externally, add a simple password gate or run behind an
|
||||||
|
SSO-aware reverse proxy.
|
||||||
|
|
||||||
|
### Styling pass
|
||||||
|
|
||||||
|
Current CSS is functional but minimal. A typography and spacing pass
|
||||||
|
would not change semantics but would improve the "looks like a
|
||||||
|
spreadsheet" feel.
|
||||||
|
|
||||||
|
## Out of scope (probably forever)
|
||||||
|
|
||||||
|
* Multi-currency
|
||||||
|
* Multi-user / shared budgets
|
||||||
|
* Mobile app
|
||||||
|
* Cloud sync
|
||||||
Loading…
Reference in a new issue