feat(monitor/frontend): zustand stores (globe / ticker / prices / ui / audio + ticker tests)
This commit is contained in:
parent
f7f276df39
commit
7a97aac091
|
|
@ -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 }),
|
||||
}))
|
||||
|
|
@ -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 }),
|
||||
}))
|
||||
|
|
@ -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 } }
|
||||
}),
|
||||
}))
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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: [] }),
|
||||
}))
|
||||
|
|
@ -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 }),
|
||||
}))
|
||||
Loading…
Reference in New Issue