Implement Wettkampf mode with random type-aware question selection and answer persistence

This commit is contained in:
2026-04-26 11:01:58 +02:00
parent c1b93e3ab6
commit 7b886c4355
3 changed files with 797 additions and 7 deletions
+9 -1
View File
@@ -61,4 +61,12 @@ async function batchCreateRows(tableId, rows) {
}
}
module.exports = { listRows, createRow, batchCreateRows };
async function updateRow(tableId, rowId, data) {
console.log(`[baserow] Update row ${rowId} in table ${tableId}`);
return baserowFetch(`/database/rows/table/${tableId}/${rowId}/?user_field_names=true`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
module.exports = { listRows, createRow, batchCreateRows, updateRow };
+128 -4
View File
@@ -2,7 +2,7 @@ const express = require('express');
const http = require('http');
const path = require('path');
const { getEnvConfig, readConfig, writeConfig } = require('./config');
const { listRows, createRow, batchCreateRows } = require('./baserow');
const { listRows, createRow, batchCreateRows, updateRow } = require('./baserow');
const { setupWebSocket, getSessions } = require('./live-session');
const { extractFieldValue } = require('./quiz-logic');
@@ -106,19 +106,77 @@ app.get('/api/sets/:id/questions', async (req, res) => {
}
});
/* ---- Solo: Save single answer immediately ---- */
app.get('/api/sets/:id/count', async (req, res) => {
try {
const config = readConfig();
const set = config.sets.find((s) => s.id === req.params.id);
if (!set) return res.status(404).json({ error: 'Fragenset nicht gefunden' });
const rows = await listRows(set.tableId);
res.json({ count: rows.length });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
/* ---- Solo/Wettkampf: Save single answer immediately ---- */
app.post('/api/results/single', async (req, res) => {
try {
const config = readConfig();
if (!config.answersTableId) return res.status(400).json({ error: 'Antworten-Tabelle nicht konfiguriert' });
await createRow(config.answersTableId, req.body);
res.json({ success: true });
const row = await createRow(config.answersTableId, req.body);
res.json({ success: true, id: row.id });
} catch (e) {
console.error('[api] Single save error:', e.message);
res.status(500).json({ error: e.message });
}
});
/* ---- Wettkampf: Update existing answer ---- */
app.put('/api/results/update', async (req, res) => {
try {
const config = readConfig();
if (!config.answersTableId) return res.status(400).json({ error: 'Antworten-Tabelle nicht konfiguriert' });
const { sessionId, frageId, nutzername, antwort } = req.body;
if (!sessionId || !frageId || !nutzername) return res.status(400).json({ error: 'sessionId, frageId und nutzername erforderlich' });
// Find the existing row
const rows = await listRows(config.answersTableId, {
Session_ID: sessionId,
Nutzername: nutzername,
});
const existing = rows.find((r) => r['Frage_ID'] === frageId);
if (!existing) return res.status(404).json({ error: 'Antwort nicht gefunden' });
await updateRow(config.answersTableId, existing.id, { Antwort: antwort });
res.json({ success: true });
} catch (e) {
console.error('[api] Update answer error:', e.message);
res.status(500).json({ error: e.message });
}
});
/* ---- Wettkampf: Finalize scores ---- */
app.put('/api/results/finalize', async (req, res) => {
try {
const config = readConfig();
if (!config.answersTableId) return res.status(400).json({ error: 'Antworten-Tabelle nicht konfiguriert' });
const { results } = req.body;
if (!results || !results.length) return res.status(400).json({ error: 'Keine Ergebnisse' });
// results is array of { rowId, richtig, punkte }
for (const r of results) {
await updateRow(config.answersTableId, r.rowId, {
Richtig: r.richtig,
Punkte: r.punkte,
});
}
res.json({ success: true });
} catch (e) {
console.error('[api] Finalize error:', e.message);
res.status(500).json({ error: e.message });
}
});
/* ---- Solo: Check for incomplete session ---- */
app.get('/api/progress/:name/:setId', async (req, res) => {
try {
@@ -187,6 +245,72 @@ app.get('/api/progress/:name/:setId', async (req, res) => {
}
});
/* ---- Wettkampf: Check for incomplete session ---- */
app.get('/api/wettkampf-progress/:name/:setId', async (req, res) => {
try {
const config = readConfig();
const set = config.sets.find((s) => s.id === req.params.setId);
if (!set) return res.status(404).json({ error: 'Set nicht gefunden' });
if (!config.answersTableId) return res.json({ hasProgress: false });
const allAnswers = await listRows(config.answersTableId, {
Nutzername: req.params.name,
Fragenset: set.name,
});
// Filter to Wettkampf only
const wkAnswers = allAnswers.filter((a) => {
return extractFieldValue(a['Modus'], '') === 'Wettkampf';
});
// Group by Session_ID
const bySession = {};
wkAnswers.forEach((a) => {
const sid = a['Session_ID'] || '';
if (!bySession[sid]) bySession[sid] = [];
bySession[sid].push(a);
});
// Find the most recent session where NOT all answers are finalized (Punkte is still 0 for all)
let best = null;
for (const [sid, answers] of Object.entries(bySession)) {
// A session is "incomplete" if all Punkte are 0 and all Richtig are false (not yet confirmed)
const allUnscored = answers.every((a) => {
const punkte = Number(a['Punkte']) || 0;
const richtig = a['Richtig'];
return punkte === 0 && (richtig === false || richtig === 'false' || !richtig);
});
if (allUnscored && answers.length > 0) {
const latest = answers.reduce((max, a) => {
const ts = a['Zeitstempel'] || '';
return ts > max ? ts : max;
}, '');
if (!best || latest > best.latest) {
best = { sid, answers, latest };
}
}
}
if (best) {
res.json({
hasProgress: true,
sessionId: best.sid,
answers: best.answers.map((a) => ({
rowId: a.id,
Frage_ID: a['Frage_ID'],
Antwort: a['Antwort'] || '',
})),
totalAnswered: best.answers.length,
});
} else {
res.json({ hasProgress: false });
}
} catch (e) {
console.error('[api] Wettkampf progress error:', e.message);
res.status(500).json({ error: e.message });
}
});
/* ---- Batch save (still used by live sessions) ---- */
app.post('/api/results', async (req, res) => {
try {