Server-Embed aufgeraeumt: volle Reihen statt halber
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Discord setzt drei Inline-Felder in eine Reihe. Es waren fuenf — Status,
Spieler, Map, Version, Mods —, also drei plus zwei, und die zweite Reihe stand
halb leer. Bei einem Server, der weniger meldet, stand ein einzelnes Feld
allein herum.

Neu sortiert nach dem, was man wissen will:

Der Zustand ist die Ueberschrift, kein Datenpunkt. Er steht jetzt in der
Beschreibung, eine Zeile mit Trennpunkten: online, Schloss, Ping. Das spart
eine ganze Feldreihe und liest sich als Satz. Der Ping ist damit aus der
Fusszeile raus, wo er zwischen Marke und Intervall stand.

Die Auslastung ist die Zahl, wegen der man hinschaut — sie bekommt die volle
Breite statt eines Drittels, und der Balken wurde von zehn auf sechzehn
Zeichen breiter.

Map, Version und Mods bilden genau eine Reihe. Fehlt eins davon, fuellen
unsichtbare Felder auf, damit die Reihe nicht schief steht.

Adresse und alles, was Discord nicht klickbar macht — fivem://, steam://,
ts3server:// —, standen als je eigener Codeblock untereinander. Das war eine
Wand aus Kaesten. Jetzt ein Block "Zum Kopieren" mit allem drin.

Nebenbei: fuer den Namen im Spiel hatte ich Discords Kleintext-Auszeichnung
genommen. Die rendert in Embeds nicht verlaesslich, und dann staende das
Steuerzeichen woertlich da. Kursiv tut es auch.

