web/admin/index.html
Rama main · réplica de lectura del repositorio original.
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- See web/index.html's identical meta for the full reasoning -- this
page IS the sink finding 2 reproduced against (renderKeys'
data-pid/data-hash, the vocabulary card's data-vocab-refresh), so its
own copy of this policy is the most load-bearing one in the repo: an
injected inline handler here has same-origin access to whatever admin
key sits in this page's localStorage (web/js/apikey.js). script-src
'self' only, no 'unsafe-inline' -- this page's only script is main.js. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src *; base-uri 'none'; form-action 'self'; object-src 'none'">
<title>پنل مدیریت — عامل هوشمند SQL</title>
<link rel="stylesheet" href="../styles/tokens.css">
<link rel="stylesheet" href="../styles/fonts.css">
<link rel="stylesheet" href="../styles/style.css">
<link rel="stylesheet" href="admin.css">
<style>
.icon { display: inline-block; vertical-align: -0.15em; flex: 0 0 auto; }
</style>
</head>
<body class="admin-body">
<header class="topbar admin-topbar">
<div class="brand">
<span class="brand-mark admin-mark" aria-hidden="true">ADM</span>
<div class="brand-text">
<h1>پنل مدیریت</h1>
<p>وضعیت استقرار، ممیزی، پیکربندی و مدیریت کلیدها</p>
</div>
</div>
<div class="topbar-right">
<div class="live-base-row" id="live-base-row">
<input type="text" id="live-base-input" dir="ltr" aria-label="نشانی بکاند" class="live-base-input">
<button class="live-base-connect" id="live-base-connect" type="button">اتصال</button>
</div>
<!-- Same two-state key control as the analyst UI (web/index.html) --
see the comment there for why an always-visible empty password
box was read as "signed out". -->
<div class="live-key-row" id="live-key-row">
<span class="live-key-status" id="live-key-status"></span>
<span class="live-key-entry" id="live-key-entry">
<input type="password" id="live-key-input" dir="ltr" aria-label="کلید API" class="live-key-input" placeholder="کلید مدیریتی" autocomplete="off">
<button class="live-key-save" id="live-key-save" type="button">ذخیره</button>
</span>
<button class="live-key-change" id="live-key-change" type="button" hidden>تغییر کلید</button>
<button class="live-key-clear" id="live-key-clear" type="button">حذف</button>
</div>
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="تغییر پوسته">
<svg class="icon" id="theme-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8.5 8.5 0 019.5 4a7 7 0 1010.5 10.5z"/></svg>
<span id="theme-toggle-label">پوسته</span>
</button>
<button class="admin-refresh-all" id="admin-refresh-all" type="button">
<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
بروزرسانی
</button>
<span class="admin-last-updated" id="admin-last-updated" aria-live="polite"></span>
</div>
</header>
<nav class="admin-jump" id="admin-jump" aria-label="بخشها">
<a href="#overview">نمای کلی</a>
<a href="#sec-maintenance">تعمیر</a>
<a href="#sec-health">بررسیها</a>
<a href="#sec-stats">ممیزی</a>
<a href="#sec-feedback">بازخورد</a>
<a href="#sec-access-requests">درخواست دسترسی</a>
<a href="#sec-cache">کش</a>
<a href="#sec-config">پیکربندی</a>
<a href="#sec-keys">کلیدها</a>
<a href="#sec-schema">شِما</a>
<a href="#sec-vocab">واژگان</a>
<a href="#sec-quality">کیفیت</a>
<a href="#sec-usage">مصرف</a>
<a href="#sec-auth">احراز</a>
</nav>
<main class="admin-layout">
<div id="admin-forbidden-banner" class="admin-banner admin-banner-warn" hidden></div>
<!-- Filled by main.js through js/diagnosis.js when the address in the top
bar is not the API, or the API was refused/unreachable: said once here,
each card then points to it. Text only (textContent). -->
<div id="admin-diagnosis-banner" class="admin-banner admin-banner-diagnosis" role="alert" hidden></div>
<div id="admin-notice-area"></div>
<!-- Health summary rail: the first thing an operator reads. Answers
"is anything on fire?" without scrolling ten sections. -->
<section class="admin-summary-rail" id="overview" aria-label="نمای کلی">
<div class="rail-item" id="rail-maintenance">
<div class="rail-lab">حالت تعمیر</div>
<div class="rail-val">—</div>
<div class="rail-sub">—</div>
</div>
<div class="rail-item" id="rail-checks">
<div class="rail-lab">بررسیهای استقرار</div>
<div class="rail-val">—</div>
<div class="rail-sub">—</div>
</div>
<div class="rail-item" id="rail-feedback">
<div class="rail-lab">بازخورد باز</div>
<div class="rail-val">—</div>
<div class="rail-sub">—</div>
</div>
<div class="rail-item" id="rail-cache">
<div class="rail-lab">کش پرسوجو</div>
<div class="rail-val">—</div>
<div class="rail-sub">—</div>
</div>
</section>
<!-- Maintenance mode leads even the deployment checks: it is an
operations action that stops analyst queries, so its current state
is the single most important fact on this page whenever it is on.
The panel itself must never depend on maintenance being off to
render this section. -->
<section class="admin-section" id="sec-maintenance" aria-labelledby="maintenance-heading">
<div class="admin-section-head">
<h2 id="maintenance-heading">حالت تعمیر و نگهداری</h2>
<button class="admin-btn-refresh" data-refresh="maintenance" type="button" aria-label="بروزرسانی وضعیت تعمیر">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="maintenance-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<!-- Deployment checks lead, because they are the only thing on this page
that might need action right now. State is encoded as a severity
stripe, so a failure reads before any number does. -->
<section class="admin-section" id="sec-health" aria-labelledby="rail-heading">
<div class="admin-section-head">
<h2 id="rail-heading">بررسیهای استقرار</h2>
<div class="admin-section-actions">
<!-- Deep checks (a rolled-back CREATE TABLE DDL attempt, a
multi-second WAITFOR probe, and with several data sources a
full catalogue read to place each table) are opt-in only -- never part
of the ordinary refresh, and never the 30s auto-refresh.
See web/admin/main.js's EXPENSIVE_CARDS and this button's
confirm() dialog. -->
<button class="admin-btn-refresh" id="health-deep-btn" type="button">
بررسیهای عمیق (DDL/WAITFOR)
</button>
<button class="admin-btn-refresh" data-refresh="health" type="button" aria-label="اجرای دوبارهٔ بررسیهای استقرار">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
</div>
<div id="health-body" class="admin-rail">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<!-- The figures are the point of this page, so they lead the detail. -->
<section class="admin-section" id="sec-stats" aria-labelledby="stats-heading">
<div class="admin-section-head">
<h2 id="stats-heading">ممیزی</h2>
<div class="admin-section-actions">
<label class="admin-toggle">
<input type="checkbox" id="include-examples-toggle">
<span>نمونهٔ سؤالهای واقعی</span>
</label>
<button class="admin-btn-refresh" data-refresh="summary" type="button" aria-label="بروزرسانی ممیزی">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
</div>
<div id="summary-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<!-- The feedback loop this panel exists for (spec §3, §5). -->
<section class="admin-section" id="sec-feedback" aria-labelledby="feedback-heading">
<div class="admin-section-head">
<h2 id="feedback-heading">بازخورد و رفع اشکال</h2>
<div class="admin-section-actions">
<select id="feedback-status-filter" class="admin-toggle">
<option value="open">باز</option>
<option value="resolved">رفعشده</option>
<option value="">همه</option>
</select>
<button class="admin-btn-refresh" data-refresh="feedback" type="button" aria-label="بروزرسانی بازخوردها">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
</div>
<div id="feedback-stats-body"></div>
<div id="feedback-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<!-- ADR-004 part 1: "Request access" from a denied-column guard
rejection. SECURITY only, for both listing and acting (owner
decision) -- unlike the feedback queue above, which either admin
role may triage. An operations-only key sees the same capability-
gap message web/admin/main.js already renders for the keys/roles
section below, never the page-level "not an admin key" banner. -->
<section class="admin-section" id="sec-access-requests" aria-labelledby="access-requests-heading">
<div class="admin-section-head">
<h2 id="access-requests-heading">درخواستهای دسترسی (security)</h2>
<button class="admin-btn-refresh" data-refresh="accessRequests" type="button" aria-label="بروزرسانی درخواستهای دسترسی">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="accessRequests-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<div class="admin-columns">
<section class="admin-section" id="sec-cache" aria-labelledby="cache-heading">
<div class="admin-section-head">
<h2 id="cache-heading">کش پرسوجو</h2>
<div class="admin-section-actions">
<button class="admin-btn-refresh" id="cache-clear-btn" type="button">پاک کردن کش</button>
<button class="admin-btn-refresh" data-refresh="cache" type="button" aria-label="بروزرسانی کش">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
</div>
<div id="cache-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<section class="admin-section" id="sec-config" aria-labelledby="config-heading">
<div class="admin-section-head">
<h2 id="config-heading">پیکربندی دامنه</h2>
<button class="admin-btn-refresh" data-refresh="config" type="button" aria-label="بروزرسانی پیکربندی">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="config-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
</div>
<!-- Admin panel phase 7: the key lifecycle and the two admin roles.
Every route behind this section has existed since phase 2 with
nothing in the UI calling it, so issuing a key or granting a role
meant the CLI plus a server restart. This is the panel's first
write that changes who can get in, so it sits below the read-only
diagnostics rather than above them: nothing here needs to be seen
to understand whether the deployment is healthy. -->
<section class="admin-section" id="sec-keys" aria-labelledby="keys-heading">
<div class="admin-section-head">
<h2 id="keys-heading">کلیدها و دسترسیها</h2>
<div class="admin-section-actions">
<button class="admin-btn-refresh" id="keys-issue-btn" type="button">صدور کلید تازه</button>
<button class="admin-btn-refresh" data-refresh="keys" type="button" aria-label="بروزرسانی کلیدها">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
</div>
<!-- The one-time reveal. Lives outside keys-body so a background
refresh of the list can never re-render it away while the raw
key it is showing is still the only copy in existence. -->
<div id="keys-reveal" class="keys-reveal" hidden></div>
<div id="keys-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<!-- Admin panel phase 6: schema drift and vocabulary freshness, both
read-only diagnostics -- neither applies anything by itself. -->
<div class="admin-columns">
<section class="admin-section" id="sec-schema" aria-labelledby="schema-drift-heading">
<div class="admin-section-head">
<h2 id="schema-drift-heading">انحراف شِما</h2>
<button class="admin-btn-refresh" data-refresh="schemaDrift" type="button" aria-label="بررسی انحراف شِما">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="schemaDrift-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<section class="admin-section" id="sec-vocab" aria-labelledby="vocabulary-heading">
<div class="admin-section-head">
<h2 id="vocabulary-heading">تازگی واژگان ابعاد</h2>
<button class="admin-btn-refresh" data-refresh="vocabulary" type="button" aria-label="بروزرسانی واژگان">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="vocabulary-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
</div>
<!-- Real-use quality: the audit log joined with analysts' "this
answer is correct" confirmations and wrong-answer flags, plus latency.
GET /admin/quality is server-cached (it reads the whole audit log), so
this card stays in the 30s auto-refresh. -->
<section class="admin-section" id="sec-quality" aria-labelledby="quality-heading">
<div class="admin-section-head">
<h2 id="quality-heading">کیفیت در استفادهی واقعی</h2>
<div class="admin-section-actions">
<label class="admin-inline-label" for="quality-days">بازه</label>
<select id="quality-days" class="admin-select">
<option value="7" selected>۷ روز</option>
<option value="30">۳۰ روز</option>
<option value="90">۹۰ روز</option>
</select>
<button class="admin-btn-refresh" data-refresh="quality" type="button" aria-label="بروزرسانی کیفیت">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
</div>
<div id="quality-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<!-- Admin panel phase 6: per-analyst usage/rate-limit pressure, and
failed-authentication visibility. -->
<div class="admin-columns">
<section class="admin-section" id="sec-usage" aria-labelledby="usage-heading">
<div class="admin-section-head">
<h2 id="usage-heading">مصرف هر تحلیلگر</h2>
<button class="admin-btn-refresh" data-refresh="usage" type="button" aria-label="بروزرسانی مصرف">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="usage-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
<section class="admin-section" id="sec-auth" aria-labelledby="auth-failures-heading">
<div class="admin-section-head">
<h2 id="auth-failures-heading">تلاشهای ناموفق احراز هویت</h2>
<button class="admin-btn-refresh" data-refresh="authFailures" type="button" aria-label="بروزرسانی احراز هویت">
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0114-5.3M20 12a8 8 0 01-14 5.3M18 4v4h-4M6 20v-4h4"/></svg>
</button>
</div>
<div id="authFailures-body">
<p class="admin-loading">در حال بارگذاری…</p>
</div>
</section>
</div>
</main>
<footer class="foot admin-foot">
<span>هیچ پرسوجویی روی انبار داده اجرا نمیشود و هیچ ردیف نتیجهای اینجا نمایش داده نمیشود.</span>
<span id="foot-time"></span>
</footer>
<script type="module" src="main.js"></script>
</body>
</html>