Files
Ten31-Portal/ROADMAP.md
T
Jonathan Kirkwood 77eeb3bd7f 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).
2026-07-01 09:21:59 -05:00

9.2 KiB

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.