2026-08-04 06:13:51 +00:00
|
|
|
|
# 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.
|
|
|
|
|
|
|
2026-08-15 11:58:01 +00:00
|
|
|
|
- [x] rewrite README (currently the stock template)
|
|
|
|
|
|
- [x] deduplicate `foundry.json` load in `src/statics/mod.rs` (loaded twice)
|
|
|
|
|
|
- [x] clear compiler warnings (unused imports, unused `qty` in `item.rs`)
|
|
|
|
|
|
- [x] 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,
|
2026-08-04 06:39:37 +00:00
|
|
|
|
`lazy_static` → `LazyLock`, local replacements for `num-format`/`num-traits`)
|
2026-08-15 11:58:01 +00:00
|
|
|
|
(done: `lazy_static` → `LazyLock` only; the rest stays pending)
|
|
|
|
|
|
- [~] decide on the under-consideration unused deps from `MODERNIZE.md`
|
2026-08-04 11:53:52 +00:00
|
|
|
|
(`seq-macro`, `leptos-use`, `http`, `console_log`, `js-sys`); the rest
|
|
|
|
|
|
(`itertools`, `thiserror`, `anyhow`, `pretty_env_logger`, `dotenvy`) stay
|
2026-08-15 11:58:01 +00:00
|
|
|
|
(decision: nothing removed; revisit later)
|
|
|
|
|
|
- [x] replace `end2end/tests/example.spec.ts` with a smoke test (app boots, nav renders, no console errors)
|
2026-08-04 06:13:51 +00:00
|
|
|
|
|
|
|
|
|
|
## Phase 1 — Persistence: localStorage + JSON export/import (L, ~3-5 days)
|
|
|
|
|
|
|
|
|
|
|
|
The foundation. Every later phase depends on it.
|
|
|
|
|
|
|
2026-08-16 08:50:41 +00:00
|
|
|
|
- [x] derive `serde::Serialize` / `Deserialize` on all entities in `src/entities/mod.rs`
|
2026-08-04 06:13:51 +00:00
|
|
|
|
(Dashboard, PlayerData, Balance, Inventory, InventoryEntry, InventoryItem,
|
|
|
|
|
|
QuestBook, Quest, NoteBook, Note, ContactBook, Contact, SpellSlots, Attunements,
|
|
|
|
|
|
PreparedSpells, Spell, HitDice, DeathSaveThrows, Name, Settings; enums:
|
|
|
|
|
|
InventoryKind, ContactStatus, PreparedSpell)
|
2026-08-16 08:50:41 +00:00
|
|
|
|
- [x] verify `Store` derive and `serde` derive coexist on the same structs (prototype first)
|
|
|
|
|
|
- [x] add a `version` field to `Settings`; add a migration hook for future format changes
|
|
|
|
|
|
- [x] serialize Dashboard to JSON; save to localStorage via `web_sys::Storage`
|
2026-08-04 06:13:51 +00:00
|
|
|
|
(feature-gate to `hydrate`/`csr`; SSR renders mock/empty)
|
2026-08-16 08:50:41 +00:00
|
|
|
|
- [x] auto-save on change — debounced effect over the `Store` — plus manual Save button
|
|
|
|
|
|
- [x] Load button: restore from localStorage with an overwrite confirm
|
|
|
|
|
|
- [x] Clear button: wipe localStorage with a confirm
|
|
|
|
|
|
- [x] export: download `dashboard.json` as a blob
|
|
|
|
|
|
- [x] import: file input → parse → validate version → replace state
|
|
|
|
|
|
- [x] on startup, hydrate from localStorage; keep `mock()` as the fallback / "New dashboard"
|
2026-08-04 06:13:51 +00:00
|
|
|
|
- **Acceptance:** close the tab, reopen — state is intact. Export → import into a
|
|
|
|
|
|
fresh browser — identical state.
|
|
|
|
|
|
|
2026-08-16 08:50:41 +00:00
|
|
|
|
**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).
|
|
|
|
|
|
|
2026-08-16 08:57:31 +00:00
|
|
|
|
## 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.
|
|
|
|
|
|
|
2026-08-16 09:32:25 +00:00
|
|
|
|
- [x] `state::undo.rs` (hydrate-gated): undo/redo stacks of serialized
|
2026-08-16 08:57:31 +00:00
|
|
|
|
`Dashboard`, capped (e.g. 100); push coalesced on the store trigger
|
|
|
|
|
|
(one snapshot per ~1 s window → one undo step = one edit burst)
|
2026-08-16 09:32:25 +00:00
|
|
|
|
- [x] a new edit after undo clears the redo stack; Clear and import push a
|
2026-08-16 08:57:31 +00:00
|
|
|
|
snapshot first, so they are undoable too
|
2026-08-16 09:32:25 +00:00
|
|
|
|
- [x] `undo()` / `redo()` call `replace_state`; expose stack depths as
|
2026-08-16 08:57:31 +00:00
|
|
|
|
`RwSignal<usize>` for button disabled states
|
2026-08-16 09:32:25 +00:00
|
|
|
|
- [x] header buttons: ↩ Отменить / Вернуть (disabled when empty)
|
|
|
|
|
|
- [x] shortcuts: Ctrl+Z, Ctrl+Shift+Z (Ctrl+Y); ignore when focus is in an
|
2026-08-16 08:57:31 +00:00
|
|
|
|
input/textarea/select, so typing never triggers undo
|
2026-08-16 09:32:25 +00:00
|
|
|
|
- [x] unit tests for stack semantics; e2e: edit → undo → redo; reload keeps
|
2026-08-16 08:57:31 +00:00
|
|
|
|
the undone state (undo writes through autosave)
|
|
|
|
|
|
- **Acceptance:** every edit since the last window is reversible with a
|
|
|
|
|
|
keystroke; undo survives a reload.
|
|
|
|
|
|
|
2026-08-16 09:32:25 +00:00
|
|
|
|
**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.
|
|
|
|
|
|
|
2026-08-04 06:13:51 +00:00
|
|
|
|
## Phase 2 — Character page (M, ~1-2 days)
|
|
|
|
|
|
|
|
|
|
|
|
`/` currently renders the literal string "character".
|
|
|
|
|
|
|
2026-08-16 09:44:31 +00:00
|
|
|
|
- [x] player card: portrait, name, class, level, XP (data already in sidebar)
|
|
|
|
|
|
- [x] HP block: temp / current / max, `Numput`-style editing
|
|
|
|
|
|
- [x] prepared spells UI: render Vacant/Occupied slots; free-text spell name entry
|
2026-08-04 06:13:51 +00:00
|
|
|
|
(upgrade the `Spell` stub from `name`-only to name + optional level/description)
|
2026-08-16 09:44:31 +00:00
|
|
|
|
- [x] attunements UI: 3 slots, free text, clear button (model exists)
|
2026-08-04 06:13:51 +00:00
|
|
|
|
- **Acceptance:** player can set prepared spells and attunements; they persist.
|
|
|
|
|
|
|
2026-08-16 09:44:31 +00:00
|
|
|
|
**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).
|
|
|
|
|
|
|
2026-08-04 06:13:51 +00:00
|
|
|
|
## 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)
|
2026-08-16 08:57:31 +00:00
|
|
|
|
- Playwright e2e: add item from search, quest CRUD, notes filter, undo
|
|
|
|
|
|
shortcuts (persistence round trip is covered by `persistence.spec.ts`)
|
2026-08-04 06:13:51 +00:00
|
|
|
|
- [ ] 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.
|
2026-08-16 08:57:31 +00:00
|
|
|
|
- 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.
|
2026-08-04 06:13:51 +00:00
|
|
|
|
- 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.
|