feat(monitor/frontend): zustand stores (globe / ticker / prices / ui / audio + ticker tests)

This commit is contained in:
CarterPerez-dev 2026-05-02 05:36:18 -04:00
parent f7f276df39
commit 7a97aac091
6 changed files with 236 additions and 0 deletions

View File

@ -0,0 +1,22 @@
// ©AngelaMos | 2026
// audio.ts
import { create } from 'zustand'
interface AudioStore {
ctx: AudioContext | null
buffer: AudioBuffer | null
unlocked: boolean
setCtx: (ctx: AudioContext | null) => void
setBuffer: (b: AudioBuffer | null) => void
setUnlocked: (u: boolean) => void
}
export const useAudioStore = create<AudioStore>((set) => ({
ctx: null,
buffer: null,
unlocked: false,
setCtx: (ctx) => set({ ctx }),
setBuffer: (buffer) => set({ buffer }),
setUnlocked: (unlocked) => set({ unlocked }),
}))

View File

@ -0,0 +1,62 @@
// ©AngelaMos | 2026
// globeEvents.ts
import { create } from 'zustand'
export type GlobePointType =
| 'earthquake'
| 'ransomware'
| 'scan'
| 'iss'
| 'outage'
| 'hijack'
export interface GlobePoint {
id: string
type: GlobePointType
lat: number
lng: number
emittedAt: number
meta?: Record<string, unknown>
}
export interface GlobeRing {
id: string
lat: number
lng: number
emittedAt: number
ttlMs: number
}
interface GlobeStore {
points: GlobePoint[]
rings: GlobeRing[]
focusEvent: GlobePoint | null
pushPoint: (p: GlobePoint) => void
pushRing: (r: GlobeRing) => void
evict: (now: number) => void
focus: (p: GlobePoint | null) => void
}
const POINT_TTL_MS = 15 * 60 * 1000
const POINT_CAP = 500
export const useGlobeEvents = create<GlobeStore>((set) => ({
points: [],
rings: [],
focusEvent: null,
pushPoint: (p) =>
set((s) => ({
points: [...s.points.slice(-POINT_CAP + 1), p],
})),
pushRing: (r) =>
set((s) => ({
rings: [...s.rings, r],
})),
evict: (now) =>
set((s) => ({
points: s.points.filter((p) => now - p.emittedAt < POINT_TTL_MS),
rings: s.rings.filter((r) => now - r.emittedAt < r.ttlMs),
})),
focus: (p) => set({ focusEvent: p }),
}))

View File

@ -0,0 +1,51 @@
// ©AngelaMos | 2026
// prices.ts
import { create } from 'zustand'
export interface PriceTick {
symbol: string
price: string
ts: number
volume24h?: string
}
export interface MinuteBar {
symbol: string
minute: number
open: string
high: string
low: string
close: string
volume?: string
}
interface PricesStore {
latest: Record<string, PriceTick>
history: Record<string, MinuteBar[]>
pushTick: (t: PriceTick) => void
pushMinute: (b: MinuteBar) => void
}
const HISTORY_CAP = 60
export const usePrices = create<PricesStore>((set) => ({
latest: {},
history: {},
pushTick: (t) =>
set((s) => ({
latest: { ...s.latest, [t.symbol]: t },
})),
pushMinute: (b) =>
set((s) => {
const cur = s.history[b.symbol] ?? []
const idx = cur.findIndex((c) => c.minute === b.minute)
let next: MinuteBar[]
if (idx >= 0) {
next = [...cur.slice(0, idx), b, ...cur.slice(idx + 1)]
} else {
next = [...cur, b].slice(-HISTORY_CAP)
}
return { history: { ...s.history, [b.symbol]: next } }
}),
}))

View File

@ -0,0 +1,33 @@
// ©AngelaMos | 2026
// ticker.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useTicker } from './ticker'
describe('useTicker', () => {
beforeEach(() => {
useTicker.getState().clear()
})
it('caps at 50 entries oldest-out', () => {
for (let i = 0; i < 100; i++) {
useTicker.getState().push({
id: `e${i}`,
source: 'wiki',
headline: `headline ${i}`,
ts: Date.now() + i,
})
}
expect(useTicker.getState().items).toHaveLength(50)
// oldest survivors are e50..e99
expect(useTicker.getState().items[0].id).toBe('e50')
expect(useTicker.getState().items.at(-1)?.id).toBe('e99')
})
it('deduplicates by id', () => {
const item = { id: 'dup', source: 'gdelt', headline: 'h', ts: 0 }
useTicker.getState().push(item)
useTicker.getState().push(item)
expect(useTicker.getState().items).toHaveLength(1)
})
})

View File

@ -0,0 +1,30 @@
// ©AngelaMos | 2026
// ticker.ts
import { create } from 'zustand'
export interface TickerItem {
id: string
source: string
headline: string
ts: number
}
interface TickerStore {
items: TickerItem[]
push: (item: TickerItem) => void
clear: () => void
}
const TICKER_CAP = 50
export const useTicker = create<TickerStore>((set) => ({
items: [],
push: (item) =>
set((s) => {
if (s.items.some((i) => i.id === item.id)) return s
const next = [...s.items, item]
return { items: next.slice(-TICKER_CAP) }
}),
clear: () => set({ items: [] }),
}))

View File

@ -0,0 +1,38 @@
// ©AngelaMos | 2026
// ui.ts
import { create } from 'zustand'
export interface AlertState {
message: string
severity: 'info' | 'warn'
}
interface UIStore {
presentationMode: boolean
setPresentationMode: (on: boolean) => void
togglePresentationMode: () => void
currentAlert: AlertState | null
showAlert: (a: AlertState) => void
dismissAlert: () => void
aboutOpen: boolean
openAbout: () => void
closeAbout: () => void
}
export const useUIStore = create<UIStore>((set) => ({
presentationMode: false,
setPresentationMode: (on) => set({ presentationMode: on }),
togglePresentationMode: () =>
set((s) => ({ presentationMode: !s.presentationMode })),
currentAlert: null,
showAlert: (a) => set({ currentAlert: a }),
dismissAlert: () => set({ currentAlert: null }),
aboutOpen: false,
openAbout: () => set({ aboutOpen: true }),
closeAbout: () => set({ aboutOpen: false }),
}))