19 Commits
Author SHA1 Message Date
Git Manager GUI 5eb462e5a7 Upload via Git Manager GUI 2026-07-06 07:40:39 +02:00
Git Manager GUI e5dda9531f Upload via Git Manager GUI 2026-07-06 07:40:37 +02:00
Git Manager GUI b851cbc20b Upload via Git Manager GUI 2026-05-22 11:16:17 +02:00
Git Manager GUI de04e58242 Upload via Git Manager GUI 2026-05-22 11:16:15 +02:00
Git Manager GUI 166bfad1d4 Upload via Git Manager GUI 2026-05-22 11:16:13 +02:00
Git Manager GUI 1de9e4b155 Upload via Git Manager GUI 2026-05-22 11:16:12 +02:00
M_Viper 501be8649a Delete BungeeCord-Chrome/BungeeCord-Chrome.zip via Git Manager GUI 2026-05-22 09:16:02 +00:00
M_Viper 3c80758b12 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-04-02 19:38:17 +00:00
Git Manager GUI f7e0dcb23d Upload via Git Manager GUI 2026-04-02 21:13:38 +02:00
Git Manager GUI 65ea93e22d Upload via Git Manager GUI 2026-04-02 20:57:27 +02:00
M_Viper fde1947ae7 README.md aktualisiert 2026-03-01 11:21:36 +00:00
M_Viper 7556521a0e Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-02-11 19:13:59 +00:00
M_Viper 40c0ed7b0c Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-02-10 22:31:09 +00:00
M_Viper f407604ff6 Upload manifest.json via GUI 2026-02-09 21:06:02 +00:00
M_Viper 2fce65544b Upload file BungeeCord-Chrome.zip via GUI 2026-02-09 22:06:01 +01:00
M_Viper 1c89b4fdfe Upload background.js via GUI 2026-02-09 21:05:53 +00:00
M_Viper 700e6026b6 Upload popup.js via GUI 2026-02-09 21:05:51 +00:00
M_Viper b4ee8b0564 Upload popup.html via GUI 2026-02-09 21:05:50 +00:00
M_Viper 111158967f Upload popup.css via GUI 2026-02-09 21:05:50 +00:00
7 changed files with 1430 additions and 895 deletions
+186 -186
View File
@@ -1,187 +1,187 @@
// background.js
const POLL_INTERVAL_SEC = 2;
// Alarm erstellen
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicRefresh') {
refreshAllServers().catch(console.error);
}
});
// Message Listener
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.cmd === 'refreshNow') {
refreshAllServers()
.then(statuses => sendResponse({ ok: true, statuses }))
.catch(err => sendResponse({ ok: false, error: String(err) }));
return true;
}
});
async function refreshAllServers() {
const obj = await chrome.storage.local.get('servers');
const servers = obj.servers || [];
if (servers.length === 0) {
await chrome.storage.local.set({ serverStatuses: {} });
chrome.action.setBadgeText({ text: '' });
return {};
}
const results = {};
let totalOnlinePlayers = 0;
const tasks = servers.map(async (srv) => {
try {
const data = await fetchServerStatus(srv, 2000);
results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data };
if (data) {
if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length;
else if (typeof data.players === 'number') totalOnlinePlayers += data.players;
}
} catch (e) {
results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) };
}
});
await Promise.all(tasks);
await chrome.storage.local.set({ serverStatuses: results });
const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : '';
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
return results;
}
async function fetchServerStatus(server, timeoutMs = 2000) {
let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server;
let fetchUrl = serverObj.url || '';
// 1. Direkte URL-Abfrage
if (fetchUrl) {
if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl;
try {
const startTime = performance.now();
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal });
clearTimeout(id);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (resp.ok) {
let text = await resp.text();
const lastBrace = text.lastIndexOf('}');
if (lastBrace !== -1) text = text.substring(0, lastBrace + 1);
text = text.trim();
try {
const parsed = JSON.parse(text);
if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) {
parsed.ping = latency; // Ping hinzufügen
return parsed;
}
} catch (e) {
// Parsing fehlgeschlagen → Fallback-Objekt mit Ping
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping
};
}
}
// HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout)
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
// Timeout oder Netzwerkfehler
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// 2. WordPress AJAX-Abfrage
const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null;
const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null;
if (wpSite && wpServerId) {
try {
const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php';
const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId);
const startTime = performance.now();
const controller2 = new AbortController();
const id2 = setTimeout(() => controller2.abort(), timeoutMs);
const resp2 = await fetch(ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
signal: controller2.signal
});
clearTimeout(id2);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status);
const json = await resp2.json();
if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) {
json.ping = latency; // Ping hinzufügen
return json;
}
// JSON nicht im erwarteten Format
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// Keine gültige Konfiguration
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
// background.js
const POLL_INTERVAL_SEC = 2;
// Alarm erstellen
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicRefresh') {
refreshAllServers().catch(console.error);
}
});
// Message Listener
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.cmd === 'refreshNow') {
refreshAllServers()
.then(statuses => sendResponse({ ok: true, statuses }))
.catch(err => sendResponse({ ok: false, error: String(err) }));
return true;
}
});
async function refreshAllServers() {
const obj = await chrome.storage.local.get('servers');
const servers = obj.servers || [];
if (servers.length === 0) {
await chrome.storage.local.set({ serverStatuses: {} });
chrome.action.setBadgeText({ text: '' });
return {};
}
const results = {};
let totalOnlinePlayers = 0;
const tasks = servers.map(async (srv) => {
try {
const data = await fetchServerStatus(srv, 2000);
results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data };
if (data) {
if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length;
else if (typeof data.players === 'number') totalOnlinePlayers += data.players;
}
} catch (e) {
results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) };
}
});
await Promise.all(tasks);
await chrome.storage.local.set({ serverStatuses: results });
const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : '';
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
return results;
}
async function fetchServerStatus(server, timeoutMs = 2000) {
let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server;
let fetchUrl = serverObj.url || '';
// 1. Direkte URL-Abfrage
if (fetchUrl) {
if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl;
try {
const startTime = performance.now();
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal });
clearTimeout(id);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (resp.ok) {
let text = await resp.text();
const lastBrace = text.lastIndexOf('}');
if (lastBrace !== -1) text = text.substring(0, lastBrace + 1);
text = text.trim();
try {
const parsed = JSON.parse(text);
if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) {
parsed.ping = latency; // Ping hinzufügen
return parsed;
}
} catch (e) {
// Parsing fehlgeschlagen → Fallback-Objekt mit Ping
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping
};
}
}
// HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout)
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
// Timeout oder Netzwerkfehler
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// 2. WordPress AJAX-Abfrage
const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null;
const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null;
if (wpSite && wpServerId) {
try {
const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php';
const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId);
const startTime = performance.now();
const controller2 = new AbortController();
const id2 = setTimeout(() => controller2.abort(), timeoutMs);
const resp2 = await fetch(ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
signal: controller2.signal
});
clearTimeout(id2);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status);
const json = await resp2.json();
if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) {
json.ping = latency; // Ping hinzufügen
return json;
}
// JSON nicht im erwarteten Format
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// Keine gültige Konfiguration
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Minecraft Bungee Status",
"description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.",
"version": "1.1.0",
"version": "1.1.1",
"action": {
"default_popup": "popup.html",
"default_icon": {
+435 -220
View File
@@ -1,220 +1,435 @@
:root {
--bg: #111827;
--panel: #0b1220;
--text: #e6eef8;
--muted: #9aa7b3;
--accent: #3b82f6;
--danger: #ef4444;
--radius: 8px;
--online: #22c55e;
--offline: #ef4444;
}
* { box-sizing:border-box; }
html,body { margin:0; padding:0; font-family:Inter, Arial, sans-serif; background:var(--bg); color:var(--text);}
.root { width:520px; max-width:520px; padding:12px; }
header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
header h1 { font-size:16px; margin:0; }
header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; }
.main {
display:flex;
gap:12px;
position: relative;
}
.server-list {
width:45%;
background:var(--panel);
padding:8px;
border-radius:var(--radius);
flex-shrink: 0;
}
.detail {
flex:1;
background:var(--panel);
padding:8px;
border-radius:var(--radius);
min-height:220px;
transition: all 0.3s ease;
}
.detail.full-width {
width: 100%;
flex: 1 1 100%;
}
.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; }
.add-form input { padding:6px 8px; border-radius:6px; border:1px solid #21303b; background:#071019; color:var(--text); }
.add-form button { padding:6px 8px; border-radius:6px; border:none; background:var(--accent); color:white; cursor:pointer; }
#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; }
.server-item {
display:flex;
align-items:center;
justify-content:space-between;
padding:6px;
margin-bottom:6px;
border-radius:6px;
cursor:pointer;
background: rgba(255,255,255,0.02);
transition: background 0.2s ease;
}
.server-item:hover { background: rgba(255,255,255,0.05); }
.server-item .meta { display:flex; flex-direction:column; }
.server-item .meta .name { font-weight:600; }
.server-item .meta .url { font-size:12px; color:var(--muted); }
.server-item .status-bubble {
font-size:12px;
padding:3px 6px;
border-radius:999px;
color:#fff;
transition: background-color 0.3s ease;
}
.placeholder { color:var(--muted); padding:12px; }
.hidden { display:none; }
/* --- Detail Header (Zeile 1) --- */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.server-identity h2 {
margin: 0;
font-size: 18px;
line-height: 1.2;
}
.server-url {
display: block;
font-size: 12px;
color: var(--muted);
margin-top: 2px;
}
.status-wrapper {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
/* --- Pulsierender Punkt Animation --- */
.pulsing-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--offline); /* Standard Offline Rot */
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
animation: pulse-red 2s infinite;
}
.pulsing-dot.online {
background-color: var(--online); /* Online Grün */
animation: pulse-green 2s infinite;
}
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
/* --- Detail Stats (Zeile 2) --- */
.detail-stats {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.05);
padding: 8px;
border-radius: var(--radius);
margin-bottom: 16px;
font-size: 13px;
}
/* ---------------------------------- */
.playerList {
list-style:none;
padding-left:16px;
max-height:140px;
overflow:auto;
margin:6px 0;
display:flex;
flex-wrap:wrap;
gap:8px;
}
.playerList li {
display:flex;
align-items:center;
gap:4px;
font-size:13px;
color:var(--text);
}
.player-avatar {
width:24px;
height:24px;
border-radius:4px;
transition: transform 0.2s ease;
}
.player-avatar:hover { transform: scale(1.1); }
.detailButtons {
display:flex;
gap:8px;
margin-top:8px;
transition: opacity 0.3s ease;
}
.detailButtons button {
padding:6px 8px;
border-radius:6px;
border:none;
cursor:pointer;
transition: opacity 0.2s ease, transform 0.1s ease;
}
.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); }
.detailButtons button:active { transform: translateY(0); }
#btnEdit { background: #f59e0b; color:#000; }
#btnDelete { background: var(--danger); color:#fff; }
#serversContainer::-webkit-scrollbar,
.playerList::-webkit-scrollbar {
width: 6px;
}
#serversContainer::-webkit-scrollbar-track,
.playerList::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
border-radius: 3px;
}
#serversContainer::-webkit-scrollbar-thumb,
.playerList::-webkit-scrollbar-thumb {
background: var(--muted);
border-radius: 3px;
}
#serversContainer::-webkit-scrollbar-thumb:hover,
.playerList::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
:root {
--bg: #0d1117;
--panel: #161b27;
--panel2: #111620;
--text: #e6eef8;
--muted: #5a6a7a;
--muted2: #8899aa;
--accent: #3b82f6;
--radius: 9px;
--online: #22c55e;
--offline: #ef4444;
--border: rgba(255,255,255,0.07);
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
font-family: Inter, Arial, sans-serif;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
}
.root {
width: 560px;
display: flex;
flex-direction: column;
}
/* ── Header ── */
header {
background: var(--panel);
display: flex;
align-items: center;
justify-content: space-between;
padding: 13px 20px;
border-bottom: 1px solid var(--border);
}
header h1 {
font-size: 14px;
font-weight: 700;
margin: 0;
letter-spacing: 0.03em;
color: #fff;
}
header .actions { display: flex; gap: 7px; }
header .actions button {
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.1);
color: var(--muted2);
border-radius: 7px;
width: 32px; height: 32px;
cursor: pointer;
font-size: 16px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
}
header .actions button:hover {
background: rgba(255,255,255,0.12);
color: #fff;
}
/* ── Main layout ── */
.main {
display: flex;
flex-direction: column;
}
/* ── Settings / add-form panel ── */
.settings-panel {
background: var(--panel2);
border-bottom: 1px solid var(--border);
padding: 14px 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.settings-panel input {
padding: 8px 12px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,0.09);
background: rgba(0,0,0,0.3);
color: var(--text);
font-size: 13px;
outline: none;
}
.settings-panel input:focus {
border-color: var(--accent);
}
.settings-panel button {
padding: 8px 12px;
border-radius: 7px;
border: none;
background: var(--accent);
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
/* ── Server list ── */
.server-list-panel {
background: var(--panel2);
border-bottom: 1px solid var(--border);
padding: 10px 12px;
}
#serversContainer {
list-style: none;
margin: 0; padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.server-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 12px;
border-radius: 8px;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.server-item:hover { background: rgba(255,255,255,0.04); }
.server-item.selected { background: rgba(59,130,246,0.08); border-color: rgba(59,130,246,0.25); }
.server-item .meta { display: flex; flex-direction: column; gap: 2px; }
.server-item .meta .name { font-size: 13px; font-weight: 600; color: #fff; }
.server-item .meta .url { font-size: 11px; color: var(--muted); }
.status-bubble {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 999px;
color: #fff;
background: transparent;
}
/* ── Detail panel ── */
.detail-panel {
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 200px;
}
.placeholder {
color: var(--muted);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
/* Server identity + online badge */
.srv-identity {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.srv-name { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 4px; }
.srv-url { font-size: 12px; color: var(--muted); }
.online-badge {
display: flex;
align-items: center;
gap: 7px;
border-radius: 999px;
padding: 5px 14px;
font-size: 13px;
font-weight: 600;
}
.online-badge.online {
background: rgba(34,197,94,0.12);
border: 1px solid rgba(34,197,94,0.25);
color: #4ade80;
}
.online-badge.offline {
background: rgba(239,68,68,0.1);
border: 1px solid rgba(239,68,68,0.2);
color: #f87171;
}
.pulsing-dot {
width: 7px; height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.pulsing-dot.online {
background: var(--online);
animation: pulse-green 2s infinite;
}
.pulsing-dot.offline {
background: var(--offline);
animation: pulse-red 2s infinite;
}
@keyframes pulse-green {
0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
50% { box-shadow: 0 0 0 5px rgba(34,197,94,0); }
}
@keyframes pulse-red {
0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
50% { box-shadow: 0 0 0 5px rgba(239,68,68,0); }
}
/* MOTD */
.motd-bar {
font-size: 12px;
color: var(--muted2);
background: rgba(255,255,255,0.03);
border-radius: 8px;
padding: 8px 14px;
border-left: 2px solid rgba(59,130,246,0.35);
font-style: italic;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px;
}
/* Stats row – 3 gleich breite Karten */
.stats-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 4px;
}
.stat-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 16px;
}
.stat-label {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 5px;
}
.stat-val {
font-size: 18px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-val.small {
font-size: 12px;
padding-top: 4px;
font-weight: 500;
color: var(--muted2);
}
/* Section label */
.section-label {
font-size: 11px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 9px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
/* Sub-server chips – immer 3 pro Zeile */
.sub-servers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.sub-chip {
display: flex;
align-items: center;
gap: 7px;
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted2);
cursor: default;
transition: background 0.15s;
min-width: 0;
}
.sub-chip.active {
background: rgba(59,130,246,0.1);
border-color: rgba(59,130,246,0.3);
color: #93c5fd;
}
.sub-chip-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #2a3040;
flex-shrink: 0;
}
.sub-chip.active .sub-chip-dot {
background: var(--accent);
}
.sub-chip-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.sub-chip-count {
font-size: 11px;
font-weight: 500;
opacity: 0.65;
flex-shrink: 0;
margin-left: auto;
padding-left: 4px;
}
/* Players grid – 2 Spalten */
.players-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.player-card {
display: flex;
align-items: center;
gap: 12px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 14px;
transition: background 0.15s;
min-width: 0;
}
.player-card:hover {
background: rgba(255,255,255,0.05);
}
.player-avatar {
width: 40px; height: 40px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,0.1);
flex-shrink: 0;
image-rendering: pixelated;
}
.player-info { flex: 1; min-width: 0; }
.player-name {
font-size: 14px;
font-weight: 700;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.player-prefix {
font-size: 11px;
color: var(--muted2);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.player-server-tag {
display: inline-block;
margin-top: 3px;
font-size: 11px;
font-weight: 600;
background: rgba(59,130,246,0.18);
color: #93c5fd;
border-radius: 5px;
padding: 2px 7px;
}
/* Edit/Delete buttons */
.detail-buttons {
display: flex;
gap: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.detail-buttons button {
padding: 7px 16px;
border-radius: 7px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
#btnEdit { background: rgba(245,158,11,0.15); color: #fbbf24; border: 1px solid rgba(245,158,11,0.25); }
#btnDelete { background: rgba(239,68,68,0.12); color: #f87171; border: 1px solid rgba(239,68,68,0.2); }
.hidden { display: none !important; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
+96 -67
View File
@@ -1,68 +1,97 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="root">
<header>
<h1>Bungee Status</h1>
<div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren">↻</button>
<button id="btnToggleSettings">⚙</button>
</div>
</header>
<section class="main">
<aside class="server-list">
<div class="add-form" id="settingsForm">
<input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (127.0.0.1:9191)" />
<input id="inputWpSite" placeholder="WP Site" />
<input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
</div>
<ul id="serversContainer"></ul>
</aside>
<div class="detail">
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
<div id="detailContent" class="detailContent hidden">
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
<div class="detail-header">
<div class="server-identity">
<h2 id="detailName"></h2>
<span id="detailUrlText" class="server-url"></span>
</div>
<div class="status-wrapper">
<div id="detailPulse" class="pulsing-dot"></div>
<span id="detailStatus"></span>
</div>
</div>
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping -->
<div class="detail-stats">
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
<div><strong>Ping:</strong> <span id="detailPing">-</span></div>
</div>
<h3>Online Spieler</h3>
<ul id="detailPlayerList" class="playerList"></ul>
<div class="detailButtons">
<button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button>
</div>
</div>
</div>
</section>
</div>
<script src="popup.js"></script>
</body>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="root">
<header>
<h1>Bungee Status</h1>
<div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren">↻</button>
<button id="btnToggleSettings" title="Einstellungen">⚙</button>
</div>
</header>
<div class="main">
<div id="settingsForm" class="settings-panel hidden">
<h3 id="settingsTitle">Neuen Server hinzufügen</h3>
<input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (z. B. 192.168.1.1:9191)" />
<input id="inputWpSite" placeholder="WP Site (z. B. https://meine-seite.de)" />
<input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
</div>
<div id="serverListPanel" class="server-list-panel hidden">
<ul id="serversContainer"></ul>
</div>
<div class="detail-panel">
<div id="noSelection" class="placeholder">Noch kein Server ausgewählt.</div>
<div id="detailContent" class="hidden">
<!-- Server Name + Status Badge -->
<div class="srv-identity">
<div>
<div class="srv-name" id="detailName"></div>
<div class="srv-url" id="detailUrlText"></div>
</div>
<div class="online-badge offline" id="statusBadge">
<div class="pulsing-dot offline" id="detailPulse"></div>
<span id="detailStatus">Offline</span>
</div>
</div>
<!-- MOTD -->
<div id="motdBar" class="motd-bar hidden"></div>
<!-- Stats: Spieler / Version / Ping -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">Spieler</div>
<div class="stat-val" id="detailPlayers">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Version</div>
<div class="stat-val small" id="detailVersion">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Ping</div>
<div class="stat-val" id="detailPing">-</div>
</div>
</div>
<!-- Sub-Server -->
<div id="backendSection" class="hidden">
<div class="section-label">Sub-Server</div>
<div class="sub-servers" id="backendServersList"></div>
</div>
<!-- Online Spieler -->
<div>
<div class="section-label">Online Spieler</div>
<div class="players-grid" id="detailPlayerList"></div>
</div>
<!-- Edit / Delete -->
<div class="detail-buttons hidden" id="detailButtons">
<button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button>
</div>
</div>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
+312 -266
View File
@@ -1,377 +1,423 @@
// 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 previousStatuses = {};
let settingsVisible = false;
let editingId = null; // Welcher Server gerade bearbeitet wird
document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd);
btnRefresh.addEventListener('click', manualRefresh);
btnToggleSettings.addEventListener('click', toggleSettings);
btnEdit.addEventListener('click', handleEdit);
btnDelete.addEventListener('click', handleDelete);
$('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);
adjustDetailLayout();
}
// --- Settings Visibility ---
// ── 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 += `<span style="color:${map[code]}">`;
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 !== undefined ? obj.settingsVisible : false;
if (!settingsVisible) settingsForm.classList.add('hidden');
else settingsForm.classList.remove('hidden');
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;
settingsForm.classList.toggle('hidden');
applySettingsVisibility();
await saveSettingsVisibility();
adjustDetailLayout();
}
// --- Storage ---
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 saveServersToStorage() {
await chrome.storage.local.set({ servers });
}
async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {};
}
// --- Layout ---
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';
}
// --- Render Server List ---
// ── Server list ──
function renderServerList() {
serversContainer.innerHTML = '';
const container = $('serversContainer');
container.innerHTML = '';
if (servers.length === 0) {
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
container.innerHTML = '<div class="placeholder" style="padding:8px 0;">Noch kein Server hinzugefügt.</div>';
return;
}
for (const s of servers) {
const item = document.createElement('li');
item.className = 'server-item';
item.dataset.id = s.id;
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 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;
const nameEl = document.createElement('div');
nameEl.className = 'name';
nameEl.textContent = s.name || '(Kein Name)';
item.append(meta, statusBubble);
item.addEventListener('click', () => {
selectedId = s.id;
renderDetail(selectedId);
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);
});
serversContainer.appendChild(item);
container.appendChild(li);
}
updateServerListStatuses(false);
updateServerListStatuses();
}
// --- Render Detail ---
// ── Detail ──
function renderDetail(id) {
if (!id) {
noSelection.classList.remove('hidden');
detailContent.classList.add('hidden');
return;
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;
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;
$('noSelection').classList.add('hidden');
$('detailContent').classList.remove('hidden');
updateDetailForServer(srv, true);
$('detailName').textContent = srv.name || 'Unbenannter Server';
let urlToShow = srv.url || (srv.wpSite || 'Lokal');
urlToShow = urlToShow.replace(':9191', '');
$('detailUrlText').textContent = urlToShow;
updateDetailForServer(srv, true);
}
// --- Update Detail ---
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);
const changed = force || JSON.stringify(st) !== JSON.stringify(prevSt);
if (!changed) return;
const badge = $('statusBadge');
const pulse = $('detailPulse');
if (!st || !st.ok || !st.data) {
if (statusChanged) {
detailStatus.textContent = 'Offline';
detailPulse.classList.remove('online');
detailVersion.textContent = '-';
detailPlayers.textContent = '-';
detailPing.textContent = '-';
updatePlayerList([]);
}
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;
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 isOnline = !!d.online;
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}`;
}
badge.className = 'online-badge ' + (isOnline ? 'online' : 'offline');
pulse.className = 'pulsing-dot ' + (isOnline ? 'online' : 'offline');
$('detailStatus').textContent = isOnline ? 'Online' : 'Offline';
$('detailVersion').textContent = d.version || 'unknown';
if (force || detailPlayers.textContent !== newPlayersText) {
detailPlayers.textContent = newPlayersText;
}
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);
let pingVal = d.ping || d.latency || '-';
if (pingVal !== '-' && typeof pingVal === 'number') {
pingVal = pingVal + ' ms';
}
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) {
detailPing.textContent = pingVal;
const ping = d.ping || d.latency;
$('detailPing').textContent = typeof ping === 'number' ? ping + ' ms' : '-';
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 : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
let playersChanged = false;
if (force) {
playersChanged = true;
} else {
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
}
if (playersChanged) {
updatePlayerList(currentPlayers);
}
updatePlayerList(currentPlayers);
updateBackendServers(d.network?.backend_servers || null);
}
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
}
// --- Spielerliste ---
function updatePlayerList(players) {
detailPlayerList.innerHTML = '';
if (!players || players.length === 0) {
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
// ── Sub-Server chips ──
function updateBackendServers(list) {
const section = $('backendSection');
const container = $('backendServersList');
if (!list || list.length === 0) {
section.classList.add('hidden');
container.innerHTML = '';
return;
}
for (const p of players) {
const li = document.createElement('li');
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p);
if (typeof p === 'object' && p.avatar) {
const img = document.createElement('img');
img.src = p.avatar;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
} else {
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net
const img = document.createElement('img');
img.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/32`;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
}
detailPlayerList.appendChild(li);
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);
}
}
// --- Update Server List Statuses ---
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];
const prevSt = previousStatuses[s.id];
const statusChanged = !prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
if (!statusChanged) return;
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)';
// ── Player list ──
function updatePlayerList(players) {
const grid = $('detailPlayerList');
grid.innerHTML = '';
if (!players || players.length === 0) {
grid.innerHTML = '<div class="placeholder">Keine Spieler online.</div>';
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 || '';
serverName = p.server || p.current_server || p.connected_server || null;
} else {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
name = String(p);
}
previousStatuses[s.id] = st ? JSON.parse(JSON.stringify(st)) : null;
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';
}
});
}
// --- Add / Edit / Delete ---
// ── Edit / Add Mode ──
function enterEditMode(server) {
editingId = server.id;
$('settingsTitle').textContent = 'Server bearbeiten';
$('btnAddServer').textContent = 'Änderungen speichern';
$('inputName').value = server.name || '';
$('inputUrl').value = server.url || '';
$('inputWpSite').value = server.wpSite || '';
$('inputWpServerId').value = server.wpServerId || '';
}
function exitEditMode() {
editingId = null;
$('settingsTitle').textContent = 'Neuen Server hinzufügen';
$('btnAddServer').textContent = 'Hinzufügen';
$('inputName').value = '';
$('inputUrl').value = '';
$('inputWpSite').value = '';
$('inputWpServerId').value = '';
}
// ── 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) {
alert('Bitte URL oder WP Site angeben');
return;
const name = $('inputName').value.trim();
const url = $('inputUrl').value.trim();
const wpSite = $('inputWpSite').value.trim();
const wpServerId = $('inputWpServerId').value.trim();
if (!url && !wpSite) {
alert('Bitte eine URL oder WordPress-Einstellungen angeben.');
return;
}
const s = {
id: uid(),
name: name || url || wpSite,
url: url || null,
wpSite: wpSite || null,
wpServerId: wpServerId || null
};
servers.push(s);
if (editingId) {
// === BEARBEITEN ===
const srv = servers.find(s => s.id === editingId);
if (srv) {
srv.name = name || url || wpSite;
if (url) srv.url = url;
if (wpSite) srv.wpSite = wpSite;
if (wpServerId) srv.wpServerId = wpServerId;
}
exitEditMode();
} else {
// === NEU HINZUFÜGEN ===
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();
if (selectedId) renderDetail(selectedId);
}
async function handleEdit() {
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name;
const newUrl = prompt('URL:', srv.url || '') || srv.url;
const newWpSite = prompt('WP Site:', srv.wpSite || '') || srv.wpSite;
const newWpServerId = prompt('WP Server ID:', srv.wpServerId || '') || srv.wpServerId;
srv.name = newName.trim();
srv.url = newUrl.trim();
srv.wpSite = newWpSite.trim();
srv.wpServerId = newWpServerId.trim();
await saveServersToStorage();
renderServerList();
renderDetail(selectedId);
settingsVisible = true;
applySettingsVisibility();
await saveSettingsVisibility();
enterEditMode(srv);
}
async function handleDelete() {
if (!selectedId) return;
if (!confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
if (!selectedId || !confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
selectedId = null;
await saveServersToStorage();
await saveServersToStorage();
renderServerList();
renderDetail(null);
exitEditMode();
}
async function manualRefresh() {
try {
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
} catch(e) {
console.error('Refresh fehlgeschlagen:', e);
}
async function manualRefresh() {
try { chrome.runtime.sendMessage({ cmd: 'refreshNow' }); } catch(e) {}
}
// --- Storage Listener ---
// ── Live updates from background ──
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {};
@@ -1,12 +1,21 @@
<?php
/*
* Plugin Name: Minecraft BungeeCord Status – Network Edition
* Description: Der ultimative Live-Status für dein BungeeCord Netzwerk (Border None Fix).
* Tags: minecraft, bungeecord, server status, player list
* Version: 3.6.3
* Author: M_Viper
* Requires at least: 6.0
* Requires PHP: 7.4
Plugin Name: Minecraft BungeeCord Status – Network Edition
Plugin URI:https://git.viper.ipv64.net/M_Viper/Minecraft-BungeeCord-Status
Description: Der ultimative Live-Status für dein BungeeCord Netzwerk.
Version: 3.6.6
Author: M_Viper
Author URI: https://m-viper.de
Requires at least: 6.0
Requires PHP: 7.4
Tested up to: 6.8
PHP Version: 7.4
License: GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: minecraft-bungeecord-status
Tags: minecraft, bungeecord, server status, player list
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
Support: [Telegram Support](https://t.me/M_Viper04)
*/
if (!defined('ABSPATH')) exit;
@@ -76,144 +85,116 @@ function mcss_get_3d_avatar($name, $uuid = null) {
}
}
/* ---------------- AUTO UPDATE (MCSS) ---------------- */
if ( ! class_exists( 'MCSS_Auto_Update' ) ) {
class MCSS_Auto_Update {
private $plugin_file;
private $repo_owner = 'M_Viper';
private $repo_name = 'Minecraft-BungeeCord-Status';
private $api_url;
private $transient_key;
// ===============================
// MCSS - UPDATE NOTICE SYSTEM
// ===============================
public function __construct( $plugin_file ) {
$this->plugin_file = $plugin_file;
$this->api_url = 'https://git.viper.ipv64.net/api/v1/repos/' . rawurlencode( $this->repo_owner ) . '/' . rawurlencode( $this->repo_name ) . '/releases';
$this->transient_key = 'mcss_update_check_' . md5( $this->repo_owner . '/' . $this->repo_name );
// Plugin-Version aus Header lesen
function mcss_get_plugin_version() {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_admin() ) {
add_action( 'admin_init', array( $this, 'check_for_update' ) );
$plugin_data = get_plugin_data( __FILE__ );
return $plugin_data['Version'] ?? '0.0.0';
}
// Cache manuell leeren
function mcss_clear_update_cache() {
if ( isset($_GET['mcss_clear_cache']) && current_user_can('manage_options') ) {
check_admin_referer('mcss_clear_cache_action');
delete_transient('mcss_latest_release');
wp_redirect( admin_url('plugins.php') );
exit;
}
}
add_action('admin_init', 'mcss_clear_update_cache');
// Neueste Release-Infos von Gitea holen
function mcss_get_latest_release_info( $force_refresh = false ) {
$transient_key = 'mcss_latest_release';
if ( $force_refresh ) {
delete_transient( $transient_key );
}
$release_info = get_transient( $transient_key );
if ( false === $release_info ) {
$response = wp_remote_get(
'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases/latest',
['timeout' => 10]
);
if ( ! is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response) ) {
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if ( $data && isset($data['tag_name']) ) {
$tag = ltrim( $data['tag_name'], 'vV' );
$release_info = [
'version' => $tag,
'download_url' => $data['zipball_url'] ?? '',
'notes' => $data['body'] ?? '',
'published_at' => $data['published_at'] ?? '',
];
set_transient( $transient_key, $release_info, 6 * HOUR_IN_SECONDS );
} else {
set_transient( $transient_key, [], HOUR_IN_SECONDS );
}
}
public function check_for_update() {
if ( ! function_exists( 'get_file_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$current = get_file_data( $this->plugin_file, array( 'Version' => 'Version' ), 'plugin' );
$current_version = isset( $current['Version'] ) ? $current['Version'] : '0.0.0';
$latest = $this->get_latest_release_info();
if ( $latest && ! empty( $latest['version'] ) && ! empty( $latest['url'] ) ) {
if ( version_compare( $latest['version'], $current_version, '>' ) ) {
add_action( 'admin_notices', function() use ( $latest, $current_version ) {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>Minecraft BungeeCord Status – Update verfügbar</strong><br>
Neue Version: <strong><?php echo esc_html( $latest['version'] ); ?></strong><br>
Installiert: <strong><?php echo esc_html( $current_version ); ?></strong><br>
<a href="<?php echo esc_url( $latest['url'] ); ?>" class="button button-primary" target="_blank" rel="noreferrer noopener">Direkter Download (ZIP)</a>
<a href="<?php echo esc_url( 'https://git.viper.ipv64.net/' . rawurlencode( $this->repo_owner ) . '/' . rawurlencode( $this->repo_name ) . '/releases' ); ?>" class="button" target="_blank" style="margin-left:8px;" rel="noreferrer noopener">Releases ansehen</a>
</p>
</div>
<?php
} );
}
}
}
private function get_latest_release_info() {
$cached = get_transient( $this->transient_key );
if ( false !== $cached && is_array( $cached ) && ! empty( $cached['version'] ) ) {
return $cached;
}
$result = false;
$response = wp_remote_get( $this->api_url, array(
'timeout' => 8,
'headers' => array(
'Accept' => 'application/json',
'User-Agent' => 'MCSS-Update-Checker/1.0',
),
) );
if ( is_wp_error( $response ) ) {
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), HOUR_IN_SECONDS );
return false;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== (int) $code ) {
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), HOUR_IN_SECONDS );
return false;
}
$body = wp_remote_retrieve_body( $response );
$json = json_decode( $body, true );
if ( ! is_array( $json ) || empty( $json ) ) {
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), HOUR_IN_SECONDS );
return false;
}
foreach ( $json as $release ) {
$tag = '';
if ( ! empty( $release['tag_name'] ) ) {
$tag = ltrim( (string) $release['tag_name'], 'vV' );
} elseif ( ! empty( $release['name'] ) ) {
$tag = ltrim( (string) $release['name'], 'vV' );
}
if ( ! empty( $release['assets'] ) && is_array( $release['assets'] ) ) {
foreach ( $release['assets'] as $asset ) {
if ( empty( $asset['name'] ) || empty( $asset['browser_download_url'] ) ) {
continue;
}
if ( strtolower( $asset['name'] ) === 'minecraft-bungeecord-status.zip' ) {
$version = $tag ?: $this->extract_version_from_string( $asset['name'] . ' ' . ( $release['name'] ?? '' ) );
if ( $version ) {
$result = array(
'version' => $version,
'url' => $asset['browser_download_url'],
);
break 2;
} else {
$result = array(
'version' => ( $tag ?: (string) ( $release['name'] ?? '' ) ),
'url' => $asset['browser_download_url'],
);
break 2;
}
}
}
}
}
if ( $result ) {
set_transient( $this->transient_key, $result, 12 * HOUR_IN_SECONDS );
return $result;
}
set_transient( $this->transient_key, array( 'version' => '', 'url' => '' ), 6 * HOUR_IN_SECONDS );
return false;
}
private function extract_version_from_string( $str ) {
if ( preg_match( '/([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $str, $m ) ) {
return $m[1];
}
return '';
} else {
set_transient( $transient_key, [], HOUR_IN_SECONDS );
}
}
new MCSS_Auto_Update( __FILE__ );
return $release_info;
}
// Admin-Update-Hinweis anzeigen
function mcss_show_update_notice() {
if ( ! current_user_can('manage_options') ) {
return;
}
$current_version = mcss_get_plugin_version();
$latest_release = mcss_get_latest_release_info();
if ( ! empty($latest_release['version']) && version_compare($current_version, $latest_release['version'], '<') ) {
$refresh_url = wp_nonce_url(
admin_url('plugins.php?mcss_clear_cache=1'),
'mcss_clear_cache_action'
);
?>
<div class="notice notice-warning is-dismissible">
<h3>Minecraft BungeeCord Status – Update verfügbar</h3>
<p>
Installiert: <strong><?php echo esc_html($current_version); ?></strong><br>
Neueste Version: <strong><?php echo esc_html($latest_release['version']); ?></strong>
</p>
<p>
<a href="<?php echo esc_url($latest_release['download_url']); ?>" class="button button-primary" target="_blank">
Update herunterladen
</a>
<a href="https://git.viper.ipv64.net/M_Viper/Minecraft-BungeeCord-Status/releases" class="button" target="_blank">
Release Notes
</a>
<a href="<?php echo esc_url($refresh_url); ?>" class="button">
Jetzt neu prüfen
</a>
</p>
</div>
<?php
}
}
add_action('admin_notices', 'mcss_show_update_notice');
/* ---------------- Assets ---------------- */
add_action('admin_enqueue_scripts', function($hook){
global $pagenow;
@@ -516,25 +497,89 @@ function mcss_render_row($srv, $i, $font_sizes, $announcement_types) {
}
/* ---------------- LOGIC: STATUS API FETCHER ---------------- */
function mcss_fetch_data_via_api($host, $port, $timeout = 2) {
function mcss_socket_http_json($host, $port, $path = '/', $timeout = 2.0) {
$socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
if (!$socket) return false;
if (!$socket) {
return false;
}
$out = "GET / HTTP/1.1\r\n";
stream_set_timeout($socket, (int)ceil($timeout));
$out = "GET " . $path . " HTTP/1.1\r\n";
$out .= "Host: " . $host . "\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($socket, $out);
$out .= "User-Agent: MCSS-WordPress/3.6.6\r\n";
$out .= "Accept: application/json\r\n";
$out .= "Connection: close\r\n\r\n";
if (@fwrite($socket, $out) === false) {
@fclose($socket);
return false;
}
$response = '';
while (!feof($socket)) {
$response .= fgets($socket, 128);
$chunk = @fread($socket, 2048);
if ($chunk === false) {
break;
}
if ($chunk === '') {
$meta = stream_get_meta_data($socket);
if (!empty($meta['timed_out'])) {
break;
}
usleep(10000);
continue;
}
$response .= $chunk;
}
fclose($socket);
$parts = explode("\r\n\r\n", $response);
$body = end($parts);
$data = json_decode($body, true);
return $data;
$meta = stream_get_meta_data($socket);
@fclose($socket);
if (!empty($meta['timed_out'])) {
return false;
}
$parts = explode("\r\n\r\n", $response, 2);
if (count($parts) !== 2) {
return false;
}
$statusLine = strtok($parts[0], "\r\n");
if (!is_string($statusLine) || stripos($statusLine, '200') === false) {
return false;
}
$data = json_decode($parts[1], true);
return is_array($data) ? $data : false;
}
function mcss_fetch_data_via_api($host, $port, $timeout = 2) {
// Primärer Endpunkt mit Detaildaten
$full = mcss_socket_http_json($host, $port, '/', $timeout);
if (is_array($full) && isset($full['online'])) {
return $full;
}
// Fallback auf Health-Endpunkt (StatusAPI >= hardened build)
$health = mcss_socket_http_json($host, $port, '/health', $timeout);
if (is_array($health) && !empty($health['online'])) {
return [
'online' => true,
'players' => [],
'version' => 'BungeeCord',
'ping' => 1,
'motd' => 'StatusAPI erreichbar (Health)',
];
}
// Ein zweiter kurzer Versuch reduziert False Negatives bei Paketverlust.
$retry = mcss_socket_http_json($host, $port, '/', max(1.0, $timeout));
if (is_array($retry) && isset($retry['online'])) {
return $retry;
}
return false;
}
function mcss_parse_bungeecord_version($raw_version) {
@@ -545,16 +590,26 @@ function mcss_parse_bungeecord_version($raw_version) {
}
function mcss_fetch_server_with_ranks($srv) {
$cache_key = 'mcss_data_' . md5($srv['host']);
$host = $srv['host'];
$port = $srv['player_port'] ?? 9191;
$cache_key = 'mcss_data_' . md5($host . ':' . $port);
$last_good_key = 'mcss_last_good_' . md5($host . ':' . $port);
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
$host = $srv['host'];
$port = $srv['player_port'] ?? 9191;
$api_data = mcss_fetch_data_via_api($host, $port, 1);
$api_data = mcss_fetch_data_via_api($host, $port, 2);
if (!$api_data || !isset($api_data['online'])) {
$last_good = get_transient($last_good_key);
if (is_array($last_good) && !empty($last_good['online'])) {
$last_good['stale'] = true;
$last_good['stale_reason'] = 'temporary_connection_loss';
set_transient($cache_key, $last_good, 5);
return $last_good;
}
return ['online'=>false,'players'=>[],'version'=>'Offline','ping'=>9999,'motd'=>'Verbindung fehlgeschlagen'];
}
@@ -602,7 +657,7 @@ function mcss_fetch_server_with_ranks($srv) {
$clean_version = mcss_parse_bungeecord_version($api_data['version'] ?? 'BungeeCord');
$result = [
'online' => true,
'online' => !empty($api_data['online']),
'players' => $players_info,
'version' => $clean_version,
'ping' => 1,
@@ -611,6 +666,9 @@ function mcss_fetch_server_with_ranks($srv) {
$fast_cache_ttl = max(2, absint($srv['cache_ttl'] ?? 10));
set_transient($cache_key, $result, $fast_cache_ttl);
if (!empty($result['online'])) {
set_transient($last_good_key, $result, 180);
}
return $result;
}
@@ -897,4 +955,185 @@ function mcss_shortcode($atts) {
<?php return ob_get_clean();
}
}
/* ---------------- SIDEBAR WIDGET: MODERN PILL BADGE (ROBUST INLINE STYLES) ---------------- */
class MCSS_Sidebar_Status_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'mcss_sidebar_status', // Basis ID
'MC Server Status (Simple)', // Name
array( 'description' => 'Zeigt einen modernen Online/Offline Status in der Sidebar an.' ) // Args
);
}
// Backend: Formular im Widget-Bereich
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : 'Server Status';
$server_id = ! empty( $instance['server_id'] ) ? $instance['server_id'] : '';
// Server-Liste holen
$servers = get_option('mcss_servers', []);
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">Titel:</label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id( 'server_id' ); ?>">Server wählen:</label>
<select class="widefat" id="<?php echo $this->get_field_id( 'server_id' ); ?>" name="<?php echo $this->get_field_name( 'server_id' ); ?>">
<option value="">-- Bitte wählen --</option>
<?php foreach ($servers as $srv): ?>
<option value="<?php echo esc_attr($srv['id'] ?? ''); ?>" <?php selected($server_id, $srv['id'] ?? ''); ?>>
<?php echo esc_html($srv['name'] ?? 'Unbenannt'); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<?php
}
// Backend: Speichern
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['server_id'] = ( ! empty( $new_instance['server_id'] ) ) ? sanitize_text_field( $new_instance['server_id'] ) : '';
return $instance;
}
// Frontend: Ausgabe
public function widget( $args, $instance ) {
echo $args['before_widget'];
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
}
$server_id = $instance['server_id'];
$servers = get_option('mcss_servers', []);
$target_srv = null;
// Server suchen
foreach ($servers as $srv) {
if (($srv['id'] ?? '') === $server_id) { $target_srv = $srv; break; }
}
if ($target_srv) {
// Initiale Daten holen
$data = mcss_fetch_server_with_ranks($target_srv);
$is_online = $data['online'] ?? false;
$uid = md5($target_srv['host'] . 'widget'); // Einzigartige ID für JS
// Farben definieren
$bg_online = '#ecfdf5';
$bg_offline = '#fef2f2';
$text_online = '#059669';
$text_offline = '#dc2626';
$dot_online = '#10b981';
$dot_offline = '#ef4444';
$border_online = 'rgba(16, 185, 129, 0.2)';
$border_offline = 'rgba(239, 68, 68, 0.2)';
$anim_name_online = 'mcss-pulse-modern';
$anim_name_offline = 'mcss-pulse-modern-red';
// Werte für den Initialzustand setzen
$current_bg = $is_online ? $bg_online : $bg_offline;
$current_text = $is_online ? $text_online : $text_offline;
$current_dot = $is_online ? $dot_online : $dot_offline;
$current_border = $is_online ? $border_online : $border_offline;
$current_anim = $is_online ? $anim_name_online : $anim_name_offline;
$status_label = $is_online ? 'Online' : 'Offline';
// Inline Animation Definitionen für Robustheit
?>
<style>
@keyframes mcss-pulse-modern {
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
@keyframes mcss-pulse-modern-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
</style>
<!-- Wrapper mit PUREN Inline Styles -->
<div id="mcss-sw-wrapper-<?php echo esc_attr($uid); ?>"
style="display: flex; align-items: center; justify-content: center; width: 93%; padding: 8px; border-radius: 50px; background-color: <?php echo $current_bg; ?>; border: 1px solid <?php echo $current_border; ?>; transition: all 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; cursor: default; margin: 10px 0;">
<span id="mcss-sw-dot-<?php echo esc_attr($uid); ?>"
style="width: 8px; height: 8px; border-radius: 50%; margin-right: 10px; background-color: <?php echo $current_dot; ?>; animation: <?php echo $current_anim; ?> 2s infinite;"></span>
<span id="mcss-sw-text-<?php echo esc_attr($uid); ?>"
style="font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: <?php echo $current_text; ?>;">
<?php echo $status_label; ?>
</span>
</div>
<!-- Kleines Skript für Live-Update (alle 3 Sekunden) -->
<script>
(function(){
var uid = "<?php echo esc_js($uid); ?>";
var serverId = "<?php echo esc_js($target_srv['id']); ?>";
// Konstanten Farben
var cOnlineBg = '<?php echo esc_js($bg_online); ?>';
var cOfflineBg = '<?php echo esc_js($bg_offline); ?>';
var cOnlineText = '<?php echo esc_js($text_online); ?>';
var cOfflineText = '<?php echo esc_js($text_offline); ?>';
var cOnlineDot = '<?php echo esc_js($dot_online); ?>';
var cOfflineDot = '<?php echo esc_js($dot_offline); ?>';
var bOnline = '<?php echo esc_js($border_online); ?>';
var bOffline = '<?php echo esc_js($border_offline); ?>';
var aOnline = '<?php echo esc_js($anim_name_online); ?>';
var aOffline = '<?php echo esc_js($anim_name_offline); ?>';
setInterval(function(){
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded'},
body: 'action=mcss_fetch&server_id=' + serverId
})
.then(r => r.json())
.then(d => {
var wrapper = document.getElementById('mcss-sw-wrapper-' + uid);
var dot = document.getElementById('mcss-sw-dot-' + uid);
var txt = document.getElementById('mcss-sw-text-' + uid);
if(wrapper && dot && txt && d) {
if(d.online) {
wrapper.style.backgroundColor = cOnlineBg;
wrapper.style.borderColor = bOnline;
dot.style.backgroundColor = cOnlineDot;
dot.style.animationName = aOnline;
txt.style.color = cOnlineText;
txt.textContent = 'Online';
} else {
wrapper.style.backgroundColor = cOfflineBg;
wrapper.style.borderColor = bOffline;
dot.style.backgroundColor = cOfflineDot;
dot.style.animationName = aOffline;
txt.style.color = cOfflineText;
txt.textContent = 'Offline';
}
}
});
}, 3000);
})();
</script>
<?php
} else {
echo '<p>Bitte Server in den Einstellungen wählen.</p>';
}
echo $args['after_widget'];
}
}
// Widget registrieren
add_action( 'widgets_init', function(){
register_widget( 'MCSS_Sidebar_Status_Widget' );
});
+7 -1
View File
@@ -135,4 +135,10 @@ Bei Problemen überprüfe bitte:
## Weitere Informationen
Für detaillierte Informationen zur Installation, Konfiguration und Fehlerbehebung der StatusAPI:
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**
---
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten**
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.