init: Phase 1 project skeleton

Express entry point, SQLite schema init with seed data, static
frontend shell, and empty route/JS stubs per CLAUDE.md Phase 1 scope.
This commit is contained in:
claudecode
2026-06-30 22:06:19 -04:00
parent 7075f57e88
commit 3bcd5bc694
24 changed files with 1588 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
data/mythic-oracle.db
+1252
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"name": "mythic-oracle",
"version": "1.0.0",
"description": "Solo TTRPG session companion — Mythic GME oracle, campaign tracking, character management, and session notes.",
"private": true,
"main": "server/index.js",
"scripts": {
"start": "node server/index.js"
},
"dependencies": {
"express": "^4.19.2",
"better-sqlite3": "^11.3.0"
}
}
+1
View File
@@ -0,0 +1 @@
/* Mythic Oracle — styles */
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mythic Oracle</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Lora:ital,wght@0,400;0,500;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
</body>
<script src="js/app.js" type="module"></script>
</html>
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — shared fetch wrapper, all API calls
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — entry, sidebar routing, campaign context
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — character sheets
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — dice
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — meaning tables
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — notes
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — NPCs
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — oracle
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — tables
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — threads
+1
View File
@@ -0,0 +1 @@
// Mythic Oracle — UNE
+235
View File
@@ -0,0 +1,235 @@
const path = require('path');
const Database = require('better-sqlite3');
const db = new Database(path.join(__dirname, '..', 'data', 'mythic-oracle.db'));
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS systems (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS 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'))
);
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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'))
);
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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)
);
CREATE TABLE IF NOT EXISTS 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'))
);
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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
);
`);
const systemCount = db.prepare('SELECT COUNT(*) AS count FROM systems').get().count;
if (systemCount === 0) {
const insertSystem = db.prepare('INSERT INTO systems (name, slug) VALUES (?, ?)');
const seedSystems = db.transaction((systems) => {
for (const system of systems) {
insertSystem.run(system.name, system.slug);
}
});
seedSystems([
{ name: 'D&D 5e', slug: 'dnd5e' },
{ name: 'Mork Borg', slug: 'morkborg' },
{ name: 'Cairn', slug: 'cairn' },
{ name: "Cha'alt", slug: 'chaalt' },
{ name: 'Ironsworn', slug: 'ironsworn' },
{ name: 'Shadowrun', slug: 'shadowrun' },
]);
}
module.exports = db;
+31
View File
@@ -0,0 +1,31 @@
const path = require('path');
const express = require('express');
require('./db');
const campaignsRouter = require('./routes/campaigns');
const charactersRouter = require('./routes/characters');
const threadsRouter = require('./routes/threads');
const npcsRouter = require('./routes/npcs');
const notesRouter = require('./routes/notes');
const tablesRouter = require('./routes/tables');
const app = express();
const PORT = 4000;
app.use(express.json());
app.use(express.static(path.join(__dirname, '..', 'public')));
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.use('/api/campaigns', campaignsRouter);
app.use('/api/characters', charactersRouter);
app.use('/api/threads', threadsRouter);
app.use('/api/npcs', npcsRouter);
app.use('/api/notes', notesRouter);
app.use('/api/tables', tablesRouter);
app.listen(PORT, () => {
console.log(`Mythic Oracle running at http://localhost:${PORT}`);
});
+4
View File
@@ -0,0 +1,4 @@
const express = require('express');
const router = express.Router();
module.exports = router;
+4
View File
@@ -0,0 +1,4 @@
const express = require('express');
const router = express.Router();
module.exports = router;
+4
View File
@@ -0,0 +1,4 @@
const express = require('express');
const router = express.Router();
module.exports = router;
+4
View File
@@ -0,0 +1,4 @@
const express = require('express');
const router = express.Router();
module.exports = router;
+4
View File
@@ -0,0 +1,4 @@
const express = require('express');
const router = express.Router();
module.exports = router;
+4
View File
@@ -0,0 +1,4 @@
const express = require('express');
const router = express.Router();
module.exports = router;