pediatric-ai-scribe-v3/public/components/admin.html
Daniel a505244b97
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 45s
Forgejo Android APK / Build signed APK (push) Successful in 2m8s
Forgejo Docker Build / Build Docker image (push) Successful in 10s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
refactor: remove the embedding settings, whose only consumer is gone
Embeddings existed here for Learning Hub semantic search — the card said
so itself. Learning Hub was removed, and nothing took its place: the
clinical corpus is embedded by the indexing service, not by this app.
What was left was a settings page that configured a model, tested it,
reported its dimensions, and fed nothing.

src/utils/embeddings.js had exactly one importer, src/routes/adminConfig
.js, which used it for the three routes this deletes. Outside those, the
only mentions of embedding in the server were a comment and a settings
prefix.

Gone: the module, its three admin routes, the dimension probe, the
Discover & test kind and its two panels, the admin.js block behind them,
the embeddings. prefix from both the writable-settings allowlist and the
lockdown list (it can no longer be written at all, so locking it says
nothing), and docs/embeddings-setup.md, which documented Learning Hub
search end to end.

'embedding' stays in NON_CHAT_MODES — that is the filter keeping
embedding models out of the chat-model list, and the gateway still
serves them.

Docs still describe nine /api/learning endpoints that no longer exist,
left from the Learning Hub removal. Not touched here; that is its own
subject.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 21:47:09 +02:00

706 lines
49 KiB
HTML

