2e8de105b2
Adds detailed tracking columns to threads (stakes, origin, next beat, etc.) and npcs (appearance, personality, secrets, disposition, etc.), plus a migration script to add the columns to existing databases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
526 lines
17 KiB
Markdown
526 lines
17 KiB
Markdown
# 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
|
|
│ └── systems.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
|
|
│ └── campaigns.js # Campaign management view UI
|
|
├── 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,
|
|
related_npcs TEXT,
|
|
related_location TEXT,
|
|
origin TEXT,
|
|
stakes TEXT,
|
|
last_development TEXT,
|
|
next_beat TEXT,
|
|
suspected_resolution 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,
|
|
appearance TEXT,
|
|
age TEXT,
|
|
gender TEXT,
|
|
pronouns TEXT,
|
|
voice TEXT,
|
|
distinguishing_features TEXT,
|
|
faction TEXT,
|
|
occupation TEXT,
|
|
social_status TEXT,
|
|
relationship_to_pc TEXT,
|
|
loyalty TEXT,
|
|
personality_traits TEXT,
|
|
fears TEXT,
|
|
desires TEXT,
|
|
secrets TEXT,
|
|
first_encountered TEXT,
|
|
last_seen TEXT,
|
|
current_location TEXT,
|
|
current_goal TEXT,
|
|
role_in_threads TEXT,
|
|
alive_status TEXT DEFAULT 'alive',
|
|
disposition TEXT DEFAULT 'unknown',
|
|
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 <html>.
|
|
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.
|