style(webui): unify UI across all pages to solid modern system

tokens instead of hardcoded, v9 everywhere, dialog guarded, confirm->dialog, empty unified, health out of nav, swagger icons/topbar, gutters, th uppercase, content 1280 centered.
This commit is contained in:
2026-09-08 12:30:50 +02:00
parent 2a75537fb9
commit ef62b8e94a
9 changed files with 1587 additions and 1668 deletions
+77 -41
View File
@@ -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 =
'<div class="confirm-dialog-body"><div class="eyebrow">Confirm</div>' +
'<p class="confirm-dialog-message muted"></p>' +
'<div class="confirm-dialog-actions">' +
'<button class="button" data-confirm-cancel type="button">Cancel</button>' +
'<button class="button primary" data-confirm-ok type="button">Confirm</button>' +
'</div></div>';
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 =
'<td colspan="5"><div class="empty-state">No tracked channels yet. Add an ID or @username to start scraping this account.</div></td>';
'<td colspan="5"><div class="empty-state"><p class="muted">No tracked channels yet. Add an ID or @username to start scraping this account.</p></div></td>';
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 = '<div class="empty-state">No jobs yet. Start a scrape or export to see progress here.</div>';
root.innerHTML = '<div class="empty-state"><p class="muted">No jobs yet. Start a scrape or export to see progress here.</p></div>';
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);