Freie Links je Server statt fester Felder
connect_url und mods_url waren zwei Spalten fuer zwei Zwecke — und beim naechsten Zweck waere eine dritte dazugekommen. Jetzt gibt es eine Liste: Beschriftung plus Adresse, so viele wie noetig. Regeln, TeamSpeak, Shop, Bewerbung. Als JSON-Spalte, nicht als eigene Tabelle: es haengt an genau einem Server, wird immer komplett gelesen und immer komplett geschrieben. Gelesen wird es als Liste, geschrieben darf es als Liste hereinkommen — better-sqlite3 kann kein Array binden und wuerde sonst werfen. Kaputtes JSON ergibt eine leere Liste statt eines Absturzes im Monitor. Discord nimmt fuenf Knoepfe je Reihe; ab jetzt werden sie umgebrochen, bei zwei Reihen ist Schluss. Die Grenze steht im Code und nicht im Fehler beim Senden, den niemand sieht. Zu lange Beschriftungen werden auf 80 Zeichen gekuerzt, halbe Zeilen fliegen raus statt als leerer Knopf zu erscheinen. Was nicht http(s) ist — ts3server://, steam:// —, wird wie bisher ein Codeblock im Embed zum Kopieren. Dabei umbenannt: das Embed-Feld fuer einen nicht klickbaren Connect-Link hiess "Connect", der Knopf daneben "Verbinden". Jetzt heisst beides gleich. Geprueft: Umbruch bei zwoelf Links, Reihenfolge mit Connect und Mods vorn, halbe Zeilen, ueberlange Beschriftung, nicht klickbare Schemata, fehlendes und unbrauchbares links-Feld, dazu Speichern, Aendern und kaputtes JSON in der Datenbank. Panel und /server-Seite im laufenden Frontend angesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
|
|||||||
|
|
||||||
| Funktion | Was sie macht |
|
| Funktion | Was sie macht |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 🎮 **Game-Server-Monitor** | DiscordGSM-Stil: pro Server ein Live-Embed (🟢/🔴, Spieler-Balken, Map, Version, Mod-Anzahl, Spieler-Liste, Knöpfe für Verbinden und Mod-Download, Ping); **alle Spiele, die gamedig kennt** — die Auswahl im Panel kommt aus der Bibliothek selbst, inklusive Standard-Port und Hinweis auf nötige Zugangs-Codes (Farming Simulator, Terraria) — plus FiveM + HTTP-Check; Down/Up-Alerts mit Ausfalldauer |
|
| 🎮 **Game-Server-Monitor** | DiscordGSM-Stil: pro Server ein Live-Embed (🟢/🔴, Spieler-Balken, Map, Version, Mod-Anzahl, Spieler-Liste, Knöpfe für Verbinden, Mod-Download und frei eintragbare Links wie Regeln oder TeamSpeak, Ping); **alle Spiele, die gamedig kennt** — die Auswahl im Panel kommt aus der Bibliothek selbst, inklusive Standard-Port und Hinweis auf nötige Zugangs-Codes (Farming Simulator, Terraria) — plus FiveM + HTTP-Check; Down/Up-Alerts mit Ausfalldauer |
|
||||||
| 🚜 **LS — Höfe & Preise** | Für Farming-Simulator-Server: Besitzkarte auf dem Luftbild des Servers (Parzellen nach Hof eingefärbt, Fläche = Hektar, Fahrzeuge und Spieler live), Hof-Übersicht mit Hektar und Wert, Meldung bei Landkauf, dazu die Preiskurven übers Jahr mit „jetzt verkaufen oder warten". **Nicht möglich:** welche Frucht auf welchem Feld steht — das gibt der Feed nicht her (siehe [docs/ls-feed.md](docs/ls-feed.md)) |
|
| 🚜 **LS — Höfe & Preise** | Für Farming-Simulator-Server: Besitzkarte auf dem Luftbild des Servers (Parzellen nach Hof eingefärbt, Fläche = Hektar, Fahrzeuge und Spieler live), Hof-Übersicht mit Hektar und Wert, Meldung bei Landkauf, dazu die Preiskurven übers Jahr mit „jetzt verkaufen oder warten". **Nicht möglich:** welche Frucht auf welchem Feld steht — das gibt der Feed nicht her (siehe [docs/ls-feed.md](docs/ls-feed.md)) |
|
||||||
|
|
||||||
### Server & Technik
|
### Server & Technik
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ function ServerCard({ s }) {
|
|||||||
|
|
||||||
<UptimeTrack buckets={s.uptime} />
|
<UptimeTrack buckets={s.uptime} />
|
||||||
|
|
||||||
{(s.connect_url || s.mods_url || s.address) && (
|
{(s.connect_url || s.mods_url || s.links?.length > 0 || s.address) && (
|
||||||
<div className="srv-connect">
|
<div className="srv-connect">
|
||||||
{s.connect_url && (
|
{s.connect_url && (
|
||||||
<a className="btn btn-save with-icon" href={s.connect_url}>
|
<a className="btn btn-save with-icon" href={s.connect_url}>
|
||||||
@@ -192,6 +192,13 @@ function ServerCard({ s }) {
|
|||||||
<IconDownload size={15} /> {t('server.modsDownload')}
|
<IconDownload size={15} /> {t('server.modsDownload')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
{/* Frei eingetragene Links. `noreferrer` weil sie überall
|
||||||
|
hinführen können — das ist ja der Sinn. */}
|
||||||
|
{(s.links ?? []).map((l) => (
|
||||||
|
<a className="btn btn-ghost" key={`${l.label}${l.url}`} href={l.url} rel="noreferrer">
|
||||||
|
{l.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
{s.address && (
|
{s.address && (
|
||||||
<button className="alpha-key srv-addr" onClick={copyAddress} title={t('server.copyAddress')}>
|
<button className="alpha-key srv-addr" onClick={copyAddress} title={t('server.copyAddress')}>
|
||||||
{copied ? <span className="with-icon"><IconCheck size={14} /> {t('server.copied')}</span> : s.address}
|
{copied ? <span className="with-icon"><IconCheck size={14} /> {t('server.copied')}</span> : s.address}
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export default function Settings({ me }) {
|
|||||||
const [triggers, setTriggers] = useState([]);
|
const [triggers, setTriggers] = useState([]);
|
||||||
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
||||||
// Game-Server — die Spielliste kommt aus gamedig (siehe /api/gamedig-games)
|
// 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: '', mods_url: '' };
|
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '', token: '', mods_url: '', links: [] };
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [games, setGames] = useState([]);
|
const [games, setGames] = useState([]);
|
||||||
const [serverDraft, setServerDraft] = useState(emptyServer);
|
const [serverDraft, setServerDraft] = useState(emptyServer);
|
||||||
@@ -3354,6 +3354,7 @@ export default function Settings({ me }) {
|
|||||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '',
|
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 ?? '',
|
address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '',
|
||||||
image_url: s.image_url ?? '', token: s.token ?? '', mods_url: s.mods_url ?? '',
|
image_url: s.image_url ?? '', token: s.token ?? '', mods_url: s.mods_url ?? '',
|
||||||
|
links: s.links ?? [],
|
||||||
})}>Bearbeiten</button>
|
})}>Bearbeiten</button>
|
||||||
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3423,6 +3424,47 @@ export default function Settings({ me }) {
|
|||||||
<input type="text" placeholder="http://host:8200/all_mods_download?onlyActive=true" value={serverDraft.mods_url} onChange={(e) => setServerDraft({ ...serverDraft, mods_url: e.target.value })} />
|
<input type="text" placeholder="http://host:8200/all_mods_download?onlyActive=true" value={serverDraft.mods_url} onChange={(e) => setServerDraft({ ...serverDraft, mods_url: e.target.value })} />
|
||||||
<label style={{ marginTop: '.6rem' }}>Logo-URL <span className="field-hint">https-Bild (z. B. vom CDN) — erscheint auf der /server-Seite und als Embed-Thumbnail</span></label>
|
<label style={{ marginTop: '.6rem' }}>Logo-URL <span className="field-hint">https-Bild (z. B. vom CDN) — erscheint auf der /server-Seite und als Embed-Thumbnail</span></label>
|
||||||
<input type="text" placeholder="https://cdn.d4rkst3r.de/logos/eco.png" value={serverDraft.image_url} onChange={(e) => setServerDraft({ ...serverDraft, image_url: e.target.value })} />
|
<input type="text" placeholder="https://cdn.d4rkst3r.de/logos/eco.png" value={serverDraft.image_url} onChange={(e) => setServerDraft({ ...serverDraft, image_url: e.target.value })} />
|
||||||
|
|
||||||
|
<label style={{ marginTop: '.9rem' }}>
|
||||||
|
Weitere Links <span className="field-hint">
|
||||||
|
Regeln, TeamSpeak, Shop … je ein Knopf unter dem Embed und auf der /server-Seite.
|
||||||
|
Mit „Verbinden" und „Mods" zusammen höchstens zehn — mehr nimmt Discord nicht.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{serverDraft.links.map((l, i) => (
|
||||||
|
<div className="field-row" key={i} style={{ marginBottom: '.4rem' }}>
|
||||||
|
<input
|
||||||
|
type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Beschriftung, z. B. 📜 Regeln"
|
||||||
|
value={l.label}
|
||||||
|
onChange={(e) => setServerDraft({
|
||||||
|
...serverDraft,
|
||||||
|
links: serverDraft.links.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text" placeholder="https://…"
|
||||||
|
value={l.url}
|
||||||
|
onChange={(e) => setServerDraft({
|
||||||
|
...serverDraft,
|
||||||
|
links: serverDraft.links.map((x, j) => (j === i ? { ...x, url: e.target.value } : x)),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="card-delete" title="Link entfernen"
|
||||||
|
onClick={() => setServerDraft({
|
||||||
|
...serverDraft,
|
||||||
|
links: serverDraft.links.filter((_, j) => j !== i),
|
||||||
|
})}
|
||||||
|
>✕</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{serverDraft.links.length < 8 && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-mini"
|
||||||
|
onClick={() => setServerDraft({ ...serverDraft, links: [...serverDraft.links, { label: '', url: '' }] })}
|
||||||
|
>+ Link</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
||||||
<button className="btn btn-save" onClick={saveServer}>{serverDraft.id ? 'Aktualisieren' : 'Hinzufügen'}</button>
|
<button className="btn btn-save" onClick={saveServer}>{serverDraft.id ? 'Aktualisieren' : 'Hinzufügen'}</button>
|
||||||
{serverDraft.id && <button className="btn btn-ghost" onClick={() => setServerDraft(emptyServer)}>Abbrechen</button>}
|
{serverDraft.id && <button className="btn btn-ghost" onClick={() => setServerDraft(emptyServer)}>Abbrechen</button>}
|
||||||
|
|||||||
+31
-12
@@ -142,17 +142,36 @@ export function istWebLink(url) {
|
|||||||
* eckigen Klammern wörtlich da. Ein Knopf lässt diese Frage gar nicht erst
|
* eckigen Klammern wörtlich da. Ein Knopf lässt diese Frage gar nicht erst
|
||||||
* aufkommen und ist obendrein als Knopf erkennbar.
|
* aufkommen und ist obendrein als Knopf erkennbar.
|
||||||
*/
|
*/
|
||||||
|
// Discord: fünf Knöpfe je Reihe. Mehr als zwei Reihen erschlägt das Embed,
|
||||||
|
// also ist bei zehn Schluss — und zwar sichtbar hier, nicht als Fehler beim
|
||||||
|
// Senden, den niemand sieht.
|
||||||
|
const PRO_REIHE = 5;
|
||||||
|
const MAX_REIHEN = 2;
|
||||||
|
|
||||||
|
/** Alle Links eines Servers in einer Liste: die festen zuerst, dann die freien */
|
||||||
|
export function serverLinks(r) {
|
||||||
|
return [
|
||||||
|
{ label: '🎮 Verbinden', url: r.connect_url },
|
||||||
|
{ label: '📦 Mods laden', url: r.mods_url },
|
||||||
|
...(Array.isArray(r.links) ? r.links : []),
|
||||||
|
].filter((l) => l?.url && String(l.label ?? '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
export function serverKnoepfe(r) {
|
export function serverKnoepfe(r) {
|
||||||
const knoepfe = [
|
const knoepfe = serverLinks(r)
|
||||||
['🎮 Verbinden', r.connect_url],
|
.filter((l) => istWebLink(l.url))
|
||||||
['📦 Mods laden', r.mods_url],
|
.slice(0, PRO_REIHE * MAX_REIHEN)
|
||||||
]
|
.map((l) => new ButtonBuilder()
|
||||||
.filter(([, url]) => istWebLink(url))
|
|
||||||
.map(([label, url]) => new ButtonBuilder()
|
|
||||||
.setStyle(ButtonStyle.Link)
|
.setStyle(ButtonStyle.Link)
|
||||||
.setLabel(label)
|
// Discord nimmt höchstens 80 Zeichen und weist längere ab
|
||||||
.setURL(url.trim()));
|
.setLabel(String(l.label).trim().slice(0, 80))
|
||||||
return knoepfe.length > 0 ? [new ActionRowBuilder().addComponents(knoepfe)] : [];
|
.setURL(l.url.trim()));
|
||||||
|
|
||||||
|
const reihen = [];
|
||||||
|
for (let i = 0; i < knoepfe.length; i += PRO_REIHE) {
|
||||||
|
reihen.push(new ActionRowBuilder().addComponents(knoepfe.slice(i, i + PRO_REIHE)));
|
||||||
|
}
|
||||||
|
return reihen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Embed + Knöpfe — das, was der Monitor in den Kanal schreibt */
|
/** Embed + Knöpfe — das, was der Monitor in den Kanal schreibt */
|
||||||
@@ -191,9 +210,9 @@ export function buildServerEmbed(r) {
|
|||||||
// Connect und Mods stehen als Knopf darunter, wenn es https ist. Alles
|
// Connect und Mods stehen als Knopf darunter, wenn es https ist. Alles
|
||||||
// andere (fivem://, steam://) macht Discord nicht klickbar — dann lieber
|
// andere (fivem://, steam://) macht Discord nicht klickbar — dann lieber
|
||||||
// als Codeblock zum Kopieren, statt einen toten Link vorzugaukeln.
|
// als Codeblock zum Kopieren, statt einen toten Link vorzugaukeln.
|
||||||
for (const [titel, url] of [['Connect', r.connect_url], ['Mods', r.mods_url]]) {
|
for (const l of serverLinks(r)) {
|
||||||
if (url && !istWebLink(url)) {
|
if (!istWebLink(l.url)) {
|
||||||
embed.addFields({ name: titel, value: `\`\`\`\n${url}\n\`\`\`` });
|
embed.addFields({ name: String(l.label).trim().slice(0, 256), value: `\`\`\`\n${l.url}\n\`\`\`` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` });
|
if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` });
|
||||||
|
|||||||
@@ -927,6 +927,10 @@ db.exec(`
|
|||||||
// Eigener Link für den Mod-Download. Vorher landete der beim Connect-Link,
|
// Eigener Link für den Mod-Download. Vorher landete der beim Connect-Link,
|
||||||
// weil es kein anderes Feld gab — und dann führte "Connect" zu einer ZIP.
|
// weil es kein anderes Feld gab — und dann führte "Connect" zu einer ZIP.
|
||||||
if (!cols.includes('mods_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN mods_url TEXT NOT NULL DEFAULT ''`);
|
if (!cols.includes('mods_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN mods_url TEXT NOT NULL DEFAULT ''`);
|
||||||
|
// Freie Links: Regeln, TeamSpeak, Shop, was auch immer. Als JSON-Liste,
|
||||||
|
// weil sonst für jeden neuen Zweck eine Spalte dazukäme — und genau so
|
||||||
|
// sind connect_url und mods_url entstanden.
|
||||||
|
if (!cols.includes('links')) db.exec(`ALTER TABLE gameservers ADD COLUMN links TEXT NOT NULL DEFAULT '[]'`);
|
||||||
}
|
}
|
||||||
// Die Auswahl im Panel bot mal Spiel-IDs an, die es bei gamedig gar nicht gibt.
|
// Die Auswahl im Panel bot mal Spiel-IDs an, die es bei gamedig gar nicht gibt.
|
||||||
// Wer eine davon gewählt hat, hatte einen Server, der dauerhaft offline stand,
|
// Wer eine davon gewählt hat, hatte einen Server, der dauerhaft offline stand,
|
||||||
@@ -940,13 +944,13 @@ db.exec(`
|
|||||||
for (const [alt, neu] of Object.entries(echteIds)) um.run(neu, alt);
|
for (const [alt, neu] of Object.entries(echteIds)) um.run(neu, alt);
|
||||||
}
|
}
|
||||||
const insertGameserver = db.prepare(`
|
const insertGameserver = db.prepare(`
|
||||||
INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url, token, mods_url)
|
INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url, token, mods_url, links)
|
||||||
VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url, @token, @mods_url)
|
VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url, @token, @mods_url, @links)
|
||||||
`);
|
`);
|
||||||
const updateGameserverStmt = db.prepare(`
|
const updateGameserverStmt = db.prepare(`
|
||||||
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address,
|
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address,
|
||||||
host = @host, port = @port, connect_url = @connect_url, image_url = @image_url, token = @token,
|
host = @host, port = @port, connect_url = @connect_url, image_url = @image_url, token = @token,
|
||||||
mods_url = @mods_url
|
mods_url = @mods_url, links = @links
|
||||||
WHERE id = @id
|
WHERE id = @id
|
||||||
`);
|
`);
|
||||||
const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?');
|
const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?');
|
||||||
@@ -958,14 +962,36 @@ const deleteGameserverStmt = db.prepare('DELETE FROM gameservers WHERE id = ?');
|
|||||||
// auffüllen ist billiger, als das an jeder Aufrufstelle nachzuziehen.
|
// auffüllen ist billiger, als das an jeder Aufrufstelle nachzuziehen.
|
||||||
const gameserverFelder = {
|
const gameserverFelder = {
|
||||||
name: '', type: 'fivem', query_url: '', address: '', host: '', port: null,
|
name: '', type: 'fivem', query_url: '', address: '', host: '', port: null,
|
||||||
connect_url: '', image_url: '', token: '', mods_url: '',
|
connect_url: '', image_url: '', token: '', mods_url: '', links: '[]',
|
||||||
|
};
|
||||||
|
// Links dürfen als Liste hereinkommen — so gibt getGameserver sie ja auch
|
||||||
|
// heraus. better-sqlite3 kann kein Array binden und würde sonst werfen.
|
||||||
|
const vollstaendig = (s) => {
|
||||||
|
const voll = { ...gameserverFelder, ...s };
|
||||||
|
if (Array.isArray(voll.links)) voll.links = JSON.stringify(voll.links);
|
||||||
|
return voll;
|
||||||
};
|
};
|
||||||
const vollstaendig = (s) => ({ ...gameserverFelder, ...s });
|
|
||||||
export const createGameserver = (s) => insertGameserver.run(vollstaendig(s)).lastInsertRowid;
|
export const createGameserver = (s) => insertGameserver.run(vollstaendig(s)).lastInsertRowid;
|
||||||
export const updateGameserver = (s) => updateGameserverStmt.run(vollstaendig(s)).changes > 0;
|
export const updateGameserver = (s) => updateGameserverStmt.run(vollstaendig(s)).changes > 0;
|
||||||
export const setGameserverMessage = (id, messageId) => setGameserverMessageStmt.run(messageId, id);
|
export const setGameserverMessage = (id, messageId) => setGameserverMessageStmt.run(messageId, id);
|
||||||
export const listGameservers = () => listGameserversStmt.all();
|
/**
|
||||||
export const getGameserver = (id) => getGameserverStmt.get(id) ?? null;
|
* Links kommen als JSON aus der Spalte und gehen als Liste weiter.
|
||||||
|
*
|
||||||
|
* Einmal hier statt an jeder Aufrufstelle: sonst parst jeder selbst, und der
|
||||||
|
* erste, der es vergisst, mappt über einen String. Kaputtes JSON ergibt eine
|
||||||
|
* leere Liste — ein Server ohne Links ist besser als ein Absturz im Monitor.
|
||||||
|
*/
|
||||||
|
function mitLinks(row) {
|
||||||
|
if (!row) return null;
|
||||||
|
let links = [];
|
||||||
|
try {
|
||||||
|
const roh = JSON.parse(row.links || '[]');
|
||||||
|
if (Array.isArray(roh)) links = roh.filter((l) => l?.url);
|
||||||
|
} catch { /* kaputt gespeichert — dann eben ohne */ }
|
||||||
|
return { ...row, links };
|
||||||
|
}
|
||||||
|
export const listGameservers = () => listGameserversStmt.all().map(mitLinks);
|
||||||
|
export const getGameserver = (id) => mitLinks(getGameserverStmt.get(id));
|
||||||
export const deleteGameserver = (id) => deleteGameserverStmt.run(id).changes > 0;
|
export const deleteGameserver = (id) => deleteGameserverStmt.run(id).changes > 0;
|
||||||
|
|
||||||
// Spielerzahl-Verlauf für die öffentliche /server-Seite (Sample alle 2 min, 7 Tage Retention)
|
// Spielerzahl-Verlauf für die öffentliche /server-Seite (Sample alle 2 min, 7 Tage Retention)
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export function registerApiRoutes(app, client) {
|
|||||||
address: s.address || null,
|
address: s.address || null,
|
||||||
connect_url: s.connect_url || null,
|
connect_url: s.connect_url || null,
|
||||||
mods_url: s.mods_url || null,
|
mods_url: s.mods_url || null,
|
||||||
|
links: s.links ?? [],
|
||||||
online: r?.online ?? null, // null = noch nie gecheckt
|
online: r?.online ?? null, // null = noch nie gecheckt
|
||||||
players: r?.players ?? null,
|
players: r?.players ?? null,
|
||||||
max: r?.max ?? null,
|
max: r?.max ?? null,
|
||||||
@@ -1609,9 +1610,28 @@ ${rssItems}
|
|||||||
mods_url: /^https?:\/\/.+/i.test(String(body.mods_url ?? '').trim())
|
mods_url: /^https?:\/\/.+/i.test(String(body.mods_url ?? '').trim())
|
||||||
? String(body.mods_url).trim().slice(0, 300)
|
? String(body.mods_url).trim().slice(0, 300)
|
||||||
: '',
|
: '',
|
||||||
|
links: freieLinks(body.links),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freie Links säubern.
|
||||||
|
*
|
||||||
|
* Zeilen ohne Beschriftung oder ohne Adresse fliegen raus statt als leerer
|
||||||
|
* Knopf im Discord zu landen. Zehn ist die Grenze, weil Discord fünf
|
||||||
|
* Knöpfe je Reihe nimmt und zwei Reihen unter einem Embed genug sind.
|
||||||
|
*/
|
||||||
|
function freieLinks(roh) {
|
||||||
|
if (!Array.isArray(roh)) return [];
|
||||||
|
return roh
|
||||||
|
.map((l) => ({
|
||||||
|
label: String(l?.label ?? '').trim().slice(0, 80),
|
||||||
|
url: String(l?.url ?? '').trim().slice(0, 400),
|
||||||
|
}))
|
||||||
|
.filter((l) => l.label && l.url)
|
||||||
|
.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
app.get('/api/gameservers', async (request, reply) => {
|
app.get('/api/gameservers', async (request, reply) => {
|
||||||
if (requireScope(request, reply, 'server')) return;
|
if (requireScope(request, reply, 'server')) return;
|
||||||
return { servers: listGameservers() };
|
return { servers: listGameservers() };
|
||||||
|
|||||||
Reference in New Issue
Block a user