spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1'use client';23import { useState } from 'react';4import { Bar } from '@/components/ui/primitives';5import { adminFetch } from '@/lib/admin-fetch';6import { fmt } from '@/lib/format';7import { COMPONENT_LABEL } from '@/lib/pressure';8import type { AdminConfig, ComponentId } from '@/lib/types';9import { AdminPage, ErrorNote, Panel, Toast, btnCls, btnPrimary, inputCls, useAdmin } from './shared';1011export function Config() {12 const { data, err, reload } = useAdmin<AdminConfig>('/config');13 // Edits are kept separately and overlay the fetched config (no effect-driven copy).14 const [edits, setEdits] = useState<AdminConfig | null>(null);15 const cfg: AdminConfig | null = edits ?? data;16 const setCfg = (c: AdminConfig) => setEdits(c);17 const [msg, setMsg] = useState<string | null>(null);18 const [saveErr, setSaveErr] = useState<string | null>(null);19 const [busy, setBusy] = useState(false);2021 if (!cfg) return <AdminPage title="Scoring config">{err ? <ErrorNote err={err} /> : <p className="text-[12px] text-ink-3">Loading…</p>}</AdminPage>;2223 const sum = Object.values(cfg.pressure_weights).reduce((a, b) => a + Number(b || 0), 0);24 const valid = Math.abs(sum - 1) <= 0.001;25 const levelsOk = cfg.levels.every((l, i) => i === 0 || l.max > cfg.levels[i - 1]!.max) && cfg.levels.at(-1)?.max === 100;26 const dirty = JSON.stringify(cfg) !== JSON.stringify(data);2728 const save = async () => {29 setBusy(true);30 setSaveErr(null);31 try {32 await adminFetch('/config', { method: 'PUT', body: cfg });33 setMsg('Configuration stored — applied on the next engine cycle.');34 setEdits(null);35 reload();36 } catch (e) {37 setSaveErr(String(e));38 } finally {39 setBusy(false);40 setTimeout(() => setMsg(null), 4000);41 }42 };4344 const setEngine = (k: string, v: string) => setCfg({ ...cfg, engine: { ...cfg.engine, [k]: v === '' ? '' : Number.isNaN(Number(v)) ? v : Number(v) } });4546 return (47 <AdminPage48 title={`Scoring config · v${cfg.version}`}49 desc="Weights, levels and engine parameters (pressure.yaml). Stored in Postgres and hot-reloaded by the engine. Weights must sum to 1 ± 0.001."50 right={51 <div className="flex gap-2">52 <button type="button" className={btnCls} disabled={!dirty} onClick={() => setEdits(null)}>53 reset54 </button>55 <button type="button" className={btnPrimary} disabled={!valid || !levelsOk || !dirty || busy} onClick={() => void save()}>56 PUT config57 </button>58 </div>59 }60 >61 <ErrorNote err={err ?? saveErr} />62 <Toast msg={msg} />63 <div className="grid grid-cols-[minmax(0,1fr)] gap-3 lg:grid-cols-3">64 <Panel title="Component weights" right={<span className="num" style={{ color: valid ? 'var(--ok)' : 'var(--bad)' }}>Σ {fmt(sum, 3)} {valid ? '✓' : '≠ 1.000'}</span>}>65 <ul className="space-y-2">66 {Object.entries(cfg.pressure_weights).map(([id, w]) => (67 <li key={id} className="grid grid-cols-[110px_72px_minmax(0,1fr)] items-center gap-3 text-[12.5px]">68 <span className="text-ink">{COMPONENT_LABEL[id as ComponentId] ?? id}</span>69 <input type="number" step="0.01" min={0} max={1} value={w} onChange={(e) => setCfg({ ...cfg, pressure_weights: { ...cfg.pressure_weights, [id]: Number(e.target.value) } })} className={`${inputCls} num w-full`} aria-label={`${id} weight`} />70 <Bar value={Number(w) * 100} max={40} color="var(--accent)" />71 </li>72 ))}73 </ul>74 </Panel>75 <Panel title="Levels" right={<span style={{ color: levelsOk ? 'var(--ok)' : 'var(--bad)' }}>{levelsOk ? 'monotonic, ends at 100' : 'must increase and end at 100'}</span>}>76 <ul className="space-y-2">77 {cfg.levels.map((l, i) => (78 <li key={l.id} className="grid grid-cols-[80px_72px_minmax(0,1fr)] items-center gap-3 text-[12.5px]">79 <span className="text-ink" style={{ color: `var(--p-${l.id})` }}>80 {l.id}81 </span>82 <input type="number" min={0} max={100} value={l.max} onChange={(e) => setCfg({ ...cfg, levels: cfg.levels.map((x, j) => (j === i ? { ...x, max: Number(e.target.value) } : x)) })} className={`${inputCls} num w-full`} aria-label={`${l.id} max`} />83 <input value={l.label} onChange={(e) => setCfg({ ...cfg, levels: cfg.levels.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)) })} className={`${inputCls} w-full`} aria-label={`${l.id} label`} />84 </li>85 ))}86 </ul>87 </Panel>88 <Panel title="Engine parameters">89 <ul className="space-y-2">90 {Object.entries(cfg.engine).map(([k, v]) => (91 <li key={k} className="grid grid-cols-[minmax(0,1fr)_110px] items-center gap-3 text-[12px]">92 <span className="num truncate text-ink-2" title={k}>93 {k}94 </span>95 <input value={String(v)} onChange={(e) => setEngine(k, e.target.value)} className={`${inputCls} num w-full`} aria-label={k} />96 </li>97 ))}98 </ul>99 </Panel>100 </div>101 <details className="mt-3">102 <summary className="cursor-pointer text-[12px] text-ink-2">Full JSON (events, fronts, scheduler, component signal recipes)</summary>103 <pre className="num mt-2 max-h-[420px] overflow-auto rounded-[4px] border border-line bg-panel p-3 text-[11px] text-ink-2">{JSON.stringify(cfg, null, 2)}</pre>104 </details>105 </AdminPage>106 );107}108