UI overhaul: fire-gradient theme and visual polish; logic: added answer shuffling and improved Baserow error handling
This commit is contained in:
+33
-24
@@ -5,10 +5,6 @@ const { calculateScore, sanitizeQuestion, parseCorrectAnswers } = require('./qui
|
||||
|
||||
const sessions = new Map();
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function generateCode() {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
let code = '';
|
||||
@@ -87,13 +83,13 @@ function setupWebSocket(server) {
|
||||
try {
|
||||
handleMessage(ws, JSON.parse(raw));
|
||||
} catch (e) {
|
||||
console.error('[ws] Message error:', e);
|
||||
send(ws, { type: 'error', message: 'Ungueltige Nachricht' });
|
||||
}
|
||||
});
|
||||
ws.on('close', () => handleDisconnect(ws));
|
||||
});
|
||||
|
||||
// Heartbeat every 30 s
|
||||
setInterval(() => {
|
||||
wss.clients.forEach((ws) => {
|
||||
if (!ws.isAlive) return ws.terminate();
|
||||
@@ -108,6 +104,7 @@ function setupWebSocket(server) {
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
async function handleMessage(ws, msg) {
|
||||
console.log(`[ws] Received: ${msg.type}`);
|
||||
switch (msg.type) {
|
||||
case 'admin-create':
|
||||
return adminCreate(ws, msg);
|
||||
@@ -115,6 +112,8 @@ async function handleMessage(ws, msg) {
|
||||
return adminJoin(ws, msg);
|
||||
case 'admin-start':
|
||||
return adminStart(ws, msg);
|
||||
case 'admin-show-result':
|
||||
return adminShowResult(ws, msg);
|
||||
case 'admin-next':
|
||||
return adminNext(ws, msg);
|
||||
case 'admin-end':
|
||||
@@ -126,7 +125,7 @@ async function handleMessage(ws, msg) {
|
||||
case 'answer':
|
||||
return playerAnswer(ws, msg);
|
||||
default:
|
||||
send(ws, { type: 'error', message: 'Unbekannter Typ' });
|
||||
send(ws, { type: 'error', message: 'Unbekannter Typ: ' + msg.type });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +143,15 @@ async function adminCreate(ws, msg) {
|
||||
let questions;
|
||||
try {
|
||||
questions = await listRows(set.tableId);
|
||||
console.log(`[live] Loaded ${questions.length} questions for set "${set.name}"`);
|
||||
if (questions.length > 0) {
|
||||
console.log(`[live] Sample fields:`, Object.keys(questions[0]).join(', '));
|
||||
}
|
||||
} catch (e) {
|
||||
return send(ws, { type: 'error', message: 'Fehler beim Laden: ' + e.message });
|
||||
console.error('[live] Baserow error:', e.message);
|
||||
return send(ws, { type: 'error', message: 'Fehler beim Laden der Fragen: ' + e.message });
|
||||
}
|
||||
if (!questions.length) return send(ws, { type: 'error', message: 'Keine Fragen gefunden' });
|
||||
if (!questions.length) return send(ws, { type: 'error', message: 'Keine Fragen in dieser Tabelle gefunden' });
|
||||
|
||||
const opts = {
|
||||
shuffle: msg.options.shuffle !== false,
|
||||
@@ -216,6 +220,14 @@ function adminStart(ws, msg) {
|
||||
sendNextQuestion(s);
|
||||
}
|
||||
|
||||
function adminShowResult(ws, msg) {
|
||||
if (!isAdmin(msg.token)) return;
|
||||
const s = sessions.get(msg.code);
|
||||
if (!s || s.status !== 'active') return;
|
||||
console.log(`[live] Admin forced show result for session ${s.code}`);
|
||||
showResult(s);
|
||||
}
|
||||
|
||||
function adminNext(ws, msg) {
|
||||
if (!isAdmin(msg.token)) return;
|
||||
const s = sessions.get(msg.code);
|
||||
@@ -236,8 +248,8 @@ async function adminEnd(ws, msg) {
|
||||
|
||||
function playerJoin(ws, msg) {
|
||||
const s = sessions.get(msg.code);
|
||||
if (!s) return send(ws, { type: 'error', message: 'Session nicht gefunden' });
|
||||
if (s.status !== 'waiting') return send(ws, { type: 'error', message: 'Session laeuft bereits' });
|
||||
if (!s) return send(ws, { type: 'error', message: 'Session nicht gefunden. Pruefe den Code.' });
|
||||
if (s.status !== 'waiting') return send(ws, { type: 'error', message: 'Session laeuft bereits oder ist beendet.' });
|
||||
|
||||
const name = (msg.name || '').trim();
|
||||
if (!name) return send(ws, { type: 'error', message: 'Name darf nicht leer sein' });
|
||||
@@ -247,7 +259,7 @@ function playerJoin(ws, msg) {
|
||||
if (existing.ws && existing.ws.readyState === WebSocket.OPEN) {
|
||||
return send(ws, { type: 'error', message: 'Name bereits online. Bitte waehle einen anderen.' });
|
||||
}
|
||||
existing.ws = ws; // Reconnect
|
||||
existing.ws = ws;
|
||||
} else {
|
||||
s.players.set(name, { ws, score: 0, answers: [] });
|
||||
}
|
||||
@@ -263,13 +275,13 @@ function playerAnswer(ws, msg) {
|
||||
if (!qa || qa.role !== 'player') return;
|
||||
const s = sessions.get(qa.code);
|
||||
if (!s || s.status !== 'active') return;
|
||||
if (s.currentAnswers.has(qa.name)) return; // Already answered
|
||||
if (s.currentAnswers.has(qa.name)) return;
|
||||
|
||||
const elapsed = (Date.now() - s.questionStartTime) / 1000;
|
||||
const tl = s.options.scoring === 'time' ? s.options.timeLimit : 0;
|
||||
const remaining = tl > 0 ? Math.max(0, tl - elapsed) : 0;
|
||||
|
||||
if (tl > 0 && remaining <= 0) return; // Time up
|
||||
if (tl > 0 && remaining <= 0) return;
|
||||
|
||||
s.currentAnswers.set(qa.name, { answers: msg.answers || [], timeRemaining: remaining });
|
||||
broadcastAdmins(s, { type: 'answer-count', count: s.currentAnswers.size, total: s.players.size });
|
||||
@@ -292,12 +304,15 @@ function sendNextQuestion(s) {
|
||||
|
||||
const q = s.questions[s.currentQuestionIndex];
|
||||
const tl = s.options.scoring === 'time' ? s.options.timeLimit : 0;
|
||||
const sanitized = sanitizeQuestion(q);
|
||||
|
||||
console.log(`[live] Session ${s.code}: Sending question ${s.currentQuestionIndex + 1}/${s.questions.length}`);
|
||||
|
||||
broadcastAll(s, {
|
||||
type: 'question',
|
||||
index: s.currentQuestionIndex,
|
||||
total: s.questions.length,
|
||||
question: sanitizeQuestion(q),
|
||||
question: sanitized,
|
||||
timeLimit: tl,
|
||||
});
|
||||
|
||||
@@ -317,11 +332,9 @@ function showResult(s) {
|
||||
const correct = parseCorrectAnswers(q);
|
||||
const tl = s.options.scoring === 'time' ? s.options.timeLimit : 0;
|
||||
|
||||
// Answer distribution
|
||||
const stats = {};
|
||||
['A', 'B', 'C', 'D'].forEach((k) => { if (q[`Antwort ${k}`]) stats[k] = 0; });
|
||||
|
||||
// Score for each player who answered
|
||||
for (const [name, ans] of s.currentAnswers) {
|
||||
const player = s.players.get(name);
|
||||
const result = calculateScore(q, ans.answers, ans.timeRemaining, tl, s.options.scoring);
|
||||
@@ -336,7 +349,6 @@ function showResult(s) {
|
||||
});
|
||||
}
|
||||
|
||||
// Players who didn't answer
|
||||
for (const [name, player] of s.players) {
|
||||
if (!s.currentAnswers.has(name)) {
|
||||
player.answers.push({ qId: q.id, idx: s.currentQuestionIndex, answers: [], correct: false, points: 0 });
|
||||
@@ -350,7 +362,6 @@ function showResult(s) {
|
||||
|
||||
const lb = leaderboard(s);
|
||||
|
||||
// Send personal results to players
|
||||
for (const [name, player] of s.players) {
|
||||
const last = player.answers[player.answers.length - 1];
|
||||
send(player.ws, {
|
||||
@@ -372,10 +383,12 @@ async function endSession(s) {
|
||||
const config = readConfig();
|
||||
if (config.answersTableId && s.results.length) {
|
||||
await batchCreateRows(config.answersTableId, s.results);
|
||||
console.log(`[quizalarm] Session ${s.code}: ${s.results.length} Ergebnisse gespeichert`);
|
||||
console.log(`[live] Session ${s.code}: ${s.results.length} Ergebnisse in Baserow gespeichert`);
|
||||
} else {
|
||||
console.log(`[live] Session ${s.code}: Keine Antworten-Tabelle konfiguriert oder keine Ergebnisse`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[quizalarm] Fehler beim Speichern:', e.message);
|
||||
console.error('[live] Fehler beim Speichern:', e.message);
|
||||
}
|
||||
|
||||
setTimeout(() => sessions.delete(s.code), 300000);
|
||||
@@ -398,10 +411,6 @@ function handleDisconnect(ws) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exports */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function getSessions() {
|
||||
return Array.from(sessions.values()).map(sessionInfo);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user