Remove Needle; stop paging on nginx 4xx

Needle scored ~chance on the golden set and never gated alerts, so drop the
sidecar, proxy handlers, UI buttons, k8s container and build step. The labelled
eval sets move to cmd/triage-eval/data.

nginx 4xx (registry 401 challenges, /oauth2/auth, scanner 405s) is now dropped
in FilterActionable instead of only being discounted, so it cannot ride along
with a real 5xx. nginx 5xx still alerts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
matsandClaude Sonnet 5.5 committed 2026-11-01 11:40:53 +01:00
1 parent e046bd86d1
commit 51a8412ae1
22 files changed
+29 -1162

No files matched your search

@@ -16,7 +16,6 @@ import {
ScrollText,
ShieldBan,
Bot,
Sparkles,
Loader2,
X,
} from "lucide-react"
@@ -38,7 +37,6 @@ import { useSubRoute } from "../lib/routes"
import { CopyRef } from "./common/CopyRef"
import { IpButton } from "./common/IpButton"
import { IssueOccurrences } from "./IssueOccurrences"
import { NeedleTestModal } from "./NeedleTestModal"
import { FilterBar, type FilterChip } from "./FilterBar"
import { CRAWLER_CLASSES, UA_CLASSES, uaClassLabel } from "../lib/ua-class"
import type { Issue, IssueSample } from "../lib/types"
@@ -94,16 +92,6 @@ interface DetectorTestResult {
error?: string
}
interface NeedleResult {
action?: string
tool?: string
confidence?: number
reasoning?: string
latency_ms?: number
arguments?: Record<string, unknown>
error?: string
}
const statusTabs: { value: StatusFilter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "open", label: "Open" },
@@ -315,26 +303,8 @@ const IssueCard = ({
detectedIPByAddress: Map<string, DetectedIP>
}) => {
const [expanded, setExpanded] = useState(false)
const [needleResult, setNeedleResult] = useState<NeedleResult | null>(null)
const [needleLoading, setNeedleLoading] = useState(false)
const { sources, loading: sourcesLoading, error: sourcesError } = useIssueSources(issue.id, expanded)
const handleNeedleTriage = async () => {
setNeedleLoading(true)
try {
const response = await authFetch("/api/needle/classify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ issue_id: issue.id }),
})
setNeedleResult(await response.json())
} catch (err) {
setNeedleResult({ error: err instanceof Error ? err.message : String(err) })
} finally {
setNeedleLoading(false)
}
}
const meta = kindMeta[issue.kind] ?? { label: issue.kind, Icon: Bug }
const tone = issueSeverityTone(issue)
const latestSample = issue.samples[issue.samples.length - 1]
@@ -505,57 +475,6 @@ const IssueCard = ({
)}
</div>
<div className="space-y-2">
<button
type="button"
onClick={() => void handleNeedleTriage()}
disabled={needleLoading}
title="Classify this issue using local Needle 3 model"
className="inline-flex h-8 items-center gap-2 rounded-[9px] border border-ds-line-strong px-3 text-[12px] font-medium text-ds-text-2 hover:text-ds-text disabled:opacity-50"
>
{needleLoading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
) : (
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
)}
{needleLoading ? "Needle 3 triaging…" : "Needle 3 triage"}
</button>
{needleResult && (
<div className="space-y-2 rounded-[12px] border border-ds-line bg-ds-surface-2 p-3 text-[12px]">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-ds-line pb-2">
<span className="flex flex-wrap items-center gap-2">
<span className="text-[10.5px] font-bold uppercase tracking-wider text-ds-text-3">Needle 3 action</span>
<span className="rounded-[6px] border border-ds-line-strong px-2 py-0.5 font-[family-name:var(--font-data)] text-ds-text">
{needleResult.action || needleResult.tool || "Processed"}
</span>
</span>
{needleResult.confidence !== undefined && (
<span className="font-[family-name:var(--font-data)] text-[11px] text-ds-text-3">
confidence{" "}
<span className={needleResult.confidence >= 0.6 ? "text-ds-ok" : "text-ds-warn"}>
{(needleResult.confidence * 100).toFixed(0)}%
</span>
{needleResult.latency_ms ? ` (${needleResult.latency_ms}ms)` : ""}
</span>
)}
</div>
{needleResult.reasoning && <p className="italic text-ds-text-2">&ldquo;{needleResult.reasoning}&rdquo;</p>}
{needleResult.arguments && Object.keys(needleResult.arguments).length > 0 && (
<div className="flex flex-wrap gap-1.5 font-[family-name:var(--font-data)] text-[11px]">
{Object.entries(needleResult.arguments).map(([k, v]) => (
<span key={k} className="rounded-[6px] border border-ds-line bg-ds-surface px-2 py-0.5 text-ds-text-2">
<span className="text-ds-text-3">{k}:</span> {String(v)}
</span>
))}
</div>
)}
{needleResult.error && (
<p className="font-[family-name:var(--font-data)] text-[11px] text-ds-critical">{needleResult.error}</p>
)}
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-baseline justify-between gap-3">
<p className="text-[10.5px] font-bold uppercase tracking-widest text-ds-text-3">
@@ -750,7 +669,6 @@ export const IssuesVisualizer = ({
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [kindFilter, setKindFilter] = useState<KindFilter>("all")
const [groupBy, setGroupBy] = useState<GroupBy>("none")
const [needleModalOpen, setNeedleModalOpen] = useState(false)
const [detectorTest, setDetectorTest] = useState<DetectorTestResult | null>(null)
const [detectorTestLoading, setDetectorTestLoading] = useState(false)
const [excludeSpamIPs, setExcludeSpamIPs] = useState(false)
@@ -1119,15 +1037,6 @@ export const IssuesVisualizer = ({
<Bot className="h-3.5 w-3.5" aria-hidden="true" />
{crawlersHidden ? "Crawlers hidden" : "Hide crawlers"}
</button>
<button
type="button"
onClick={() => setNeedleModalOpen(true)}
title="Open interactive Needle 3 telemetry classifier"
className="inline-flex h-9 items-center gap-1.5 rounded-[9px] border border-ds-line-strong px-2.5 text-[12px] font-medium text-ds-text-2 hover:text-ds-text"
>
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
Test Needle 3
</button>
<button
type="button"
onClick={() => void runDetectorTest()}
@@ -1295,7 +1204,6 @@ export const IssuesVisualizer = ({
</aside>
</div>
<NeedleTestModal isOpen={needleModalOpen} onClose={() => setNeedleModalOpen(false)} />
</div>
)
}
-236
View File
@@ -1,236 +0,0 @@
import React, { useState } from "react"
import { X, Sparkles, Loader2, Cpu, CheckCircle2, AlertTriangle } from "lucide-react"
import { authFetch } from "../lib/auth"
interface NeedleTestModalProps {
isOpen: boolean
onClose: () => void
}
interface NeedleResponse {
success?: boolean
action?: string
tool?: string
confidence?: number
reasoning?: string
latency_ms?: number
arguments?: Record<string, unknown>
error?: string
}
const PRESETS = [
{
label: "Frontend RUM Crash",
service: "eventsourced-ai-web",
text: "User reported client browser crash on service eventsourced-ai-web: Script error on url https://event-agent.k6n.net/#session=angry-koala-1",
},
{
label: "Security CVE Probe",
service: "nginx",
text: "Alert security team: scanner metabase-cve-2026-72898-detect/1.0 probed target /api/session/properties with status code 503",
},
{
label: "Web Vital Metric",
service: "storefront-web",
text: "Store RUM metric on service storefront-web: record TTFB of 84 ms",
},
{
label: "Benign 404 Route",
service: "git.k6n.net",
text: "Client requested missing git resource: 404 on path /mats/go-cart-actor/blame/commit/81569b on host git.k6n.net",
},
{
label: "Database Pool Deadlock",
service: "checkout-service",
text: "Critical incident: Escalate severe backend database timeout on service checkout-service: deadlocked while acquiring connection pool",
},
]
export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClose }) => {
const [inputText, setInputText] = useState(PRESETS[0].text)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<NeedleResponse | null>(null)
if (!isOpen) return null
const handleClassify = async () => {
if (!inputText.trim()) return
setLoading(true)
setResult(null)
try {
const resp = await authFetch("/api/needle/classify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: inputText }),
})
const data = await resp.json()
setResult(data)
} catch (err) {
setResult({ error: err instanceof Error ? err.message : String(err) })
} finally {
setLoading(false)
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ds-bg/80 p-4 animate-fade-in">
<div className="relative flex flex-col w-full max-w-2xl rounded-[14px] border border-ds-ok/30 bg-ds-bg/95 p-6 max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between border-b border-ds-line pb-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-ds-ok/15 border border-ds-ok/30 text-ds-ok">
<Cpu className="h-5 w-5" />
</div>
<div>
<h2 className="text-base font-bold text-ds-text flex items-center gap-2">
Needle 3 AI Telemetry Classifier
<span className="rounded-full bg-ds-ok/15 border border-ds-ok/30 px-2 py-0.5 text-[10px] font-mono text-ds-ok">
Cactus-Compute
</span>
</h2>
<p className="text-xs text-ds-text-2">
On-device ~35MB foundation model executing real-time tool calling and extraction on local CPU.
</p>
</div>
</div>
<button
type="button"
onClick={onClose}
className="rounded-lg p-1.5 text-ds-text-2 hover:bg-ds-fill hover:text-ds-text transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Preset Selector */}
<div className="mt-4">
<label className="text-[11px] font-bold uppercase tracking-wider text-ds-text-2">
Quick Telemetry Presets (from ClickHouse)
</label>
<div className="flex flex-wrap gap-2 mt-2">
{PRESETS.map((p) => (
<button
key={p.label}
type="button"
onClick={() => {
setInputText(p.text)
setResult(null)
}}
className={`rounded-lg border px-2.5 py-1 text-[11px] font-medium transition-all ${
inputText === p.text
? "border-ds-ok bg-ds-ok/15 text-ds-ok"
: "border-ds-line bg-ds-surface-2 text-ds-text-2 hover:border-ds-line-strong hover:bg-ds-fill"
}`}
>
{p.label}
</button>
))}
</div>
</div>
{/* Input Textarea */}
<div className="mt-4">
<label className="text-[11px] font-bold uppercase tracking-wider text-ds-text-2">
Telemetry Event / Log / Trace Summary
</label>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
rows={3}
className="mt-1.5 w-full rounded-xl border border-ds-line bg-ds-surface-2 p-3 font-mono text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-ok/50 focus:outline-none focus:ring-1 focus:ring-ds-ok/40"
placeholder="Type or paste telemetry event..."
/>
</div>
{/* Classify Button */}
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={handleClassify}
disabled={loading || !inputText.trim()}
className="inline-flex items-center gap-2 rounded-xl bg-ds-surface-2 px-4 py-2 text-xs font-bold text-ds-on-accent transition-all hover:opacity-95 active:scale-95 disabled:opacity-50"
>
{loading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Classifying on CPU...
</>
) : (
<>
<Sparkles className="h-4 w-4" />
Classify with Needle 3
</>
)}
</button>
</div>
{/* Results Area */}
{result && (
<div className="mt-5 rounded-xl border border-ds-ok/30 bg-ds-ok/15 p-4 space-y-3">
<div className="flex items-center justify-between border-b border-ds-ok/20 pb-3">
<div className="flex items-center gap-2">
{result.error ? (
<AlertTriangle className="h-4 w-4 text-ds-critical" />
) : (
<CheckCircle2 className="h-4 w-4 text-ds-ok" />
)}
<span className="text-xs font-bold uppercase tracking-wider text-ds-text-2">
{result.error ? "Error" : "Triage Result"}
</span>
{result.action && (
<span className="rounded-lg bg-ds-ok/15 border border-ds-ok/40 px-2 py-0.5 font-mono text-xs font-bold text-ds-ok">
{result.action}
</span>
)}
</div>
{result.confidence !== undefined && (
<div className="flex items-center gap-3 text-xs font-mono">
<span className="text-ds-text-2">
Confidence:{" "}
<span className="font-bold text-ds-ok">
{(result.confidence * 100).toFixed(0)}%
</span>
</span>
{result.latency_ms !== undefined && (
<span className="text-ds-text-3">({result.latency_ms}ms)</span>
)}
</div>
)}
</div>
{result.reasoning && (
<p className="text-xs text-ds-text-2 italic">
&ldquo;{result.reasoning}&rdquo;
</p>
)}
{result.arguments && Object.keys(result.arguments).length > 0 && (
<div className="space-y-1.5 pt-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-ds-text-2">
Extracted Typed Arguments:
</span>
<div className="grid grid-cols-2 gap-2 font-mono text-xs">
{Object.entries(result.arguments).map(([key, value]) => (
<div
key={key}
className="rounded-lg border border-ds-line bg-ds-surface-2 p-2 text-ds-text break-all"
>
<span className="text-ds-text-3">{key}: </span>
<span className="text-ds-ok font-semibold">{String(value)}</span>
</div>
))}
</div>
</div>
)}
{result.error && (
<div className="rounded-lg bg-ds-critical-bg border border-ds-critical/20 p-3 text-xs text-ds-critical">
{result.error}
</div>
)}
</div>
)}
</div>
</div>
)
}