commit 14fe8ab775ff4ed1d514c81db6ba89e5a3a6d695 Author: claudecode Date: Tue Jun 30 21:39:10 2026 -0400 init: add CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7b82628 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,494 @@ +# Mythic Oracle — CLAUDE.md + +A reference for Claude Code sessions on this project. +Read this in full before starting any task. + +--- + +## Project overview + +A solo TTRPG session companion running as a local Node/Express +server opened via Chromium's --app= flag. It presents as a +frameless standalone window on an EndeavourOS workstation. +It consolidates the Mythic GME v2 oracle, campaign tracking, +character management, and session notes into one tool for +personal use. + +A reference copy of the original single-file web app exists at +/var/www/html/index.html on a Proxmox LXC (host: mythicgme-cc). +Use it as a reference for porting existing feature logic in +Phase 4. Do not modify it. + +--- + +## Tech stack + +- Runtime: Node.js (system-wide at /usr/bin/node) +- Backend: Express +- Database: SQLite via better-sqlite3 +- Frontend: Vanilla JS — no frameworks, no build step +- Fonts: Cormorant Garamond (headings), Lora (body/UI), + JetBrains Mono (numbers/stats/dice) — Google Fonts +- Aesthetic: Dark and atmospheric — deep backgrounds, + muted accents, readable but moody + Light mode available, dark mode is default +- Port: 4000 +- Launcher: Chromium --app=http://localhost:4000 + via mythic-oracle.desktop (runs as joseph) +- Process: systemd user service running as claudecode + Linger enabled — starts on boot without login + +--- + +## Users + +- joseph — desktop user, passwordless sudoer, owns the + .desktop launcher and Chromium session +- claudecode — no sudo, no password login, owns the project + and runs the Node server and Claude Code + +Project root: /home/claudecode/projects/mythic-oracle + +To start a Claude Code session: + sudo -u claudecode -i + cd ~/projects/mythic-oracle + claude + +To manage the systemd service: + sudo -u claudecode systemctl --user status mythic-oracle + sudo -u claudecode systemctl --user restart mythic-oracle + sudo -u claudecode systemctl --user stop mythic-oracle + +--- + +## Folder structure + +mythic-oracle/ +├── server/ +│ ├── index.js # Express entry point, middleware, port +│ ├── db.js # SQLite connection + schema init +│ └── routes/ +│ ├── campaigns.js +│ ├── characters.js +│ ├── threads.js +│ ├── npcs.js +│ ├── notes.js +│ └── tables.js +├── public/ +│ ├── index.html # Shell only — sidebar + view containers +│ ├── css/ +│ │ └── styles.css +│ └── js/ +│ ├── app.js # Entry, sidebar routing, campaign context +│ ├── api.js # Shared fetch wrapper — all API calls here +│ ├── oracle.js +│ ├── meaning.js +│ ├── une.js +│ ├── dice.js +│ ├── threads.js +│ ├── npcs.js +│ ├── notes.js +│ ├── character.js +│ └── tables.js +├── data/ +│ ├── tables/ # Static JSON tables — version controlled +│ └── mythic-oracle.db # SQLite database — gitignored +├── systemd/ +│ └── mythic-oracle.service +├── CLAUDE.md +├── package.json +├── .gitignore +└── mythic-oracle.desktop + +--- + +## Database schema + +Initialized in full by db.js on first boot. +Never modify the schema outside of an explicit migration +discussion with the user. + +--- Core --- + +CREATE TABLE systems ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE +); + +CREATE TABLE campaigns ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + system_id INTEGER NOT NULL REFERENCES systems(id), + chaos_factor INTEGER NOT NULL DEFAULT 5, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +--- Trackers --- + +CREATE TABLE threads ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL REFERENCES campaigns(id), + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + notes TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE npcs ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL REFERENCES campaigns(id), + name TEXT NOT NULL, + description TEXT, + notes TEXT, + motivations TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +--- Notes --- + +CREATE TABLE session_logs ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL REFERENCES campaigns(id), + title TEXT, + content TEXT, + session_date TEXT NOT NULL DEFAULT (date('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE campaign_docs ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL REFERENCES campaigns(id), + doc_type TEXT NOT NULL, + content TEXT, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE (campaign_id, doc_type) +); + +--- Custom tables --- + +CREATE TABLE custom_tables ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER REFERENCES campaigns(id), + name TEXT NOT NULL, + description TEXT, + entries TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +--- Character sheets --- + +CREATE TABLE characters_dnd5e ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL UNIQUE REFERENCES campaigns(id), + name TEXT, class TEXT, level INTEGER DEFAULT 1, + background TEXT, race TEXT, alignment TEXT, + str INTEGER, dex INTEGER, con INTEGER, + int_score INTEGER, wis INTEGER, cha INTEGER, + hp_max INTEGER, hp_current INTEGER, hp_temp INTEGER, + ac INTEGER, speed INTEGER, + proficiency INTEGER DEFAULT 2, + inspiration INTEGER DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE characters_morkborg ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL UNIQUE REFERENCES campaigns(id), + name TEXT, class TEXT, + str INTEGER, agi INTEGER, pre INTEGER, tou INTEGER, + hp_max INTEGER, hp_current INTEGER, + omens_max INTEGER, omens_current INTEGER, + silver INTEGER DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE characters_cairn ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL UNIQUE REFERENCES campaigns(id), + name TEXT, background TEXT, + str INTEGER, dex INTEGER, wil INTEGER, + hp_max INTEGER, hp_current INTEGER, + armor INTEGER DEFAULT 0, + deprived INTEGER DEFAULT 0, + gold INTEGER DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE cairn_inventory ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters_cairn(id), + slot INTEGER, + name TEXT NOT NULL, + description TEXT, + is_bulky INTEGER DEFAULT 0 +); + +CREATE TABLE characters_chaalt ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL UNIQUE REFERENCES campaigns(id), + name TEXT, class TEXT, level INTEGER DEFAULT 1, race TEXT, + str INTEGER, dex INTEGER, con INTEGER, + int_score INTEGER, wis INTEGER, cha INTEGER, + hp_max INTEGER, hp_current INTEGER, + ac INTEGER, thac0 INTEGER, + gold INTEGER DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE characters_ironsworn ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL UNIQUE REFERENCES campaigns(id), + name TEXT, + edge INTEGER DEFAULT 1, heart INTEGER DEFAULT 1, + iron INTEGER DEFAULT 1, shadow INTEGER DEFAULT 1, + wits INTEGER DEFAULT 1, + health INTEGER DEFAULT 5, spirit INTEGER DEFAULT 5, + supply INTEGER DEFAULT 5, + momentum INTEGER DEFAULT 2, momentum_reset INTEGER DEFAULT 2, + momentum_max INTEGER DEFAULT 10, + wounded INTEGER DEFAULT 0, shaken INTEGER DEFAULT 0, + unprepared INTEGER DEFAULT 0, encumbered INTEGER DEFAULT 0, + maimed INTEGER DEFAULT 0, corrupted INTEGER DEFAULT 0, + cursed INTEGER DEFAULT 0, tormented INTEGER DEFAULT 0, + experience INTEGER DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE ironsworn_assets ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters_ironsworn(id), + name TEXT NOT NULL, + asset_type TEXT, + ability_1 TEXT, ability_1_checked INTEGER DEFAULT 0, + ability_2 TEXT, ability_2_checked INTEGER DEFAULT 0, + ability_3 TEXT, ability_3_checked INTEGER DEFAULT 0, + notes TEXT +); + +CREATE TABLE ironsworn_vows ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL REFERENCES campaigns(id), + title TEXT NOT NULL, + rank TEXT NOT NULL, + progress INTEGER DEFAULT 0, + status TEXT NOT NULL DEFAULT 'active', + notes TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE characters_shadowrun ( + id INTEGER PRIMARY KEY, + campaign_id INTEGER NOT NULL UNIQUE REFERENCES campaigns(id), + name TEXT, metatype TEXT, archetype TEXT, + gender TEXT, age INTEGER, + body INTEGER, agility INTEGER, reaction INTEGER, + strength INTEGER, willpower INTEGER, logic INTEGER, + intuition INTEGER, charisma INTEGER, + essence REAL DEFAULT 6.0, + edge INTEGER, edge_current INTEGER, + magic_resonance INTEGER, + nuyen INTEGER DEFAULT 0, + karma INTEGER DEFAULT 0, total_karma INTEGER DEFAULT 0, + street_cred INTEGER DEFAULT 0, + notoriety INTEGER DEFAULT 0, reputation INTEGER DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE shadowrun_skills ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters_shadowrun(id), + name TEXT NOT NULL, + rating INTEGER DEFAULT 0, + specialization TEXT, + linked_attr TEXT +); + +CREATE TABLE shadowrun_contacts ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters_shadowrun(id), + name TEXT NOT NULL, + loyalty INTEGER, + connection INTEGER, + notes TEXT +); + +CREATE TABLE shadowrun_qualities ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters_shadowrun(id), + name TEXT NOT NULL, + quality_type TEXT NOT NULL, + karma_cost INTEGER, + description TEXT +); + +--- Seed data (insert on first boot if systems table is empty) --- + +INSERT INTO systems (name, slug) VALUES + ('D&D 5e', 'dnd5e'), + ('Mork Borg', 'morkborg'), + ('Cairn', 'cairn'), + ('Cha''alt', 'chaalt'), + ('Ironsworn', 'ironsworn'), + ('Shadowrun', 'shadowrun'); + +--- + +## Architectural decisions + +These are final. Do not propose alternatives unless explicitly asked. + +- Express serves everything — static files and API. No nginx. +- Vanilla JS only. No frameworks, no build step. +- better-sqlite3 only. No ORMs, no query builders. +- Ability score modifiers are never stored. Derive on the frontend. +- Table data is never hardcoded in JS or HTML. Always loaded from + data/tables/ JSON files or the custom_tables database table. +- custom_tables.campaign_id is nullable — NULL means global, + a value means campaign-scoped. +- One character sheet row per campaign per system table. +- Everything is campaign-scoped except global custom tables. +- Chaos factor lives in the sidebar only — not in any view. +- Session logs are append-style with a date per entry. +- World and lore are single persistent documents per campaign + stored in campaign_docs with doc_type 'world' and 'lore'. + +--- + +## Navigation + +Collapsible sidebar. Expands to labeled nav, collapses to +icon-only. Chaos factor displayed persistently in the sidebar +with +/- controls, always visible in both states. + +Sections and items: + Oracle: Oracle, Meaning, UNE + Tools: Dice, Tables + Campaign: Threads, NPCs, Notes, Character + Settings: Campaign management, Theme toggle + +Theme: + Dark mode by default. Light mode available via toggle in + Settings. Controlled via data-theme attribute on . + CSS design tokens defined for both themes. + +--- + +## Static table files + +Location: data/tables/*.json +Format: { "name": "Table Name", "entries": ["entry", "entry"] } + +Planned tables: + Core oracle: action, descriptor, event-focus + Character/NPC: appearance, personality, background-hook, + occupation, flaw, secret + Scene/setting: location-type, location-atmosphere, weather, + time-passage, sensory-detail + Adventure/plot: complication, plot-twist, discovery, + rumor-hook, consequence + Creature: creature-type, behavior, motivation, + unique-trait + Loot/treasure: treasure-type, magic-item, mundane-item, + item-condition + Dungeon: room-type, room-feature, trap-type, + dungeon-dressing, exit-type + Wilderness: terrain-feature, point-of-interest, + wilderness-encounter, travel-event + Urban: district-type, urban-encounter, + building-type, street-event + Miscellaneous: magical-effects, bodily-injury + +An empty or missing file does not break the app. +The UI handles missing tables gracefully. +Table content is filled in over time independently of code work. + +--- + +## Workflow rules + +- claudecode owns the project. No sudo inside Claude Code sessions. +- Always run Claude Code as claudecode from the project root. +- One commit per meaningful change. No bundling unrelated changes. +- Do not commit without explicit user approval. Show a file-by-file + summary of what changed and why before every commit. +- After any change: curl http://localhost:4000/health to confirm + the server is running cleanly. +- Priority order: (1) features/fixes (2) cleanup/refactoring + (3) visual changes. No style changes during feature work + unless explicitly asked. +- Stay scoped to what's asked. Do not touch unrelated views, + routes, or files. +- Never modify the SQLite schema without an explicit migration + discussion with the user first. +- Default effort: medium for scoped UI/JS tasks. + High for: Mythic GME mechanics, schema changes, + multi-file architectural work. + +--- + +## Prompt conventions + +Preface every prompt with: + Before making changes: confirm git status is clean and do not + commit until I explicitly approve. Show a file-by-file summary + of what changed and why when done. Only touch [specific scope]. + Once I approve, write a single commit for this change only. + +What makes a good prompt: +- Specify mechanics, not just goals +- Specify exact display and formatting +- State explicitly what NOT to touch +- Specify validation behavior +- Reference existing patterns by file and function name +- Use a task list for 3+ distinct structural changes + +After changes: run git diff before approving. Test actual +behavior in the browser before approving. Do not rely on +the summary alone. + +--- + +## Build phases + +Phase 1 — Project skeleton + package.json, folder structure, .gitignore, + Express entry point, static file serving, + GET /health route, db.js with full schema + initialization and seed data + +Phase 2 — Frontend shell + index.html with collapsible sidebar, + styles.css with design tokens and typography + (dark default, light mode via data-theme), + app.js with sidebar routing and view switching. + No feature logic yet. + +Phase 3 — Campaign management + Campaign CRUD API routes and UI, + active campaign context in app.js, + chaos factor display and controls in sidebar + +Phase 4 — Migrate existing features + Oracle, Meaning, UNE, Dice — ported from reference + file at /var/www/html/index.html on mythicgme-cc. + Oracle connects to campaign API for chaos factor. + Meaning and UNE read from data/tables/ JSON files. + Dice is pure frontend. + +Phase 5 — Thread tracker +Phase 6 — NPC tracker +Phase 7 — Notes (session logs + campaign docs) +Phase 8 — Tables (static serving + custom table CRUD) + +Phase 9 — Character sheets (one prompt per system) + Order: dnd5e → morkborg → cairn → chaalt + → ironsworn → shadowrun + +Phase 10 — Deployment + systemd unit file, .desktop launcher, + setup README + +Phase 11 — Visual retheming pass + All features complete before this phase begins.