init: add CLAUDE.md
This commit is contained in:
@@ -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 <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.
|
||||
Reference in New Issue
Block a user