Geprueft: drei Zusammenstellungen im Klartext durchgespielt (LS mit allem,
FiveM mit wenig, offline), und die Auffuellung mit einem, zwei und drei
Feldern sowie ohne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 14:25:58 +02:00
co-authored by Claude Opus 5
parent ea523b76b3
commit 0875dff127
+60 -36
View File
@@ -217,58 +217,82 @@ export function buildServerNachricht(r) {
}
/** GSM-Style-Embed für ein Query-Ergebnis */
// Discord setzt drei Inline-Felder in eine Reihe. Bleiben eins oder zwei
// übrig, steht die letzte Reihe halb leer und alles wirkt schief. Ein Feld
// aus unsichtbaren Zeichen füllt die Lücke — das ist der übliche Kniff, und
// billiger als eine Layout-Entscheidung pro Kombination.
const LEER = '';
function dreierReihe(felder) {
const drin = felder.filter(Boolean);
if (drin.length === 0) return [];
while (drin.length % 3 !== 0) drin.push({ name: LEER, value: LEER, inline: true });
return drin;
}
/** Auslastung als Balken — breiter als die alten zehn Zeichen */
function spielerBalken(players, max, breite = 16) {
const anteil = Math.max(0, Math.min(1, players / max));
const voll = Math.round(anteil * breite);
return `\`${'█'.repeat(voll)}${'░'.repeat(breite - voll)}\` **${players}**/${max} · ${Math.round(anteil * 100)} %`;
}
export function buildServerEmbed(r) {
const icon = GAME_ICONS[r.type] ?? '🎮';
const zugang = zugangStand(r, r);
const embed = new EmbedBuilder()
.setColor(r.online ? GREEN : RED)
.setTitle(`${icon} ${r.name}${zugang.zu ? ' 🔒' : ''}`)
.addFields({
name: 'Status',
value: `${r.online ? '🟢 **Online**' : '🔴 **Offline**'}${zugang.grund ? `\n🔒 ${zugang.grund}` : ''}`,
inline: true,
})
.setTitle(`${icon} ${r.name}${zugang.zu ? ' 🔒' : ''}`)
// Intervall aus den Werten lesen, nicht festschreiben — sonst behauptet
// das Embed weiter „alle 2 min", nachdem im Panel etwas anderes steht.
.setFooter({
text: `${brandFooter('STATUS')}${r.ping != null ? `${r.ping}ms` : ''}`
+ ` • alle ${tuning('monitor_interval')} min`,
})
.setFooter({ text: `${brandFooter('STATUS')} • alle ${tuning('monitor_interval')} min` })
.setTimestamp();
if (r.image_url) embed.setThumbnail(r.image_url);
// Wie der Server sich im Spiel nennt. Nur wenn es etwas anderes ist als
// der Name im Panel — sonst stünde dieselbe Zeile zweimal da.
if (r.spielname && r.spielname.trim() && r.spielname.trim() !== r.name?.trim()) {
embed.setDescription(`*${r.spielname.trim().slice(0, 200)}*`);
// Kopf: Name im Spiel und der Zustand in einer Zeile. Als Beschreibung
// statt als Feld — der Status ist die Überschrift und kein Datenpunkt,
// und ein eigenes Feld dafür verbrauchte eine ganze Reihe.
const kopf = [];
if (r.spielname?.trim() && r.spielname.trim() !== r.name?.trim()) {
// Kursiv statt Discords Kleintext-Auszeichnung: die rendert in Embeds
// nicht verlässlich, und dann stünde das Steuerzeichen wörtlich da.
kopf.push(`*${r.spielname.trim().slice(0, 200)}*`);
}
kopf.push([
r.online ? '🟢 **Online**' : '🔴 **Offline**',
zugang.grund ? `🔒 ${zugang.grund}` : null,
r.online && r.ping != null ? `${r.ping} ms` : null,
].filter(Boolean).join(' · '));
embed.setDescription(kopf.join('\n'));
// Die Zahl, wegen der man hinschaut — über die volle Breite
if (r.online && r.players != null && r.max) {
embed.addFields({ name: 'Auslastung', value: spielerBalken(r.players, r.max) });
}
if (r.online && r.players != null && r.max) {
const pct = Math.min(1, r.players / r.max);
const bar = '█'.repeat(Math.round(pct * 10)).padEnd(10, '░');
embed.addFields({
name: 'Spieler',
value: `\`${bar}\` ${r.players}/${r.max} (${Math.round(pct * 100)}%)`,
inline: true,
});
}
if (r.map) embed.addFields({ name: 'Map', value: r.map, inline: true });
if (r.version) embed.addFields({ name: 'Version', value: String(r.version).slice(0, 60), inline: true });
if (r.mods != null) embed.addFields({ name: 'Mods', value: String(r.mods), inline: true });
// Connect und Mods stehen als Knopf darunter, wenn es https ist. Alles
// andere (fivem://, steam://) macht Discord nicht klickbar — dann lieber
// als Codeblock zum Kopieren, statt einen toten Link vorzugaukeln.
for (const l of serverLinks(r)) {
if (!istWebLink(l.url)) {
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\`\`\`` });
// Genau eine volle Reihe, notfalls aufgefüllt
embed.addFields(...dreierReihe([
r.map ? { name: 'Map', value: String(r.map).slice(0, 60), inline: true } : null,
r.version ? { name: 'Version', value: String(r.version).slice(0, 60), inline: true } : null,
r.mods != null ? { name: 'Mods', value: String(r.mods), inline: true } : null,
]));
if (r.online && r.playerNames.length > 0) {
embed.addFields({
name: `Spieler-Liste (${r.playerNames.length})`,
name: `Wer drauf ist (${r.playerNames.length})`,
value: r.playerNames.map((n) => `\`${n}\``).join(' ').slice(0, 1000),
});
}
// Zum Kopieren: die Adresse und alles, was Discord nicht klickbar macht
// (fivem://, steam://, ts3server://). Ein Codeblock je Eintrag wäre eine
// Wand — deshalb alle zusammen in einen.
const zumKopieren = [
r.address ? `${r.address}` : null,
...serverLinks(r).filter((l) => !istWebLink(l.url)).map((l) => `${String(l.label).trim()}: ${l.url}`),
].filter(Boolean);
if (zumKopieren.length > 0) {
embed.addFields({ name: 'Zum Kopieren', value: `\`\`\`\n${zumKopieren.join('\n').slice(0, 1000)}\n\`\`\`` });
}
return embed;
}