diff --git a/webui/app.js b/webui/app.js index 98192bd..37a11ed 100644 --- a/webui/app.js +++ b/webui/app.js @@ -68,7 +68,34 @@ function setBusy(button, busy) { } function confirmAction(message) { - return window.confirm(message); + return new Promise((resolve) => { + let settled = false; + const done = (value) => { + if (settled) return; + settled = true; + if (dialog.open) dialog.close(); + resolve(value); + }; + let dialog = document.getElementById('confirm-dialog'); + if (!dialog) { + dialog = document.createElement('dialog'); + dialog.id = 'confirm-dialog'; + dialog.className = 'confirm-dialog'; + dialog.innerHTML = + '
Confirm
' + + '

' + + '
' + + '' + + '' + + '
'; + document.body.appendChild(dialog); + } + dialog.querySelector('.confirm-dialog-message').textContent = message; + dialog.querySelector('[data-confirm-ok]').onclick = () => done(true); + dialog.querySelector('[data-confirm-cancel]').onclick = () => done(false); + dialog.onclose = () => done(false); + if (!dialog.open) dialog.showModal(); + }); } function showToast(message, type = 'info') { @@ -202,7 +229,7 @@ function renderAccountPanel(accountId) { (item) => item.value, ); if (!runAllTracked && channels.length === 0 && enabled) { - if (!confirmAction('Continuous scraping enabled with no selected channels. Save anyway?')) return; + if (!(await confirmAction('Continuous scraping enabled with no selected channels. Save anyway?'))) return; } const resp = await api(`/api/accounts/${accountId}/continuous`, { method: 'POST', @@ -289,15 +316,17 @@ function updateSidebarAccount() { const acc = state.accounts.find((a) => a.id === state.activeAccount); const nameEl = document.getElementById('active-account-name'); const statusEl = document.getElementById('active-account-status'); + if (!nameEl || !statusEl) return; + statusEl.classList.remove('is-ok', 'is-error', 'is-dim'); if (acc) { nameEl.textContent = acc.label || acc.id; const authOk = isAccountAuthorized(acc.auth); statusEl.textContent = authOk ? 'Authorized session' : 'Needs login'; - statusEl.style.color = authOk ? 'var(--ok)' : 'var(--danger)'; + statusEl.classList.add(authOk ? 'is-ok' : 'is-error'); } else { nameEl.textContent = 'None'; statusEl.textContent = 'Add an account in Settings'; - statusEl.style.color = 'var(--dim)'; + statusEl.classList.add('is-dim'); } } @@ -313,7 +342,7 @@ function renderChannels(accountId, channels) { if (!channels.length) { const row = document.createElement('tr'); row.innerHTML = - '
No tracked channels yet. Add an ID or @username to start scraping this account.
'; + '

No tracked channels yet. Add an ID or @username to start scraping this account.

'; tbody.appendChild(row); } @@ -354,7 +383,7 @@ function renderChannels(accountId, channels) { }); node.querySelector('.remove-btn').addEventListener('click', async () => { - if (!confirmAction(`Remove ${channel.name} from tracked channels?`)) return; + if (!(await confirmAction(`Remove ${channel.name} from tracked channels?`))) return; await api(`/api/accounts/${accountId}/channels/remove`, { method: 'POST', body: JSON.stringify({ channel_id: channel.channel_id }), @@ -379,7 +408,7 @@ function renderJobs(accountId, jobs) { root.innerHTML = ''; if (!jobs.length) { - root.innerHTML = '
No jobs yet. Start a scrape or export to see progress here.
'; + root.innerHTML = '

No jobs yet. Start a scrape or export to see progress here.

'; return; } @@ -628,8 +657,10 @@ function renderSummary(accountId, data) { const toggle = panel.querySelector('.scrape-media-label'); toggle.textContent = d.scrape_media ? 'ON' : 'OFF'; const healthOk = health.api_credentials && health.session_ready && health.data_dir_exists; - panel.querySelector('.account-health-label').textContent = healthOk ? 'Ready' : 'Check'; - panel.querySelector('.account-health-label').style.color = healthOk ? 'var(--ok)' : 'var(--warn)'; + const healthLabel = panel.querySelector('.account-health-label'); + healthLabel.textContent = healthOk ? 'Ready' : 'Check'; + healthLabel.classList.remove('is-ok', 'is-warn'); + healthLabel.classList.add(healthOk ? 'is-ok' : 'is-warn'); panel.querySelector('.account-health-detail').textContent = [ `${health.message_count || 0} messages`, `${health.media_count || 0} media`, @@ -721,7 +752,9 @@ async function loadAccounts() { function renderSettingsAccounts() { const container = document.getElementById('accounts-list'); + if (!container) return; const template = document.getElementById('account-list-item-template'); + if (!template) return; container.innerHTML = ''; state.accounts.forEach((acc) => { @@ -732,12 +765,12 @@ function renderSettingsAccounts() { const authStatus = node.querySelector('.account-list-auth-status'); const authOk = isAccountAuthorized(acc.auth); authStatus.textContent = authOk ? 'Authorized' : 'Needs auth'; - authStatus.style.color = authOk ? 'var(--ok)' : 'var(--danger)'; - authStatus.style.fontSize = '0.78rem'; + authStatus.classList.remove('is-ok', 'is-error'); + authStatus.classList.add(authOk ? 'is-ok' : 'is-error'); node.querySelector('.account-select-btn').addEventListener('click', () => { switchAccount(acc.id); - document.getElementById('settings-dialog').close(); + document.getElementById('settings-dialog')?.close(); }); node.querySelector('.account-export-btn').addEventListener('click', async () => { @@ -751,7 +784,7 @@ function renderSettingsAccounts() { }); node.querySelector('.account-remove-btn').addEventListener('click', async () => { - if (!confirmAction(`Remove account "${acc.label || acc.id}"? All its data will be deleted.`)) return; + if (!(await confirmAction(`Remove account "${acc.label || acc.id}"? All its data will be deleted.`))) return; try { const removingActive = state.activeAccount === acc.id; await api(`/api/accounts/${acc.id}`, { method: 'DELETE' }); @@ -773,6 +806,7 @@ function renderSettingsAccounts() { function updateAuthSection(accountId) { const label = document.getElementById('auth-account-label'); + if (!label) return; const acc = state.accounts.find((a) => a.id === accountId); label.textContent = acc ? acc.label || acc.id : '-'; } @@ -836,7 +870,7 @@ async function submitPassword(accountId) { async function scrapeAll() { if (!state.activeAccount) return; - if (!confirmAction('Queue scraping for all tracked channels?')) return; + if (!(await confirmAction('Queue scraping for all tracked channels?'))) return; await api(`/api/accounts/${state.activeAccount}/jobs/scrape`, { method: 'POST', body: JSON.stringify({}), @@ -846,7 +880,7 @@ async function scrapeAll() { async function exportAll() { if (!state.activeAccount) return; - if (!confirmAction('Queue export for all tracked channels?')) return; + if (!(await confirmAction('Queue export for all tracked channels?'))) return; await api(`/api/accounts/${state.activeAccount}/jobs/export`, { method: 'POST', body: JSON.stringify({}), @@ -901,25 +935,18 @@ async function main() { } }); - // ── Settings dialog ── + // ── Legacy settings dialog (removed from dashboard; Settings page owns auth) ── + // Kept guarded so old markup, if present, never throws. const settingsDialog = document.getElementById('settings-dialog'); - const openSettingsBtn = document.getElementById('open-settings-btn'); - if (!openSettingsBtn.matches('a[href]')) { - openSettingsBtn.addEventListener('click', () => { - // Update auth section for active account - if (state.activeAccount) { - updateAuthSection(state.activeAccount); - } - renderSettingsAccounts(); - settingsDialog.showModal(); - }); + const closeSettingsBtn = document.getElementById('close-settings-btn'); + if (settingsDialog && closeSettingsBtn) { + closeSettingsBtn.addEventListener('click', () => settingsDialog.close()); } - document.getElementById('close-settings-btn').addEventListener('click', () => { - settingsDialog.close(); - }); - // ── Add account form ── - document.getElementById('add-account-form').addEventListener('submit', async (event) => { + // ── Add account form (only when legacy dialog markup exists) ── + const addAccountForm = document.getElementById('add-account-form'); + if (addAccountForm) { + addAccountForm.addEventListener('submit', async (event) => { event.preventDefault(); const accountId = document.getElementById('add-account-id').value.trim(); const label = document.getElementById('add-account-label').value.trim(); @@ -945,9 +972,12 @@ async function main() { } catch (err) { showToast('Failed to add account: ' + err.message, 'error'); } - }); + }); + } - document.getElementById('import-account-file').addEventListener('change', async (event) => { + const importAccountFile = document.getElementById('import-account-file'); + if (importAccountFile) { + importAccountFile.addEventListener('change', async (event) => { const file = event.currentTarget.files?.[0]; if (!file) return; try { @@ -966,10 +996,16 @@ async function main() { } catch (err) { showToast(`Failed to import account: ${err.message}`, 'error'); } - }); + }); + } - // ── Credentials form ── - document.getElementById('credentials-form').addEventListener('submit', async (event) => { + // ── Credentials / auth forms (only when legacy dialog markup exists) ── + const bindOptional = (id, evt, handler) => { + const el = document.getElementById(id); + if (el) el.addEventListener(evt, handler); + }; + + bindOptional('credentials-form', 'submit', async (event) => { event.preventDefault(); if (!state.activeAccount) return; try { @@ -980,7 +1016,7 @@ async function main() { }); // ── QR login ── - document.getElementById('start-qr-btn').addEventListener('click', async (event) => { + bindOptional('start-qr-btn', 'click', async (event) => { setBusy(event.currentTarget, true); try { if (!state.activeAccount) return; @@ -993,7 +1029,7 @@ async function main() { }); // ── Phone ── - document.getElementById('phone-form').addEventListener('submit', async (event) => { + bindOptional('phone-form', 'submit', async (event) => { event.preventDefault(); if (!state.activeAccount) return; try { @@ -1004,7 +1040,7 @@ async function main() { }); // ── Code ── - document.getElementById('code-form').addEventListener('submit', async (event) => { + bindOptional('code-form', 'submit', async (event) => { event.preventDefault(); if (!state.activeAccount) return; try { @@ -1015,7 +1051,7 @@ async function main() { }); // ── Password ── - document.getElementById('password-form').addEventListener('submit', async (event) => { + bindOptional('password-form', 'submit', async (event) => { event.preventDefault(); if (!state.activeAccount) return; try { @@ -1026,7 +1062,7 @@ async function main() { }); // ── Media toggle ── - document.getElementById('scrape-media-toggle').addEventListener('change', async (event) => { + bindOptional('scrape-media-toggle', 'change', async (event) => { const checked = event.currentTarget.checked; try { await toggleMedia(checked); diff --git a/webui/index.html b/webui/index.html index c6034d5..6e75695 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4,7 +4,7 @@ Telegram Scraper Control Panel - +
@@ -43,10 +43,6 @@ API Docs - Health
@@ -90,87 +86,17 @@

+
+
+ +
+
- - -
-
-
-
Settings
-

Global & Accounts

-
- -
- - -
-
Accounts
-
- - - -
- - -
-
Account Auth: -
-
- - - -
- -
- -
- - - -
- - -
- - - - -
- - -
-
Scraping
- -
-
-
- @@ -239,25 +165,38 @@

Per-account channel list.

- - + +
-
- - - - - - - - - - - -
ChannelMessagesMediaLast messageActions
+
+
+ + + + + + + + + + + +
ChannelMessagesMediaLast messageActions
+
@@ -269,7 +208,9 @@
-
+
+
+
@@ -283,40 +224,42 @@ -
- - - -
-
Continuous channel set
-
+
+ + + + +
+
Continuous channel set
+
+
+ + + +
+
Status: -
+
Last run: -
+
Next run: -
+
Last error: -
- - -
-
Status: -
-
Last run: -
-
Next run: -
-
Last error: -
+
- -
@@ -372,6 +315,6 @@
- + diff --git a/webui/settings.html b/webui/settings.html index 66d0f75..0973b4a 100644 --- a/webui/settings.html +++ b/webui/settings.html @@ -4,23 +4,45 @@ Telegram Scraper Settings - +
+
+
Settings
+
+ Service: Running Scraper: Idle +
+
Account Settings

Loading accounts

Select an account to manage credentials and scraper options.

+

+ First step: add your first account below. Export and Delete unlock after you select an account. +

@@ -44,8 +76,12 @@ @@ -76,17 +112,49 @@

New accounts start without continuous scraping enabled.

+
- - - - - + + + + +
+
@@ -96,9 +164,20 @@

No account selected.

+
+

+ Select an account first, then save API credentials and complete login below. +

- - + +
@@ -112,19 +191,35 @@
- +
+
@@ -134,6 +229,7 @@

Account-level parser settings.

+
+ - + diff --git a/webui/settings.js b/webui/settings.js index 2fd62f6..cf43a79 100644 --- a/webui/settings.js +++ b/webui/settings.js @@ -38,6 +38,42 @@ function showToast(message, type = 'info') { }, 3600); } +function confirmAction(message) { + return new Promise((resolve) => { + let settled = false; + const done = (value) => { + if (settled) return; + settled = true; + if (dialog.open) dialog.close(); + resolve(value); + }; + let dialog = document.getElementById('confirm-dialog'); + if (!dialog) { + dialog = document.createElement('dialog'); + dialog.id = 'confirm-dialog'; + dialog.className = 'confirm-dialog'; + dialog.innerHTML = + '
Confirm
' + + '

' + + '
' + + '' + + '' + + '
'; + document.body.appendChild(dialog); + } + dialog.querySelector('.confirm-dialog-message').textContent = message; + dialog.querySelector('[data-confirm-ok]').onclick = () => done(true); + dialog.querySelector('[data-confirm-cancel]').onclick = () => done(false); + dialog.onclose = () => done(false); + if (!dialog.open) dialog.showModal(); + }); +} + +function setHealthState(el, ok, warnOnly = false) { + el.classList.remove('is-ok', 'is-warn', 'is-dim'); + el.classList.add(ok ? 'is-ok' : warnOnly ? 'is-dim' : 'is-warn'); +} + function downloadJson(filename, payload) { const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], { type: 'application/json', @@ -105,7 +141,7 @@ function renderAccountList() { root.innerHTML = ''; if (!settingsState.accounts.length) { - root.innerHTML = '
No accounts yet.
'; + root.innerHTML = '

No accounts yet.

'; return; } @@ -168,17 +204,19 @@ function renderAccountData() { : 'No account selected.'; document.getElementById('health-credentials').textContent = health.api_credentials ? 'Saved' : 'Missing'; - document.getElementById('health-credentials').style.color = health.api_credentials ? 'var(--ok)' : 'var(--warn)'; + setHealthState(document.getElementById('health-credentials'), Boolean(health.api_credentials)); document.getElementById('health-session').textContent = health.session_ready || isAccountAuthorized(auth) ? 'Ready' : 'Missing'; - document.getElementById('health-session').style.color = - health.session_ready || isAccountAuthorized(auth) ? 'var(--ok)' : 'var(--warn)'; + setHealthState( + document.getElementById('health-session'), + Boolean(health.session_ready || isAccountAuthorized(auth)), + ); document.getElementById('health-continuous').textContent = continuousStatus.running ? 'Running' : continuousConfig.enabled ? 'Enabled' : 'Stopped'; - document.getElementById('health-continuous').style.color = continuousStatus.running ? 'var(--ok)' : 'var(--dim)'; + setHealthState(document.getElementById('health-continuous'), Boolean(continuousStatus.running), true); document.getElementById('health-last-scrape').textContent = displayTime( health.last_scrape || continuousStatus.last_iteration_at, ); @@ -342,7 +380,7 @@ async function exportAccount() { async function deleteAccount() { if (!settingsState.activeAccount) return; const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount); - if (!window.confirm(`Remove account "${accountLabel(account)}"? All its account data will be deleted.`)) return; + if (!(await confirmAction(`Remove account "${accountLabel(account)}"? All its account data will be deleted.`))) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}`, { method: 'DELETE' }); setActiveAccount(null); await loadAccounts(); diff --git a/webui/style.css b/webui/style.css index f698dcd..26b08fc 100644 --- a/webui/style.css +++ b/webui/style.css @@ -1,27 +1,57 @@ +/* Telegram Scraper WebUI — single solid design system (opaque solid colors only). + One cascade layer. Opaque hex palette. Two radii levels + badge-only pill. */ + :root { color-scheme: dark; - --bg-color: #000000; - --panel: #0b0b0b; - --panel-solid: #101010; - --panel-alt: #050505; - --text-color: #f3f5f8; - --accent: #2f57b8; - --accent-hover: #3b63c6; - --accent-soft: #0d1730; - --accent-line: #24428d; - --accent-2: #6f95eb; - --bubble-own: #0f1c3f; - --bubble-other: #111111; - --dim: #9ea7b7; - --line: #1f1f1f; - --danger: #ff7373; + + /* Core opaque palette */ + --bg: #0c0c0d; + --panel: #141414; + --panel-alt: #0f0f10; + --inset: #0e0e0e; + --border: #3a3a3a; + --border-strong: #4a4a4a; + --text: #f2f2f2; + --dim: #a8a8a8; + --muted: #8f8f8f; + --accent: #f1f1f1; + --accent-ink: #111111; + --link: #6ea8fe; + --danger: #ff6b6b; + --danger-border: #7a2e2e; + --danger-bg: #2a1212; + --danger-bg-hover: #3a1616; --ok: #65e6a4; --warn: #f6c969; --info: #8ec7ff; - --radius: 22px; - --shadow: none; - --font-ui: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; + + /* Viewer bubbles (out of redesign scope — kept, opaque hex) */ + --bubble-own: #0f1c3f; + --bubble-other: #111111; + + /* Radii: panel/dialog/toast 10px, controls 8px, badges 999px only */ + --radius-panel: 10px; + --radius-control: 8px; + --radius-badge: 999px; + + /* Gutters */ + --gutter: 18px; + --gap: 16px; + + --font-ui: Inter, 'SF Pro Display', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; --font-mono: 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace; + + /* Legacy aliases (single system — all map to opaque tokens) */ + --bg-color: var(--bg); + --panel-solid: var(--panel); + --text-color: var(--text); + --line: var(--border); + --accent-hover: #ffffff; + --accent-soft: #1c1c1d; + --accent-line: var(--border-strong); + --accent-2: #d8d8d8; + + --sidebar-width: 244px; } * { @@ -30,88 +60,144 @@ body { margin: 0; - background: var(--bg-color); - color: var(--text-color); + background: var(--bg); + color: var(--text); font-family: var(--font-ui); - font-size: 15px; - line-height: 1.5; + font-size: 14px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +h1 { + margin: 0; + font-size: 20px; + font-weight: 700; + line-height: 1.35; + letter-spacing: -0.02em; +} + +h2 { + margin: 0; + font-size: 17px; + font-weight: 600; + line-height: 1.35; + letter-spacing: -0.02em; +} + +p { + margin: 0; } a { - color: inherit; + color: var(--link); text-decoration: none; - border-bottom: 1px solid var(--dim); } -a:hover, -button:hover { - border-color: #2b2b2b; - background: #111111; - color: var(--text-color); +a:hover { + text-decoration: underline; } code, pre, input, -button { +button, +select { font: inherit; } +:focus-visible { + outline: 2px solid var(--link); + outline-offset: 2px; +} + +/* Type scale */ +.eyebrow, +.section-title { + color: var(--muted); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.muted { + color: var(--muted); + font-size: 13px; + line-height: 1.55; +} + +.small { + font-size: 12px; + line-height: 1.5; +} + +/* ---------- App shell / sidebar / content ---------- */ + .app-shell { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + width: 100vw; + max-width: 100vw; + min-width: 0; + min-height: 100vh; height: 100vh; overflow: hidden; - display: grid; - grid-template-columns: 360px minmax(0, 1fr); } .sidebar { - background: var(--panel-alt); - border-right: 1px solid var(--line); - padding: 30px 24px; + width: var(--sidebar-width); + position: sticky; + top: 0; + height: 100vh; overflow-y: auto; + padding: var(--gutter) 12px; + background: var(--panel-alt); + border-right: 1px solid var(--border); } .content { overflow-y: auto; - padding: 32px; - max-width: 1440px; + min-width: 0; width: 100%; + max-width: 1280px; + margin-inline: auto; + padding: 20px 20px 28px; } .brand-block { - padding: 0 2px; + position: relative; + display: block; + min-height: 62px; + padding: 5px 9px 14px; } -.brand-block h1, -.dialog-header h2 { - margin: 0; - color: var(--accent); - font-size: 1.65rem; - letter-spacing: -0.04em; +.brand-icon { + position: absolute; + top: 9px; + left: 9px; + width: 27px; + height: 27px; + fill: none; + stroke: var(--accent); + stroke-width: 1.65; + stroke-linecap: round; + stroke-linejoin: round; +} + +.brand-block .eyebrow { + margin: 0 0 3px 37px; +} + +.brand-block h1 { + margin-left: 37px; } .brand-block .muted { - margin: 22px 0 0; - max-width: 26ch; - font-size: 1.03rem; - line-height: 1.62; + margin: 8px 0 0 37px; + max-width: 30ch; } -.eyebrow, -.section-title { - color: var(--dim); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.14em; -} - -.muted { - color: var(--dim); -} - -.small { - font-size: 0.82rem; -} +/* ---------- Navigation ---------- */ .nav-links, .jobs-list, @@ -121,107 +207,68 @@ button { } .nav-links { - margin: 34px 0; -} - -.nav-link, -.button, -input, -.content-tab, -.viewer-channel-item { - border: 1px solid var(--line); - background: #101010; - color: var(--text-color); - border-radius: 999px; -} - -a.nav-link, -a.button, -a.content-tab { - border-bottom: 1px solid var(--line); -} - -.nav-link, -.button, -.content-tab { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 42px; - padding: 10px 18px; - cursor: pointer; - transition: - background-color 0.18s ease, - border-color 0.18s ease, - box-shadow 0.18s ease, - transform 0.18s ease; + gap: 5px; + margin: 6px 0 0; } .nav-link { - min-height: 52px; - font-size: 1rem; + display: inline-flex; + align-items: center; + gap: 12px; + min-height: 45px; + padding: 0 13px; + border: 1px solid transparent; + border-radius: var(--radius-control); + background: transparent; + color: var(--dim); + font-size: 13px; + font-weight: 650; + cursor: pointer; } -.nav-link.active, -.button.primary, -.content-tab.active { - border-color: var(--accent-line); - color: #ffffff; - background: var(--accent); - box-shadow: none; +.nav-link:hover { + background: #1c1c1d; + border-color: var(--border); + color: var(--text); + text-decoration: none; } -.nav-link.active:hover, -.button.primary:hover, -.content-tab.active:hover { - background: var(--accent-hover); - color: #ffffff; +.nav-link.active { + background: #202020; + border-color: transparent; + color: var(--text); } -.button.danger { - color: var(--danger); +.nav-icon { + display: inline-grid; + flex: 0 0 20px; + width: 20px; + height: 20px; + place-items: center; + fill: none; + stroke: currentColor; + stroke-width: 1.7; + stroke-linecap: round; + stroke-linejoin: round; + font-size: 17px; + line-height: 1; } -.button.button-small { - min-height: 34px; - padding: 6px 10px; - font-size: 0.82rem; -} - -.button:disabled { - cursor: not-allowed; - opacity: 0.45; -} - -.status-card, -.panel, -.stat-panel, -.job-card, -.message-card, -.settings-dialog { - background: var(--panel); - border: 1px solid var(--line); - border-radius: var(--radius); - box-shadow: var(--shadow); -} - -.status-card, -.panel, -.job-card, -.message-card { - padding: 22px; -} +/* ---------- Sidebar status cards ---------- */ .status-card { - margin-top: 20px; display: grid; gap: 12px; align-content: start; + margin-top: 20px; + padding: 15px 10px 5px; + background: transparent; + border: 0; + border-top: 1px solid var(--border); } -.status-card .button { - width: 100%; - margin-top: 0; +.status-card:first-of-type { + margin-top: auto; } .status-card .section-title, @@ -231,20 +278,150 @@ a.content-tab { margin-bottom: 0; } +.status-card .button { + width: 100%; +} + .status-badge { display: inline-flex; padding: 7px 12px; - border: 1px solid var(--line); - border-radius: 999px; - color: var(--ok); - background: #0d1410; + border: 1px solid var(--border); + border-radius: var(--radius-badge); + background: var(--inset); + color: var(--text); + font-size: 12px; + font-weight: 700; } -.content-tabs { +/* ---------- Topbar ---------- */ + +.dashboard-topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + min-height: 58px; + margin: -20px -20px 16px; + padding: 0 20px; + border-bottom: 1px solid var(--border); +} + +.dashboard-title, +.runtime-status { + display: inline-flex; + align-items: center; + gap: 12px; + color: var(--text); + font-size: 14px; + font-weight: 650; +} + +.dashboard-title { + gap: 22px; +} + +.menu-icon { + color: var(--dim); + font-size: 21px; + font-weight: 400; +} + +.runtime-status { + gap: 13px; + color: var(--dim); + font-size: 12px; + font-weight: 500; +} + +.runtime-status i { + width: 1px; + height: 17px; + background: var(--border-strong); +} + +.runtime-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--ok); +} + +.runtime-clock::before { + content: '◷'; + color: var(--dim); + font-size: 17px; +} + +/* ---------- Tabs ---------- */ + +.content-tabs, +.account-tabs { display: flex; flex-wrap: wrap; - gap: 8px; - margin-bottom: 16px; + gap: 0; + margin-bottom: var(--gap); + padding-bottom: 0; + border-bottom: 0; +} + +.account-tabs { + padding-bottom: 14px; + border-bottom: 1px solid var(--border); +} + +.content-tab, +.account-tab { + min-height: 36px; + padding: 7px 14px; + border: 1px solid var(--border); + border-radius: 0; + background: var(--panel-alt); + color: var(--dim); + font-size: 13px; + cursor: pointer; +} + +.content-tab:first-child, +.account-tab:first-child { + border-radius: var(--radius-control) 0 0 var(--radius-control); +} + +.content-tab + .content-tab, +.account-tab + .account-tab { + margin-left: -1px; +} + +.content-tab:last-child, +.account-tab:last-child { + border-radius: 0 var(--radius-control) var(--radius-control) 0; +} + +.content-tab:hover, +.account-tab:hover { + background: #1c1c1d; + color: var(--text); +} + +.content-tab.active, +.account-tab.active { + position: relative; + z-index: 1; + border-color: var(--border-strong); + background: #202020; + color: var(--text); +} + +.account-tab .account-tab-status { + font-size: 12px; + color: var(--muted); +} + +.account-tab .account-tab-status.ok { + color: var(--ok); +} + +.account-tab .account-tab-status.errored { + color: var(--danger); } .tab-panel { @@ -255,57 +432,467 @@ a.content-tab { display: block; } +.account-panel { + display: block; +} + +.account-panel.hidden { + display: none; +} + +/* ---------- Panels (single level: border + bg + radius only here) ---------- */ + +.panel, +.stat-panel, +.job-card, +.message-card, +.settings-dialog { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius-panel); +} + +.panel { + margin-bottom: var(--gap); + padding: 0; + overflow: hidden; +} + .panel-grid { display: grid; - gap: 18px; - grid-template-columns: repeat(3, minmax(0, 1fr)); - margin-bottom: 18px; + gap: var(--gap); + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-bottom: var(--gap); } .settings-health-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } -.panel { - margin-bottom: 18px; -} - .panel-header, .dialog-header, .job-head { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 16px; + gap: var(--gap); +} + +.panel-header { + min-height: 52px; + padding: 14px var(--gutter); + border-bottom: 1px solid var(--border); +} + +/* Panel subtitles are always visible (muted returns) */ +.panel-header .muted { + display: block; + margin: 4px 0 0; +} + +/* Single body wrapper: the only padded content zone inside .panel */ +.panel-body { + display: grid; + gap: 12px; + padding: 0 var(--gutter) var(--gutter); +} + +.panel-body:first-child { + padding-top: var(--gutter); +} + +.panel-header + .panel-body { + padding-top: var(--gutter); +} + +.panel-body > *:first-child { + margin-top: 0; +} + +/* Stat panels keep their own padding (no header/body split) */ +.stat-panel { + position: relative; + min-height: 208px; + padding: var(--gutter) var(--gutter) 16px; } .stat-value { - margin-top: 10px; - font-size: 2.25rem; - font-weight: 700; - letter-spacing: -0.05em; + margin-top: 20px; + font-size: 27px; + font-weight: 500; + letter-spacing: -0.04em; } .stat-compact { - font-size: 1.3rem; + font-size: 19px; } +.stat-caption { + margin-top: 3px; + color: var(--dim); + font-size: 12px; +} + +.stat-meta { + position: absolute; + right: var(--gutter); + bottom: 15px; + left: var(--gutter); + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + color: var(--dim); + font-size: 12px; +} + +.stat-meta span + span { + padding-left: 12px; + border-left: 1px solid var(--border); +} + +.stat-meta strong { + display: block; + margin-bottom: 1px; + color: var(--text); + font-size: 14px; + font-weight: 500; +} + +.stat-panel .section-title { + max-width: none; +} + +.stat-panel .stat-value, +.stat-panel .stat-caption { + padding-left: 2px; +} + +.stat-panel .stat-caption { + margin-bottom: 0; +} + +.stat-panel .account-health-detail { + display: none; +} + +/* ---------- Hero (solid: border + panel bg + radius, no border-top:0) ---------- */ + +.settings-hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--gap); + margin-bottom: var(--gap); + padding: var(--gutter); + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius-panel); +} + +.settings-hero .eyebrow { + margin-bottom: 4px; +} + +#settings-empty { + padding: 0; +} + +/* ---------- Empty states: single component, flat, no nested card ---------- */ + +.empty-state, +.viewer-empty-state { + padding: var(--gutter); + border: 0; + border-radius: 0; + background: transparent; + color: var(--dim); +} + +.empty-state .eyebrow { + margin-bottom: 4px; +} + +.empty-state h2, +.viewer-empty-state h2, +#settings-empty h2 { + font-size: 17px; + font-weight: 600; + line-height: 1.35; + color: var(--text); +} + +.empty-state p, +.viewer-empty-state p { + margin: 6px 0 0; + color: var(--dim); +} + +.empty-state .empty-actions, +.viewer-empty-state .empty-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.viewer-empty-state { + align-self: center; + margin-top: var(--gap); + text-align: center; +} + +.viewer-empty-state .empty-actions { + justify-content: center; +} + +/* ---------- Status helpers (JS sets classes, never inline style.color) ---------- */ + +.is-ok { + color: var(--ok); +} + +.is-warn { + color: var(--warn); +} + +.is-error { + color: var(--danger); +} + +.is-dim { + color: var(--dim); +} + +.account-list-auth-status.is-ok { + color: var(--ok); +} + +.account-list-auth-status.is-error { + color: var(--danger); +} + +/* ---------- Buttons (8px control radius everywhere, no pills) ---------- */ + +.button, +.nav-link.button, +a.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 38px; + padding: 8px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: #1b1b1c; + color: var(--text); + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +.button:hover { + background: #232324; + border-color: var(--border-strong); + color: var(--text); + text-decoration: none; +} + +.button:active { + background: #2a2a2b; + border-color: var(--border-strong); +} + +.button.primary { + background: var(--accent); + border-color: var(--accent); + color: var(--accent-ink); +} + +.button.primary:hover { + background: #ffffff; + border-color: #ffffff; + color: #080808; +} + +.button.primary:active { + background: #d8d8d8; + border-color: #d8d8d8; +} + +.button.danger { + background: var(--danger-bg); + border-color: var(--danger-border); + color: var(--danger); +} + +.button.danger:hover { + background: var(--danger-bg-hover); + border-color: var(--danger); + color: var(--danger); +} + +.button.button-small { + min-height: 30px; + padding: 5px 10px; + font-size: 12px; +} + +.button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.button-block { + width: 100%; +} + +/* ---------- Forms: visible labels above fields ---------- */ + .inline-form { display: flex; flex-wrap: wrap; gap: 8px; } +.inline-form input { + width: 168px; +} + .stack-form { display: grid; - gap: 8px; - margin-top: 12px; + gap: 12px; + margin-top: 0; } +.field { + display: grid; + gap: 6px; +} + +.field-label { + font-size: 13px; + font-weight: 600; + color: var(--text); +} + +.field-hint, +.form-hint { + font-size: 12px; + color: var(--muted); + line-height: 1.5; +} + +.form-hint { + margin: 0; +} + +input, +select { + width: 100%; + min-width: 0; + min-height: 38px; + padding: 9px 12px; + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--inset); + color: var(--text); + font-size: 14px; + outline: 0; +} + +input::placeholder { + color: var(--muted); +} + +input:hover, +select:hover { + border-color: var(--border-strong); +} + +input:focus, +select:focus { + border-color: var(--border-strong); + outline: 2px solid var(--link); + outline-offset: 1px; +} + +label.toggle-row, +.toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--gap); + font-size: 14px; +} + +/* Inner group boxes: border + radius with transparent bg (no third bg layer) */ +.add-account-form, +.import-account-row, +.qr-wrap, +.account-list-item, +.checkbox-row, +.settings-account-button { + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: transparent; +} + +.add-account-form { + display: grid; + gap: 12px; + padding: 14px; +} + +.import-account-row { + display: grid; + gap: 8px; + padding: 14px; + cursor: pointer; +} + +.import-account-row input { + cursor: pointer; +} + +.file-example { + font-family: var(--font-mono); + font-size: 12px; + color: var(--dim); + background: var(--inset); + border-radius: var(--radius-control); + padding: 8px 10px; + overflow-x: auto; + white-space: pre; +} + +.qr-wrap { + margin-top: 0; + padding: 12px; +} + +.qr-wrap img { + display: block; + width: 100%; + max-width: 240px; + aspect-ratio: 1; + margin: 0 auto 10px; + background: #ffffff; + border-radius: var(--radius-control); +} + +.auth-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +/* ---------- Settings page structure ---------- */ + .settings-layout { display: grid; grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr); - gap: 18px; + gap: var(--gap); align-items: start; } @@ -315,13 +902,34 @@ a.content-tab { .settings-page-shell .content { max-width: 1280px; + margin-inline: auto; } -.settings-hero { +/* ---------- Confirm dialog (styled replacement for window.confirm) ---------- */ + +.confirm-dialog { + width: min(440px, calc(100vw - 32px)); + padding: 0; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius-panel); + color: var(--text); +} + +.confirm-dialog::backdrop { + background: #000000; +} + +.confirm-dialog-body { + display: grid; + gap: 12px; + padding: var(--gutter); +} + +.confirm-dialog-actions { display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; + justify-content: flex-end; + gap: 8px; } .settings-account-list { @@ -338,17 +946,20 @@ a.content-tab { gap: 8px; align-items: center; text-align: left; - border: 1px solid var(--line); - border-radius: 16px; - background: #101010; - color: var(--text-color); padding: 12px 14px; + color: var(--text); cursor: pointer; + font-size: 14px; +} + +.settings-account-button:hover { + border-color: var(--border-strong); + background: #1c1c1d; } .settings-account-button.active { - border-color: var(--accent-line); - background: var(--accent-soft); + border-color: var(--border-strong); + background: #202020; } .settings-account-name { @@ -360,7 +971,7 @@ a.content-tab { .settings-account-status { color: var(--warn); - font-size: 0.78rem; + font-size: 12px; } .settings-account-status.ok { @@ -370,78 +981,117 @@ a.content-tab { .settings-meta-list { display: grid; gap: 8px; - margin-top: 16px; color: var(--dim); + font-size: 13px; } -input { - min-width: 160px; - padding: 11px 14px; - outline: 0; - border-radius: 16px; +/* ---------- Account list / tabs helpers ---------- */ + +.accounts-list { + display: grid; + gap: 10px; + margin-bottom: 14px; } -input:focus { - border-color: var(--accent); - box-shadow: none; -} - -.toggle-row { +.account-list-item { display: flex; align-items: center; justify-content: space-between; - gap: 16px; + gap: 12px; + padding: 12px 14px; } +.account-list-info { + display: grid; + gap: 2px; + min-width: 0; +} + +.account-list-label { + font-weight: 700; + font-size: 14px; +} + +.account-list-id { + font-size: 12px; + color: var(--muted); +} + +.account-list-auth-status { + font-size: 12px; + color: var(--muted); +} + +.account-list-actions { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +#active-account-name { + word-break: break-all; +} + +#active-account-status { + margin-top: 6px; +} + +/* ---------- Toggle (geometry 42x22 preserved) ---------- */ + .switch { position: relative; display: inline-flex; - width: 48px; - min-width: 48px; - height: 26px; + width: 42px; + min-width: 42px; + height: 22px; } .switch input { position: absolute; inset: 0; - opacity: 0; min-width: 0; + width: 100%; margin: 0; + opacity: 0; cursor: pointer; } .switch-slider { position: absolute; inset: 0; - border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.06); - border-radius: 999px; + border: 1px solid var(--border); + border-radius: var(--radius-badge); + background: var(--inset); } .switch-slider::before { content: ''; position: absolute; - top: 4px; - left: 4px; - width: 16px; - height: 16px; - background: var(--dim); + top: 3px; + left: 3px; + width: 14px; + height: 14px; border-radius: 50%; - transition: - transform 0.16s ease, - background-color 0.16s ease; + background: var(--muted); } .switch input:checked + .switch-slider { - border-color: var(--accent); - background: var(--accent-soft); + border-color: var(--border-strong); + background: var(--accent); } .switch input:checked + .switch-slider::before { - transform: translateX(22px); - background: var(--accent); + transform: translateX(20px); + background: var(--accent-ink); } +.switch input:focus-visible + .switch-slider { + outline: 2px solid var(--link); + outline-offset: 2px; +} + +/* ---------- Tables / jobs / logs ---------- */ + .table-wrap { overflow-x: auto; } @@ -453,14 +1103,28 @@ table { th, td { - padding: 12px 10px; + height: 38px; + padding: 9px 10px; text-align: left; - vertical-align: top; - border-bottom: 1px solid var(--line); + vertical-align: middle; + border-bottom: 1px solid var(--border); +} + +th { + color: var(--muted); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +td { + color: var(--dim); + font-size: 12px; } tbody tr:hover { - background: #0d0d0d; + background: #1c1c1d; } .action-row { @@ -469,30 +1133,56 @@ tbody tr:hover { gap: 8px; } +.action-row .button, +td .button, +.job-card .button { + min-height: 30px; + padding: 5px 10px; + font-size: 12px; +} + +.jobs-list { + display: grid; + gap: 10px; +} + +.job-card { + padding: 14px; +} + .job-status { color: var(--dim); - font-size: 0.82rem; + font-size: 11px; text-transform: uppercase; } +.job-logs, +.log-viewer { + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--inset); + font-family: var(--font-mono); + font-size: 12px; +} + .job-logs { max-height: 220px; margin: 10px 0 0; overflow: auto; white-space: pre-wrap; color: var(--dim); + padding: 10px 12px; } .continuous-meta { display: grid; gap: 4px; - margin: 16px 0; color: var(--dim); - font-size: 0.88rem; + font-size: 13px; } .continuous-meta span { - color: var(--text-color); + color: var(--text); } .checkbox-grid { @@ -508,12 +1198,11 @@ tbody tr:hover { gap: 10px; min-height: 38px; padding: 10px 12px; - border: 1px solid var(--line); - border-radius: 16px; - background: #101010; + font-size: 13px; } .checkbox-row input { + width: auto; min-width: auto; } @@ -525,9 +1214,6 @@ tbody tr:hover { display: grid; max-height: 420px; overflow: auto; - border: 1px solid var(--line); - background: rgba(0, 0, 0, 0.2); - border-radius: 16px; } .log-line { @@ -535,12 +1221,12 @@ tbody tr:hover { grid-template-columns: 86px 112px 74px minmax(0, 1fr); gap: 10px; padding: 5px 9px; - border-bottom: 1px solid #151515; - font-size: 0.88rem; + border-bottom: 1px solid var(--border); + font-size: 12px; } .log-line:hover { - background: rgba(53, 95, 199, 0.08); + background: #1c1c1d; } .log-time, @@ -574,47 +1260,149 @@ tbody tr:hover { color: var(--danger); } +/* ---------- Dialog / toast ---------- */ + .settings-dialog { width: min(720px, calc(100vw - 32px)); - color: var(--text-color); padding: 0; + color: var(--text); } .settings-dialog::backdrop { - background: rgba(0, 0, 0, 0.82); + background: #000000; } .dialog-shell { - padding: 24px; + padding: 20px; } .settings-section { margin-top: 18px; padding-top: 16px; - border-top: 1px solid var(--line); + border-top: 1px solid var(--border); } -.qr-wrap { - margin-top: 14px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 16px; +.toast-root { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 1000; + display: grid; + gap: 8px; + width: min(360px, calc(100vw - 36px)); } -.qr-wrap img { - display: block; - width: 100%; - max-width: 240px; - aspect-ratio: 1; - margin: 0 auto 10px; - background: white; +.toast { + padding: 12px 14px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-panel); + background: var(--panel); + color: var(--text); + font-size: 13px; +} + +.toast-success { + border-color: var(--border-strong); +} + +.toast-info { + border-color: var(--border-strong); + color: var(--info); +} + +.toast-warn { + border-color: var(--border-strong); + color: var(--warn); +} + +.toast-error { + border-color: var(--danger-border); + color: #ffd4d4; +} + +.toast-hide { + opacity: 0; +} + +/* ---------- API docs ---------- */ + +.api-docs { + display: grid; + gap: var(--gap); + padding: var(--gutter); +} + +.api-path { + margin: 0 0 12px; + color: var(--text); + font-size: 17px; + font-weight: 600; + overflow-wrap: anywhere; +} + +.api-methods { + display: grid; + gap: 12px; +} + +.api-method { + border-top: 1px solid var(--border); + padding-top: 12px; +} + +.api-method:first-child { + border-top: 0; + padding-top: 0; +} + +.api-method-head { + display: flex; + align-items: center; + gap: 10px; +} + +.api-verb { + min-width: 58px; + padding: 2px 6px; + border: 1px solid var(--border); + border-radius: var(--radius-control); + color: var(--ok); + font-size: 12px; + text-align: center; +} + +.api-summary { + font-weight: 700; + font-size: 14px; +} + +.api-body { + max-height: none; +} + +.panel.api-section { + padding: 0; + overflow: hidden; +} + +.panel.api-section .api-path { + padding: 14px var(--gutter) 12px; + border-bottom: 1px solid var(--border); +} + +.panel.api-section .api-methods { + padding: var(--gutter); +} + +#api-docs { + padding: var(--gutter); } .hidden { display: none !important; } -/* ---------- Viewer layout: sidebar + chat ---------- */ +/* ---------- Viewer layout (scope-preserved, opaque hex only) ---------- */ .viewer-shell { display: grid; @@ -624,13 +1412,13 @@ tbody tr:hover { } .viewer-sidebar { - background: var(--panel-alt); - border-right: 1px solid var(--line); - padding: 18px 14px; display: flex; flex-direction: column; - gap: 14px; + gap: var(--gap); overflow-y: auto; + padding: var(--gutter); + background: var(--panel-alt); + border-right: 1px solid var(--border); } .sidebar-toggle { @@ -641,15 +1429,15 @@ tbody tr:hover { display: flex; align-items: flex-start; justify-content: space-between; - gap: 16px; - padding: 10px 6px 8px; + gap: var(--gap); + padding: 0 0 var(--gap); } .viewer-sidebar-head h1 { margin: 0; - color: var(--accent); - font-size: 1.55rem; - letter-spacing: -0.04em; + font-size: 20px; + font-weight: 700; + color: var(--text); } .viewer-channel-list { @@ -668,19 +1456,19 @@ tbody tr:hover { text-align: left; cursor: pointer; border: 1px solid transparent; + border-radius: var(--radius-control); background: transparent; - color: var(--text-color); - border-radius: 16px; -} - -.viewer-channel-item.active { - border-color: var(--accent-line); - background: var(--accent-soft); - box-shadow: none; + color: var(--text); } .viewer-channel-item:hover { - background: #0f0f0f; + background: #1c1c1d; + border-color: var(--border); +} + +.viewer-channel-item.active { + border-color: var(--border-strong); + background: #202020; } .viewer-channel-avatar { @@ -690,8 +1478,8 @@ tbody tr:hover { width: 48px; height: 48px; border-radius: 50%; - background: #274894; - color: white; + background: var(--border-strong); + color: var(--text); font-weight: 700; letter-spacing: -0.03em; } @@ -711,8 +1499,9 @@ tbody tr:hover { } .viewer-channel-name { - color: var(--text-color); + color: var(--text); font-weight: 700; + font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -721,7 +1510,7 @@ tbody tr:hover { .viewer-channel-time, .viewer-channel-preview { color: var(--dim); - font-size: 0.82rem; + font-size: 12px; } .viewer-channel-preview { @@ -733,39 +1522,39 @@ tbody tr:hover { .viewer-channel-count { min-width: 22px; padding: 2px 7px; - border-radius: 999px; - background: #111a32; - color: var(--accent-2); - font-size: 0.76rem; + border-radius: var(--radius-badge); + background: var(--inset); + color: var(--info); + font-size: 12px; text-align: center; } -/* ---------- Main panel ---------- */ - .viewer-main { display: flex; flex-direction: column; height: 100vh; overflow: hidden; - background: var(--bg-color); + background: var(--bg); } .viewer-header { flex-shrink: 0; - padding: 18px 26px; - border-bottom: 1px solid var(--line); - background: var(--panel-alt); display: flex; align-items: flex-start; justify-content: space-between; - gap: 16px; + gap: var(--gap); + padding: var(--gutter); + border-bottom: 1px solid var(--border); + background: var(--panel-alt); } .viewer-header h2 { - margin: 0; - color: var(--accent); - font-size: 1.55rem; - letter-spacing: -0.04em; + color: var(--text); +} + +.viewer-header .muted { + display: block; + margin: 4px 0 0; } .viewer-tools { @@ -778,27 +1567,34 @@ tbody tr:hover { .viewer-tools input[type='search'] { min-width: min(260px, 40vw); + min-height: 38px; } .viewer-auto-refresh { display: inline-flex; align-items: center; gap: 6px; - min-height: 34px; + min-height: 38px; + padding: 0 4px; color: var(--dim); - font-size: 0.82rem; + font-size: 13px; + font-weight: 600; } .viewer-auto-refresh input { + width: auto; min-width: auto; + min-height: auto; + accent-color: var(--text); } .messages-list { flex: 1; overflow-y: auto; - padding: 22px 38px 26px; + padding: var(--gutter); display: flex; flex-direction: column; + gap: 0; } .scroll-sentinel { @@ -807,16 +1603,18 @@ tbody tr:hover { width: 100%; } -/* ---------- Date separator ---------- */ - .chat-date-sep { display: flex; align-items: center; + align-self: center; justify-content: center; gap: 12px; margin: 12px 0; - font-size: 0.78rem; - color: #d6e6e8; + padding: 5px 12px; + border-radius: var(--radius-badge); + background: var(--inset); + font-size: 12px; + color: var(--dim); flex-shrink: 0; } @@ -825,22 +1623,13 @@ tbody tr:hover { content: ''; flex: 1; height: 1px; - background: #242424; -} - -.chat-date-sep { - align-self: center; - padding: 5px 12px; - border-radius: 999px; - background: #111111; + background: var(--border); } .chat-date-sep:empty { display: none; } -/* ---------- Bubble wrap ---------- */ - .chat-bubble-wrap { display: flex; justify-content: flex-start; @@ -851,19 +1640,16 @@ tbody tr:hover { justify-content: flex-end; } -/* ---------- Chat bubble ---------- */ - .chat-bubble { max-width: min(680px, 74%); padding: 9px 13px 8px; background: var(--bubble-other); - border: 1px solid var(--line); + border: 1px solid var(--border); border-radius: 18px 18px 18px 6px; margin-bottom: 7px; overflow-wrap: anywhere; word-break: break-word; position: relative; - box-shadow: none; } .chat-bubble::before { @@ -887,29 +1673,18 @@ tbody tr:hover { .chat-bubble-wrap--own .chat-bubble { background: var(--bubble-own); - border-color: var(--accent-line); + border-color: var(--border-strong); border-radius: 18px 18px 6px 18px; } .chat-bubble--highlight { - animation: chat-highlight 0.5s ease-in-out 3; + outline: 2px solid var(--link); + outline-offset: 2px; } -@keyframes chat-highlight { - 0%, - 100% { - box-shadow: 0 0 0 0 rgba(139, 180, 248, 0); - } - 50% { - box-shadow: 0 0 0 3px rgba(139, 180, 248, 0.4); - } -} - -/* ---------- Sender ---------- */ - .chat-sender { - font-size: 0.82rem; - color: var(--accent); + font-size: 12px; + color: var(--info); font-weight: 700; margin-bottom: 2px; } @@ -918,16 +1693,14 @@ tbody tr:hover { display: none; } -/* ---------- Reply preview ---------- */ - .chat-reply { display: flex; gap: 6px; margin-bottom: 4px; padding: 6px 8px; cursor: pointer; - border-radius: 10px; - background: rgba(0, 0, 0, 0.18); + border-radius: var(--radius-control); + background: var(--inset); } .chat-reply.hidden { @@ -937,9 +1710,8 @@ tbody tr:hover { .chat-reply-border { flex-shrink: 0; width: 2px; - background: var(--accent); - border-radius: 999px; - opacity: 0.5; + background: var(--link); + border-radius: var(--radius-badge); } .chat-reply-body { @@ -947,33 +1719,29 @@ tbody tr:hover { } .chat-reply-author { - font-size: 0.78rem; + font-size: 12px; color: var(--info); font-weight: 700; } .chat-reply-text { - font-size: 0.82rem; + font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -/* ---------- Message text ---------- */ - .chat-text { white-space: pre-wrap; line-height: 1.45; - font-size: 0.96rem; + font-size: 14px; } .chat-text:empty { display: none; } -/* ---------- Media ---------- */ - .chat-media { margin-top: 6px; } @@ -986,15 +1754,13 @@ tbody tr:hover { .chat-media video { display: block; max-width: 100%; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 14px; + border: 1px solid var(--border); + border-radius: var(--radius-control); } -/* ---------- Footer ---------- */ - .chat-footer { margin-top: 4px; - font-size: 0.72rem; + font-size: 12px; color: var(--dim); text-align: right; } @@ -1003,103 +1769,7 @@ tbody tr:hover { display: none; } -.empty-state, -.viewer-empty-state { - padding: 16px; - color: var(--dim); - border: 1px solid var(--line); - border-radius: 14px; - background: #0b0b0b; -} - -.viewer-empty-state { - align-self: center; - margin-top: 16px; -} - -.toast-root { - position: fixed; - right: 18px; - bottom: 18px; - z-index: 1000; - display: grid; - gap: 8px; - width: min(360px, calc(100vw - 36px)); -} - -.toast { - padding: 12px 14px; - border: 1px solid var(--line); - border-radius: 14px; - background: var(--panel-solid); - color: var(--text-color); - transition: - opacity 0.2s ease, - transform 0.2s ease; -} - -.toast-success { - border-color: var(--accent-line); -} - -.toast-error { - border-color: #7a2a2a; - color: #ffd4d4; -} - -.toast-hide { - opacity: 0; - transform: translateY(8px); -} - -.api-docs { - display: grid; - gap: 16px; -} - -.api-path { - margin: 0 0 12px; - color: var(--accent); - font-size: 1rem; - overflow-wrap: anywhere; -} - -.api-methods { - display: grid; - gap: 12px; -} - -.api-method { - border-top: 1px solid var(--line); - padding-top: 12px; -} - -.api-method:first-child { - border-top: 0; - padding-top: 0; -} - -.api-method-head { - display: flex; - align-items: center; - gap: 10px; -} - -.api-verb { - min-width: 58px; - padding: 2px 6px; - border: 1px solid var(--line); - color: var(--ok); - text-align: center; -} - -.api-summary { - font-weight: 700; -} - -.api-body { - max-height: none; -} +/* ---------- Responsive ---------- */ @media (max-width: 1100px) { .app-shell, @@ -1114,150 +1784,76 @@ tbody tr:hover { .sidebar, .viewer-sidebar { border-right: 0; - border-bottom: 1px solid var(--line); + border-bottom: 1px solid var(--border); } .panel-grid { - grid-template-columns: 1fr; + grid-template-columns: repeat(2, minmax(0, 1fr)); } } -/* ── Account Tabs ─────────────────────────────────────── */ +@media (max-width: 900px) { + .settings-layout { + grid-template-columns: 1fr; + } -.account-tabs { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-bottom: 18px; - padding-bottom: 14px; - border-bottom: 1px solid var(--line); + .settings-layout .panel:last-child { + grid-column: auto; + } } -.account-tab { - display: inline-flex; - align-items: center; - gap: 8px; - min-height: 40px; - padding: 8px 15px; - border: 1px solid var(--line); - border-radius: 999px; - background: #101010; - color: var(--text-color); - cursor: pointer; - font: inherit; - font-size: 0.88rem; -} - -.account-tab.active { - border-color: var(--accent-line); - color: #ffffff; - background: var(--accent); - box-shadow: none; -} - -.account-tab .account-tab-status { - font-size: 0.72rem; - opacity: 0.7; -} - -.account-tab .account-tab-status.ok { - color: var(--ok); -} - -.account-tab .account-tab-status.errored { - color: var(--danger); -} - -/* ── Account Panels ──────────────────────────────────── */ - -.account-panel { - display: block; -} - -.account-panel.hidden { - display: none; -} - -/* ── Settings: Account List ──────────────────────────── */ - -.accounts-list { - display: grid; - gap: 10px; - margin-bottom: 14px; -} - -.account-list-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 12px 14px; - border: 1px solid var(--line); - border-radius: 16px; - background: #101010; -} - -.account-list-info { - display: grid; - gap: 2px; - min-width: 0; -} - -.account-list-label { - font-weight: 700; -} - -.account-list-id { - font-size: 0.82rem; -} - -.account-list-auth-status { - font-size: 0.78rem; -} - -.account-list-actions { - display: flex; - gap: 6px; - flex-shrink: 0; -} - -.add-account-form { - display: grid; - gap: 10px; - margin-top: 10px; - padding: 14px; - border: 1px solid var(--line); - border-radius: 18px; - background: #101010; -} - -.import-account-row { - display: grid; - gap: 8px; - margin-top: 12px; - padding: 14px; - border: 1px solid var(--line); - border-radius: 18px; - background: #101010; -} - -/* ── Active Account Card ─────────────────────────────── */ - -#active-account-name { - word-break: break-all; -} - -#active-account-status { - margin-top: 6px; -} - -/* ---------- Mobile: tablet & phone ---------- */ - @media (max-width: 768px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + width: auto; + height: auto; + padding: 12px; + border-right: 0; + border-bottom: 1px solid var(--border); + } + .sidebar-toggle { display: inline-flex; } + .brand-block { + min-height: 46px; + padding-bottom: 5px; + } + + .nav-links { + display: flex; + overflow-x: auto; + margin: 8px 0 0; + } + + .nav-link { + min-height: 34px; + white-space: nowrap; + } + + .status-card { + display: none; + } + + .content { + padding: 16px 16px 20px; + } + + .dashboard-topbar { + margin: -16px -16px 16px; + padding: 0 16px; + } + + .settings-hero { + flex-direction: column; + padding: 16px; + } + .viewer-sidebar { position: fixed; left: -360px; @@ -1265,8 +1861,7 @@ tbody tr:hover { bottom: 0; width: 340px; z-index: 100; - transition: left 0.2s ease; - border-right: 1px solid var(--line); + border-right: 1px solid var(--border); border-bottom: 0; } @@ -1274,14 +1869,6 @@ tbody tr:hover { left: 0; } - .viewer-sidebar.open::before { - content: ''; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: -1; - } - .viewer-header { padding: 12px 14px; } @@ -1298,12 +1885,26 @@ tbody tr:hover { display: none; } - .content { - padding: 18px; + .viewer-sidebar-head h1 { + font-size: 20px; + } +} + +@media (max-width: 560px) { + .panel-grid, + .settings-health-grid { + grid-template-columns: 1fr; } - .viewer-sidebar-head h1 { - font-size: 1.2rem; + .content-tabs, + .account-tabs { + overflow-x: auto; + flex-wrap: nowrap; + } + + .content-tab, + .account-tab { + white-space: nowrap; } } @@ -1317,11 +1918,11 @@ tbody tr:hover { } .viewer-header { - padding: 10px 10px; + padding: 10px; } .viewer-header h2 { - font-size: 1.1rem; + font-size: 17px; } .messages-list { @@ -1331,23 +1932,11 @@ tbody tr:hover { .chat-bubble { max-width: 96%; padding: 6px 9px; - font-size: 0.92rem; + font-size: 14px; } .chat-footer { - font-size: 0.65rem; - } - - .chat-sender { - font-size: 0.75rem; - } - - .chat-reply-author { - font-size: 0.72rem; - } - - .chat-reply-text { - font-size: 0.75rem; + font-size: 12px; } .sidebar { @@ -1362,7 +1951,7 @@ tbody tr:hover { .panel-header, .viewer-header, .dialog-header { - display: grid; + flex-wrap: wrap; } .inline-form, @@ -1380,863 +1969,3 @@ tbody tr:hover { display: none; } } - -/* ── GPU Radar visual system ──────────────────────────── - A restrained, data-first monochrome treatment inspired by gpu.chernuha.space. */ -:root { - --bg-color: #080808; - --panel: #101010; - --panel-solid: #151515; - --panel-alt: #0b0b0b; - --text-color: #f5f5f5; - --dim: #9a9a9a; - --line: #2b2b2b; - --accent: #f1f1f1; - --accent-hover: #ffffff; - --accent-soft: rgba(255, 255, 255, 0.08); - --accent-line: #3b3b3b; - --accent-2: #d8d8d8; - --bubble-own: #181818; - --bubble-other: #101010; - --danger: #d0d0d0; - --ok: #e0e0e0; - --warn: #b8b8b8; - --info: #d7d7d7; - --radius: 8px; - --shadow: none; - --font-ui: Inter, 'SF Pro Display', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; -} - -body { - font-size: 14px; - -webkit-font-smoothing: antialiased; -} -a { - border-bottom: 0; -} -a:hover, -button:hover { - border-color: var(--accent-line); - background: var(--accent-soft); -} - -.app-shell { - grid-template-columns: 236px minmax(0, 1fr); - min-height: 100vh; -} -.sidebar { - position: sticky; - top: 0; - height: 100vh; - padding: 18px 12px; - background: var(--panel-alt); - border-right-color: var(--line); -} -.brand-block { - position: relative; - display: block; - min-height: 64px; - padding: 5px 9px 16px; -} -.brand-block::before { - content: '◈'; - position: absolute; - top: 8px; - left: 9px; - color: var(--text-color); - font-size: 25px; - line-height: 1; -} -.brand-block h1, -.dialog-header h2 { - color: var(--text-color); - font-size: 15px; - line-height: 1.12; - letter-spacing: -0.01em; -} -.brand-block h1 { - margin-left: 36px; -} -.brand-block .eyebrow { - margin: 0 0 3px 36px; - color: var(--muted, #747474); - font-size: 12px; - letter-spacing: 0; - text-transform: none; -} -.brand-block .muted { - display: none; -} -.eyebrow, -.section-title { - color: #747474; - font-size: 10px; - font-weight: 650; - letter-spacing: 0.08em; -} -.muted { - color: #9a9a9a; -} - -.nav-links { - gap: 5px; - margin: 10px 0 24px; -} -.nav-link, -.button, -input, -.content-tab, -.viewer-channel-item, -.account-tab { - border-color: transparent; - border-radius: 6px; - background: transparent; -} -.nav-link { - justify-content: flex-start; - min-height: 46px; - padding: 0 13px; - color: #979797; - font-size: 13px; - font-weight: 650; -} -.nav-link.active { - border-color: #3b3b3b; - color: var(--text-color); - background: var(--accent-soft); -} -.nav-link::before { - display: none; -} -.nav-icon { - display: inline-grid; - width: 18px; - place-items: center; - color: #c6c6c6; - font-size: 17px; - font-weight: 400; - line-height: 1; -} - -.status-card, -.panel, -.stat-panel, -.job-card, -.message-card, -.settings-dialog { - border-color: var(--line); - border-radius: var(--radius); - background: var(--panel); - box-shadow: none; -} -.status-card { - padding: 14px 10px 4px; - background: transparent; - border: 0; - border-top: 1px solid rgba(255, 255, 255, 0.09); -} -.status-card + .status-card { - margin-top: 12px; -} -.status-card .button { - width: auto; - justify-content: flex-start; - padding: 0 13px; -} -.status-badge { - padding: 0; - border: 0; - border-radius: 0; - color: var(--text-color); - background: transparent; - font-size: 12px; - font-weight: 700; -} - -.content { - max-width: none; - padding: 0 28px 40px; -} -.content::before { - display: none; -} -.dashboard-topbar { - display: flex; - min-height: 58px; - align-items: center; - justify-content: space-between; - gap: 20px; - margin: 0 -28px 20px; - padding: 0 28px; - border-bottom: 1px solid var(--line); -} -.dashboard-title, -.runtime-status { - display: inline-flex; - align-items: center; - gap: 12px; - color: #efefef; - font-size: 13px; - font-weight: 650; -} -.menu-icon { - color: #dedede; - font-size: 20px; - font-weight: 400; -} -.runtime-status { - color: #b8b8b8; - font-size: 12px; - font-weight: 500; -} -.runtime-status i { - width: 1px; - height: 17px; - background: #363636; -} -.runtime-dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: #e2e2e2; -} -.runtime-clock::before { - content: '◷'; - color: #e1e1e1; - font-size: 17px; -} -.content-tabs, -.account-tabs { - gap: 0; - margin-bottom: 18px; - padding-bottom: 0; - border-bottom: 0; -} -.content-tab, -.account-tab { - min-height: 38px; - padding: 8px 14px; - border: 1px solid var(--line); - border-radius: 0; - color: #aaa; - font-size: 13px; -} -.content-tab:first-child, -.account-tab:first-child { - border-radius: 6px 0 0 6px; -} -.content-tab + .content-tab, -.account-tab + .account-tab { - margin-left: -1px; -} -.content-tab:last-child, -.account-tab:last-child { - border-radius: 0 6px 6px 0; -} -.content-tab.active, -.account-tab.active { - position: relative; - z-index: 1; - border-color: #535353; - color: var(--text-color); - background: var(--accent-soft); -} - -.panel-grid { - gap: 14px; - margin-bottom: 14px; - grid-template-columns: repeat(4, minmax(0, 1fr)); -} -.settings-health-grid { - grid-template-columns: repeat(4, minmax(0, 1fr)); -} -.panel, -.status-card, -.job-card, -.message-card { - padding: 18px; -} -.panel { - margin-bottom: 14px; -} -.stat-panel { - position: relative; - min-height: 187px; - padding: 18px; -} -.stat-panel::after { - content: ''; - position: absolute; - right: 18px; - bottom: 65px; - left: 18px; - height: 1px; - background: #343434; -} -.stat-panel::before { - position: absolute; - top: 17px; - right: 18px; - display: grid; - width: 24px; - height: 24px; - place-items: center; - border: 1px solid #9c9c9c; - border-radius: 50%; - color: #e4e4e4; - font-size: 15px; - font-weight: 400; -} -.stat-panel:nth-child(1)::before { - content: '⌘'; -} -.stat-panel:nth-child(2)::before { - content: '⇩'; -} -.stat-panel:nth-child(3)::before { - content: '⇄'; -} -.stat-panel:nth-child(4)::before { - content: '◇'; -} -.stat-value { - margin-top: 24px; - font-size: 28px; - font-weight: 650; - letter-spacing: -0.04em; -} -.stat-compact { - font-size: 19px; -} -.stat-panel .account-health-detail { - position: absolute; - right: 18px; - bottom: 17px; - left: 18px; -} -.stat-panel .section-title { - max-width: calc(100% - 34px); -} -.panel-header, -.dialog-header, -.job-head { - align-items: center; - gap: 16px; -} -.panel-header h2, -.settings-hero h2 { - font-size: 16px; - font-weight: 650; - letter-spacing: -0.02em; -} -.panel-header .muted { - margin-top: 4px; - font-size: 12px; -} - -.button { - min-height: 36px; - padding: 7px 12px; - border: 1px solid var(--line); - color: #d4d4d4; - font-size: 12px; - font-weight: 650; -} -.button.primary { - border-color: #d9d9d9; - color: #111; - background: #f1f1f1; -} -.button.primary:hover { - border-color: #fff; - background: #fff; - color: #080808; -} -.button.danger { - color: #c5c5c5; -} -.button.button-small { - min-height: 32px; - padding: 5px 9px; -} -input { - min-height: 36px; - padding: 8px 11px; - border: 1px solid var(--line); - color: var(--text-color); - background: #0d0d0d; - font-size: 12px; -} -input:focus { - border-color: #777; -} - -th, -td { - padding: 12px 10px; - border-bottom-color: var(--line); - font-size: 12px; -} -th { - color: #888; - font-size: 10px; - font-weight: 650; - letter-spacing: 0.06em; - text-transform: uppercase; -} -tbody tr:hover { - background: rgba(255, 255, 255, 0.035); -} -.job-card { - border-radius: 6px; -} -.job-status { - color: #aaa; - font-size: 10px; -} -.job-logs, -.log-viewer { - border-color: var(--line); - border-radius: 6px; - background: #0b0b0b; - font-family: var(--font-mono); - font-size: 12px; -} -.log-line { - border-bottom-color: #202020; -} -.log-line:hover { - background: rgba(255, 255, 255, 0.04); -} - -.switch { - width: 42px; - min-width: 42px; - height: 22px; -} -.switch-slider { - border-color: #3a3a3a; - background: #181818; -} -.switch-slider::before { - top: 3px; - left: 3px; - width: 14px; - height: 14px; - background: #8d8d8d; -} -.switch input:checked + .switch-slider { - border-color: #858585; - background: #f1f1f1; -} -.switch input:checked + .switch-slider::before { - transform: translateX(20px); - background: #111; -} -.checkbox-row, -.settings-account-button, -.account-list-item, -.add-account-form, -.import-account-row, -.qr-wrap { - border-color: var(--line); - border-radius: 6px; - background: #111; -} -.settings-account-button.active { - border-color: #535353; - background: var(--accent-soft); -} - -.settings-hero { - border-top: 0; -} -.settings-dialog { - border-color: #3b3b3b; -} -.dialog-shell { - padding: 20px; -} -.settings-section { - border-top-color: var(--line); -} -.toast { - border-color: #4a4a4a; - border-radius: 6px; - background: #151515; -} - -@media (max-width: 900px) { - .panel-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} -@media (max-width: 768px) { - .app-shell { - grid-template-columns: 1fr; - } - .sidebar { - position: static; - height: auto; - padding: 12px; - border-right: 0; - border-bottom: 1px solid var(--line); - } - .brand-block { - min-height: 46px; - padding-bottom: 5px; - } - .nav-links { - display: flex; - overflow-x: auto; - margin: 8px 0 0; - } - .nav-link { - min-height: 34px; - white-space: nowrap; - } - .status-card { - display: none; - } - .content { - padding: 16px; - } - .content::before { - height: 0; - margin: -16px -16px 16px; - } -} -@media (max-width: 560px) { - .panel-grid, - .settings-health-grid { - grid-template-columns: 1fr; - } - .content-tabs, - .account-tabs { - overflow-x: auto; - flex-wrap: nowrap; - } - .content-tab, - .account-tab { - white-space: nowrap; - } -} - -/* Dashboard reference fidelity pass */ -:root { - --sidebar-width: 244px; -} - -.app-shell { - grid-template-columns: var(--sidebar-width) minmax(0, 1fr); - width: 100vw; - max-width: 100vw; - min-width: 0; -} - -.sidebar { - width: var(--sidebar-width); - padding: 18px 12px; -} - -.brand-block { - min-height: 62px; - padding: 5px 9px 14px; -} - -.brand-block::before { - display: none; -} - -.brand-icon { - position: absolute; - top: 9px; - left: 9px; - width: 27px; - height: 27px; - fill: none; - stroke: #f1f1f1; - stroke-width: 1.65; - stroke-linecap: round; - stroke-linejoin: round; -} - -.brand-block h1 { - margin-left: 37px; - font-size: 15px; -} - -.brand-block .eyebrow { - margin-left: 37px; - color: #f5f5f5; - font-size: 15px; - font-weight: 650; -} - -.nav-links { - gap: 5px; - margin: 6px 0 0; -} - -.nav-link { - min-height: 45px; - gap: 12px; - padding: 0 13px; - color: #a1a1a1; - font-size: 13px; -} - -.nav-link.active { - border-color: transparent; - background: #202020; -} - -a.nav-link { - border-bottom-color: transparent; -} - -.nav-icon { - width: 20px; - height: 20px; - flex: 0 0 20px; - fill: none; - stroke: currentColor; - stroke-width: 1.7; - stroke-linecap: round; - stroke-linejoin: round; -} - -.status-card:first-of-type { - margin-top: auto; -} - -.status-card { - padding: 15px 10px 5px; -} - -.content { - min-width: 0; - padding: 0 20px 28px; -} - -.dashboard-topbar { - min-height: 58px; - margin: 0 -20px 20px; - padding: 0 28px; -} - -.dashboard-title { - gap: 22px; - font-size: 14px; -} - -.menu-icon { - font-size: 21px; -} - -.runtime-status { - gap: 13px; -} - -.account-tabs { - margin-bottom: 18px; -} - -.account-tab { - min-height: 36px; - padding: 7px 16px; -} - -.content-tabs { - margin-bottom: 16px; -} - -.content-tab { - min-height: 35px; - padding: 6px 13px; -} - -.panel-grid { - gap: 16px; - margin-bottom: 16px; -} - -.panel, -.stat-panel { - border-color: #303030; - border-radius: 7px; -} - -.stat-panel { - min-height: 208px; - padding: 18px 18px 16px; -} - -.stat-panel::after { - right: 16px; - bottom: 66px; - left: 16px; - background: #363636; -} - -.stat-panel::before { - top: 16px; - right: 16px; - width: 25px; - height: 25px; -} - -.section-title { - color: #ebebeb; - font-size: 14px; - font-weight: 600; - letter-spacing: -0.01em; - text-transform: none; -} - -.stat-value { - margin-top: 20px; - font-size: 27px; - font-weight: 500; -} - -.stat-caption { - margin-top: 3px; - color: #bdbdbd; - font-size: 12px; -} - -.stat-meta { - position: absolute; - right: 18px; - bottom: 15px; - left: 18px; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; - color: #bdbdbd; - font-size: 12px; -} - -.stat-meta span + span { - padding-left: 12px; - border-left: 1px solid #363636; -} - -.stat-meta strong { - display: block; - margin-bottom: 1px; - color: #ededed; - font-size: 14px; - font-weight: 500; -} - -.stat-panel .account-health-detail { - display: none; -} - -.panel { - padding: 0; - overflow: hidden; -} - -.panel-header { - min-height: 52px; - padding: 0 18px; - border-bottom: 1px solid #343434; -} - -.panel-header h2 { - font-size: 15px; - font-weight: 600; -} - -.panel-header .muted { - display: none; -} - -.inline-form { - gap: 8px; -} - -.inline-form input { - width: 168px; -} - -.table-wrap { - padding: 0 8px; -} - -th, -td { - height: 38px; - padding: 9px 10px; - border-bottom-color: #303030; -} - -th { - color: #e6e6e6; - font-size: 11px; - font-weight: 500; - letter-spacing: 0; - text-transform: none; -} - -td { - color: #d0d0d0; - font-size: 12px; -} - -.action-row .button, -td .button, -.job-card .button { - min-height: 29px; - padding: 4px 9px; - border-radius: 5px; - font-size: 11px; -} - -.jobs-list { - padding: 12px 16px 16px; -} - -.job-card { - padding: 14px; -} - -@media (max-width: 768px) { - .app-shell { - grid-template-columns: 1fr; - } - - .sidebar { - width: auto; - } - - .content { - padding: 0 16px 20px; - } - - .dashboard-topbar { - margin: 0 -16px 16px; - padding: 0 16px; - } -} - -/* Stat panel: restore left padding & drop card icons */ -.stat-panel { - padding: 18px 18px 16px; -} -.stat-panel .section-title { - max-width: none; -} -.stat-panel .stat-value, -.stat-panel .stat-caption { - padding-left: 2px; -} -.stat-panel .stat-caption { - margin-bottom: 0; -} -.stat-panel::before { - content: none !important; -} diff --git a/webui/swagger.html b/webui/swagger.html index a4dddf1..daae103 100644 --- a/webui/swagger.html +++ b/webui/swagger.html @@ -4,7 +4,7 @@ Telegram Scraper API - +
@@ -17,18 +17,44 @@ Dashboard
-

Endpoints

+
OpenAPI
+

API Docs

Local API surface exposed by the web UI server.

+
+
Service: Running
+ openapi.json +
@@ -53,6 +79,6 @@ - + diff --git a/webui/swagger.js b/webui/swagger.js index cc27af1..d220c06 100644 --- a/webui/swagger.js +++ b/webui/swagger.js @@ -19,7 +19,30 @@ function renderSpec(spec) { const methodTemplate = document.getElementById('api-method-template'); root.innerHTML = ''; - Object.entries(spec.paths || {}).forEach(([path, methods]) => { + const paths = Object.entries(spec.paths || {}); + if (!paths.length) { + const section = document.createElement('section'); + section.className = 'panel'; + const header = document.createElement('div'); + header.className = 'panel-header'; + const title = document.createElement('h2'); + title.textContent = 'No endpoints'; + header.appendChild(title); + const body = document.createElement('div'); + body.className = 'panel-body'; + const empty = document.createElement('div'); + empty.className = 'empty-state'; + const text = document.createElement('p'); + text.className = 'muted'; + text.textContent = 'The OpenAPI spec contains no paths.'; + empty.appendChild(text); + body.appendChild(empty); + section.append(header, body); + root.appendChild(section); + return; + } + + paths.forEach(([path, methods]) => { const section = sectionTemplate.content.firstElementChild.cloneNode(true); section.querySelector('.api-path').textContent = path; const methodsRoot = section.querySelector('.api-methods'); @@ -53,6 +76,28 @@ loadSpec() docsEl.textContent = ''; const section = document.createElement('section'); section.className = 'panel'; - section.textContent = error.message; + const header = document.createElement('div'); + header.className = 'panel-header'; + const title = document.createElement('h2'); + title.textContent = 'Failed to load API spec'; + header.appendChild(title); + const body = document.createElement('div'); + body.className = 'panel-body'; + const empty = document.createElement('div'); + empty.className = 'empty-state'; + const text = document.createElement('p'); + text.className = 'muted'; + text.textContent = error.message; + const actions = document.createElement('div'); + actions.className = 'empty-actions'; + const retry = document.createElement('button'); + retry.className = 'button'; + retry.type = 'button'; + retry.textContent = 'Retry'; + retry.addEventListener('click', () => window.location.reload()); + actions.appendChild(retry); + empty.append(text, actions); + body.appendChild(empty); + section.append(header, body); docsEl.appendChild(section); }); diff --git a/webui/viewer.html b/webui/viewer.html index f1b8985..caaea77 100644 --- a/webui/viewer.html +++ b/webui/viewer.html @@ -4,7 +4,7 @@ Telegram Scraper Viewer - +
@@ -15,7 +15,7 @@

Messages

Account: - +

@@ -31,7 +31,7 @@

Reading messages from the local SQLite database.

- +
- + diff --git a/webui/viewer.js b/webui/viewer.js index 903fade..5e69c1d 100644 --- a/webui/viewer.js +++ b/webui/viewer.js @@ -395,10 +395,15 @@ function renderMessages(payload, append = false) { viewerState.oldestMessageId = null; viewerState.newestMessageId = null; const empty = document.createElement('div'); - empty.className = 'viewer-empty-state'; - empty.textContent = viewerState.search + empty.className = 'empty-state viewer-empty-state'; + const title = document.createElement('h2'); + title.textContent = viewerState.search ? 'No messages found' : 'No saved messages'; + const text = document.createElement('p'); + text.className = 'muted'; + text.textContent = viewerState.search ? `No messages found for "${viewerState.search}".` : 'No saved messages in this channel yet.'; + empty.append(title, text); root.appendChild(empty); }