Server-Seite ueberarbeitet, und die Spielauswahl kommt jetzt aus gamedig
LS25 fehlte, aber nicht nur LS25: die Auswahl im Panel war eine von Hand gepflegte Liste mit neunzehn Eintraegen, und fuenf davon gab es bei gamedig gar nicht — "arkse", "ark", "sevendaystodie", "minecraftbe" und "terraria". Wer eines davon auswaehlte, bekam einen Server, der dauerhaft offline stand, ohne dass irgendwo ein Grund stand. Die echten IDs heissen ase, sdtd, mbe und terrariatshock. Also nicht LS25 nachtragen, sondern die Liste abschaffen: sie kommt jetzt aus gamedig selbst, 360 Eintraege, sortiert, mit Suche. Jedes Update bringt neue Spiele automatisch mit. Dazu der Standard-Query-Port als Vorschlag, sobald man ein Spiel waehlt. Bestehende Eintraege mit den toten IDs werden beim Start einmalig umgebogen. Farming Simulator hat noch eine zweite Huerde: der Server antwortet nur mit Zugangs-Code, und den hat queryServer nie durchgereicht. LS25 waere also auch mit richtiger ID offline geblieben. Jetzt gibt es eine Spalte dafuer, das Feld erscheint nur bei Spielen, die einen brauchen (Farming Simulator, Terraria — bei Satisfactory optional), und Speichern ohne Code wird abgelehnt statt stillschweigend hingenommen. Der Code steht nicht in der oeffentlichen API. Nebenbei aufgefallen: input[type=password] war im CSS nirgends erfasst. Das erste solche Feld kam voellig ungestylt daher. Die /server-Seite dazu: - Auslastungs-Balken mit eigener Farbe statt des XP-Balkens vom Level-System. Gruen frei, gelb ab 80 Prozent, rot voll — die Zahl steht rechtsbuendig, mittig laege sie bei 50 Prozent genau auf der Fuellkante. - Map, Version und Ping als Chips. Vorher standen sie mit Punkten getrennt in der Statuszeile und wurden auf dem Handy zu Brei. - Wer gerade drauf ist. Die Namen holt der Monitor ohnehin schon fuers Embed, auf der Seite standen sie nur nirgends. - Welches Spiel es ueberhaupt ist. Das stand nirgends ausser im Emoji. - Offline-Karten waren bisher leer bis auf das Wort. Jetzt steht dort, seit wann — soweit der Verlauf reicht, sonst gar nichts statt einer Schaetzung. - Zeitachse und Peak-Linie am Verlauf. Eine Flaeche ohne Bezugspunkt sagt nicht, wie hoch hoch ist. - Plaetze insgesamt in der Uebersicht oben. Geprueft: Token speichern und aendern, Spielnamen inklusive unbekannter ID, Token-Bedarf pro Protokoll, Liste ohne Doppler, alle Icon-Schluessel gegen gamedig, "zuletzt online" ignoriert Offline-Proben, und die Migration mit einer Datenbank im alten Stand. Beide Seiten am laufenden Frontend angesehen, auch auf Handybreite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,9 +161,20 @@ const TEAM_SCOPE_LABELS = {
|
||||
};
|
||||
|
||||
/** 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;
|
||||
|
||||
/** Combobox für die Spiel-Auswahl — datalist ist nicht stylebar, das hier schon.
|
||||
* Filtert die Presets beim Tippen, freie gamedig-IDs bleiben erlaubt. */
|
||||
function GameSelect({ presets, value, onChange }) {
|
||||
* 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);
|
||||
|
||||
@@ -176,39 +187,56 @@ function GameSelect({ presets, value, onChange }) {
|
||||
}, []);
|
||||
|
||||
const q = value.trim().toLowerCase();
|
||||
const filtered = presets.filter(([id, label]) =>
|
||||
!q || id.includes(q) || label.toLowerCase().includes(q)
|
||||
);
|
||||
// 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 (
|
||||
<div className="gamesel" ref={wrapRef} style={{ flex: 'none', width: '13rem' }}>
|
||||
<input
|
||||
type="text" placeholder="Spiel (gamedig-ID)"
|
||||
title="Vorschläge oder beliebige gamedig-Spiel-ID"
|
||||
type="text" placeholder="Spiel suchen …"
|
||||
title="Name oder gamedig-ID — es geht auch jede ID, die nicht in der Liste steht"
|
||||
value={value}
|
||||
onFocus={() => setOpen(true)}
|
||||
onChange={(e) => { onChange(e.target.value); setOpen(true); }}
|
||||
onKeyDown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') setOpen(false); }}
|
||||
/>
|
||||
<span className="gamesel-caret" onMouseDown={(e) => { e.preventDefault(); setOpen((o) => !o); }}>▾</span>
|
||||
{open && filtered.length > 0 && (
|
||||
{open && gezeigt.length > 0 && (
|
||||
<div className="gamesel-list">
|
||||
{filtered.map(([id, label]) => (
|
||||
{gezeigt.map((g) => (
|
||||
<button
|
||||
type="button" key={id}
|
||||
className={`gamesel-item${id === value ? ' active' : ''}`}
|
||||
onMouseDown={(e) => { e.preventDefault(); onChange(id); setOpen(false); }}
|
||||
type="button" key={g.id}
|
||||
className={`gamesel-item${g.id === value ? ' active' : ''}`}
|
||||
onMouseDown={(e) => { e.preventDefault(); onChange(g.id); setOpen(false); }}
|
||||
>
|
||||
<span className="gamesel-label">{label}</span>
|
||||
<span className="gamesel-id">{id}</span>
|
||||
<span className="gamesel-label">
|
||||
{GAME_EMOJI[g.id] ?? (g.eigen ? '🔧' : '🎮')} {g.name}
|
||||
{g.token && <span className="gamesel-key" title="braucht einen Zugangs-Code">🔑</span>}
|
||||
</span>
|
||||
<span className="gamesel-id">{g.id}</span>
|
||||
</button>
|
||||
))}
|
||||
{rest > 0 && <div className="gamesel-rest">… und {rest} weitere — weitertippen</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Nur Deko in der Auswahlliste; die Seite holt sich ihr Icon aus der API.
|
||||
const GAME_EMOJI = {
|
||||
fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️', rust: '🪓', valheim: '🛡️',
|
||||
palworld: '🐑', farmingsimulator19: '🚜', farmingsimulator22: '🚜', farmingsimulator25: '🚜',
|
||||
projectzomboid: '🧟', sdtd: '🧟', dayz: '🧟', ase: '🦖', asa: '🦖',
|
||||
garrysmod: '🔧', counterstrike2: '🔫', csgo: '🔫', arma3: '🪖', squad: '🪖',
|
||||
satisfactory: '🏭', unturned: '🏝️', terrariatshock: '🌳', teamfortress2: '🎩',
|
||||
};
|
||||
|
||||
/**
|
||||
* Durchsuchbare Auswahl für Kanäle und Rollen — dieselbe Bedienung wie bei den
|
||||
* gamedig-IDs. Ein natives <select> lässt sich nicht durchsuchen; bei vielen
|
||||
@@ -340,19 +368,10 @@ export default function Settings({ me }) {
|
||||
// Triggers
|
||||
const [triggers, setTriggers] = useState([]);
|
||||
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
||||
// Game-Server
|
||||
const GAME_PRESETS = [
|
||||
['fivem', '🚗 FiveM'], ['http', '🌐 HTTP-Check'], ['minecraft', '⛏️ Minecraft (Java)'],
|
||||
['minecraftbe', '⛏️ Minecraft (Bedrock)'], ['rust', '🪓 Rust'], ['valheim', '🛡️ Valheim'],
|
||||
['palworld', '🐑 Palworld'], ['arkse', '🦖 ARK: Survival Evolved'], ['dayz', '🧟 DayZ'],
|
||||
['projectzomboid', '🧟 Project Zomboid'], ['sevendaystodie', '🧟 7 Days to Die'],
|
||||
['teamfortress2', '🎩 Team Fortress 2'], ['counterstrike2', '🔫 Counter-Strike 2'],
|
||||
['garrysmod', '🔧 Garry’s Mod'], ['satisfactory', '🏭 Satisfactory'],
|
||||
['terraria', '🌳 Terraria'], ['arma3', '🪖 Arma 3'], ['squad', '🪖 Squad'],
|
||||
['unturned', '🏝️ Unturned'],
|
||||
];
|
||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '' };
|
||||
// Game-Server — die Spielliste kommt aus gamedig (siehe /api/gamedig-games)
|
||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '', token: '' };
|
||||
const [servers, setServers] = useState([]);
|
||||
const [games, setGames] = useState([]);
|
||||
const [serverDraft, setServerDraft] = useState(emptyServer);
|
||||
// Bot-Beschreibung + Team
|
||||
const [botDesc, setBotDesc] = useState('');
|
||||
@@ -489,6 +508,7 @@ export default function Settings({ me }) {
|
||||
apiGet('/api/appforms').then((d) => setAppForms(d.forms)).catch(() => {});
|
||||
apiGet('/api/triggers').then((d) => setTriggers(d.triggers)).catch(() => {});
|
||||
apiGet('/api/gameservers').then((d) => setServers(d.servers)).catch(() => {});
|
||||
apiGet('/api/gamedig-games').then((d) => setGames(d.games ?? [])).catch(() => {});
|
||||
apiGet('/api/webadmins')
|
||||
.then((d) => { setTeam(d.admins); setOwner(d.owner); })
|
||||
.catch(() => {});
|
||||
@@ -811,11 +831,26 @@ export default function Settings({ me }) {
|
||||
}
|
||||
|
||||
const serverNeedsUrl = ['fivem', 'http'].includes(serverDraft.type);
|
||||
const gewaehltesSpiel = games.find((g) => g.id === serverDraft.type) ?? null;
|
||||
/** Spiel gewählt: Standard-Port vorschlagen, solange keiner drinsteht */
|
||||
function spielWaehlen(type) {
|
||||
const spiel = games.find((g) => g.id === type);
|
||||
setServerDraft((d) => ({
|
||||
...d, type,
|
||||
port: d.port || (spiel && !spiel.eigen ? String(spiel.port ?? '') : d.port),
|
||||
}));
|
||||
}
|
||||
async function saveServer() {
|
||||
if (!serverDraft.name.trim() || (serverNeedsUrl ? !serverDraft.query_url.trim() : !serverDraft.host.trim())) {
|
||||
flash(serverNeedsUrl ? '✗ Name und Query-URL nötig' : '✗ Name und Host nötig');
|
||||
return;
|
||||
}
|
||||
// Ohne Code antwortet so ein Server gar nicht — er stünde dauerhaft auf
|
||||
// "offline", ohne dass irgendwo der Grund stünde. Lieber hier stoppen.
|
||||
if (gewaehltesSpiel?.token === 'pflicht' && !serverDraft.token.trim()) {
|
||||
flash(`✗ ${gewaehltesSpiel.name} antwortet nur mit Zugangs-Code`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (serverDraft.id) await apiPut(`/api/gameservers/${serverDraft.id}`, serverDraft);
|
||||
else await apiPost('/api/gameservers', serverDraft);
|
||||
@@ -3265,12 +3300,18 @@ export default function Settings({ me }) {
|
||||
{servers.map((s) => (
|
||||
<div className="key-row" key={s.id}>
|
||||
<span className="key-name">{s.name}</span>
|
||||
<span className="key-scopes">{s.type}</span>
|
||||
<span className="key-used" title={s.query_url}>{s.query_url}</span>
|
||||
<span className="key-scopes">
|
||||
{games.find((g) => g.id === s.type)?.name ?? s.type}
|
||||
{s.token ? ' 🔑' : ''}
|
||||
</span>
|
||||
{/* gamedig-Server haben keine URL, sondern Host + Port */}
|
||||
<span className="key-used" title={s.type}>
|
||||
{s.query_url || [s.host, s.port].filter(Boolean).join(':')}
|
||||
</span>
|
||||
<button className="btn btn-mini" onClick={() => setServerDraft({
|
||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '',
|
||||
address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '',
|
||||
image_url: s.image_url ?? '',
|
||||
image_url: s.image_url ?? '', token: s.token ?? '',
|
||||
})}>Bearbeiten</button>
|
||||
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
||||
</div>
|
||||
@@ -3283,23 +3324,45 @@ export default function Settings({ me }) {
|
||||
<div className="field-row">
|
||||
<input type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Name, z. B. FiveM" value={serverDraft.name} onChange={(e) => setServerDraft({ ...serverDraft, name: e.target.value })} />
|
||||
<GameSelect
|
||||
presets={GAME_PRESETS}
|
||||
games={games}
|
||||
value={serverDraft.type}
|
||||
onChange={(type) => setServerDraft({ ...serverDraft, type })}
|
||||
onChange={spielWaehlen}
|
||||
/>
|
||||
{serverNeedsUrl ? (
|
||||
<input type="text" placeholder={serverDraft.type === 'fivem' ? 'http://ip:30120' : 'https://dienst.example/health'} value={serverDraft.query_url} onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })} />
|
||||
) : (
|
||||
<>
|
||||
<input type="text" placeholder="Host / IP" value={serverDraft.host} onChange={(e) => setServerDraft({ ...serverDraft, host: e.target.value })} />
|
||||
<input type="text" style={{ flex: 'none', width: '6rem' }} placeholder="Port" value={serverDraft.port} onChange={(e) => setServerDraft({ ...serverDraft, port: e.target.value })} />
|
||||
<input type="text" style={{ flex: 'none', width: '6rem' }} placeholder={gewaehltesSpiel?.port ? `Port ${gewaehltesSpiel.port}` : 'Port'} value={serverDraft.port} onChange={(e) => setServerDraft({ ...serverDraft, port: e.target.value })} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="field-hint" style={{ marginTop: '.3rem' }}>
|
||||
Neben FiveM/HTTP funktioniert jede <a href="https://github.com/gamedig/node-gamedig#games-list" target="_blank" rel="noreferrer" style={{ textDecoration: 'underline' }}>gamedig-Spiel-ID</a> (300+ Spiele) — Host + Query-Port eintragen.
|
||||
{gewaehltesSpiel
|
||||
? <>Gewählt: <b>{gewaehltesSpiel.name}</b>{!gewaehltesSpiel.eigen && gewaehltesSpiel.port ? ` — Standard-Query-Port ${gewaehltesSpiel.port}` : ''}. Die Liste kommt direkt aus gamedig, {games.length} Einträge.</>
|
||||
: <>Unbekannte ID — sie wird trotzdem versucht. Alle bekannten stehen in der <a href="https://github.com/gamedig/node-gamedig/blob/master/GAMES_LIST.md" target="_blank" rel="noreferrer" style={{ textDecoration: 'underline' }}>gamedig-Liste</a>.</>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Nur bei Spielen, die ohne Code gar nicht erst antworten */}
|
||||
{gewaehltesSpiel?.token && (
|
||||
<div className="field">
|
||||
<label>
|
||||
Zugangs-Code {gewaehltesSpiel.token === 'optional' && <span className="field-hint">optional</span>}
|
||||
</label>
|
||||
<input
|
||||
type="password" autoComplete="off"
|
||||
placeholder={gewaehltesSpiel.token === 'pflicht' ? 'Pflicht für dieses Spiel' : 'leer lassen, wenn nicht nötig'}
|
||||
value={serverDraft.token}
|
||||
onChange={(e) => setServerDraft({ ...serverDraft, token: e.target.value })}
|
||||
/>
|
||||
<p className="field-hint" style={{ marginTop: '.3rem' }}>
|
||||
{gewaehltesSpiel.id.startsWith('farmingsimulator')
|
||||
? <>Im Spiel heißt er <b>Code</b>. Steht im Webinterface des Servers unter <code>http://host:{gewaehltesSpiel.port ?? 8080}/settings.html</code> — er gilt nur für den eigenen Server.</>
|
||||
: <>Steht im Webinterface des Servers. Ohne ihn liefert die Abfrage nichts und der Server stünde dauerhaft auf „offline".</>}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label>Connect-Link + Anzeige-Adresse <span className="field-hint">Link ist im Embed klickbar (auch steam:// / fivem://); Adresse als Copy-Paste-Block</span></label>
|
||||
<div className="field-row">
|
||||
|
||||
Reference in New Issue
Block a user