| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593 |
- <!doctype html>
- <html lang="pl">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta name="robots" content="noindex,nofollow">
- <title>EKSRelay — panel operacyjny</title>
- <style>
- :root {
- --bg: #f6f7f9; --panel: #ffffff; --border: #dfe3e8; --text: #1b1f24;
- --muted: #6a737d; --accent: #2f6feb; --accent-soft: #e8f0fe;
- --ok: #1a7f37; --warn: #9a6700; --bad: #b42318; --code-bg: #f2f4f7;
- }
- @media (prefers-color-scheme: dark) {
- :root {
- --bg: #0f1419; --panel: #171d24; --border: #2a323c; --text: #e6edf3;
- --muted: #8b949e; --accent: #4c8dff; --accent-soft: #16263f;
- --ok: #3fb950; --warn: #d29922; --bad: #f85149; --code-bg: #10161d;
- }
- }
- * { box-sizing: border-box; }
- body {
- margin: 0; background: var(--bg); color: var(--text);
- font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
- }
- header {
- display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
- padding: 12px 20px; background: var(--panel); border-bottom: 1px solid var(--border);
- position: sticky; top: 0; z-index: 10;
- }
- header h1 { font-size: 16px; margin: 0; font-weight: 650; letter-spacing: -0.01em; }
- .spacer { flex: 1; }
- .badge {
- display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px;
- border-radius: 999px; font-size: 12px; font-weight: 600;
- background: var(--accent-soft); color: var(--accent); white-space: nowrap;
- }
- .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
- .dot.ok { background: var(--ok); } .dot.bad { background: var(--bad); }
- button {
- font: inherit; padding: 6px 12px; border-radius: 6px; cursor: pointer;
- border: 1px solid var(--border); background: var(--panel); color: var(--text);
- }
- button:hover { border-color: var(--accent); color: var(--accent); }
- button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
- button.primary:hover { opacity: .9; color: #fff; }
- input, select {
- font: inherit; padding: 6px 10px; border-radius: 6px;
- border: 1px solid var(--border); background: var(--panel); color: var(--text);
- }
- main { padding: 20px; max-width: 1500px; margin: 0 auto; }
- .tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
- .tabs button.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
- .filters {
- display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;
- padding: 14px; background: var(--panel); border: 1px solid var(--border);
- border-radius: 8px; margin-bottom: 16px;
- }
- .field { display: flex; flex-direction: column; gap: 4px; }
- .field label { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; }
- .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
- .card-head {
- padding: 10px 14px; border-bottom: 1px solid var(--border);
- display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 13px;
- }
- .table-wrap { overflow-x: auto; }
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
- th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
- th { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; white-space: nowrap; }
- tbody tr:last-child td { border-bottom: none; }
- tbody tr:hover { background: var(--accent-soft); }
- td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
- .ts { white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums; }
- .pill {
- display: inline-block; padding: 2px 8px; border-radius: 999px;
- font-size: 11px; font-weight: 600; background: var(--code-bg); white-space: nowrap;
- }
- .pill.ok { color: var(--ok); } .pill.warn { color: var(--warn); } .pill.bad { color: var(--bad); }
- .meta { color: var(--muted); font-size: 12px; word-break: break-word; max-width: 420px; }
- .stats { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
- .stat {
- background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
- padding: 10px 14px; min-width: 96px;
- }
- .stat .k { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; }
- .stat .v { font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; }
- .empty, .error { padding: 28px; text-align: center; color: var(--muted); }
- .error { color: var(--bad); }
- #gate {
- position: fixed; inset: 0; background: var(--bg); display: flex;
- align-items: center; justify-content: center; padding: 20px; z-index: 100;
- }
- #gate .box {
- background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
- padding: 28px; width: 100%; max-width: 400px;
- }
- #gate h2 { margin: 0 0 6px; font-size: 17px; }
- #gate p { margin: 0 0 18px; color: var(--muted); font-size: 13px; }
- #gate input { width: 100%; margin-bottom: 12px; }
- #gate button { width: 100%; }
- #gate .err { color: var(--bad); font-size: 13px; margin-top: 12px; display: none; }
- .hint { color: var(--muted); font-size: 12px; margin-top: 14px; }
- [hidden] { display: none !important; }
- </style>
- </head>
- <body>
- <div id="gate">
- <div class="box">
- <h2>EKSRelay — panel operacyjny</h2>
- <p>Podgląd tylko do odczytu. Podaj <code>ADMIN_TOKEN</code>, żeby wczytać dane.</p>
- <div id="gate-form" role="group" aria-label="Logowanie do panelu" autocomplete="off">
- <input type="password" id="token-input" placeholder="ADMIN_TOKEN" autocomplete="off" spellcheck="false" required>
- <button type="button" id="login-button" class="primary">Zaloguj</button>
- </div>
- <div class="err" id="gate-err"></div>
- <div class="hint">
- Token trzymany jest wyłącznie w <code>sessionStorage</code> tej karty i wysyłany
- w nagłówku <code>Authorization</code>. Nigdy nie trafia do adresu URL ani do logów.
- </div>
- </div>
- </div>
- <div id="app" hidden>
- <header>
- <h1>EKSRelay</h1>
- <span class="badge" id="mode-badge">—</span>
- <span class="badge"><span class="dot" id="health-dot"></span><span id="health-text">health</span></span>
- <span class="badge"><span class="dot" id="ready-dot"></span><span id="ready-text">ready</span></span>
- <span class="badge" id="bot-badge">AI —</span>
- <button id="panic-off" title="Natychmiast zatrzymaj kolejkowanie AI">AI OFF</button>
- <button id="panic-on" class="primary" title="Włącz AI tylko dla nowych wiadomości od teraz">AI ON od teraz</button>
- <span class="spacer"></span>
- <label class="badge" style="cursor:pointer">
- <input type="checkbox" id="autorefresh"> auto 15 s
- </label>
- <button id="refresh">Odśwież</button>
- <button id="logout">Wyloguj</button>
- </header>
- <main>
- <div class="stats" id="stats"></div>
- <div class="tabs" id="tabs">
- <button data-tab="events" class="active">Audyt</button>
- <button data-tab="jobs">Kolejka</button>
- <button data-tab="messages">Wiadomości</button>
- <button data-tab="tickets">Tickety</button>
- <button data-tab="kb">KB</button>
- <button data-tab="ready">Zależności</button>
- </div>
- <div class="filters">
- <div class="field">
- <label for="f-conv">conversationId</label>
- <input id="f-conv" type="number" min="1" placeholder="wszystkie" style="width:140px">
- </div>
- <div class="field" id="f-type-wrap">
- <label for="f-type">typ eventu</label>
- <select id="f-type" style="width:200px"><option value="">wszystkie</option></select>
- </div>
- <div class="field" id="f-status-wrap" hidden>
- <label for="f-status">status</label>
- <select id="f-status" style="width:160px"><option value="">wszystkie</option></select>
- </div>
- <div class="field">
- <label for="f-limit">limit</label>
- <select id="f-limit" style="width:100px">
- <option>25</option><option selected>50</option><option>100</option><option>200</option>
- </select>
- </div>
- <button class="primary" id="apply">Zastosuj</button>
- <span class="spacer"></span>
- <span class="badge" id="updated">—</span>
- </div>
- <div class="card">
- <div class="card-head"><span id="card-title">Audyt</span><span class="spacer"></span><span class="pill" id="card-count"></span></div>
- <div class="table-wrap"><div id="content"><div class="empty">Ładowanie…</div></div></div>
- </div>
- </main>
- </div>
- <script data-cfasync="false">
- (function () {
- 'use strict';
- var KEY = 'eksrelay.adminToken';
- var tab = 'events';
- var timer = null;
- var $ = function (id) { return document.getElementById(id); };
- function token() { return sessionStorage.getItem(KEY) || ''; }
- // Every value rendered below goes through this. Audit summaries and job
- // errors are partly derived from customer-supplied text, so nothing is
- // injected as markup.
- function el(tag, cls, text) {
- var n = document.createElement(tag);
- if (cls) n.className = cls;
- if (text !== undefined && text !== null) n.textContent = String(text);
- return n;
- }
- function fmtTime(iso) {
- if (!iso) return '—';
- var d = new Date(iso);
- if (isNaN(d)) return String(iso);
- var p = function (n) { return String(n).padStart(2, '0'); };
- return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
- ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
- }
- function shorten(v, max) {
- if (v === null || v === undefined) return '—';
- var s = typeof v === 'string' ? v : JSON.stringify(v);
- return s.length > max ? s.slice(0, max) + '…' : s;
- }
- // Token travels in the header only — never as a query parameter, so it can
- // never end up in an access log, a proxy log or the browser history.
- function api(path, params) {
- var url = new URL(path, window.location.origin);
- Object.keys(params || {}).forEach(function (k) {
- if (params[k] !== '' && params[k] !== undefined && params[k] !== null) {
- url.searchParams.set(k, params[k]);
- }
- });
- return fetch(url.toString(), {
- headers: { Authorization: 'Bearer ' + token() },
- cache: 'no-store'
- }).then(function (r) {
- if (r.status === 401) { logout('Token odrzucony przez serwer.'); throw new Error('unauthorized'); }
- if (!r.ok) throw new Error('HTTP ' + r.status);
- return r.json();
- });
- }
- function apiPatch(path, body) {
- return fetch(path, {
- method: 'PATCH',
- headers: { Authorization: 'Bearer ' + token(), 'Content-Type': 'application/json' },
- body: JSON.stringify(body || {}),
- cache: 'no-store'
- }).then(function (r) {
- if (r.status === 401) { logout('Token odrzucony przez serwer.'); throw new Error('unauthorized'); }
- if (!r.ok) throw new Error('HTTP ' + r.status);
- return r.json();
- });
- }
- function logout(msg) {
- sessionStorage.removeItem(KEY);
- stopAuto();
- $('app').hidden = true;
- $('gate').hidden = false;
- if (msg) { $('gate-err').textContent = msg; $('gate-err').style.display = 'block'; }
- }
- // ---------------------------------------------------------------- renderers
- function table(cols, rows, build) {
- if (!rows.length) return el('div', 'empty', 'Brak rekordów dla tych filtrów.');
- var t = el('table');
- var thead = el('thead'), tr = el('tr');
- cols.forEach(function (c) {
- var th = el('th', c.num ? 'num' : null, c.label);
- tr.appendChild(th);
- });
- thead.appendChild(tr); t.appendChild(thead);
- var tb = el('tbody');
- rows.forEach(function (row) { tb.appendChild(build(row)); });
- t.appendChild(tb);
- return t;
- }
- function statusPill(status) {
- var cls = 'pill';
- if (status === 'done' || status === 'replied') cls += ' ok';
- else if (status === 'dead' || status === 'failed') cls += ' bad';
- else if (status === 'spam' || status === 'skipped' || status === 'ticket') cls += ' warn';
- return el('span', cls, status);
- }
- function eventPill(type) {
- var cls = 'pill';
- if (type === 'reply_sent' || type === 'webhook_accepted') cls += ' ok';
- else if (type === 'job_dead' || type === 'no_reply') cls += ' bad';
- else if (type.indexOf('ticket') !== -1 || type.indexOf('spam') !== -1 || type.indexOf('skipped') !== -1) cls += ' warn';
- return el('span', cls, type);
- }
- var VIEWS = {
- events: {
- title: 'Audyt decyzji bramki',
- filters: ['conv', 'type', 'limit'],
- load: function (f) { return api('/admin/events', { conversationId: f.conv, eventType: f.type, limit: f.limit }); },
- render: function (d) {
- return table(
- [{ label: 'Czas' }, { label: 'Typ' }, { label: 'conversationId', num: true },
- { label: 'messageId' }, { label: 'jobId' }, { label: 'Podsumowanie' }, { label: 'Meta' }],
- d.events,
- function (e) {
- var tr = el('tr');
- tr.appendChild(el('td', 'ts', fmtTime(e.createdAt)));
- var td = el('td'); td.appendChild(eventPill(e.eventType)); tr.appendChild(td);
- tr.appendChild(el('td', 'num mono', e.conversationId === null ? '—' : e.conversationId));
- tr.appendChild(el('td', 'mono', shorten(e.messageId, 24)));
- tr.appendChild(el('td', 'mono', shorten(e.jobId, 14)));
- tr.appendChild(el('td', null, e.summary));
- tr.appendChild(el('td', 'meta mono', e.meta ? shorten(e.meta, 160) : '—'));
- return tr;
- }
- );
- }
- },
- jobs: {
- title: 'Kolejka zadań',
- filters: ['status', 'limit'],
- statuses: ['queued', 'processing', 'done', 'dead'],
- load: function (f) { return api('/admin/jobs', { status: f.status, limit: f.limit }); },
- render: function (d) {
- return table(
- [{ label: 'Zaktualizowano' }, { label: 'Status' }, { label: 'Typ' },
- { label: 'Próby', num: true }, { label: 'Ponów po' }, { label: 'Ostatni błąd' }, { label: 'id' }],
- d.jobs,
- function (j) {
- var tr = el('tr');
- tr.appendChild(el('td', 'ts', fmtTime(j.updatedAt)));
- var td = el('td'); td.appendChild(statusPill(j.status)); tr.appendChild(td);
- tr.appendChild(el('td', null, j.type));
- tr.appendChild(el('td', 'num mono', j.attempts + '/' + j.maxAttempts));
- tr.appendChild(el('td', 'ts', j.status === 'queued' ? fmtTime(j.runAfter) : '—'));
- tr.appendChild(el('td', 'meta mono', j.lastError ? shorten(j.lastError, 200) : '—'));
- tr.appendChild(el('td', 'mono', shorten(j.id, 14)));
- return tr;
- }
- );
- }
- },
- messages: {
- title: 'Przetworzone wiadomości (idempotencja)',
- filters: ['conv', 'status', 'limit'],
- statuses: ['queued', 'processing', 'replied', 'skipped', 'ticket', 'failed', 'spam'],
- load: function (f) { return api('/admin/messages', { conversationId: f.conv, status: f.status, limit: f.limit }); },
- render: function (d) {
- return table(
- [{ label: 'Utworzono' }, { label: 'Status' }, { label: 'Powód' },
- { label: 'conversationId', num: true }, { label: 'messageId' }, { label: 'Źródło' }, { label: 'Zmieniono' }],
- d.messages,
- function (m) {
- var tr = el('tr');
- tr.appendChild(el('td', 'ts', fmtTime(m.createdAt)));
- var td = el('td'); td.appendChild(statusPill(m.status)); tr.appendChild(td);
- tr.appendChild(el('td', null, m.reason || '—'));
- tr.appendChild(el('td', 'num mono', m.conversationId));
- tr.appendChild(el('td', 'mono', shorten(m.messageId, 28)));
- tr.appendChild(el('td', null, m.source));
- tr.appendChild(el('td', 'ts', fmtTime(m.updatedAt)));
- return tr;
- }
- );
- }
- },
- tickets: {
- title: 'Tickety / handoff',
- filters: ['conv', 'limit'],
- load: function (f) { return api('/admin/tickets', { conversationId: f.conv, limit: f.limit }); },
- render: function (d) {
- return table(
- [{ label: 'Utworzono' }, { label: 'Numer ticketu' }, { label: 'conversationId', num: true },
- { label: 'Powód' }, { label: 'Ostatni event' }, { label: 'Kiedy' }, { label: 'Pominiętych od', num: true }],
- d.tickets,
- function (t) {
- var tr = el('tr');
- tr.appendChild(el('td', 'ts', fmtTime(t.createdAt)));
- tr.appendChild(el('td', 'mono', t.ticketNumber));
- tr.appendChild(el('td', 'num mono', t.conversationId));
- tr.appendChild(el('td', 'meta', shorten(t.reason, 90)));
- var td = el('td');
- if (t.lastEventType) td.appendChild(eventPill(t.lastEventType)); else td.textContent = '—';
- tr.appendChild(td);
- tr.appendChild(el('td', 'ts', fmtTime(t.lastEventAt)));
- tr.appendChild(el('td', 'num mono', t.messagesSkippedSince));
- return tr;
- }
- );
- }
- },
- kb: {
- title: 'Knowledge Base',
- filters: ['limit'],
- load: function () { return api('/admin/kb', { path: '/kb/status' }); },
- render: function (d) {
- var box = el('div', 'meta');
- box.style.maxWidth = 'none';
- box.style.padding = '14px';
- box.appendChild(el('div', null, d.ok ? 'KB API działa. Dane poniżej są bez embeddingów/sekretów.' : 'KB API niedostępne.'));
- box.appendChild(el('pre', 'mono', shorten(d.data, 4000)));
- return box;
- }
- },
- ready: {
- title: 'Zależności (/ready)',
- filters: [],
- load: function () { return fetch('/ready', { cache: 'no-store' }).then(function (r) { return r.json(); }); },
- render: function (d) {
- var deps = d.dependencies || {};
- var rows = Object.keys(deps).map(function (k) { return [k, deps[k]]; });
- return table(
- [{ label: 'Zależność' }, { label: 'Stan' }, { label: 'HTTP', num: true }, { label: 'Cel' }],
- rows,
- function (pair) {
- var tr = el('tr');
- tr.appendChild(el('td', null, pair[0]));
- var td = el('td');
- td.appendChild(el('span', 'pill ' + (pair[1].ok ? 'ok' : 'bad'), pair[1].ok ? 'ok' : 'błąd'));
- tr.appendChild(td);
- tr.appendChild(el('td', 'num mono', pair[1].status));
- tr.appendChild(el('td', 'mono', pair[1].target));
- return tr;
- }
- );
- }
- }
- };
- // -------------------------------------------------------------------- state
- function currentFilters() {
- return {
- conv: $('f-conv').value.trim(),
- type: $('f-type').value,
- status: $('f-status').value,
- limit: $('f-limit').value
- };
- }
- function syncFilterVisibility() {
- var v = VIEWS[tab];
- $('f-conv').parentElement.hidden = v.filters.indexOf('conv') === -1;
- $('f-type-wrap').hidden = v.filters.indexOf('type') === -1;
- $('f-status-wrap').hidden = v.filters.indexOf('status') === -1;
- $('f-limit').parentElement.hidden = v.filters.indexOf('limit') === -1;
- var sel = $('f-status');
- sel.innerHTML = '';
- sel.appendChild(new Option('wszystkie', ''));
- (v.statuses || []).forEach(function (s) { sel.appendChild(new Option(s, s)); });
- }
- function setContent(node) {
- var c = $('content');
- c.innerHTML = '';
- c.appendChild(node);
- }
- function loadTab() {
- var v = VIEWS[tab];
- $('card-title').textContent = v.title;
- setContent(el('div', 'empty', 'Ładowanie…'));
- v.load(currentFilters()).then(function (d) {
- setContent(v.render(d));
- var n = d.count !== undefined ? d.count : (d.dependencies ? Object.keys(d.dependencies).length : 0);
- $('card-count').textContent = n + ' rekordów';
- $('updated').textContent = 'odświeżono ' + fmtTime(new Date().toISOString());
- }).catch(function (err) {
- if (err.message === 'unauthorized') return;
- setContent(el('div', 'error', 'Błąd pobierania: ' + err.message));
- });
- }
- function loadHeader() {
- fetch('/health', { cache: 'no-store' })
- .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error(r.status)); })
- .then(function (d) {
- $('health-dot').className = 'dot ok';
- $('health-text').textContent = 'health ' + d.uptimeSeconds + 's';
- $('mode-badge').textContent = 'tryb: ' + d.mode + ' · v' + d.version;
- })
- .catch(function () { $('health-dot').className = 'dot bad'; $('health-text').textContent = 'health błąd'; });
- fetch('/ready', { cache: 'no-store' })
- .then(function (r) { return r.json(); })
- .then(function (d) {
- $('ready-dot').className = 'dot ' + (d.ok ? 'ok' : 'bad');
- var bad = Object.keys(d.dependencies || {}).filter(function (k) { return !d.dependencies[k].ok; });
- $('ready-text').textContent = bad.length ? 'ready: ' + bad.join(', ') : 'ready ok';
- })
- .catch(function () { $('ready-dot').className = 'dot bad'; $('ready-text').textContent = 'ready błąd'; });
- api('/admin/meta').then(function (d) {
- var sel = $('f-type'), keep = sel.value;
- sel.innerHTML = '';
- sel.appendChild(new Option('wszystkie', ''));
- (d.eventTypes || []).forEach(function (t) { sel.appendChild(new Option(t, t)); });
- sel.value = keep;
- var s = $('stats');
- s.innerHTML = '';
- var jobs = d.counts.jobs || {};
- [['Eventy', d.counts.auditEvents], ['Tickety', d.counts.tickets],
- ['W kolejce', jobs.queued || 0], ['Wykonane', jobs.done || 0], ['Dead', jobs.dead || 0]]
- .forEach(function (pair) {
- var box = el('div', 'stat');
- box.appendChild(el('div', 'k', pair[0]));
- var v = el('div', 'v', pair[1]);
- if (pair[0] === 'Dead' && pair[1] > 0) v.style.color = 'var(--bad)';
- box.appendChild(v);
- s.appendChild(box);
- });
- var st = d.settings || {};
- var rt = d.runtime || {};
- $('bot-badge').textContent = rt.botEnabled ? 'AI ON · max ' + rt.maxMessageAgeMinutes + 'm' : 'AI OFF';
- $('bot-badge').style.color = rt.botEnabled ? 'var(--ok)' : 'var(--bad)';
- var box = el('div', 'stat');
- box.appendChild(el('div', 'k', 'Ustawienia'));
- box.appendChild(el('div', 'meta',
- 'label=' + st.ticketLabel + ' · unassign=' + st.unassignOnTicket +
- ' · spamGate=' + st.spamGate + ' · worker=' + st.worker + ' · logPii=' + st.logPii));
- s.appendChild(box);
- }).catch(function () { /* handled by api() */ });
- }
- function refresh() { loadHeader(); loadTab(); }
- function startAuto() { stopAuto(); timer = setInterval(refresh, 15000); }
- function stopAuto() { if (timer) { clearInterval(timer); timer = null; } }
- function enterApp() {
- $('gate').hidden = true;
- $('app').hidden = false;
- syncFilterVisibility();
- refresh();
- }
- // ------------------------------------------------------------------- wiring
- function submitLogin() {
- var value = $('token-input').value.trim();
- if (!value) return;
- sessionStorage.setItem(KEY, value);
- $('token-input').value = '';
- $('gate-err').style.display = 'none';
- // Probe with a real request so a wrong token fails at the gate, not later.
- api('/admin/meta').then(enterApp).catch(function (err) {
- if (err.message !== 'unauthorized') {
- $('gate-err').textContent = 'Błąd połączenia: ' + err.message;
- $('gate-err').style.display = 'block';
- }
- });
- }
- $('login-button').addEventListener('click', submitLogin);
- $('token-input').addEventListener('keydown', function (e) {
- if (e.key === 'Enter') { e.preventDefault(); submitLogin(); }
- });
- $('tabs').addEventListener('click', function (e) {
- var btn = e.target.closest('button[data-tab]');
- if (!btn) return;
- Array.prototype.forEach.call($('tabs').children, function (b) { b.classList.remove('active'); });
- btn.classList.add('active');
- tab = btn.dataset.tab;
- syncFilterVisibility();
- loadTab();
- });
- $('apply').addEventListener('click', loadTab);
- $('f-conv').addEventListener('keydown', function (e) { if (e.key === 'Enter') loadTab(); });
- $('refresh').addEventListener('click', refresh);
- $('panic-off').addEventListener('click', function () {
- if (!confirm('Wyłączyć AI? Nowe wiadomości nie będą kolejkowane.')) return;
- apiPatch('/admin/runtime', { botEnabled: false }).then(refresh).catch(function (err) { alert('Błąd: ' + err.message); });
- });
- $('panic-on').addEventListener('click', function () {
- if (!confirm('Włączyć AI tylko dla wiadomości od teraz? Backlog zostanie odcięty.')) return;
- apiPatch('/admin/runtime', { botEnabled: true, enableFromNow: true, maxMessageAgeMinutes: 240 }).then(refresh).catch(function (err) { alert('Błąd: ' + err.message); });
- });
- $('logout').addEventListener('click', function () { logout(); });
- $('autorefresh').addEventListener('change', function (e) { e.target.checked ? startAuto() : stopAuto(); });
- if (token()) enterApp(); else $('token-input').focus();
- })();
- </script>
- </body>
- </html>
|