From b851cbc20b0d74757a2eb5e8f324bfd552d71f74 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Fri, 22 May 2026 11:16:17 +0200 Subject: [PATCH] Upload via Git Manager GUI --- BungeeCord-Chrome/popup.js | 719 ++++++++++++++++++++----------------- 1 file changed, 384 insertions(+), 335 deletions(-) diff --git a/BungeeCord-Chrome/popup.js b/BungeeCord-Chrome/popup.js index a628b66..edda474 100644 --- a/BungeeCord-Chrome/popup.js +++ b/BungeeCord-Chrome/popup.js @@ -1,335 +1,384 @@ -//popup.js -const $ = id => document.getElementById(id); -const uid = () => 'srv_' + Math.random().toString(36).slice(2,9); - -const inputName = $('inputName'); -const inputUrl = $('inputUrl'); -const inputWpSite = $('inputWpSite'); -const inputWpServerId = $('inputWpServerId'); -const btnAdd = $('btnAddServer'); -const serversContainer = $('serversContainer'); -const serverListPanel = document.querySelector('.server-list'); -const btnRefresh = $('btnRefresh'); -const btnToggleSettings = $('btnToggleSettings'); -const settingsForm = $('settingsForm'); - -const noSelection = $('noSelection'); -const detailContent = $('detailContent'); -const detailName = $('detailName'); -const detailUrlText = $('detailUrlText'); -const detailStatus = $('detailStatus'); -const detailPulse = $('detailPulse'); -const detailVersion = $('detailVersion'); -const detailPlayers = $('detailPlayers'); -const detailPing = $('detailPing'); -const detailPlayerList = $('detailPlayerList'); -const btnEdit = $('btnEdit'); -const btnDelete = $('btnDelete'); - -let servers = []; -let selectedId = null; -let statuses = {}; -let previousStatuses = {}; -let settingsVisible = false; - -document.addEventListener('DOMContentLoaded', init); -btnAdd.addEventListener('click', handleAdd); -btnRefresh.addEventListener('click', manualRefresh); -btnToggleSettings.addEventListener('click', toggleSettings); -btnEdit.addEventListener('click', handleEdit); -btnDelete.addEventListener('click', handleDelete); - -async function init() { - await loadServersFromStorage(); - await loadStatusesFromStorage(); - await loadSettingsVisibility(); - renderServerList(); - - if (servers.length === 1) selectedId = servers[0].id; - renderDetail(selectedId); - - adjustDetailLayout(); -} - -// --- NEU: Minecraft Color Parser (Eingebaut ohne Löschung) --- -function parseMinecraftColors(text) { - if (!text) return ''; - const map = { - '&0': '#000000', '&1': '#0000AA', '&2': '#00AA00', '&3': '#00AAAA', - '&4': '#AA0000', '&5': '#AA00AA', '&6': '#FFAA00', '&7': '#AAAAAA', - '&8': '#555555', '&9': '#5555FF', '&a': '#55FF55', '&b': '#55FFFF', - '&c': '#FF5555', '&d': '#FF55FF', '&e': '#FFFF55', '&f': '#FFFFFF' - }; - let html = text.replace(/[<>]/g, ''); - Object.keys(map).forEach(code => { - const color = map[code]; - const regex = new RegExp(code, 'g'); - html = html.replace(regex, ``); - }); - return `${html}`.replace(/&l/g, '').replace(/&r/g, ''); -} - -// --- 3D Avatar URL Generator --- -function get3DAvatarUrl(playerName, uuid = null) { - const isBedrock = playerName.includes('.') || (uuid && uuid.startsWith('xuid')); - if (isBedrock) { - if (uuid && uuid.length > 0) { - return `https://mc-heads.net/head/${encodeURIComponent(uuid)}/64`; - } - return `https://mc-heads.net/head/${encodeURIComponent(playerName)}/64`; - } else { - return `https://mc-heads.net/head/${encodeURIComponent(playerName)}/64`; - } -} - -async function loadSettingsVisibility() { - const obj = await chrome.storage.local.get(['settingsVisible']); - settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false; - if (!settingsVisible) settingsForm.classList.add('hidden'); - else settingsForm.classList.remove('hidden'); -} - -async function saveSettingsVisibility() { - await chrome.storage.local.set({ settingsVisible }); -} - -async function toggleSettings() { - settingsVisible = !settingsVisible; - settingsForm.classList.toggle('hidden'); - await saveSettingsVisibility(); - adjustDetailLayout(); -} - -async function loadServersFromStorage() { - const obj = await chrome.storage.local.get(['servers']); - servers = obj.servers || []; - for (const s of servers) if (!s.id) s.id = uid(); - await chrome.storage.local.set({ servers }); -} - -async function saveServersToStorage() { - await chrome.storage.local.set({ servers }); -} - -async function loadStatusesFromStorage() { - const obj = await chrome.storage.local.get(['serverStatuses']); - statuses = obj.serverStatuses || {}; -} - -function adjustDetailLayout() { - const settingsHidden = settingsForm.classList.contains('hidden'); - detailContent.classList.toggle('full-width', settingsHidden); - serverListPanel.classList.toggle('hidden', settingsHidden); - btnEdit.style.display = settingsHidden ? 'none' : 'inline-block'; - btnDelete.style.display = settingsHidden ? 'none' : 'inline-block'; -} - -function renderServerList() { - serversContainer.innerHTML = ''; - if (servers.length === 0) { - serversContainer.innerHTML = '
Noch keine Server hinzugefügt.
'; - return; - } - for (const s of servers) { - const item = document.createElement('li'); - item.className = 'server-item'; - item.dataset.id = s.id; - const meta = document.createElement('div'); - meta.className = 'meta'; - const name = document.createElement('div'); - name.className = 'name'; - name.textContent = s.name || '(Kein Name)'; - let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : ''); - listUrl = listUrl.replace(':9191', ''); - const url = document.createElement('div'); - url.className = 'url'; - url.textContent = listUrl; - meta.append(name, url); - const statusBubble = document.createElement('div'); - statusBubble.className = 'status-bubble'; - statusBubble.textContent = '—'; - statusBubble.style.backgroundColor = 'transparent'; - item.statusBubble = statusBubble; - item.append(meta, statusBubble); - item.addEventListener('click', () => { - selectedId = s.id; - renderDetail(selectedId); - }); - serversContainer.appendChild(item); - } - updateServerListStatuses(); -} - -// --- Fortsetzung popup.js --- - -function renderDetail(id) { - if (!id) { - noSelection.classList.remove('hidden'); - detailContent.classList.add('hidden'); - return; - } - const srv = servers.find(x => x.id === id); - if (!srv) { - noSelection.classList.remove('hidden'); - detailContent.classList.add('hidden'); - return; - } - noSelection.classList.add('hidden'); - detailContent.classList.remove('hidden'); - detailName.textContent = srv.name || 'Unbenannter Server'; - let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal'); - urlToShow = urlToShow.replace(':9191', ''); - detailUrlText.textContent = urlToShow; - updateDetailForServer(srv, true); -} - -function updateDetailForServer(srv, force = false) { - const st = statuses[srv.id]; - const prevSt = previousStatuses[srv.id]; - const statusChanged = force || !prevSt || (prevSt.ok !== st?.ok) || (prevSt.data?.online !== st?.data?.online); - - if (!st || !st.ok || !st.data) { - if (statusChanged) { - detailStatus.textContent = 'Offline'; - detailPulse.classList.remove('online'); - detailVersion.textContent = '-'; - detailPlayers.textContent = '-'; - detailPing.textContent = '-'; - updatePlayerList([]); - } - } else { - const d = st.data; - if (statusChanged) { - detailStatus.textContent = d.online ? 'Online' : 'Offline'; - if (d.online) detailPulse.classList.add('online'); - else detailPulse.classList.remove('online'); - } - const newVersion = d.version || 'unknown'; - if (force || detailVersion.textContent !== newVersion) detailVersion.textContent = newVersion; - const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0); - const maxPlayers = d.max_players; - let newPlayersText = String(playersCount); - if (maxPlayers && maxPlayers !== '-1') newPlayersText += ` / ${maxPlayers}`; - if (force || detailPlayers.textContent !== newPlayersText) detailPlayers.textContent = newPlayersText; - let pingVal = d.ping || d.latency || '-'; - if (pingVal !== '-' && typeof pingVal === 'number') pingVal = pingVal + ' ms'; - if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) detailPing.textContent = pingVal; - - const currentPlayers = Array.isArray(d.players) ? d.players : []; - const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : []; - let playersChanged = force || JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers); - if (playersChanged) updatePlayerList(currentPlayers); - } - previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null; -} - -// --- Spielerliste mit Prefix-Hover und Farbsupport --- -function updatePlayerList(players) { - detailPlayerList.innerHTML = ''; - if (!players || players.length === 0) { - detailPlayerList.innerHTML = '
  • Keine Spieler online.
  • '; - return; - } - - for (const p of players) { - const li = document.createElement('li'); - li.className = 'player-item'; // CSS Klasse für den Hover-Container - - let name = ''; - let uuid = null; - let prefix = ''; - - if (typeof p === 'object') { - name = p.name || p.username || p.player || ''; - uuid = p.uuid || null; - prefix = p.prefix || ''; - } else { - name = String(p); - } - - const img = document.createElement('img'); - img.src = (typeof p === 'object' && p.avatar) ? p.avatar : get3DAvatarUrl(name, uuid); - img.className = 'player-avatar'; - img.loading = 'lazy'; - img.onerror = function() { - this.onerror = null; - this.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/64`; - }; - - // Das Hover-Element mit farbigem Prefix + Name - const info = document.createElement('div'); - info.className = 'player-hover-info'; - info.innerHTML = `${parseMinecraftColors(prefix)} ${name}`.trim(); - - li.appendChild(img); - li.appendChild(info); - detailPlayerList.appendChild(li); - } -} - -function updateServerListStatuses() { - const items = serversContainer.querySelectorAll('.server-item'); - items.forEach(item => { - const s = servers.find(x => x.id === item.dataset.id); - if (!s) return; - const st = statuses[s.id]; - if (!st || !st.ok || !st.data) { - item.statusBubble.textContent = 'Offline'; - item.statusBubble.style.backgroundColor = 'var(--offline)'; - } else if (st.data.online) { - item.statusBubble.textContent = 'Online'; - item.statusBubble.style.backgroundColor = 'var(--online)'; - } else { - item.statusBubble.textContent = 'Offline'; - item.statusBubble.style.backgroundColor = 'var(--offline)'; - } - }); -} - -async function handleAdd() { - const name = inputName.value.trim(); - const url = inputUrl.value.trim(); - const wpSite = inputWpSite.value.trim(); - const wpServerId = inputWpServerId.value.trim(); - if (!url && !wpSite) return; - const s = { id: uid(), name: name || url || wpSite, url: url || null, wpSite: wpSite || null, wpServerId: wpServerId || null }; - servers.push(s); - await saveServersToStorage(); - inputName.value = inputUrl.value = inputWpSite.value = inputWpServerId.value = ''; - renderServerList(); -} - -async function handleEdit() { - if (!selectedId) return; - const srv = servers.find(s => s.id === selectedId); - if (!srv) return; - const newName = prompt('Name:', srv.name) || srv.name; - srv.name = newName.trim(); - await saveServersToStorage(); - renderServerList(); - renderDetail(selectedId); -} - -async function handleDelete() { - if (!selectedId || !confirm('Server wirklich löschen?')) return; - servers = servers.filter(s => s.id !== selectedId); - selectedId = null; - await saveServersToStorage(); - renderServerList(); - renderDetail(null); -} - -async function manualRefresh() { - try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {} -} - -chrome.storage.onChanged.addListener((changes, area) => { - if (area === 'local' && changes.serverStatuses) { - statuses = changes.serverStatuses.newValue || {}; - updateServerListStatuses(); - if (selectedId) { - const srv = servers.find(s => s.id === selectedId); - if (srv) updateDetailForServer(srv); - } - } -}); \ No newline at end of file +// popup.js +const $ = id => document.getElementById(id); +const uid = () => 'srv_' + Math.random().toString(36).slice(2,9); + +let servers = []; +let selectedId = null; +let statuses = {}; +let previousStatuses = {}; +let settingsVisible = false; + +document.addEventListener('DOMContentLoaded', init); +$('btnAdd')?.addEventListener('click', handleAdd); +$('btnAddServer')?.addEventListener('click', handleAdd); +$('btnRefresh').addEventListener('click', manualRefresh); +$('btnToggleSettings').addEventListener('click', toggleSettings); +$('btnEdit').addEventListener('click', handleEdit); +$('btnDelete').addEventListener('click', handleDelete); + +async function init() { + await loadServersFromStorage(); + await loadStatusesFromStorage(); + await loadSettingsVisibility(); + renderServerList(); + if (servers.length === 1) selectedId = servers[0].id; + renderDetail(selectedId); +} + +// ── Minecraft color code parser ── +function parseMinecraftColors(text) { + if (!text) return ''; + const map = { + '§0':'#000000','§1':'#0000AA','§2':'#00AA00','§3':'#00AAAA', + '§4':'#AA0000','§5':'#AA00AA','§6':'#FFAA00','§7':'#AAAAAA', + '§8':'#555555','§9':'#5555FF','§a':'#55FF55','§b':'#55FFFF', + '§c':'#FF5555','§d':'#FF55FF','§e':'#FFFF55','§f':'#FFFFFF', + '&0':'#000000','&1':'#0000AA','&2':'#00AA00','&3':'#00AAAA', + '&4':'#AA0000','&5':'#AA00AA','&6':'#FFAA00','&7':'#AAAAAA', + '&8':'#555555','&9':'#5555FF','&a':'#55FF55','&b':'#55FFFF', + '&c':'#FF5555','&d':'#FF55FF','&e':'#FFFF55','&f':'#FFFFFF' + }; + let html = text.replace(/[<>]/g, ''); + let result = ''; + let i = 0; + while (i < html.length) { + const ch = html[i]; + if ((ch === '§' || ch === '&') && i + 1 < html.length) { + const code = ch + html[i+1].toLowerCase(); + if (map[code]) { + result += ``; + i += 2; + continue; + } + } + result += html[i]; + i++; + } + return result; +} + +function get3DAvatarUrl(name, uuid = null) { + return `https://mc-heads.net/head/${encodeURIComponent(name)}/64`; +} + +// ── Storage ── +async function loadSettingsVisibility() { + const obj = await chrome.storage.local.get(['settingsVisible']); + settingsVisible = !!obj.settingsVisible; + applySettingsVisibility(); +} +async function saveSettingsVisibility() { + await chrome.storage.local.set({ settingsVisible }); +} +function applySettingsVisibility() { + $('settingsForm').classList.toggle('hidden', !settingsVisible); + $('serverListPanel').classList.toggle('hidden', !settingsVisible); + $('detailButtons').classList.toggle('hidden', !settingsVisible); +} +async function toggleSettings() { + settingsVisible = !settingsVisible; + applySettingsVisibility(); + await saveSettingsVisibility(); +} + +async function loadServersFromStorage() { + const obj = await chrome.storage.local.get(['servers']); + servers = obj.servers || []; + for (const s of servers) if (!s.id) s.id = uid(); + await chrome.storage.local.set({ servers }); +} +async function saveServersToStorage() { + await chrome.storage.local.set({ servers }); +} +async function loadStatusesFromStorage() { + const obj = await chrome.storage.local.get(['serverStatuses']); + statuses = obj.serverStatuses || {}; +} + +// ── Server list ── +function renderServerList() { + const container = $('serversContainer'); + container.innerHTML = ''; + if (servers.length === 0) { + container.innerHTML = '
    Noch kein Server hinzugefügt.
    '; + return; + } + for (const s of servers) { + const li = document.createElement('li'); + li.className = 'server-item' + (s.id === selectedId ? ' selected' : ''); + li.dataset.id = s.id; + + const meta = document.createElement('div'); + meta.className = 'meta'; + + const nameEl = document.createElement('div'); + nameEl.className = 'name'; + nameEl.textContent = s.name || '(Kein Name)'; + + let urlText = s.url || (s.wpSite || ''); + urlText = urlText.replace(':9191', ''); + const urlEl = document.createElement('div'); + urlEl.className = 'url'; + urlEl.textContent = urlText; + + meta.append(nameEl, urlEl); + + const bubble = document.createElement('div'); + bubble.className = 'status-bubble'; + bubble.textContent = '—'; + li._bubble = bubble; + + li.append(meta, bubble); + li.addEventListener('click', () => { + selectedId = s.id; + document.querySelectorAll('.server-item').forEach(el => el.classList.remove('selected')); + li.classList.add('selected'); + renderDetail(selectedId); + }); + container.appendChild(li); + } + updateServerListStatuses(); +} + +// ── Detail ── +function renderDetail(id) { + if (!id) { + $('noSelection').classList.remove('hidden'); + $('detailContent').classList.add('hidden'); + return; + } + const srv = servers.find(x => x.id === id); + if (!srv) { + $('noSelection').classList.remove('hidden'); + $('detailContent').classList.add('hidden'); + return; + } + $('noSelection').classList.add('hidden'); + $('detailContent').classList.remove('hidden'); + + $('detailName').textContent = srv.name || 'Unbenannter Server'; + let urlToShow = srv.url || (srv.wpSite || 'Lokal'); + urlToShow = urlToShow.replace(':9191', ''); + $('detailUrlText').textContent = urlToShow; + + updateDetailForServer(srv, true); +} + +function updateDetailForServer(srv, force = false) { + const st = statuses[srv.id]; + const prevSt = previousStatuses[srv.id]; + const changed = force || JSON.stringify(st) !== JSON.stringify(prevSt); + if (!changed) return; + + const badge = $('statusBadge'); + const pulse = $('detailPulse'); + + if (!st || !st.ok || !st.data) { + badge.className = 'online-badge offline'; + pulse.className = 'pulsing-dot offline'; + $('detailStatus').textContent = 'Offline'; + $('detailVersion').textContent = '-'; + $('detailPlayers').textContent = '-'; + $('detailPing').textContent = '-'; + $('motdBar').classList.add('hidden'); + updatePlayerList([]); + updateBackendServers(null); + } else { + const d = st.data; + const isOnline = !!d.online; + + badge.className = 'online-badge ' + (isOnline ? 'online' : 'offline'); + pulse.className = 'pulsing-dot ' + (isOnline ? 'online' : 'offline'); + $('detailStatus').textContent = isOnline ? 'Online' : 'Offline'; + $('detailVersion').textContent = d.version || 'unknown'; + + const count = Array.isArray(d.players) ? d.players.length + : (typeof d.players === 'number' ? d.players : 0); + const max = d.max_players; + $('detailPlayers').textContent = max && max !== '-1' ? `${count} / ${max}` : String(count); + + const ping = d.ping || d.latency; + $('detailPing').textContent = typeof ping === 'number' ? ping + ' ms' : '-'; + + // MOTD + const motdRaw = d.motd || (d.network && d.network.motd) || ''; + if (motdRaw) { + const motdEl = $('motdBar'); + motdEl.innerHTML = parseMinecraftColors(motdRaw); + motdEl.classList.remove('hidden'); + } else { + $('motdBar').classList.add('hidden'); + } + + const currentPlayers = Array.isArray(d.players) ? d.players : []; + updatePlayerList(currentPlayers); + updateBackendServers(d.network?.backend_servers || null); + } + + previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null; +} + +// ── Sub-Server chips ── +function updateBackendServers(list) { + const section = $('backendSection'); + const container = $('backendServersList'); + if (!list || list.length === 0) { + section.classList.add('hidden'); + container.innerHTML = ''; + return; + } + section.classList.remove('hidden'); + container.innerHTML = ''; + for (const bs of list) { + const count = bs.online_players || 0; + const chip = document.createElement('div'); + chip.className = 'sub-chip' + (count > 0 ? ' active' : ''); + + const dot = document.createElement('span'); + dot.className = 'sub-chip-dot'; + + const nameEl = document.createElement('span'); + nameEl.className = 'sub-chip-name'; + nameEl.textContent = bs.name; + + const countEl = document.createElement('span'); + countEl.className = 'sub-chip-count'; + countEl.textContent = count > 0 ? count : 'leer'; + + chip.append(dot, nameEl, countEl); + container.appendChild(chip); + } +} + +// ── Player list ── +// Versucht den Server-Namen aus den Daten zu lesen. +// Wenn dein Plugin "server" im Spieler-Objekt mitgibt, wird es als Tag angezeigt. +function updatePlayerList(players) { + const grid = $('detailPlayerList'); + grid.innerHTML = ''; + + if (!players || players.length === 0) { + grid.innerHTML = '
    Keine Spieler online.
    '; + return; + } + + for (const p of players) { + const card = document.createElement('div'); + card.className = 'player-card'; + + let name = '', uuid = null, prefix = '', serverName = null; + + if (typeof p === 'object') { + name = p.name || p.username || p.player || ''; + uuid = p.uuid || null; + prefix = p.prefix || p.group || ''; + // Mögliche Felder: server, current_server, connected_server + serverName = p.server || p.current_server || p.connected_server || null; + } else { + name = String(p); + } + + const img = document.createElement('img'); + img.src = (typeof p === 'object' && p.avatar) ? p.avatar : get3DAvatarUrl(name, uuid); + img.className = 'player-avatar'; + img.loading = 'lazy'; + img.onerror = function() { + this.onerror = null; + this.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/64`; + }; + + const info = document.createElement('div'); + info.className = 'player-info'; + + if (prefix) { + const prefixEl = document.createElement('div'); + prefixEl.className = 'player-prefix'; + prefixEl.innerHTML = parseMinecraftColors(prefix); + info.appendChild(prefixEl); + } + + const nameEl = document.createElement('div'); + nameEl.className = 'player-name'; + nameEl.textContent = name; + info.appendChild(nameEl); + + if (serverName) { + const tag = document.createElement('span'); + tag.className = 'player-server-tag'; + tag.textContent = serverName; + info.appendChild(tag); + } + + card.append(img, info); + grid.appendChild(card); + } +} + +// ── Server list status bubbles ── +function updateServerListStatuses() { + const items = $('serversContainer').querySelectorAll('.server-item'); + items.forEach(item => { + const s = servers.find(x => x.id === item.dataset.id); + if (!s || !item._bubble) return; + const st = statuses[s.id]; + if (!st || !st.ok || !st.data || !st.data.online) { + item._bubble.textContent = 'Offline'; + item._bubble.style.background = 'rgba(239,68,68,0.15)'; + item._bubble.style.color = '#f87171'; + } else { + item._bubble.textContent = 'Online'; + item._bubble.style.background = 'rgba(34,197,94,0.15)'; + item._bubble.style.color = '#4ade80'; + } + }); +} + +// ── Actions ── +async function handleAdd() { + const name = $('inputName').value.trim(); + const url = $('inputUrl').value.trim(); + const wpSite = $('inputWpSite').value.trim(); + const wpServerId = $('inputWpServerId').value.trim(); + if (!url && !wpSite) return; + const s = { id: uid(), name: name || url || wpSite, url: url || null, wpSite: wpSite || null, wpServerId: wpServerId || null }; + servers.push(s); + await saveServersToStorage(); + $('inputName').value = $('inputUrl').value = $('inputWpSite').value = $('inputWpServerId').value = ''; + renderServerList(); +} + +async function handleEdit() { + if (!selectedId) return; + const srv = servers.find(s => s.id === selectedId); + if (!srv) return; + const newName = prompt('Name:', srv.name); + if (newName !== null) srv.name = newName.trim(); + await saveServersToStorage(); + renderServerList(); + renderDetail(selectedId); +} + +async function handleDelete() { + if (!selectedId || !confirm('Server wirklich löschen?')) return; + servers = servers.filter(s => s.id !== selectedId); + selectedId = null; + await saveServersToStorage(); + renderServerList(); + renderDetail(null); +} + +async function manualRefresh() { + try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {} +} + +// ── Live updates from background ── +chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && changes.serverStatuses) { + statuses = changes.serverStatuses.newValue || {}; + updateServerListStatuses(); + if (selectedId) { + const srv = servers.find(s => s.id === selectedId); + if (srv) updateDetailForServer(srv); + } + } +});