import { useEffect, useRef, useState } from 'react';
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
import EmbedComposer from '../components/EmbedComposer.jsx';
import {
IconStatus, IconBrand, IconFeeds, IconCommunity, IconRoles, IconSupport,
IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX,
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
IconSearch, IconSystem as IconTune, IconStar, IconGame, IconTrash,
} from '../icons.jsx';
import { moduleIcon } from '../module-icons.jsx';
import ModulePage from '../components/ModulePage.jsx';
import UptimeTrack from '../components/UptimeTrack.jsx';
import Dialog from '../components/Dialog.jsx';
import EmojiPick from '../components/EmojiPick.jsx';
import { findSettings, SETTING_INDEX } from '../setting-index.js';
import { Markdown } from '../markdown.jsx';
const API_SCOPES = [
{ id: 'message', label: 'message', hint: 'Nachrichten/Embeds in Kanäle posten' },
{ id: 'dm', label: 'dm', hint: 'Direktnachrichten senden' },
{ id: 'roles', label: 'roles', hint: 'Rollen geben/nehmen' },
{ id: 'read', label: 'read', hint: 'Member-Infos & Statistiken lesen' },
];
// Bereiche der Seitenleiste — bei 15 Tabs findet man ohne Gruppierung nichts mehr.
const TAB_SECTIONS = [
{ id: 'ueberblick', label: 'Überblick' },
{ id: 'auftritt', label: 'Auftritt' },
{ id: 'inhalte', label: 'Inhalte' },
{ id: 'community', label: 'Community' },
{ id: 'technik', label: 'Technik' },
{ id: 'zugang', label: 'Zugang' },
];
// `find` sind zusätzliche Suchbegriffe: was jemand tippt, wenn er den Tab-Namen
// nicht kennt („geburtstag" → Community, „willkommen" → Support).
const TABS = [
{ id: 'status', Icon: IconStatus, label: 'Status', section: 'ueberblick',
find: 'uptime laufzeit datenbank backup version zahlen übersicht' },
{ id: 'module', Icon: IconLayers, label: 'Module', section: 'ueberblick',
find: 'funktionen an aus schalter aktivieren deaktivieren' },
{ id: 'texte', Icon: IconMessage, label: 'Texte', section: 'ueberblick',
find: 'nachrichten vorlagen platzhalter begrüßung wortlaut' },
{ id: 'werte', Icon: IconTune, label: 'Werte', section: 'ueberblick',
find: 'xp cooldown wartezeit intervall prüfung timeout obergrenze zahlen uhrzeit' },
{ id: 'brand', Icon: IconBrand, label: 'Brand', section: 'auftritt',
find: 'avatar banner farbe logo name footer aussehen embed' },
{ id: 'seiten', Icon: IconPages, label: 'Seiten', section: 'auftritt',
find: 'regeln über uns faq markdown menü impressum' },
{ id: 'feeds', Icon: IconFeeds, label: 'Feeds', section: 'inhalte',
find: 'devlog commit release changelog rückblick threads ping' },
{ id: 'composer', Icon: IconComposer, label: 'Composer', section: 'inhalte',
find: 'nachricht senden embed bauen tags trigger geplant zeitplan' },
{ id: 'community', Icon: IconCommunity, label: 'Community', section: 'community',
find: 'level xp starboard galerie geburtstag playtester alpha keys voting wünsche autorolle' },
{ id: 'rollen', Icon: IconRoles, label: 'Rollen', section: 'community',
find: 'rollen menü selbstbedienung buttons reaction roles' },
{ id: 'support', Icon: IconSupport, label: 'Support', section: 'community',
find: 'ticket modmail willkommen protokoll modlog moderation warnung' },
{ id: 'playtester', Icon: IconGame, label: 'Playtester', section: 'community',
find: 'tester alpha keys schlüssel vergeben pool testen einladen' },
{ id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen', section: 'community',
find: 'formular fragen bewerbung prüfen annehmen ablehnen' },
{ id: 'server', Icon: IconServer, label: 'Server', section: 'technik',
find: 'game server monitor fivem minecraft status spieler gamedig' },
{ id: 'system', Icon: IconSystem, label: 'System', section: 'technik',
find: 'backup gitea watchdog twitch youtube events sprachkanäle impressum datenschutz' },
{ id: 'api', Icon: IconApi, label: 'API', section: 'zugang',
find: 'schlüssel key token zugriff sso webhook' },
{ id: 'team', Icon: IconTeam, label: 'Team', section: 'zugang',
find: 'mitarbeiter rechte bereiche protokoll audit zugriff' },
];
// Stand eines Wunsches. Reihenfolge = Weg durchs Verfahren, damit die Knöpfe
// in der Ansicht so nebeneinander liegen, wie man sie durchläuft.
const WUNSCH_STATUS = [
['offen', 'Wird geprüft'],
['geplant', 'Geplant'],
['in_arbeit', 'In Arbeit'],
['umgesetzt', 'Umgesetzt'],
['abgelehnt', 'Nicht geplant'],
];
// Tabs, die nur aus Formular-Blöcken bestehen — die dürfen zweispaltig laufen.
// Alles mit Editor, Vorschau oder Liste bleibt einspaltig.
const DENSE_TABS = new Set(['feeds', 'community', 'support', 'system']);
// Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner)
const TAB_SCOPES = {
status: 'any',
module: 'settings',
texte: 'settings',
werte: 'settings',
brand: null,
feeds: 'settings',
community: 'community',
rollen: 'rollen',
support: 'settings',
playtester: 'community',
bewerbungen: 'bewerbungen',
composer: 'content',
seiten: 'content',
server: 'server',
system: null,
api: null,
team: null,
};
/** „vor 3 Minuten" statt rohem Zeitstempel */
/**
* Zeitstempel aus der Datenbank lesen — es kommen zwei Formate an:
*
* "2026-07-30 12:34:56" SQLite datetime('now'), UTC ohne Kennzeichnung
* "2026-07-30T12:34:56.789Z" toISOString(), Zone schon dabei
*
* Beim ersten muss das Z ergänzt werden, sonst rechnet der Browser die Zeit
* als Ortszeit. Beim zweiten darf es das gerade nicht — "…ZZ" ergibt ein
* ungültiges Datum und in der Anzeige stand dann NaN.
*/
function alsDatum(wert) {
if (!wert) return null;
const s = String(wert).trim();
const hatZone = /[Zz]$|[+-]\d{2}:?\d{2}$/.test(s);
const d = new Date(hatZone ? s : `${s.replace(' ', 'T')}Z`);
return Number.isNaN(d.getTime()) ? null : d;
}
function vorWieLange(wert) {
const d = alsDatum(wert);
if (!d) return '—';
const min = Math.floor((Date.now() - d.getTime()) / 60000);
if (min < 1) return 'gerade eben';
if (min < 60) return `vor ${min} Min`;
const std = Math.floor(min / 60);
if (std < 24) return `vor ${std} Std`;
const tage = Math.floor(std / 24);
return tage === 1 ? 'gestern' : `vor ${tage} Tagen`;
}
/** Avatar mit Anfangsbuchstaben als Rückfallebene.
* Bewusst außerhalb der Komponente: als Inline-Komponente wäre der Typ bei
* jedem Rendern neu und React würde die Bilder jedes Mal neu einhängen. */
const Avatar = ({ src, name, klein }) => (src
?
: {(name ?? '?').slice(0, 1).toUpperCase()});
const TEAM_SCOPE_LABELS = {
content: 'Composer, Tags, Triggers & geplante Posts',
community: 'Playtester & Alpha-Keys',
rollen: 'Rollen-Menüs',
bewerbungen: 'Bewerbungen',
server: 'Game-Server',
settings: 'Kanäle & Feed-Einstellungen',
devlogs: 'Devlogs löschen',
};
/** Sekunden → "2d 4h 13m" */
// Was ohne Suche oben steht. Nur eine Startauswahl — gesucht wird über alle
// Spiele, die gamedig kennt. Die IDs hier sind echte gamedig-IDs; wer eine
// ergänzt, prüft sie gegen /api/gamedig-games und nicht gegen sein Gedächtnis.
const BELIEBTE_SPIELE = [
'fivem', 'http', 'minecraft', 'mbe', 'rust', 'valheim', 'palworld',
'farmingsimulator25', 'projectzomboid', 'sdtd', 'ase', 'garrysmod',
'counterstrike2', 'arma3', 'satisfactory', 'unturned',
];
const GAMESEL_MAX = 60;
/**
* Ganze Abfrage-URL eingefügt statt Host getippt? Dann Host, Port und Code
* daraus ziehen.
*
* Bei Farming Simulator ist genau das der Normalfall: der Hoster gibt einem
* eine fertige URL wie
* `http://1.2.3.4:8200/feed/dedicated-server-stats.xml?code=…`,
* und die drei Felder von Hand auseinanderzupflücken ist Arbeit, die niemand
* machen muss. Der Port steht mit drin — er ist nicht immer 8080.
*
* @returns {{host: string, port: string, token?: string} | null}
*/
function feedUrlZerlegen(text) {
const roh = String(text).trim();
if (!/^https?:\/\//i.test(roh)) return null;
let u;
try { u = new URL(roh); } catch { return null; }
if (!u.hostname) return null;
const code = u.searchParams.get('code') || u.searchParams.get('token') || '';
return {
host: u.hostname,
port: u.port || '',
// Ohne Code im Link den vorhandenen stehen lassen, statt ihn zu leeren
...(code ? { token: code } : {}),
};
}
/** Combobox für die Spiel-Auswahl — datalist ist nicht stylebar, das hier schon.
* Die Liste kommt aus gamedig selbst, damit sie mit jedem Update mitwächst;
* freie IDs bleiben trotzdem erlaubt. */
function GameSelect({ games, value, onChange }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef(null);
useEffect(() => {
function onDocClick(e) {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
}
document.addEventListener('mousedown', onDocClick);
return () => document.removeEventListener('mousedown', onDocClick);
}, []);
const q = value.trim().toLowerCase();
// Ohne Eingabe die Startauswahl, sonst alles durchsuchen. 360 Einträge auf
// einmal wären keine Auswahl mehr, sondern eine Wand.
const treffer = q
? games.filter((g) => g.id.includes(q) || g.name.toLowerCase().includes(q))
: BELIEBTE_SPIELE.map((id) => games.find((g) => g.id === id)).filter(Boolean);
const gezeigt = treffer.slice(0, GAMESEL_MAX);
const rest = treffer.length - gezeigt.length;
return (