diff --git a/database/backend.log b/database/backend.log index 5cdc37b..bb5a231 100644 --- a/database/backend.log +++ b/database/backend.log @@ -13121,3 +13121,132 @@ Connected to MongoDB [2026-06-24T09:16:50.443Z] API: Fetch player gm success [2026-06-24T09:17:13.311Z] MariaDB: Updated mission progress 9 [2026-06-24T10:07:25.442Z] API: Fetch all players (public) 6 +[2026-06-24T11:13:31.777Z] API: Fetch all players (public) 6 +[2026-06-24T11:13:31.785Z] SESSION: Session validation successful gm +[2026-06-24T11:13:31.811Z] API: Fetch player gm success +[2026-06-24T12:45:04.798Z] SESSION: Session validation successful gm +[2026-06-24T12:45:04.805Z] API: Fetch all players (public) 6 +[2026-06-24T12:45:04.824Z] API: Fetch player gm success +[2026-06-24T12:46:04.605Z] MariaDB: Error updating mission 9 {"message":"Bind parameters must not contain undefined. To pass SQL NULL specify JS null"} +[2026-06-24T12:46:38.984Z] MariaDB: Updated mission 9 +[2026-06-24T12:48:39.161Z] API: Fetch all players (public) 6 +[2026-06-24T12:48:39.166Z] SESSION: Session validation successful gm +[2026-06-24T12:48:39.191Z] API: Fetch player gm success +[2026-06-24T12:48:52.972Z] Session deleted: session_gm_1782253730353_zuylf6 +[2026-06-24T12:48:52.973Z] SESSION: Logout session_gm_1782253730353_zuylf6 +[2026-06-24T12:48:56.784Z] Session created: session_anders_1782305336733_4u6anw +[2026-06-24T12:48:56.786Z] API: Player login anders success +[2026-06-24T12:48:56.812Z] API: Fetch player anders success +[2026-06-24T12:48:56.815Z] SESSION: Session validation successful anders +[2026-06-24T12:48:56.838Z] API: Fetch player anders success +[2026-06-24T12:51:56.437Z] API: Fetch all players (public) 6 +[2026-06-24T12:51:56.440Z] SESSION: Session validation successful anders +[2026-06-24T12:51:56.466Z] API: Fetch player anders success +[2026-06-24T12:51:59.217Z] API: Fetch all players (public) 6 +[2026-06-24T12:51:59.221Z] SESSION: Session validation successful anders +[2026-06-24T12:51:59.242Z] API: Fetch player anders success +[2026-06-24T12:56:50.534Z] MariaDB: Tables created successfully +[2026-06-24T12:56:57.616Z] API: Fetch all players (public) 6 +[2026-06-24T12:56:57.621Z] SESSION: Session validation successful anders +[2026-06-24T12:56:57.643Z] API: Fetch player anders success +[2026-06-24T12:57:05.758Z] Session deleted: session_anders_1782305336733_4u6anw +[2026-06-24T12:57:05.762Z] SESSION: Logout session_anders_1782305336733_4u6anw +[2026-06-24T12:57:14.903Z] Session created: session_gm_1782305834876_ess5vv +[2026-06-24T12:57:14.905Z] API: Player login gm success +[2026-06-24T12:57:14.929Z] API: Fetch player gm success +[2026-06-24T12:57:14.935Z] SESSION: Session validation successful gm +[2026-06-24T12:57:14.957Z] API: Fetch player gm success +[2026-06-24T13:00:49.021Z] MariaDB: Created mission Blodprøve: Deathwatch-protokol +[2026-06-24T13:00:49.024Z] API: Created mission Blodprøve: Deathwatch-protokol +[2026-06-24T13:02:00.410Z] MariaDB: Tables created successfully +[2026-06-24T13:02:11.196Z] API: Fetch all players (public) 6 +[2026-06-24T13:02:11.200Z] SESSION: Session validation successful gm +[2026-06-24T13:02:11.225Z] API: Fetch player gm success +[2026-06-24T13:02:18.722Z] MariaDB: Set active mission 10 +[2026-06-24T13:02:24.197Z] Session deleted: session_gm_1782305834876_ess5vv +[2026-06-24T13:02:24.199Z] SESSION: Logout session_gm_1782305834876_ess5vv +[2026-06-24T13:02:26.592Z] Session created: session_anders_1782306146489_gun7dv +[2026-06-24T13:02:26.601Z] API: Player login anders success +[2026-06-24T13:02:26.625Z] API: Fetch player anders success +[2026-06-24T13:02:26.712Z] SESSION: Session validation successful anders +[2026-06-24T13:02:26.734Z] API: Fetch player anders success +[2026-06-24T13:02:28.987Z] Session deleted: session_anders_1782306146489_gun7dv +[2026-06-24T13:02:28.989Z] SESSION: Logout session_anders_1782306146489_gun7dv +[2026-06-24T13:02:33.451Z] Session created: session_gm_1782306153328_tekfnj +[2026-06-24T13:02:33.453Z] API: Player login gm success +[2026-06-24T13:02:33.476Z] API: Fetch player gm success +[2026-06-24T13:02:33.487Z] SESSION: Session validation successful gm +[2026-06-24T13:02:33.512Z] API: Fetch player gm success +[2026-06-24T13:02:36.821Z] MariaDB: Updated mission progress 10 +[2026-06-24T13:02:46.780Z] MariaDB: Updated mission progress 10 +[2026-06-24T13:02:50.920Z] MariaDB: Updated mission progress 10 +[2026-06-24T13:02:54.943Z] MariaDB: Updated mission progress 10 +[2026-06-24T13:03:01.361Z] MariaDB: Updated mission progress 10 +[2026-06-24T13:03:29.961Z] Session deleted: session_gm_1782306153328_tekfnj +[2026-06-24T13:03:29.963Z] SESSION: Logout session_gm_1782306153328_tekfnj +[2026-06-24T13:03:33.955Z] Session created: session_anders_1782306213899_ax07g9 +[2026-06-24T13:03:33.957Z] API: Player login anders success +[2026-06-24T13:03:33.982Z] API: Fetch player anders success +[2026-06-24T13:03:33.986Z] SESSION: Session validation successful anders +[2026-06-24T13:03:34.022Z] API: Fetch player anders success +[2026-06-24T13:04:49.349Z] API: Fetch all players (public) 6 +[2026-06-24T13:05:14.644Z] API: Fetch all players (public) 6 +[2026-06-24T13:06:39.436Z] MariaDB: Tables created successfully +[2026-06-24T13:06:56.306Z] SESSION: Session validation successful anders +[2026-06-24T13:06:56.317Z] API: Fetch all players (public) 6 +[2026-06-24T13:06:56.330Z] API: Fetch player anders success +[2026-06-24T13:15:59.895Z] API: Fetch all players (public) 6 +[2026-06-24T13:16:02.099Z] Session created: session_gm_1782306962032_bk9oxw +[2026-06-24T13:16:02.101Z] API: Player login gm success +[2026-06-24T13:16:02.149Z] API: Fetch player gm success +[2026-06-24T13:16:02.170Z] SESSION: Session validation successful gm +[2026-06-24T13:16:02.200Z] API: Fetch player gm success +[2026-06-24T13:16:04.142Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"} +[2026-06-24T13:16:09.908Z] SESSION: Session validation successful gm +[2026-06-24T13:16:09.934Z] API: Fetch all players (public) 6 +[2026-06-24T13:16:09.969Z] API: Fetch player gm success +[2026-06-24T13:16:11.659Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"} +[2026-06-24T13:16:14.458Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"} +[2026-06-24T13:16:14.488Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"} +[2026-06-24T13:16:21.266Z] SESSION: Session validation successful gm +[2026-06-24T13:16:21.290Z] API: Fetch all players (public) 6 +[2026-06-24T13:16:21.307Z] API: Fetch player gm success +[2026-06-24T14:33:15.059Z] API: Fetch all players (public) 6 +[2026-06-24T17:10:42.395Z] API: Fetch all players (public) 6 +[2026-06-24T17:10:42.398Z] SESSION: Session validation successful anders +[2026-06-24T17:10:42.418Z] API: Fetch player anders success +[2026-06-24T17:36:27.487Z] API: Fetch all players (public) 6 +[2026-06-24T17:36:27.497Z] SESSION: Session validation successful anders +[2026-06-24T17:36:27.521Z] API: Fetch player anders success +[2026-06-24T19:14:38.022Z] API: Fetch all players (public) 6 +[2026-06-24T19:14:38.028Z] SESSION: Session validation successful anders +[2026-06-24T19:14:38.050Z] API: Fetch player anders success +[2026-06-24T19:15:04.986Z] API: Fetch all players (public) 6 +[2026-06-24T19:15:04.988Z] SESSION: Session validation successful anders +[2026-06-24T19:15:05.016Z] API: Fetch player anders success +[2026-06-24T20:19:04.288Z] Session deleted: session_anders_1782306213899_ax07g9 +[2026-06-24T20:19:04.290Z] SESSION: Logout session_anders_1782306213899_ax07g9 +[2026-06-24T20:19:08.990Z] Session created: session_gm_1782332348947_2te0d3 +[2026-06-24T20:19:08.991Z] API: Player login gm success +[2026-06-24T20:19:09.020Z] SESSION: Session validation successful gm +[2026-06-24T20:19:09.022Z] API: Fetch player gm success +[2026-06-24T20:19:09.045Z] API: Fetch player gm success +[2026-06-24T20:20:51.141Z] MariaDB: Set active mission 9 +[2026-06-24T20:20:51.228Z] MariaDB: Updated mission progress 9 +[2026-06-25T15:15:57.529Z] API: Fetch all players (public) 6 +[2026-06-25T15:15:57.560Z] Session expired: session_gm_1782306962032_bk9oxw +[2026-06-25T15:15:57.562Z] SESSION: Invalid or expired session session_gm_1782306962032_bk9oxw +[2026-06-25T15:16:05.730Z] Session created: session_gm_1782400565707_g5nqvb +[2026-06-25T15:16:05.731Z] API: Player login gm success +[2026-06-25T15:16:05.814Z] API: Fetch player gm success +[2026-06-25T15:16:05.830Z] SESSION: Session validation successful gm +[2026-06-25T15:16:05.880Z] API: Fetch player gm success +[2026-06-25T15:16:12.647Z] MariaDB: Set active mission 10 +[2026-06-25T15:16:19.002Z] Session deleted: session_gm_1782400565707_g5nqvb +[2026-06-25T15:16:19.004Z] SESSION: Logout session_gm_1782400565707_g5nqvb +[2026-06-25T15:16:24.667Z] Session created: session_anders_1782400584597_wa4sfb +[2026-06-25T15:16:24.669Z] API: Player login anders success +[2026-06-25T15:16:24.740Z] API: Fetch player anders success +[2026-06-25T15:16:24.745Z] SESSION: Session validation successful anders +[2026-06-25T15:16:24.816Z] API: Fetch player anders success +[2026-06-25T16:28:12.280Z] MariaDB: Tables created successfully diff --git a/database/mariadb.js b/database/mariadb.js index 9ab958d..6369c55 100644 --- a/database/mariadb.js +++ b/database/mariadb.js @@ -173,6 +173,26 @@ const createTables = async () => { ) `); + // Error reports table - logged-in users can submit error reports + await connection.execute(` + CREATE TABLE IF NOT EXISTS error_reports ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_name VARCHAR(255) NOT NULL, + category VARCHAR(100) NOT NULL DEFAULT 'bug', + title VARCHAR(500) NOT NULL, + description TEXT NOT NULL, + page_url VARCHAR(1000) DEFAULT '', + status VARCHAR(50) NOT NULL DEFAULT 'open', + resolved_by VARCHAR(255) DEFAULT '', + resolved_note TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_error_reports_status (status), + INDEX idx_error_reports_player (player_name) + ) + `); + try { await connection.execute(`ALTER TABLE missions ADD COLUMN is_active TINYINT(1) NOT NULL DEFAULT 0`); } catch (error) { @@ -863,6 +883,88 @@ const simulationHelpers = { }, }; +// Error report helpers +const errorReportHelpers = { + create: async (data) => { + try { + const [result] = await pool.execute( + 'INSERT INTO error_reports (player_name, category, title, description, page_url) VALUES (?, ?, ?, ?, ?)', + [data.playerName, data.category || 'other', data.title, data.description, data.pageUrl || ''] + ); + return result.insertId; + } catch (error) { + logToFile('MariaDB: Error creating error report', error); + throw error; + } + }, + + getAll: async () => { + try { + const [rows] = await pool.execute('SELECT * FROM error_reports ORDER BY created_at DESC'); + return rows; + } catch (error) { + logToFile('MariaDB: Error fetching error reports', error); + return []; + } + }, + + getByPlayer: async (playerName) => { + try { + const [rows] = await pool.execute( + 'SELECT * FROM error_reports WHERE player_name = ? ORDER BY created_at DESC', + [playerName] + ); + return rows; + } catch (error) { + logToFile('MariaDB: Error fetching player error reports', playerName, error); + return []; + } + }, + + resolve: async (id, resolvedBy, resolvedNote) => { + try { + await pool.execute( + 'UPDATE error_reports SET status = ?, resolved_by = ?, resolved_note = ?, resolved_at = NOW() WHERE id = ?', + ['resolved', resolvedBy, resolvedNote || '', id] + ); + } catch (error) { + logToFile('MariaDB: Error resolving error report', id, error); + throw error; + } + }, + + updateStatus: async (id, status) => { + try { + await pool.execute( + 'UPDATE error_reports SET status = ? WHERE id = ?', + [status, id] + ); + } catch (error) { + logToFile('MariaDB: Error updating error report status', id, error); + throw error; + } + }, + + delete: async (id) => { + try { + await pool.execute('DELETE FROM error_reports WHERE id = ?', [id]); + } catch (error) { + logToFile('MariaDB: Error deleting error report', id, error); + throw error; + } + }, + + getById: async (id) => { + try { + const [rows] = await pool.execute('SELECT * FROM error_reports WHERE id = ?', [id]); + return rows[0] || null; + } catch (error) { + logToFile('MariaDB: Error fetching error report by id', id, error); + return null; + } + } +}; + // Initialize database createTables().catch(error => { console.error('Failed to initialize MariaDB:', error); @@ -881,5 +983,6 @@ module.exports = { missionHelpers, missionRollHelpers, simulationHelpers, + errorReportHelpers, logToFile }; diff --git a/database/routes/errorRoutes.js b/database/routes/errorRoutes.js new file mode 100644 index 0000000..c119fb4 --- /dev/null +++ b/database/routes/errorRoutes.js @@ -0,0 +1,116 @@ +const express = require('express'); +const { errorReportHelpers, logToFile } = require('../mariadb'); +const { validateSession } = require('../sessionModel'); +const router = express.Router(); + +// Middleware: require valid session (logged-in user) +const requireSession = async (req, res, next) => { + const sessionId = req.headers['x-session-id'] || (req.body && req.body.sessionId) || (req.query && req.query.sessionId); + if (!sessionId) return res.status(401).json({ error: 'Login required' }); + const session = await validateSession(sessionId); + if (!session || !session.data || !session.data.playerName) { + return res.status(401).json({ error: 'Invalid or expired session' }); + } + req.session = session; + req.sessionPlayerName = session.data.playerName; + next(); +}; + +// POST /api/errors - Submit error report (requires login) +router.post('/', requireSession, async (req, res) => { + try { + const { title, description, category, pageUrl } = req.body; + if (!title || !description) { + return res.status(400).json({ error: 'Title and description are required' }); + } + const id = await errorReportHelpers.create({ + playerName: req.sessionPlayerName, + title, + description, + category: category || 'other', + pageUrl: pageUrl || '' + }); + logToFile(`API: Error report #${id} created by ${req.sessionPlayerName}`); + res.json({ success: true, id }); + } catch (error) { + logToFile('API: Error creating report', error); + res.status(500).json({ error: String(error) }); + } +}); + +// GET /api/errors - List error reports (requires login, GM sees all, player sees their own) +router.get('/', requireSession, async (req, res) => { + try { + const isGM = req.sessionPlayerName && req.sessionPlayerName.toLowerCase() === 'gm'; + const reports = isGM + ? await errorReportHelpers.getAll() + : await errorReportHelpers.getByPlayer(req.sessionPlayerName); + res.json(reports); + } catch (error) { + logToFile('API: Error listing reports', error); + res.status(500).json({ error: String(error) }); + } +}); + +// GET /api/errors/:id - Get one error report (requires login) +router.get('/:id', requireSession, async (req, res) => { + try { + const report = await errorReportHelpers.getById(parseInt(req.params.id)); + if (!report) return res.status(404).json({ error: 'Not found' }); + // Players can only see their own reports, GM sees all + const isGM = req.sessionPlayerName && req.sessionPlayerName.toLowerCase() === 'gm'; + if (!isGM && report.player_name !== req.sessionPlayerName) { + return res.status(403).json({ error: 'Access denied' }); + } + res.json(report); + } catch (error) { + logToFile('API: Error getting report', error); + res.status(500).json({ error: String(error) }); + } +}); + +// PUT /api/errors/:id/resolve - Resolve error report (GM only) +router.put('/:id/resolve', requireSession, async (req, res) => { + try { + const isGM = req.sessionPlayerName && req.sessionPlayerName.toLowerCase() === 'gm'; + if (!isGM) return res.status(403).json({ error: 'GM access required' }); + const { note } = req.body; + await errorReportHelpers.resolve(parseInt(req.params.id), req.sessionPlayerName, note || ''); + logToFile(`API: Error report #${req.params.id} resolved by ${req.sessionPlayerName}`); + res.json({ success: true }); + } catch (error) { + logToFile('API: Error resolving report', error); + res.status(500).json({ error: String(error) }); + } +}); + +// PUT /api/errors/:id/status - Update status (GM only) +router.put('/:id/status', requireSession, async (req, res) => { + try { + const isGM = req.sessionPlayerName && req.sessionPlayerName.toLowerCase() === 'gm'; + if (!isGM) return res.status(403).json({ error: 'GM access required' }); + const { status } = req.body; + await errorReportHelpers.updateStatus(parseInt(req.params.id), status); + res.json({ success: true }); + } catch (error) { + logToFile('API: Error updating status', error); + res.status(500).json({ error: String(error) }); + } +}); + +// DELETE /api/errors/:id - Delete error report (GM only) +router.delete('/:id', requireSession, async (req, res) => { + try { + const isGM = req.sessionPlayerName && req.sessionPlayerName.toLowerCase() === 'gm'; + if (!isGM) return res.status(403).json({ error: 'GM access required' }); + await errorReportHelpers.delete(parseInt(req.params.id)); + logToFile(`API: Error report #${req.params.id} deleted by ${req.sessionPlayerName}`); + res.json({ success: true }); + } catch (error) { + logToFile('API: Error deleting report', error); + res.status(500).json({ error: String(error) }); + } +}); + +console.log('Error report routes registered'); +module.exports = router; \ No newline at end of file diff --git a/database/server.js b/database/server.js index 44aff83..e6eba8e 100755 --- a/database/server.js +++ b/database/server.js @@ -13,6 +13,7 @@ const weaponsRoutes = require('./routes/weaponsRoutes'); const rulesStagingRoutes = require('./routes/rulesStagingRoutes'); const missionRoutes = require('./routes/missionRoutes'); const simulationRoutes = require('./routes/simulationRoutes'); +const errorRoutes = require('./routes/errorRoutes'); // const rulesRoutes = require('./routes/rulesRoutes-simple'); const gmkitDir = path.join(__dirname, '..', 'data', 'gamemasters_kit'); @@ -112,6 +113,14 @@ try { console.error('Error mounting /api/simulations:', e && e.stack ? e.stack : e); } + try { + console.log('Registering /api/errors'); + app.use('/api/errors', errorRoutes); + console.log('Error report routes registered'); + } catch (e) { + console.error('Error mounting /api/errors:', 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'); diff --git a/src/App.js b/src/App.js index 77e43aa..ee7513d 100755 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import GMKit from './components/GMKit'; import PlayerManagement from './components/PlayerManagement'; import MissionTab from './components/MissionTab'; import SimulationTab from './components/SimulationTab'; +import ErrorReports from './components/ErrorReports'; import { useState, useEffect } from 'react'; import axios from 'axios'; import { debug, info, warn, error, logApiCall, logApiError, logUserAction } from './utils/logger'; @@ -326,9 +327,21 @@ function App() { rel="noopener noreferrer" className="px-4 py-2 rounded-lg font-medium transition-all bg-slate-700/50 text-slate-200 hover:bg-slate-600/50" > - 🖨 Print +🖨 Print + {authedPlayer === 'gm' && ( + className={`px-4 py-2 rounded-lg font-medium transition-all ${tab==='error' ? 'bg-red-600 text-white shadow-lg' : 'bg-slate-700/50 text-slate-200 hover:bg-slate-600/50'}`} + onClick={()=>{logUserAction('navigation', 'Tab switch', { from: tab, to: 'error' }); setTab('error')}} + > + ⚠ Error + + {authedPlayer === 'gm' && ( <> + + + {/* Submit Form */} + {showForm && ( +
+
+ + +
+
+ + setForm({ ...form, title: e.target.value })} + placeholder="Short description of the issue" + className="w-full rounded-lg border border-slate-600 bg-slate-700 px-3 py-2 text-white placeholder-slate-400" + /> +
+
+ +