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:
@@ -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
|
||||
};
|
||||
|
||||
66
database/routes/missionRoutes.js
Normal file
66
database/routes/missionRoutes.js
Normal 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;
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user