From ef0fb066f7a0afc1d55b3c6eb0708648b66b15c5 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 30 Jul 2026 22:25:30 +0200 Subject: [PATCH] Startseite, Server-Seite v2 mit Logos, Lightbox-Fix, Navbar-Ausrichtung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Startseite (/): Hero mit CTAs (Discord-Invite + Devlogs), Live-Zahlen (Member, Server online, Spieler), Bereichs-Kacheln, neuestes Devlog als Teaser — kein Redirect mehr auf /devlogs - Server-Seite v2: Übersichtsleiste (X/Y online, Spieler gesamt), 2-Spalten-Grid, Server-Logo (neues Feld image_url, https-only, auch als Embed-Thumbnail im Discord-Status), große Spielerzahl, Uptime % + Peak aus den 24h-Samples, Adresse als Copy-Button - Lightbox: rendert jetzt per Portal in (Header/Scanlines schienen durchs Overlay), Pfeile stehen neben dem Bild statt drauf, Info-Zeile unterm Bild - Navbar: Community-Dropdown-Button auf Link-Höhe ausgerichtet (line-height + align-items) - Setup Server-Tab: Logo-URL-Feld Co-Authored-By: Claude Fable 5 --- README.md | 3 +- frontend/src/App.jsx | 3 +- frontend/src/components/Lightbox.jsx | 37 +++++---- frontend/src/pages/Home.jsx | 112 +++++++++++++++++++++++++++ frontend/src/pages/Server.jsx | 101 +++++++++++++++++++----- frontend/src/pages/Settings.jsx | 5 +- frontend/src/style.css | 111 ++++++++++++++++++++------ src/bot/server-monitor.js | 1 + src/db.js | 7 +- src/web/api.js | 4 + 10 files changed, 316 insertions(+), 68 deletions(-) create mode 100644 frontend/src/pages/Home.jsx diff --git a/README.md b/README.md index 066cfe3..64e0431 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,8 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | `/changelog` | öffentlich | Alle Releases mit Notes, Tag- und Pre-Release-Chips | | `/feed.xml` | öffentlich | RSS-Feed der Devlogs | | `/profil` | Member | Eigenes Profil: Rang, XP, Rollen (togglebar), Alpha-Key | -| `/server` | öffentlich | Live-Status aller Game-Server + 24h-Spielerzahl-Verlauf | +| `/` | öffentlich | Startseite: Hero, Live-Zahlen (Member/Server/Spieler), Bereichs-Kacheln, neuestes Devlog | +| `/server` | öffentlich | Live-Status aller Game-Server: Logo, Spieler-Balken, 24h-Verlauf, Uptime & Peak, Copy-Adresse | | `/commits` | nur Admin | Archivierte Commits aller Repos (SHA → Gitea-Link) | | `/settings` | nur Admin | **Setup-Seite** — siehe unten | diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7cf76fd..9e17c64 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,7 @@ import Commits from './pages/Commits.jsx'; import Settings from './pages/Settings.jsx'; import Profil from './pages/Profil.jsx'; import Server from './pages/Server.jsx'; +import Home from './pages/Home.jsx'; const COMMUNITY_LINKS = [ ['/galerie', 'Galerie'], @@ -184,7 +185,7 @@ export default function App() { )} - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Lightbox.jsx b/frontend/src/components/Lightbox.jsx index 27210e7..3947112 100644 --- a/frontend/src/components/Lightbox.jsx +++ b/frontend/src/components/Lightbox.jsx @@ -1,5 +1,8 @@ // Bild-Overlay mit Blättern (Pfeiltasten, Buttons, Wischen) statt neuem Tab. +// Rendert per Portal direkt in , damit Header/Scanlines nicht durchscheinen; +// die Pfeile stehen NEBEN dem Bild, die Info-Zeile darunter. import { useCallback, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; export default function Lightbox({ images, start = 0, onClose }) { const [index, setIndex] = useState(start); @@ -25,7 +28,7 @@ export default function Lightbox({ images, start = 0, onClose }) { const img = images[index]; if (!img) return null; - return ( + return createPortal(
- {images.length > 1 && ( - - )} - {img.alt e.stopPropagation()} - /> - {images.length > 1 && ( - - )} -
e.stopPropagation()}> - {img.caption && {img.caption}} - {images.length > 1 && {index + 1} / {images.length}} +
+ {images.length > 1 && ( + + )} +
e.stopPropagation()}> + {img.alt +
+ {img.caption && {img.caption}} + {images.length > 1 && {index + 1} / {images.length}} +
+
+ {images.length > 1 && ( + + )}
-
+
, + document.body ); } diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..3d584af --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,112 @@ +// Startseite: Hero mit CTAs, Live-Zahlen, neuestes Devlog und Bereichs-Kacheln. +// Nutzt nur öffentliche APIs — alles best effort, die Seite steht auch ohne Daten. +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { apiGet } from '../api.js'; +import { Markdown } from '../markdown.jsx'; + +const TILES = [ + ['/devlogs', '📔', 'Devlog', 'Entwicklungs-Updates, frisch aus dem Editor'], + ['/roadmap', '🗺️', 'Roadmap', 'Meilensteine + Community-Wünsche zum Abstimmen'], + ['/server', '🎮', 'Server', 'Live-Status aller Game-Server mit Verlauf'], + ['/galerie', '🖼️', 'Galerie', 'Screenshots aus der Community'], + ['/level', '📈', 'Level', 'XP-Bestenliste — wer redet am meisten mit?'], + ['/events', '📅', 'Events', 'Playtests, Streams und was sonst ansteht'], +]; + +/** Devlog-Text für den Teaser kürzen (erste Sätze, ohne Titel-Zeile) */ +function teaser(content) { + const body = content + .split(/\n{2,}/) + .filter((b) => !/^Devlog\s*[—–-]/i.test(b.trim()) && !/^\*[^*]+\*$/s.test(b.trim())) + .join('\n\n'); + return body.length > 420 ? `${body.slice(0, 420).trimEnd()} …` : body; +} + +export default function Home() { + const [stats, setStats] = useState(null); + const [servers, setServers] = useState(null); + const [latest, setLatest] = useState(null); + const [invite, setInvite] = useState(null); + + useEffect(() => { + apiGet('/api/serverstats').then(setStats).catch(() => {}); + apiGet('/api/servers').then((d) => setServers(d.servers)).catch(() => {}); + apiGet('/api/devlogs?page=1').then((d) => setLatest(d.items?.[0] ?? null)).catch(() => {}); + apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {}); + window.scrollTo(0, 0); + }, []); + + const online = servers?.filter((s) => s.online) ?? []; + const players = online.reduce((sum, s) => sum + (s.players ?? 0), 0); + + return ( + <> +
+
D4RKST3R
+
+ +
+ {(stats || servers) && ( +
+ {stats?.members > 0 && ( +
+ {stats.members.toLocaleString('de-DE')} + Member im Discord +
+ )} + {servers?.length > 0 && ( +
+ {online.length} / {servers.length} + Server online +
+ )} + {online.length > 0 && ( +
+ {players} + Spieler gerade drauf +
+ )} +
+ )} + +
+ {TILES.map(([to, icon, title, desc]) => ( + + {icon} + {title} + {desc} + + ))} +
+ + {latest && ( +
+

// Neuestes Devlog

+ +
+ +
+ Weiterlesen → + +
+ )} +
+ + ); +} diff --git a/frontend/src/pages/Server.jsx b/frontend/src/pages/Server.jsx index 3638c40..d62f402 100644 --- a/frontend/src/pages/Server.jsx +++ b/frontend/src/pages/Server.jsx @@ -1,5 +1,6 @@ -// Öffentliche Server-Status-Seite: Live-Status + 24h-Spielerzahl-Verlauf -// (Daten sammelt der Server-Monitor alle 2 Minuten). +// Öffentliche Server-Status-Seite: Übersichtsleiste, Karte pro Server mit Logo, +// Live-Status, 24h-Spielerzahl-Verlauf, Uptime und Peak. +// Daten sammelt der Server-Monitor alle 2 Minuten. import { useEffect, useState } from 'react'; import { apiGet } from '../api.js'; import { SkeletonCards } from '../components/Skeleton.jsx'; @@ -35,39 +36,78 @@ function HistoryChart({ history, max }) { ); } +/** Uptime (%) und Spieler-Peak aus den 24h-Samples */ +function historyStats(history) { + if (history.length === 0) return null; + const online = history.filter((h) => h.online).length; + return { + uptime: Math.round((online / history.length) * 100), + peak: Math.max(0, ...history.map((h) => h.players ?? 0)), + }; +} + function ServerCard({ s }) { + const [copied, setCopied] = useState(false); const pct = s.online && s.max ? Math.round((s.players / s.max) * 100) : 0; + const stats = historyStats(s.history); + + async function copyAddress() { + try { + await navigator.clipboard.writeText(s.address); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { /* Browser ohne Clipboard-Rechte */ } + } + return (
- {s.icon} {s.name} - - {s.online === null ? '⏳ Warte auf Check' : s.online ? '🟢 Online' : '🔴 Offline'} - {s.ping != null && s.online && · {s.ping}ms} - + {s.image_url + ? + : {s.icon}} +
+ {s.name} + + {s.online === null ? '⏳ Warte auf Check' : s.online ? '🟢 Online' : '🔴 Offline'} + {s.ping != null && s.online && · {s.ping}ms} + {s.map && s.online && · 🗺️ {s.map}} + +
+ {s.online && s.players != null && ( +
+ {s.players} + {s.max > 0 && / {s.max}} +
+ )}
{s.online && s.players != null && s.max > 0 && ( - <> -
-
-
-

- {s.players} / {s.max} Spieler ({pct}%) - {s.map && · 🗺️ {s.map}} -

- +
+
+
)} {s.history.length > 1 && } - {s.history.length > 1 &&

Spielerzahl · letzte 24h

} + {stats && ( +
+ Spielerzahl · letzte 24h + + Peak {stats.peak} · Uptime{' '} + = 99 ? 'up-good' : stats.uptime >= 90 ? '' : 'up-bad'}> + {stats.uptime}% + + +
+ )} {(s.connect_url || s.address) && (
- {s.connect_url && ( - Connect + {s.connect_url && 🎮 Connect} + {s.address && ( + )} - {s.address && {s.address}}
)} {s.checkedAt && ( @@ -89,6 +129,9 @@ export default function Server() { return () => clearInterval(timer); }, []); + const online = servers?.filter((s) => s.online) ?? []; + const totalPlayers = online.reduce((sum, s) => sum + (s.players ?? 0), 0); + return ( <>
@@ -107,7 +150,23 @@ export default function Server() { {error &&

Server-Status konnte nicht geladen werden.

} {!error && !servers && } {servers?.length === 0 &&

Noch keine Server eingetragen.

} - {servers?.map((s) => )} + + {servers?.length > 0 && ( +
+
+ {online.length} / {servers.length} + Server online +
+
+ {totalPlayers} + Spieler gerade drauf +
+
+ )} + +
+ {servers?.map((s) => )} +
); diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 8452a18..13c6741 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -154,7 +154,7 @@ export default function Settings({ me }) { ['terraria', '🌳 Terraria'], ['arma3', '🪖 Arma 3'], ['squad', '🪖 Squad'], ['unturned', '🏝️ Unturned'], ]; - const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '' }; + const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '' }; const [servers, setServers] = useState([]); const [serverDraft, setServerDraft] = useState(emptyServer); // Bot-Beschreibung + Team @@ -1372,6 +1372,7 @@ export default function Settings({ me }) {
@@ -1407,6 +1408,8 @@ export default function Settings({ me }) { setServerDraft({ ...serverDraft, connect_url: e.target.value })} /> setServerDraft({ ...serverDraft, address: e.target.value })} />
+ + setServerDraft({ ...serverDraft, image_url: e.target.value })} />
{serverDraft.id && } diff --git a/frontend/src/style.css b/frontend/src/style.css index ea5366e..c6d4e43 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -118,7 +118,7 @@ body::after { .nav-logo:hover { text-shadow: 0 0 18px rgba(245, 197, 24, .55); } .nav-logo span { color: var(--neon2); } -.nav-links { display: flex; gap: 1.6rem; flex: 1; } +.nav-links { display: flex; gap: 1.6rem; flex: 1; align-items: center; } .nav-links a { position: relative; font-family: var(--mono); @@ -1257,13 +1257,14 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } } /* ── NAV-DROPDOWNS + BURGER ────────────────────────── */ -.nav-drop { position: relative; } +.nav-drop { position: relative; display: flex; align-items: center; } .nav-drop-btn { display: inline-flex; align-items: center; gap: .35rem; background: none; border: 0; cursor: pointer; font-family: var(--mono); font-size: .7rem; letter-spacing: .25em; text-transform: uppercase; color: var(--muted); padding: .3rem 0; + line-height: 1.6; /* wie die Nav-Links, sonst hängt der Button tiefer */ transition: color .2s, text-shadow .2s; } .nav-drop-btn:hover { color: var(--text); } @@ -1320,15 +1321,21 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } } /* ── LIGHTBOX ──────────────────────────────────────── */ +/* z-index über Noise/Scanlines (900); rendert per Portal direkt in */ .lightbox { - position: fixed; inset: 0; z-index: 1000; + position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center; - background: rgba(0, 0, 0, .92); + background: rgba(0, 0, 0, .95); animation: lbIn .18s ease both; } @keyframes lbIn { from { opacity: 0; } to { opacity: 1; } } +.lb-row { + display: flex; align-items: center; gap: 1.2rem; + max-width: 96vw; +} +.lb-figure { display: flex; flex-direction: column; align-items: center; margin: 0; min-width: 0; } .lb-img { - max-width: min(92vw, 1400px); max-height: 84vh; + max-width: 100%; max-height: 78vh; object-fit: contain; border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 24px 80px rgba(0, 0, 0, .8); } @@ -1340,24 +1347,23 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } } .lb-close:hover { color: var(--neon2); border-color: var(--neon2); } .lb-nav { - position: absolute; top: 50%; transform: translateY(-50%); - background: rgba(0, 0, 0, .5); border: 1px solid var(--border); border-radius: 999px; + flex: none; + background: rgba(255, 255, 255, .04); border: 1px solid var(--border); border-radius: 999px; color: var(--text); font-size: 1.8rem; line-height: 1; width: 3rem; height: 3rem; cursor: pointer; - transition: color .15s, border-color .15s; + transition: color .15s, border-color .15s, background .15s; } -.lb-nav:hover { color: var(--neon); border-color: var(--neon); } -.lb-prev { left: 1.2rem; } -.lb-next { right: 1.2rem; } +.lb-nav:hover { color: var(--neon); border-color: var(--neon); background: rgba(245, 197, 24, .08); } .lb-meta { - position: absolute; bottom: 1.4rem; left: 50%; transform: translateX(-50%); display: flex; gap: 1.2rem; align-items: baseline; font-family: var(--mono); font-size: .72rem; letter-spacing: .12em; - color: var(--muted); background: rgba(0, 0, 0, .6); - border: 1px solid rgba(255, 255, 255, .08); border-radius: 999px; - padding: .4rem 1rem; white-space: nowrap; + color: var(--muted); margin-top: .8rem; white-space: nowrap; } .lb-count { color: var(--neon); } +@media (max-width: 640px) { + .lb-row { gap: .5rem; } + .lb-nav { width: 2.4rem; height: 2.4rem; font-size: 1.4rem; } +} /* ── SKELETON-LOADER ───────────────────────────────── */ .skel { @@ -1378,24 +1384,53 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } .skel-row { height: 2.6rem; border-radius: 10px; margin-bottom: .5rem; animation: riseIn .3s ease both; } /* ── SERVER-STATUS-SEITE ───────────────────────────── */ -.srv-card { margin-bottom: 1.3rem; } +.srv-summary { + display: flex; gap: 2.5rem; flex-wrap: wrap; + background: var(--bg2); border: 1px solid var(--border); border-radius: 12px; + padding: 1.1rem 1.6rem; margin-bottom: 1.5rem; +} +.srv-sum-item { display: flex; flex-direction: column; } +.srv-sum-n { + font-family: var(--display); font-size: 2.2rem; line-height: 1; + color: var(--neon); text-shadow: 0 0 16px rgba(245, 197, 24, .3); +} +.srv-sum-of { font-size: 1.2rem; color: var(--muted2); text-shadow: none; } +.srv-sum-label { + font-family: var(--mono); font-size: .62rem; letter-spacing: .18em; + text-transform: uppercase; color: var(--muted); margin-top: .35rem; +} +.srv-grid { display: grid; grid-template-columns: 1fr; gap: 1.2rem; } +@media (min-width: 980px) { .srv-grid { grid-template-columns: 1fr 1fr; } } +.srv-card { margin-bottom: 0; } .srv-card.srv-down { border-left: 3px solid rgba(242, 63, 67, .6); } -.srv-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; flex-wrap: wrap; } -.srv-name { font-family: var(--display); font-size: 1.5rem; letter-spacing: .04em; } -.srv-status { font-family: var(--mono); font-size: .78rem; letter-spacing: .08em; color: var(--muted); } +.srv-head { display: flex; align-items: center; gap: 1rem; } +.srv-logo { + width: 52px; height: 52px; flex: none; + border-radius: 12px; object-fit: cover; + border: 1px solid var(--border); background: var(--bg3); +} +.srv-logo-emoji { display: flex; align-items: center; justify-content: center; font-size: 1.7rem; } +.srv-title { display: flex; flex-direction: column; min-width: 0; flex: 1; } +.srv-name { font-family: var(--display); font-size: 1.5rem; letter-spacing: .04em; line-height: 1.1; } +.srv-status { font-family: var(--mono); font-size: .72rem; letter-spacing: .08em; color: var(--muted); } .srv-status.on { color: var(--success); } .srv-status.off { color: #f23f43; } -.srv-ping { color: var(--muted2); } +.srv-ping, .srv-map { color: var(--muted2); } +.srv-count { font-family: var(--display); font-size: 1.9rem; line-height: 1; color: var(--neon); white-space: nowrap; } +.srv-count-max { font-size: 1.1rem; color: var(--muted2); } .srv-bar { margin-top: .9rem; } -.srv-players { font-family: var(--mono); font-size: .75rem; color: var(--muted); margin-top: .4rem; } -.srv-map { color: var(--muted2); } .srv-chart { width: 100%; height: 90px; margin-top: 1rem; display: block; } -.srv-chart-label { +.srv-stats { + display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; font-family: var(--mono); font-size: .62rem; letter-spacing: .15em; text-transform: uppercase; color: var(--muted2); margin-top: .25rem; } +.srv-stats b { color: var(--text); font-weight: 400; } +.srv-stats .up-good { color: var(--success); } +.srv-stats .up-bad { color: #f23f43; } .srv-connect { display: flex; gap: .8rem; align-items: center; flex-wrap: wrap; margin-top: 1rem; } -.srv-addr { font-size: .8rem; } +.srv-addr { font-size: .8rem; cursor: pointer; transition: border-color .15s, color .15s; } +.srv-addr:hover { border-color: var(--neon); color: var(--neon); } .srv-checked { font-family: var(--mono); font-size: .62rem; color: var(--muted2); margin-top: .8rem; } /* ── AUDIT-LOG ─────────────────────────────────────── */ @@ -1403,3 +1438,31 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } .audit-ts { font-family: var(--mono); font-size: .65rem; color: var(--muted2); white-space: nowrap; } .audit-action { font-family: var(--mono); font-size: .7rem; color: var(--neon); white-space: nowrap; } .audit-detail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 18rem; } + +/* ── STARTSEITE ────────────────────────────────────── */ +.home-hero { min-height: 420px; } +.home-accent { color: var(--neon2); } +.home-cta { display: flex; gap: .9rem; flex-wrap: wrap; margin-top: 1.6rem; } +.home-cta-alt { border: 1px solid var(--border); } +.home-stats { margin-bottom: 1.5rem; } +.home-tiles { + display: grid; gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +} +.home-tile { + display: flex; flex-direction: column; gap: .3rem; + padding: 1.3rem 1.4rem; +} +.home-tile-icon { font-size: 1.6rem; line-height: 1; margin-bottom: .3rem; } +.home-tile-title { + font-family: var(--display); font-size: 1.4rem; letter-spacing: .05em; + color: var(--text); transition: color .2s; +} +.home-tile:hover .home-tile-title { color: var(--neon); } +.home-tile-desc { font-weight: 300; color: var(--muted); font-size: .95rem; line-height: 1.4; } +.home-devlog { display: block; } +.home-devlog-more { + display: inline-block; margin-top: .8rem; + font-family: var(--mono); font-size: .7rem; letter-spacing: .2em; + text-transform: uppercase; color: var(--neon); +} diff --git a/src/bot/server-monitor.js b/src/bot/server-monitor.js index 7f5869b..8cbdeed 100644 --- a/src/bot/server-monitor.js +++ b/src/bot/server-monitor.js @@ -87,6 +87,7 @@ export function buildServerEmbed(r) { .addFields({ name: 'Status', value: r.online ? '🟢 **Online**' : '🔴 **Offline**', inline: true }) .setFooter({ text: `${brandFooter('STATUS')}${r.ping != null ? ` • ${r.ping}ms` : ''} • alle 2 min` }) .setTimestamp(); + if (r.image_url) embed.setThumbnail(r.image_url); if (r.online && r.players != null && r.max) { const pct = Math.min(1, r.players / r.max); diff --git a/src/db.js b/src/db.js index ee70146..0a805fb 100644 --- a/src/db.js +++ b/src/db.js @@ -531,14 +531,15 @@ db.exec(` if (!cols.includes('host')) db.exec(`ALTER TABLE gameservers ADD COLUMN host TEXT NOT NULL DEFAULT ''`); if (!cols.includes('port')) db.exec('ALTER TABLE gameservers ADD COLUMN port INTEGER'); if (!cols.includes('connect_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN connect_url TEXT NOT NULL DEFAULT ''`); + if (!cols.includes('image_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN image_url TEXT NOT NULL DEFAULT ''`); } const insertGameserver = db.prepare(` - INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url) - VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url) + INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url) + VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url) `); const updateGameserverStmt = db.prepare(` UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address, - host = @host, port = @port, connect_url = @connect_url + host = @host, port = @port, connect_url = @connect_url, image_url = @image_url WHERE id = @id `); const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?'); diff --git a/src/web/api.js b/src/web/api.js index 29365b7..8484686 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -134,6 +134,7 @@ export function registerApiRoutes(app, client) { name: s.name, type: s.type, icon: GAME_ICONS[s.type] ?? '🎮', + image_url: s.image_url || null, address: s.address || null, connect_url: s.connect_url || null, online: r?.online ?? null, // null = noch nie gecheckt @@ -818,6 +819,9 @@ ${rssItems} host: host.slice(0, 120), port, connect_url: String(body.connect_url ?? '').trim().slice(0, 200), + image_url: /^https:\/\/.+/.test(String(body.image_url ?? '').trim()) + ? String(body.image_url).trim().slice(0, 300) + : '', }; }