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 677a0e4d2c
20 files changed
+1239 -180

No files matched your search

+113 -86
View File
@@ -4,99 +4,126 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Utwórz kanał</title>
<link rel="stylesheet" href="/assets/app.css">
<link rel="stylesheet" href="{{asset "app.css"}}">
<script src="{{asset "app.js"}}" defer></script>
</head>
<body class="channel-page">
<main class="channel-shell">
<section class="channel-card">
<div class="channel-topbar">
<a class="back-link" href="/stats">Statystyki
</a>
<a class="back-link" href="{{.VoiceURL}}" target="_top">Połącz z TS3</a>
</div>
<div class="channel-title">
<div>
<p class="eyebrow">TeamSpeak 3</p>
<h1>Utwórz swój kanał</h1>
<p>Aplikacja wykryje Twojego klienta, utworzy kanał, nada prawa i przeniesie Cię automatycznie.</p>
</div>
</div>
{{if .Error}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
{{if .Success}}
<div class="alert alert-success">
<strong>Kanał został utworzony.</strong>
<span>{{.Success}} ID kanału: {{.ChannelID}}</span>
</div>
<div class="button-row">
<a class="btn btn-primary grow" href="{{.VoiceURL}}" target="_top">Otwórz TeamSpeak</a>
<a class="btn btn-ghost" href="/stats">Statystyki</a>
</div>
{{else}}
{{if eq (len .Clients) 0}}{{if not .Client}}
<div class="empty-state">
<h2>Nie wykryto klienta TS3</h2>
<p>Połącz się z serwerem TeamSpeak, a następnie odśwież stronę.</p>
<div class="button-row">
<a class="btn btn-primary grow" href="{{.VoiceURL}}" target="_top">Połącz z TeamSpeak</a>
<a class="btn btn-ghost" href="">Odśwież</a>
<body>
{{template "icons" .}}
<main class="channel-view">
<div class="channel-shell">
<section class="channel-card">
<div class="channel-topbar">
<a href="/stats"><svg class="icon"><use href="#icon-arrow-left"/></svg>Statystyki</a>
<div class="channel-topbar-right">
<a href="{{.VoiceURL}}" target="_top"><svg class="icon"><use href="#icon-zap"/></svg>Połącz z TS3</a>
<button class="theme-toggle" type="button" aria-label="Przełącz jasny/ciemny motyw" title="Jasny / ciemny motyw">
<svg class="icon ic-sun"><use href="#icon-sun"/></svg>
<svg class="icon ic-moon"><use href="#icon-moon"/></svg>
</button>
</div>
</div>
{{end}}{{end}}
{{if gt (len .Clients) 1}}
<div class="selection-box">
<h2>Wybierz swojego klienta</h2>
<p>Pod tym samym adresem IP jest kilka osób. Kod potwierdzający przyjdzie jako prywatna wiadomość na TS3.</p>
<div class="client-list">
{{range .Clients}}
<form method="post" action="/channel/verify/start">
<input type="hidden" name="form_token" value="{{$.FormToken}}">
<input type="hidden" name="clid" value="{{.ID}}">
<button type="submit" class="client-choice">
<span class="avatar" aria-hidden="true">{{printf "%.1s" .Nick}}</span>
<span><strong>{{.Nick}}</strong><small>Wyślij kod weryfikacyjny</small></span></button>
<div class="channel-title">
<svg class="icon"><use href="#icon-headset"/></svg>
<div>
<p class="eyebrow">TeamSpeak 3</p>
<h1>Utwórz swój kanał</h1>
<p>Aplikacja wykryje Twojego klienta, utworzy kanał, nada prawa i przeniesie Cię automatycznie.</p>
</div>
</div>
<div class="channel-body">
{{if .Error}}<div class="alert danger"><svg class="icon"><use href="#icon-circle-alert"/></svg><span>{{.Error}}</span></div>{{end}}
{{if .Success}}
<div class="alert success">
<svg class="icon"><use href="#icon-check-circle"/></svg>
<span><strong>Kanał został utworzony.</strong><br>{{.Success}} ID kanału: {{.ChannelID}}</span>
</div>
<div class="button-row">
<a class="btn primary lg" href="{{.VoiceURL}}" target="_top"><svg class="icon"><use href="#icon-zap"/></svg>Otwórz TeamSpeak</a>
<a class="btn ghost" href="/stats">Statystyki</a>
</div>
{{else}}
{{if eq (len .Clients) 0}}{{if not .Client}}
<div class="empty-state">
<h2>Nie wykryto klienta TS3</h2>
<p>Połącz się z serwerem TeamSpeak, a następnie odśwież stronę.</p>
<div class="button-row">
<a class="btn primary lg" href="{{.VoiceURL}}" target="_top"><svg class="icon"><use href="#icon-zap"/></svg>Połącz z TeamSpeak</a>
<a class="btn ghost" href=""><svg class="icon"><use href="#icon-refresh-cw"/></svg>Odśwież</a>
</div>
</div>
{{end}}{{end}}
{{if gt (len .Clients) 1}}
<div class="selection-box">
<h2>Wybierz swojego klienta</h2>
<p>Pod tym samym adresem IP jest kilka osób. Kod potwierdzający przyjdzie jako prywatna wiadomość na TS3.</p>
<div class="client-list">
{{range .Clients}}
<form method="post" action="/channel/verify/start">
<input type="hidden" name="form_token" value="{{$.FormToken}}">
<input type="hidden" name="clid" value="{{.ID}}">
<button type="submit" class="client-choice">
<span class="avatar" aria-hidden="true">{{printf "%.1s" .Nick}}</span>
<span class="meta"><strong>{{.Nick}}</strong><small>Wyślij kod weryfikacyjny</small></span>
</button>
</form>
{{end}}
</div>
</div>
{{end}}
{{if .Client}}
<div class="client-badge">
<span class="dot" aria-hidden="true"></span>
<span>Wykryty klient</span>
<strong>{{.Client.Nick}}</strong>
</div>
<form method="post" action="/channel/create">
<input type="hidden" name="form_token" value="{{.FormToken}}">
<input type="hidden" name="client_token" value="{{.ClientToken}}">
<div class="field">
<label for="channel-name"><svg class="icon"><use href="#icon-hash"/></svg>Nazwa kanału</label>
<input id="channel-name" name="name" maxlength="40" autocomplete="off" placeholder="Np. Pokój Mateusza" value="{{.Name}}">
<small>Pozostaw puste, aby wygenerować nazwę automatycznie.</small>
</div>
<div class="form-grid">
<div class="field">
<label for="channel-password"><svg class="icon"><use href="#icon-lock"/></svg>Hasło</label>
<input id="channel-password" name="password" maxlength="64" type="password" autocomplete="new-password" placeholder="Opcjonalne">
</div>
<div class="field">
<label for="channel-maxclients"><svg class="icon"><use href="#icon-users"/></svg>Limit użytkowników</label>
<input id="channel-maxclients" name="maxclients" type="number" min="0" max="512" placeholder="0 = bez limitu" value="{{.MaxClients}}">
</div>
</div>
<div class="field wide">
<label for="channel-topic"><svg class="icon"><use href="#icon-sliders"/></svg>Temat / opis</label>
<input id="channel-topic" name="topic" maxlength="255" autocomplete="off" placeholder="Opcjonalne" value="{{.Topic}}">
</div>
<div class="field wide">
<label for="channel-codec-quality"><svg class="icon"><use href="#icon-activity"/></svg>Jakość kodeka (1–10)</label>
<input id="channel-codec-quality" name="codec_quality" type="number" min="1" max="10" placeholder="Domyślna serwera" value="{{.CodecQuality}}">
<small>Wyższa wartość oznacza lepszą jakość głosu, ale większe zużycie pasma.</small>
</div>
<button class="btn primary lg full" type="submit">
<svg class="icon"><use href="#icon-plus-circle"/></svg>Utwórz kanał i przenieś mnie
</button>
</form>
{{end}}
{{end}}
</div>
</div>
{{end}}
{{if .Client}}
<div class="client-badge">
<span class="online-dot" aria-hidden="true"></span>
<span>Wykryty klient</span>
<strong>{{.Client.Nick}}</strong>
</div>
<form method="post" action="/channel/create" class="channel-form">
<input type="hidden" name="form_token" value="{{.FormToken}}">
<input type="hidden" name="client_token" value="{{.ClientToken}}">
<div class="field">
<label for="channel-name">Nazwa kanału</label>
<input id="channel-name" name="name" maxlength="40" autocomplete="off" placeholder="Np. Pokój Mateusza">
<small>Pozostaw puste, aby wygenerować nazwę automatycznie.</small>
</div>
<div class="field">
<label for="channel-password">Hasło</label>
<input id="channel-password" name="password" maxlength="64" type="password" autocomplete="new-password" placeholder="Opcjonalne">
</div>
<div class="field">
<label for="channel-topic">Temat / opis</label>
<input id="channel-topic" name="topic" maxlength="255" autocomplete="off" placeholder="Opcjonalne">
</div>
<button class="btn btn-primary btn-large" type="submit">
Utwórz kanał i przenieś mnie</button>
</form>
{{end}}
{{end}}
</section>
</section>
</div>
</main>
</body>
</html>{{end}}
+23
View File
@@ -0,0 +1,23 @@
{{define "icons"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon-sprite">
<symbol id="icon-headset" viewBox="0 0 24 24"><path d="M4 13a8 8 0 0 1 16 0"/><path d="M4 13v3a2 2 0 0 0 2 2h1v-6H6a2 2 0 0 0-2 2Z"/><path d="M20 13v3a2 2 0 0 1-2 2h-1v-6h1a2 2 0 0 1 2 2Z"/><path d="M15 18v1a2 2 0 0 1-2 2h-1"/></symbol>
<symbol id="icon-activity" viewBox="0 0 24 24"><path d="M3 12h4l3 8 4-16 3 8h4"/></symbol>
<symbol id="icon-users" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.2 20a6 6 0 0 1 11.6 0"/><circle cx="17" cy="9" r="2.4"/><path d="M15.5 13.2A5 5 0 0 1 20.8 18"/></symbol>
<symbol id="icon-zap" viewBox="0 0 24 24"><path d="M13 2 4 14h6l-1 8 9-12h-6z"/></symbol>
<symbol id="icon-lock" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></symbol>
<symbol id="icon-server" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M7 7.5h.01M7 16.5h.01"/></symbol>
<symbol id="icon-plus-circle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v8M8 12h8"/></symbol>
<symbol id="icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></symbol>
<symbol id="icon-link-2" viewBox="0 0 24 24"><path d="M9 17H7A5 5 0 0 1 7 7h2"/><path d="M15 7h2a5 5 0 0 1 0 10h-2"/><path d="M8 12h8"/></symbol>
<symbol id="icon-arrow-left" viewBox="0 0 24 24"><path d="M19 12H5"/><path d="m11 18-6-6 6-6"/></symbol>
<symbol id="icon-x" viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="M6 6l12 12"/></symbol>
<symbol id="icon-image" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="1.5"/><path d="m21 15-5-5-4 4-2-2-5 5"/></symbol>
<symbol id="icon-sliders" viewBox="0 0 24 24"><path d="M4 6h10M4 12h16M4 18h8"/><circle cx="16" cy="6" r="2"/><circle cx="8" cy="18" r="2"/></symbol>
<symbol id="icon-hash" viewBox="0 0 24 24"><path d="M5 9h14M5 15h14M9 4 7 20M17 4l-2 16"/></symbol>
<symbol id="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></symbol>
<symbol id="icon-moon" viewBox="0 0 24 24"><path d="M20.5 14.2A8.5 8.5 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2Z"/></symbol>
<symbol id="icon-check-circle" viewBox="0 0 24 24"><path d="M22 11.1V12a10 10 0 1 1-5.9-9.1"/><path d="m9 11 3 3L22 4"/></symbol>
<symbol id="icon-circle-alert" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></symbol>
<symbol id="icon-refresh-cw" viewBox="0 0 24 24"><path d="M20 7v5h-5"/><path d="M4 17v-5h5"/><path d="M6.1 8a7 7 0 0 1 11.2-1L20 12M4 12l2.7 5a7 7 0 0 0 11.2-1"/></symbol>
</svg>
{{end}}
+77 -40
View File
@@ -4,61 +4,98 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>TeamSpeak — statystyki</title>
<link rel="stylesheet" href="/assets/app.css">
<script src="/assets/app.js" defer></script>
<link rel="stylesheet" href="{{asset "app.css"}}">
<script src="{{asset "app.js"}}" defer></script>
</head>
<body>
<div class="app-shell">
{{template "icons" .}}
<div class="shell">
<header class="topbar">
<a class="brand" href="/stats" aria-label="TeamSpeak — statystyki">
<span><strong>TeamSpeak</strong><small>Panel serwera</small></span>
<a class="brand" href="/stats">
<svg class="icon" width="24" height="24"><use href="#icon-headset"/></svg>
<span>Panel serwera</span>
</a>
<div class="header-right">
<div class="header-online" aria-live="polite">
<span class="online-dot" aria-hidden="true"></span>
<span>Online</span>
<strong id="online-count">{{.Current}}</strong>
</div>
<nav class="top-actions" aria-label="Nawigacja">
<a class="btn btn-ghost" href="{{.VoiceURL}}">Połącz</a>
<a class="btn btn-primary" href="/channel">Utwórz kanał</a>
</nav>
<div class="header-online" aria-live="polite">
<span class="badge ok"><span class="dot"></span>Online</span>
<strong id="online-count">{{.Current}}</strong>
<span class="muted" id="refresh-status">Dane aktualne</span>
</div>
<nav class="top-actions" aria-label="Nawigacja">
<a class="btn ghost" href="{{.VoiceURL}}"><svg class="icon"><use href="#icon-zap"/></svg>Połącz</a>
<a class="btn primary" href="/channel"><svg class="icon"><use href="#icon-plus-circle"/></svg>Utwórz kanał</a>
<button class="theme-toggle" type="button" aria-label="Przełącz jasny/ciemny motyw" title="Jasny / ciemny motyw">
<svg class="icon ic-sun"><use href="#icon-sun"/></svg>
<svg class="icon ic-moon"><use href="#icon-moon"/></svg>
</button>
</nav>
<div class="route-progress" id="route-progress" hidden><span></span></div>
</header>
<main class="container">
<section class="page-intro">
<div>
<p class="eyebrow">Statystyki serwera</p>
<h1>Aktywność TeamSpeak 3</h1>
<p>Historia liczby użytkowników połączonych z serwerem.</p>
<div class="body">
<aside class="sidebar">
<nav class="nav">
<a class="active" href="/stats"><svg class="icon"><use href="#icon-activity"/></svg>Statystyki</a>
<a href="/channel"><svg class="icon"><use href="#icon-plus-circle"/></svg>Utwórz kanał</a>
<a href="{{.VoiceURL}}"><svg class="icon"><use href="#icon-zap"/></svg>Połącz z TS3</a>
</nav>
<p class="nav-section">Serwer</p>
<div class="nav-server" title="{{if .InfoAvailable}}{{.ServerName}}{{end}}">
<svg class="icon"><use href="#icon-server"/></svg><span class="mono">{{if .InfoAvailable}}{{.ServerName}}{{else}}—{{end}}</span>
</div>
<span class="refresh-status" id="refresh-status">Odświeżanie automatyczne</span>
</section>
</aside>
<section class="graphs" aria-label="Wykresy statystyk">
{{range .Ranges}}
<article class="graph-card">
<div class="graph-head">
<div>
<h2>{{.Title}}</h2>
<p>Liczba użytkowników online</p>
</div>
<main class="content">
<section class="page-head">
<div>
<p class="eyebrow">Statystyki serwera</p>
<h1>Aktywność TeamSpeak 3</h1>
<p>Historia liczby użytkowników połączonych z serwerem.</p>
</div>
<button class="graph-image-button js-graph-open" type="button" data-full="/graph/{{.Key}}.png" aria-label="Powiększ wykres {{.Title}}">
<img class="js-live-graph" loading="lazy" src="/graph/{{.Key}}.png" data-src="/graph/{{.Key}}.png" alt="{{.Title}}">
</button>
</article>
{{end}}
</section>
</main>
</section>
<section class="cards" aria-label="Podsumowanie">
<div class="card">
<svg class="icon"><use href="#icon-users"/></svg>
<div><span class="k">Online teraz</span><div class="v">{{.Current}}{{if .InfoAvailable}}{{if gt .MaxClients 0}}<span class="vs muted"> / {{.MaxClients}}</span>{{end}}{{end}}</div></div>
</div>
<div class="card">
<svg class="icon"><use href="#icon-clock"/></svg>
<div><span class="k">Uptime</span><div class="vs">{{if .InfoAvailable}}{{.Uptime}}{{else}}—{{end}}</div></div>
</div>
<div class="card">
<svg class="icon"><use href="#icon-server"/></svg>
<div><span class="k">Wersja serwera</span><div class="vs">{{if .InfoAvailable}}{{.ServerVersion}}{{else}}—{{end}}</div></div>
</div>
<div class="card">
<svg class="icon"><use href="#icon-sliders"/></svg>
<div><span class="k">Platforma</span><div class="vs">{{if .InfoAvailable}}{{.ServerPlatform}}{{else}}—{{end}}</div></div>
</div>
</section>
<p class="section-title">Wykresy obecności</p>
<section class="graphs" aria-label="Wykresy statystyk">
{{range .Ranges}}
<article class="panel">
<div class="panel-head">
<div>
<h2>{{.Title}}</h2>
<p>Liczba użytkowników online</p>
</div>
</div>
<button class="graph-image-button js-graph-open" type="button" data-full="/graph/{{.Key}}.png" data-full-light="/graph/{{.Key}}-light.png" aria-label="Powiększ wykres {{.Title}}">
<img class="js-live-graph" loading="lazy" src="/graph/{{.Key}}.png" data-src="/graph/{{.Key}}.png" data-src-light="/graph/{{.Key}}-light.png" alt="{{.Title}}">
</button>
</article>
{{end}}
</section>
</main>
</div>
</div>
<div class="lightbox" id="graph-lightbox" hidden aria-hidden="true">
<button class="lightbox-backdrop" type="button" data-lightbox-close aria-label="Zamknij podgląd"></button>
<div class="lightbox-dialog" role="dialog" aria-modal="true" aria-label="Powiększony wykres">
<button class="lightbox-close" type="button" data-lightbox-close>Zamknij</button>
<button class="btn ghost lightbox-close" type="button" data-lightbox-close><svg class="icon"><use href="#icon-x"/></svg>Zamknij</button>
<img id="lightbox-image" src="" alt="Powiększony wykres">
</div>
</div>
+50 -2
View File
@@ -1,3 +1,51 @@
{{define "verify.html"}}<!doctype html>
<html lang="pl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Weryfikacja TS3</title><link rel="stylesheet" href="/assets/app.css"></head>
<body class="embed"><main class="panel"><nav class="panel-nav"><a href="/stats">← Statystyki</a><a href="{{.VoiceURL}}" target="_top">Połącz z TS3</a></nav><h1>Potwierdź klienta TS3</h1><p>Kod został wysłany prywatnie do <strong>{{.Client.Nick}}</strong>.</p><form method="post" action="/channel/verify/complete"><input type="hidden" name="form_token" value="{{.FormToken}}"><input type="hidden" name="challenge_token" value="{{.ChallengeToken}}"><label>Kod<input name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" required autofocus></label><button class="button full" type="submit">Potwierdź</button></form></main></body></html>{{end}}
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Weryfikacja TS3</title>
<link rel="stylesheet" href="{{asset "app.css"}}">
<script src="{{asset "app.js"}}" defer></script>
</head>
<body>
{{template "icons" .}}
<main class="channel-view">
<div class="channel-shell">
<section class="channel-card">
<div class="channel-topbar">
<a href="/stats"><svg class="icon"><use href="#icon-arrow-left"/></svg>Statystyki</a>
<div class="channel-topbar-right">
<a href="{{.VoiceURL}}" target="_top"><svg class="icon"><use href="#icon-zap"/></svg>Połącz z TS3</a>
<button class="theme-toggle" type="button" aria-label="Przełącz jasny/ciemny motyw" title="Jasny / ciemny motyw">
<svg class="icon ic-sun"><use href="#icon-sun"/></svg>
<svg class="icon ic-moon"><use href="#icon-moon"/></svg>
</button>
</div>
</div>
<div class="channel-title">
<svg class="icon"><use href="#icon-lock"/></svg>
<div>
<p class="eyebrow">Weryfikacja</p>
<h1>Potwierdź klienta TS3</h1>
<p>Kod został wysłany prywatnie do <strong>{{.Client.Nick}}</strong>.</p>
</div>
</div>
<div class="channel-body">
<form method="post" action="/channel/verify/complete">
<input type="hidden" name="form_token" value="{{.FormToken}}">
<input type="hidden" name="challenge_token" value="{{.ChallengeToken}}">
<div class="field">
<label for="verify-code"><svg class="icon"><use href="#icon-hash"/></svg>Kod weryfikacyjny</label>
<input id="verify-code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" required autofocus placeholder="000000">
<small>Kod jest ważny 5 minut.</small>
</div>
<button class="btn primary lg full" type="submit"><svg class="icon"><use href="#icon-check-circle"/></svg>Potwierdź</button>
</form>
</div>
</section>
</div>
</main>
</body>
</html>{{end}}