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:
+75
-39
@@ -68,7 +68,34 @@ function setBusy(button, busy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmAction(message) {
|
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') {
|
function showToast(message, type = 'info') {
|
||||||
@@ -202,7 +229,7 @@ function renderAccountPanel(accountId) {
|
|||||||
(item) => item.value,
|
(item) => item.value,
|
||||||
);
|
);
|
||||||
if (!runAllTracked && channels.length === 0 && enabled) {
|
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`, {
|
const resp = await api(`/api/accounts/${accountId}/continuous`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -289,15 +316,17 @@ function updateSidebarAccount() {
|
|||||||
const acc = state.accounts.find((a) => a.id === state.activeAccount);
|
const acc = state.accounts.find((a) => a.id === state.activeAccount);
|
||||||
const nameEl = document.getElementById('active-account-name');
|
const nameEl = document.getElementById('active-account-name');
|
||||||
const statusEl = document.getElementById('active-account-status');
|
const statusEl = document.getElementById('active-account-status');
|
||||||
|
if (!nameEl || !statusEl) return;
|
||||||
|
statusEl.classList.remove('is-ok', 'is-error', 'is-dim');
|
||||||
if (acc) {
|
if (acc) {
|
||||||
nameEl.textContent = acc.label || acc.id;
|
nameEl.textContent = acc.label || acc.id;
|
||||||
const authOk = isAccountAuthorized(acc.auth);
|
const authOk = isAccountAuthorized(acc.auth);
|
||||||
statusEl.textContent = authOk ? 'Authorized session' : 'Needs login';
|
statusEl.textContent = authOk ? 'Authorized session' : 'Needs login';
|
||||||
statusEl.style.color = authOk ? 'var(--ok)' : 'var(--danger)';
|
statusEl.classList.add(authOk ? 'is-ok' : 'is-error');
|
||||||
} else {
|
} else {
|
||||||
nameEl.textContent = 'None';
|
nameEl.textContent = 'None';
|
||||||
statusEl.textContent = 'Add an account in Settings';
|
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) {
|
if (!channels.length) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML =
|
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);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +383,7 @@ function renderChannels(accountId, channels) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.querySelector('.remove-btn').addEventListener('click', async () => {
|
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`, {
|
await api(`/api/accounts/${accountId}/channels/remove`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ channel_id: channel.channel_id }),
|
body: JSON.stringify({ channel_id: channel.channel_id }),
|
||||||
@@ -379,7 +408,7 @@ function renderJobs(accountId, jobs) {
|
|||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
|
|
||||||
if (!jobs.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,8 +657,10 @@ function renderSummary(accountId, data) {
|
|||||||
const toggle = panel.querySelector('.scrape-media-label');
|
const toggle = panel.querySelector('.scrape-media-label');
|
||||||
toggle.textContent = d.scrape_media ? 'ON' : 'OFF';
|
toggle.textContent = d.scrape_media ? 'ON' : 'OFF';
|
||||||
const healthOk = health.api_credentials && health.session_ready && health.data_dir_exists;
|
const healthOk = health.api_credentials && health.session_ready && health.data_dir_exists;
|
||||||
panel.querySelector('.account-health-label').textContent = healthOk ? 'Ready' : 'Check';
|
const healthLabel = panel.querySelector('.account-health-label');
|
||||||
panel.querySelector('.account-health-label').style.color = healthOk ? 'var(--ok)' : 'var(--warn)';
|
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 = [
|
panel.querySelector('.account-health-detail').textContent = [
|
||||||
`${health.message_count || 0} messages`,
|
`${health.message_count || 0} messages`,
|
||||||
`${health.media_count || 0} media`,
|
`${health.media_count || 0} media`,
|
||||||
@@ -721,7 +752,9 @@ async function loadAccounts() {
|
|||||||
|
|
||||||
function renderSettingsAccounts() {
|
function renderSettingsAccounts() {
|
||||||
const container = document.getElementById('accounts-list');
|
const container = document.getElementById('accounts-list');
|
||||||
|
if (!container) return;
|
||||||
const template = document.getElementById('account-list-item-template');
|
const template = document.getElementById('account-list-item-template');
|
||||||
|
if (!template) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
state.accounts.forEach((acc) => {
|
state.accounts.forEach((acc) => {
|
||||||
@@ -732,12 +765,12 @@ function renderSettingsAccounts() {
|
|||||||
const authStatus = node.querySelector('.account-list-auth-status');
|
const authStatus = node.querySelector('.account-list-auth-status');
|
||||||
const authOk = isAccountAuthorized(acc.auth);
|
const authOk = isAccountAuthorized(acc.auth);
|
||||||
authStatus.textContent = authOk ? 'Authorized' : 'Needs auth';
|
authStatus.textContent = authOk ? 'Authorized' : 'Needs auth';
|
||||||
authStatus.style.color = authOk ? 'var(--ok)' : 'var(--danger)';
|
authStatus.classList.remove('is-ok', 'is-error');
|
||||||
authStatus.style.fontSize = '0.78rem';
|
authStatus.classList.add(authOk ? 'is-ok' : 'is-error');
|
||||||
|
|
||||||
node.querySelector('.account-select-btn').addEventListener('click', () => {
|
node.querySelector('.account-select-btn').addEventListener('click', () => {
|
||||||
switchAccount(acc.id);
|
switchAccount(acc.id);
|
||||||
document.getElementById('settings-dialog').close();
|
document.getElementById('settings-dialog')?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
node.querySelector('.account-export-btn').addEventListener('click', async () => {
|
node.querySelector('.account-export-btn').addEventListener('click', async () => {
|
||||||
@@ -751,7 +784,7 @@ function renderSettingsAccounts() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.querySelector('.account-remove-btn').addEventListener('click', async () => {
|
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 {
|
try {
|
||||||
const removingActive = state.activeAccount === acc.id;
|
const removingActive = state.activeAccount === acc.id;
|
||||||
await api(`/api/accounts/${acc.id}`, { method: 'DELETE' });
|
await api(`/api/accounts/${acc.id}`, { method: 'DELETE' });
|
||||||
@@ -773,6 +806,7 @@ function renderSettingsAccounts() {
|
|||||||
|
|
||||||
function updateAuthSection(accountId) {
|
function updateAuthSection(accountId) {
|
||||||
const label = document.getElementById('auth-account-label');
|
const label = document.getElementById('auth-account-label');
|
||||||
|
if (!label) return;
|
||||||
const acc = state.accounts.find((a) => a.id === accountId);
|
const acc = state.accounts.find((a) => a.id === accountId);
|
||||||
label.textContent = acc ? acc.label || acc.id : '-';
|
label.textContent = acc ? acc.label || acc.id : '-';
|
||||||
}
|
}
|
||||||
@@ -836,7 +870,7 @@ async function submitPassword(accountId) {
|
|||||||
|
|
||||||
async function scrapeAll() {
|
async function scrapeAll() {
|
||||||
if (!state.activeAccount) return;
|
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`, {
|
await api(`/api/accounts/${state.activeAccount}/jobs/scrape`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
@@ -846,7 +880,7 @@ async function scrapeAll() {
|
|||||||
|
|
||||||
async function exportAll() {
|
async function exportAll() {
|
||||||
if (!state.activeAccount) return;
|
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`, {
|
await api(`/api/accounts/${state.activeAccount}/jobs/export`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({}),
|
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 settingsDialog = document.getElementById('settings-dialog');
|
||||||
const openSettingsBtn = document.getElementById('open-settings-btn');
|
const closeSettingsBtn = document.getElementById('close-settings-btn');
|
||||||
if (!openSettingsBtn.matches('a[href]')) {
|
if (settingsDialog && closeSettingsBtn) {
|
||||||
openSettingsBtn.addEventListener('click', () => {
|
closeSettingsBtn.addEventListener('click', () => settingsDialog.close());
|
||||||
// Update auth section for active account
|
|
||||||
if (state.activeAccount) {
|
|
||||||
updateAuthSection(state.activeAccount);
|
|
||||||
}
|
}
|
||||||
renderSettingsAccounts();
|
|
||||||
settingsDialog.showModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.getElementById('close-settings-btn').addEventListener('click', () => {
|
|
||||||
settingsDialog.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Add account form ──
|
// ── Add account form (only when legacy dialog markup exists) ──
|
||||||
document.getElementById('add-account-form').addEventListener('submit', async (event) => {
|
const addAccountForm = document.getElementById('add-account-form');
|
||||||
|
if (addAccountForm) {
|
||||||
|
addAccountForm.addEventListener('submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const accountId = document.getElementById('add-account-id').value.trim();
|
const accountId = document.getElementById('add-account-id').value.trim();
|
||||||
const label = document.getElementById('add-account-label').value.trim();
|
const label = document.getElementById('add-account-label').value.trim();
|
||||||
@@ -946,8 +973,11 @@ async function main() {
|
|||||||
showToast('Failed to add account: ' + err.message, 'error');
|
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];
|
const file = event.currentTarget.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
try {
|
try {
|
||||||
@@ -967,9 +997,15 @@ async function main() {
|
|||||||
showToast(`Failed to import account: ${err.message}`, 'error');
|
showToast(`Failed to import account: ${err.message}`, 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Credentials form ──
|
// ── Credentials / auth forms (only when legacy dialog markup exists) ──
|
||||||
document.getElementById('credentials-form').addEventListener('submit', async (event) => {
|
const bindOptional = (id, evt, handler) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener(evt, handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
bindOptional('credentials-form', 'submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.activeAccount) return;
|
if (!state.activeAccount) return;
|
||||||
try {
|
try {
|
||||||
@@ -980,7 +1016,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── QR login ──
|
// ── QR login ──
|
||||||
document.getElementById('start-qr-btn').addEventListener('click', async (event) => {
|
bindOptional('start-qr-btn', 'click', async (event) => {
|
||||||
setBusy(event.currentTarget, true);
|
setBusy(event.currentTarget, true);
|
||||||
try {
|
try {
|
||||||
if (!state.activeAccount) return;
|
if (!state.activeAccount) return;
|
||||||
@@ -993,7 +1029,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Phone ──
|
// ── Phone ──
|
||||||
document.getElementById('phone-form').addEventListener('submit', async (event) => {
|
bindOptional('phone-form', 'submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.activeAccount) return;
|
if (!state.activeAccount) return;
|
||||||
try {
|
try {
|
||||||
@@ -1004,7 +1040,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Code ──
|
// ── Code ──
|
||||||
document.getElementById('code-form').addEventListener('submit', async (event) => {
|
bindOptional('code-form', 'submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.activeAccount) return;
|
if (!state.activeAccount) return;
|
||||||
try {
|
try {
|
||||||
@@ -1015,7 +1051,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Password ──
|
// ── Password ──
|
||||||
document.getElementById('password-form').addEventListener('submit', async (event) => {
|
bindOptional('password-form', 'submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.activeAccount) return;
|
if (!state.activeAccount) return;
|
||||||
try {
|
try {
|
||||||
@@ -1026,7 +1062,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Media toggle ──
|
// ── Media toggle ──
|
||||||
document.getElementById('scrape-media-toggle').addEventListener('change', async (event) => {
|
bindOptional('scrape-media-toggle', 'change', async (event) => {
|
||||||
const checked = event.currentTarget.checked;
|
const checked = event.currentTarget.checked;
|
||||||
try {
|
try {
|
||||||
await toggleMedia(checked);
|
await toggleMedia(checked);
|
||||||
|
|||||||
+31
-88
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Telegram Scraper Control Panel</title>
|
<title>Telegram Scraper Control Panel</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=8" />
|
<link rel="stylesheet" href="/static/style.css?v=9" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
@@ -43,10 +43,6 @@
|
|||||||
<path d="m8 5-5 7 5 7m8-14 5 7-5 7M14 3l-4 18" /></svg
|
<path d="m8 5-5 7 5 7m8-14 5 7-5 7M14 3l-4 18" /></svg
|
||||||
>API Docs</a
|
>API Docs</a
|
||||||
>
|
>
|
||||||
<a class="nav-link" href="/health"
|
|
||||||
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h4l2-6 4 12 2-6h6" /></svg
|
|
||||||
>Health</a
|
|
||||||
>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="status-card" id="active-account-card">
|
<section class="status-card" id="active-account-card">
|
||||||
@@ -90,87 +86,17 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-actions">
|
||||||
|
<a class="button primary" href="/settings">Open Settings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ════════ Settings Dialog ════════ -->
|
|
||||||
<dialog id="settings-dialog" class="settings-dialog">
|
|
||||||
<div class="dialog-shell">
|
|
||||||
<div class="dialog-header">
|
|
||||||
<div>
|
|
||||||
<div class="eyebrow">Settings</div>
|
|
||||||
<h2>Global & Accounts</h2>
|
|
||||||
</div>
|
|
||||||
<button class="button button-small" id="close-settings-btn" type="button">Close</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Accounts Management ── -->
|
|
||||||
<section class="settings-section">
|
|
||||||
<div class="section-title">Accounts</div>
|
|
||||||
<div id="accounts-list" class="accounts-list"></div>
|
|
||||||
|
|
||||||
<form id="add-account-form" class="stack-form add-account-form">
|
|
||||||
<input id="add-account-id" name="account_id" placeholder="Account ID (e.g. work)" required />
|
|
||||||
<input id="add-account-label" name="label" placeholder="Display name (e.g. Work Account)" />
|
|
||||||
<input id="add-account-api-id" name="api_id" placeholder="API ID" />
|
|
||||||
<input id="add-account-api-hash" name="api_hash" placeholder="API Hash" />
|
|
||||||
<button class="button primary" type="submit">Add Account</button>
|
|
||||||
</form>
|
|
||||||
<label class="import-account-row">
|
|
||||||
<span class="muted small">Import account settings JSON</span>
|
|
||||||
<input id="import-account-file" type="file" accept="application/json,.json" />
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Account Auth ── (shown per account selected in UI) -->
|
|
||||||
<section class="settings-section" id="account-auth-section">
|
|
||||||
<div class="section-title">Account Auth: <span id="auth-account-label">-</span></div>
|
|
||||||
<form id="credentials-form" class="stack-form">
|
|
||||||
<input id="api-id-input" name="api_id" placeholder="API ID" />
|
|
||||||
<input id="api-hash-input" name="api_hash" placeholder="API Hash" />
|
|
||||||
<button class="button" type="submit">Save credentials</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="auth-actions">
|
|
||||||
<button class="button primary" id="start-qr-btn" type="button">Start QR login</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="qr-wrap" class="qr-wrap hidden">
|
|
||||||
<img id="qr-image" alt="Telegram QR login" />
|
|
||||||
<p class="muted small">Open Telegram -> Settings -> Devices -> Scan QR.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="phone-form" class="stack-form">
|
|
||||||
<input id="phone-input" name="phone" placeholder="+1234567890" />
|
|
||||||
<button class="button" type="submit">Send code</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form id="code-form" class="stack-form hidden">
|
|
||||||
<input id="code-input" name="code" placeholder="Telegram code" />
|
|
||||||
<button class="button" type="submit">Confirm code</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form id="password-form" class="stack-form hidden">
|
|
||||||
<input id="password-input" name="password" type="password" placeholder="2FA password" />
|
|
||||||
<button class="button" type="submit">Confirm password</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Scraping ── -->
|
|
||||||
<section class="settings-section">
|
|
||||||
<div class="section-title">Scraping</div>
|
|
||||||
<label class="toggle-row">
|
|
||||||
<span>Download media</span>
|
|
||||||
<span class="switch">
|
|
||||||
<input id="scrape-media-toggle" type="checkbox" />
|
|
||||||
<span class="switch-slider"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<!-- ════════ Templates ════════ -->
|
<!-- ════════ Templates ════════ -->
|
||||||
|
|
||||||
<!-- Account Tab -->
|
<!-- Account Tab -->
|
||||||
@@ -239,12 +165,24 @@
|
|||||||
<p class="muted">Per-account channel list.</p>
|
<p class="muted">Per-account channel list.</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="inline-form add-channel-form">
|
<form class="inline-form add-channel-form">
|
||||||
<input class="add-channel-id" name="channel_id" placeholder="ID or @username" required />
|
<input
|
||||||
<input class="add-channel-name" name="name" placeholder="Display name" />
|
class="add-channel-id"
|
||||||
|
name="channel_id"
|
||||||
|
placeholder="ID or @username"
|
||||||
|
aria-label="Channel ID or username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="add-channel-name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Display name"
|
||||||
|
aria-label="Channel display name"
|
||||||
|
/>
|
||||||
<button class="button primary" type="submit">Add</button>
|
<button class="button primary" type="submit">Add</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -259,6 +197,7 @@
|
|||||||
<tbody class="channels-table"></tbody>
|
<tbody class="channels-table"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel jobs-panel">
|
<section class="panel jobs-panel">
|
||||||
@@ -269,7 +208,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="button refresh-jobs-btn">Refresh</button>
|
<button class="button refresh-jobs-btn">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="jobs-list"></div>
|
<div class="jobs-list"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -283,6 +224,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
<form class="continuous-form">
|
<form class="continuous-form">
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span>Enabled</span>
|
<span>Enabled</span>
|
||||||
@@ -291,9 +233,9 @@
|
|||||||
<span class="switch-slider"></span>
|
<span class="switch-slider"></span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="stack-form">
|
<label class="field">
|
||||||
<span class="muted small">Interval, minutes</span>
|
<span class="field-label">Interval, minutes</span>
|
||||||
<input type="number" class="continuous-interval" min="1" value="1" />
|
<input type="number" class="continuous-interval" min="1" value="1" aria-label="Interval, minutes" />
|
||||||
</label>
|
</label>
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span>All tracked channels</span>
|
<span>All tracked channels</span>
|
||||||
@@ -317,6 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="log-viewer continuous-logs"></div>
|
<div class="log-viewer continuous-logs"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -372,6 +315,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/static/app.js?v=5"></script>
|
<script src="/static/app.js?v=9"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+116
-19
@@ -4,23 +4,45 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Telegram Scraper Settings</title>
|
<title>Telegram Scraper Settings</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=8" />
|
<link rel="stylesheet" href="/static/style.css?v=9" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-shell settings-page-shell">
|
<div class="app-shell settings-page-shell">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand-block">
|
<div class="brand-block">
|
||||||
|
<svg class="brand-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="m21 3-7.6 18-3.8-7.6L2 9.6zM9.6 13.4 14 10" />
|
||||||
|
</svg>
|
||||||
<div class="eyebrow">Telegram Scraper</div>
|
<div class="eyebrow">Telegram Scraper</div>
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<p class="muted">Account credentials, import/export, and runtime safety controls.</p>
|
<p class="muted">Account credentials, import/export, and runtime safety controls.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a class="nav-link" href="/">Dashboard</a>
|
<a class="nav-link" href="/"
|
||||||
<a class="nav-link active" href="/settings">Settings</a>
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<a class="nav-link" href="/viewer">Message Viewer</a>
|
<rect x="4" y="4" width="6" height="6" />
|
||||||
<a class="nav-link" href="/swagger">API Docs</a>
|
<rect x="14" y="4" width="6" height="6" />
|
||||||
<a class="nav-link" href="/health">Health</a>
|
<rect x="4" y="14" width="6" height="6" />
|
||||||
|
<rect x="14" y="14" width="6" height="6" /></svg
|
||||||
|
>Dashboard</a
|
||||||
|
>
|
||||||
|
<a class="nav-link active" href="/settings"
|
||||||
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path
|
||||||
|
d="M12 2v3m0 14v3M2 12h3m14 0h3m-2.9-7.1-2.1 2.1M4.9 19.1 7 17m0-10-2.1-2.1m12.2 14.2-2.1-2.1" /></svg
|
||||||
|
>Settings</a
|
||||||
|
>
|
||||||
|
<a class="nav-link" href="/viewer"
|
||||||
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h16v12H8l-4 3z" /></svg>Message
|
||||||
|
Viewer</a
|
||||||
|
>
|
||||||
|
<a class="nav-link" href="/swagger"
|
||||||
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="m8 5-5 7 5 7m8-14 5 7-5 7M14 3l-4 18" /></svg
|
||||||
|
>API Docs</a
|
||||||
|
>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="status-card">
|
<section class="status-card">
|
||||||
@@ -30,11 +52,21 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="content settings-page">
|
<main class="content settings-page">
|
||||||
|
<header class="dashboard-topbar">
|
||||||
|
<div class="dashboard-title"><span class="menu-icon" aria-hidden="true">☰</span>Settings</div>
|
||||||
|
<div class="runtime-status">
|
||||||
|
<span class="runtime-dot"></span>Service: Running <i></i> Scraper: Idle <i></i
|
||||||
|
><span class="runtime-clock"></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
<section class="panel settings-hero">
|
<section class="panel settings-hero">
|
||||||
<div>
|
<div>
|
||||||
<div class="eyebrow">Account Settings</div>
|
<div class="eyebrow">Account Settings</div>
|
||||||
<h2 id="settings-title">Loading accounts</h2>
|
<h2 id="settings-title">Loading accounts</h2>
|
||||||
<p id="settings-subtitle" class="muted">Select an account to manage credentials and scraper options.</p>
|
<p id="settings-subtitle" class="muted">Select an account to manage credentials and scraper options.</p>
|
||||||
|
<p class="muted small" id="settings-hero-hint">
|
||||||
|
First step: add your first account below. Export and Delete unlock after you select an account.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<button class="button" id="settings-export-btn" type="button" disabled>Export</button>
|
<button class="button" id="settings-export-btn" type="button" disabled>Export</button>
|
||||||
@@ -44,8 +76,12 @@
|
|||||||
|
|
||||||
<section id="settings-empty" class="panel hidden">
|
<section id="settings-empty" class="panel hidden">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
|
<div class="eyebrow">Getting started</div>
|
||||||
<h2>Add first account</h2>
|
<h2>Add first account</h2>
|
||||||
<p class="muted">Create an account below or import an exported account JSON.</p>
|
<p class="muted">Create an account below or import an exported account JSON. No accounts yet.</p>
|
||||||
|
<div class="empty-actions">
|
||||||
|
<a class="button primary" href="#settings-add-account-form">Add first account</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -76,18 +112,50 @@
|
|||||||
<p class="muted">New accounts start without continuous scraping enabled.</p>
|
<p class="muted">New accounts start without continuous scraping enabled.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<form id="settings-add-account-form" class="stack-form">
|
<form id="settings-add-account-form" class="stack-form">
|
||||||
<input id="settings-add-account-id" name="account_id" placeholder="Account ID (e.g. work)" required />
|
<label class="field">
|
||||||
<input id="settings-add-account-label" name="label" placeholder="Display name" />
|
<span class="field-label">Account ID</span>
|
||||||
<input id="settings-add-account-api-id" name="api_id" placeholder="API ID" />
|
<input
|
||||||
<input id="settings-add-account-api-hash" name="api_hash" placeholder="API Hash" />
|
id="settings-add-account-id"
|
||||||
<button class="button primary" type="submit">Add Account</button>
|
name="account_id"
|
||||||
|
placeholder="Account ID (e.g. work)"
|
||||||
|
aria-label="Account ID"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="field-hint">Lowercase short id used in file names and URLs.</span>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">Display name</span>
|
||||||
|
<input
|
||||||
|
id="settings-add-account-label"
|
||||||
|
name="label"
|
||||||
|
placeholder="Display name"
|
||||||
|
aria-label="Display name"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">API ID</span>
|
||||||
|
<input id="settings-add-account-api-id" name="api_id" placeholder="API ID" aria-label="API ID" />
|
||||||
|
<span class="field-hint">From my.telegram.org > API development tools.</span>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">API Hash</span>
|
||||||
|
<input id="settings-add-account-api-hash" name="api_hash" placeholder="API Hash" aria-label="API Hash" />
|
||||||
|
<span class="field-hint">Paste the hash exactly as shown on my.telegram.org.</span>
|
||||||
|
</label>
|
||||||
|
<button class="button primary button-block" type="submit">Add Account</button>
|
||||||
</form>
|
</form>
|
||||||
<label class="import-account-row">
|
<label class="import-account-row">
|
||||||
<span class="muted small">Import account settings JSON</span>
|
<span class="field-label">Import account settings JSON</span>
|
||||||
|
<span class="muted small"
|
||||||
|
>Choose a file previously exported from Settings. It must be account JSON, for example:</span
|
||||||
|
>
|
||||||
|
<span class="file-example">{"account_id": "work", "api_id": 12345, ...}</span>
|
||||||
<input id="settings-import-account-file" type="file" accept="application/json,.json" />
|
<input id="settings-import-account-file" type="file" accept="application/json,.json" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -96,9 +164,20 @@
|
|||||||
<p class="muted" id="settings-auth-label">No account selected.</p>
|
<p class="muted" id="settings-auth-label">No account selected.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="muted small" id="settings-credentials-hint">
|
||||||
|
Select an account first, then save API credentials and complete login below.
|
||||||
|
</p>
|
||||||
<form id="settings-credentials-form" class="stack-form">
|
<form id="settings-credentials-form" class="stack-form">
|
||||||
<input id="settings-api-id-input" name="api_id" placeholder="API ID" />
|
<label class="field">
|
||||||
<input id="settings-api-hash-input" name="api_hash" placeholder="API Hash" />
|
<span class="field-label">API ID</span>
|
||||||
|
<input id="settings-api-id-input" name="api_id" placeholder="API ID" aria-label="API ID" />
|
||||||
|
<span class="field-hint">From my.telegram.org > API development tools.</span>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">API Hash</span>
|
||||||
|
<input id="settings-api-hash-input" name="api_hash" placeholder="API Hash" aria-label="API Hash" />
|
||||||
|
</label>
|
||||||
<button class="button" type="submit">Save credentials</button>
|
<button class="button" type="submit">Save credentials</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -112,20 +191,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="settings-phone-form" class="stack-form">
|
<form id="settings-phone-form" class="stack-form">
|
||||||
<input id="settings-phone-input" name="phone" placeholder="+1234567890" />
|
<label class="field">
|
||||||
|
<span class="field-label">Phone number</span>
|
||||||
|
<input id="settings-phone-input" name="phone" placeholder="+1234567890" aria-label="Phone number" />
|
||||||
|
</label>
|
||||||
<button class="button" type="submit">Send code</button>
|
<button class="button" type="submit">Send code</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form id="settings-code-form" class="stack-form hidden">
|
<form id="settings-code-form" class="stack-form hidden">
|
||||||
<input id="settings-code-input" name="code" placeholder="Telegram code" />
|
<label class="field">
|
||||||
|
<span class="field-label">Telegram code</span>
|
||||||
|
<input id="settings-code-input" name="code" placeholder="Telegram code" aria-label="Telegram code" />
|
||||||
|
</label>
|
||||||
<button class="button" type="submit">Confirm code</button>
|
<button class="button" type="submit">Confirm code</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form id="settings-password-form" class="stack-form hidden">
|
<form id="settings-password-form" class="stack-form hidden">
|
||||||
<input id="settings-password-input" name="password" type="password" placeholder="2FA password" />
|
<label class="field">
|
||||||
|
<span class="field-label">2FA password</span>
|
||||||
|
<input
|
||||||
|
id="settings-password-input"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="2FA password"
|
||||||
|
aria-label="2FA password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<button class="button" type="submit">Confirm password</button>
|
<button class="button" type="submit">Confirm password</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -134,6 +229,7 @@
|
|||||||
<p class="muted">Account-level parser settings.</p>
|
<p class="muted">Account-level parser settings.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span>Download media</span>
|
<span>Download media</span>
|
||||||
<span class="switch">
|
<span class="switch">
|
||||||
@@ -147,10 +243,11 @@
|
|||||||
<div>Media: <span id="settings-media-count">0</span></div>
|
<div>Media: <span id="settings-media-count">0</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/settings.js?v=1"></script>
|
<script src="/static/settings.js?v=9"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+44
-6
@@ -38,6 +38,42 @@ function showToast(message, type = 'info') {
|
|||||||
}, 3600);
|
}, 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 =
|
||||||
|
'<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 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) {
|
function downloadJson(filename, payload) {
|
||||||
const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], {
|
const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], {
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
@@ -105,7 +141,7 @@ function renderAccountList() {
|
|||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
|
|
||||||
if (!settingsState.accounts.length) {
|
if (!settingsState.accounts.length) {
|
||||||
root.innerHTML = '<div class="empty-state">No accounts yet.</div>';
|
root.innerHTML = '<div class="empty-state"><p class="muted">No accounts yet.</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,17 +204,19 @@ function renderAccountData() {
|
|||||||
: 'No account selected.';
|
: 'No account selected.';
|
||||||
|
|
||||||
document.getElementById('health-credentials').textContent = health.api_credentials ? 'Saved' : 'Missing';
|
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 =
|
document.getElementById('health-session').textContent =
|
||||||
health.session_ready || isAccountAuthorized(auth) ? 'Ready' : 'Missing';
|
health.session_ready || isAccountAuthorized(auth) ? 'Ready' : 'Missing';
|
||||||
document.getElementById('health-session').style.color =
|
setHealthState(
|
||||||
health.session_ready || isAccountAuthorized(auth) ? 'var(--ok)' : 'var(--warn)';
|
document.getElementById('health-session'),
|
||||||
|
Boolean(health.session_ready || isAccountAuthorized(auth)),
|
||||||
|
);
|
||||||
document.getElementById('health-continuous').textContent = continuousStatus.running
|
document.getElementById('health-continuous').textContent = continuousStatus.running
|
||||||
? 'Running'
|
? 'Running'
|
||||||
: continuousConfig.enabled
|
: continuousConfig.enabled
|
||||||
? 'Enabled'
|
? 'Enabled'
|
||||||
: 'Stopped';
|
: '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(
|
document.getElementById('health-last-scrape').textContent = displayTime(
|
||||||
health.last_scrape || continuousStatus.last_iteration_at,
|
health.last_scrape || continuousStatus.last_iteration_at,
|
||||||
);
|
);
|
||||||
@@ -342,7 +380,7 @@ async function exportAccount() {
|
|||||||
async function deleteAccount() {
|
async function deleteAccount() {
|
||||||
if (!settingsState.activeAccount) return;
|
if (!settingsState.activeAccount) return;
|
||||||
const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount);
|
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' });
|
await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}`, { method: 'DELETE' });
|
||||||
setActiveAccount(null);
|
setActiveAccount(null);
|
||||||
await loadAccounts();
|
await loadAccounts();
|
||||||
|
|||||||
+1187
-1458
File diff suppressed because it is too large
Load Diff
+32
-6
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Telegram Scraper API</title>
|
<title>Telegram Scraper API</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=6" />
|
<link rel="stylesheet" href="/static/style.css?v=9" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="viewer-shell">
|
<div class="viewer-shell">
|
||||||
@@ -17,18 +17,44 @@
|
|||||||
<a class="button button-small" href="/">Dashboard</a>
|
<a class="button button-small" href="/">Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a class="nav-link" href="/openapi.json">openapi.json</a>
|
<a class="nav-link" href="/"
|
||||||
<a class="nav-link" href="/health">Health</a>
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<a class="nav-link" href="/viewer">Message Viewer</a>
|
<rect x="4" y="4" width="6" height="6" />
|
||||||
|
<rect x="14" y="4" width="6" height="6" />
|
||||||
|
<rect x="4" y="14" width="6" height="6" />
|
||||||
|
<rect x="14" y="14" width="6" height="6" /></svg
|
||||||
|
>Dashboard</a
|
||||||
|
>
|
||||||
|
<a class="nav-link" href="/settings"
|
||||||
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path
|
||||||
|
d="M12 2v3m0 14v3M2 12h3m14 0h3m-2.9-7.1-2.1 2.1M4.9 19.1 7 17m0-10-2.1-2.1m12.2 14.2-2.1-2.1" /></svg
|
||||||
|
>Settings</a
|
||||||
|
>
|
||||||
|
<a class="nav-link" href="/viewer"
|
||||||
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h16v12H8l-4 3z" /></svg>Message
|
||||||
|
Viewer</a
|
||||||
|
>
|
||||||
|
<a class="nav-link active" href="/swagger"
|
||||||
|
><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="m8 5-5 7 5 7m8-14 5 7-5 7M14 3l-4 18" /></svg
|
||||||
|
>API Docs</a
|
||||||
|
>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="viewer-main">
|
<main class="viewer-main">
|
||||||
<header class="viewer-header">
|
<header class="viewer-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Endpoints</h2>
|
<div class="eyebrow">OpenAPI</div>
|
||||||
|
<h2>API Docs</h2>
|
||||||
<p class="muted">Local API surface exposed by the web UI server.</p>
|
<p class="muted">Local API surface exposed by the web UI server.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="viewer-tools">
|
||||||
|
<div class="runtime-status"><span class="runtime-dot"></span>Service: Running</div>
|
||||||
|
<a class="button button-small" href="/openapi.json">openapi.json</a>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section id="api-docs" class="api-docs"></section>
|
<section id="api-docs" class="api-docs"></section>
|
||||||
@@ -53,6 +79,6 @@
|
|||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/static/swagger.js"></script>
|
<script src="/static/swagger.js?v=9"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+47
-2
@@ -19,7 +19,30 @@ function renderSpec(spec) {
|
|||||||
const methodTemplate = document.getElementById('api-method-template');
|
const methodTemplate = document.getElementById('api-method-template');
|
||||||
root.innerHTML = '';
|
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);
|
const section = sectionTemplate.content.firstElementChild.cloneNode(true);
|
||||||
section.querySelector('.api-path').textContent = path;
|
section.querySelector('.api-path').textContent = path;
|
||||||
const methodsRoot = section.querySelector('.api-methods');
|
const methodsRoot = section.querySelector('.api-methods');
|
||||||
@@ -53,6 +76,28 @@ loadSpec()
|
|||||||
docsEl.textContent = '';
|
docsEl.textContent = '';
|
||||||
const section = document.createElement('section');
|
const section = document.createElement('section');
|
||||||
section.className = 'panel';
|
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);
|
docsEl.appendChild(section);
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Telegram Scraper Viewer</title>
|
<title>Telegram Scraper Viewer</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=8" />
|
<link rel="stylesheet" href="/static/style.css?v=9" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="viewer-shell">
|
<div class="viewer-shell">
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<h1>Messages</h1>
|
<h1>Messages</h1>
|
||||||
<p class="muted small">
|
<p class="muted small">
|
||||||
Account:
|
Account:
|
||||||
<select id="viewer-account-select"></select>
|
<select id="viewer-account-select" aria-label="Viewer account"></select>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="sidebar-toggle" class="button button-small sidebar-toggle">☰</button>
|
<button id="sidebar-toggle" class="button button-small sidebar-toggle">☰</button>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<p id="viewer-subtitle" class="muted">Reading messages from the local SQLite database.</p>
|
<p id="viewer-subtitle" class="muted">Reading messages from the local SQLite database.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer-tools">
|
<div class="viewer-tools">
|
||||||
<input id="viewer-search" type="search" placeholder="Search messages" />
|
<input id="viewer-search" type="search" placeholder="Search messages" aria-label="Search messages" />
|
||||||
<label class="viewer-auto-refresh">
|
<label class="viewer-auto-refresh">
|
||||||
<input id="viewer-auto-refresh" type="checkbox" checked />
|
<input id="viewer-auto-refresh" type="checkbox" checked />
|
||||||
<span>Auto-refresh</span>
|
<span>Auto-refresh</span>
|
||||||
@@ -80,6 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/static/viewer.js"></script>
|
<script src="/static/viewer.js?v=9"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+7
-2
@@ -395,10 +395,15 @@ function renderMessages(payload, append = false) {
|
|||||||
viewerState.oldestMessageId = null;
|
viewerState.oldestMessageId = null;
|
||||||
viewerState.newestMessageId = null;
|
viewerState.newestMessageId = null;
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'viewer-empty-state';
|
empty.className = 'empty-state viewer-empty-state';
|
||||||
empty.textContent = viewerState.search
|
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 messages found for "${viewerState.search}".`
|
||||||
: 'No saved messages in this channel yet.';
|
: 'No saved messages in this channel yet.';
|
||||||
|
empty.append(title, text);
|
||||||
root.appendChild(empty);
|
root.appendChild(empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user