Repository navigation
fix multiple core issues and mount admin dashboard UI #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
| </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"> | ||||||||||||||||||||||||||||||||||
|
|
@@ -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" | ||||||||||||||||||||||||||||||||||
|
|
@@ -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" | ||||||||||||||||||||||||||||||||||
|
|
@@ -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" | ||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
|
|
@@ -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']; | ||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -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(); | ||||||||||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||
|
|
@@ -697,7 +713,6 @@ <h2 class="text-base font-bold">Recent Escalations</h2> | |||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let resizeTimer; | ||||||||||||||||||||||||||||||||||
| const ro = new ResizeObserver(() => { | ||||||||||||||||||||||||||||||||||
| clearTimeout(resizeTimer); | ||||||||||||||||||||||||||||||||||
|
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
The 🔧 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Consider deriving these from the intended fields or relabeling the cards to avoid misleading operators. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Unguarded If any item is missing 🛡️ 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 |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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.
≡ƒöæ Keyis mojibake — a 🔑 emoji that was mis-encoded (UTF-8 bytes read as CP1252). It will render as garbage in the header button.🔤 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents