feat: footer, auto-pause on no viewers, and Redsys question submissions

- Footer: add site-footer with one-liner + Cloud Host branding linking to cloudhost.es (mobile only, hidden on desktop)
- Auto-pause: game pauses automatically after AUTOPAUSE_DELAY_MS (default 60s) with no viewers connected; auto-resumes when first viewer connects; shows "Esperando espectadores…" in the UI
- Redsys: new /pregunta page lets viewers pay 1€ via Redsys to submit a fill-in question; submitted questions are used as prompts in the next round instead of AI generation; new redsys.ts implements HMAC_SHA256_V1 signing (3DES key derivation + HMAC-SHA256); notification endpoint at /api/redsys/notificacion handles server-to-server confirmation
- db.ts: questions table with createPendingQuestion / markQuestionPaid / getNextPendingQuestion / markQuestionUsed
- .env.sample: added AUTOPAUSE_DELAY_MS, PUBLIC_URL, REDSYS_* vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 14:03:30 +01:00
parent 4b0b9f8f50
commit 2fac92356d
12 changed files with 747 additions and 20 deletions

33
db.ts
View File

@@ -41,3 +41,36 @@ export function clearAllRounds() {
db.exec("DELETE FROM rounds;");
db.exec("DELETE FROM sqlite_sequence WHERE name = 'rounds';");
}
// ── Questions (user-submitted via Redsys) ───────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
order_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
export function createPendingQuestion(text: string, orderId: string): number {
const stmt = db.prepare("INSERT INTO questions (text, order_id) VALUES ($text, $orderId)");
const result = stmt.run({ $text: text, $orderId: orderId });
return result.lastInsertRowid as number;
}
export function markQuestionPaid(orderId: string): boolean {
const stmt = db.prepare("UPDATE questions SET status = 'paid' WHERE order_id = $orderId AND status = 'pending'");
const result = stmt.run({ $orderId: orderId });
return result.changes > 0;
}
export function getNextPendingQuestion(): { id: number; text: string; order_id: string } | null {
return db.query("SELECT id, text, order_id FROM questions WHERE status = 'paid' ORDER BY id ASC LIMIT 1")
.get() as { id: number; text: string; order_id: string } | null;
}
export function markQuestionUsed(id: number): void {
db.prepare("UPDATE questions SET status = 'used' WHERE id = $id").run({ $id: id });
}