v0.3.0-prod

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-04 14:57:48 +01:00
1 parent 387933957e
commit 9d093b691c
21 files changed
+1239 -180

No files matched your search

+177 -1
View File
@@ -1 +1,177 @@
:root{color-scheme:dark;--bg:#0b0f14;--surface:#121821;--surface-2:#171f2a;--surface-3:#0f151d;--border:#263140;--border-soft:#1c2531;--text:#f5f7fa;--muted:#91a0b2;--primary:#3274d9;--primary-hover:#3d82eb;--success:#43b581;--danger:#e66a70;--shadow:0 14px 35px rgba(0,0,0,.25);--radius:14px}*{box-sizing:border-box}html{background:var(--bg)}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}a{color:inherit}.app-shell{min-height:100vh}.topbar{min-height:70px;border-bottom:1px solid var(--border-soft);background:rgba(18,24,33,.96);display:flex;align-items:center;justify-content:space-between;gap:24px;padding:10px max(24px,calc((100vw - 1480px)/2));position:sticky;top:0;z-index:20;backdrop-filter:blur(10px)}.brand{text-decoration:none}.brand span{display:flex;flex-direction:column;line-height:1.2}.brand strong{font-size:15px}.brand small{margin-top:3px;color:var(--muted);font-size:12px}.header-right,.top-actions,.button-row{display:flex;align-items:center;gap:10px}.header-online{height:40px;padding:0 13px;border:1px solid var(--border);border-radius:9px;background:var(--surface-2);display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)}.header-online strong{color:var(--text);font-size:17px}.online-dot{width:8px;height:8px;border-radius:50%;background:var(--success);box-shadow:0 0 0 3px rgba(67,181,129,.12);flex:none}.btn{appearance:none;border:1px solid transparent;border-radius:9px;min-height:40px;padding:9px 14px;display:inline-flex;align-items:center;justify-content:center;font:inherit;font-weight:650;text-decoration:none;cursor:pointer;transition:background .16s ease,border-color .16s ease,transform .16s ease}.btn:hover{transform:translateY(-1px)}.btn-primary{background:var(--primary);border-color:var(--primary);color:#fff}.btn-primary:hover{background:var(--primary-hover);border-color:var(--primary-hover)}.btn-ghost{background:var(--surface-2);border-color:var(--border);color:var(--text)}.btn-ghost:hover{background:#1c2633}.btn-large{width:100%;min-height:48px;font-size:15px;margin-top:4px}.grow{flex:1}.container{max-width:1480px;margin:0 auto;padding:30px 24px 48px}.page-intro{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:18px}.eyebrow{margin:0 0 5px;text-transform:uppercase;letter-spacing:.13em;font-size:11px;font-weight:800;color:#7890aa}.page-intro h1,.channel-title h1{margin:0;font-size:clamp(26px,3vw,36px);line-height:1.15;letter-spacing:-.035em}.page-intro p:not(.eyebrow),.channel-title p{margin:8px 0 0;color:var(--muted)}.refresh-status{font-size:12px;color:var(--muted)}.graphs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.graph-card{min-width:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:0 8px 26px rgba(0,0,0,.16)}.graph-head{padding:15px 17px 12px;border-bottom:1px solid var(--border-soft)}.graph-head h2{margin:0;font-size:15px;letter-spacing:-.01em}.graph-head p{margin:3px 0 0;font-size:12px;color:var(--muted)}.graph-image-button{display:block;width:100%;padding:0;border:0;background:var(--surface-3);cursor:zoom-in;overflow:hidden}.graph-image-button img{display:block;width:100%;height:auto;aspect-ratio:3.56/1;object-fit:contain;transition:transform .18s ease,opacity .18s ease}.graph-image-button:hover img{transform:scale(1.015);opacity:.96}.lightbox[hidden]{display:none}.lightbox{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;padding:28px}.lightbox-backdrop{position:absolute;inset:0;width:100%;height:100%;border:0;background:rgba(0,0,0,.82);cursor:zoom-out}.lightbox-dialog{position:relative;z-index:1;width:min(96vw,1500px);max-height:92vh;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:48px 16px 16px;box-shadow:0 24px 80px rgba(0,0,0,.55);overflow:auto}.lightbox-dialog img{display:block;width:100%;height:auto}.lightbox-close{position:absolute;top:10px;right:12px;border:1px solid var(--border);background:var(--surface-2);color:var(--text);border-radius:8px;padding:7px 11px;font:inherit;cursor:pointer}.lightbox-open{overflow:hidden}.channel-page{min-height:100vh;display:grid;place-items:center;padding:28px;background:var(--bg)}.channel-shell{width:min(100%,700px)}.channel-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;box-shadow:var(--shadow);overflow:hidden}.channel-topbar{min-height:56px;padding:0 22px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border-soft)}.back-link{color:var(--muted);font-size:13px;text-decoration:none}.back-link:hover{color:var(--text)}.channel-title{padding:28px 28px 20px}.channel-title h1{font-size:28px}.alert{margin:0 28px 20px;padding:13px 15px;border:1px solid var(--border);border-radius:10px;background:var(--surface-2);display:flex;flex-direction:Line truncated
/* === tokens ============================================================ */
:root{
color-scheme:dark;
--bg:#121513;--panel:#181c19;--panel2:#1e231f;--panel3:#242a25;--sidebar:#101310;
--line:#313832;--line2:#485149;--text:#edf0ec;--muted:#9da79f;--subtle:#7c877f;
--accent:#8fb9a4;--accent2:#a4c9b6;--accentink:#102018;--danger:#e18f8a;--warn:#d0ad70;--success:#a7c892;
--field:#121713;--selected:#28352e;--hover:#202621;--shadow:rgba(0,0,0,.32);
--font:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--mono:ui-monospace,SFMono-Regular,Consolas,monospace;--r:4px;--control:38px
}
html[data-theme=light]{
color-scheme:light;
--bg:#f2f4f1;--panel:#fff;--panel2:#f7f8f6;--panel3:#edf1ec;--sidebar:#e9eee9;
--line:#cbd2cb;--line2:#aeb8af;--text:#202622;--muted:#647068;--subtle:#7a847d;
--accent:#507b67;--accent2:#416b58;--accentink:#fff;--danger:#a94c48;--warn:#8a6930;--success:#587a49;
--field:#fff;--selected:#dce8e0;--hover:#e8ede8;--shadow:rgba(31,39,33,.16)
}
/* === base =============================================================== */
*{box-sizing:border-box;scrollbar-color:var(--line2) var(--panel);scrollbar-width:thin}
html,body{min-height:100%}
body{margin:0;background:var(--bg);color:var(--text);font:15px/1.5 var(--font);-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
button,input,select,textarea{font:inherit;color:inherit}
input,select,textarea{width:100%;min-height:var(--control);border:1px solid var(--line2);border-radius:var(--r);background:var(--field);padding:8px 11px;outline:0}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
input::placeholder{color:var(--subtle)}
button{cursor:pointer}
.icon{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:0 0 auto}
.muted{color:var(--muted)}
.mono{font-family:var(--mono)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:var(--control);padding:7px 14px;border:1px solid var(--line2);border-radius:var(--r);background:var(--panel2);color:var(--text);font-weight:650;white-space:nowrap}
.btn:hover{border-color:var(--accent)}
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accentink)}
.btn.primary:hover{background:var(--accent2);border-color:var(--accent2)}
.btn.ghost{background:transparent;border-color:transparent}
.btn.danger{color:var(--danger)}
.btn.full{width:100%}
.btn.lg{min-height:46px;font-size:15px}
.badge{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--line);border-radius:999px;padding:3px 10px;font-size:11px;color:var(--muted)}
.badge.ok{color:var(--success);border-color:color-mix(in srgb,var(--success) 40%,var(--line))}
.badge .dot{width:6px;height:6px;border-radius:50%;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,currentColor 18%,transparent)}
.alert{margin:0 0 16px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--r);background:var(--panel2);display:flex;align-items:flex-start;gap:10px;font-size:13px;line-height:1.5}
.alert .icon{margin-top:1px;color:var(--muted)}
.alert.success{border-color:color-mix(in srgb,var(--success) 45%,var(--line));color:var(--success)}
.alert.success .icon{color:var(--success)}
.alert.danger{border-color:color-mix(in srgb,var(--danger) 45%,var(--line));color:var(--danger)}
.alert.danger .icon{color:var(--danger)}
.alert strong{color:var(--text)}
/* theme toggle, shared by every page */
.theme-toggle{width:36px;height:36px;min-height:36px;padding:0;border:1px solid var(--line2);border-radius:var(--r);background:var(--panel2);color:var(--accent2);outline:1px solid color-mix(in srgb,var(--text) 18%,transparent);flex:0 0 auto}
.theme-toggle:hover{background:var(--hover);border-color:var(--accent)}
.theme-toggle .icon{width:18px;height:18px;display:block;margin:auto}
.theme-toggle .ic-moon{display:none}
html[data-theme=light] .theme-toggle .ic-sun{display:none}
html[data-theme=light] .theme-toggle .ic-moon{display:block}
/* === dashboard shell (stats page) ======================================= */
.shell{min-height:100vh;display:grid;grid-template-rows:56px minmax(0,1fr)}
.topbar{position:relative;display:grid;grid-template-columns:240px 1fr auto;align-items:center;gap:14px;padding:0 16px;border-bottom:1px solid var(--line);background:var(--panel)}
.brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:16px}
.brand svg{color:var(--accent);width:24px;height:24px}
.header-online{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px}
.header-online strong{color:var(--text);font-size:16px}
.top-actions{display:flex;align-items:center;gap:8px}
.route-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden;background:transparent}
.route-progress>span{display:block;width:35%;height:100%;background:var(--accent);animation:route-progress 1.1s ease-in-out infinite}
.route-progress[hidden]{display:none}
@keyframes route-progress{0%{transform:translateX(-110%)}50%{transform:translateX(120%)}100%{transform:translateX(310%)}}
.body{min-height:0;display:grid;grid-template-columns:220px minmax(0,1fr)}
.sidebar{min-height:0;padding:14px 10px;border-right:1px solid var(--line);background:var(--sidebar);overflow:auto}
.nav{display:grid;gap:3px}
.nav a{display:flex;align-items:center;gap:10px;height:38px;padding:0 11px;border:1px solid transparent;border-radius:var(--r);color:var(--muted)}
.nav a:hover{background:var(--hover);color:var(--text)}
.nav a.active{background:var(--selected);color:var(--text);border-color:var(--line)}
.nav a svg{width:17px;height:17px}
.nav-section{margin:16px 11px 6px;color:var(--subtle);font-size:10px;font-weight:750;letter-spacing:.08em;text-transform:uppercase}
.nav-server{display:flex;align-items:center;gap:9px;height:30px;padding:0 11px;color:var(--muted);font-size:12px}
.nav-server svg{width:15px;height:15px;flex:0 0 auto}
.nav-server span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.icon-sprite{display:none}
.content{min-width:0;min-height:0;overflow:auto;padding:26px 28px 48px}
.page-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:20px;flex-wrap:wrap}
.page-head h1{margin:0;font-size:24px;letter-spacing:-.3px}
.page-head p{margin:4px 0 0;color:var(--muted)}
.eyebrow{margin:0 0 6px;text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:750;color:var(--subtle)}
.cards{display:grid;grid-template-columns:repeat(4,minmax(150px,1fr));gap:12px;margin-bottom:22px}
.card{padding:16px;border:1px solid var(--line);border-radius:var(--r);background:var(--panel);display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;align-items:start}
.card>.icon{width:22px;height:22px;color:var(--accent);margin-top:2px}
.card .k{color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.07em}
.card .v{margin-top:5px;font-size:24px;font-weight:720;overflow-wrap:anywhere}
.card .vs{margin-top:5px;font-size:14px;font-weight:650;overflow-wrap:anywhere}
.section-title{margin:0 2px 10px;color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.08em;text-transform:uppercase}
.panel{border:1px solid var(--line);border-radius:var(--r);background:var(--panel);overflow:hidden}
.panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;min-height:44px;padding:0 13px;border-bottom:1px solid var(--line)}
.panel-head h2{margin:0;font-size:14px;font-weight:650}
.panel-head p{margin:0;font-size:11px;color:var(--muted)}
.graphs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.graph-image-button{display:block;width:100%;padding:0;border:0;background:var(--field);cursor:zoom-in;overflow:hidden}
.graph-image-button img{display:block;width:100%;height:auto;aspect-ratio:3.56/1;object-fit:contain;transition:transform .18s ease,opacity .18s ease}
.graph-image-button:hover img{transform:scale(1.012);opacity:.96}
.lightbox[hidden]{display:none}
.lightbox{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;padding:28px}
.lightbox-backdrop{position:absolute;inset:0;width:100%;height:100%;border:0;background:rgba(0,0,0,.78);cursor:zoom-out}
.lightbox-dialog{position:relative;z-index:1;width:min(96vw,1500px);max-height:92vh;background:var(--panel);border:1px solid var(--line2);border-radius:var(--r);padding:48px 16px 16px;box-shadow:0 24px 80px var(--shadow);overflow:auto}
.lightbox-dialog img{display:block;width:100%;height:auto}
.lightbox-close{position:absolute;top:10px;right:12px;display:inline-flex;gap:6px;align-items:center}
.lightbox-open{overflow:hidden}
/* === channel / verify (standalone card pages) ========================== */
.channel-view{min-height:100vh;display:grid;place-items:center;padding:28px}
.channel-shell{width:min(100%,640px)}
.channel-card{border:1px solid var(--line);background:var(--panel);border-radius:var(--r);box-shadow:0 18px 46px var(--shadow);overflow:hidden}
.channel-topbar{min-height:50px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);background:var(--panel2)}
.channel-topbar-right{display:flex;align-items:center;gap:10px}
.channel-topbar a{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:12px;font-weight:650}
.channel-topbar a:hover{color:var(--text)}
.channel-topbar a svg{width:14px;height:14px}
.channel-title{padding:22px 24px 6px;display:flex;gap:12px;align-items:flex-start}
.channel-title svg{width:30px;height:30px;color:var(--accent);margin-top:2px}
.channel-title h1{margin:0;font-size:21px;letter-spacing:-.2px}
.channel-title p{margin:6px 0 0;color:var(--muted);font-size:13px}
.channel-body{padding:18px 24px 24px}
.empty-state,.selection-box{margin:0 0 18px;padding:20px;border:1px solid var(--line);background:var(--panel2);border-radius:var(--r);text-align:center}
.empty-state h2,.selection-box h2{font-size:15px;margin:0 0 6px}
.empty-state p,.selection-box p{margin:0 0 16px;color:var(--muted);font-size:13px}
.selection-box{text-align:left}
.client-list{display:grid;gap:7px}
.client-choice{width:100%;display:flex;align-items:center;gap:11px;text-align:left;border:1px solid var(--line);background:var(--field);color:var(--text);padding:10px 12px;border-radius:var(--r)}
.client-choice:hover{border-color:var(--accent)}
.client-choice .avatar{width:32px;height:32px;border-radius:var(--r);background:var(--selected);color:var(--accent2);display:grid;place-items:center;font-weight:800;flex:0 0 auto}
.client-choice .meta{display:flex;flex-direction:column;flex:1;min-width:0}
.client-choice small{color:var(--muted)}
.client-badge{margin:0 0 16px;padding:11px 13px;border:1px solid var(--line);border-radius:var(--r);background:var(--panel2);display:flex;align-items:center;gap:9px;font-size:13px}
.client-badge .dot{width:8px;height:8px;border-radius:50%;background:var(--success);box-shadow:0 0 0 3px color-mix(in srgb,var(--success) 18%,transparent);flex:0 0 auto}
.client-badge span{color:var(--muted)}
.button-row{display:flex;gap:8px;flex-wrap:wrap}
.button-row .btn{flex:1}
.field{margin-bottom:15px}
.field label{display:flex;align-items:center;gap:7px;font-size:12px;font-weight:650;margin-bottom:6px;color:var(--text)}
.field label svg{width:14px;height:14px;color:var(--muted)}
.field small{display:block;color:var(--subtle);font-size:11px;margin-top:5px}
.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 14px}
.form-grid .wide{grid-column:1/-1}
/* === responsive ========================================================= */
@media(max-width:1060px){
.cards{grid-template-columns:repeat(2,minmax(140px,1fr))}
.graphs{grid-template-columns:1fr}
.graph-image-button img{aspect-ratio:auto;min-height:260px}
}
@media(max-width:860px){
.topbar{grid-template-columns:auto 1fr auto}
.body{grid-template-columns:1fr}
.sidebar{display:none}
}
@media(max-width:620px){
.cards{grid-template-columns:1fr}
.form-grid{grid-template-columns:1fr}
.content{padding:18px 14px 36px}
.lightbox{padding:10px}
.lightbox-dialog{width:100%;padding:46px 8px 8px}
.channel-view{padding:0}
.channel-shell{width:100%}
.channel-card{min-height:100vh;border-radius:0;border-left:0;border-right:0}
}
+72 -14
View File
@@ -1,17 +1,69 @@
(function () {
'use strict';
// --- theme (dark/light), persisted per browser -------------------------
var THEME_KEY = 'ts3monolith-theme';
var root = document.documentElement;
var toggle = document.querySelector('.theme-toggle');
var graphs = document.querySelectorAll('.js-live-graph');
var openers = document.querySelectorAll('.js-graph-open');
function currentTheme() {
return root.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
}
// RRD graphs are static PNGs rendered server-side in both a dark and a
// light palette (see internal/rrd/rrd.go); swap to the variant matching
// the active theme so panel graphs never clash with the page around them.
function applyGraphTheme(theme) {
var stamp = Date.now();
graphs.forEach(function (img) {
var src = theme === 'light' ? img.dataset.srcLight : img.dataset.src;
if (src) img.src = src + '?v=' + stamp;
});
openers.forEach(function (button) {
var full = theme === 'light' ? button.getAttribute('data-full-light') : button.getAttribute('data-full');
if (full) button.setAttribute('data-full-active', full);
});
}
function applyTheme(mode) {
if (mode === 'light') root.setAttribute('data-theme', 'light');
else root.removeAttribute('data-theme');
applyGraphTheme(mode === 'light' ? 'light' : 'dark');
}
try {
var saved = window.localStorage.getItem(THEME_KEY);
if (saved) applyTheme(saved);
else applyGraphTheme(currentTheme());
} catch (e) {
applyGraphTheme(currentTheme());
}
if (toggle) {
toggle.addEventListener('click', function () {
var next = currentTheme() === 'light' ? 'dark' : 'light';
applyTheme(next);
try { window.localStorage.setItem(THEME_KEY, next); } catch (e) { /* ignore */ }
});
}
// --- live stats + graph refresh -----------------------------------------
var count = document.getElementById('online-count');
var status = document.getElementById('refresh-status');
var graphs = document.querySelectorAll('.js-live-graph');
var lightbox = document.getElementById('graph-lightbox');
var lightboxImage = document.getElementById('lightbox-image');
var openers = document.querySelectorAll('.js-graph-open');
var closeButtons = document.querySelectorAll('[data-lightbox-close]');
var progress = document.getElementById('route-progress');
function setBusy(busy) {
if (progress) progress.hidden = !busy;
}
function refresh() {
setBusy(true);
if (status) status.textContent = 'Odświeżanie…';
var done = Promise.resolve();
if (count) {
fetch('/stats/live', {cache: 'no-store'})
done = fetch('/stats/live', { cache: 'no-store' })
.then(function (response) {
if (!response.ok) throw new Error('stats');
return response.json();
@@ -25,13 +77,21 @@
});
}
var stamp = Date.now();
graphs.forEach(function (img) {
var src = img.dataset.src;
if (src) img.src = src + '?v=' + stamp;
});
applyGraphTheme(currentTheme());
done.then(function () { setBusy(false); });
}
if (count || graphs.length) {
window.setInterval(refresh, 60000);
}
// --- graph lightbox -------------------------------------------------------
var lightbox = document.getElementById('graph-lightbox');
var lightboxImage = document.getElementById('lightbox-image');
var openers = document.querySelectorAll('.js-graph-open');
var closeButtons = document.querySelectorAll('[data-lightbox-close]');
function openLightbox(src) {
if (!lightbox || !lightboxImage) return;
lightboxImage.src = src + (src.indexOf('?') === -1 ? '?' : '&') + 'v=' + Date.now();
@@ -50,7 +110,7 @@
openers.forEach(function (button) {
button.addEventListener('click', function () {
var src = button.getAttribute('data-full');
var src = button.getAttribute('data-full-active') || button.getAttribute('data-full');
if (src) openLightbox(src);
});
});
@@ -62,6 +122,4 @@
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && lightbox && !lightbox.hidden) closeLightbox();
});
window.setInterval(refresh, 60000);
})();