Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions smolrouter/static/vendor/api-key-copy.js
Original file line number Diff line number Diff line change
@@ -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};
});
21 changes: 13 additions & 8 deletions smolrouter/templates/client_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -148,7 +151,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
<th></th>
<th>Time</th>
<th>Service</th>
<th>Project Identity</th>
<th>Project</th>
<th>Path</th>
<th>Model</th>
<th>Size</th>
Expand Down Expand Up @@ -230,13 +233,15 @@ <h2>Recent Requests for {{ client_ip }}</h2>

const canonical = `${identityKind}:${identitySubject}`;
const displayName = log.identity_display_name;
const label = displayName ? `${canonical} (${displayName})` : canonical;

if (identityKind === 'facade_key') {
return `<a href="/projects/${encodeURIComponent(identitySubject)}" class="token-button secondary">${escapeHtml(label)}</a>`;
const label = displayName || identitySubject;
const secondaryId = displayName && displayName !== identitySubject
? `<span class="project-cell-secondary">${escapeHtml(identitySubject)}</span>`
: '';
return `<div class="project-cell"><a href="/projects/${encodeURIComponent(identitySubject)}" class="project-detail-link" onclick="event.stopPropagation()">${escapeHtml(label)}</a>${secondaryId}</div>`;
}

return escapeHtml(label);
return `<div class="project-cell"><span>${escapeHtml(canonical)}</span><span class="identity-cue">Identity</span></div>`;
}

// JSON formatting functions (same as main dashboard)
Expand Down Expand Up @@ -401,7 +406,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
${log.service_type || 'unknown'}
</span>
</td>
<td data-label="Project Identity">${renderProjectCell(log)}</td>
<td data-label="Project">${renderProjectCell(log)}</td>
<td data-label="Path">${log.path || '-'}</td>
<td data-label="Model">${formatModel(log.original_model, log.mapped_model, log.model_observation_status)}</td>
<td data-label="Size">${formatSize(log.request_size)}</td>
Expand Down Expand Up @@ -549,7 +554,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
${request.service_type || 'unknown'}
</span>
</td>
<td data-label="Project Identity">${renderProjectCell(request)}</td>
<td data-label="Project">${renderProjectCell(request)}</td>
<td data-label="Path">${request.path || '-'}</td>
<td data-label="Model">${formatModel(request.original_model, request.mapped_model, request.model_observation_status)}</td>
<td data-label="Size">${formatSize(request.request_size)}</td>
Expand Down Expand Up @@ -583,7 +588,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
${request.service_type || 'unknown'}
</span>
</td>
<td data-label="Project Identity">${renderProjectCell(request)}</td>
<td data-label="Project">${renderProjectCell(request)}</td>
<td data-label="Path">${request.path || '-'}</td>
<td data-label="Model">${formatModel(request.original_model, request.mapped_model, request.model_observation_status)}</td>
<td data-label="Size">${formatSize(request.request_size)}</td>
Expand Down
59 changes: 48 additions & 11 deletions smolrouter/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
color: #666;
}

.project-primary {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}

.token-button {
padding: 0;
border: none;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -242,7 +259,7 @@ <h2>Recent Requests</h2>
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"
>
</div>
Expand All @@ -251,7 +268,7 @@ <h2>Recent Requests</h2>
</form>

<div class="filter-help dashboard-filter-help">
Space-separated clauses are ANDed. Supported fields: <strong>host</strong>, <strong>provider</strong>, <strong>model</strong>, <strong>project</strong>, and <strong>identity</strong>. Bare words also search across the visible request fields.
Space-separated clauses are ANDed. Supported fields: <strong>host</strong>, <strong>provider</strong>, <strong>model</strong>, and <strong>project</strong>. Bare words also search across the visible request fields.
</div>

<div id="error-container"></div>
Expand All @@ -263,7 +280,7 @@ <h2>Recent Requests</h2>
<th></th>
<th>Time</th>
<th>Source</th>
<th>Project Identity</th>
<th>Project</th>
<th>Provider</th>
<th>Path</th>
<th>Model</th>
Expand Down Expand Up @@ -367,6 +384,10 @@ <h2>Recent Requests</h2>
return div.innerHTML;
}

function escapeAttribute(text) {
return escapeHtml(text).replaceAll('"', '&quot;').replaceAll("'", '&#39;');
}

function escapeClassName(text) {
return String(text || 'unknown').toLowerCase().replaceAll(/[^a-z0-9_-]+/g, '-');
}
Expand Down Expand Up @@ -430,19 +451,35 @@ <h2>Recent Requests</h2>
function renderProjectCell(log) {
const identityKind = log.identity_kind;
const identitySubject = log.identity_subject_id;
const identityDisplayName = log.identity_display_name;

if (!identityKind || !identitySubject) {
return '<span style="color:#999;">-</span>';
}

const canonical = `${identityKind}:${identitySubject}`;
const label = log.identity_display_name ? `${canonical} (${identityDisplayName})` : canonical;
if (identityKind !== 'facade_key') {
return `
<div class="cell-stack">
<span>${escapeHtml(canonical)}</span>
<span class="identity-cue">Identity</span>
</div>
`;
}

const displayName = log.identity_display_name;
const label = displayName || identitySubject;
const secondaryId = displayName && displayName !== identitySubject
? `<div class="cell-secondary">${renderFilterButton('project', identitySubject, identitySubject, 'secondary')}</div>`
: '';

return `
<div class="cell-stack">
<div>${renderFilterButton('project', identitySubject, canonical)}</div>
<div class="cell-secondary">${renderFilterButton('identity', canonical, label, 'secondary')}</div>
<div class="project-primary">
${renderFilterButton('project', identitySubject, label)}
<a class="icon-link project-detail-link" href="/projects/${encodeURIComponent(identitySubject)}" aria-label="Open project ${escapeAttribute(label)}" title="Open project ${escapeAttribute(label)}">
<span class="material-icons" aria-hidden="true" style="font-size: 0.95rem;">folder_open</span>
</a>
</div>
${secondaryId}
</div>
`;
}
Expand Down Expand Up @@ -527,7 +564,7 @@ <h2>Recent Requests</h2>
</td>
<td data-label="Time" class="timestamp">${formatTimestamp(log.timestamp)}</td>
<td data-label="Source">${renderSourceCell(log)}</td>
<td data-label="Project Identity">${renderProjectCell(log)}</td>
<td data-label="Project">${renderProjectCell(log)}</td>
<td data-label="Provider">${renderProviderCell(log)}</td>
<td data-label="Path">${escapeHtml(log.path || '-')}</td>
<td data-label="Model">${formatModel(log.original_model, log.mapped_model, log.model_observation_status)}</td>
Expand Down Expand Up @@ -730,8 +767,8 @@ <h2>Recent Requests</h2>
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();
}
}
Expand Down
Loading