From fe90a86b30a1c9ccc8782787229c32245e6bc02c Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 10 Jun 2026 00:28:15 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20security=20alerts=20tab=20=E2=80=94=20U?= =?UTF-8?q?I=20view=20for=20anomaly=20detections=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New SecurityAlertsView (/alerts route) surfaces the detections table built in #10. Features: - All / Unacknowledged tab filter with live counts - Label dropdown (SECURITY_ANOMALY, SYSTEM_FAILURE, NETWORK_ANOMALY, etc.) - Score confidence bar per detection (colour-coded by threshold) - Acknowledge drawer: full log text, optional notes, in-place row dim on save - Scorer status badge + manual "Run scorer" button - Config warning when TURNSTONE_ANOMALY_MODEL is unset Dashboard: new "Unreviewed Alerts" stat card (red border when > 0) links to /alerts so alerts surface on the landing page without navigating away. Closes: https://git.opensourcesolarpunk.com/Circuit-Forge/turnstone/issues/11 --- web/src/App.vue | 1 + web/src/router/index.ts | 2 + web/src/views/DashboardView.vue | 22 +- web/src/views/SecurityAlertsView.vue | 458 +++++++++++++++++++++++++++ 4 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 web/src/views/SecurityAlertsView.vue diff --git a/web/src/App.vue b/web/src/App.vue index 914984d..f6a1b48 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -76,6 +76,7 @@ const navLinks = [ { to: '/search', label: 'Search' }, { to: '/diagnose', label: 'Diagnose' }, { to: '/incidents', label: 'Incidents' }, + { to: '/alerts', label: 'Alerts' }, { to: '/bundles', label: 'Bundles' }, { to: '/sources', label: 'Sources' }, { to: '/context', label: 'Context' }, diff --git a/web/src/router/index.ts b/web/src/router/index.ts index b2c7f97..e5bba57 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -8,6 +8,7 @@ import BundlesView from '@/views/BundlesView.vue' import SettingsView from '@/views/SettingsView.vue' import ContextView from '@/views/ContextView.vue' import BlocklistView from '@/views/BlocklistView.vue' +import SecurityAlertsView from '@/views/SecurityAlertsView.vue' export default createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -17,6 +18,7 @@ export default createRouter({ { path: '/search', component: LogSearchView }, { path: '/diagnose', component: DiagnoseView }, { path: '/incidents', component: IncidentsView }, + { path: '/alerts', component: SecurityAlertsView }, { path: '/bundles', component: BundlesView }, { path: '/sources', component: SourcesView }, { path: '/context', component: ContextView }, diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 98a9c4f..3d6a73a 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -52,6 +52,16 @@ {{ incidentsLoading ? '…' : activeIncidents }}

+ +

Unreviewed Alerts

+

+ {{ alertsLoading ? '…' : unackedAlerts }} +

+
@@ -201,6 +211,8 @@ const loading = ref(true) const incidents = ref([]) const incidentsLoading = ref(true) const watchSources = ref([]) +const unackedAlerts = ref(0) +const alertsLoading = ref(true) const activeIncidents = computed(() => incidents.value.filter(i => !i.ended_at).length @@ -217,7 +229,7 @@ const isStale = computed(() => { }) onMounted(async () => { - await Promise.all([loadStats(), loadIncidents(), loadWatchStatus()]) + await Promise.all([loadStats(), loadIncidents(), loadWatchStatus(), loadAlertCount()]) }) async function loadStats() { @@ -245,6 +257,14 @@ async function loadWatchStatus() { } catch { /* non-critical */ } } +async function loadAlertCount() { + try { + const res = await fetch(`${BASE}/turnstone/api/anomaly/detections?unacked_only=true&limit=1000`) + if (res.ok) unackedAlerts.value = (await res.json()).total ?? 0 + } catch { /* non-critical — scorer may be disabled */ } + finally { alertsLoading.value = false } +} + function healthDot(errors: number, total: number): string { if (errors === 0) return 'bg-green-500' const ratio = errors / Math.max(total, 1) diff --git a/web/src/views/SecurityAlertsView.vue b/web/src/views/SecurityAlertsView.vue new file mode 100644 index 0000000..7ac5361 --- /dev/null +++ b/web/src/views/SecurityAlertsView.vue @@ -0,0 +1,458 @@ + + + + +