GeezerVison is showing github.com — feat[frontend](alerts/live_operations): added new live operations vie… · utmstack/UTMStack@0d0d856
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: frontend/src/features/alerts/types/alert.types.ts +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,8 @@ export interface Geolocation { | 12 | 12 | city?: string | 13 | 13 | latitude?: number | 14 | 14 | longitude?: number | | 15 | + asn?: number | | 16 | + aso?: string | 15 | 17 | } | 16 | 18 |
| 17 | 19 | /** "Side" object — adversary / target. Only the fields the UI uses. */ | @@ -20,6 +22,8 @@ export interface Side { | 20 | 22 | host?: string | 21 | 23 | user?: string | 22 | 24 | domain?: string | | 25 | + url?: string | | 26 | + port?: number | 23 | 27 | geolocation?: Geolocation | 24 | 28 | } | 25 | 29 |
| |
Collapse file Copy file name to clipboard +30Lines changed: 30 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { Bell } from 'lucide-react' | | 2 | +import { usePalette } from '@/features/liveops/hooks/usePalette' | | 3 | +import { useGlow } from '@/features/liveops/hooks/useGlow' | | 4 | +import { backOut, easeOut, prog, type Pt } from './geometry' | | 5 | +import { Pill } from './Pill' | | 6 | + | | 7 | +// The bell that pops when the alert is raised. | | 8 | +export function AlertBell({ at, t, start, label = 'Alert raised', labelAbove = false }: { at: Pt; t: number; start: number; label?: string; labelAbove?: boolean }) { | | 9 | + const p = usePalette() | | 10 | + const glow = useGlow(true) | | 11 | + const u = prog(t, start, start + 550) | | 12 | + if (u <= 0) return null | | 13 | + const s = 0.55 + 0.45 * backOut(u) | | 14 | + const wobble = Math.sin((t - start) / 90) * Math.max(0, 1 - (t - start) / 1200) * 12 | | 15 | + const ripples = [0, 450].map((d) => prog(t, start + 150 + d, start + 1350 + d)).filter((x) => x > 0 && x < 1) | | 16 | + return ( | | 17 | + <g transform={`translate(${at.x},${at.y})`} opacity={Math.min(1, u * 2)}> | | 18 | + {ripples.map((x, i) => ( | | 19 | + <circle key={i} r={22 + easeOut(x) * 26} fill="none" stroke={p.alert} strokeWidth={2} strokeOpacity={0.6 * (1 - x)} /> | | 20 | + ))} | | 21 | + <g transform={`scale(${s})`}> | | 22 | + <circle r={22} fill={p.alertWash} stroke={p.alert} strokeWidth={2} filter={glow} /> | | 23 | + <g transform={`rotate(${wobble})`}> | | 24 | + <Bell x={-11} y={-11} width={22} height={22} color={p.alertText} strokeWidth={2.2} /> | | 25 | + </g> | | 26 | + </g> | | 27 | + <Pill x={0} y={labelAbove ? -38 : 38} text={label} tone={p.alert} small /> | | 28 | + </g> | | 29 | + ) | | 30 | +} |
Collapse file Copy file name to clipboard +14Lines changed: 14 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { easeOut, prog, type Pt } from './geometry' | | 2 | + | | 3 | +// An expanding ring where something hits. | | 4 | +export function Burst({ at, t, start, tone, size = 46, dur = 700 }: { at: Pt; t: number; start: number; tone: string; size?: number; dur?: number }) { | | 5 | + const u = prog(t, start, start + dur) | | 6 | + if (u <= 0 || u >= 1) return null | | 7 | + const k = easeOut(u) | | 8 | + return ( | | 9 | + <g> | | 10 | + <circle cx={at.x} cy={at.y} r={8 + size * 0.45 * k} fill={tone} fillOpacity={0.18 * (1 - u)} /> | | 11 | + <circle cx={at.x} cy={at.y} r={10 + size * k} fill="none" stroke={tone} strokeWidth={2.5 * (1 - u * 0.6)} strokeOpacity={1 - u} /> | | 12 | + </g> | | 13 | + ) | | 14 | +} |
Collapse file Copy file name to clipboard +19Lines changed: 19 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { usePalette } from '@/features/liveops/hooks/usePalette' | | 2 | + | | 3 | +const MONO = 'JetBrains Mono, monospace' | | 4 | + | | 5 | +export function Counter({ x, y, label, value, tone, opacity = 1 }: { x: number; y: number; label: string; value: string; tone: string; opacity?: number }) { | | 6 | + const p = usePalette() | | 7 | + if (opacity <= 0) return null | | 8 | + return ( | | 9 | + <g opacity={opacity} transform={`translate(${x},${y})`}> | | 10 | + <rect x={-92} y={-30} width={184} height={60} rx={12} fill={p.surfaceAlt} stroke={tone} strokeOpacity={0.5} /> | | 11 | + <text y={-9} textAnchor="middle" fill={p.textMuted} fontSize={10.5} letterSpacing="0.08em"> | | 12 | + {label.toUpperCase()} | | 13 | + </text> | | 14 | + <text y={18} textAnchor="middle" fill={tone} fontSize={24} fontWeight={700} fontFamily={MONO}> | | 15 | + {value} | | 16 | + </text> | | 17 | + </g> | | 18 | + ) | | 19 | +} |
Collapse file Copy file name to clipboard +127Lines changed: 127 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { Ban, Cloud, Globe, Laptop, Monitor, Radar, Server, ShieldCheck, Skull, Terminal, User, Boxes, BrickWall, Network } from 'lucide-react' | | 2 | +import type { NodeKind, Party } from '@/features/liveops/model' | | 3 | +import { usePalette } from '@/features/liveops/hooks/usePalette' | | 4 | +import { useGlow } from '@/features/liveops/hooks/useGlow' | | 5 | +import { backOut, ease, easeOut, prog, type Pt } from './geometry' | | 6 | + | | 7 | +const MONO = 'JetBrains Mono, monospace' | | 8 | + | | 9 | +function halo(bg: string, width = 4) { | | 10 | + return { stroke: bg, strokeWidth: width, strokeLinejoin: 'round' as const, paintOrder: 'stroke' as const } | | 11 | +} | | 12 | + | | 13 | +function subLines(sub: string): string[] { | | 14 | + const clip = (x: string, n: number) => (x.length > n ? `${x.slice(0, n - 1)}…` : x) | | 15 | + if (sub.length <= 30 || !sub.includes(' · ')) return [clip(sub, 34)] | | 16 | + const [first, ...rest] = sub.split(' · ') | | 17 | + return [clip(first, 30), clip(rest.join(' · '), 30)] | | 18 | +} | | 19 | + | | 20 | +const ICON: Record<NodeKind, typeof Globe> = { | | 21 | + globe: Globe, | | 22 | + firewall: BrickWall, | | 23 | + server: Server, | | 24 | + dc: Network, | | 25 | + workstation: Monitor, | | 26 | + laptop: Laptop, | | 27 | + web: Server, | | 28 | + k8s: Boxes, | | 29 | + cloud: Cloud, | | 30 | + skull: Skull, | | 31 | + user: User, | | 32 | + sensor: Radar, | | 33 | +} | | 34 | + | | 35 | +export type NodeState = 'idle' | 'active' | 'targeted' | 'compromised' | 'blocked' | 'flagged' | | 36 | + | | 37 | +interface NodeProps { | | 38 | + party: Party | | 39 | + at: Pt | | 40 | + t: number | | 41 | + state?: NodeState | | 42 | + /** When the machine fades in (ms). */ | | 43 | + appear?: number | | 44 | + selected?: boolean | | 45 | + onSelect?: (p: Party) => void | | 46 | + r?: number | | 47 | +} | | 48 | + | | 49 | +interface NodeProps { | | 50 | + party: Party | | 51 | + at: Pt | | 52 | + t: number | | 53 | + state?: NodeState | | 54 | + /** When the machine fades in (ms). */ | | 55 | + appear?: number | | 56 | + selected?: boolean | | 57 | + onSelect?: (p: Party) => void | | 58 | + r?: number | | 59 | +} | | 60 | + | | 61 | +export function MachineNode({ party, at, t, state = 'idle', appear = 0, selected, onSelect, r = 36 }: NodeProps) { | | 62 | + const p = usePalette() | | 63 | + const glow = useGlow() | | 64 | + // An outside machine that has been blocked stays red but fades: it is cut off, not safe. | | 65 | + const cutOff = state === 'blocked' && (party.role === 'attacker' || party.role === 'external') | | 66 | + const tone = state === 'compromised' || state === 'flagged' ? p.alert : state === 'blocked' && !cutOff ? p.ok : p[party.role] | | 67 | + const Icon = ICON[party.kind] ?? Server | | 68 | + const grow = prog(t, appear, appear + 650) | | 69 | + const o = ease(Math.min(1, grow * 1.6)) | | 70 | + const pulsing = state === 'targeted' || state === 'compromised' || state === 'flagged' | | 71 | + // Two rings a half-beat apart, slowing as they spread, so the pulse reads as a steady signal. | | 72 | + const rings = pulsing ? [(t % 1600) / 1600, ((t + 800) % 1600) / 1600] : [] | | 73 | + const clickable = !!onSelect | | 74 | + const subs = party.sub ? subLines(party.sub) : [] | | 75 | + const userY = r + 36 + subs.length * 15 | | 76 | + return ( | | 77 | + <g | | 78 | + transform={`translate(${at.x},${at.y}) scale(${0.82 + 0.18 * backOut(grow)})`} | | 79 | + opacity={o * (cutOff ? 0.6 : 1)} | | 80 | + style={{ cursor: clickable ? 'pointer' : 'default' }} | | 81 | + onClick={clickable ? () => onSelect(party) : undefined} | | 82 | + data-party={party.id} | | 83 | + > | | 84 | + {rings.map((u, i) => ( | | 85 | + <circle key={i} r={r + 4 + easeOut(u) * 30} fill="none" stroke={tone} strokeOpacity={0.5 * (1 - u) * (1 - u)} strokeWidth={2} /> | | 86 | + ))} | | 87 | + {selected && <circle r={r + 9} fill="none" stroke={p.text} strokeOpacity={0.7} strokeDasharray="4 5" strokeWidth={1.5} />} | | 88 | + <circle r={r} fill={p.surface} stroke={tone} strokeWidth={2.2} filter={glow} /> | | 89 | + <circle r={r - 7} fill={tone} fillOpacity={0.09} /> | | 90 | + <Icon x={-14} y={-14} width={28} height={28} color={tone} strokeWidth={1.7} /> | | 91 | + {party.flag && ( | | 92 | + <text x={r - 6} y={-r + 10} fontSize={18} textAnchor="middle"> | | 93 | + {party.flag} | | 94 | + </text> | | 95 | + )} | | 96 | + {party.agentId && ( | | 97 | + <g transform={`translate(${r - 8},${r - 8})`}> | | 98 | + <circle r={10} fill={p.surfaceAlt} stroke={p.textMuted} strokeOpacity={0.6} /> | | 99 | + <Terminal x={-6} y={-6} width={12} height={12} color={p.textSoft} strokeWidth={2} /> | | 100 | + </g> | | 101 | + )} | | 102 | + {state === 'blocked' && ( | | 103 | + <g transform={`translate(${-r + 8},${-r + 8})`}> | | 104 | + <circle r={11} fill={cutOff ? p.alertWash : p.okWash} stroke={cutOff ? p.alert : p.ok} /> | | 105 | + {cutOff ? <Ban x={-7} y={-7} width={14} height={14} color={p.alert} strokeWidth={2.2} /> : <ShieldCheck x={-7} y={-7} width={14} height={14} color={p.ok} strokeWidth={2.2} />} | | 106 | + </g> | | 107 | + )} | | 108 | + <text y={r + 20} textAnchor="middle" fill={p.text} fontSize={13} fontWeight={600} {...halo(p.bg)}> | | 109 | + {party.name.length > 30 ? `${party.name.slice(0, 29)}…` : party.name} | | 110 | + </text> | | 111 | + {subs.map((line, i) => ( | | 112 | + <text key={i} y={r + 36 + i * 15} textAnchor="middle" fill={p.textMuted} fontSize={11} fontFamily={MONO} {...halo(p.bg)}> | | 113 | + {line} | | 114 | + </text> | | 115 | + ))} | | 116 | + {party.user && party.role === 'internal' && ( | | 117 | + <g transform={`translate(0,${userY})`}> | | 118 | + <rect x={-56} y={-11} width={112} height={20} rx={10} fill={p.surfaceAlt} stroke={p.line} /> | | 119 | + <User x={-50} y={-7} width={13} height={13} color={p.textMuted} /> | | 120 | + <text x={6} y={4} textAnchor="middle" fill={p.textSoft} fontSize={11}> | | 121 | + {party.user.length > 16 ? `${party.user.slice(0, 15)}…` : party.user} | | 122 | + </text> | | 123 | + </g> | | 124 | + )} | | 125 | + </g> | | 126 | + ) | | 127 | +} |
Collapse file Copy file name to clipboard +20Lines changed: 20 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { useGlow } from '@/features/liveops/hooks/useGlow' | | 2 | +import { along, type Pt } from './geometry' | | 3 | +import { Pill } from './Pill' | | 4 | + | | 5 | +export function Packet({ a, b, u, tone, bend = 0, r = 4.5, label }: { a: Pt; b: Pt; u: number; tone: string; bend?: number; r?: number; label?: string }) { | | 6 | + const glow = useGlow() | | 7 | + const p = along(a, b, u, bend) | | 8 | + const mid = along(a, b, Math.max(0, u - 0.03), bend) | | 9 | + const tail = along(a, b, Math.max(0, u - 0.07), bend) | | 10 | + // Packets fade in as they leave and out as they arrive, instead of blinking on and off. | | 11 | + const fade = Math.min(1, u / 0.06, (1 - u) / 0.06) | | 12 | + return ( | | 13 | + <g opacity={Math.max(0, fade)}> | | 14 | + <line x1={tail.x} y1={tail.y} x2={mid.x} y2={mid.y} stroke={tone} strokeOpacity={0.22} strokeWidth={r * 0.8} strokeLinecap="round" /> | | 15 | + <line x1={mid.x} y1={mid.y} x2={p.x} y2={p.y} stroke={tone} strokeOpacity={0.5} strokeWidth={r * 0.95} strokeLinecap="round" /> | | 16 | + <circle cx={p.x} cy={p.y} r={r} fill={tone} filter={glow} /> | | 17 | + {label && <Pill x={p.x} y={p.y - 18} text={label} tone={tone} small />} | | 18 | + </g> | | 19 | + ) | | 20 | +} |
Collapse file Copy file name to clipboard +20Lines changed: 20 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { usePalette } from '@/features/liveops/hooks/usePalette' | | 2 | + | | 3 | +const MONO = 'JetBrains Mono, monospace' | | 4 | + | | 5 | +export function Pill({ x, y, text, tone, small, mono, opacity = 1, anchor = 'middle' }: { x: number; y: number; text: string; tone: string; small?: boolean; mono?: boolean; opacity?: number; anchor?: 'middle' | 'start' | 'end' }) { | | 6 | + const p = usePalette() | | 7 | + if (opacity <= 0) return null | | 8 | + const fs = small ? 10.5 : 12 | | 9 | + const w = Math.max(28, text.length * (mono ? fs * 0.62 : fs * 0.56) + 18) | | 10 | + const h = small ? 19 : 24 | | 11 | + const left = anchor === 'middle' ? x - w / 2 : anchor === 'start' ? x : x - w | | 12 | + return ( | | 13 | + <g opacity={opacity} className="lo-pop"> | | 14 | + <rect x={left} y={y - h / 2} width={w} height={h} rx={h / 2} fill={p.surfaceAlt} stroke={tone} strokeOpacity={0.75} /> | | 15 | + <text x={left + w / 2} y={y + fs * 0.36} textAnchor="middle" fill={tone} fontSize={fs} fontWeight={600} fontFamily={mono ? MONO : undefined}> | | 16 | + {text} | | 17 | + </text> | | 18 | + </g> | | 19 | + ) | | 20 | +} |
Collapse file Copy file name to clipboard +21Lines changed: 21 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Filter definitions shared by every scene (referenced by id from the glow hook). | | 2 | +export function SceneDefs() { | | 3 | + return ( | | 4 | + <defs> | | 5 | + <filter id="lo-glow" x="-50%" y="-50%" width="200%" height="200%"> | | 6 | + <feGaussianBlur stdDeviation="4" result="b" /> | | 7 | + <feMerge> | | 8 | + <feMergeNode in="b" /> | | 9 | + <feMergeNode in="SourceGraphic" /> | | 10 | + </feMerge> | | 11 | + </filter> | | 12 | + <filter id="lo-glow-strong" x="-80%" y="-80%" width="260%" height="260%"> | | 13 | + <feGaussianBlur stdDeviation="8" result="b" /> | | 14 | + <feMerge> | | 15 | + <feMergeNode in="b" /> | | 16 | + <feMergeNode in="SourceGraphic" /> | | 17 | + </feMerge> | | 18 | + </filter> | | 19 | + </defs> | | 20 | + ) | | 21 | +} |
|