From eb3dd1de56dd9751b6cef34ff926efcba1a73c40 Mon Sep 17 00:00:00 2001 From: pavl21 <36771750+pavl21@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:48:00 +0200 Subject: [PATCH 1/3] feat: add PVQ Panel presence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows current activity in PVQ Panel (pv-q.de) on Discord — server name, server status, and current action (console, files, backups, etc.) based on the visited sub-page. Configurable via settings (show server name, show status, show elapsed time). --- websites/P/PVQ Panel/metadata.json | 65 ++++++++ websites/P/PVQ Panel/presence.ts | 229 +++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 websites/P/PVQ Panel/metadata.json create mode 100644 websites/P/PVQ Panel/presence.ts diff --git a/websites/P/PVQ Panel/metadata.json b/websites/P/PVQ Panel/metadata.json new file mode 100644 index 000000000000..ab1aab9644d2 --- /dev/null +++ b/websites/P/PVQ Panel/metadata.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://schemas.premid.app/metadata/1.10", + "apiVersion": 1, + "author": { + "name": "PVQ", + "id": "289810872396808193" + }, + "service": "PVQ Panel", + "description": { + "de": "Zeigt deine aktuelle Aktivität im PVQ Panel auf Discord.", + "en": "Shows what you are doing in PVQ Panel on Discord." + }, + "url": "pv-q.de", + "regExp": "^https://(?:[a-z0-9-]+\\.)?pv-q\\.de(/.*)?$", + "version": "1.0.1", + "logo": "https://pv-q.de/favicons/pvq-icon-maskable-512x512.png", + "thumbnail": "https://pv-q.de/favicons/pvq-icon-maskable-512x512.png", + "color": "#3b82f6", + "category": "other", + "tags": ["pvq", "panel", "gameserver", "pterodactyl"], + "settings": [ + { + "id": "showServerName", + "icon": "fas fa-server", + "value": true, + "multiLanguage": true, + "title": { + "en": "Show Server Name", + "de": "Servername anzeigen" + }, + "description": { + "en": "Show the current server name on Discord.", + "de": "Zeigt den aktuellen Servernamen auf Discord an." + } + }, + { + "id": "showStatus", + "icon": "fas fa-signal", + "value": true, + "multiLanguage": true, + "title": { + "en": "Show Server Status", + "de": "Server-Status anzeigen" + }, + "description": { + "en": "Show the server status (Online/Offline) on Discord.", + "de": "Zeigt den Server-Status (Online/Offline) auf Discord an." + } + }, + { + "id": "showElapsedTime", + "icon": "fas fa-clock", + "value": true, + "multiLanguage": true, + "title": { + "en": "Show Elapsed Time", + "de": "Vergangene Zeit anzeigen" + }, + "description": { + "en": "Show how long you've been on the page.", + "de": "Zeigt, wie lange du auf der Seite bist." + } + } + ] +} diff --git a/websites/P/PVQ Panel/presence.ts b/websites/P/PVQ Panel/presence.ts new file mode 100644 index 000000000000..fda845ae661d --- /dev/null +++ b/websites/P/PVQ Panel/presence.ts @@ -0,0 +1,229 @@ +const presence = new Presence({ clientId: '1509262800776728726' }) +const startTimestamp = Math.floor(Date.now() / 1000) + +enum ActivityAssets { + Logo = 'https://pv-q.de/favicons/pvq-icon-maskable-512x512.png', +} + +// Liest Server-Namen aus PVQ Panel Sidebar DOM. +// Regulärer Server: +// Node-Server: zwei Spans — "Node 555" + "Germany 1" → "Node 555 - Germany 1" +function getServerName(): string | null { + const regularSpan = document.querySelector( + 'span.font-semibold.text-lg.text-gray-50.truncate[title]', + ) + if (regularSpan) { + const name = regularSpan.getAttribute('title')?.trim() + if (name) + return name + } + + const nodeLabel = document.querySelector( + 'span.text-2xl.font-extrabold.text-gray-50', + )?.textContent?.trim() + const nodeDesc = document.querySelector( + 'span.text-md.text-gray-300', + )?.textContent?.trim() + + if (nodeLabel && nodeDesc) + return `${nodeLabel} - ${nodeDesc}` + if (nodeLabel) + return nodeLabel + + return null +} + +function isNodeServer(name: string): boolean { + return /^Node \d+ - .+$/.test(name) +} + +function isBotPath(subPath: string): boolean { + return ['/knowledge', '/blacklist', '/channels', '/profile', '/access', '/memory'].some( + p => subPath.startsWith(p), + ) +} + +function isNodeManagerPath(subPath: string): boolean { + return ['/firewall', '/system-logs', '/node-users', '/api-control'].some( + p => subPath.startsWith(p), + ) +} + +// Liest Server-Status aus aria-label des ServerStatusBadge DOM-Elements. +// ServerStatusBadge rendert:
+function getServerStatus(): string | null { + return ( + document.querySelector('div[role="img"][aria-label]') + ?.getAttribute('aria-label') ?? null + ) +} + +// Zählt Server-Rows im Node-Manager-Dashboard. +// ServerSplitterContainer:
→ Kinder sind motion.div je Server +function getNodeServerCount(): number { + const container = document.querySelector('.server-list-container') + if (!container) + return 0 + return container.children.length +} + +function getServerAction(subPath: string): string { + if (subPath === '/' || subPath === '') + return 'Verwaltet Konsole' + if (subPath.startsWith('/files/edit') || subPath.startsWith('/files/new')) + return 'Bearbeitet Dateien' + if (subPath.startsWith('/files')) + return 'Verwaltet Dateien' + if (subPath.startsWith('/databases')) + return 'Verwaltet Datenbanken' + if (subPath.startsWith('/backups')) + return 'Verwaltet Backups' + if (subPath.startsWith('/minecraft-plugins')) + return 'Installiert Plugins' + if (subPath.startsWith('/modpacks')) + return 'Durchsucht Modpacks' + if (subPath.startsWith('/players')) + return 'Verwaltet Spieler' + if (subPath.startsWith('/minecraft/bedrock-addons')) + return 'Verwaltet Bedrock-Addons' + if (subPath.startsWith('/minecraft/properties')) + return 'Bearbeitet Server-Einstellungen' + if (subPath.startsWith('/minecraft/versions')) + return 'Wechselt Server-Version' + if (subPath.startsWith('/bedrock-support')) + return 'Konfiguriert Bedrock' + if (subPath.startsWith('/network')) + return 'Verwaltet Port-Freigaben' + if (subPath.startsWith('/proxy')) + return 'Konfiguriert Proxy' + if (subPath.startsWith('/subdomain')) + return 'Konfiguriert Subdomains' + if (subPath.startsWith('/schedules/') && subPath.length > '/schedules/'.length) + return 'Bearbeitet Automatisierung' + if (subPath.startsWith('/schedules')) + return 'Verwaltet Automatisierungen' + if (subPath.startsWith('/users')) + return 'Verwaltet Zugänge' + if (subPath.startsWith('/startup')) + return 'Konfiguriert Startup' + if (subPath.startsWith('/settings')) + return 'Verwaltet Einstellungen' + if (subPath.startsWith('/Statistics')) + return 'Analysiert Statistiken' + if (subPath.startsWith('/activity')) + return 'Prüft Aktivitätslogs' + if (subPath.startsWith('/picoclaw')) + return 'Verwaltet PicoClaw' + return 'Im Panel' +} + +function getNodeAction(subPath: string): string { + if (subPath === '/' || subPath === '') { + const count = getNodeServerCount() + return count > 0 ? `Verwaltet ${count} Server` : 'Verwaltet Node' + } + if (subPath.startsWith('/firewall')) + return 'Konfiguriert Firewall' + if (subPath.startsWith('/system-logs')) + return 'Liest System-Logs' + if (subPath.startsWith('/node-users')) + return 'Verwaltet Node-Nutzer' + if (subPath.startsWith('/api-control')) + return 'Verwaltet API-Keys' + return 'Verwaltet Node' +} + +function getBotAction(subPath: string): string { + if (subPath === '/' || subPath === '') + return 'Bot-Übersicht' + if (subPath.startsWith('/knowledge')) + return 'Trainiert Wissensdatenbank' + if (subPath.startsWith('/blacklist')) + return 'Verwaltet Blacklist' + if (subPath.startsWith('/channels')) + return 'Konfiguriert Kanäle' + if (subPath.startsWith('/profile')) + return 'Bearbeitet Bot-Profil' + if (subPath.startsWith('/logs')) + return 'Liest Bot-Logs' + if (subPath.startsWith('/access')) + return 'Verwaltet Bot-Zugang' + if (subPath.startsWith('/activity')) + return 'Prüft Bot-Aktivität' + if (subPath.startsWith('/memory')) + return 'Verwaltet Bot-Memory' + if (subPath.startsWith('/settings')) + return 'Bot-Einstellungen' + return 'Verwaltet Bot' +} + +function getAccountAction(pathname: string): string { + if (pathname.includes('/activity')) + return 'Account-Aktivität' + if (pathname.includes('/snippets')) + return 'Command Snippets' + if (pathname.includes('/data-export')) + return 'DSGVO-Datenauskunft' + return 'Account-Einstellungen' +} + +presence.on('UpdateData', async () => { + const presenceData: PresenceData = { + largeImageKey: ActivityAssets.Logo, + buttons: [{ label: 'PVQ Panel öffnen', url: 'https://pv-q.de/auth/login' }], + } + + const showServerName = await presence.getSetting('showServerName') + const showStatus = await presence.getSetting('showStatus') + const showElapsedTime = await presence.getSetting('showElapsedTime') + + if (showElapsedTime) { + presenceData.startTimestamp = startTimestamp + } + + const { pathname } = document.location + + const serverMatch = pathname.match(/^\/server\/([a-f0-9-]+)(\/.*)?$/i) + + if (serverMatch) { + const subPath = serverMatch[2] || '/' + const serverName = getServerName() + + if (isBotPath(subPath)) { + presenceData.details = showServerName && serverName + ? `Discord Bot: ${serverName}` + : 'Discord Bot' + presenceData.state = getBotAction(subPath) + } + else if (isNodeManagerPath(subPath) || (serverName !== null && isNodeServer(serverName))) { + presenceData.details = showServerName && serverName + ? `Node Manager: ${serverName}` + : 'Node Manager' + presenceData.state = getNodeAction(subPath) + } + else { + let details = showServerName && serverName + ? `Server: ${serverName}` + : 'Server verwalten' + + if (showStatus && showServerName) { + const status = getServerStatus() + if (status) + details += ` · ${status}` + } + + presenceData.details = details + presenceData.state = getServerAction(subPath) + } + } + else if (pathname.startsWith('/account')) { + presenceData.details = 'PVQ Panel' + presenceData.state = getAccountAction(pathname) + } + else { + presenceData.details = 'PVQ Panel' + presenceData.state = 'Dashboard' + } + + presence.setActivity(presenceData) +}) From 280295464e523545559949a7384d12744ad4eb9e Mon Sep 17 00:00:00 2001 From: pavl21 <36771750+pavl21@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:08:11 +0200 Subject: [PATCH 2/3] chore: update author name to Pavl21 --- websites/P/PVQ Panel/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websites/P/PVQ Panel/metadata.json b/websites/P/PVQ Panel/metadata.json index ab1aab9644d2..838d6a0bcaec 100644 --- a/websites/P/PVQ Panel/metadata.json +++ b/websites/P/PVQ Panel/metadata.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.premid.app/metadata/1.10", "apiVersion": 1, "author": { - "name": "PVQ", + "name": "Pavl21", "id": "289810872396808193" }, "service": "PVQ Panel", From 95aad3411c10c82a22e6ffb8adcbbf2a22a29593 Mon Sep 17 00:00:00 2001 From: pavl21 <36771750+pavl21@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:18:03 +0200 Subject: [PATCH 3/3] fix: avoid redundant "Server" wording, truncate long names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status label from the panel already reads "Server läuft" etc. — strip the "Server " prefix so it doesn't repeat after "Server: ". - Fallback details text changed from "Server verwalten" to "Server" to match the Node Manager / Discord Bot fallbacks and avoid echoing the "Verwaltet ..." wording used in state. - Long server/node names (e.g. auto-generated egg names) are truncated to 40 chars for a cleaner compact status line. --- websites/P/PVQ Panel/presence.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/websites/P/PVQ Panel/presence.ts b/websites/P/PVQ Panel/presence.ts index fda845ae661d..08ad8971115c 100644 --- a/websites/P/PVQ Panel/presence.ts +++ b/websites/P/PVQ Panel/presence.ts @@ -37,6 +37,11 @@ function isNodeServer(name: string): boolean { return /^Node \d+ - .+$/.test(name) } +// Kürzt lange Server-/Node-Namen für die kompakte Discord-Anzeige. +function truncate(text: string, max: number): string { + return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text +} + function isBotPath(subPath: string): boolean { return ['/knowledge', '/blacklist', '/channels', '/profile', '/access', '/memory'].some( p => subPath.startsWith(p), @@ -51,11 +56,14 @@ function isNodeManagerPath(subPath: string): boolean { // Liest Server-Status aus aria-label des ServerStatusBadge DOM-Elements. // ServerStatusBadge rendert:
+// Panel-Texte sind z.B. "Server läuft" — "Server "-Präfix entfernt, da details +// bereits mit "Server: " beginnt (sonst "Server: X · Server läuft"). function getServerStatus(): string | null { - return ( - document.querySelector('div[role="img"][aria-label]') - ?.getAttribute('aria-label') ?? null - ) + const label = document.querySelector('div[role="img"][aria-label]') + ?.getAttribute('aria-label') + if (!label) + return null + return label.replace(/^Server\s+/i, '') } // Zählt Server-Rows im Node-Manager-Dashboard. @@ -191,20 +199,20 @@ presence.on('UpdateData', async () => { if (isBotPath(subPath)) { presenceData.details = showServerName && serverName - ? `Discord Bot: ${serverName}` + ? `Discord Bot: ${truncate(serverName, 40)}` : 'Discord Bot' presenceData.state = getBotAction(subPath) } else if (isNodeManagerPath(subPath) || (serverName !== null && isNodeServer(serverName))) { presenceData.details = showServerName && serverName - ? `Node Manager: ${serverName}` + ? `Node Manager: ${truncate(serverName, 40)}` : 'Node Manager' presenceData.state = getNodeAction(subPath) } else { let details = showServerName && serverName - ? `Server: ${serverName}` - : 'Server verwalten' + ? `Server: ${truncate(serverName, 40)}` + : 'Server' if (showStatus && showServerName) { const status = getServerStatus()