Skip to content
Open
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
200 changes: 173 additions & 27 deletions dashboard/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
<div
class="hidden rounded-full border border-zinc-200 px-3 py-1 text-xs font-medium text-zinc-500 dark:border-white/10 dark:text-zinc-400 sm:block">
admin.local</div>
<button id="auth-btn" class="hidden rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-semibold hover:bg-zinc-100 dark:border-white/10 dark:bg-white/[0.04] dark:hover:bg-white/[0.08] sm:block">🔑 Key</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Corrupted key-icon glyph in the auth button label.

≡ƒöæ Key is mojibake — a 🔑 emoji that was mis-encoded (UTF-8 bytes read as CP1252). It will render as garbage in the header button.

🔤 Proposed fix
-                <button id="auth-btn" class="hidden rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-semibold hover:bg-zinc-100 dark:border-white/10 dark:bg-white/[0.04] dark:hover:bg-white/[0.08] sm:block">🔑 Key</button>
+                <button id="auth-btn" class="hidden rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-semibold hover:bg-zinc-100 dark:border-white/10 dark:bg-white/[0.04] dark:hover:bg-white/[0.08] sm:block">🔑 Key</button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button id="auth-btn" class="hidden rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-semibold hover:bg-zinc-100 dark:border-white/10 dark:bg-white/[0.04] dark:hover:bg-white/[0.08] sm:block">🔑 Key</button>
<button id="auth-btn" class="hidden rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-semibold hover:bg-zinc-100 dark:border-white/10 dark:bg-white/[0.04] dark:hover:bg-white/[0.08] sm:block">🔑 Key</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/templates/index.html` at line 144, The auth button label contains
mojibake instead of the intended key icon. Update the button text in the header
markup so the corrupted glyph in the auth button is replaced with the correct
key emoji/character, keeping the existing button structure and styling intact.
Use the auth button element as the target for the fix.

</div>
<div
class="flex min-w-0 items-center gap-3 rounded-full border border-zinc-200 bg-zinc-50 px-4 py-1.5 text-xs font-semibold text-zinc-600 shadow-sm dark:border-white/10 dark:bg-white/[0.04] dark:text-zinc-300">
Expand Down Expand Up @@ -231,7 +232,7 @@ <h1 class="mt-2 text-2xl font-bold tracking-tight sm:text-3xl">Safety operations
</div>
<div class="mt-5 flex items-end justify-between gap-4">
<div>
<p class="text-3xl font-extrabold tracking-tight">48,219</p>
<p id="stat-total-messages" class="text-3xl font-extrabold tracking-tight">48,219</p>
<p class="mt-2 text-xs text-zinc-500">vs. previous 30 days</p>
</div>
<canvas class="mini-chart h-14 w-28" data-values="18,22,26,31,35,41,48"
Expand All @@ -250,7 +251,7 @@ <h1 class="mt-2 text-2xl font-bold tracking-tight sm:text-3xl">Safety operations
</div>
<div class="mt-5 flex items-end justify-between gap-4">
<div>
<p class="text-3xl font-extrabold tracking-tight">23</p>
<p id="stat-escalations-today" class="text-3xl font-extrabold tracking-tight">23</p>
<p class="mt-2 text-xs text-zinc-500">operator review queue</p>
</div>
<canvas class="mini-chart h-14 w-28" data-values="8,9,12,11,16,18,23"
Expand All @@ -268,7 +269,7 @@ <h1 class="mt-2 text-2xl font-bold tracking-tight sm:text-3xl">Safety operations
</div>
<div class="mt-5 flex items-end justify-between gap-4">
<div>
<p class="text-3xl font-extrabold tracking-tight">86</p>
<p id="stat-active-sessions" class="text-3xl font-extrabold tracking-tight">86</p>
<p class="mt-2 text-xs text-zinc-500">currently monitored</p>
</div>
<canvas class="mini-chart h-14 w-28" data-values="52,58,61,66,72,79,86"
Expand Down Expand Up @@ -396,18 +397,19 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
<th class="px-5 py-3 font-semibold">Category</th>
<th class="px-5 py-3 font-semibold">Risk Score</th>
<th class="px-5 py-3 font-semibold">Triggers</th>
<th class="px-5 py-3 font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200 dark:divide-white/10">
<tbody id="escalations-tbody" class="divide-y divide-zinc-200 dark:divide-white/10">
<tr class="transition hover:bg-zinc-50 dark:hover:bg-white/[0.035]">
<td class="px-5 py-4 font-mono text-xs text-zinc-500">2026-06-26 14:32</td>
<td class="px-5 py-4 font-mono font-semibold">sess-a91f2</td>
<td class="px-5 py-4"><span
class="rounded-full bg-rose-500/10 px-2.5 py-1 text-xs font-bold text-rose-500">self_harm</span>
</td>
<td class="px-5 py-4 font-mono font-bold text-rose-500">0.92</td>
<td class="px-5 py-4 text-zinc-500">urgent phrasing, intent signal, repeat spike
</td>
<td class="px-5 py-4 text-zinc-500">urgent phrasing, intent signal, repeat spike</td>
<td class="px-5 py-4 text-right"><span class="text-xs text-zinc-400">Sample</span></td>
</tr>
<tr class="transition hover:bg-zinc-50 dark:hover:bg-white/[0.035]">
<td class="px-5 py-4 font-mono text-xs text-zinc-500">2026-06-26 13:58</td>
Expand All @@ -417,6 +419,7 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
</td>
<td class="px-5 py-4 font-mono font-bold text-amber-500">0.78</td>
<td class="px-5 py-4 text-zinc-500">threat language, named target</td>
<td class="px-5 py-4 text-right"><span class="text-xs text-zinc-400">Sample</span></td>
</tr>
<tr class="transition hover:bg-zinc-50 dark:hover:bg-white/[0.035]">
<td class="px-5 py-4 font-mono text-xs text-zinc-500">2026-06-26 13:21</td>
Expand All @@ -426,6 +429,7 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
</td>
<td class="px-5 py-4 font-mono font-bold text-violet-500">0.66</td>
<td class="px-5 py-4 text-zinc-500">harassment marker, toxicity score</td>
<td class="px-5 py-4 text-right"><span class="text-xs text-zinc-400">Sample</span></td>
</tr>
<tr class="transition hover:bg-zinc-50 dark:hover:bg-white/[0.035]">
<td class="px-5 py-4 font-mono text-xs text-zinc-500">2026-06-26 12:40</td>
Expand All @@ -435,6 +439,7 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
</td>
<td class="px-5 py-4 font-mono font-bold text-blue-500">0.51</td>
<td class="px-5 py-4 text-zinc-500">unsafe advice request</td>
<td class="px-5 py-4 text-right"><span class="text-xs text-zinc-400">Sample</span></td>
</tr>
<tr class="transition hover:bg-zinc-50 dark:hover:bg-white/[0.035]">
<td class="px-5 py-4 font-mono text-xs text-zinc-500">2026-06-26 11:55</td>
Expand All @@ -444,6 +449,7 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
</td>
<td class="px-5 py-4 font-mono font-bold text-emerald-500">0.28</td>
<td class="px-5 py-4 text-zinc-500">sentiment dip</td>
<td class="px-5 py-4 text-right"><span class="text-xs text-zinc-400">Sample</span></td>
</tr>
</tbody>
</table>
Expand Down Expand Up @@ -512,21 +518,28 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
ctx.stroke();
}

let donutItems = [9, 6, 5, 3];
let donutTotalVal = '23';
let barOpened = [9, 13, 15, 14, 16, 18];
let barResolved = [4, 5, 7, 6, 5, 6];
let riskValues = [0.38, 0.42, 0.49, 0.58, 0.55, 0.66, 0.73];
let baseValues = [0.22, 0.25, 0.29, 0.32, 0.35, 0.38, 0.40];
let riskLabels = ['Jun 20', 'Jun 21', 'Jun 22', 'Jun 23', 'Jun 24', 'Jun 25', 'Jun 26'];

function drawRisk(progress) {
const canvas = document.getElementById('risk-chart');
const { ctx, width, height } = prep(canvas);
const c = colors();
const pad = 34;
const risk = [0.38, 0.42, 0.49, 0.58, 0.55, 0.66, 0.73];
const base = [0.22, 0.25, 0.29, 0.32, 0.35, 0.38, 0.40];
const labels = ['Jun 20', 'Jun 21', 'Jun 22', 'Jun 23', 'Jun 24', 'Jun 25', 'Jun 26'];
const risk = riskValues;
const base = baseValues;
const labels = riskLabels;

ctx.clearRect(0, 0, width, height);
ctx.font = '12px Inter';
ctx.strokeStyle = c.grid;
ctx.fillStyle = c.text;


ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
const yLabels = ['1.0', '0.75', '0.50', '0.25', '0.0'];
Expand All @@ -541,7 +554,6 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
ctx.fillText(yLabels[i], pad - 8, y);
}


ctx.beginPath();
ctx.moveTo(pad, pad);
ctx.lineTo(pad, height - pad);
Expand All @@ -553,7 +565,7 @@ <h2 class="text-base font-bold">Recent Escalations</h2>

function points(data) {
return data.map((v, i) => ({
x: pad + i * ((width - pad * 2) / (data.length - 1)),
x: pad + i * ((width - pad * 2) / (data.length - 1 || 1)),
y: height - pad - (v * progress) * (height - pad * 2)
}));
}
Expand All @@ -566,8 +578,10 @@ <h2 class="text-base font-bold">Recent Escalations</h2>

ctx.beginPath();
riskPts.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
ctx.lineTo(width - pad, height - pad);
ctx.lineTo(pad, height - pad);
if (riskPts.length > 0) {
ctx.lineTo(riskPts[riskPts.length - 1].x, height - pad);
ctx.lineTo(riskPts[0].x, height - pad);
}
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
Expand All @@ -586,17 +600,19 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
ctx.lineJoin = 'round';
ctx.stroke();

labels.forEach((label, i) => ctx.fillText(label, riskPts[i].x - 18, height - 8));
labels.forEach((label, i) => {
if (riskPts[i]) ctx.fillText(label, riskPts[i].x, height - 8);
});
}

function drawBars(progress) {
const canvas = document.getElementById('bar-chart');
const { ctx, width, height } = prep(canvas);
const c = colors();
const labels = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00'];
const opened = [9, 13, 15, 14, 16, 18];
const resolved = [4, 5, 7, 6, 5, 6];
const max = 20;
const opened = barOpened;
const resolved = barResolved;
const max = Math.max(...opened, ...resolved, 20);
const pad = 36;
const gap = (width - pad * 2) / labels.length;

Expand Down Expand Up @@ -631,9 +647,9 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
function drawDonut(progress) {
const canvas = document.getElementById('donut-chart');
const { ctx, width, height } = prep(canvas);
const items = [9, 6, 5, 3];
const palette = ['#f43f5e', '#f59e0b', '#1d7cff', '#10b981'];
const total = items.reduce((a, b) => a + b, 0);
const items = donutItems;
const palette = ['#f43f5e', '#f59e0b', '#1d7cff', '#10b981', '#a855f7', '#6366f1'];
const total = items.reduce((a, b) => a + b, 0) || 1;
const cx = width / 2;
const cy = height / 2;
const radius = Math.min(width, height) * .38;
Expand All @@ -649,17 +665,17 @@ <h2 class="text-base font-bold">Recent Escalations</h2>

items.forEach((value, i) => {
const angle = (value / total) * Math.PI * 2 * progress;
ctx.strokeStyle = palette[i];
ctx.strokeStyle = palette[i % palette.length];
ctx.beginPath();
ctx.arc(cx, cy, radius, start, start + angle - .04);
ctx.arc(cx, cy, radius, start, start + angle - (angle > 0.05 ? 0.04 : 0));
ctx.stroke();
start += (value / total) * Math.PI * 2;
});

ctx.fillStyle = root.classList.contains('dark') ? '#f4f4f5' : '#18181b';
ctx.font = '700 28px Inter';
ctx.textAlign = 'center';
ctx.fillText('23', cx, cy + 2);
ctx.fillText(donutTotalVal, cx, cy + 2);
ctx.font = '12px Inter';
ctx.fillStyle = '#71717a';
ctx.fillText('Escalations', cx, cy + 24);
Expand Down Expand Up @@ -697,7 +713,6 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
});
}


let resizeTimer;
const ro = new ResizeObserver(() => {
clearTimeout(resizeTimer);
Expand All @@ -706,8 +721,139 @@ <h2 class="text-base font-bold">Recent Escalations</h2>
const mainEl = document.querySelector('main');
if (mainEl) ro.observe(mainEl);

animateCharts();
async function apiFetch(endpoint, options = {}) {
const key = localStorage.getItem('hp-admin-key') || '';
const headers = options.headers || {};
if (key) {
headers['Authorization'] = `Bearer ${key}`;
}
const res = await fetch(endpoint, {
...options,
headers
});
if (res.status === 401 || res.status === 403) {
const newKey = prompt("HumaneProxy Admin Key is required or invalid. Please enter the key:");
if (newKey) {
localStorage.setItem('hp-admin-key', newKey.trim());
location.reload();
}
}
if (res.status === 204) {
return null;
}
if (!res.ok) {
throw new Error(`API error: ${res.status}`);
}
return res.json();
}

function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

async function deleteSession(sessionId) {
if (!confirm(`Are you sure you want to delete all data for session "${sessionId}"?`)) {
return;
}
try {
await apiFetch(`/admin/sessions/${sessionId}`, {
method: 'DELETE'
});
alert('Session deleted successfully.');
loadData();
} catch (err) {
alert(`Error deleting session: ${err.message}`);
}
}

async function loadData() {
try {
const health = await apiFetch('/admin/health');
const stats = await apiFetch('/admin/stats');

if (stats.by_category) {
const keys = Object.keys(stats.by_category);
if (keys.length > 0) {
donutItems = keys.map(k => stats.by_category[k]);
donutTotalVal = String(stats.total_escalations || 0);
}
document.getElementById('stat-escalations-today').textContent = String(stats.total_escalations || 0);
}
Comment on lines +775 to +782

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

stat-escalations-today only updates when by_category is present.

The #stat-escalations-today write on Line 781 is nested inside the if (stats.by_category) block. When stats.by_category is empty or absent (e.g. no escalations yet), the card silently keeps the hardcoded 23 sample value even though a valid total_escalations is available. Move the metric update out of the by_category guard.

🔧 Proposed fix
-                if (stats.by_category) {
-                    const keys = Object.keys(stats.by_category);
-                    if (keys.length > 0) {
-                        donutItems = keys.map(k => stats.by_category[k]);
-                        donutTotalVal = String(stats.total_escalations || 0);
-                    }
-                    document.getElementById('stat-escalations-today').textContent = String(stats.total_escalations || 0);
-                }
+                if (stats.by_category) {
+                    const keys = Object.keys(stats.by_category);
+                    if (keys.length > 0) {
+                        donutItems = keys.map(k => stats.by_category[k]);
+                        donutTotalVal = String(stats.total_escalations || 0);
+                    }
+                }
+                document.getElementById('stat-escalations-today').textContent = String(stats.total_escalations || 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (stats.by_category) {
const keys = Object.keys(stats.by_category);
if (keys.length > 0) {
donutItems = keys.map(k => stats.by_category[k]);
donutTotalVal = String(stats.total_escalations || 0);
}
document.getElementById('stat-escalations-today').textContent = String(stats.total_escalations || 0);
}
if (stats.by_category) {
const keys = Object.keys(stats.by_category);
if (keys.length > 0) {
donutItems = keys.map(k => stats.by_category[k]);
donutTotalVal = String(stats.total_escalations || 0);
}
}
document.getElementById('stat-escalations-today').textContent = String(stats.total_escalations || 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/templates/index.html` around lines 775 - 782, The
`stat-escalations-today` update is incorrectly nested inside the
`stats.by_category` guard in the dashboard template, so the total escalations
card can stay stale when categories are missing. Update the logic around the
`stats.by_category` handling so
`document.getElementById('stat-escalations-today').textContent` is set whenever
`stats.total_escalations` is available, regardless of whether `by_category`
exists. Keep the donut chart population inside the category check, but move the
metric write outside it in the same script block.


