import { packageStorage } from "kody:runtime"; export default { async fetch(request) { if (new URL(request.url).pathname.endsWith("/api/complaints")) { if (request.method !== "POST") return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "content-type": "application/json", "allow": "POST" } }); let data; try { data = await request.json(); } catch { return new Response(JSON.stringify({ error: "Please submit a valid form." }), { status: 400, headers: { "content-type": "application/json" } }); } const allowed = ["Bug or technical issue", "AI difficulty or gameplay", "Accessibility", "Suggestion", "Other"]; const category = typeof data.category === "string" ? data.category.slice(0, 60) : ""; const subject = typeof data.subject === "string" ? data.subject.trim().slice(0, 100) : ""; const details = typeof data.details === "string" ? data.details.trim().slice(0, 2000) : ""; const contact = typeof data.contact === "string" ? data.contact.trim().slice(0, 120) : ""; if (!allowed.includes(category) || !subject || !details) return new Response(JSON.stringify({ error: "Please complete the topic, subject, and details." }), { status: 400, headers: { "content-type": "application/json" } }); try { const db = packageStorage(); const ticket = crypto.randomUUID(); const createdAt = new Date().toISOString(); const isBug = category === "Bug or technical issue"; const reportText = (subject + " " + details).toLowerCase(); const signals = [ { tag: "gameplay", words: ["win", "draw", "turn", "square", "board", "move", "score"] }, { tag: "ai", words: ["ai", "computer", "difficulty", "easy", "medium", "hard", "opponent"] }, { tag: "interface", words: ["button", "screen", "layout", "mobile", "display", "click", "page"] }, { tag: "performance", words: ["slow", "freeze", "hang", "load", "crash", "stuck"] } ]; const matched = signals.filter(signal => signal.words.some(word => reportText.includes(word))).map(signal => signal.tag); const triage = isBug ? { state: "needs_code_review", labels: matched.length ? matched : ["uncategorized"], priority: /crash|security|private|data loss|can't submit|cannot submit/.test(reportText) ? "high" : "normal", nextStep: "Reproduce the report, prepare a minimal patch, run checks and regression tests, then request approval before publishing." } : { state: "not_a_bug_report", labels: [category.toLowerCase().replace(/[^a-z0-9]+/g, "-")], priority: "normal", nextStep: "Review feedback and decide whether to make a change." }; const status = isBug ? "pending_review" : "received"; const complaint = { ticket, category, subject, details, contact: contact || null, status, triage, createdAt }; await db.set("complaint:" + ticket, JSON.stringify(complaint)); await db.set("complaint-index:" + createdAt + ":" + ticket, ticket); return new Response(JSON.stringify({ ok: true, ticket, status, triage: { state: triage.state, labels: triage.labels, priority: triage.priority } }), { status: 201, headers: { "content-type": "application/json", "cache-control": "no-store" } }); } catch (err) { return new Response(JSON.stringify({ error: "We couldn’t save your report right now. Please try again later." }), { status: 503, headers: { "content-type": "application/json", "cache-control": "no-store" } }); } } return new Response("Tic-Tac-Toe
Classic strategy

Tic-Tac-Toe

Three in a row wins the round.

✕ Player X
0
○ Player O
0
Player X's turn

Tap a square to make your move. The computer plays as O in AI mode.

Help us improve

Complaints

Tell us what went wrong or how we can improve.

Privacy note: reports are saved for review. Bug reports are automatically labeled using simple keyword matching, but code is never changed automatically from public comments. Avoid entering private or sensitive information.

", { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }); } };