feat: initial Papo bot scaffold

This commit is contained in:
Pascal.P
2025-11-30 11:04:41 +01:00
commit 000481a3b0
12168 changed files with 1584750 additions and 0 deletions

110
src/web/routes/dashboard.ts Normal file
View File

@@ -0,0 +1,110 @@
import { Router } from 'express';
import { settings } from '../../config/state.js';
const router = Router();
router.get('/', (_req, res) => {
res.send(`
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Papo Dashboard</title>
<style>
:root { --bg:#0f172a; --card:#111827; --text:#e5e7eb; --accent:#22c55e; --muted:#9ca3af; }
body { margin:0; font-family: 'Inter', system-ui, -apple-system, sans-serif; background:radial-gradient(circle at 10% 20%, #1f2937 0, transparent 25%), radial-gradient(circle at 80% 0, #0ea5e9 0, transparent 25%), var(--bg); color:var(--text); }
header { padding:24px 32px; display:flex; justify-content:space-between; align-items:center; }
h1 { margin:0; font-size:24px; letter-spacing:0.5px; }
main { padding:0 32px 48px; display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap:16px; }
.card { background:var(--card); border:1px solid rgba(255,255,255,0.05); border-radius:14px; padding:16px 18px; box-shadow:0 10px 30px rgba(0,0,0,0.25); }
.stat { font-size:32px; font-weight:700; margin:0; }
.label { color:var(--muted); margin:4px 0 0 0; }
.ticket-list { max-height:320px; overflow:auto; }
.ticket { padding:10px; border-radius:10px; background:rgba(255,255,255,0.02); margin-bottom:8px; border:1px solid rgba(255,255,255,0.04); }
.pill { display:inline-block; padding:4px 8px; border-radius:999px; font-size:12px; font-weight:600; }
.pill.open { background:rgba(34,197,94,0.15); color:#22c55e; }
.pill.in-progress { background:rgba(234,179,8,0.15); color:#eab308; }
.pill.closed { background:rgba(239,68,68,0.15); color:#ef4444; }
form { display:flex; flex-direction:column; gap:8px; margin-top:8px; }
input, select { padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.05); color:var(--text); }
button { padding:10px 12px; border-radius:10px; border:none; background:linear-gradient(135deg, #10b981, #22c55e); color:white; font-weight:700; cursor:pointer; }
button:hover { filter:brightness(1.05); }
.muted { color:var(--muted); font-size:13px; }
</style>
</head>
<body>
<header>
<h1>Papo Control</h1>
<span class="muted">Live Overview & Tickets</span>
</header>
<main>
<section class="card">
<p class="label">Tickets offen</p>
<p class="stat" id="openCount">-</p>
<p class="label">in-progress / geschlossen: <span id="ipCount">-</span> / <span id="closedCount">-</span></p>
</section>
<section class="card ticket-list">
<p class="label">Neueste Tickets</p>
<div id="ticketList"></div>
</section>
<section class="card">
<p class="label">Einstellungen speichern</p>
<form id="settingsForm">
<input name="guildId" placeholder="Guild ID" required />
<input name="welcomeChannelId" placeholder="Welcome Channel ID" />
<input name="logChannelId" placeholder="Log Channel ID" />
<select name="automodEnabled">
<option value="">Automod unverändert</option>
<option value="true">Automod an</option>
<option value="false">Automod aus</option>
</select>
<select name="levelingEnabled">
<option value="">Leveling unverändert</option>
<option value="true">Leveling an</option>
<option value="false">Leveling aus</option>
</select>
<button type="submit">Speichern</button>
<p class="muted" id="saveStatus"></p>
</form>
</section>
</main>
<script>
async function loadOverview() {
const res = await fetch('/api/overview'); const data = await res.json();
document.getElementById('openCount').textContent = data.tickets?.open ?? '-';
document.getElementById('ipCount').textContent = data.tickets?.inProgress ?? '-';
document.getElementById('closedCount').textContent = data.tickets?.closed ?? '-';
}
async function loadTickets() {
const res = await fetch('/api/tickets'); const data = await res.json();
const box = document.getElementById('ticketList'); box.innerHTML = '';
(data.tickets || []).forEach(t => {
const div = document.createElement('div'); div.className = 'ticket';
div.innerHTML = \`<div><strong>\${t.topic || 'Ticket'}</strong> — \${t.userId}</div>
<div class="muted">#\${t.channelId} • Priorität: \${t.priority || 'normal'}</div>
<div class="pill \${(t.status||'open').replace(/\\\\s/g,'-')}">\${t.status}</div>\`;
box.appendChild(div);
});
}
document.getElementById('settingsForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
const payload = Object.fromEntries(form.entries());
['automodEnabled','levelingEnabled'].forEach(k => { if (payload[k]==='') delete payload[k]; });
const res = await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
document.getElementById('saveStatus').textContent = res.ok ? 'Gespeichert' : 'Fehler';
});
loadOverview(); loadTickets();
setInterval(loadOverview, 10000); setInterval(loadTickets, 12000);
</script>
</body>
</html>
`);
});
router.get('/settings', (_req, res) => {
res.json({ settings: Array.from(settings.entries()) });
});
export default router;