Something went wrong. Try again.
The IoT platform Cloudflare didn't build. Telemetry, realtime dashboards, and automations on Workers + D1 + R2 + Durable Objects — in your… nodrix.live
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132// Lightweight validator for the dashboard layout JSON. Allowed widget types// and variable extractors are driven by the shared manifest catalog — never// per-type branches in this file.
import { extractVariables, isChartSeriesExtractor } from '@nodrix/widgets-shared';import { ALLOWED_TYPES as MANIFEST_ALLOWED, manifestFor } from '@nodrix/widgets-shared';
export type { WidgetType } from '@nodrix/widgets-shared';
const ALLOWED_TYPES: ReadonlySet<string> = MANIFEST_ALLOWED;
export type WidgetInstance = { id: string; x: number; y: number; w: number; h: number; type: string; props: Record<string, unknown>;};
// Phone (<768px) position override for one widget — positions only, by id.export type MobilePlacement = { id: string; x: number; y: number; w: number; h: number };
export type Layout = { grid: { columns: number }; items: WidgetInstance[]; // Optional phone override, nested in the same layout JSON. Absent/null means // the phone layout is auto-derived from the desktop items. mobile?: { items: MobilePlacement[] } | null; // Public-view auto-refresh cadence in seconds (server-clamped). refresh?: number;};
// Public-view refresh bounds: floor matches the /state edge-cache TTL (polling// faster just returns the cached response); ceiling is 1h.const REFRESH_MIN = 5;const REFRESH_MAX = 3600;
export function validateLayout(input: unknown): { ok: true; value: Layout } | { ok: false; reason: string } { if (!isObject(input)) return { ok: false, reason: 'layout must be an object' }; const grid = input['grid']; if (!isObject(grid) || typeof grid['columns'] !== 'number') { return { ok: false, reason: 'layout.grid.columns must be a number' }; } const items = input['items']; if (!Array.isArray(items)) return { ok: false, reason: 'layout.items must be an array' };
for (const item of items) { if (!isObject(item)) return { ok: false, reason: 'each item must be an object' }; for (const f of ['id', 'type'] as const) { if (typeof item[f] !== 'string') return { ok: false, reason: `item.${f} must be a string` }; } for (const f of ['x', 'y', 'w', 'h'] as const) { if (typeof item[f] !== 'number') return { ok: false, reason: `item.${f} must be a number` }; } if (!ALLOWED_TYPES.has(item['type'] as string)) { return { ok: false, reason: `unknown widget type: ${item['type'] as string}` }; } if (!isObject(item['props'])) return { ok: false, reason: 'item.props must be an object' }; }
// Optional phone override (positions only); absent/null => auto-derive. let mobile: { items: MobilePlacement[] } | null | undefined; const m = input['mobile']; if (m === null) { mobile = null; } else if (m !== undefined) { if (!isObject(m) || !Array.isArray(m['items'])) { return { ok: false, reason: 'layout.mobile.items must be an array' }; } for (const it of m['items']) { if (!isObject(it)) return { ok: false, reason: 'each mobile item must be an object' }; if (typeof it['id'] !== 'string') return { ok: false, reason: 'mobile item.id must be a string' }; for (const f of ['x', 'y', 'w', 'h'] as const) { if (typeof it[f] !== 'number') return { ok: false, reason: `mobile item.${f} must be a number` }; } } mobile = { items: m['items'] as MobilePlacement[] }; }
// Optional public-view refresh cadence (seconds), clamped to safe bounds so a // crafted payload can't drive viewers to poll aggressively. let refresh: number | undefined; const r = input['refresh']; if (r !== undefined) { if (typeof r !== 'number' || !Number.isFinite(r)) { return { ok: false, reason: 'layout.refresh must be a number' }; } refresh = Math.min(Math.max(Math.round(r), REFRESH_MIN), REFRESH_MAX); }
return { ok: true, value: { grid: { columns: grid['columns'] }, items: items as WidgetInstance[], ...(mobile !== undefined ? { mobile } : {}), ...(refresh !== undefined ? { refresh } : {}), }, };}
function isObject(x: unknown): x is Record<string, unknown> { return typeof x === 'object' && x !== null && !Array.isArray(x);}
// Variable keys referenced by a layout. Driven by each widget's manifest// variableExtractor — no per-type branches in this file.export function variablesFromLayout(layout: Layout): string[] { const set = new Set<string>(); for (const item of layout.items) { const m = manifestFor(item.type as Parameters<typeof manifestFor>[0]); if (!m) continue; for (const v of extractVariables(m, item.props)) set.add(v); } return [...set];}
// Variable keys that need historical series on a dashboard. Only widgets whose// extractor declares isChartSeries consume series; others render from latest// state alone, so the snapshot doesn't ship history for them.export function chartVariablesFromLayout(layout: Layout): string[] { const set = new Set<string>(); for (const item of layout.items) { const m = manifestFor(item.type as Parameters<typeof manifestFor>[0]); if (!m || !isChartSeriesExtractor(m.runtime.variableExtractor)) continue; for (const v of extractVariables(m, item.props)) set.add(v); } return [...set];}