feat: schedule persistent archive jobs with console management
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
const state = { token: '', authenticated: false, queue: '', base: location.origin, consoleConfig: null, retention: null, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: null, selectedWorkflow: null, workflowActionMode: '', workflowRecoveryMode: '', workflowRecoveryContext: null, selectedSubscription: null, subscriptionActionMode: '', selectedDelivery: null };
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const text = (id, value) => { const el = $(id); if (el) el.textContent = value; };
|
||||
const pageNames = { overview: '总览', tasks: '任务执行', schedules: '定时计划', workflows: '流程实例', subscriptions: '结果订阅', retention: '保留策略' };
|
||||
const pageNames = { overview: '总览', tasks: '任务执行', schedules: '定时计划', workflows: '流程实例', subscriptions: '结果订阅', retention: '保留策略', archives: '归档作业' };
|
||||
|
||||
function showToast(message) {
|
||||
const toast = $('toast'); toast.textContent = message; toast.classList.add('show');
|
||||
@@ -29,6 +29,7 @@
|
||||
if (updateHash && location.hash !== `#/${next}`) location.hash = `/${next}`;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
if (next === 'retention' && state.authenticated) loadRetention();
|
||||
if (next === 'archives' && state.authenticated) loadArchives();
|
||||
}
|
||||
function redirectToLogin() {
|
||||
const returnTo = `${location.pathname}${location.search}${location.hash}`;
|
||||
@@ -46,7 +47,7 @@
|
||||
const raw = await response.text(); let body = {};
|
||||
try { body = raw ? JSON.parse(raw) : {}; } catch { body = { message: raw }; }
|
||||
if (response.status === 401) { state.authenticated = false; redirectToLogin(); throw new Error('登录已过期,请重新登录'); }
|
||||
if (!response.ok) throw new Error(body?.error?.message || body?.message || `HTTP ${response.status}`);
|
||||
if (!response.ok) { const error = new Error(body?.error?.message || body?.message || `HTTP ${response.status}`); error.status = response.status; throw error; }
|
||||
return body;
|
||||
}
|
||||
async function loadConsoleConfig() {
|
||||
@@ -186,6 +187,96 @@
|
||||
} catch (error) { text('retention-error', error.message || '归档快照导出失败'); }
|
||||
finally { retentionUI.busy = false; syncRetentionControls(); }
|
||||
}
|
||||
const archiveUI = { busy: false, loading: false, dirty: false, plans: [], jobs: [], configured: false, policy: null, editing: null, editPolicy: '0', before: '', next: '', pending: null, selected: null };
|
||||
const archiveLabels = { tasks: '任务历史', workflows: '流程历史', events: '事件日志', audit: '管理审计', deliveries: '交付历史', inbox: 'Inbox 历史', idempotency: '幂等记录' };
|
||||
const archiveStates = { PENDING: '等待执行', RUNNING: '执行中', SUCCEEDED: '已完成', FAILED: '待处理', ABANDONED: '已关闭' };
|
||||
const archiveErrors = { FILE_CHECK_FAILED: '文件检查未通过,请核查持久卷、原目录与文件完整性。', STORAGE_LIMIT: '归档容量达到上限,请核查单作业与归档卷容量。', STORAGE_BUSY: '文件正被其他执行器使用,确认旧进程退出后可重试。', POLICY_CHANGED: '保留策略已变更或关闭。原作业不能替换策略版本,可关闭后重新配置周期。', PERMISSION_DENIED: '所属应用或命名空间已停用。', EXPORT_FAILED: '数据库导出未完成,确认数据库恢复后可重试。', INTERRUPTED: '执行中断,核查后可从原作业续跑。' };
|
||||
function syncArchiveControls() {
|
||||
document.querySelectorAll('#archive-plan-form input, #archive-plan-form select, #archive-plan-form button, #archive-action-form input, #archive-action-form button, [data-archive-action]').forEach(el => { el.disabled = archiveUI.busy; });
|
||||
$('archive-plan-save').disabled = archiveUI.busy || !archiveUI.editing || archiveUI.editPolicy === '0';
|
||||
$('archive-action-budget').disabled = archiveUI.busy || archiveUI.selected?.action === 'abandon';
|
||||
$('archive-next').disabled = archiveUI.busy || archiveUI.loading || !archiveUI.next;
|
||||
$('archive-first').disabled = archiveUI.busy || archiveUI.loading || !archiveUI.before;
|
||||
}
|
||||
function renderArchiveEditor() {
|
||||
const p = archiveUI.plans.find(p => p.kind === $('archive-kind').value); if (!p) return;
|
||||
archiveUI.editing = p; archiveUI.editPolicy = String(archiveUI.policy?.revision || '0'); archiveUI.dirty = false;
|
||||
$('archive-enabled').checked = p.enabled; $('archive-interval').value = p.interval_seconds / 3600;
|
||||
$('archive-batch').value = p.batch_size; $('archive-budget').value = p.max_bytes / 1048576;
|
||||
text('archive-plan-revision', `计划版本 ${p.revision} · 保留策略 ${archiveUI.editPolicy}`);
|
||||
text('archive-next-run', p.active_job_id ? `当前作业 ${shortID(p.active_job_id)} · 完成或关闭后继续周期` : `下次执行 ${p.enabled ? formatTime(p.next_run_at) : '尚未开启'}`);
|
||||
const stale = p.enabled && String(p.policy_revision) !== archiveUI.editPolicy;
|
||||
text('archive-policy-note', !archiveUI.policy?.enabled ? '请先在“保留策略”中允许只读归档。' : stale ? '保留策略已变更,重新保存本计划后才会触发新作业。' : '每个作业固定创建时的策略版本。');
|
||||
text('archive-plan-hint', archiveUI.configured ? '关闭周期只停止新作业,已有作业继续结清。失败作业需先处理,后续周期才会继续。' : '当前节点尚未配置归档持久卷,可查看历史或关闭已保存的周期。');
|
||||
syncArchiveControls();
|
||||
}
|
||||
function archiveSize(bytes) { return bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KiB` : `${(bytes / 1048576).toFixed(2)} MiB`; }
|
||||
function renderArchiveJobs() {
|
||||
const jobs = archiveUI.jobs;
|
||||
text('archive-job-count', `${jobs.length} 项`);
|
||||
text('archive-enabled-count', String(archiveUI.plans.filter(p => p.enabled).length));
|
||||
text('archive-failed-count', String(jobs.filter(j => j.state === 'FAILED').length));
|
||||
text('archive-root-count', jobs.reduce((sum, j) => sum + j.roots, 0).toLocaleString('zh-CN'));
|
||||
text('archive-executor', archiveUI.configured ? '已配置持久卷' : '未配置持久卷');
|
||||
$('archive-executor').classList.toggle('ready', archiveUI.configured);
|
||||
$('archive-plan-chips').innerHTML = archiveUI.plans.map(p => `<span class="${p.enabled ? 'enabled' : ''}">${archiveLabels[p.kind]} · ${p.enabled ? '开启' : '关闭'}</span>`).join('');
|
||||
$('archive-job-list').innerHTML = jobs.length ? jobs.map(j => `<article class="archive-job"><div class="archive-job-head"><div><b>${escapeHTML(archiveLabels[j.kind] || j.kind)}</b><span class="state-pill ${stateClass(j.state)}">${escapeHTML(archiveStates[j.state] || j.state)}</span></div><time>${escapeHTML(formatTime(j.created_at))}</time></div><code class="archive-job-id">${escapeHTML(j.id)}</code><div class="archive-progress"><span><b>${j.batches}</b> 批已确认</span><span><b>${j.roots.toLocaleString('zh-CN')}</b> 条根记录</span><span><b>${archiveSize(j.bytes)}</b> / ${j.max_bytes / 1048576} MiB</span><span>策略 v${escapeHTML(j.policy_revision)}</span></div>${j.last_sha256 ? `<details><summary>查看文件校验摘要</summary><code class="archive-digest">${escapeHTML(j.last_sha256)}</code></details>` : ''}${j.error_code ? `<p class="archive-job-error">${escapeHTML(archiveErrors[j.error_code] || j.error_code)}</p>` : ''}${j.state === 'RUNNING' ? `<p class="archive-job-note">领取次数 ${j.attempts} · 租约至 ${escapeHTML(formatTime(j.lease_until))},中断后可接管。</p>` : ''}${j.state === 'FAILED' ? `<div class="archive-job-actions"><button type="button" class="secondary-button compact" data-archive-action="retry" data-id="${escapeHTML(j.id)}">从原作业重试 ↻</button><button type="button" class="text-button" data-archive-action="abandon" data-id="${escapeHTML(j.id)}">关闭此作业</button></div>` : ''}</article>`).join('') : '<div class="empty-row archive-empty"><span class="empty-icon">▤</span><b>还没有归档作业</b><small>保存并开启周期后,系统会按保留窗口生成作业。</small></div>';
|
||||
syncArchiveControls();
|
||||
}
|
||||
async function loadArchives() {
|
||||
if (archiveUI.loading || archiveUI.busy) return; archiveUI.loading = true;
|
||||
try {
|
||||
const [plans, policy, jobs] = await Promise.all([request('/console/archive/plans'), request('/console/retention'), request(`/console/archive/jobs${archiveUI.before ? `?before=${encodeURIComponent(archiveUI.before)}` : ''}`)]);
|
||||
const config = extractData(plans), page = extractData(jobs);
|
||||
archiveUI.plans = config.items || []; archiveUI.configured = config.executor_configured;
|
||||
archiveUI.policy = extractData(policy); archiveUI.jobs = page.items || []; archiveUI.next = page.next_before || '';
|
||||
if (!archiveUI.dirty) renderArchiveEditor(); renderArchiveJobs();
|
||||
} catch (error) { text('archive-error', error.message || '归档作业读取失败'); }
|
||||
finally { archiveUI.loading = false; syncArchiveControls(); }
|
||||
}
|
||||
async function archiveMutation(path, method, input) {
|
||||
const body = JSON.stringify(input), signature = `${method}:${path}:${body}`;
|
||||
if (archiveUI.pending && archiveUI.pending.signature !== signature) throw new Error('上次操作尚未确认,请保持原内容重试;确认结果后再修改。');
|
||||
if (!archiveUI.pending) archiveUI.pending = { signature, key: crypto.randomUUID() };
|
||||
try {
|
||||
const response = await request(path, { method, headers: { 'Content-Type': 'application/json', 'Idempotency-Key': archiveUI.pending.key }, body });
|
||||
archiveUI.pending = null; return response;
|
||||
} catch (error) {
|
||||
// 明确的客户端拒绝允许核对后更正;网络故障和服务端错误保留原幂等键。
|
||||
if (error.status >= 400 && error.status < 500) archiveUI.pending = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function saveArchivePlan(event) {
|
||||
event.preventDefault(); if (archiveUI.busy || !archiveUI.editing) return;
|
||||
archiveUI.busy = true; syncArchiveControls(); text('archive-error', '');
|
||||
try {
|
||||
await archiveMutation(`/console/archive/plans/${encodeURIComponent(archiveUI.editing.kind)}`, 'PUT', { expected_revision: String(archiveUI.editing.revision), policy_revision: archiveUI.editPolicy, enabled: $('archive-enabled').checked, interval_seconds: Number($('archive-interval').value) * 3600, batch_size: Number($('archive-batch').value), max_bytes: Number($('archive-budget').value) * 1048576, reason: $('archive-reason').value });
|
||||
archiveUI.dirty = false; $('archive-reason').value = ''; showToast('周期配置已保存,操作已留存审计');
|
||||
} catch (error) { text('archive-error', `${error.message}。网络故障请保持原内容重试;版本冲突请刷新核对。`); }
|
||||
finally { archiveUI.busy = false; syncArchiveControls(); }
|
||||
if (!archiveUI.dirty) await loadArchives();
|
||||
}
|
||||
function openArchiveAction(id, action) {
|
||||
if (archiveUI.busy) return; const job = archiveUI.jobs.find(j => j.id === id); if (!job) return;
|
||||
archiveUI.selected = { job, action }; $('archive-action-form').hidden = false;
|
||||
text('archive-action-title', action === 'retry' ? '从原作业继续' : '关闭失败作业');
|
||||
text('archive-action-note', action === 'retry' ? `${shortID(id)} · 校验原文件后续跑,保留原策略与作业身份。容量不足时可提高上限。` : `${shortID(id)} · 保留已有文件和审计,不再重试本作业。已开启的计划可继续生成后续作业。`);
|
||||
$('archive-action-budget').value = job.max_bytes / 1048576; $('archive-action-budget').min = job.max_bytes / 1048576;
|
||||
$('archive-action-reason').value = ''; syncArchiveControls(); $('archive-action-reason').focus();
|
||||
}
|
||||
async function submitArchiveAction(event) {
|
||||
event.preventDefault(); if (archiveUI.busy || !archiveUI.selected) return;
|
||||
archiveUI.busy = true; syncArchiveControls(); text('archive-error', '');
|
||||
try {
|
||||
const { job, action } = archiveUI.selected;
|
||||
await archiveMutation(`/console/archive/jobs/${encodeURIComponent(job.id)}/actions`, 'POST', { expected_revision: String(job.revision), action, max_bytes: Number($('archive-action-budget').value) * 1048576, reason: $('archive-action-reason').value });
|
||||
$('archive-action-form').hidden = true; archiveUI.selected = null; showToast('作业处理已保存,操作已留存审计');
|
||||
} catch (error) { text('archive-error', `${error.message}。网络故障请保持原内容重试。`); }
|
||||
finally { archiveUI.busy = false; syncArchiveControls(); }
|
||||
await loadArchives();
|
||||
}
|
||||
|
||||
function renderConnectionSummary() {
|
||||
text('summary-api', state.base || location.origin);
|
||||
text('summary-api-hint', state.connected ? '接口响应正常,按 10 秒周期刷新' : 'API 地址自动跟随当前页面');
|
||||
@@ -597,6 +688,15 @@
|
||||
finally { state.loading = false; $('refresh-button').classList.remove('spinning'); }
|
||||
}
|
||||
async function logout() { await fetch('/console/auth/logout', { method: 'POST', credentials: 'same-origin' }); redirectToLogin(); }
|
||||
$('archive-plan-form').addEventListener('submit', saveArchivePlan);
|
||||
$('archive-plan-form').addEventListener('input', () => { archiveUI.dirty = true; });
|
||||
$('archive-kind').addEventListener('change', () => { if (archiveUI.pending) { $('archive-kind').value = archiveUI.editing.kind; showToast('请先保持原内容重试,确认上次操作'); return; } renderArchiveEditor(); $('archive-reason').value = ''; });
|
||||
$('archive-refresh').addEventListener('click', () => { if (!archiveUI.busy && !archiveUI.pending) { archiveUI.dirty = false; text('archive-error', ''); loadArchives(); } });
|
||||
$('archive-next').addEventListener('click', () => { if (archiveUI.loading || archiveUI.busy) return; archiveUI.before = archiveUI.next; loadArchives(); });
|
||||
$('archive-first').addEventListener('click', () => { if (archiveUI.loading || archiveUI.busy) return; archiveUI.before = ''; loadArchives(); });
|
||||
$('archive-job-list').addEventListener('click', event => { const button = event.target.closest('[data-archive-action]'); if (button) openArchiveAction(button.dataset.id, button.dataset.archiveAction); });
|
||||
$('archive-action-form').addEventListener('submit', submitArchiveAction);
|
||||
$('archive-action-cancel').addEventListener('click', () => { if (archiveUI.pending) { showToast('请先保持原内容重试,确认上次操作'); return; } $('archive-action-form').hidden = true; archiveUI.selected = null; });
|
||||
$('retention-kind').addEventListener('change', resetRetentionExport);
|
||||
$('retention-restart-button').addEventListener('click', resetRetentionExport);
|
||||
$('retention-form').addEventListener('input', event => { if (event.target.matches('input, textarea')) { retentionUI.dirty = true; syncRetentionControls(); } });
|
||||
@@ -607,6 +707,6 @@
|
||||
document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', event => { event.preventDefault(); setView(button.dataset.view); }));
|
||||
window.addEventListener('hashchange', () => setView(currentView(), false));
|
||||
setView(currentView(), false);
|
||||
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(() => Promise.all([loadRetention(), refresh()])); }).catch(() => redirectToLogin());
|
||||
state.timer = setInterval(() => { if (state.authenticated) refresh(); }, 10000);
|
||||
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(() => Promise.all([loadRetention(), refresh(), currentView() === 'archives' ? loadArchives() : Promise.resolve()])); }).catch(() => redirectToLogin());
|
||||
state.timer = setInterval(() => { if (state.authenticated) { refresh(); if (currentView() === 'archives') loadArchives(); } }, 10000);
|
||||
})();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<button class="nav-item" data-view="workflows"><span class="nav-icon">⌘</span><span>流程实例</span><em>04</em></button>
|
||||
<button class="nav-item" data-view="subscriptions"><span class="nav-icon">⌁</span><span>结果订阅</span><em>05</em></button>
|
||||
<button class="nav-item" data-view="retention"><span class="nav-icon">⌛</span><span>保留策略</span><em>06</em></button>
|
||||
<button class="nav-item" data-view="archives"><span class="nav-icon">▤</span><span>归档作业</span><em>07</em></button>
|
||||
</nav>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="rail-card">
|
||||
@@ -130,6 +131,41 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="page-view" id="view-archives" data-page="archives">
|
||||
<div class="archive-heading"><div><span class="panel-kicker">DATA / ARCHIVE JOBS</span><h1>让历史数据,有序归档。</h1><p>配置周期,自动分批保存历史;每次执行都有进度与校验记录。</p></div><button class="secondary-button compact" id="archive-refresh">刷新作业 ↻</button></div>
|
||||
<div class="archive-stats"><article><span>周期计划</span><strong id="archive-enabled-count">—</strong><small>已启用 / 7 个类别</small></article><article><span>本页待处理</span><strong id="archive-failed-count">—</strong><small>失败作业保留进度,等待处理</small></article><article><span>本页归档记录</span><strong id="archive-root-count">—</strong><small>数据库已确认的根记录数</small></article></div>
|
||||
<div id="archive-error" class="action-error" role="alert"></div>
|
||||
<section class="archive-grid">
|
||||
<article class="panel archive-config">
|
||||
<div class="panel-heading"><div><span class="panel-kicker">SCHEDULE</span><h2>周期配置</h2></div><span class="connection-state" id="archive-executor">正在读取</span></div>
|
||||
<form id="archive-plan-form">
|
||||
<label class="field-label" for="archive-kind">归档类别</label><select id="archive-kind"><option value="tasks">任务历史</option><option value="workflows">流程历史</option><option value="events">事件日志</option><option value="audit">管理审计</option><option value="deliveries">交付历史</option><option value="inbox">Inbox 历史</option><option value="idempotency">幂等记录</option></select>
|
||||
<div class="retention-switch"><div><b>开启周期归档</b><small>首次启用立即运行,之后按周期执行。</small></div><label class="switch"><input id="archive-enabled" type="checkbox" aria-label="开启周期归档"><span></span></label></div>
|
||||
<div class="archive-input-grid"><label for="archive-interval">执行间隔(小时)<input id="archive-interval" type="number" min="1" max="720" required></label><label for="archive-batch">每批根记录数<input id="archive-batch" type="number" min="1" max="100" required></label></div>
|
||||
<label class="field-label" for="archive-budget">单作业容量上限(MiB)</label><input id="archive-budget" type="number" min="1" max="1024" required>
|
||||
<div class="archive-plan-meta"><span id="archive-plan-revision">配置尚未保存</span><span id="archive-next-run">下次执行 —</span><span id="archive-policy-note"></span></div>
|
||||
<label class="field-label" for="archive-reason">变更原因</label><input id="archive-reason" type="text" maxlength="1024" placeholder="说明本次周期配置的调整原因" required>
|
||||
<button id="archive-plan-save" class="primary-button compact" type="submit">保存周期配置 <span>→</span></button>
|
||||
<p class="panel-description" id="archive-plan-hint">归档使用已保存的保留策略。关闭周期只停止新作业,已有作业继续结清。</p>
|
||||
</form>
|
||||
<div class="archive-plan-chips" id="archive-plan-chips"></div>
|
||||
<button class="text-button" data-view="retention">调整历史保留时间 ↗</button>
|
||||
</article>
|
||||
<article class="panel archive-history">
|
||||
<div class="panel-heading"><div><span class="panel-kicker">EXECUTION HISTORY</span><h2>归档作业</h2></div><span class="table-count" id="archive-job-count">0 项</span></div>
|
||||
<p class="panel-description">源数据库记录始终保留。文件写入服务端持久卷,页面展示已确认的批次和 SHA-256 摘要。</p>
|
||||
<div class="archive-job-list" id="archive-job-list"><div class="empty-row">正在读取归档作业…</div></div>
|
||||
<div class="archive-pager"><button class="secondary-button compact" id="archive-first">返回最新</button><button class="secondary-button compact" id="archive-next" disabled>更早作业 ↓</button></div>
|
||||
<form id="archive-action-form" hidden>
|
||||
<h3 id="archive-action-title">处理失败作业</h3><p class="panel-description" id="archive-action-note"></p>
|
||||
<label class="field-label" for="archive-action-budget">单作业容量上限(MiB)</label><input id="archive-action-budget" type="number" min="1" max="1024" required>
|
||||
<label class="field-label" for="archive-action-reason">处理原因</label><input id="archive-action-reason" type="text" maxlength="1024" placeholder="说明核查情况和处理原因" required>
|
||||
<div class="action-form-buttons"><button id="archive-action-submit" type="submit" class="primary-button compact">确认处理</button><button id="archive-action-cancel" type="button" class="secondary-button compact">取消</button></div>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="page-view" id="view-retention" data-page="retention">
|
||||
<section class="content-grid single-column">
|
||||
<article class="panel wide-panel retention-panel">
|
||||
|
||||
@@ -10,3 +10,5 @@
|
||||
.retention-tools{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-top:22px;padding-top:18px;border-top:1px solid rgba(156,183,218,.1)}.retention-select{display:flex;align-items:center;gap:10px;color:#9eb4cb;font-size:10px}.retention-select select{min-width:150px;border:1px solid rgba(156,183,218,.15);border-radius:9px;background:#0a1a2e;color:#dceafb;font:inherit;padding:9px 11px;outline:0}.retention-select select:focus{border-color:rgba(98,230,212,.58)}.retention-tool-buttons{display:flex;gap:9px}.secondary-button.compact{min-height:37px;padding:0 14px}.retention-preview{margin-top:13px;padding:13px 14px;border:1px solid rgba(156,183,218,.1);border-radius:11px;background:rgba(4,13,24,.28);color:#7189a3;font-size:10px;line-height:1.5}.retention-preview-head{display:flex;justify-content:space-between;gap:10px;padding-bottom:8px;color:#c9d8e8}.retention-preview-head small{color:#7189a3}.retention-preview-row{display:grid;grid-template-columns:1fr auto 1.6fr;gap:12px;padding:7px 0;border-top:1px solid rgba(156,183,218,.08)}.retention-preview-row b{color:#9beade;font-weight:600}.retention-preview-row small{color:#7189a3;text-align:right}@media(max-width:780px){.retention-tools{align-items:stretch;flex-direction:column}.retention-tool-buttons{justify-content:flex-start}.retention-preview-row{grid-template-columns:1fr auto}.retention-preview-row small{grid-column:span 2;text-align:left}}
|
||||
|
||||
.retention-tool-buttons{flex-wrap:wrap}.secondary-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid rgba(156,183,218,.18);border-radius:9px;background:rgba(23,46,65,.6);color:#b9cddd;font:inherit;font-size:10px;cursor:pointer}.secondary-button:hover{border-color:#62e6d4;color:#9beade}.retention-form button:disabled{opacity:.45;cursor:not-allowed}.retention-export-result{display:flex;flex-wrap:wrap;gap:10px;color:#9eb4cb;font-size:10px;line-height:1.6}.retention-export-result a{color:#62e6d4}.retention-export-result code{width:100%;font-size:9px;overflow-wrap:anywhere;color:#7189a3}.retention-export-result [hidden]{display:none}.retention-tools{flex-wrap:wrap}
|
||||
|
||||
.archive-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin:22px 0}.archive-heading h1{font-size:28px;letter-spacing:-.7px;margin:12px 0;color:#e4effb}.archive-heading p{color:#849bb3;font-size:12px;line-height:1.7}.archive-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px;margin-bottom:20px}.archive-stats article{padding:22px 24px;border:1px solid #20354a;border-radius:18px;background:linear-gradient(125deg,#112439,#0a1728)}.archive-stats article:first-child{background:linear-gradient(125deg,#13373e,#0b202d);border-color:#24535a}.archive-stats span,.archive-stats small{display:block;color:#8ba2b8;font-size:11px}.archive-stats strong{display:block;font-size:30px;margin:10px 0;color:#e5f9f5}.archive-grid{display:grid;grid-template-columns:minmax(290px,360px) minmax(0,1fr);gap:20px;align-items:start}.archive-config,.archive-history{padding:24px;min-width:0}.archive-config .panel-heading,.archive-history .panel-heading{padding:0;margin-bottom:20px}.archive-config input:not([type=checkbox]),.archive-config select,#archive-action-form input{width:100%;border:1px solid #294157;border-radius:9px;padding:11px 12px;background:#09192a;color:#dbe8f5;font:inherit;font-size:12px;outline:0}.archive-config input:focus,.archive-config select:focus,#archive-action-form input:focus{border-color:#61dfd0;box-shadow:0 0 0 3px #61dfd010}.archive-config .field-label,#archive-action-form .field-label{display:block;margin:18px 0 9px;font-size:11px}.archive-config .retention-switch{margin-top:19px;padding:13px 12px}.archive-input-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:20px;color:#94adc4;font-size:11px}.archive-input-grid input{margin-top:9px}.archive-plan-meta{display:grid;gap:9px;margin:17px 0;color:#7893ae;font-size:10px;line-height:1.6}.archive-config .primary-button{width:100%;margin:17px 0 6px}.archive-config .panel-description{line-height:1.8;font-size:10px}.archive-plan-chips{display:flex;flex-wrap:wrap;gap:7px;margin:16px 0}.archive-plan-chips span{padding:6px 8px;border:1px solid #24374a;border-radius:7px;font-size:9px;color:#7590aa}.archive-plan-chips .enabled{color:#85e5d8;border-color:#2d6564;background:#184b4b44}.archive-history>.panel-description{margin-bottom:18px;line-height:1.8}.archive-job{border:1px solid #243a50;border-radius:13px;padding:18px;margin:12px 0;background:#09172766}.archive-job-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.archive-job-head>div{display:flex;align-items:center;gap:12px}.archive-job-head b{font-size:13px;color:#d2e4f1}.archive-job-head time{font-size:10px;color:#7c98b1}.archive-job-id{display:block;margin:10px 0;color:#65849f;font-size:10px;overflow-wrap:anywhere}.archive-progress{display:flex;flex-wrap:wrap;gap:14px;margin:14px 0;color:#8eaac1;font-size:10px}.archive-progress b{color:#ccdee9;font-size:12px;font-weight:550}.archive-job details{font-size:10px;color:#7297b5;line-height:1.7}.archive-job summary{cursor:pointer}.archive-digest{display:block;overflow-wrap:anywhere;margin-top:8px;color:#9cbad0}.archive-job-error{padding:10px 12px;background:#59412338;border-radius:8px;color:#d9b484;font-size:11px;line-height:1.8}.archive-job-note{font-size:10px;color:#7698b2}.archive-job-actions,.archive-pager{display:flex;gap:14px;align-items:center;margin-top:16px}.archive-pager{justify-content:space-between}.archive-empty{padding:60px 20px}.archive-empty b,.archive-empty small{display:block;margin-top:15px}.archive-grid button:disabled{opacity:.4;cursor:not-allowed}.archive-history [hidden]{display:none}#archive-action-form{border:1px solid #36736f;border-radius:13px;padding:20px;margin-top:20px;background:#102c3544}#archive-action-form h3{font-size:16px;color:#cdeae6}.state-pill.abandoned{color:#9bb0c2;background:#52637933}@media(max-width:1150px){.archive-grid{grid-template-columns:1fr}.archive-stats{gap:10px}.archive-heading h1{font-size:24px}}@media(max-width:600px){.archive-stats{grid-template-columns:1fr}.archive-heading{align-items:flex-start;flex-direction:column}.archive-config,.archive-history{padding:18px}.archive-job-head{align-items:flex-start;flex-direction:column}}
|
||||
|
||||
Reference in New Issue
Block a user