feat: initial Papo bot scaffold
This commit is contained in:
110
src/web/routes/dashboard.ts
Normal file
110
src/web/routes/dashboard.ts
Normal 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;
|
||||
Reference in New Issue
Block a user