diff --git a/smolrouter/static/vendor/api-key-copy.js b/smolrouter/static/vendor/api-key-copy.js
new file mode 100644
index 0000000..4143053
--- /dev/null
+++ b/smolrouter/static/vendor/api-key-copy.js
@@ -0,0 +1,137 @@
+(function (root, factory) {
+ const api = factory();
+ if (typeof module === 'object' && module.exports) module.exports = api;
+ if (root) root.smolrouterApiKeyCopy = api;
+})(typeof window !== 'undefined' ? window : globalThis, function () {
+ 'use strict';
+
+ const COPY_LABEL = 'Copy API key';
+ const COPIED_LABEL = 'API key copied';
+
+ function legacyCopy(text, doc) {
+ const previousFocus = doc.activeElement;
+ const temporary = doc.createElement('textarea');
+ temporary.setAttribute('readonly', '');
+ temporary.setAttribute('aria-label', 'Temporary API key copy field');
+ temporary.setAttribute('tabindex', '-1');
+ temporary.style.position = 'fixed';
+ temporary.style.left = '-9999px';
+ temporary.style.opacity = '0';
+ temporary.value = text;
+ doc.body.appendChild(temporary);
+ try {
+ temporary.focus();
+ temporary.select();
+ temporary.setSelectionRange(0, temporary.value.length);
+ if (!doc.execCommand('copy')) throw new Error('Copy command was rejected');
+ } finally {
+ temporary.value = '';
+ temporary.remove();
+ if (previousFocus && previousFocus.isConnected !== false && typeof previousFocus.focus === 'function') {
+ previousFocus.focus();
+ }
+ }
+ }
+
+ async function copyText(text, options) {
+ const win = options && options.window ? options.window : window;
+ const doc = options && options.document ? options.document : document;
+ const clipboard = win.navigator && win.navigator.clipboard;
+ if (win.isSecureContext && clipboard && typeof clipboard.writeText === 'function') {
+ try {
+ await clipboard.writeText(text);
+ return;
+ } catch (_) {
+ // Firefox and hardened browsers can reject Clipboard API calls even when exposed.
+ }
+ }
+ legacyCopy(text, doc);
+ }
+
+ function createController(options) {
+ const win = options.window;
+ const input = options.input;
+ const panel = options.panel;
+ const copyButton = options.copyButton;
+ const copyIcon = options.copyIcon;
+ const dismissButton = options.dismissButton;
+ const status = options.status;
+ const performCopy = options.copyText || ((text) => copyText(text, {window: win, document: options.document}));
+ let secretCopied = false;
+ let copyResetTimer = null;
+
+ function clearResetTimer() {
+ if (copyResetTimer !== null) {
+ win.clearTimeout(copyResetTimer);
+ copyResetTimer = null;
+ }
+ }
+
+ function resetCopyFeedback() {
+ clearResetTimer();
+ copyIcon.textContent = 'content_copy';
+ copyButton.setAttribute('aria-label', COPY_LABEL);
+ copyButton.setAttribute('title', COPY_LABEL);
+ status.textContent = '';
+ status.classList.remove('success', 'warning');
+ }
+
+ function markCopied() {
+ secretCopied = true;
+ clearResetTimer();
+ copyIcon.textContent = 'check';
+ copyButton.setAttribute('aria-label', COPIED_LABEL);
+ copyButton.setAttribute('title', COPIED_LABEL);
+ status.textContent = 'API key copied.';
+ status.classList.remove('warning');
+ status.classList.add('success');
+ copyResetTimer = win.setTimeout(resetCopyFeedback, 2000);
+ }
+
+ function clearSecret() {
+ clearResetTimer();
+ input.value = '';
+ secretCopied = false;
+ resetCopyFeedback();
+ }
+
+ function showSecret(secret) {
+ clearSecret();
+ input.value = secret;
+ panel.hidden = false;
+ input.focus();
+ input.select();
+ }
+
+ async function handleCopy() {
+ try {
+ await performCopy(input.value);
+ markCopied();
+ } catch (_) {
+ resetCopyFeedback();
+ status.textContent = 'Automatic copy is unavailable. The key is selected; press Ctrl+C or Command+C to copy it.';
+ status.classList.add('warning');
+ input.focus();
+ input.select();
+ }
+ }
+
+ function handleDismiss() {
+ if (input.value && !secretCopied && !win.confirm('This key will not be shown again. Dismiss without copying?')) return;
+ clearSecret();
+ panel.hidden = true;
+ win.location.reload();
+ }
+
+ copyButton.addEventListener('click', handleCopy);
+ input.addEventListener('copy', () => {
+ if (input.value && input.selectionStart === 0 && input.selectionEnd === input.value.length) markCopied();
+ });
+ dismissButton.addEventListener('click', handleDismiss);
+ win.addEventListener('pagehide', clearSecret);
+
+ return {showSecret: showSecret, clearSecret: clearSecret};
+ }
+
+ return {copyText: copyText, createController: createController};
+});
diff --git a/smolrouter/templates/client_dashboard.html b/smolrouter/templates/client_dashboard.html
index 3b14bfb..bb8126b 100644
--- a/smolrouter/templates/client_dashboard.html
+++ b/smolrouter/templates/client_dashboard.html
@@ -58,6 +58,9 @@
text-decoration: underline;
}
+ .project-cell { display:flex; flex-direction:column; gap:.2rem; }
+ .project-cell-secondary, .identity-cue { color:#666; font-size:.8rem; }
+
@media (max-width: 768px) {
.stats {
grid-template-columns: repeat(2, 1fr);
@@ -148,7 +151,7 @@
Recent Requests for {{ client_ip }}
|
Time |
Service |
- Project Identity |
+ Project |
Path |
Model |
Size |
@@ -230,13 +233,15 @@ Recent Requests for {{ client_ip }}
const canonical = `${identityKind}:${identitySubject}`;
const displayName = log.identity_display_name;
- const label = displayName ? `${canonical} (${displayName})` : canonical;
-
if (identityKind === 'facade_key') {
- return `${escapeHtml(label)}`;
+ const label = displayName || identitySubject;
+ const secondaryId = displayName && displayName !== identitySubject
+ ? `${escapeHtml(identitySubject)}`
+ : '';
+ return ``;
}
- return escapeHtml(label);
+ return `${escapeHtml(canonical)}Identity
`;
}
// JSON formatting functions (same as main dashboard)
@@ -401,7 +406,7 @@ Recent Requests for {{ client_ip }}
${log.service_type || 'unknown'}
- ${renderProjectCell(log)} |
+ ${renderProjectCell(log)} |
${log.path || '-'} |
${formatModel(log.original_model, log.mapped_model, log.model_observation_status)} |
${formatSize(log.request_size)} |
@@ -549,7 +554,7 @@ Recent Requests for {{ client_ip }}
${request.service_type || 'unknown'}
- ${renderProjectCell(request)} |
+ ${renderProjectCell(request)} |
${request.path || '-'} |
${formatModel(request.original_model, request.mapped_model, request.model_observation_status)} |
${formatSize(request.request_size)} |
@@ -583,7 +588,7 @@ Recent Requests for {{ client_ip }}
${request.service_type || 'unknown'}
- ${renderProjectCell(request)} |
+ ${renderProjectCell(request)} |
${request.path || '-'} |
${formatModel(request.original_model, request.mapped_model, request.model_observation_status)} |
${formatSize(request.request_size)} |
diff --git a/smolrouter/templates/index.html b/smolrouter/templates/index.html
index f107518..6283233 100644
--- a/smolrouter/templates/index.html
+++ b/smolrouter/templates/index.html
@@ -94,6 +94,12 @@
color: #666;
}
+ .project-primary {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ }
+
.token-button {
padding: 0;
border: none;
@@ -125,6 +131,17 @@
color: #007bff;
}
+ .icon-link:focus-visible {
+ outline: 2px solid #2563eb;
+ outline-offset: 2px;
+ border-radius: 3px;
+ }
+
+ .identity-cue {
+ color: #6c757d;
+ font-size: 0.72rem;
+ }
+
.provider-primary {
font-weight: 500;
}
@@ -242,7 +259,7 @@ Recent Requests
class="filter-input"
id="dashboard-query"
type="search"
- placeholder="host:192.168.1.14 provider:google-gen-ai model:gemma3-12b project:project-a identity:facade_key:project-a"
+ placeholder="host:192.168.1.14 provider:google-gen-ai model:gemma3-12b project:project-a"
autocomplete="off"
>
@@ -251,7 +268,7 @@ Recent Requests
- Space-separated clauses are ANDed. Supported fields: host, provider, model, project, and identity. Bare words also search across the visible request fields.
+ Space-separated clauses are ANDed. Supported fields: host, provider, model, and project. Bare words also search across the visible request fields.
@@ -263,7 +280,7 @@ Recent Requests
|
Time |
Source |
- Project Identity |
+ Project |
Provider |
Path |
Model |
@@ -367,6 +384,10 @@ Recent Requests
return div.innerHTML;
}
+ function escapeAttribute(text) {
+ return escapeHtml(text).replaceAll('"', '"').replaceAll("'", ''');
+ }
+
function escapeClassName(text) {
return String(text || 'unknown').toLowerCase().replaceAll(/[^a-z0-9_-]+/g, '-');
}
@@ -430,19 +451,35 @@ Recent Requests
function renderProjectCell(log) {
const identityKind = log.identity_kind;
const identitySubject = log.identity_subject_id;
- const identityDisplayName = log.identity_display_name;
-
if (!identityKind || !identitySubject) {
return '-';
}
const canonical = `${identityKind}:${identitySubject}`;
- const label = log.identity_display_name ? `${canonical} (${identityDisplayName})` : canonical;
+ if (identityKind !== 'facade_key') {
+ return `
+
+ ${escapeHtml(canonical)}
+ Identity
+
+ `;
+ }
+
+ const displayName = log.identity_display_name;
+ const label = displayName || identitySubject;
+ const secondaryId = displayName && displayName !== identitySubject
+ ? `${renderFilterButton('project', identitySubject, identitySubject, 'secondary')}
`
+ : '';
return `
-
${renderFilterButton('project', identitySubject, canonical)}
-
${renderFilterButton('identity', canonical, label, 'secondary')}
+
+ ${renderFilterButton('project', identitySubject, label)}
+
+ folder_open
+
+
+ ${secondaryId}
`;
}
@@ -527,7 +564,7 @@ Recent Requests
${formatTimestamp(log.timestamp)} |
${renderSourceCell(log)} |
- ${renderProjectCell(log)} |
+ ${renderProjectCell(log)} |
${renderProviderCell(log)} |
${escapeHtml(log.path || '-')} |
${formatModel(log.original_model, log.mapped_model, log.model_observation_status)} |
@@ -730,8 +767,8 @@ Recent Requests
return;
}
- const clientLink = event.target.closest('.client-dashboard-link');
- if (clientLink) {
+ const navigationLink = event.target.closest('.client-dashboard-link, .project-detail-link');
+ if (navigationLink) {
event.stopPropagation();
}
}
diff --git a/smolrouter/templates/project_detail.html b/smolrouter/templates/project_detail.html
index 42f6cf3..12e32d7 100644
--- a/smolrouter/templates/project_detail.html
+++ b/smolrouter/templates/project_detail.html
@@ -105,8 +105,19 @@
.management-panel { background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1rem 1.5rem; margin-bottom:1rem; }
.management-row { display:flex; gap:.6rem; align-items:center; flex-wrap:wrap; }
.management-message { color:#b91c1c; min-height:1.2rem; margin-top:.5rem; }
- .secret-panel { display:none; background:#fffbeb; border:1px solid #f59e0b; border-radius:8px; padding:1rem; margin-top:.75rem; }
+ .secret-panel { position:relative; background:#fffbeb; border:1px solid #f59e0b; border-radius:8px; padding:1rem; margin-top:.75rem; }
+ .secret-panel[hidden] { display:none; }
+ .secret-title { display:block; padding-right:2.5rem; margin-bottom:.5rem; }
+ .secret-row { display:flex; gap:.4rem; align-items:stretch; }
+ .secret-value { flex:1; min-width:0; border:1px solid #d1d5db; border-radius:6px; background:#fff; padding:.55rem .65rem; }
.secret-value, .fingerprint { font-family:"Monaco","Menlo",monospace; overflow-wrap:anywhere; }
+ .icon-button { display:inline-flex; align-items:center; justify-content:center; min-width:2.6rem; padding:.45rem; }
+ .dismiss-secret { position:absolute; right:.55rem; top:.55rem; border:0; background:transparent; color:#475569; border-radius:5px; cursor:pointer; }
+ .dismiss-secret:hover { background:#fef3c7; color:#0f172a; }
+ .icon-button:focus-visible, .dismiss-secret:focus-visible, .secret-value:focus-visible { outline:3px solid #2563eb; outline-offset:2px; }
+ .copy-status { display:block; min-height:1.2rem; margin-top:.4rem; color:#475569; font-size:.9rem; }
+ .copy-status.success { color:#166534; }
+ .copy-status.warning { color:#b45309; }
.danger { background:#b91c1c !important; color:#fff !important; }
{% include 'responsive_shell.html' %}
@@ -120,7 +131,9 @@