Add adjudicated ROADMAP: 13 drop, 13 do, 4 escalate
Grounded survey of the tree (backend/frontend/deploy), then adjudicated each P2/P3 item investigate -> debate -> judge. Dropped as not-worth-it (removed): B2 rollup N+1 (fine at scale), F1 money formatters (2 trivial lines), F3 SW cache (stale-shell can't happen), D1 health check (no real race), D3 aarch64 (x86 internal tool), D4 pin SDK (lockfile already deterministic), B5 string caps (SQLite ignores), B6 created_at (already present), B8 VelvetSweatshop (harmless), B9 empty-strip (already handled), F9 loading text / F10 spinner / F11 confirm() (cosmetic). Kept as DO with plans: B3 tests, B4 upload cap, B7 audit type, B10 FK indexes, B11 cli errors, F2 auth-vs-network, F4 import confirm, F6 audit detail, F7 empty-state, F8 role helpers, D2 commit version files, D5 type-check, D6 log-level env. Escalated (your call): B1 pagination (epic, perm-filtered lists), F5 valuation negatives (money write path), D7 default admin creds (auth, onboarding externals), D8 uninstall wording (corrected fact; ties to H1 backup question).
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
# Ten31Portal — ROADMAP
|
||||
|
||||
Deferred / parked work, grounded in a read-only survey of the current tree on 2026-07-01 and
|
||||
adjudicated the same day (investigate → debate → judge over each item). 13 items were dropped
|
||||
as not-worth-it and removed; what remains is either scheduled work (DO) with a ready plan, or
|
||||
your decision (ESCALATE / HIGH). Priority key: **P2** = nice-to-have, **P3** = trivial.
|
||||
|
||||
## Current state
|
||||
|
||||
Working internal fund-admin portal (FastAPI + SQLModel + SQLite backend, React/Vite/Tailwind
|
||||
frontend, StartOS 0.4.0 packaging). Phase-1 (entities, holdings, positions, valuation rounds,
|
||||
sign-off, audit, CSV import, four roles) is built. Phase-2 (external investor / fund-admin
|
||||
accounts, capital accounts, documents) is built but largely **uncommitted** in the working
|
||||
tree. No test suite. Single git commit to date.
|
||||
|
||||
---
|
||||
|
||||
## HIGH — owner's call (not backlog; surfaced, never auto-judged)
|
||||
|
||||
- **H1. Session secret: backup coverage + `change-me` fallbacks.** `start.sh:5-11` persists a
|
||||
random secret to `/data/.session-secret`, but `backups.ts:4` backs up only the `main`
|
||||
volume — if `/data` is a separate volume, a restore regenerates the secret and silently logs
|
||||
everyone out. Separately, `main.ts:27` and `config.py:7` fall back to a hardcoded `change-me`
|
||||
string if the env var is unset. **Reinforced by D8 below:** adjudication confirmed the secret
|
||||
really is regenerated whenever `/data` is wiped, so the backup-coverage question is live.
|
||||
Needs verification of the volume mapping, then a fix. *(security + data continuity)*
|
||||
- **H2. Login hardening.** `auth_router.py:14` has no rate limiting / lockout on failed logins,
|
||||
and `auth_router.py:60` allows a 4-character password minimum. External-facing attack
|
||||
surface. *(security)*
|
||||
|
||||
---
|
||||
|
||||
## DO — adjudicated worth-doing + low-risk (your go-ahead to schedule)
|
||||
|
||||
### Backend
|
||||
- **B3. Backend test suite** (P2). ~3,500-line financial system of record with zero tests.
|
||||
*Plan:* add pytest/pytest-asyncio/httpx as dev deps in `pyproject.toml` (leave runtime deps);
|
||||
add `backend/tests/` with a `conftest.py` overriding `get_session` (database.py:10) to a fresh
|
||||
in-memory SQLite + a TestClient fixture and an authenticated-user fixture; first tests cover
|
||||
auth (login works; anon + cross-user access rejected), one CRUD router round-trip, and the
|
||||
rollup (entity_router.py:36) asserting correct aggregate numbers. Additive; no prod-code change.
|
||||
- **B4. Document upload size limit** (P2). Uploads stream to disk with no cap, on the same volume
|
||||
as the DB — a runaway upload can fill it and take the portal down. *Plan:* add
|
||||
`MAX_UPLOAD_SIZE` to `config.py` (env `TEN31_MAX_UPLOAD_SIZE`, default 50 MB); in
|
||||
`storage.save_upload` (19-30) check size in the stream loop, `unlink` the partial and raise
|
||||
413; optional early Content-Length check; add an over-limit test.
|
||||
- **B7. Fix audit `detail` type label** (P3). `models.py:154` types `detail` as `str | None` but
|
||||
callers store dicts. *Plan:* change annotation to `dict | list | str | None` to match
|
||||
`schemas.py`; add a one-line comment with examples; note it in `record_audit` (audit.py:17).
|
||||
Documentation/label only — no migration, no call-site changes.
|
||||
- **B10. FK indexes** (P3). ~a dozen linking columns have no indexes; zero indexes anywhere.
|
||||
*Plan:* one additive Alembic migration adding `create_index` on the FK columns
|
||||
(users.primary_account_id, holdings.entity_id, positions.holding_id, valuation_rounds.*,
|
||||
valuations.*, audit_logs.actor_user_id, entity_access.*, documents.*,
|
||||
capital_account_statements.*); mirror with `index=True` on the model Fields; `downgrade`
|
||||
drops them. No query-code change.
|
||||
- **B11. cli.py silent delete errors** (P3). `cli.py:123-126` `except Exception: pass` discards
|
||||
real disk/permission failures and orphans files. *Plan:* capture the exception, print a
|
||||
warning to stderr (matching existing style) with the failing `storage_path`, continue the
|
||||
loop; optionally narrow the catch to `OSError`. No DB/API change.
|
||||
|
||||
### Frontend
|
||||
- **F2. AuthContext: network vs auth failure** (P2, medium conf). A momentary connection blip
|
||||
wrongly shows a logged-out login screen. *Plan:* export `ApiError` from `api.ts`; add an
|
||||
`error`/`offline` flag to `AuthState`; change the line-18 bootstrap catch to inspect the error
|
||||
— 401/403 = logged out, network/5xx = offline flag; render a "couldn't reach the server,
|
||||
retrying" banner (with retry) vs. the login screen.
|
||||
- **F4. Import page: add a real confirmation** (P2). One checkbox + Confirm can permanently
|
||||
erase a fund's holdings, positions, and valuation history with no final "are you sure?".
|
||||
*Plan:* in `Import.tsx`, gate the destructive replace (before the commit at ~145-152) behind a
|
||||
confirmation dialog that spells out what's cleared; add per-step progress labels ("Saving
|
||||
members…", "Loading holdings…"). Inputs already persist, so the retry concern is minor.
|
||||
- **F6. AuditLog expandable detail** (P2). The "what changed" column is truncated with no way to
|
||||
read the rest — exactly when the log matters (a dispute). Data's already sent. *Plan:* add an
|
||||
expanded-row state; remove the always-truncate classes / make the row clickable; render a
|
||||
full-width row with pretty-printed JSON in a `<pre>`; add a `title` on the collapsed cell.
|
||||
- **F7. Investments empty-state** (P3, medium conf). An entity with no holdings shows a row of
|
||||
zeros + empty table that reads as "failed to load." *Plan:* in `Investments.tsx`, when
|
||||
`groups.length === 0` render an empty-state block ("No investments recorded yet" + one line of
|
||||
guidance) instead of the table. Display-only.
|
||||
- **F8. Centralize role checks** (P3). Four pages hand-copy role lists; the shared helpers
|
||||
already exist in `api.ts` and just aren't used. *Plan:* add `WRITER_ROLES` + `canEditRound` to
|
||||
`api.ts`; point AuditLog/Import at the existing `isAdmin`; import the shared list in
|
||||
EntitiesList; use `canEditRound` in ValuationWorkflow. Keep every list byte-identical; do NOT
|
||||
merge `fund_admin` (internal) and `fund_administrator` (external) — they are distinct roles.
|
||||
|
||||
### Deploy
|
||||
- **D2. Commit the version stub files** (P2). `index.ts` is tracked and imports 20 untracked
|
||||
`v_0_2_*.ts` files — committing as-is breaks a fresh checkout. *Plan:* stage the 20 files with
|
||||
the modified `index.ts` and commit together (they carry real release notes); verify a clean
|
||||
checkout builds. The missing `v_0_2_2` is intentional (versions are an unordered set).
|
||||
- **D5. Type-check in build** (P2). A `tsc --noEmit` `check` script exists but never runs; it
|
||||
passes clean today. *Plan:* in `s9pk.mk` (~117) change `npm run build` to
|
||||
`npm run check && npm run build`. One line; identical behavior today, catches future type errors.
|
||||
- **D6. `TEN31_LOG_LEVEL` env var** (P3). `start.sh:41` hardcodes `--log-level info`. *Plan:* add
|
||||
`LOG_LEVEL="${TEN31_LOG_LEVEL:-info}"` near the other env defaults and use `"$LOG_LEVEL"` on
|
||||
line 41 — same generate-or-default pattern already in the file. Default stays `info`.
|
||||
|
||||
---
|
||||
|
||||
## ESCALATE — your decision (touches something that matters)
|
||||
|
||||
- **B1. Pagination on list endpoints** (P2, blast radius HIGH). Every list except audit loads all
|
||||
rows, two with an extra query per row — fine now, will slow and time out as data grows.
|
||||
- *Judge's lean:* worth doing, but **split it first** — it's an epic.
|
||||
- *Why it's yours:* changes observable API behaviour across 5 investor-facing endpoints; two
|
||||
lists filter for permissions **in Python after the query**, so a naive page limit could
|
||||
silently return short/wrong lists of capital-account figures. Sequence the mechanical
|
||||
offset/limit pass separately from the two permission-filtered endpoints.
|
||||
- **F5. ValuationWorkflow input validation** (P2, blast radius HIGH). The Value box accepts a
|
||||
negative number and it reaches the DB uncorrected.
|
||||
- *Judge's lean:* fix it, but the robust fix is **not frontend-only**.
|
||||
- *Why it's yours:* `ValuationBulkItem.value_cents` (schemas.py:165) has no `ge=0` and
|
||||
`round_router.py:140` writes it directly — proper fix adds a backend constraint + the three
|
||||
frontend guards, crossing onto a money-write path beyond the item's stated scope.
|
||||
- **D7. Force override of default admin creds** (P3, blast radius HIGH). A normal install ships
|
||||
the top admin account with a password printed in the public setup guide (`admin` / `Ten31`) and
|
||||
nothing forces a change.
|
||||
- *Judge's lean:* worth fixing **now** — external investor/fund-admin accounts are being
|
||||
onboarded, so "I'm the only user" no longer holds.
|
||||
- *Why it's yours:* changes auth on the most powerful account and rewrites first-boot in ~3
|
||||
spots + 2 docs; a careless change could lock you (or a live deploy) out. Suggested direction:
|
||||
reuse the session-secret generate-and-persist pattern to mint a random admin password on
|
||||
first boot and print it once; add a StartOS config field; test an upgrade path.
|
||||
- **D8. Uninstall alert wording** (P3, blast radius LOW — escalated on a corrected fact). The
|
||||
adjudication caught that the original survey premise was wrong: the session secret **is**
|
||||
regenerated whenever `/data` is wiped (`start.sh:5-11`), so the item is valid, not moot. It
|
||||
sits next to the open **H1** backup question. Re-triage together with H1 using the corrected
|
||||
facts rather than as a standalone wording tweak.
|
||||
Reference in New Issue
Block a user