Add mission persistence: missions table, API routes, and history UI

- MariaDB missions table with scenes JSON
- /api/missions CRUD routes
- MissionTab saves completed missions to DB
- Mission history panel in UI

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 10:07:46 +02:00
parent dbb79c2537
commit fe1b77ec68
5 changed files with 505 additions and 67 deletions

View File

@@ -88,6 +88,26 @@ const createTables = async () => {
)
`);
// Missions table
await connection.execute(`
CREATE TABLE IF NOT EXISTS missions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(500) NOT NULL DEFAULT 'Untitled Mission',
theme VARCHAR(100) NOT NULL DEFAULT 'tyranid',
scene_count INT NOT NULL DEFAULT 4,
enemy_count INT NOT NULL DEFAULT 3,
threat_level VARCHAR(50) NOT NULL DEFAULT 'Medium',
player_count INT NOT NULL DEFAULT 3,
scenes JSON DEFAULT ('[]'),
gm_player VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
`);
await connection.execute(`CREATE INDEX IF NOT EXISTS idx_missions_created_at ON missions(created_at)`);
await connection.execute(`CREATE INDEX IF NOT EXISTS idx_missions_theme ON missions(theme)`);
// Create indexes
await connection.execute(`CREATE INDEX IF NOT EXISTS idx_players_name ON players(name)`);
await connection.execute(`CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id)`);
@@ -441,6 +461,78 @@ const bestiaryHelpers = {
}
};
// Mission helpers
const missionHelpers = {
getAll: async () => {
try {
const [rows] = await pool.execute('SELECT * FROM missions ORDER BY created_at DESC');
return rows.map(row => ({
...row,
scenes: typeof row.scenes === 'string' ? JSON.parse(row.scenes) : row.scenes
}));
} catch (error) {
logToFile('MariaDB: Error getting all missions', error);
return [];
}
},
getById: async (id) => {
try {
const [rows] = await pool.execute('SELECT * FROM missions WHERE id = ?', [id]);
if (rows.length === 0) return null;
const row = rows[0];
return {
...row,
scenes: typeof row.scenes === 'string' ? JSON.parse(row.scenes) : row.scenes
};
} catch (error) {
logToFile('MariaDB: Error getting mission', id, error);
return null;
}
},
create: async (missionData) => {
try {
const { name, theme, sceneCount, enemyCount, threatLevel, playerCount, scenes, gmPlayer } = missionData;
const [result] = await pool.execute(
'INSERT INTO missions (name, theme, scene_count, enemy_count, threat_level, player_count, scenes, gm_player) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[name, theme, sceneCount, enemyCount, threatLevel, playerCount, JSON.stringify(scenes || []), gmPlayer || null]
);
logToFile('MariaDB: Created mission', name);
return result.insertId;
} catch (error) {
logToFile('MariaDB: Error creating mission', error);
return null;
}
},
update: async (id, missionData) => {
try {
const { name, theme, sceneCount, enemyCount, threatLevel, playerCount, scenes } = missionData;
const [result] = await pool.execute(
'UPDATE missions SET name = ?, theme = ?, scene_count = ?, enemy_count = ?, threat_level = ?, player_count = ?, scenes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[name, theme, sceneCount, enemyCount, threatLevel, playerCount, JSON.stringify(scenes), id]
);
logToFile('MariaDB: Updated mission', id);
return result.affectedRows > 0;
} catch (error) {
logToFile('MariaDB: Error updating mission', id, error);
return false;
}
},
delete: async (id) => {
try {
const [result] = await pool.execute('DELETE FROM missions WHERE id = ?', [id]);
logToFile('MariaDB: Deleted mission', id);
return result.affectedRows > 0;
} catch (error) {
logToFile('MariaDB: Error deleting mission', id, error);
return false;
}
}
};
// Initialize database
createTables().catch(error => {
console.error('Failed to initialize MariaDB:', error);
@@ -456,5 +548,6 @@ module.exports = {
stagingHelpers,
weaponsHelpers,
bestiaryHelpers,
missionHelpers,
logToFile
};

View File

@@ -0,0 +1,66 @@
const express = require('express');
const { missionHelpers, logToFile } = require('../mariadb');
const router = express.Router();
// Get all missions
router.get('/', async (req, res) => {
try {
const missions = await missionHelpers.getAll();
res.json(missions);
} catch (error) {
logToFile('API: Failed to get missions', error);
res.status(500).json({ error: String(error) });
}
});
// Get mission by ID
router.get('/:id', async (req, res) => {
try {
const mission = await missionHelpers.getById(req.params.id);
if (!mission) return res.status(404).json({ error: 'Mission not found' });
res.json(mission);
} catch (error) {
logToFile('API: Failed to get mission', req.params.id, error);
res.status(500).json({ error: String(error) });
}
});
// Create mission
router.post('/', async (req, res) => {
try {
const id = await missionHelpers.create(req.body);
if (!id) return res.status(500).json({ error: 'Failed to create mission' });
logToFile('API: Created mission', req.body.name);
res.json({ success: true, id });
} catch (error) {
logToFile('API: Failed to create mission', error);
res.status(500).json({ error: String(error) });
}
});
// Update mission
router.put('/:id', async (req, res) => {
try {
const ok = await missionHelpers.update(req.params.id, req.body);
if (!ok) return res.status(404).json({ error: 'Mission not found' });
res.json({ success: true });
} catch (error) {
logToFile('API: Failed to update mission', req.params.id, error);
res.status(500).json({ error: String(error) });
}
});
// Delete mission
router.delete('/:id', async (req, res) => {
try {
const ok = await missionHelpers.delete(req.params.id);
if (!ok) return res.status(404).json({ error: 'Mission not found' });
res.json({ success: true });
} catch (error) {
logToFile('API: Failed to delete mission', req.params.id, error);
res.status(500).json({ error: String(error) });
}
});
console.log('Mission routes registered (MariaDB)');
module.exports = router;

View File

@@ -11,6 +11,7 @@ const rulesRoutes = require('./routes/rulesRoutes');
const bestiaryRoutes = require('./routes/bestiaryRoutes');
const weaponsRoutes = require('./routes/weaponsRoutes');
const rulesStagingRoutes = require('./routes/rulesStagingRoutes');
const missionRoutes = require('./routes/missionRoutes');
// const rulesRoutes = require('./routes/rulesRoutes-simple');
const gmkitDir = path.join(__dirname, '..', 'data', 'gamemasters_kit');
@@ -92,7 +93,17 @@ try {
console.log('Rules staging routes registered');
} catch (e) {
console.error('Error mounting /api/rules/staging:', e && e.stack ? e.stack : e);
} // Expose gamemaster kit files and a simple listing API for GM-only resources
}
try {
console.log('Registering /api/missions');
app.use('/api/missions', missionRoutes);
console.log('Mission routes registered');
} catch (e) {
console.error('Error mounting /api/missions:', e && e.stack ? e.stack : e);
}
// Expose gamemaster kit files and a simple listing API for GM-only resources
try {
console.log('Registering /api/gmkit and /gmkit static');
app.get('/api/gmkit/list', (req, res) => {