ARCHITECTURE: LazyLock, tracked Cargo.lock, dedup'd foundry.json, warnings cleared. ROADMAP: phase 0 checkboxes closed; MODERNIZE: lazy_static marked applied, rest pending.
8.8 KiB
8.8 KiB
ROADMAP — aex
A D&D 5e character sheet and campaign dashboard. One page per concern: character, inventory, quests, notes, contacts, item reference. Built with Leptos 0.8 (SSR + hydration), actix-web, SCSS.
MVP definition
The MVP is done when a player can run a real session with the app:
- the app opens with last session's state (survives tab close / reload)
- the player can track HP, spell slots, hit dice, death saves, money, inventory (add, remove, move items — including from item search)
- the player can record quests, notes, and contacts as the session happens
- the player can look up any item's stats and add it to inventory
- the player can back up the dashboard to a JSON file and restore it
Current state (Feb 2026)
| Area | Status |
|---|---|
| Header: campaign name, image, in-game date, session steppers | Works |
| Header: Save / Load / Clear buttons | Dead (no persistence) |
| Sidebar: image, names, class/level/XP, HP, hit dice, death saves, spell slots | Works |
Character page / |
Stub — renders literal text "character" |
Inventory page /inv: personal + common, money, item rows |
Works |
| Item search (nucleo over ~5k bundled 5e.tools items) | Component exists, wired to no page |
| Item "+" (add) and "…" (details) buttons | Dead |
| Quests / Notes / Contacts | Models + mock data exist, zero UI, no routes |
| Prepared spells | Model exists, list is empty; Spell is a @TODO stub |
| Attunements | Model exists, no UI |
| Settings | Empty struct |
| Persistence | None — hardcoded Dashboard::mock() on every load |
| e2e tests | Example Playwright spec only |
| README | Stock Leptos template |
Non-goals (explicitly out of MVP)
- accounts, multi-user, or sync across devices
- server-side database
- dice rolling, character builder, full rulebook
- multiple campaigns / dashboards switching
- images hosted by the app (remote URLs are fine)
Effort legend
- S — ~0.5 day
- M — ~1-2 days
- L — ~3-5 days
Phase 0 — Build hygiene (S, ~0.5-1 day)
Cheap, unblocks the rest. Can happen any time; do it first.
- rewrite README (currently the stock template)
- deduplicate
foundry.jsonload insrc/statics/mod.rs(loaded twice) - clear compiler warnings (unused imports, unused
qtyinitem.rs) - decide Cargo.lock policy — currently gitignored, builds are non-reproducible; commit it (done: committed 2026-08-04)
- [~] dependency and idiom cleanup — see
MODERNIZE.md(drop unused deps,lazy_static→LazyLock, local replacements fornum-format/num-traits) (done:lazy_static→LazyLockonly; the rest stays pending) - [~] decide on the under-consideration unused deps from
MODERNIZE.md(seq-macro,leptos-use,http,console_log,js-sys); the rest (itertools,thiserror,anyhow,pretty_env_logger,dotenvy) stay (decision: nothing removed; revisit later) - replace
end2end/tests/example.spec.tswith a smoke test (app boots, nav renders, no console errors)
Phase 1 — Persistence: localStorage + JSON export/import (L, ~3-5 days)
The foundation. Every later phase depends on it.
- derive
serde::Serialize/Deserializeon all entities insrc/entities/mod.rs(Dashboard, PlayerData, Balance, Inventory, InventoryEntry, InventoryItem, QuestBook, Quest, NoteBook, Note, ContactBook, Contact, SpellSlots, Attunements, PreparedSpells, Spell, HitDice, DeathSaveThrows, Name, Settings; enums: InventoryKind, ContactStatus, PreparedSpell) - verify
Storederive andserdederive coexist on the same structs (prototype first) - add a
versionfield toSettings; add a migration hook for future format changes - serialize Dashboard to JSON; save to localStorage via
web_sys::Storage(feature-gate tohydrate/csr; SSR renders mock/empty) - auto-save on change — debounced effect over the
Store— plus manual Save button - Load button: restore from localStorage with an overwrite confirm
- Clear button: wipe localStorage with a confirm
- export: download
dashboard.jsonas a blob - import: file input → parse → validate version → replace state
- on startup, hydrate from localStorage; keep
mock()as the fallback / "New dashboard" - Acceptance: close the tab, reopen — state is intact. Export → import into a fresh browser — identical state.
Phase 2 — Character page (M, ~1-2 days)
/ currently renders the literal string "character".
- player card: portrait, name, class, level, XP (data already in sidebar)
- HP block: temp / current / max,
Numput-style editing - prepared spells UI: render Vacant/Occupied slots; free-text spell name entry
(upgrade the
Spellstub fromname-only to name + optional level/description) - attunements UI: 3 slots, free text, clear button (model exists)
- Acceptance: player can set prepared spells and attunements; they persist.
Phase 3 — Inventory & item search end-to-end (L, ~3-5 days)
Rows are editable and move/remove works. Search exists but is orphaned; "+" and "…" buttons are dead.
- host
ItemSearchFieldon a page (Wiki page is the natural home — see Phase 7) - "+" adds the
BookItemto the personal inventory: merge into a row with the same name, else new row, quantity 1 - "…" opens an item detail view: description, rarity, weight, tags, source data
(
BookItem.rest) - free-form add-entry UI: add a row (name/quantity/weight) without search
- edit a row: name, quantity, weight, description
- stretch: carrying capacity — sum row weights per inventory, show total vs limit
- Acceptance: search "potion" → click + → a row appears in the personal inventory with the correct weight; state persists.
Phase 4 — Quests page (M, ~1-2 days)
New route /quests. Model and mock data exist; zero UI.
- add a completion state to
Quest(the model lacks one today) - quest list: title, giver (Name), taken/task locations, dates, reward (Balance,
reuse
Numput), description - add / update / delete quest; toggle complete
- sort by deadline; highlight overdue
- Acceptance: create a quest, reload — it is still there; mark it complete.
Phase 5 — Notes page (M, ~1-2 days)
New route /notes. Model and mock data exist; zero UI.
- note list: content, in-game date, session, tags; add / edit / delete
- tag chips; filter by tag or by session
- quick-add defaults to the header's current session and in-game date
- Acceptance: log a note mid-session, filter by tag, reload — still there.
Phase 6 — Contacts page (M, ~1-2 days)
New route /contacts. Model and mock data exist; zero UI.
- contact cards: name, alias, status, location, notes, optional image
- status filter: Alive / Dead / DeadBygone / Unknown
- add / edit / delete contact
- Acceptance: find a contact by status filter; edit its notes; persists.
Phase 7 — Wiki page: item reference (M, ~1-2 days)
/wiki currently renders "books". Search infrastructure already exists.
- host
ItemSearchFieldhere with results list - item detail panel: rarity, weight, full entry text from the JSON
restfield - filters: rarity / category tags; lazy list or pagination — never render 5k rows
- "Add to inventory" shortcut (reuses the Phase 3 path)
- Acceptance: browse or search items, open a detail, add to inventory.
Phase 8 — Polish, tests, release (L, ~2-4 days)
- responsive SCSS pass (sidebar and header on mobile widths)
- Playwright e2e: persistence round-trip, add item from search, quest CRUD, notes filter
- rewrite README: usage, dev workflow, deploy
- release build via cargo-leptos; verify SSR + hydration; pick a deploy target
- Acceptance: fresh clone →
cargo leptos build→ app runs; e2e green.
Sequencing notes
- Phase 1 before Phases 2-7: every new page must persist. Building pages first against mock data and persisting later costs a data-migration pass instead.
- Phases 2 and 3 can run in parallel after Phase 1 (different components).
- Phases 4-6 are independent of each other and share one CRUD-list pattern — build the pattern once (e.g. a generic list editor) and reuse it.
- Phase 7 reuses the Phase 3 add-to-inventory path; keep that path shared.
Known debt and risks
- Cargo.lock is gitignored — builds are not reproducible. Decide in Phase 0.
- serde × Store derive on the same structs — unverified combination; prototype before committing to it (Phase 1).
- ~3.9 MB of item JSON bundled via
include_str!— parses on startup in the browser. Fine for MVP; moving to a server function is a post-MVP option. - localStorage has a ~5 MB budget — plenty for text JSON state.
- Browser APIs on SSR — localStorage access must be gated to
hydrate/csr; SSR renders from mock or empty state.