// Global refresh rate: one setting for every live view. Kept in localStorage for instant // use and mirrored to the hub's UI prefs so it follows the login. import { useEffect, useState } from 'react' export const REFRESH_OPTIONS = [250, 500, 1000, 2000, 5000] as const export const DEFAULT_REFRESH = 2000 // No floors: the rate means exactly what it says on every page, at the host's expense. export const LIST_FLOOR = 0 const KEY = 'hamlab.refresh' export function getRefreshMs(): number { const v = Number(localStorage.getItem(KEY)) return v >= 100 && v <= 60000 ? v : DEFAULT_REFRESH } export function setRefreshMs(ms: number) { if (!(ms >= 100 && ms <= 60000)) return // never accept a nonsense rate, wherever it came from localStorage.setItem(KEY, String(ms)) window.dispatchEvent(new CustomEvent('hamlab:refresh', { detail: ms })) } export function fmtMs(ms: number): string { return ms >= 1000 ? `${ms / 1000} s` : `${ms} ms` } // ---- how far back the graphs reach ---- // Unlike the refresh rate this is the machine's own setting, not a look: the hub keeps the // window and pushes it to every machine, so the graphs of any of them reach equally far back. // The value is cached here only so a page can draw before the hub has answered. export const HISTORY_OPTIONS = [60, 300, 600, 1800, 3600, 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600] as const export const DEFAULT_HISTORY_SEC = 600 export const HISTORY_POINTS = 300 // what a machine keeps per curve, whatever the window const HKEY = 'hamlab.historySec' export function getHistorySec(): number { const v = Number(localStorage.getItem(HKEY)) return v >= 60 && v <= 86400 ? v : DEFAULT_HISTORY_SEC } export function setHistorySec(sec: number) { if (!(sec >= 60 && sec <= 86400)) return localStorage.setItem(HKEY, String(sec)) window.dispatchEvent(new CustomEvent('hamlab:history', { detail: sec })) } // useHistorySec re-renders the caller when the window changes. export function useHistorySec(): number { const [sec, setSec] = useState(getHistorySec()) useEffect(() => { const h = (e: Event) => setSec((e as CustomEvent).detail) window.addEventListener('hamlab:history', h) return () => window.removeEventListener('hamlab:history', h) }, []) return sec } // How far apart the kept points are: the refresh rate until the window is wider than // HISTORY_POINTS of it, then the window split that many ways. Mirrors the server. export function strideMs(windowSec: number, refreshMs: number): number { return Math.max(refreshMs, Math.round((windowSec * 1000) / HISTORY_POINTS)) } // Is it time for a curve's next point? `last` is when the current one was started (this // browser's clock; the caller keeps it). Half a tick of slack, because ticks never land exactly // on time: without it a tick 5 ms early is "not yet" and every other point is skipped. The // returned start keeps to the stride's own beat rather than drifting with each late tick. export function nextPoint(now: number, last: number, stride: number, tickMs: number): { fresh: boolean; start: number } { if (last === 0 || now - last >= stride - tickMs / 2) return { fresh: true, start: last === 0 || now - last > 2 * stride ? now : last + stride } return { fresh: false, start: last } } export function fmtWindowSec(sec: number): string { if (sec >= 3600) { const h = sec / 3600; return `${Number.isInteger(h) ? h : h.toFixed(1)} hour${h === 1 ? '' : 's'}` } if (sec >= 60) { const m = sec / 60; return `${Number.isInteger(m) ? m : m.toFixed(1)} min` } return `${Math.round(sec)} s` } // useRefreshMs re-renders the caller when the global rate changes. export function useRefreshMs(floor = 0): number { const [ms, setMs] = useState(getRefreshMs()) useEffect(() => { const h = (e: Event) => setMs((e as CustomEvent).detail) window.addEventListener('hamlab:refresh', h) return () => window.removeEventListener('hamlab:refresh', h) }, []) return Math.max(ms, floor) }