UX-Paket: Navbar aufgeräumt, /server-Seite, Lightbox, Skeletons, Audit-Log, Reroll
- Navbar entschlackt: nur noch Devlog · Roadmap · Server · Community · Setup; Galerie/Level/Events/Changelog im Community-Dropdown, Profil/Commits/Logout im Avatar-Menü; Mobile-Burger mit Drawer - Öffentliche /server-Seite: Live-Status-Karten mit Spieler-Balken, Map, Connect-Button und 24h-Spielerzahl-Chart (SVG); Monitor sampelt jede Abfrage in player_history (7 Tage Retention), GET /api/servers liefert nur öffentliche Felder (keine Query-URLs/Hosts) - Lightbox: Galerie- und Devlog-Bilder öffnen im Overlay mit Pfeiltasten, Buttons, Wisch-Gesten und Zähler statt neuem Tab - Skeleton-Loader: schimmernde Platzhalter auf Devlogs, Roadmap, Galerie, Level, Changelog und Server statt "Lade …" - Team-Audit-Log: audit_log-Tabelle (max 500 Einträge), Logging bei Settings/Composer/Vorlagen/Rollen-Menüs/Gameservern/Team/API-Keys/ Branding, GET /api/auditlog (Owner) + Liste im Team-Tab - Giveaway: 🔁 Reroll-Button (Admin-only) + 👥 Teilnehmerliste am Gewinner-Post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { registerExtras } from './extras.js';
|
||||
import { handleApplyButton, handleApplyModal, handleReviewButton } from './app-forms.js';
|
||||
import { closeTicketWithTranscript } from './tickets.js';
|
||||
import { handleRoleMenuButton } from './role-menus.js';
|
||||
import { handleGiveawayAdminButton } from './giveaways.js';
|
||||
import {
|
||||
addPlaytester, removePlaytester, isWish, bumpWish,
|
||||
getGiveaway, toggleGiveawayEntry, giveawayEntries,
|
||||
@@ -251,6 +252,16 @@ export async function startBot() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔁/👥-Buttons am Gewinner-Post: Reroll (Admin) + Teilnehmerliste
|
||||
if (interaction.isButton() && /^(greroll|glist):/.test(interaction.customId)) {
|
||||
try {
|
||||
await handleGiveawayAdminButton(interaction);
|
||||
} catch (error) {
|
||||
console.error('[giveaway] Admin-Button fehlgeschlagen:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 🎉-Button: Giveaway-Teilnahme togglen
|
||||
if (interaction.isButton() && interaction.customId === 'giveaway_enter') {
|
||||
const g = getGiveaway(interaction.message.id);
|
||||
|
||||
+51
-3
@@ -1,6 +1,7 @@
|
||||
// Giveaway-Ziehung: Minuten-Scheduler beendet fällige Verlosungen automatisch
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { dueGiveaways, markGiveawayEnded, giveawayEntries } from '../db.js';
|
||||
// Giveaway-Ziehung: Minuten-Scheduler beendet fällige Verlosungen automatisch,
|
||||
// danach gibt's am Gewinner-Post einen Reroll- und Teilnehmer-Button (Admin).
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags, PermissionFlagsBits } from 'discord.js';
|
||||
import { dueGiveaways, markGiveawayEnded, giveawayEntries, getGiveaway } from '../db.js';
|
||||
import { brandFooter } from '../runtime-settings.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 60 * 1000;
|
||||
@@ -50,6 +51,7 @@ export async function giveawayTick(client) {
|
||||
await channel.send({
|
||||
content: `🎉 **${g.prize}** geht an: ${mentions} — Glückwunsch! 🏆`,
|
||||
allowedMentions: { users: winners },
|
||||
components: [giveawayAdminRow(g.message_id)],
|
||||
});
|
||||
}
|
||||
console.log(`[giveaway] '${g.prize}' beendet (${entries.length} Teilnahmen)`);
|
||||
@@ -59,6 +61,52 @@ export async function giveawayTick(client) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Admin-Buttons unterm Gewinner-Post: neu auslosen + Teilnehmer ansehen */
|
||||
function giveawayAdminRow(giveawayId) {
|
||||
return new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder().setCustomId(`greroll:${giveawayId}`).setStyle(ButtonStyle.Secondary).setLabel('Neu auslosen').setEmoji('🔁'),
|
||||
new ButtonBuilder().setCustomId(`glist:${giveawayId}`).setStyle(ButtonStyle.Secondary).setLabel('Teilnehmer').setEmoji('👥')
|
||||
);
|
||||
}
|
||||
|
||||
/** greroll:/glist:-Buttons — Reroll nur für Admins, Liste ephemeral */
|
||||
export async function handleGiveawayAdminButton(interaction) {
|
||||
const [kind, giveawayId] = interaction.customId.split(':');
|
||||
const g = getGiveaway(giveawayId);
|
||||
if (!g) {
|
||||
await interaction.reply({ content: '❌ Giveaway nicht mehr gefunden.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
const entries = giveawayEntries(giveawayId);
|
||||
|
||||
if (kind === 'glist') {
|
||||
const list = entries.slice(0, 50).map((id) => `<@${id}>`).join(', ');
|
||||
await interaction.reply({
|
||||
content: entries.length
|
||||
? `👥 **${entries.length} Teilnahmen** an **${g.prize}**:\n${list}${entries.length > 50 ? ' …' : ''}`
|
||||
: `👥 Keine Teilnahmen an **${g.prize}**.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
allowedMentions: { parse: [] },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Reroll: nur Admins
|
||||
if (!interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)) {
|
||||
await interaction.reply({ content: '❌ Neu auslosen dürfen nur Admins.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
await interaction.reply({ content: '❌ Keine Teilnahmen — nichts zum Auslosen.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
const [winner] = drawWinners(entries, 1);
|
||||
await interaction.reply({
|
||||
content: `🔁 Neu ausgelost: **${g.prize}** geht jetzt an <@${winner}> — Glückwunsch! 🏆`,
|
||||
allowedMentions: { users: [winner] },
|
||||
});
|
||||
}
|
||||
|
||||
export function startGiveaways(client) {
|
||||
setInterval(() => giveawayTick(client).catch((e) => console.error('[giveaway]', e)), CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Down/Up-Alerts mit 2-Fails-Schwelle. Verwaltet im Setup-Tab "Server".
|
||||
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
|
||||
import { GameDig } from 'gamedig';
|
||||
import { getSetting, listGameservers, setGameserverMessage } from '../db.js';
|
||||
import { getSetting, listGameservers, setGameserverMessage, recordPlayerSample, prunePlayerHistory } from '../db.js';
|
||||
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
|
||||
@@ -25,6 +25,9 @@ export const GAME_ICONS = {
|
||||
// Alert-Zustand pro Server (in-memory)
|
||||
const alertState = new Map(); // id → { fails, down, since }
|
||||
|
||||
// Letztes Query-Ergebnis pro Server — für die öffentliche /server-Seite
|
||||
export const lastResults = new Map(); // id → Query-Ergebnis + checkedAt
|
||||
|
||||
/** Einen Server abfragen — fivem/http direkt, alles andere über gamedig */
|
||||
export async function queryServer(server) {
|
||||
const base = {
|
||||
@@ -155,6 +158,13 @@ export async function monitorTick(client) {
|
||||
|
||||
const results = await Promise.all(servers.map(queryServer));
|
||||
|
||||
// Verlauf für die Web-Seite festhalten + Cache fürs API
|
||||
for (const r of results) {
|
||||
lastResults.set(r.id, { ...r, checkedAt: Date.now() });
|
||||
try { recordPlayerSample(r.id, r.online, r.players); } catch { /* History ist nice-to-have */ }
|
||||
}
|
||||
try { prunePlayerHistory(); } catch { /* dito */ }
|
||||
|
||||
// Bot-Presence: Gesamtspielerzahl — nur wenn kein eigener Status (Brand-Tab) gesetzt ist
|
||||
const withPlayers = results.filter((r) => r.online && r.players != null);
|
||||
const totalPlayers = withPlayers.reduce((sum, r) => sum + r.players, 0);
|
||||
|
||||
Reference in New Issue
Block a user