<div class="module-header">
<h2><i class="fas fa-user-shield"></i> Admin Panel</h2>
<p>Manage users, app settings, and view usage statistics</p>
</div>
<!-- Stats -->
<div class="admin-stats" id="admin-stats">
<div class="stat-card"><div class="stat-value" id="stat-users"></div><div class="stat-label">Total Users</div></div>
<div class="stat-card"><div class="stat-value" id="stat-api-total"></div><div class="stat-label">Total API Calls</div></div>
<div class="stat-card"><div class="stat-value" id="stat-api-today"></div><div class="stat-label">API Calls Today</div></div>
</div>
<!-- Every card is a <details> so a long page can be folded, and so a
"Save & Close" has something to close. All open by default: nothing
is hidden from an admin who never notices the chevron. Cards are
grouped under a heading by what they are about, in the order an
admin setting up a fresh install would need them. -->
<!-- ═══ Accounts ═══════════════════════════════════════════════ -->
<h2 class="admin-section-title">Accounts</h2>
<!-- Registration ─────────────────────────────────────────────
One card, read top to bottom: open registration at all, then decide
whether it needs an invitation, then hand out the codes. These were
two cards far apart, which made "enabled but invite-only" look like
two unrelated settings instead of one decision. -->
<details class="card" open>
<summary class="card-header"><h3><i class="fas fa-door-open"></i> Registration</h3></summary>
<div class="admin-card-body" style="gap:14px;">
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;">
<span id="reg-status-text" style="font-size:13px;color:var(--g600);">Loading...</span>
<button id="btn-toggle-reg" class="btn-sm btn-primary" type="button">Toggle</button>
</div>
<div class="admin-row">
<strong class="admin-row-label">Invite only</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" id="admin-invite-only">
Require an invitation code to register
</label>
</div>
</div>
<div style="border-top:1px solid var(--g100);padding-top:12px;">
<label for="admin-invite-note" class="admin-subhead" style="display:block;">Create an invitation</label>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<input type="text" id="admin-invite-note" placeholder="Who is this for? (optional)" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:180px;">
<label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;">Valid for
<input type="number" id="admin-invite-days" value="7" min="1" max="90" style="width:70px;font-size:13px;padding:6px 8px;border:1px solid var(--g300);border-radius:6px;"> days
</label>
<button id="btn-create-invite" class="btn-sm btn-primary" type="button"><i class="fas fa-plus"></i> Create</button>
</div>
<div id="admin-invite-new" style="margin-top:10px;"></div>
<p class="admin-note" style="margin-top:6px;">The code is shown once, here. Only its hash is stored, so it cannot be read again afterwards.</p>
</div>
<div id="admin-invites-list" style="display:flex;flex-direction:column;gap:4px;max-height:340px;overflow-y:auto;"></div>
<!-- Only ever clears spent codes. Hidden until there are some. -->
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
<button id="btn-clear-used-invites" class="btn-sm btn-ghost" type="button" hidden style="color:var(--red);"></button>
</div>
</div>
</details>
<!-- Users. The search and refresh sit in a toolbar under the heading
rather than inside it: a control inside <summary> toggles the card
when clicked. -->
<details class="card" open>
<summary class="card-header"><h3><i class="fas fa-users"></i> Users</h3></summary>
<div class="admin-toolbar">
<input id="admin-users-search" type="search" placeholder="Search name or email" style="flex:1;max-width:260px;font-size:13px;padding:5px 10px;border:1px solid var(--g300);border-radius:6px;" aria-label="Search users">
<button id="btn-refresh-users" class="btn-sm btn-ghost" type="button"><i class="fas fa-rotate"></i> Refresh</button>
</div>
<div style="overflow:auto;max-height:420px;">
<table class="admin-table" id="admin-users-table">
<thead style="position:sticky;top:0;background:white;z-index:1;">
<tr>
<th>Name / Email</th>
<th>Role</th>
<th>Status</th>
<th>Joined</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="admin-users-body">
<tr><td colspan="5" style="text-align:center;color:var(--g400);padding:20px;">Loading...</td></tr>
</tbody>
</table>
</div>
</details>
<!-- ── OIDC / SSO Configuration ──────────────────────────────── -->
<details class="card" open>
<summary class="card-header"><h3><i class="fas fa-shield-halved"></i> Single Sign-On (OIDC)</h3></summary>
<div class="admin-card-body">
<p class="admin-note">Configure OpenID Connect for SSO with Azure AD, Okta, Keycloak, PocketID, Google, etc.<br>Callback URL: <code style="font-size:11px;background:var(--g100);padding:2px 6px;border-radius:4px;" id="oidc-callback-url"></code></p>
<div class="admin-row">
<label for="oidc-enabled" class="admin-row-label">Enable SSO:</label>
<select id="oidc-enabled" class="admin-control">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
</div>
<div class="admin-row">
<label for="oidc-issuer" class="admin-row-label">Issuer URL:</label>
<input type="url" id="oidc-issuer" placeholder="https://id.example.com" class="admin-control">
</div>
<div class="admin-row">
<label for="oidc-client-id" class="admin-row-label">Client ID:</label>
<input type="text" id="oidc-client-id" placeholder="your-client-id" class="admin-control">
</div>
<div class="admin-row">
<label for="oidc-client-secret" class="admin-row-label">Client Secret:</label>
<input type="password" id="oidc-client-secret" placeholder="Leave blank to keep current" class="admin-control">
</div>
<div class="admin-row">
<label for="oidc-button-label" class="admin-row-label">Button Label:</label>
<input type="text" id="oidc-button-label" placeholder="Sign in with SSO" class="admin-control">
</div>
<div class="admin-row">
<label for="oidc-disable-local" class="admin-row-label">Disable local login:</label>
<select id="oidc-disable-local" class="admin-control">
<option value="false">No (both SSO and local login)</option>
<option value="true">Yes (SSO only)</option>
</select>
</div>
<div class="admin-save-row">
<button id="btn-save-oidc" class="btn-sm btn-primary" type="button"><i class="fas fa-floppy-disk"></i> Save OIDC Settings</button>
<span id="oidc-save-status" role="status" style="font-size:12px;color:var(--g500);"></span>
</div>
</div>
</details>
<!-- ── CMS: Email Templates ───────────────────────────────────── -->
<details class="card" open>
<summary class="card-header"><h3><i class="fas fa-envelope"></i> Email Templates</h3></summary>
<div class="admin-card-body" style="gap:16px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label for="cms-email-template" style="font-size:13px;font-weight:600;">Template:</label>
<select id="cms-email-template" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="verify">Email Verification</option>
<option value="reset">Password Reset</option>
</select>
</div>
<div>
<label for="cms-email-subject" class="admin-subhead" style="display:block;margin-bottom:4px;">Subject</label>
<input type="text" id="cms-email-subject" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Email subject...">
</div>
<div>
<label for="cms-email-body" class="admin-subhead" style="display:block;margin-bottom:4px;">Body (plain text — a button link will be appended automatically)</label>
<textarea id="cms-email-body" rows="4" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Email body text..."></textarea>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button id="btn-save-email" class="btn-sm btn-primary" type="button">Save Template</button>
<button id="btn-test-email" class="btn-sm btn-ghost" type="button"><i class="fas fa-paper-plane"></i> Send Test</button>
</div>
</div>
</details>
<!-- ── CMS: SMTP Email Server ─────────────────────────────────── -->
<details class="card" open>
<summary class="card-header">
<h3><i class="fas fa-server"></i> SMTP Email Server</h3>
<span id="smtp-source-badge" class="admin-badge">Loading...</span>
</summary>
<div class="admin-card-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label for="cms-smtp-host" class="admin-subhead" style="display:block;margin-bottom:4px;">SMTP Host</label>
<input type="text" id="cms-smtp-host" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="smtp.gmail.com">
</div>
<div>
<label for="cms-smtp-port" class="admin-subhead" style="display:block;margin-bottom:4px;">Port</label>
<input type="number" id="cms-smtp-port" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="587" value="587">
</div>
<div>
<label for="cms-smtp-user" class="admin-subhead" style="display:block;margin-bottom:4px;">Username / Email</label>
<input type="text" id="cms-smtp-user" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="user@example.com">
</div>
<div>
<label for="cms-smtp-pass" class="admin-subhead" style="display:block;margin-bottom:4px;">Password / App Password</label>
<input type="password" id="cms-smtp-pass" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Leave blank to keep current">
</div>
<div>
<label for="cms-smtp-from" class="admin-subhead" style="display:block;margin-bottom:4px;">From Address</label>
<input type="text" id="cms-smtp-from" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Pediatric Scribe &lt;noreply@example.com&gt;">
</div>
<div style="display:flex;align-items:flex-end;gap:8px;">
<label for="cms-smtp-secure" class="admin-subhead" style="display:block;">TLS/SSL (port 465):</label>
<select id="cms-smtp-secure" style="font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;">
<option value="false">STARTTLS (587)</option>
<option value="true">SSL/TLS (465)</option>
</select>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button id="btn-save-smtp" class="btn-sm btn-primary" type="button">Save SMTP</button>
<button id="btn-clear-smtp" class="btn-sm btn-ghost" type="button"><i class="fas fa-trash"></i> Clear DB (use env)</button>
</div>
<p class="admin-note"><i class="fas fa-info-circle"></i> Environment variables take precedence over DB settings. Storing password in DB is convenient but consider using env vars for production security.</p>
</div>
</details>
<!-- ═══ Models ═════════════════════════════════════════════════
Three cards, one workflow, read top to bottom. Discover & test finds
a model on the gateway and adds it; Roster is what has been added;
Availability says which of the roster each feature may use. These
used to be spread over three unrelated-looking cards in different
parts of the page, with a paragraph apologising for it. -->
<h2 class="admin-section-title">Models</h2>
<p class="admin-section-intro">
<strong>1. Discover &amp; test</strong> a model the gateway offers and add it &rarr;
<strong>2. Roster</strong> is everything added, with the default and on/off switches &rarr;
<strong>3. Availability</strong> is which roster models each feature may use.
</p>
<!-- ── Discover & test ───────────────────────────────────────── -->
<details class="card" open id="admin-discover-card">
<summary class="card-header">
<h3><i class="fas fa-magnifying-glass"></i> Discover &amp; test</h3>
<span class="card-header-note">Search the gateway, try a model, add it to the roster</span>
</summary>
<div class="admin-card-body">
<!-- One search for every kind of model. The kind decides which
gateway list is asked and what the row buttons do: chat and
image models are added to a roster; speech, transcription and
transcription models are single defaults, so their button is Set. -->
<div class="admin-kind-switch" role="group" aria-label="Kind of model">
<button type="button" class="admin-discover-kind" id="admin-discover-kind-chat" data-kind="chat" aria-pressed="true"><i class="fas fa-comments"></i> Chat</button>
<button type="button" class="admin-discover-kind" id="admin-discover-kind-image" data-kind="image" aria-pressed="false"><i class="fas fa-image"></i> Image</button>
<button type="button" class="admin-discover-kind" id="admin-discover-kind-tts" data-kind="tts" aria-pressed="false"><i class="fas fa-volume-up"></i> Speech (TTS)</button>
<button type="button" class="admin-discover-kind" id="admin-discover-kind-stt" data-kind="stt" aria-pressed="false"><i class="fas fa-microphone"></i> Transcription (STT)</button>
</div>
<!-- Provider status for the chosen kind. -->
<div class="admin-kind-panel admin-kind-status" data-kind="chat">
<span id="admin-model-provider-badge" class="admin-badge">Loading...</span>
<span class="admin-note">Chat models write notes, answer in the Clinical Assistant and draft resources. <strong>+ Add</strong> puts one on the roster.</span>
</div>
<div class="admin-kind-panel admin-kind-status" data-kind="image" hidden>
<span id="admin-image-provider-badge" class="admin-badge">Gateway</span>
<span id="admin-image-info" class="admin-note">Image models come from the LiteLLM gateway. <strong>+ Add</strong> puts one on the image roster; each workflow then picks its own under Availability.</span>
</div>
<div class="admin-kind-panel admin-kind-status" data-kind="tts" hidden>
<span id="admin-tts-provider-badge" class="admin-badge">Loading...</span>
<span id="admin-tts-info" class="admin-note">Loading...</span>
<span class="admin-note"><strong>Set</strong> makes a voice or model the default for Read Aloud.</span>
</div>
<div class="admin-kind-panel admin-kind-status" data-kind="stt" hidden>
<span id="admin-stt-provider-badge" class="admin-badge">Loading...</span>
<span id="admin-stt-info" class="admin-note">Loading...</span>
<span class="admin-note"><strong>Set</strong> makes a model the default for dictation.</span>
</div>
<div class="admin-search-row">
<input type="search" id="admin-discover-search" placeholder="Filter by name (e.g. gemini, gpt, llama)" aria-label="Filter models by name" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:200px;">
<button id="btn-discover" class="btn-sm btn-primary" type="button"><i class="fas fa-magnifying-glass"></i> Search gateway</button>
</div>
<div id="admin-discover-results" style="display:flex;flex-direction:column;gap:4px;max-height:400px;overflow-y:auto;"></div>
<p id="admin-discover-hint" class="admin-note">Press <strong>Search gateway</strong> to list what it offers for this kind. The box filters by name.</p>
<!-- Test. Chat and image accept an id the gateway does not
advertise, because a model can be routable without being
listed. -->
<div style="border-top:1px solid var(--g100);padding-top:12px;">
<div class="admin-subhead">Test</div>
<div class="admin-kind-panel" data-kind="chat">
<p class="admin-note" style="margin-bottom:8px;">Press Test on a model above or on the roster, or type an id the gateway does not advertise.</p>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<input type="text" id="admin-chat-test-model" placeholder="Model id to test" aria-label="Chat model id to test" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:180px;">
<button id="btn-test-chat-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
</div>
<div id="admin-chat-test-result" role="status" class="admin-note" style="margin-top:8px;"></div>
</div>
<div class="admin-kind-panel" data-kind="image" hidden>
<p class="admin-note" style="margin-bottom:8px;">Press Test on a model above, or type an id the gateway does not advertise. A picture is generated, so this costs one request.</p>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<input type="text" id="admin-image-test-model" placeholder="Model id to test" aria-label="Image model id to test" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:180px;">
<button id="btn-test-image-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
</div>
<div id="admin-image-test-result" role="status" class="admin-note" style="margin-top:8px;"></div>
</div>
<div class="admin-kind-panel" data-kind="tts" hidden>
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px;">
<label for="admin-tts-voice" style="font-size:13px;font-weight:600;min-width:80px;">Voice:</label>
<select id="admin-tts-voice" style="font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;flex:1;max-width:280px;"></select>
</div>
<textarea id="admin-tts-test-text" rows="2" aria-label="Text to synthesize" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Text to synthesize...">Hello, this is a text to speech test for the Pediatric AI Scribe.</textarea>
<div style="margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<button id="btn-test-tts" class="btn-sm btn-primary" type="button"><i class="fas fa-play"></i> Synthesize &amp; Play</button>
<audio id="admin-tts-audio" controls style="display:none;height:32px;flex:1;min-width:180px;"></audio>
</div>
<p id="admin-tts-result" role="status" class="admin-note" style="margin-top:6px;"></p>
</div>
<div class="admin-kind-panel" data-kind="stt" hidden>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<button id="btn-stt-record" class="btn-sm btn-primary" type="button"><i class="fas fa-microphone"></i> Start Recording</button>
<span id="admin-stt-recording-status" role="status" style="font-size:12px;color:var(--g500);"></span>
</div>
<div id="admin-stt-result" style="margin-top:10px;padding:10px;background:var(--g50);border-radius:6px;font-size:13px;color:var(--g700);display:none;">
<strong>Transcription:</strong> <span id="admin-stt-text"></span>
<div id="admin-stt-meta" style="font-size:11px;color:var(--g400);margin-top:4px;"></div>
</div>
</div>
</div>
</div>
</details>
<!-- ── Roster ────────────────────────────────────────────────── -->
<details class="card" open id="admin-roster-card">
<summary class="card-header">
<h3><i class="fas fa-layer-group"></i> Roster</h3>
<span class="card-header-note">What has been added. Availability draws from this list.</span>
</summary>
<div class="admin-card-body" style="gap:14px;">
<h4>Chat models</h4>
<div class="admin-row">
<label for="admin-default-model" class="admin-row-label">Default model</label>
<select id="admin-default-model" class="admin-control"></select>
<button id="btn-save-default-model" class="btn-sm btn-primary" type="button">Set default</button>
</div>
<p class="admin-note">Used wherever nobody chose a model explicitly. Only enabled models can be the default.</p>
<div>
<div class="admin-subhead">Built-in models (untick to hide one from users)</div>
<div id="admin-builtin-models" style="display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;padding-right:4px;">
<p style="color:var(--g400);font-size:13px;">Loading...</p>
</div>
</div>
<div id="admin-custom-models-list" style="display:flex;flex-direction:column;gap:4px;"></div>
<h4>Image models</h4>
<p class="admin-note">Added with <strong>+ Add</strong> under Discover &amp; test. Under Availability, tick one to offer it in chat and pick one per workflow.</p>
<div id="admin-image-roster" style="display:flex;flex-direction:column;gap:4px;">
<p class="admin-note">Loading...</p>
</div>
<p class="admin-note" style="border-top:1px solid var(--g100);padding-top:10px;">Speech and transcription models are single defaults rather than a roster: choose them with <strong>Set</strong> under Discover &amp; test.</p>
</div>
</details>
<!-- ── Availability ──────────────────────────────────────────── -->
<details class="card" open id="admin-availability-card">
<summary class="card-header">
<h3><i class="fas fa-list-check"></i> Availability</h3>
<span class="card-header-note">Which roster models each feature may use</span>
</summary>
<div class="admin-card-body">
<p class="admin-note">Only models on the roster appear here. If one is missing, add it under Discover &amp; test first. One <strong>Save availability</strong> at the bottom applies everything in this card.</p>
<h4>Clinical Assistant</h4>
<div class="admin-row">
<label for="assistant-chat-model" class="admin-row-label">Chat model</label>
<select id="assistant-chat-model" class="admin-control"></select>
<button id="btn-test-assistant-chat-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
</div>
<div id="assistant-chat-test-result" role="status" class="admin-note"></div>
<div class="admin-row" style="align-items:flex-start;">
<strong class="admin-row-label">Chat models users may pick</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<div id="assistant-allowed-chat-models" aria-label="Chat models available to the Clinical Assistant"></div>
<p class="admin-note">Leave every box empty to offer only the chat model above.</p>
</div>
</div>
<div class="admin-row" style="align-items:flex-start;">
<strong class="admin-row-label">Image models users may pick</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<div id="assistant-allowed-image-models" aria-label="Image models available to the Clinical Assistant"></div>
<p class="admin-note">Leave every box empty to offer only the workflow's own image model.</p>
</div>
</div>
<h4>Image generation, per workflow</h4>
<section id="workflow-image-settings" aria-label="Workflow image settings"></section>
<!-- The one setting that spends money on every generation without
a user asking for anything, so it stays Off until a model is
named here on purpose. -->
<h4>Slide review (My Resources)</h4>
<p class="admin-note">
The model that writes a deck never sees it, so it cannot tell that a slide
overflowed or that a long list wants two columns. With a reviewer chosen, each
generated deck is rendered, looked at, and its layout corrected once. Wording and
figures are never changed &mdash; a review that alters them is discarded.
</p>
<div class="admin-row">
<label for="mr-review-model" class="admin-row-label">Reviewer</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<select id="mr-review-model" class="admin-control" style="max-width:360px;"></select>
<p class="admin-note">
Needs to be able to see images. One image per slide is sent on every
presentation generated, so this costs money each time &mdash; a cheap capable
vision model is the right choice here, not the best one available.
</p>
</div>
</div>
<div id="assistant-config-error" class="assistant-config-error" role="alert" hidden
style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--red);border-radius:8px;background:rgba(220,38,38,0.06);">
<span style="font-size:13px;color:var(--g700);">
<i class="fas fa-triangle-exclamation" style="color:var(--red);"></i>
These settings could not be loaded, so saving is disabled. Nothing you have typed has been lost.
</span>
<button id="btn-retry-assistant-config" class="btn-sm btn-ghost btn-retry-assistant-config" type="button">Try again</button>
</div>
<div class="admin-save-row">
<button id="btn-save-availability" class="btn-sm btn-primary" type="button" disabled><i class="fas fa-floppy-disk"></i> Save availability</button>
<span id="assistant-availability-status" role="status" style="font-size:12px;color:var(--g500);"></span>
</div>
</div>
</details>
<!-- ═══ Assistant & prompts ════════════════════════════════════ -->
<h2 class="admin-section-title">Assistant &amp; prompts</h2>
<!-- ── Clinical Assistant ─────────────────────────────────────── -->
<details class="card" open id="admin-assistant-card">
<summary class="card-header">
<h3><i class="fas fa-brain"></i> Clinical Assistant</h3>
<span class="card-header-note">Retrieval, citations, translation, budget and starter prompts</span>
</summary>
<div class="admin-card-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="admin-row">
<label for="assistant-search-limit" class="admin-row-label">Retrieval result limit</label>
<input id="assistant-search-limit" type="number" min="3" max="20" value="8" class="admin-control">
</div>
<div class="admin-row">
<label for="assistant-context-chars" class="admin-row-label">Context per excerpt (characters)</label>
<input id="assistant-context-chars" type="number" min="300" max="4000" value="1400" class="admin-control">
</div>
</div>
<div class="admin-row">
<strong class="admin-row-label">Citations</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" id="assistant-show-sources" checked>
Show the Sources panel and numbered citations
</label>
<p class="admin-note">Display only. The AI prompt, retrieval and the stored answer are byte-for-byte identical either way, so turning this off cannot change what the model says. Citations are hidden, not removed &mdash; turning it back on restores them.</p>
</div>
</div>
<div class="admin-row">
<label for="assistant-translate-provider" class="admin-row-label">Translation provider</label>
<select id="assistant-translate-provider" class="admin-control">
<option value="libretranslate">Local LibreTranslate (patient text stays on this network)</option>
</select>
</div>
<div class="admin-row">
<label for="assistant-conversation-budget" class="admin-row-label">Conversation input budget</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<input id="assistant-conversation-budget" type="number" min="1000" max="1000000" class="admin-control" placeholder="e.g. 120000" aria-label="Conversation input budget (UTF-16 code units)">
<p id="assistant-conversation-budget-meta" role="status" class="admin-note">UTF-16 code units. Leave empty to use CLINICAL_ASSISTANT_CONVERSATION_CHARS.</p>
</div>
</div>
<div style="border:1px solid var(--g100);border-radius:8px;padding:10px;display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;">
<div>
<div style="font-size:13px;font-weight:600;color:var(--g700);">Starter prompt pool</div>
<div id="assistant-prompt-pool-status" role="status" class="admin-note" style="margin-top:3px;">Checking prompt pool...</div>
</div>
<button id="btn-regenerate-assistant-prompt-pool" class="btn-sm btn-ghost" type="button"><i class="fas fa-rotate"></i> Regenerate Pool</button>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;width:100%;">
<select id="assistant-prompt-pool-snapshots" class="admin-control" style="flex:1;min-width:220px;" aria-label="Prompt pool snapshots"><option value="">Loading snapshots...</option></select>
<button id="btn-restore-assistant-prompt-pool" class="btn-sm btn-ghost" type="button"><i class="fas fa-clock-rotate-left"></i> Restore Snapshot</button>
</div>
</div>
<!-- Same settings load as Availability, so the same failure is
shown here rather than a Save that is disabled for no stated
reason. -->
<div class="assistant-config-error" role="alert" hidden
style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--red);border-radius:8px;background:rgba(220,38,38,0.06);">
<span style="font-size:13px;color:var(--g700);">
<i class="fas fa-triangle-exclamation" style="color:var(--red);"></i>
These settings could not be loaded, so saving is disabled. Nothing you have typed has been lost.
</span>
<button class="btn-sm btn-ghost btn-retry-assistant-config" type="button">Try again</button>
</div>
<div class="admin-save-row">
<button id="btn-save-assistant-config" class="btn-sm btn-primary" type="button" disabled><i class="fas fa-floppy-disk"></i> Save &amp; Close</button>
<span id="assistant-admin-status" role="status" style="font-size:12px;color:var(--g500);"></span>
</div>
</div>
</details>
<!-- ── Prompts ───────────────────────────────────────────────── -->
<details class="card" open>
<summary class="card-header">
<h3><i class="fas fa-robot"></i> Prompts</h3>
<span class="card-header-note">Each prompt saves on its own</span>
</summary>
<div class="admin-card-body" style="gap:16px;">
<section aria-labelledby="scribe-prompts-heading">
<h4 id="scribe-prompts-heading">Scribe prompts</h4>
<div id="cms-scribe-prompts">Loading Scribe prompts...</div>
</section>
<section aria-labelledby="clinical-prompts-heading">
<h4 id="clinical-prompts-heading">Clinical Assistant prompts</h4>
<div id="cms-clinical-prompts">Loading clinical prompts...</div>
</section>
<section aria-labelledby="learning-prompts-heading">
<h4 id="learning-prompts-heading">Learning prompts</h4>
<div id="cms-learning-prompts">Loading Learning prompts...</div>
</section>
</div>
</details>
<!-- ── Web search ─────────────────────────────────────────────── -->
<details class="card" open>
<summary class="card-header">
<h3><i class="fas fa-globe"></i> Search Sources</h3>
<span class="card-header-note">Off by default</span>
</summary>
<div class="admin-card-body" style="gap:14px;">
<p style="margin:0;padding:8px 10px;background:rgba(224,142,69,0.10);border:1px solid var(--orange, #e08e45);border-radius:6px;font-size:12px;color:var(--g700);">
<strong>This sends text outside the building.</strong> When a resource author turns
it on, the model may send a short search query to the provider below. Only the query
leaves &mdash; never library excerpts, never the generated content, never anything
about the user &mdash; but a query written while drafting clinical material can still
carry clinical detail, and the provider keeps its own logs. SearXNG is the only
option here that you host yourself.
</p>
<div class="admin-row">
<label for="ws-enabled" class="admin-row-label">Enabled</label>
<div style="flex:1;min-width:0;">
<select id="ws-enabled" class="admin-control" style="max-width:200px;">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
</div>
</div>
<div class="admin-row">
<label for="ws-provider" class="admin-row-label">Provider</label>
<div style="flex:1;min-width:0;">
<select id="ws-provider" class="admin-control" style="max-width:260px;">
<option value="tavily">Tavily &mdash; built for AI, clean extracts</option>
<option value="serper">Serper &mdash; Google results</option>
<option value="brave">Brave Search API</option>
<option value="searxng">SearXNG &mdash; self-hosted, no third party</option>
</select>
</div>
</div>
<div class="admin-row">
<label for="ws-api-key" class="admin-row-label">API key</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<input id="ws-api-key" type="password" class="admin-control" placeholder="Leave blank to keep the current key" autocomplete="off">
<p class="admin-note">Not needed for SearXNG. Shown masked once saved.</p>
</div>
</div>
<div class="admin-row">
<label for="ws-base-url" class="admin-row-label">SearXNG URL</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<input id="ws-base-url" type="text" class="admin-control" placeholder="https://searxng.example.com">
<p class="admin-note">Only used when the provider is SearXNG. Its <code>/search?format=json</code> endpoint must be enabled.</p>
</div>
</div>
<div style="border-top:1px solid var(--g100);padding-top:12px;">
<div style="font-size:13px;font-weight:600;color:var(--g700);margin-bottom:8px;">
<i class="fas fa-book-medical"></i> PubMed
</div>
<p class="admin-note" style="margin-bottom:10px;">
A separate source from the web providers above, and a separate tick box for
authors. It returns structured records &mdash; title, journal, year, PMID &mdash;
so a reference can be exact rather than reconstructed from a page title. Queries
go to NCBI, not to a commercial provider.
</p>
<div class="admin-row">
<label for="pm-enabled" class="admin-row-label">Enabled</label>
<div style="flex:1;min-width:0;">
<select id="pm-enabled" class="admin-control" style="max-width:200px;">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
</div>
</div>
<div class="admin-row">
<label for="pm-api-key" class="admin-row-label">NCBI API key</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<input id="pm-api-key" type="password" class="admin-control" placeholder="Optional — leave blank to keep the current key" autocomplete="off">
<p class="admin-note">
Optional. PubMed works without one; a key raises the rate limit from 3
requests per second to 10, which matters for bulk work rather than for one
person writing a resource. Create one at
<a href="https://account.ncbi.nlm.nih.gov/settings/" target="_blank" rel="noopener noreferrer">account.ncbi.nlm.nih.gov/settings</a> &mdash; sign in, then <strong>API Key Management</strong> &rarr; <strong>Create an API Key</strong>.
</p>
</div>
</div>
<div class="admin-row">
<label for="pm-email" class="admin-row-label">Contact email</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<input id="pm-email" type="email" class="admin-control" placeholder="you@example.com">
<p class="admin-note">NCBI asks callers to identify themselves so they can contact you before blocking you. Optional but courteous.</p>
</div>
</div>
</div>
<div class="admin-save-row">
<button id="btn-save-websearch" class="btn-sm btn-primary" type="button"><i class="fas fa-floppy-disk"></i> Save search settings</button>
<button id="btn-test-websearch" class="btn-sm btn-ghost" type="button"><i class="fas fa-vial"></i> Test</button>
<span id="ws-status" role="status" style="font-size:12px;color:var(--g500);"></span>
</div>
</div>
</details>
<!-- ── Citation quality ───────────────────────────────────────── -->
<details class="card" open>
<summary class="card-header">
<h3><i class="fas fa-quote-right"></i> Citation Quality</h3>
<span id="admin-citation-summary" class="admin-badge">Loading...</span>
</summary>
<div class="admin-card-body">
<p class="admin-note">Answers where the assistant wrote a citation number that no returned source matched. Those markers are never turned into links, so a reader sees plain text — this is where they are recorded. Consistent entries usually mean retrieval is returning fewer sources than the answer assumes.</p>
<div><button id="btn-view-citation-audit" class="btn-sm btn-primary" type="button"><i class="fas fa-list"></i> View recent</button></div>
</div>
</details>
<!-- ═══ Site ═══════════════════════════════════════════════════ -->
<h2 class="admin-section-title">Site</h2>
<!-- ── CMS: Announcement Banner ──────────────────────────────── -->
<details class="card" open>
<summary class="card-header"><h3><i class="fas fa-bullhorn"></i> Announcement Banner</h3></summary>
<div class="admin-card-body">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label for="cms-ann-enabled" style="font-size:13px;font-weight:600;">Show banner:</label>
<select id="cms-ann-enabled" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="false">Hidden</option>
<option value="true">Visible</option>
</select>
<select id="cms-ann-type" aria-label="Banner style" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="info">Info (blue)</option>
<option value="warning">Warning (amber)</option>
<option value="error">Error (red)</option>
<option value="success">Success (green)</option>
</select>
</div>
<textarea id="cms-ann-text" rows="2" aria-label="Announcement message" style="font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;" placeholder="Announcement message..."></textarea>
<div>
<button id="btn-save-announcement" class="btn-sm btn-primary" type="button">Save &amp; Publish</button>
</div>
</div>
</details>
<!-- ── CMS: Feature Flags ─────────────────────────────────────── -->
<details class="card" open>
<summary class="card-header"><h3><i class="fas fa-toggle-on"></i> Feature Flags</h3></summary>
<div class="admin-card-body" style="gap:10px;" id="cms-feature-flags">
<div style="display:flex;align-items:center;gap:12px;">
<label for="cms-flag-read-aloud" style="font-size:13px;font-weight:600;min-width:160px;">Read Aloud (TTS):</label>
<select id="cms-flag-read-aloud" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="true">Enabled</option>
<option value="false">Disabled</option>
</select>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<label for="cms-flag-nextcloud" style="font-size:13px;font-weight:600;min-width:160px;">Nextcloud Integration:</label>
<select id="cms-flag-nextcloud" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="true">Enabled</option>
<option value="false">Disabled</option>
</select>
</div>
<div>
<button id="btn-save-flags" class="btn-sm btn-primary" type="button"><i class="fas fa-floppy-disk"></i> Save feature flags</button>
<span id="cms-flags-status" role="status" style="font-size:12px;color:var(--g500);margin-left:8px;"></span>
</div>
</div>
</details>
<!-- ── CMS: Saved Encounters (Admin View) ─────────────────────── -->
<details class="card" open>
<summary class="card-header">
<h3><i class="fas fa-database"></i> Saved Encounters (Site-wide)</h3>
<span class="card-header-note">Auto-deleted after <span id="admin-auto-delete-days">7</span> days</span>
</summary>
<div class="admin-card-body" style="gap:10px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label for="cms-auto-delete-days" style="font-size:13px;font-weight:600;">Auto-delete after:</label>
<select id="cms-auto-delete-days" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="1">1 day</option>
<option value="3">3 days</option>
<option value="7" selected>7 days</option>
<option value="14">14 days</option>
<option value="30">30 days</option>
</select>
<button id="btn-save-auto-delete" class="btn-sm btn-primary" type="button">Save</button>
</div>
</div>
</details>
<!-- ── CMS: Reset to Defaults ─────────────────────────────────── -->
<details class="card" open style="border:1px solid var(--red-light);">
<summary class="card-header"><h3 style="color:var(--red);"><i class="fas fa-triangle-exclamation"></i> Reset Settings</h3></summary>
<div class="admin-card-body" style="gap:10px;">
<p style="font-size:13px;color:var(--g600);margin:0;">Reset all announcement, feature flag, and email settings back to factory defaults. SMTP configuration and custom models are preserved.</p>
<div>
<button id="btn-reset-all-defaults" class="btn-sm" type="button" style="background:var(--red);color:white;border:none;border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer;"><i class="fas fa-rotate-left"></i> Reset All to Defaults</button>
</div>
</div>
</details>