From 7a97aac091ee282b94ca221d78288244e1316364 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sat, 2 May 2026 05:36:18 -0400 Subject: [PATCH] feat(monitor/frontend): zustand stores (globe / ticker / prices / ui / audio + ticker tests) --- .../frontend/src/stores/audio.ts | 22 +++++++ .../frontend/src/stores/globeEvents.ts | 62 +++++++++++++++++++ .../frontend/src/stores/prices.ts | 51 +++++++++++++++ .../frontend/src/stores/ticker.test.ts | 33 ++++++++++ .../frontend/src/stores/ticker.ts | 30 +++++++++ .../frontend/src/stores/ui.ts | 38 ++++++++++++ 6 files changed, 236 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/audio.ts create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/globeEvents.ts create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/prices.ts create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.test.ts create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.ts create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ui.ts diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/audio.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/audio.ts new file mode 100644 index 00000000..eb302b7d --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/audio.ts @@ -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((set) => ({ + ctx: null, + buffer: null, + unlocked: false, + setCtx: (ctx) => set({ ctx }), + setBuffer: (buffer) => set({ buffer }), + setUnlocked: (unlocked) => set({ unlocked }), +})) diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/globeEvents.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/globeEvents.ts new file mode 100644 index 00000000..807376a2 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/globeEvents.ts @@ -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 +} + +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((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 }), +})) diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/prices.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/prices.ts new file mode 100644 index 00000000..bfd95848 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/prices.ts @@ -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 + history: Record + pushTick: (t: PriceTick) => void + pushMinute: (b: MinuteBar) => void +} + +const HISTORY_CAP = 60 + +export const usePrices = create((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 } } + }), +})) diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.test.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.test.ts new file mode 100644 index 00000000..45dce76a --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.test.ts @@ -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) + }) +}) diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.ts new file mode 100644 index 00000000..f4bbbf3d --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ticker.ts @@ -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((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: [] }), +})) diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ui.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ui.ts new file mode 100644 index 00000000..4c72084c --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/ui.ts @@ -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((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 }), +}))