feat: initial Papo bot scaffold
This commit is contained in:
47
src/web/routes/api.ts
Normal file
47
src/web/routes/api.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../../database/index.js';
|
||||
import { settings } from '../../config/state.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/overview', async (_req, res) => {
|
||||
try {
|
||||
const [open, inProgress, closed] = await Promise.all([
|
||||
prisma.ticket.count({ where: { status: 'open' } }),
|
||||
prisma.ticket.count({ where: { status: 'in-progress' } }),
|
||||
prisma.ticket.count({ where: { status: 'closed' } })
|
||||
]);
|
||||
res.json({ tickets: { open, inProgress, closed } });
|
||||
} catch (err) {
|
||||
res.json({ tickets: { open: 0, inProgress: 0, closed: 0 }, error: 'DB unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tickets', async (_req, res) => {
|
||||
try {
|
||||
const tickets = await prisma.ticket.findMany({ orderBy: { createdAt: 'desc' }, take: 20 });
|
||||
res.json({ tickets });
|
||||
} catch (err) {
|
||||
res.json({ tickets: [], error: 'DB unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/settings', (_req, res) => {
|
||||
res.json({ guilds: Array.from(settings.entries()) });
|
||||
});
|
||||
|
||||
router.post('/settings', (req, res) => {
|
||||
const { guildId, welcomeChannelId, logChannelId, automodEnabled, levelingEnabled } = req.body;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const current = settings.get(guildId) ?? {};
|
||||
settings.set(guildId, {
|
||||
...current,
|
||||
welcomeChannelId: welcomeChannelId ?? current.welcomeChannelId,
|
||||
logChannelId: logChannelId ?? current.logChannelId,
|
||||
automodEnabled: automodEnabled ?? current.automodEnabled,
|
||||
levelingEnabled: levelingEnabled ?? current.levelingEnabled
|
||||
});
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
21
src/web/routes/auth.ts
Normal file
21
src/web/routes/auth.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Router } from 'express';
|
||||
import { env } from '../../config/env.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/discord', (_req, res) => {
|
||||
const redirect = encodeURIComponent('http://localhost:' + env.port + '/auth/callback');
|
||||
const url =
|
||||
`https://discord.com/api/oauth2/authorize?client_id=${env.clientId}` +
|
||||
`&redirect_uri=${redirect}&response_type=code&scope=identify%20guilds`;
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
router.get('/callback', (req, res) => {
|
||||
const code = req.query.code;
|
||||
if (!code) return res.status(400).send('No code provided');
|
||||
// TODO: exchange code via Discord OAuth2 token endpoint
|
||||
res.send('OAuth2 Callback erhalten (Stub).');
|
||||
});
|
||||
|
||||
export default router;
|
||||
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