diff --git a/database/mariadb.js b/database/mariadb.js
index 5654132..c03760f 100644
--- a/database/mariadb.js
+++ b/database/mariadb.js
@@ -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
};
diff --git a/database/routes/missionRoutes.js b/database/routes/missionRoutes.js
new file mode 100644
index 0000000..df25bfe
--- /dev/null
+++ b/database/routes/missionRoutes.js
@@ -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;
diff --git a/database/server.js b/database/server.js
index 54eeeec..d610406 100755
--- a/database/server.js
+++ b/database/server.js
@@ -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) => {
diff --git a/src/App.js b/src/App.js
index e2b9e38..b2aabb2 100755
--- a/src/App.js
+++ b/src/App.js
@@ -14,7 +14,7 @@ import axios from 'axios';
import { debug, info, warn, error, logApiCall, logApiError, logUserAction } from './utils/logger';
function App() {
- const [tab, setTab] = useState('roller');
+ const [tab, setTab] = useState('mission');
// Global login/session state
const [authedPlayer, setAuthedPlayer] = useState(() => localStorage.getItem('dw:shop:authedPlayer') ? JSON.parse(localStorage.getItem('dw:shop:authedPlayer')) : '');
const [sessionId, setSessionId] = useState(() => localStorage.getItem('dw:shop:sessionId') ? JSON.parse(localStorage.getItem('dw:shop:sessionId')) : '');
@@ -267,8 +267,14 @@ function App() {
{/* Tab Navigation */}
-
+
)}
+
@@ -237,13 +456,15 @@ function MissionTab({ authedPlayer }) {
-
+
@@ -307,6 +528,17 @@ function MissionTab({ authedPlayer }) {
+ {/* Copilot Narrative */}
+
+
+
Narrative
+ {copilotLoading && Loading...}
+
+
+ {copilotText || (copilotLoading ? 'Generating...' : scenes[currentScene]?.description || '')}
+
+
+
{/* Enemy List */}
Enemies
@@ -433,6 +665,36 @@ function MissionTab({ authedPlayer }) {
)}
+
+ {/* History Panel */}
+ {showHistory && (
+
+
Mission History
+ {loadingHistory ? (
+
Loading...
+ ) : history.length === 0 ? (
+
No missions yet.
+ ) : (
+
+ {history.map(m => (
+
+
+
+ {m.name}
+
+ {m.theme} · {m.threat_level} · {m.scene_count} scenes
+
+
+
+ {new Date(m.created_at).toLocaleDateString('da-DK')}
+
+
+
+ ))}
+
+ )}
+
+ )}
);