diff --git a/pb_migrations/1790000000_client_logs_gitsha.js b/pb_migrations/1790000000_client_logs_gitsha.js
new file mode 100644
index 0000000..3ffd4e3
--- /dev/null
+++ b/pb_migrations/1790000000_client_logs_gitsha.js
@@ -0,0 +1,42 @@
+///
+
+// Add `gitSha` to client_logs: which BUILD wrote this row, not which version.
+//
+// WHY THIS MIGRATION EXISTS: `1.1.7+18` was built and installed at least four
+// times in one evening with materially different code in it. Two telemetry fields
+// were broken in the earlier builds — `lastLoadError` recorded a single obfuscated
+// letter and throughput reported -1 — and nothing in the stored rows could tell
+// those builds apart from the fixed one. So a real measurement from a real
+// television had to be discarded as "probably the old build", which is exactly the
+// kind of ambiguity a diagnostic pipeline must not have.
+//
+// A version number records an intent to release. The sha records the artifact.
+//
+// Fourth time the silent-drop lesson applies (hidden `profiles.pinHash`,
+// `client_logs.flavor`, `client_logs.tester`, now this): PocketBase discards an
+// undeclared key in a create payload with no error and HTTP 200, so a field the
+// client sends does not exist until a migration declares it. Written at the same
+// time as the client change rather than discovered afterwards.
+//
+// Not required, and empty in an ad-hoc local build. An absent sha honestly says
+// "unknown"; the release scripts pass `--dart-define=GIT_SHA=$(git rev-parse
+// --short HEAD)` so every published artifact carries one.
+
+migrate(
+ (app) => {
+ const logs = app.findCollectionByNameOrId("client_logs")
+ logs.fields.add(
+ new TextField({
+ name: "gitSha",
+ required: false,
+ max: 20,
+ }),
+ )
+ app.save(logs)
+ },
+ (app) => {
+ const logs = app.findCollectionByNameOrId("client_logs")
+ logs.fields.removeByName("gitSha")
+ app.save(logs)
+ },
+)
diff --git a/pb_public/status.html b/pb_public/status.html
index 6cee48d..1c2b5f8 100644
--- a/pb_public/status.html
+++ b/pb_public/status.html
@@ -43,6 +43,7 @@
.ok{background:rgba(95,208,138,.14);color:var(--good)}
.un{background:rgba(154,160,170,.16);color:var(--muted)}
.no{background:rgba(255,107,107,.16);color:var(--bad)}
+ .wn{background:rgba(240,166,60,.16);color:var(--warn)}
.big{font-size:26px;font-weight:600;font-variant-numeric:tabular-nums}
.kv{display:flex;gap:18px;flex-wrap:wrap}
.kv div{min-width:78px}
@@ -52,11 +53,14 @@
button:hover{border-color:var(--accent2)}
button.primary{background:var(--accent);color:var(--goldbg);border-color:var(--accent);font-weight:600}
button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
- input{background:var(--panel2);color:var(--text);border:1px solid var(--line);
+ input,select{background:var(--panel2);color:var(--text);border:1px solid var(--line);
border-radius:8px;padding:9px 11px;font:inherit;width:100%}
+ select{padding:7px 9px;width:auto;font-size:13px}
.note{color:var(--muted);font-size:12.5px;margin:8px 0 0}
.warnbox{background:rgba(240,166,60,.10);border:1px solid rgba(240,166,60,.35);
border-radius:8px;padding:10px 12px;color:var(--text);font-size:13px;margin-top:8px}
+ .okbox{background:rgba(95,208,138,.10);border:1px solid rgba(95,208,138,.35);
+ border-radius:8px;padding:10px 12px;font-size:13px;margin-top:8px}
pre{margin:8px 0 0;background:#0b0c10;border:1px solid var(--line);border-radius:8px;
padding:10px;max-height:340px;overflow:auto;font-family:var(--mono);font-size:11.5px;
white-space:pre-wrap;word-break:break-word}
@@ -80,6 +84,44 @@
.sub h3{margin:0 0 6px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;
color:var(--muted);font-weight:600}
.rate{font-weight:600}
+
+ /* ── the three views ─────────────────────────────────────────────────────── */
+ /* A segmented control rather than links: this is one page with one token, and
+ a reload would drop the sessionStorage-only session on some browsers. */
+ .tabs{display:flex;gap:2px;background:var(--panel2);border:1px solid var(--line);
+ border-radius:9px;padding:2px}
+ .tabs button{border:none;background:transparent;border-radius:7px;padding:6px 12px;
+ color:var(--muted);font-size:13px}
+ .tabs button[aria-current="true"]{background:var(--accent);color:var(--goldbg);font-weight:600}
+ .tabs button:hover{border-color:transparent;color:var(--text)}
+ .tabs button[aria-current="true"]:hover{color:var(--goldbg)}
+ .ctl{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
+ .ctl label{color:var(--muted);font-size:12.5px;display:flex;align-items:center;gap:6px}
+ .ctl input[type=checkbox]{width:auto;padding:0;accent-color:var(--accent)}
+
+ /* A report is a card, not a table row: it carries fifteen numbers and the point
+ is to read one incident, not to scan a column. */
+ .rep{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px;
+ background:var(--panel2)}
+ .rep.test{opacity:.62}
+ .rep .hd{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;margin-bottom:8px}
+ .rep .who{font-weight:600}
+ .rep .when{color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums}
+ .rep .box{color:var(--muted);font-size:12px;font-family:var(--mono)}
+ .facts{display:grid;gap:6px 18px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
+ font-size:13px}
+ .facts .f{display:flex;gap:6px;justify-content:space-between;
+ border-bottom:1px solid rgba(42,45,54,.6);padding-bottom:3px}
+ .facts .k{color:var(--muted);font-size:12px}
+ .facts .v{font-family:var(--mono);font-size:12.5px;text-align:right}
+ .facts .v.hi{color:var(--warn);font-weight:600}
+ .facts .v.lo{color:var(--muted)}
+ .facts .v.bad{color:var(--bad);font-weight:600}
+ details.raw{margin-top:8px}
+ details.raw summary{color:var(--muted);font-size:12px;cursor:pointer}
+ .form{display:grid;gap:10px;max-width:420px}
+ .form .lbl{color:var(--muted);font-size:11.5px;text-transform:uppercase;
+ letter-spacing:.06em;margin-bottom:3px}
@media (max-width:560px){ main{padding:12px;gap:12px} section{padding:12px} }
@@ -102,7 +144,12 @@
-
Amber · stav služeb
+
Amber · stav
+
+
+
+
+
@@ -119,6 +166,12 @@ var KEY = "amber.status.token";
var tok = sessionStorage.getItem(KEY) || "";
var timer = null;
+// View + filters survive a refresh; the token deliberately does not.
+var PREF = "amber.status.prefs";
+var prefs = { view: "overview", days: 14, showTests: false };
+try { Object.assign(prefs, JSON.parse(localStorage.getItem(PREF) || "{}")); } catch (_) {}
+function savePrefs(){ try { localStorage.setItem(PREF, JSON.stringify(prefs)); } catch (_) {} }
+
function el(id){ return document.getElementById(id); }
function esc(s){ return String(s == null ? "" : s).replace(/[&<>"']/g, function(c){
return ({"&":"&","<":"<",">":">",'"':""","'":"'"})[c]; }); }
@@ -150,6 +203,16 @@ async function api(path){
return r.json();
}
+// Collection reads go straight to the PocketBase REST API with the superuser
+// token the page already holds. No new server route: /api/status exists because
+// the Coolify and amber-api SECRETS must not reach the browser, which does not
+// apply to our own collections.
+async function records(coll, params){
+ var q = Object.keys(params).map(function(k){
+ return k + "=" + encodeURIComponent(params[k]); }).join("&");
+ return api("/api/collections/" + coll + "/records?" + q);
+}
+
function logout(){
sessionStorage.removeItem(KEY); tok = "";
if (timer) { clearInterval(timer); timer = null; }
@@ -173,6 +236,15 @@ el("loginBtn").onclick = async function(){
el("logout").onclick = logout;
el("refresh").onclick = function(){ load(); };
+Array.prototype.forEach.call(el("tabs").children, function(b){
+ b.onclick = function(){ prefs.view = b.getAttribute("data-view"); savePrefs(); syncTabs(); load(); };
+});
+function syncTabs(){
+ Array.prototype.forEach.call(el("tabs").children, function(b){
+ b.setAttribute("aria-current", b.getAttribute("data-view") === prefs.view ? "true" : "false");
+ });
+}
+
function panelServices(c){
if (!c || c.configured === false) {
return '
Buildy se nemusí shodovat mezi platformami — když se vydává jen ' +
+ "pro počítače, televize zůstane na starším čísle a nedostane výzvu k aktualizaci." +
+ "
Hodnocení „sekalo se" při nulových stallech je slepé místo, ' +
- "které počítadla nevidí — proto je zvlášť.
";
+ "které počítadla nevidí — proto je zvlášť. Podrobnosti jsou v Hlášení." +
+ "";
}
function panelApi(a){
@@ -251,7 +327,7 @@ function panelAccounts(a){
'
celkem
' + a.total + "
" +
'
clean
' + a.clean + "
" +
'
18+
' + a.adult + "
" +
- "";
+ '
Nový účet se zakládá ve Správě.
';
}
// Errors over time. Inline SVG rather than a charting library: this page is served
@@ -301,9 +377,6 @@ function panelErrors(d){
(showRate ? "
podíl
" : "") + "" + body + "";
}
- var flav = (d.byFlavour || []);
- var flavKnown = flav.some(function(f){ return f.key !== "neznámá"; });
-
return '