Add error reporting system - logged-in users can submit bug reports, GM can resolve
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
116
database/routes/errorRoutes.js
Normal file
116
database/routes/errorRoutes.js
Normal file
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
17
src/App.js
17
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
|
||||
</a>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all ${tab==='errors' ? 'bg-yellow-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: 'errors' }); setTab('errors')}}
|
||||
>
|
||||
⚠ Error
|
||||
</button>
|
||||
{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
|
||||
</button>
|
||||
{authedPlayer === 'gm' && (
|
||||
<>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all ${tab==='mission' ? 'bg-blue-600 text-white shadow-lg' : 'bg-slate-700/50 text-slate-200 hover:bg-slate-600/50'}`}
|
||||
@@ -399,7 +412,7 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab==='roller' ? <DeathwatchRoller authedPlayer={authedPlayer} /> : tab==='shop' ? <RequisitionShop authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='rules' ? <RulesTab authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='weapons' ? <WeaponsTab /> : tab==='bestiary' ? (authedPlayer === 'gm' ? <BestiaryTab /> : <div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30"><h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2><p className="text-red-200">The Bestiary is only accessible to Game Masters. Please log in with a GM account.</p></div>) : tab==='players' ? <PlayerManagement authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='gmkit' ? <GMKit authedPlayer={authedPlayer} /> : tab==='simulation' ? <SimulationTab /> : tab==='mission' ? <MissionTab authedPlayer={authedPlayer} /> : <PlayerTab
|
||||
{tab==='roller' ? <DeathwatchRoller authedPlayer={authedPlayer} /> : tab==='shop' ? <RequisitionShop authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='rules' ? <RulesTab authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='weapons' ? <WeaponsTab /> : tab==='bestiary' ? (authedPlayer === 'gm' ? <BestiaryTab /> : <div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30"><h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2><p className="text-red-200">The Bestiary is only accessible to Game Masters. Please log in with a GM account.</p></div>) : tab==='players' ? <PlayerManagement authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='gmkit' ? <GMKit authedPlayer={authedPlayer} /> : tab==='simulation' ? <SimulationTab /> : tab==='errors' ? <ErrorReports authedPlayer={authedPlayer} sessionId={sessionId} /> : tab==='mission' ? <MissionTab authedPlayer={authedPlayer} /> : <PlayerTab
|
||||
authedPlayer={authedPlayer}
|
||||
sessionId={sessionId}
|
||||
/>}
|
||||
|
||||
182
src/components/ErrorReports.jsx
Normal file
182
src/components/ErrorReports.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function ErrorReports({ authedPlayer, sessionId }) {
|
||||
const [reports, setReports] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form, setForm] = useState({ title: '', description: '', category: 'bug', pageUrl: '' });
|
||||
|
||||
const isGM = authedPlayer && authedPlayer.toLowerCase() === 'gm';
|
||||
|
||||
const headers = { 'x-session-id': sessionId };
|
||||
|
||||
const fetchReports = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/errors', { headers });
|
||||
setReports(res.data || []);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch error reports', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchReports(); }, [sessionId, authedPlayer]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await axios.post('/api/errors', { ...form, pageUrl: form.pageUrl || window.location.pathname }, { headers });
|
||||
setForm({ title: '', description: '', category: 'bug', pageUrl: '' });
|
||||
setShowForm(false);
|
||||
fetchReports();
|
||||
} catch (err) {
|
||||
console.error('Submit failed', err);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolve = async (id, note) => {
|
||||
try {
|
||||
await axios.put(`/api/errors/${id}/resolve`, { note }, { headers });
|
||||
fetchReports();
|
||||
} catch (err) { console.error('Resolve failed', err); }
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Delete this error report?')) return;
|
||||
try {
|
||||
await axios.delete(`/api/errors/${id}`, { headers });
|
||||
fetchReports();
|
||||
} catch (err) { console.error('Delete failed', err); }
|
||||
};
|
||||
|
||||
if (!authedPlayer) {
|
||||
return (
|
||||
<div className="p-6 rounded-lg bg-yellow-900/20 border border-yellow-500/30">
|
||||
<h2 className="text-xl font-bold text-yellow-300 mb-2">Login Required</h2>
|
||||
<p className="text-yellow-200">You need to be logged in to submit or view error reports.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-white">Error Reports</h2>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors"
|
||||
>
|
||||
{showForm ? 'Cancel' : '+ New Report'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Submit Form */}
|
||||
{showForm && (
|
||||
<form onSubmit={handleSubmit} className="p-4 rounded-lg bg-slate-800/80 border border-slate-600 space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-1">Category</label>
|
||||
<select
|
||||
value={form.category}
|
||||
onChange={e => setForm({ ...form, category: e.target.value })}
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-3 py-2 text-white"
|
||||
>
|
||||
<option value="bug">Bug</option>
|
||||
<option value="feature">Feature Request</option>
|
||||
<option value="ui">UI / Display</option>
|
||||
<option value="data">Data / Content</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-1">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={form.title}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-1">Description</label>
|
||||
<textarea
|
||||
required
|
||||
value={form.description}
|
||||
onChange={e => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="What happened? What did you expect? Steps to reproduce?"
|
||||
rows={4}
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-3 py-2 text-white placeholder-slate-400 resize-vertical"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-6 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-medium transition-colors"
|
||||
>
|
||||
{submitting ? 'Submitting...' : 'Submit Report'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Reports List */}
|
||||
{loading ? (
|
||||
<div className="text-center text-slate-400 py-8">Loading...</div>
|
||||
) : reports.length === 0 ? (
|
||||
<div className="text-center text-slate-400 py-8">No error reports yet. Be the first to report one!</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{reports.map(r => (
|
||||
<div key={r.id} className={`p-4 rounded-lg border ${r.status === 'resolved' ? 'bg-green-900/10 border-green-700/30' : 'bg-slate-800/60 border-slate-600'}`}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${r.status === 'resolved' ? 'bg-green-800/50 text-green-300' : 'bg-yellow-800/50 text-yellow-300'}`}>
|
||||
{r.status}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded text-xs bg-slate-700 text-slate-300">
|
||||
{r.category}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">by <strong>{r.player_name}</strong></span>
|
||||
<span className="text-xs text-slate-500">{new Date(r.created_at).toLocaleString()}</span>
|
||||
</div>
|
||||
<h4 className="text-white font-medium">{r.title}</h4>
|
||||
<p className="text-slate-300 text-sm mt-1 whitespace-pre-wrap">{r.description}</p>
|
||||
{r.page_url && <p className="text-xs text-slate-500 mt-1">Page: {r.page_url}</p>}
|
||||
{r.resolved_by && <p className="text-xs text-green-400 mt-1">Resolved by {r.resolved_by}{r.resolved_note ? `: ${r.resolved_note}` : ''}</p>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{isGM && r.status !== 'resolved' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const note = prompt('Resolution note (optional):');
|
||||
if (note !== null) handleResolve(r.id, note);
|
||||
}}
|
||||
className="px-3 py-1 rounded text-xs bg-green-700 hover:bg-green-600 text-white"
|
||||
>
|
||||
Resolve
|
||||
</button>
|
||||
)}
|
||||
{isGM && (
|
||||
<button
|
||||
onClick={() => handleDelete(r.id)}
|
||||
className="px-3 py-1 rounded text-xs bg-red-700 hover:bg-red-600 text-white"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user