pediatric-ai-scribe-v3/public/components/admin.html
Daniel c14fa25c3f
All checks were successful
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Docker Build / Build Docker image (push) Successful in 10s
Forgejo Docker Build / End-to-end (browser) (push) Successful in 20s
fix: the image settings card can save again; Learning Hub is out of the admin
"Not all of it was saved: Workflow not found" on every press of Save
availability. The card sent image settings for three workflows, and the
server has only had two since Learning Hub was removed — the DB
constraint allows clinical_assistant and my_resources and nothing else.
One rejection failed the whole save, so the two settings that were valid
looked unsaved as well.

The frontend was the only place that still believed in it. Also gone:
the learning_hub.image_behavior prompt, its Learning prompts section in
the admin — which held that one prompt and nothing else — and the
theme's card tints, which never applied.

While there: My Resources had a Model dropdown labelled "set per
request", permanently disabled and permanently empty. A control that can
never do anything reads as broken rather than as not applicable, so it
is now a sentence saying where the model is actually chosen. Its
fallbacks stay — those apply to whichever model the request picked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 01:39:15 +02:00

702 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>
</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>