/* ═══════════════════════════════════════════════════════ Page 100 — service behaviour. Three things: the header clock, the page-number jump (the signature interaction), and the fastext colour keys. Plus guarded live polling for the two figures that must be current-second accurate. Every element lookup is guarded. Nine pages share this file and none of them carry all of its targets. ═══════════════════════════════════════════════════════ */ (function () { 'use strict'; var body = document.body; var BASE = body.dataset.urlBase || ''; var TZ = body.dataset.timezone || 'UTC'; /* ── Clock ──────────────────────────────────────────── Teletext's header carried the transmission clock to the second. It is the one thing on the page that is always moving, and it is how you know the page is live. */ var clockEl = document.getElementById('head-clock'); function tick() { if (!clockEl) return; var now = new Date(); var d = new Intl.DateTimeFormat('en-GB', { timeZone: TZ, weekday: 'short', day: '2-digit', month: 'short' }).format(now); var t = new Intl.DateTimeFormat('en-GB', { timeZone: TZ, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(now); clockEl.textContent = d.toUpperCase() + ' ' + t; } tick(); setInterval(tick, 1000); /* ── Page-number jump ───────────────────────────────── Type three digits anywhere and the service goes to that page, exactly as a teletext remote did. The digits fill in the header as you type them, so the interaction shows its own state instead of happening invisibly. */ var pages = {}; try { (JSON.parse(body.dataset.pages || '[]')).forEach(function (p) { pages[p[1]] = p[3]; // "100" -> "/" }); } catch (e) { /* no nav data: the jump simply does nothing */ } var entryEl = document.getElementById('page-entry'); var buf = ''; var clearTimer = null; function paintEntry() { if (!entryEl) return; if (!buf) { entryEl.hidden = true; entryEl.textContent = ''; return; } entryEl.hidden = false; entryEl.textContent = 'P' + buf + '_'.repeat(3 - buf.length); } function resetEntry() { buf = ''; paintEntry(); } function typing(e) { var t = e.target; return t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable); } document.addEventListener('keydown', function (e) { if (typing(e) || e.metaKey || e.ctrlKey || e.altKey) return; if (e.key === 'Escape') { resetEntry(); return; } if (e.key >= '0' && e.key <= '9') { buf += e.key; paintEntry(); clearTimeout(clearTimer); if (buf.length === 3) { var dest = pages[buf]; if (dest) { window.location.href = BASE + dest; } else { // No such page. Teletext just sat on the number and waited. if (entryEl) entryEl.textContent = 'P' + buf + ' ?'; clearTimer = setTimeout(resetEntry, 1200); } buf = ''; return; } // A half-typed number times out rather than lingering. clearTimer = setTimeout(resetEntry, 2000); return; } /* Fastext keys: r / g / y / c, the coloured buttons. */ var k = e.key.toLowerCase(); if ('rgyc'.indexOf(k) !== -1) { var link = document.querySelector('.fastext a[data-key="' + k + '"]'); if (link) { window.location.href = link.href; } } }); /* ── Two-tap add ────────────────────────────────────── Adding to the library is destructive enough to want a confirming second press. The button states its own stage rather than opening a dialog. */ var armed = null; function disarm() { if (!armed) return; armed.classList.remove('armed'); armed.textContent = armed.dataset.label || '+ ADD'; armed = null; } window.armAdd = function (btn) { if (btn.disabled) return; if (armed && armed !== btn) disarm(); if (armed === btn) { var title = btn.dataset.title; var target = btn.dataset.target; btn.disabled = true; btn.classList.remove('armed'); btn.textContent = 'SENDING'; fetch(BASE + '/add-to-' + target, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title }) }) .then(function (r) { return r.json(); }) .then(function (d) { if (d && d.ok) { btn.classList.add('done'); btn.textContent = 'ADDED'; } else { btn.classList.add('armed'); btn.disabled = false; btn.textContent = 'FAILED'; } }) .catch(function () { btn.disabled = false; btn.classList.add('armed'); btn.textContent = 'FAILED'; }); armed = null; return; } btn.dataset.label = btn.textContent.trim(); btn.classList.add('armed'); btn.textContent = 'CONFIRM?'; armed = btn; }; document.addEventListener('click', function (e) { if (armed && !armed.contains(e.target)) disarm(); }); /* ── Live figures ───────────────────────────────────── The queue and the active streams are the only two numbers that must be accurate to the second, so they are the only two that poll. Everything else is cached server-side and arrives with the page. */ function setText(id, value) { var el = document.getElementById(id); if (el) el.textContent = value; } function pollSab() { if (!document.getElementById('sab-queue')) return; fetch(BASE + '/api/sabnzbd') .then(function (r) { return r.json(); }) .then(function (d) { if (!d || d.status !== 'ok') return; setText('sab-queue', d.queue_count); setText('sab-speed', d.speed || '0'); setText('sab-remaining', d.sizeleft || '—'); }) .catch(function () { /* offline is the dev baseline, not an error */ }); } function pollTautulli() { if (!document.getElementById('tt-streams')) return; fetch(BASE + '/api/tautulli') .then(function (r) { return r.json(); }) .then(function (d) { if (!d || d.status !== 'ok') return; setText('tt-streams', d.active_streams); }) .catch(function () { /* as above */ }); } if (document.getElementById('sab-queue')) { pollSab(); setInterval(pollSab, 5000); } if (document.getElementById('tt-streams')) { pollTautulli(); setInterval(pollTautulli, 15000); } })();