const escData = await apiFetch('/admin/escalations?limit=100');
const items = escData.items || [];

document.getElementById('stat-total-messages').textContent = String(stats.total_escalations || items.length);

const uniqueSessions = new Set(items.map(i => i.session_id));
document.getElementById('stat-active-sessions').textContent = String(uniqueSessions.size);
Comment on lines +781 to +790

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Metric labels don't match the data being shown.

Two cards display values that don't correspond to their labels:

  • "Escalations Today" is set to stats.total_escalations (Line 781), which is the all‑time total, not today's count. The stats payload exposes by_day, which is a better source for a "today" figure.
  • "Total Messages" is set to stats.total_escalations || items.length (Line 787), i.e. an escalation count rather than a message count. The stats endpoint has no total-messages field.

Consider deriving these from the intended fields or relabeling the cards to avoid misleading operators.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/templates/index.html` around lines 781 - 790, The dashboard metrics
are showing values that do not match their labels, so update the stats rendering
logic in the template script that sets stat-escalations-today and
stat-total-messages. Use the correct source for the “Escalations Today” card by
deriving today’s count from the stats payload’s by_day data instead of
stats.total_escalations, and change the “Total Messages” card to either use an
actual message-count field if available or relabel the card to reflect
escalation totals. Keep the existing unique references around
apiFetch('/admin/escalations?limit=100'), stats, items, and the DOM ids so the
fix stays localized.


const tbody = document.getElementById('escalations-tbody');
if (items.length > 0) {
tbody.innerHTML = '';
items.forEach(item => {
const tr = document.createElement('tr');
tr.className = 'transition hover:bg-zinc-50 dark:hover:bg-white/[0.035]';

const d = new Date(item.timestamp * 1000);
const dateStr = d.toISOString().replace('T', ' ').substring(0, 16);

let badgeColor = 'bg-zinc-500/10 text-zinc-500';
if (item.category === 'self_harm') badgeColor = 'bg-rose-500/10 text-rose-500';
else if (item.category === 'criminal_intent') badgeColor = 'bg-amber-500/10 text-amber-500';
else if (item.category === 'violence') badgeColor = 'bg-rose-500/10 text-rose-500';

const triggersStr = (item.triggers || []).join(', ') || 'none';

tr.innerHTML = `
<td class="px-5 py-4 font-mono text-xs text-zinc-500">${dateStr}</td>
<td class="px-5 py-4 font-mono font-semibold">${escapeHtml(item.session_id)}</td>
<td class="px-5 py-4"><span class="rounded-full px-2.5 py-1 text-xs font-bold ${badgeColor}">${escapeHtml(item.category)}</span></td>
<td class="px-5 py-4 font-mono font-bold">${item.risk_score.toFixed(2)}</td>
<td class="px-5 py-4 text-zinc-500">${escapeHtml(triggersStr)}</td>
<td class="px-5 py-4 text-right">
<button data-session-id="${escapeHtml(item.session_id)}" class="delete-btn text-xs font-bold text-red-500 hover:text-red-700 bg-red-500/10 hover:bg-red-500/20 px-2 py-1 rounded">Delete</button>
</td>
`;
tbody.appendChild(tr);
});
Comment on lines +809 to +820

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unguarded risk_score.toFixed(2) can abort the whole table rebuild.

If any item is missing risk_score (null/undefined), item.risk_score.toFixed(2) on Line 813 throws mid-forEach, which propagates to the outer catch and leaves the table partially built (or empty) rather than rendering the remaining rows. item.timestamp on Line 799 has the same fragility (Invalid Date → toISOString() throws). Coerce/guard these fields before formatting.

🛡️ Proposed guard
-                        const d = new Date(item.timestamp * 1000);
-                        const dateStr = d.toISOString().replace('T', ' ').substring(0, 16);
+                        const d = new Date((item.timestamp || 0) * 1000);
+                        const dateStr = Number.isNaN(d.getTime())
+                            ? '—'
+                            : d.toISOString().replace('T', ' ').substring(0, 16);
-                            <td class="px-5 py-4 font-mono font-bold">${item.risk_score.toFixed(2)}</td>
+                            <td class="px-5 py-4 font-mono font-bold">${Number(item.risk_score ?? 0).toFixed(2)}</td>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/templates/index.html` around lines 809 - 820, Guard the row
rendering in the table rebuild so missing or invalid data does not abort the
entire loop: in the `tbody` build logic that sets `tr.innerHTML`, sanitize
`item.risk_score` before calling `toFixed(2)` and validate `item.timestamp`
before formatting it into `dateStr`, so `forEach` can continue rendering the
remaining rows even when one record is malformed. Update the formatting logic
around `item.risk_score`, `item.timestamp`, and the `tr.innerHTML` assignment to
use safe fallbacks instead of assuming both fields are always present and valid.

}

animateCharts();
} catch (err) {
console.error("Failed to load dashboard data:", err);
animateCharts();
}
}

const tbodyEl = document.getElementById('escalations-tbody');
if (tbodyEl) {
tbodyEl.addEventListener('click', (event) => {
const btn = event.target.closest('.delete-btn');
if (btn) {
const sessionId = btn.getAttribute('data-session-id');
if (sessionId) {
deleteSession(sessionId);
}
}
});
}

const authBtn = document.getElementById('auth-btn');
if (authBtn) {
authBtn.addEventListener('click', () => {
const newKey = prompt("Enter HUMANE_PROXY_ADMIN_KEY:", localStorage.getItem('hp-admin-key') || '');
if (newKey !== null) {
localStorage.setItem('hp-admin-key', newKey.trim());
location.reload();
}
});
}

// Initialise
loadData();
</script>
</body>

</html>
</html>
Loading
Loading