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:
+77
-41
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user