aex/ROADMAP.md
yaroslav k 6d0846d160 docs: mark Phase 2 done; sync architecture, readme
- ROADMAP: all Phase 2 boxes checked; done-notes (HP block already in
  the sidebar; optional Spell fields keep old JSON parseable; rows are
  plain functions inside For's let children)
- ARCHITECTURE: character/*.rs in the tree, Character route contents,
  gaps list drops the character-page items
- README: character page in 'What works today'
2026-08-16 12:44:31 +03:00

12 KiB
Raw Blame History

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.json load in src/statics/mod.rs (loaded twice)
  • clear compiler warnings (unused imports, unused qty in item.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_staticLazyLock, local replacements for num-format/num-traits) (done: lazy_staticLazyLock only; 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.ts with 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 / Deserialize on all entities in src/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 Store derive and serde derive coexist on the same structs (prototype first)
  • add a version field to Settings; add a migration hook for future format changes
  • serialize Dashboard to JSON; save to localStorage via web_sys::Storage (feature-gate to hydrate/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.json as 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.

Done (2026-08-16): implementation in src/state/persist.rs + header buttons + components/modal.rs; e2e-verified in chromium (reload persistence and export/import round trip). Notes:

  • Save = download; Load = file import; Clear = wipe (merged per user decision).
  • Clear writes the blank dashboard back to storage; mock stays the first-visit fallback.
  • Version check is the migration hook: parse_dashboard returns ImportError::{Parse, NewerVersion, OldVersion} (RU messages).

Phase 1.5 — Undo & redo (S, ~0.5-1 day)

Snapshot-based undo rides on the Phase 1 machinery: serialize → push → replace_state. It is page-agnostic: the store.track() trigger already fires on any mutation, so no component changes are needed and every later phase's edits are undoable from day one.

  • state::undo.rs (hydrate-gated): undo/redo stacks of serialized Dashboard, capped (e.g. 100); push coalesced on the store trigger (one snapshot per ~1 s window → one undo step = one edit burst)
  • a new edit after undo clears the redo stack; Clear and import push a snapshot first, so they are undoable too
  • undo() / redo() call replace_state; expose stack depths as RwSignal<usize> for button disabled states
  • header buttons: ↩ Отменить / Вернуть (disabled when empty)
  • shortcuts: Ctrl+Z, Ctrl+Shift+Z (Ctrl+Y); ignore when focus is in an input/textarea/select, so typing never triggers undo
  • unit tests for stack semantics; e2e: edit → undo → redo; reload keeps the undone state (undo writes through autosave)
  • Acceptance: every edit since the last window is reversible with a keystroke; undo survives a reload.

Done (2026-08-16): implementation in src/state/undo.rs; e2e-verified in chromium (buttons, shortcuts, reload, undoable Clear). Notes:

  • Snapshots are Dashboard clones, not JSON: the dashboard is small.
  • HistoryHandle is Copy (RwSignal fields) — context-friendly and capture-safe under edition-2024 closure rules.
  • No std::time::Instant: it panics on wasm32; the settle timer is the only clock. The undo button enables with the pending burst, so a fresh edit is undoable before the 1 s window settles.

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 Spell stub from name-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.

Done (2026-08-16): page in src/components/character/; e2e-verified in chromium (add/undo/redo a spell, remove, reload persistence, attunements with clear). Notes:

  • The HP block was already in the sidebar (hit_points.rs, editable type="number" inputs via utils::adjust_checked) — nothing to add.
  • Spell fields are optional, so name-only spells in old saved JSON still parse.
  • Rows are plain functions called from For's let(...) children: component tags are not allowed directly inside them (view macro).

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 ItemSearchField on a page (Wiki page is the natural home — see Phase 7)
  • "+" adds the BookItem to 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 ItemSearchField here with results list
  • item detail panel: rarity, weight, full entry text from the JSON rest field
  • 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: add item from search, quest CRUD, notes filter, undo shortcuts (persistence round trip is covered by persistence.spec.ts)
  • 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.5 (different components).
  • Phase 1.5 (undo) is page-agnostic: it must land before the edit-heavy phases so their edits are undoable; nothing in Phases 2-7 depends on it.
  • 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.