GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat[frontend](alerts/live_operations): added new live operations vie… · utmstack/UTMStack@0d0d856

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • frontend/src
    • features
      • alerts/types
        • alert.types.ts
      • liveops
        • components
          • AlertBell.tsx
          • Burst.tsx
          • Counter.tsx
          • MachineNode.tsx
          • Packet.tsx
          • Pill.tsx
          • SceneDefs.tsx
          • Stage.tsx
          • Timeline.tsx
          • Wire.tsx
          • Zone.tsx
          • geometry.ts
          • index.tsx
          • scenes
            • BruteForce.tsx
            • CloudChange.tsx
            • Generic.tsx
            • Lateral.tsx
            • Malware.tsx
            • Outbound.tsx
            • Scan.tsx
            • SceneProps.tsx
            • Travel.tsx
            • WebAttack.tsx
            • index.ts
        • hooks
          • useGlow.ts
          • usePalette.ts
        • liveops.css
        • model.ts
        • pages
          • LiveOpsPage.tsx
    • shared/i18n/locales
      • de.json
      • en.json
      • es.json
      • fr.json
      • it.json
      • pt.json
      • ru.json
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎frontend/src/features/alerts/types/alert.types.ts‎

Copy file name to clipboardExpand all lines: frontend/src/features/alerts/types/alert.types.ts
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface Geolocation {
1212
city?: string
1313
latitude?: number
1414
longitude?: number
15+
asn?: number
16+
aso?: string
1517
}
1618

1719
/** "Side" object — adversary / target. Only the fields the UI uses. */
@@ -20,6 +22,8 @@ export interface Side {
2022
host?: string
2123
user?: string
2224
domain?: string
25+
url?: string
26+
port?: number
2327
geolocation?: Geolocation
2428
}
2529

Collapse file

‎frontend/src/features/liveops/components/AlertBell.tsx‎

Copy file name to clipboard
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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

‎frontend/src/features/liveops/components/Burst.tsx‎

Copy file name to clipboard
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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

‎frontend/src/features/liveops/components/Counter.tsx‎

Copy file name to clipboard
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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

‎frontend/src/features/liveops/components/MachineNode.tsx‎

Copy file name to clipboard
+127Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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

‎frontend/src/features/liveops/components/Packet.tsx‎

Copy file name to clipboard
+20Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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

‎frontend/src/features/liveops/components/Pill.tsx‎

Copy file name to clipboard
+20Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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

‎frontend/src/features/liveops/components/SceneDefs.tsx‎

Copy file name to clipboard
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
}

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.