The process table's columns fit the box they are in, the rate control saves, curves are timed on the viewer's clock and keep to the stride, the power icon is drawn, and a young history draws in from the right instead of being stretched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
87 lines
3.9 KiB
TypeScript
87 lines
3.9 KiB
TypeScript
// 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<number>).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<number>).detail)
|
|
window.addEventListener('hamlab:refresh', h)
|
|
return () => window.removeEventListener('hamlab:refresh', h)
|
|
}, [])
|
|
return Math.max(ms, floor)
|
|
}
|