diff --git a/app/globals.css b/app/globals.css index 3510997a..751acaff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -48,6 +48,16 @@ --color-vital-hr: var(--chart-vital-hr); --color-vital-hrv: var(--chart-vital-hrv); --color-vital-br: var(--chart-vital-br); + --color-calib-prev: var(--calib-prev); + --color-calib-saved: var(--calib-saved); + --color-calib-none: var(--calib-none); + --color-calib-better: var(--calib-better); + --color-actor-agent: var(--actor-agent); + --color-actor-agent-bg: var(--actor-agent-bg); + --color-actor-you: var(--actor-you); + --color-actor-you-bg: var(--actor-you-bg); + --color-actor-pod: var(--actor-pod); + --color-actor-pod-bg: var(--actor-pod-bg); --radius-tag: 3px; --radius-thumb: 6px; --radius-ctl: 8px; @@ -162,6 +172,18 @@ --chart-vital-hrv: #b294e6; --chart-vital-br: #6db3e3; + /* Biometric calibration: previous vs saved profile, and who acted. */ + --calib-prev: #3a3a40; + --calib-saved: #7fc0ea; + --calib-none: #2a2a2f; + --calib-better: #5fc27a; + --actor-agent: #b89cf0; + --actor-agent-bg: #231c33; + --actor-you: #7fc0ea; + --actor-you-bg: #12202c; + --actor-pod: #5fc27a; + --actor-pod-bg: #12261a; + --toggle-off-track: var(--sp-line-2); --toggle-off-knob: #6a6a70; --toggle-on-knob: var(--sp-ink-0); @@ -204,6 +226,16 @@ --chart-vital-hr: #c94f40; --chart-vital-hrv: #7a5cc4; --chart-vital-br: #2f7fb8; + --calib-prev: #c4c4c0; + --calib-saved: #2f7fb8; + --calib-none: #e2e2df; + --calib-better: #2e9d5b; + --actor-agent: #6a4fb8; + --actor-agent-bg: #ece6f8; + --actor-you: #2f6fb5; + --actor-you-bg: #e3eef9; + --actor-pod: #2e8a52; + --actor-pod-bg: #e2f3e8; --toggle-off-track: #dcdcd8; --toggle-off-knob: #ffffff; --toggle-on-knob: #ffffff; diff --git a/src/components/Settings/sections.ts b/src/components/Settings/sections.ts index 756bb53b..34d32bab 100644 --- a/src/components/Settings/sections.ts +++ b/src/components/Settings/sections.ts @@ -3,7 +3,7 @@ import { Activity, Cog, Download, Globe, Hand, HardDriveUpload, House, Moon, Pal export const SECTIONS = [ { id: 'appearance', label: 'Appearance', icon: Palette, description: 'How this device shows the app' }, { id: 'backup', label: 'Backup', icon: HardDriveUpload, description: 'Nightly archive to your own storage' }, - { id: 'biometric-calibration', label: 'Biometric calibration', icon: Activity, description: 'Profiles, processing state and evaluation history' }, + { id: 'biometric-calibration', label: 'Biometric calibration', icon: Activity, description: 'Profiles, processing state and evaluation' }, { id: 'device', label: 'Device', icon: Cog, description: 'Pod-wide hardware and maintenance' }, { id: 'gestures', label: 'Gestures', icon: Hand, description: 'What taps on the cover do' }, { id: 'homekit', label: 'HomeKit', icon: House, description: 'Apple Home bridge' }, diff --git a/src/components/biometrics/Calibration/BiometricCalibration.tsx b/src/components/biometrics/Calibration/BiometricCalibration.tsx index c32e7a9a..e9565f95 100644 --- a/src/components/biometrics/Calibration/BiometricCalibration.tsx +++ b/src/components/biometrics/Calibration/BiometricCalibration.tsx @@ -1,63 +1,89 @@ 'use client' import { useState } from 'react' +import { ChevronLeft } from 'lucide-react' import { Trans, useLingui } from '@lingui/react/macro' -import { Button, Card } from '@/src/components/ds' -import { SideTabs } from '@/src/components/Settings/SettingsLayout' -import { SleepStageRules } from '@/src/components/Settings/SleepStageRules' -import { CalibrationOverview } from './CalibrationOverview' +import { cn } from '@/lib/utils' import { CalibrationReferences } from './CalibrationReferences' import { ReferenceLibrary } from './ReferenceLibrary' import { ProfileLibrary } from './ProfileLibrary' -import { ProfileRecovery } from './ProfileRecovery' import { CalibrationBacktests } from './CalibrationBacktests' -import { CalibrationHistory } from './CalibrationHistory' import { ShadowRuns } from './ShadowRuns' +import { CalibrationPage } from './CalibrationPage' +import { SideTabs } from '@/src/components/Settings/SettingsLayout' type Side = 'left' | 'right' +type View = 'overview' | 'references' | 'profiles' | 'backtests' | 'observations' + +/** + * The calibration page is the overview. The workbench views behind it — + * reference imports, profile revisions, datasets and backtests, prospective + * runs — stay one click away for the cases the page links out to. + */ export function BiometricCalibration({ side, onSideChange }: { side: Side, onSideChange: (side: Side) => void }) { const { t } = useLingui() - const [view, setView] = useState<'overview' | 'references' | 'backtests' | 'profiles' | 'history' | 'observations'>('overview') + const [view, setView] = useState('overview') + const views: Array<{ id: View, label: string }> = [ + { id: 'overview', label: t`Overview` }, + { id: 'references', label: t`References` }, + { id: 'profiles', label: t`Profiles` }, + { id: 'backtests', label: t`Backtests` }, + { id: 'observations', label: t`Observations` }, + ] + const nav = ( + + ) + + if (view === 'overview') { + return ( +
+ + {nav} +
+ ) + } return ( -
- +
+ {nav} +
- - - {view === 'overview' +
+ {view === 'references' ? (
- - + +
) - : view === 'references' - ? ( -
- - -
- ) - : view === 'backtests' - ? - : view === 'observations' - ? - : view === 'history' - ? - : ( - <> - -

Direct sleep-stage changes are unvalidated manual settings. Saving requests a profile change; processor adoption is reported separately in Overview.

- - - )} + : view === 'backtests' + ? + : view === 'observations' + ? + : ( + <> + +

Profile revisions here are candidates. Applying one requests it like Save and request does; the overview shows when the processor acknowledges it.

+ + )}
) } diff --git a/src/components/biometrics/Calibration/CalibrationActivity.tsx b/src/components/biometrics/Calibration/CalibrationActivity.tsx new file mode 100644 index 00000000..d00858c5 --- /dev/null +++ b/src/components/biometrics/Calibration/CalibrationActivity.tsx @@ -0,0 +1,47 @@ +'use client' + +import { useState } from 'react' +import { useLingui } from '@lingui/react/macro' +import { Card } from '@/src/components/ds' +import type { Actor } from './calibration-model' +import { ActorBadge } from './CalibrationProfileCard' + +export interface ActivityRow { id: string, atMs: number, actor: Actor, what: string, detail: string | null, when?: string } + +const VISIBLE = 6 + +/** Who did what to this side's profile, newest first. */ +export function ActivityCard({ rows, timezone }: { rows: ActivityRow[], timezone: string }) { + const { t } = useLingui() + const [all, setAll] = useState(false) + const shown = all ? rows : rows.slice(0, VISIBLE) + return ( + +
+ {t`Activity`} + {timezone} +
+ {rows.length === 0 &&

{t`Nothing has happened on this side yet.`}

} +
    + {shown.map(row => ( +
  1. +
    + + + {row.actor.kind === 'agent' && {`${row.actor.name} `}} + {row.what} + + {row.when} +
    + {row.detail &&
    {row.detail}
    } +
  2. + ))} +
+ {rows.length > VISIBLE && ( + + )} +
+ ) +} diff --git a/src/components/biometrics/Calibration/CalibrationControls.tsx b/src/components/biometrics/Calibration/CalibrationControls.tsx new file mode 100644 index 00000000..a1a40433 --- /dev/null +++ b/src/components/biometrics/Calibration/CalibrationControls.tsx @@ -0,0 +1,167 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { ChevronRight } from 'lucide-react' +import { useLingui } from '@lingui/react/macro' +import { trpc } from '@/src/utils/trpc' +import { Button, InlineError, Modal, Skeleton, StatusDot } from '@/src/components/ds' +import { cn } from '@/lib/utils' +import type { Segment } from './calibration-model' + +export type RestoreTarget = 'previous' | 'defaults' + +/** Saved › Requested › Processor, each with a dot and a mono value. */ +export function LifecycleStrip({ saved, requested, processor }: { saved: Segment, requested: Segment, processor: Segment }) { + const { t } = useLingui() + const segments = [ + { key: 'saved', label: t`Saved`, segment: saved }, + { key: 'requested', label: t`Requested`, segment: requested }, + { key: 'processor', label: t`Processor`, segment: processor }, + ] + return ( +
+ {segments.map(({ key, label, segment }, i) => ( +
+ {i > 0 && } +
+ + {label} + {segment.value} +
+
+ ))} +
+ ) +} + +export function RestoreMenu({ hasPrevious, disabled, onSelect, className }: { + hasPrevious: boolean + disabled?: boolean + onSelect: (target: RestoreTarget) => void + className?: string +}) { + const { t } = useLingui() + const [open, setOpen] = useState(false) + const root = useRef(null) + useEffect(() => { + if (!open) return + const close = (e: MouseEvent | KeyboardEvent) => { + if (e instanceof KeyboardEvent ? e.key === 'Escape' : !root.current?.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', close) + document.addEventListener('keydown', close) + return () => { + document.removeEventListener('mousedown', close) + document.removeEventListener('keydown', close) + } + }, [open]) + const pick = (target: RestoreTarget) => { + setOpen(false) + onSelect(target) + } + return ( +
+ + {open && ( +
+ + +
+ )} +
+ ) +} + +const randomKey = () => Array.from(crypto.getRandomValues(new Uint8Array(24)), b => b.toString(16).padStart(2, '0')).join('') +const show = (value: unknown) => typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean' ? String(value) : value === undefined ? '—' : JSON.stringify(value) + +/** + * Restore review: what changes, then one request. Restoring saves the target + * as the profile and requests it; the current saved profile becomes previous. + */ +export function RestoreDialog({ side, target, binding, onClose, onDone }: { + side: 'left' | 'right' + target: RestoreTarget + binding: { generation: number, desiredRevisionId: string | null, previousRevisionId: string | null } + onClose: () => void + onDone: () => void +}) { + const { t } = useLingui() + const diff = trpc.biometricTuning.diffProfileRevision.useMutation() + const rollback = trpc.biometricTuning.rollbackProfile.useMutation() + const reset = trpc.biometricTuning.resetProfile.useMutation() + const key = useRef(randomKey()) + const revisionId = target === 'previous' ? binding.previousRevisionId : null + const { mutate } = diff + useEffect(() => { + mutate({ side, revisionId, againstRevisionId: binding.desiredRevisionId }) + }, [mutate, side, revisionId, binding.desiredRevisionId]) + + const busy = rollback.isPending || reset.isPending + const error = diff.error ?? rollback.error ?? reset.error + async function confirm() { + if (!diff.data) return + const common = { side, expectedGeneration: binding.generation, expectedResolvedHash: diff.data.afterHash, boundary: 'next_session' as const, + requester: 'web-calibration', idempotencyKey: key.current, allowUnvalidated: true as const } + if (target === 'previous') await rollback.mutateAsync({ ...common, revisionId }) + else await reset.mutateAsync(common) + onDone() + } + + const changes = diff.data?.changes ?? [] + const numeric = changes.filter(c => !c.path.startsWith('sleep_stages')) + const stages = changes.length - numeric.length + const title = target === 'previous' ? t`Restore the previous profile` : t`Restore system defaults` + return ( + + + + + )} + > +

+ {t`Saves it as the profile and requests it; the current saved profile becomes previous. The processor keeps running what it has until it acknowledges the request.`} +

+ {!diff.data && !diff.error + ? + : diff.data && ( + changes.length === 0 + ?

{t`Identical to the saved profile; restoring only re-requests it.`}

+ : ( +
    + {numeric.map(c => ( +
  • + {c.path} + + {show(c.before)} + {' → '} + {show(c.after)} + +
  • + ))} + {stages > 0 &&
  • {t`${stages} sleep-stage values`}
  • } +
+ ) + )} + {error ? {error.message} : null} +
+ ) +} diff --git a/src/components/biometrics/Calibration/CalibrationEvidence.tsx b/src/components/biometrics/Calibration/CalibrationEvidence.tsx new file mode 100644 index 00000000..60918bf7 --- /dev/null +++ b/src/components/biometrics/Calibration/CalibrationEvidence.tsx @@ -0,0 +1,318 @@ +'use client' + +import { useLingui } from '@lingui/react/macro' +import { Upload } from 'lucide-react' +import { trpc } from '@/src/utils/trpc' +import { Card, CardHeader } from '@/src/components/ds' +import { cn } from '@/lib/utils' +import { + aggregate, + barHeight, + clockLabel, + compareTone, + coverage, + nightLabel, + shortDuration, + TRACE, + traceTicks, + tracePath, + type ConfigLabel, + type JobLike, + type NightScore, + type RunStatus, + type Split, +} from './calibration-model' + +export interface DatasetNight { + cohortId: string + split: Split + startMs: number + endMs: number + score: NightScore | null + /** Reference sources ("watch + ring"); null = no reference for this night. */ + reference: string | null +} + +const SPLIT_LETTER: Record = { tuning: 'T', validation: 'V', holdout: 'H' } +const splitTone = (split: Split) => split === 'holdout' ? 'text-warn' : 'text-fg-3' + +function formatBytes(n: number) { + if (n < 1024) return `${n} B` + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` + return `${(n / 1024 / 1024).toFixed(1)} MB` +} +function formatDay(ms: number, timeZone: string) { + try { + return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', timeZone }).format(ms) + } + catch { + return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }).format(ms) + } +} + +/** The frozen nights the backtest replays, split whole into tuning/validation/holdout. */ +export function DatasetCard({ dataset, nights, selected, onSelect, onImport, onFreeze, timezone }: { + dataset: { createdAtMs: number, byteLength: number, available: boolean } | null + nights: DatasetNight[] + selected: string | null + onSelect: (cohortId: string) => void + onImport: () => void + onFreeze: () => void + timezone: string +}) { + const { t } = useLingui() + const counts = { tuning: 0, validation: 0, holdout: 0 } + for (const n of nights) counts[n.split]++ + const sources = [...new Set(nights.flatMap(n => n.reference ? n.reference.split(' + ') : []))] + const explainer = dataset + ? sources.length + ? t`Frozen copy of ${nights.length} nights and ${sources.join(' + ')} reference. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own.` + : t`Frozen copy of ${nights.length} nights. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own.` + : t`No dataset is frozen for this side yet. Freeze recorded nights with an imported reference to backtest a profile.` + return ( + + + {dataset.available ? t`frozen ${formatDay(dataset.createdAtMs, timezone)} · ${formatBytes(dataset.byteLength)}` : t`released`} + + )} + /> + {nights.length > 0 && ( +
+ {nights.map((n) => { + const label = nightLabel(n.cohortId) + const on = n.cohortId === selected + return ( + + ) + })} +
+ )} +
+ {nights.length > 0 && ( + <> + + T + {` ${t`tuning`} · ${counts.tuning}`} + + + V + {` ${t`validation`} · ${counts.validation}`} + + + H + {` ${t`holdout`} · ${counts.holdout}`} + + + )} + + +
+
+ ) +} + +const fmt1 = (v: number | null) => v === null ? '—' : v.toFixed(1) + +/** "What the saved profile did": per-night error, one night's trace, and the split table. */ +export function BacktestCard({ side, group, run, nights, scores, selected, onSelect, trace, timezone, h24 }: { + side: 'left' | 'right' + group: { latest: JobLike } | null + run: RunStatus | null + nights: DatasetNight[] + scores: NightScore[] + selected: DatasetNight | null + onSelect: (cohortId: string) => void + trace: { job: JobLike, contentHash: string, previous: ConfigLabel } | null + timezone: string + h24: boolean +}) { + const { t } = useLingui() + const statusText = run && (run.kind === 'done' ? t`done · ${shortDuration(run.durationMs)}` : run.kind === 'running' ? t`running` : run.kind === 'queued' ? t`queued` : t`failed`) + const statusTone = run ? { ok: 'text-ok', warn: 'text-warn', danger: 'text-danger', muted: 'text-fg-3' }[run.tone] : '' + + if (!group) { + return ( + + + + ) + } + + // Up to two weeks of nights; older ones stay in the table aggregates. + const shown = nights.slice(-14) + const split = (s: Split) => aggregate(scores, 'heart_rate', s) + const rows: Array<{ label: string, trust?: boolean, previous: number | null, saved: number | null, unit: string, higherIsBetter?: boolean, percent?: boolean }> = [ + { label: t`HR error · tuning`, ...split('tuning'), unit: '' }, + { label: t`HR error · validation`, ...split('validation'), unit: '' }, + { label: t`HR error · holdout`, trust: true, ...split('holdout'), unit: '' }, + { label: t`Night covered`, ...coverage(scores), unit: '%', higherIsBetter: true, percent: true }, + { label: t`HRV index error`, ...aggregate(scores, 'window_ibi_variability_v1'), unit: ' ms' }, + { label: t`Breathing error`, ...aggregate(scores, 'breathing_rate'), unit: ' br/m' }, + ] + const cell = (v: number | null, percent?: boolean, unit = '') => v === null ? '—' : percent ? `${Math.round(v * 100)}%` : `${v.toFixed(1)}${unit}` + + return ( + + {t`Backtest ${group.latest.id.slice(0, 4)}. Frozen dataset replayed through the previous and saved profiles; nothing here changes the pod.`}} + right={statusText && {statusText}} + /> + +
+ {t`HR error per night · bpm vs reference`} + {t`click a night`} +
+
+ {shown.map((n) => { + const on = n.cohortId === selected?.cohortId + const prev = n.score?.previous.heart_rate.mae ?? null, saved = n.score?.saved.heart_rate.mae ?? null + const label = nightLabel(n.cohortId) + return ( + + ) + })} +
+
+ + + {t`previous`} + + + + {t`saved`} + + {t`H holdout`} +
+ +
+ {selected && trace && trace.job.evaluationScope === 'retained_waveform_replay' + ? + : ( +

+ {selected && trace + ? t`This backtest compared stored readings, so there is no recomputed heart-rate trace to draw.` + : t`Pick a scored night to see its heart-rate trace.`} +

+ )} + +
+
+ + {t`previous`} + {t`saved`} + {rows.map((r) => { + const tone = compareTone(r.previous, r.saved, r.higherIsBetter) + return ( +
+ {r.label} + {cell(r.previous, r.percent, r.unit)} + {cell(r.saved, r.percent, r.unit)} +
+ ) + })} +
+

{t`Holdout is the number to trust; tuning nights were used to pick these values.`}

+ + ) +} + +/** One night: reference, previous and saved heart rate on a 40–100 bpm scale. */ +function NightTrace({ side, night, trace, timezone, h24 }: { + side: 'left' | 'right' + night: DatasetNight + trace: { job: JobLike, contentHash: string, previous: ConfigLabel } + timezone: string + h24: boolean +}) { + const { t } = useLingui() + const base = { id: trace.job.id, expectedContentHash: trace.contentHash, cohortId: night.cohortId, offset: 0, limit: 500 } + const saved = trpc.biometricTuning.getBacktestNumericTrace.useQuery({ ...base, configuration: 'candidate' }, { staleTime: Infinity }) + const previous = trpc.biometricTuning.getBacktestNumericTrace.useQuery({ ...base, configuration: trace.previous }, { staleTime: Infinity }) + const reference = trpc.biometricTuning.getBacktestReferenceTrace.useQuery({ id: trace.job.id, expectedContentHash: trace.contentHash, cohortId: night.cohortId, metric: 'heart_rate', offset: 0, limit: 500 }, + { staleTime: Infinity, enabled: night.reference !== null }) + const label = nightLabel(night.cohortId) + const savedSide = (saved.data?.items ?? []).filter(i => i.side === side) + const points = (rows: Array<{ timestampMs: number, heartRate: number | null }>) => rows.map(r => ({ t: r.timestampMs, v: r.heartRate })) + const refPoints = (reference.data?.items ?? []).filter(r => r.compatibility === null && typeof r.value === 'number').map(r => ({ t: r.startMs, v: r.value as number })) + const truncated = Boolean(saved.data?.nextCursor || previous.data?.nextCursor || reference.data?.nextCursor) + const ticks = traceTicks(night.startMs, night.endMs) + const yAt = (bpm: number) => TRACE.height - ((bpm - TRACE.minBpm) / (TRACE.maxBpm - TRACE.minBpm)) * TRACE.height + const quality = savedSide.length ? savedSide.reduce((s, r) => s + (r.qualityScore ?? 0), 0) / savedSide.length : null + const note = night.reference === null + ? t`no reference device · excluded from error` + : quality === null ? night.reference : `${night.reference} · ${t`quality`} ${quality.toFixed(2)}` + return ( + <> +
+ {t`Heart rate · ${label.weekday} ${label.day}`} + {note} +
+
+ + {[100, 70, 40].map(bpm => )} + + + + +
+ {ticks.map(tick => {clockLabel(tick, timezone, h24)})} +
+
+ 100 + 70 + 40 +
+
+
+ {`— ${t`saved`}`} + {`┄ ${t`previous`}`} + {`— ${t`reference`}`} + {truncated && {t`first 500 readings`}} +
+ + ) +} diff --git a/src/components/biometrics/Calibration/CalibrationHistory.tsx b/src/components/biometrics/Calibration/CalibrationHistory.tsx deleted file mode 100644 index 0d321222..00000000 --- a/src/components/biometrics/Calibration/CalibrationHistory.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Trans, useLingui } from '@lingui/react/macro' -import { trpc } from '@/src/utils/trpc' -import { Button, Card, CardHeader, InlineError, Skeleton } from '@/src/components/ds' -import { useTimeFormat } from '@/src/providers/TimeFormatProvider' - -export function CalibrationHistory({ side }: { side: 'left' | 'right' }) { - const { t, i18n } = useLingui() - const timeFormat = useTimeFormat() - const [beforeGeneration, setBeforeGeneration] = useState() - const history = trpc.biometricTuning.listActivationHistory.useQuery({ side, beforeGeneration }, { refetchInterval: beforeGeneration === undefined ? 5000 : false }) - const settings = trpc.settings.getAll.useQuery({}, { staleTime: 60000 }) - const timezone = settings.data?.device.timezone ?? 'UTC' - const dates = new Intl.DateTimeFormat(i18n.locale || 'en', { dateStyle: 'medium', timeStyle: 'short', timeZone: timezone, hour12: timeFormat !== '24h' }) - const actions: Record = { activate: t`Apply profile`, reset: t`Use system defaults`, rollback: t`Restore previous profile`, migration: t`Migrate settings`, fallback: t`Compatibility fallback` } - const states: Record = { saved: t`Saved`, pending_delivery: t`Waiting for delivery`, pending_boundary: t`Waiting for a processing boundary`, applied: t`Applied`, failed: t`Failed` } - return ( - - Activation history} subtitle={Past acknowledgements do not establish which profile is running now.} /> -

- {t`Times shown in ${timezone}`} -

- {history.error - ? {history.error.message} - : !history.data - ? - : history.data.items.length === 0 - ?

No activation history for this side.

- : ( -
    - {history.data.items.map(event => ( -
  1. -
    - {actions[event.action]} - {states[event.state]} -
    - -

    - {t`Generation ${event.generation}`} -

    - {event.reason ?

    {event.reason}

    : null} -
    - Exact profile transition -

    - {event.previousHash} - {' '} - → - {' '} - {event.nextHash} -

    -

    - {t`Requested by ${event.requester}`} -

    -
    -
  2. - ))} -
- )} -
- {beforeGeneration !== undefined ? : null} - {history.data?.nextGeneration != null ? : null} -
-
- ) -} diff --git a/src/components/biometrics/Calibration/CalibrationOverview.tsx b/src/components/biometrics/Calibration/CalibrationOverview.tsx deleted file mode 100644 index 26d68f64..00000000 --- a/src/components/biometrics/Calibration/CalibrationOverview.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client' - -import { Trans, useLingui } from '@lingui/react/macro' -import { trpc } from '@/src/utils/trpc' -import { Card, CardHeader, InlineError, SettingRow, Skeleton } from '@/src/components/ds' -import { SectionColumns } from '@/src/components/Settings/SettingsLayout' - -export function CalibrationOverview({ side }: { side: 'left' | 'right' }) { - const { t } = useLingui() - const overview = trpc.biometricTuning.getProfileOverview.useQuery({ side }, { refetchInterval: 5000 }) - const activation = trpc.biometricTuning.getActivationStatus.useQuery({ side }, { refetchInterval: 5000 }) - if (overview.error) return {overview.error.message} - if (!overview.data) return - const data = overview.data - const states: Record = { unreported: t`Not reported`, current: t`Current`, stale: t`Stale`, failed: t`Failed`, - mixed_generations: t`Different versions reported`, clock_skew: t`Clock mismatch`, pending_delivery: t`Waiting for delivery`, - pending_boundary: t`Waiting for a processing boundary`, applied: t`Applied` } - const components: Record = { heart_rate: t`Heart rate`, breathing_rate: t`Breathing rate`, hrv_index: t`HRV index` } - return ( - - - Profile and processor} subtitle={Saved settings and processor reports are separate.} /> - Saved profile}>{data.desired.isDefault ? System defaults : Personalized profile} - Requested change}>{activation.data ? states[activation.data.observedState] : t`Not reported`} - Processor health}>{states[data.runtimeHealth.state]} - {activation.error ? {activation.error.message} : null} - {data.desired.warning ?

{data.desired.warning}

: null} - {data.runtimeHealth.components.map(component => ( - - {states[component.state]} - - ))} -
- Configuration identity -

{data.desired.hash}

-

- {t`Generation ${data.desired.binding.generation}`} -

-
-
- - What these metrics mean} /> -

The HRV index measures window IBI variability. It is not clinical beat-to-beat RMSSD, and incompatible reference metrics cannot validate it.

-

Historical sleep stages are recomputed with the current classifier. Original stored vitals remain separate from replay results.

- {data.stageView.provenance.warning ?

{data.stageView.provenance.warning}

: null} -
- - )} - right={( - - Measured sensor calibration} subtitle={Sensor measurements are separate from algorithm personalization.} /> - {data.sensorCalibration.length === 0 - ?

No sensor calibration has been reported for this side.

- : data.sensorCalibration.map(sensor => ( - - {sensor.freshness === 'current' ? t`Current` : sensor.freshness === 'expired' ? t`Expired` : t`Freshness unknown`} - - ))} - {data.limitations.map(limitation =>

{limitation}

)} -
- )} - /> - ) -} diff --git a/src/components/biometrics/Calibration/CalibrationPage.tsx b/src/components/biometrics/Calibration/CalibrationPage.tsx new file mode 100644 index 00000000..08173901 --- /dev/null +++ b/src/components/biometrics/Calibration/CalibrationPage.tsx @@ -0,0 +1,382 @@ +'use client' + +import { useEffect, useMemo, useRef, useState } from 'react' +import { useLingui } from '@lingui/react/macro' +import { defaultConfig, type BiometricConfig } from '@/src/biometrics/algorithms/registry-core' +import { trpc } from '@/src/utils/trpc' +import { InlineError, SegmentedControl, Skeleton } from '@/src/components/ds' +import { useSideNames } from '@/src/hooks/useSideNames' +import { useTimeFormat } from '@/src/providers/TimeFormatProvider' +import { numericReportViewSchema } from './report-view' +import { + actorOf, + activityTime, + baselineLabel, + changedPaths, + draftFrom, + lifecycle, + nightScores, + overridesFor, + paramDiffs, + runStatus, + savedBacktest, + shortDuration, + type Draft, + type NightScore, + type Split, +} from './calibration-model' +import { LifecycleStrip, RestoreMenu, RestoreDialog, type RestoreTarget } from './CalibrationControls' +import { ProfileCard } from './CalibrationProfileCard' +import { BacktestCard, DatasetCard, type DatasetNight } from './CalibrationEvidence' +import { ActivityCard, type ActivityRow } from './CalibrationActivity' + +type Side = 'left' | 'right' +export type CalibrationTool = 'references' | 'backtests' +const NIL = '00000000-0000-0000-0000-000000000000' +const randomKey = () => Array.from(crypto.getRandomValues(new Uint8Array(24)), b => b.toString(16).padStart(2, '0')).join('') + +/** Wall clock for relative times, ticking at `intervalMs`. */ +function useNowMs(intervalMs: number) { + const [now, setNow] = useState(() => Date.now()) + useEffect(() => { + const id = setInterval(() => setNow(Date.now()), intervalMs) + return () => clearInterval(id) + }, [intervalMs]) + return now +} + +function asConfig(value: unknown): BiometricConfig | null { + const v = value as BiometricConfig | null + return v && typeof v === 'object' && v.heart_rate && v.breathing_rate && v.hrv_index && v.sleep_stages ? v : null +} + +/** + * Biometric calibration (design 2a): the saved profile against the previous + * one, what the processor is running, the frozen dataset and backtest that + * judged the change, and who did what. Agents act through the same API, so + * their saves, runs and requests show up here as they happen. + */ +export function CalibrationPage({ side, onSideChange, onOpenTool }: { + side: Side + onSideChange: (side: Side) => void + onOpenTool: (tool: CalibrationTool) => void +}) { + const { t } = useLingui() + const { leftName, rightName } = useSideNames() + const timeFormat = useTimeFormat() + const h24 = timeFormat === '24h' + const utils = trpc.useUtils() + + const overview = trpc.biometricTuning.getProfileOverview.useQuery({ side }, { refetchInterval: 5000 }) + const binding = overview.data?.desired.binding + const savedId = binding?.desiredRevisionId ?? null + const previousId = binding?.previousRevisionId ?? null + const savedRevision = trpc.biometricTuning.getProfileRevision.useQuery({ id: savedId ?? NIL }, { enabled: Boolean(savedId), staleTime: Infinity }) + const previousRevision = trpc.biometricTuning.getProfileRevision.useQuery({ id: previousId ?? NIL }, { enabled: Boolean(previousId), staleTime: Infinity }) + const status = trpc.biometricTuning.getActivationStatus.useQuery({ side }, { refetchInterval: 5000 }) + const history = trpc.biometricTuning.listActivationHistory.useQuery({ side }, { refetchInterval: 15000 }) + const revisions = trpc.biometricTuning.listProfileRevisions.useQuery({ side }, { refetchInterval: 30000 }) + const datasets = trpc.biometricTuning.listDatasets.useQuery({ side }, { refetchInterval: 30000 }) + const imports = trpc.biometricTuning.listImports.useQuery({ side }, { refetchInterval: 30000 }) + const settings = trpc.settings.getAll.useQuery({}, { staleTime: 60000 }) + const timezone = settings.data?.device.timezone ?? 'UTC' + + const backtests = trpc.biometricTuning.listBacktests.useQuery({ side }, { + refetchInterval: query => query.state.data?.items.some(job => job.state === 'queued' || job.state === 'running' || job.state === 'cancel_requested') ? 3000 : 15000, + }) + const jobs = useMemo(() => backtests.data?.items ?? [], [backtests.data]) + const group = useMemo(() => savedBacktest(jobs, savedId), [jobs, savedId]) + const run = runStatus(group?.jobs ?? []) + + const finished = (group?.jobs ?? []).filter(job => job.state === 'succeeded') + const summaries = trpc.useQueries(q => finished.map(job => q.biometricTuning.getBacktestSummary({ id: job.id }, { staleTime: 60000 }))) + + const datasetId = group?.datasetId ?? datasets.data?.items.find(d => d.available)?.id ?? null + const listed = datasets.data?.items.find(d => d.id === datasetId) + const fetched = trpc.biometricTuning.getDataset.useQuery({ id: datasetId ?? NIL }, { enabled: Boolean(datasetId) && !listed && Boolean(datasets.data) }) + const dataset = listed ?? fetched.data ?? null + + const saved = overview.data?.desired.resolved ?? null + const previous = previousId ? asConfig(previousRevision.data?.resolved) : defaultConfig + const previousHash = previousId ? previousRevision.data?.resolvedHash ?? null : null + + // Draft follows the saved profile; a new save (here or by an agent) resets it. + const savedKey = `${side}:${overview.data?.desired.hash ?? ''}` + const [draftState, setDraftState] = useState<{ key: string, draft: Draft } | null>(null) + const draft = saved ? (draftState?.key === savedKey ? draftState.draft : draftFrom(saved)) : null + const setDraft = (next: Draft) => setDraftState({ key: savedKey, draft: next }) + const dirty = saved && draft ? changedPaths(draft, saved).length > 0 : false + + // Per-night scores across the splits that finished. + const scores = useMemo(() => { + const rows: NightScore[] = [] + finished.forEach((job, i) => { + const parsed = numericReportViewSchema.safeParse(summaries[i]?.data?.report) + if (parsed.success) rows.push(...nightScores(parsed.data, job.split, baselineLabel(job, previousHash))) + }) + return rows + }, [finished, summaries, previousHash]) + + const [selected, setSelected] = useState(null) + const assignments = dataset?.manifest.splitProtocol.assignments ?? [] + const nights: DatasetNight[] = assignments.map((a) => { + const score = scores.find(s => s.cohortId === a.cohortId) + const sources = (imports.data?.items ?? []).filter(i => i.available) + .filter(i => i.summary.startMs !== null && i.summary.endMs !== null && i.summary.startMs < a.endMs && i.summary.endMs > a.startMs) + .map(i => i.summary.source) + return { cohortId: a.cohortId, split: a.split, startMs: a.startMs, endMs: a.endMs, score: score ?? null, + reference: score && !score.hasReference ? null : [...new Set(sources)].join(' + ') || (score ? t`reference` : null) } + }) + const selectedNight = nights.find(n => n.cohortId === selected) ?? nights.find(n => n.split === 'holdout' && n.score?.hasReference) ?? nights.find(n => n.score) ?? null + const traceJob = selectedNight ? finished.find(job => job.split === selectedNight.split) : undefined + const traceSummary = traceJob ? summaries[finished.indexOf(traceJob)]?.data : undefined + + /* ─── Mutations ─────────────────────────────────────────────────────── */ + const createRevision = trpc.biometricTuning.createProfileRevision.useMutation() + const activate = trpc.biometricTuning.activateProfile.useMutation() + const enqueue = trpc.biometricTuning.enqueueBacktest.useMutation() + const [saveError, setSaveError] = useState() + const pending = useRef<{ fingerprint: string, key: string } | undefined>(undefined) + const saving = createRevision.isPending || activate.isPending || enqueue.isPending + + function refresh() { + void utils.biometricTuning.getProfileOverview.invalidate({ side }) + void utils.biometricTuning.getActivationStatus.invalidate({ side }) + void utils.biometricTuning.listActivationHistory.invalidate({ side }) + void utils.biometricTuning.listProfileRevisions.invalidate({ side }) + void utils.biometricTuning.listBacktests.invalidate({ side }) + } + + async function saveAndRequest() { + if (!binding || !draft || !saved || !dirty) return + setSaveError(undefined) + const overrides = overridesFor((savedRevision.data?.overrides ?? []) as Array<{ path: string, value: unknown }>, draft, defaultConfig) + const fingerprint = JSON.stringify({ side, generation: binding.generation, savedId, overrides }) + if (pending.current?.fingerprint !== fingerprint) pending.current = { fingerprint, key: randomKey() } + const key = pending.current.key + try { + const revision = await createRevision.mutateAsync({ side, ...(savedId ? { parentRevisionId: savedId } : {}), overrides, + origin: 'manual', requester: 'web-calibration', idempotencyKey: key }) + const event = await activate.mutateAsync({ side, revisionId: revision.id, expectedGeneration: binding.generation, + expectedResolvedHash: revision.resolvedHash, boundary: 'next_session', requester: 'web-calibration', idempotencyKey: key, allowUnvalidated: true }) + // Re-run the evidence for the new profile on the same frozen dataset. + // Holdout stays untouched: each exposure weakens it, so that is spent deliberately. + const experimentId = group?.experimentId ?? jobs[0]?.experimentId + const splits = new Set(assignments.map(a => a.split)) + if (experimentId) { + for (const split of (['tuning', 'validation'] as Split[]).filter(s => splits.size === 0 || splits.has(s))) { + await enqueue.mutateAsync({ experimentId, candidateRevisionId: revision.id, expectedBindingGeneration: event.generation, split, idempotencyKey: `${key}:${split}` }) + } + } + pending.current = undefined + } + catch (cause) { + setSaveError(cause instanceof Error ? cause.message : t`Save failed.`) + } + finally { + refresh() + } + } + + const [restore, setRestore] = useState(null) + + /* ─── Activity ──────────────────────────────────────────────────────── */ + const nowMs = useNowMs(15_000) + const activity = useMemo(() => { + const rows: ActivityRow[] = [] + const byId = new Map((revisions.data?.items ?? []).map(r => [r.id, r])) + const resolvedOf = (id: string | null) => id ? asConfig(byId.get(id)?.resolved) : defaultConfig + for (const event of history.data?.items ?? []) { + const requester = actorOf(event.requester) + if (event.action === 'activate') { + const revision = event.nextRevisionId ? byId.get(event.nextRevisionId) : undefined + const before = resolvedOf(event.previousRevisionId), after = resolvedOf(event.nextRevisionId) + const diffs = before && after ? paramDiffs(before, after) : [] + rows.push({ id: `${event.id}:saved`, atMs: revision?.createdAtMs ?? event.createdAtMs, + actor: revision ? actorOf(revision.requester, revision.origin) : requester, what: t`Saved the profile`, + detail: diffs.length ? diffs.join(' · ') : null }) + rows.push({ id: `${event.id}:requested`, atMs: event.createdAtMs, actor: requester, what: t`Requested activation · generation ${event.generation}`, detail: null }) + } + else if (event.action === 'rollback') rows.push({ id: event.id, atMs: event.createdAtMs, actor: requester, what: t`Restored the previous profile and requested it`, detail: null }) + else if (event.action === 'reset') rows.push({ id: event.id, atMs: event.createdAtMs, actor: requester, what: t`Restored system defaults and requested them`, detail: null }) + else rows.push({ id: event.id, atMs: event.createdAtMs, actor: { kind: 'pod', name: 'pod' }, what: event.action === 'migration' ? t`Migrated sleep-stage settings into the profile` : t`Fell back to a compatible profile`, detail: event.reason }) + if (event.state === 'applied') rows.push({ id: `${event.id}:ack`, atMs: event.updatedAtMs, actor: { kind: 'pod', name: 'pod' }, what: t`Processor acknowledged generation ${event.generation}`, detail: null }) + if (event.state === 'failed') rows.push({ id: `${event.id}:failed`, atMs: event.updatedAtMs, actor: { kind: 'pod', name: 'pod' }, what: t`Processor rejected generation ${event.generation}`, detail: event.reason }) + } + const splitLabel: Record = { tuning: t`tuning`, validation: t`validation`, holdout: t`holdout` } + for (const job of jobs.slice(0, 20)) { + if (job.state !== 'succeeded' && job.state !== 'failed') continue + const index = finished.findIndex(f => f.id === job.id) + const parsed = index >= 0 ? numericReportViewSchema.safeParse(summaries[index]?.data?.report) : null + let detail: string | null = null + if (parsed?.success) { + const rows = nightScores(parsed.data, job.split, baselineLabel(job, previousHash)).filter(n => n.previous.heart_rate.mae !== null && n.saved.heart_rate.mae !== null) + if (rows.length) { + const avg = (pick: (n: NightScore) => number) => (rows.reduce((s, n) => s + pick(n), 0) / rows.length).toFixed(1) + detail = t`${splitLabel[job.split]} HR error ${avg(n => n.previous.heart_rate.mae as number)} → ${avg(n => n.saved.heart_rate.mae as number)} bpm · ${shortDuration(job.updatedAtMs - job.createdAtMs)}` + } + } + rows.push({ id: job.id, atMs: job.updatedAtMs, actor: { kind: 'pod', name: 'pod' }, + what: job.state === 'succeeded' ? t`Finished a backtest on ${splitLabel[job.split]} nights` : t`Backtest on ${splitLabel[job.split]} nights failed`, + detail: job.state === 'failed' ? job.error : detail }) + } + for (const d of datasets.data?.items ?? []) { + rows.push({ id: d.id, atMs: d.createdAtMs, actor: { kind: 'pod', name: 'pod' }, what: t`Froze a dataset of ${d.manifest.splitProtocol.assignments.length} nights`, detail: null }) + } + for (const i of imports.data?.items ?? []) { + rows.push({ id: i.id, atMs: i.createdAtMs, actor: { kind: 'pod', name: 'pod' }, what: t`Stored a ${i.summary.source} reference import`, detail: t`${i.summary.accepted} records` }) + } + return rows.sort((a, b) => b.atMs - a.atMs).map(row => ({ ...row, when: activityTime(row.atMs, nowMs, timezone, h24) })) + }, [history.data, revisions.data, jobs, datasets.data, imports.data, summaries, finished, previousHash, nowMs, timezone, h24, t]) + + /* ─── Render ────────────────────────────────────────────────────────── */ + const error = overview.error ?? status.error + if (error) return {error.message} + if (!overview.data || !binding || !saved || !draft || (savedId && !savedRevision.data) || (previousId && !previousRevision.data)) { + return ( +
+ +
+ + +
+
+ ) + } + + const event = status.data?.event ?? null + const savedActor = savedRevision.data ? actorOf(savedRevision.data.requester, savedRevision.data.origin) : null + const strip = lifecycle({ + nowMs, + saved: savedRevision.data && savedActor ? { actor: savedActor, atMs: savedRevision.data.createdAtMs } : null, + bindingUpdatedAtMs: binding.updatedAtMs, + generation: binding.generation, + request: event && status.data?.isCurrent + ? { observedState: status.data.observedState, createdAtMs: event.createdAtMs, updatedAtMs: event.updatedAtMs, previousIsDefaults: event.previousRevisionId === null } + : null, + runtimeReported: overview.data.runtimeStatus === 'receipts_available', + }) + + return ( +
+
+ + + 0} onSelect={setRestore} /> +
+ +
+
+ + onOpenTool('references')} + onFreeze={() => onOpenTool('backtests')} + timezone={timezone} + /> + +
+ +
+ +
+ + +
+

+ {dirty ? t`Overwrites the saved profile and requests it; the current one becomes previous.` : t`Nothing changes until you move a control or restore.`} +

+ {saveError ? {saveError} : null} + +
+
+ + {restore + ? ( + setRestore(null)} + onDone={() => { + setRestore(null) + refresh() + }} + /> + ) + : null} +
+ ) +} + +function SensorLine({ sensors, hash }: { sensors: Array<{ id: number, sensorType: string, status: string, freshness: 'expired' | 'current' | 'unknown' }>, hash: string }) { + const { t } = useLingui() + const [full, setFull] = useState(false) + const current = sensors.filter(s => s.status === 'completed') + const value = current.length + ? current.map(s => `${s.sensorType} ${s.freshness === 'current' ? t`current` : s.freshness === 'expired' ? t`expired` : t`freshness unknown`}`).join(', ') + : t`not reported` + return ( +
+ + {t`Measured sensor calibration for this side:`} + {' '} + {value} + {t`. Separate from the profile.`} + + + {t`config id`} + {' '} + {full ? hash : `${hash.slice(0, 4)}…${hash.slice(-2)}`} + {' · '} + + +
+ ) +} diff --git a/src/components/biometrics/Calibration/CalibrationProfileCard.tsx b/src/components/biometrics/Calibration/CalibrationProfileCard.tsx new file mode 100644 index 00000000..1a3fbddd --- /dev/null +++ b/src/components/biometrics/Calibration/CalibrationProfileCard.tsx @@ -0,0 +1,269 @@ +'use client' + +import type { ReactNode } from 'react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useLingui } from '@lingui/react/macro' +import { defaultConfig, type BiometricConfig } from '@/src/biometrics/algorithms/registry-core' +import { STAGE_METRIC_LABELS } from '@/src/lib/sleep-stage-rules' +import { formatMetricValue } from '@/src/components/Settings/sleepStageEditing' +import { Card, CardHeader, Slider } from '@/src/components/ds' +import { cn } from '@/lib/utils' +import { aggregate, coverage, PARAMS, readParam, roundParam, stageOverrides, type Actor, type Draft, type NightScore, type ParamPath } from './calibration-model' + +export const ACTOR_BADGE: Record = { + agent: { label: 'AGENT', className: 'bg-actor-agent-bg text-actor-agent' }, + you: { label: 'YOU', className: 'bg-actor-you-bg text-actor-you' }, + pod: { label: 'POD', className: 'bg-actor-pod-bg text-actor-pod' }, +} + +export function ActorBadge({ kind }: { kind: Actor['kind'] }) { + const badge = ACTOR_BADGE[kind] + return {badge.label} +} + +function Eyebrow({ children }: { children: ReactNode }) { + return
{children}
+} + +/** Shared 0.25×–2× axis for both breathing ratios; 1× is the median. */ +const RATIO_AXIS = { min: PARAMS['breathing_rate.intervalLowerRatio'].min, max: PARAMS['breathing_rate.intervalUpperRatio'].max } +const THUMB = 18 +const axisPct = (v: number) => (v - RATIO_AXIS.min) / (RATIO_AXIS.max - RATIO_AXIS.min) +/** Native thumb centre for a fraction of the track, so the visual thumb sits on the hit area. */ +const thumbLeft = (f: number) => `calc(${f} * (100% - ${THUMB}px) + ${THUMB / 2}px)` + +/** + * Both breathing cut-offs on one axis around the median: the left handle is + * "shorter than", the right "longer than", and the band between them is the + * intervals that are kept. Each handle stays inside its own manifest range. + */ +function RatioRange({ low, high, onLow, onHigh, lowLabel, highLabel, disabled }: { + low: number + high: number + onLow: (v: number) => void + onHigh: (v: number) => void + lowLabel: string + highLabel: string + disabled?: boolean +}) { + const lo = PARAMS['breathing_rate.intervalLowerRatio'], hi = PARAMS['breathing_rate.intervalUpperRatio'] + const clamp = (v: number, spec: { min: number, max: number }) => Math.min(spec.max, Math.max(spec.min, v)) + const input = 'pointer-events-none absolute inset-0 m-0 !min-h-0 w-full appearance-none bg-transparent opacity-0 ' + + '[&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:size-[18px] [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:appearance-none ' + + '[&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-[18px] [&::-moz-range-thumb]:cursor-pointer' + const a = axisPct(low), b = axisPct(high), median = axisPct(1) + return ( +
+
+
+
+
+ {[a, b].map((f, i) => ( +
+ ))} + onLow(clamp(Number(e.target.value), lo))} + className={input} + /> + onHigh(clamp(Number(e.target.value), hi))} + className={input} + /> +
+
+ {`${RATIO_AXIS.min}×`} + 1× + {`${RATIO_AXIS.max}×`} +
+
+ ) +} + +/** + * The saved profile, value by value against the previous one. Values that + * differ from previous are white; moving a slider edits a draft of the saved + * profile that only takes effect through Save and request. + */ +export function ProfileCard({ saved, previous, previousIsDefaults, draft, onDraft, disabled, actor, savedAgo, scores }: { + saved: BiometricConfig + previous: BiometricConfig + previousIsDefaults: boolean + draft: Draft + onDraft: (draft: Draft) => void + disabled?: boolean + actor: Actor | null + savedAgo: string | null + scores: NightScore[] +}) { + const { t } = useLingui() + const pathname = usePathname() + const lang = pathname?.split('/')[1] || 'en' + const set = (path: ParamPath, value: number) => onDraft({ ...draft, [path]: roundParam(path, value) }) + + // What the saved profile did on the most trusted split that ran. + const split = (['holdout', 'validation', 'tuning'] as const).find(s => aggregate(scores, 'heart_rate', s).nights > 0) + const effect = (paths: ParamPath[], metric: 'heart_rate' | 'breathing_rate' | 'window_ibi_variability_v1', unit: string) => { + if (paths.every(p => draft[p] === readParam(previous, p))) return t`unchanged` + if (paths.some(p => draft[p] !== readParam(saved, p))) return t`tested on save` + if (!split) return t`not tested` + const a = aggregate(scores, metric, split) + if (a.previous === null || a.saved === null) return t`not tested` + const delta = a.saved - a.previous + const sign = delta > 0 ? '+' : delta < 0 ? '−' : '±' + const covered = metric === 'heart_rate' ? coverage(scores.filter(s => s.split === split)).saved : null + return `${split} ${sign}${Math.abs(delta).toFixed(1)} ${unit}${covered === null ? '' : ` · ${t`covered`} ${Math.round(covered * 100)}%`}` + } + const tone = (path: ParamPath) => draft[path] === readParam(previous, path) ? 'text-fg-2' : 'text-fg' + const fmt = (path: ParamPath) => String(draft[path]) + const sub = (path: ParamPath) => `${PARAMS[path].key} · ${t`previous`}: ${readParam(previous, path)} · ${t`range`} ${PARAMS[path].min}–${PARAMS[path].max}` + + // Overrides are counted against the shipped rules the Sleep stages page starts from. + const stages = stageOverrides(defaultConfig.sleep_stages, saved.sleep_stages) + const count = stages.overrides.length + (stages.fallbackChanged ? 1 : 0) + + const row = (label: ReactNode, sublines: ReactNode, control: ReactNode, effectText: string, divider = false) => ( +
+
+ {label} +
{sublines}
+
+
{control}
+
{effectText}
+
+ ) + const value = (path: ParamPath, suffix = '') => {`${fmt(path)}${suffix}`} + const slider = (path: ParamPath, label: string) => ( + set(path, v)} /> + ) + + const tol = 'heart_rate.trackingToleranceBpm', hist = 'heart_rate.trackingHistoryLength' + const low = 'breathing_rate.intervalLowerRatio', high = 'breathing_rate.intervalUpperRatio', windows = 'hrv_index.minimumWindows' + + return ( + + + + {actor.kind === 'agent' ? `${actor.name} · ${savedAgo}` : savedAgo} + + ) + : {t`System defaults`}} + /> + + {t`HEART RATE`} + {row( + <> + {t`Accept an estimate when it is within`} + {' '} + {value(tol)} + {' '} + {t`bpm of the tracker`} + , + sub(tol), slider(tol, t`Tracking tolerance (bpm)`), effect([tol], 'heart_rate', 'bpm'), + )} + {row( + <> + {t`Keep the last`} + {' '} + {value(hist)} + {' '} + {t`accepted estimates in the tracker`} + , + sub(hist), slider(hist, t`Tracking history length`), effect([hist], 'heart_rate', 'bpm'), true, + )} + + {t`BREATHING`} + {row( + <> + {t`Reject intervals shorter than`} + {' '} + {value(low, '×')} + {' '} + {t`or longer than`} + {' '} + {value(high, '×')} + {' '} + {t`the median`} + , + `${PARAMS[low].key} ${PARAMS[low].min}–${PARAMS[low].max} · ${PARAMS[high].key} ${PARAMS[high].min}–${PARAMS[high].max} · ${t`previous`}: ${readParam(previous, low)} / ${readParam(previous, high)}`, + set(low, v)} + onHigh={v => set(high, v)} + />, + effect([low, high], 'breathing_rate', 'br/m'), + )} + + {t`HRV INDEX`} + {row( + <> + {t`Report the index only after`} + {' '} + {value(windows)} + {' '} + {t`accepted 10-second IBI windows`} + , + `${sub(windows)} · ${t`window IBI variability, not clinical RMSSD`}`, + slider(windows, t`Minimum IBI windows`), effect([windows], 'window_ibi_variability_v1', 'ms'), + )} + + {t`SLEEP STAGES`} +
+
+ + {t`Inherits the ${stages.ruleCount} rules and fallback from`} + {' '} + {t`Sleep stages`} + {count === 1 ? t`, with 1 override` : t`, with ${count} overrides`} + + {stages.restructured && {t`The rule list itself differs from the shipped rules.`}} + {stages.overrides.map(o => ( + + {t`Rule ${o.rule}`} + {' · '} + {`${o.stage[0].toUpperCase()}${o.stage.slice(1)} ${t`when`} ${STAGE_METRIC_LABELS[o.metric].toLowerCase()} ${o.op} `} + {formatMetricValue(o.metric, o.before)} + {' '} + {formatMetricValue(o.metric, o.after)} + + ))} + {stages.fallbackChanged && ( + + {t`Fallback`} + {' '} + {defaultConfig.sleep_stages.engine === 'rules' ? defaultConfig.sleep_stages.ruleset.fallback : ''} + {' '} + {saved.sleep_stages.engine === 'rules' ? saved.sleep_stages.ruleset.fallback : ''} + + )} +
+ {t`Edit overrides`} +
+
+ ) +} diff --git a/src/components/biometrics/Calibration/ProfileRecovery.tsx b/src/components/biometrics/Calibration/ProfileRecovery.tsx deleted file mode 100644 index 611b8db3..00000000 --- a/src/components/biometrics/Calibration/ProfileRecovery.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Trans } from '@lingui/react/macro' -import { trpc } from '@/src/utils/trpc' -import { Button, Card, CardHeader, InlineError } from '@/src/components/ds' -import { ManualProfileActivation } from './ManualProfileActivation' -import { ProfileComparison } from './ProfileComparison' - -export function ProfileRecovery({ side }: { side: 'left' | 'right' }) { - const overview = trpc.biometricTuning.getProfileOverview.useQuery({ side }, { refetchInterval: 5000 }) - const [selection, setSelection] = useState<{ action: 'reset' | 'rollback', revisionId: string | null, generation: number, fallback?: { hash: string, warning: string, revisionId: string | null } }>() - const desired = overview.data?.desired - const binding = desired?.binding - return ( - - Restore a profile} subtitle={Review changes before requesting system defaults or the exact previous profile.} /> - {overview.error ? {overview.error.message} : null} -
- - -
- {selection?.fallback - ? ( -
- {selection.fallback.warning} -

The pinned profile is incompatible. Its revision will be preserved, but a valid before-and-after comparison is unavailable.

-
-
Preserved incompatible revision
-
{selection.fallback.revisionId ?? '—'}
-
Default recovery configuration hash
-
{selection.fallback.hash}
-
- {selection.generation !== binding?.generation ?

The saved profile changed. Select a recovery action again to make a new request.

: null} - -
- ) - : selection ? : null} -
- ) -} diff --git a/src/components/biometrics/Calibration/calibration-model.ts b/src/components/biometrics/Calibration/calibration-model.ts new file mode 100644 index 00000000..adafa4ae --- /dev/null +++ b/src/components/biometrics/Calibration/calibration-model.ts @@ -0,0 +1,399 @@ +import type { BiometricConfig } from '@/src/biometrics/algorithms/registry' +import type { StageClassifier, StageMetric, StageOperator } from '@/src/lib/sleep-stage-rules' +import type { NumericReportView } from './report-view' + +/** + * View model for the single-page calibration screen: one saved profile, one + * previous, the processor's acknowledgement, and the backtest that compared + * them. Pure functions only; the page component owns queries and wording. + */ + +export type ParamPath + = | 'heart_rate.trackingToleranceBpm' + | 'heart_rate.trackingHistoryLength' + | 'breathing_rate.intervalLowerRatio' + | 'breathing_rate.intervalUpperRatio' + | 'hrv_index.minimumWindows' + +export interface ParamSpec { path: ParamPath, key: string, min: number, max: number, step: number, decimals: number } + +/** Bounds mirror modules/common/biometrics/manifest.v1.json. */ +export const PARAMS: Record = { + 'heart_rate.trackingToleranceBpm': { path: 'heart_rate.trackingToleranceBpm', key: 'trackingToleranceBpm', min: 5, max: 30, step: 1, decimals: 0 }, + 'heart_rate.trackingHistoryLength': { path: 'heart_rate.trackingHistoryLength', key: 'trackingHistoryLength', min: 1, max: 10, step: 1, decimals: 0 }, + 'breathing_rate.intervalLowerRatio': { path: 'breathing_rate.intervalLowerRatio', key: 'intervalLowerRatio', min: 0.25, max: 0.9, step: 0.05, decimals: 2 }, + 'breathing_rate.intervalUpperRatio': { path: 'breathing_rate.intervalUpperRatio', key: 'intervalUpperRatio', min: 1.1, max: 2, step: 0.05, decimals: 2 }, + 'hrv_index.minimumWindows': { path: 'hrv_index.minimumWindows', key: 'minimumWindows', min: 10, max: 30, step: 1, decimals: 0 }, +} +export const PARAM_PATHS = Object.keys(PARAMS) as ParamPath[] + +export type Draft = Record + +export function readParam(config: BiometricConfig, path: ParamPath): number { + const [group, key] = path.split('.') as [keyof BiometricConfig, string] + return (config[group] as unknown as Record)[key] +} + +export function draftFrom(config: BiometricConfig): Draft { + return Object.fromEntries(PARAM_PATHS.map(path => [path, readParam(config, path)])) as Draft +} + +/** Trim float noise from slider steps (0.1 + 0.05 → 0.15000000000000002). */ +export function roundParam(path: ParamPath, value: number): number { + const factor = 10 ** PARAMS[path].decimals + return Math.round(value * factor) / factor +} + +export function changedPaths(draft: Draft, saved: BiometricConfig): ParamPath[] { + return PARAM_PATHS.filter(path => draft[path] !== readParam(saved, path)) +} + +/** + * The saved overrides with the edited numeric paths replaced. A value equal to + * the shipped default drops its override so it inherits again. + */ +export function overridesFor(saved: Array<{ path: string, value: unknown }>, draft: Draft, defaults: BiometricConfig) { + const edited = new Set(PARAM_PATHS) + const kept = saved.filter(row => !edited.has(row.path)) + const numeric = PARAM_PATHS + .filter(path => draft[path] !== readParam(defaults, path)) + .map(path => ({ path, value: draft[path] })) + return [...kept, ...numeric] +} + +/* ─── Actors ───────────────────────────────────────────────────────────── */ + +export type ActorKind = 'agent' | 'you' | 'pod' +export interface Actor { kind: ActorKind, name: string } + +/** Requesters the web app writes as; anything else came over MCP or the API. */ +const WEB_REQUESTERS = new Set(['web-calibration', 'legacy-stage-settings', 'web']) + +export function actorOf(requester: string, origin?: 'manual' | 'migration' | 'agent' | 'system'): Actor { + if (origin === 'migration' || origin === 'system' || requester === 'system' || requester === 'migration') return { kind: 'pod', name: 'pod' } + if (origin === 'manual' || WEB_REQUESTERS.has(requester)) return { kind: 'you', name: 'you' } + return { kind: 'agent', name: requester } +} + +/* ─── Time ─────────────────────────────────────────────────────────────── */ + +/** "38 s", "14 min", "3 h", "2 d" */ +export function shortDuration(ms: number): string { + const s = Math.max(0, Math.round(ms / 1000)) + if (s < 60) return `${s} s` + const m = Math.round(s / 60) + if (m < 60) return `${m} min` + const h = Math.round(m / 60) + if (h < 48) return `${h} h` + return `${Math.round(h / 24)} d` +} + +export function relativeTime(atMs: number, nowMs: number): string { + const ms = nowMs - atMs + return ms < 45_000 ? 'just now' : `${shortDuration(ms)} ago` +} + +/* ─── Lifecycle strip ──────────────────────────────────────────────────── */ + +export type DotTone = 'ok' | 'warn' | 'danger' | 'muted' +export interface Segment { tone: DotTone, value: string } + +type ObservedState = 'unreported' | 'pending_delivery' | 'pending_boundary' | 'applied' | 'failed' + +export interface LifecycleInput { + nowMs: number + /** Desired revision; null when the saved profile is system defaults. */ + saved: { actor: Actor, atMs: number } | null + bindingUpdatedAtMs: number + generation: number + /** Activation for the current generation. */ + request: { observedState: ObservedState, createdAtMs: number, updatedAtMs: number, previousIsDefaults: boolean } | null + runtimeReported: boolean +} + +export function lifecycle(input: LifecycleInput): { saved: Segment, requested: Segment, processor: Segment } { + const { nowMs, request } = input + const saved: Segment = input.saved + ? { tone: 'ok', value: `by ${input.saved.actor.name} · ${relativeTime(input.saved.atMs, nowMs)}` } + : input.generation > 0 + ? { tone: 'ok', value: `system defaults · ${relativeTime(input.bindingUpdatedAtMs, nowMs)}` } + : { tone: 'ok', value: 'system defaults' } + + let requested: Segment + let processor: Segment + if (!request || request.observedState === 'unreported') { + requested = { tone: 'muted', value: 'none' } + processor = input.runtimeReported + ? { tone: 'ok', value: input.saved ? 'running saved' : 'running system defaults' } + : { tone: 'muted', value: 'not reported' } + } + else if (request.observedState === 'applied') { + requested = { tone: 'ok', value: `acknowledged ${relativeTime(request.updatedAtMs, nowMs)}` } + processor = { tone: 'ok', value: 'running saved' } + } + else { + const failed = request.observedState === 'failed' + requested = failed + ? { tone: 'danger', value: 'failed' } + : { tone: 'warn', value: `waiting ${shortDuration(nowMs - request.createdAtMs)}` } + processor = { tone: failed ? 'danger' : 'ok', value: request.previousIsDefaults ? 'running system defaults' : 'running previous' } + } + return { saved, requested, processor } +} + +/* ─── Sleep-stage overrides ────────────────────────────────────────────── */ + +export interface StageOverride { rule: number, stage: string, metric: StageMetric, op: StageOperator, before: number, after: number } + +/** + * Condition values that differ from the shipped rules, rule by rule. When the + * rule list itself was reshaped, values can't be paired; that is reported as + * `restructured` instead. + */ +export function stageOverrides(defaults: StageClassifier, saved: StageClassifier) { + if (defaults.engine !== 'rules' || saved.engine !== 'rules') return { ruleCount: 0, overrides: [] as StageOverride[], restructured: defaults.engine !== saved.engine, fallbackChanged: false } + const a = defaults.ruleset, b = saved.ruleset + const overrides: StageOverride[] = [] + let restructured = a.rules.length !== b.rules.length + if (!restructured) { + b.rules.forEach((rule, i) => { + const base = a.rules[i] + if (base.stage !== rule.stage || base.when.length !== rule.when.length) { + restructured = true + return + } + rule.when.forEach((condition, j) => { + const was = base.when[j] + if (was.metric !== condition.metric || was.op !== condition.op) restructured = true + else if (was.value !== condition.value) overrides.push({ rule: i + 1, stage: rule.stage, metric: condition.metric, op: condition.op, before: was.value, after: condition.value }) + }) + }) + } + return { ruleCount: b.rules.length, overrides, restructured, fallbackChanged: a.fallback !== b.fallback } +} + +/* ─── Backtests ────────────────────────────────────────────────────────── */ + +export type Split = 'tuning' | 'validation' | 'holdout' +export type ConfigLabel = 'default' | 'current' | 'candidate' +export interface JobLike { + id: string + experimentId: string + datasetId: string + split: Split + state: 'queued' | 'running' | 'cancel_requested' | 'cancelled' | 'succeeded' | 'failed' | 'interrupted' + createdAtMs: number + updatedAtMs: number + configurations: Array<{ label: string, revisionId: string | null, hash: string }> + evaluationScope: 'original_readings_comparison' | 'stage_rules_on_original_readings' | 'retained_waveform_replay' +} + +/** + * The latest run of the saved profile, as one job per split on the dataset + * that run used. With no saved revision (system defaults) there is no + * candidate to look for. + */ +export function savedBacktest(jobs: J[], savedRevisionId: string | null) { + if (!savedRevisionId) return null + const mine = jobs + .filter(job => job.configurations.some(c => c.label === 'candidate' && c.revisionId === savedRevisionId)) + .sort((a, b) => b.createdAtMs - a.createdAtMs) + if (!mine.length) return null + const datasetId = mine[0].datasetId + const bySplit: Partial> = {} + for (const job of mine) { + if (job.datasetId !== datasetId || job.state === 'cancelled') continue + const prior = bySplit[job.split] + // Prefer the newest finished run of a split over a newer failed retry. + if (!prior || (prior.state !== 'succeeded' && job.state === 'succeeded')) bySplit[job.split] = job + } + const jobsInGroup = Object.values(bySplit) as J[] + return { datasetId, experimentId: mine[0].experimentId, bySplit, latest: mine[0], jobs: jobsInGroup } +} + +export interface RunStatus { tone: DotTone, kind: 'done' | 'running' | 'queued' | 'failed', durationMs: number } + +export function runStatus(jobs: JobLike[]): RunStatus | null { + if (!jobs.length) return null + if (jobs.some(job => job.state === 'running' || job.state === 'cancel_requested')) return { tone: 'warn', kind: 'running', durationMs: 0 } + if (jobs.some(job => job.state === 'queued')) return { tone: 'warn', kind: 'queued', durationMs: 0 } + if (jobs.some(job => job.state === 'failed' || job.state === 'interrupted')) return { tone: 'danger', kind: 'failed', durationMs: 0 } + const start = Math.min(...jobs.map(job => job.createdAtMs)), end = Math.max(...jobs.map(job => job.updatedAtMs)) + return { tone: 'ok', kind: 'done', durationMs: end - start } +} + +/** Previous = the pinned configuration matching the previous profile, else the runtime baseline. */ +export function baselineLabel(job: JobLike, previousHash: string | null): ConfigLabel { + const byHash = previousHash ? job.configurations.find(c => c.hash === previousHash && c.label !== 'candidate') : undefined + if (byHash) return byHash.label as ConfigLabel + return job.configurations.some(c => c.label === 'current') ? 'current' : 'default' +} + +type Metric = 'heart_rate' | 'breathing_rate' | 'window_ibi_variability_v1' +export interface MetricScore { mae: number | null, paired: number, coverage: number | null } +export interface NightScore { + cohortId: string + split: Split + previous: Record + saved: Record + /** Saved configuration was paired with at least one heart-rate reference. */ + hasReference: boolean +} + +function score(evaluation: NumericReportView['nights'][number]['evaluations'][number] | undefined, metric: Metric): MetricScore { + let paired = 0, weighted = 0, coverage = 0, observations = 0 + for (const window of evaluation?.windows ?? []) { + const row = window.numeric.find(n => n.metric === metric) + if (!row) continue + if (row.status === 'compared' && row.metrics?.mae != null && row.pairedCount > 0) { + paired += row.pairedCount + weighted += row.metrics.mae * row.pairedCount + } + if (row.outputCoverage != null && window.observationCount > 0) { + coverage += row.outputCoverage * window.observationCount + observations += window.observationCount + } + } + return { mae: paired ? weighted / paired : null, paired, coverage: observations ? coverage / observations : null } +} + +const METRICS: Metric[] = ['heart_rate', 'breathing_rate', 'window_ibi_variability_v1'] + +/** Per-night agreement of previous and saved from one split's report. */ +export function nightScores(report: NumericReportView, split: Split, previous: ConfigLabel): NightScore[] { + return report.nights.map((night) => { + const prev = night.evaluations.find(e => e.label === previous) + const saved = night.evaluations.find(e => e.label === 'candidate') + const p = Object.fromEntries(METRICS.map(m => [m, score(prev, m)])) as Record + const s = Object.fromEntries(METRICS.map(m => [m, score(saved, m)])) as Record + return { cohortId: night.cohortId, split, previous: p, saved: s, hasReference: s.heart_rate.paired > 0 } + }) +} + +const mean = (values: number[]) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : null + +/** Equal-night mean error; nights without a reference are excluded. */ +export function aggregate(nights: NightScore[], metric: Metric, split?: Split) { + const rows = nights.filter(n => (!split || n.split === split) && n.previous[metric].mae !== null && n.saved[metric].mae !== null) + return { + previous: mean(rows.map(n => n.previous[metric].mae as number)), + saved: mean(rows.map(n => n.saved[metric].mae as number)), + nights: rows.length, + } +} + +export function coverage(nights: NightScore[]) { + const pick = (side: 'previous' | 'saved') => mean(nights.flatMap(n => n[side].heart_rate.coverage === null ? [] : [n[side].heart_rate.coverage as number])) + return { previous: pick('previous'), saved: pick('saved') } +} + +/** + * Lower is better for errors; the colour only marks a real difference. The + * threshold is in the metric's own unit: 0.05 for errors (bpm, ms, br/min), + * 0.005 (half a point) for fractions such as coverage. + */ +export function compareTone(previous: number | null, saved: number | null, higherIsBetter = false, threshold = higherIsBetter ? 0.005 : 0.05): 'better' | 'worse' | 'same' { + if (previous === null || saved === null) return 'same' + const delta = higherIsBetter ? previous - saved : saved - previous + if (delta < -threshold) return 'better' + if (delta > 0) return 'worse' + return 'same' +} + +/** 0–5 bpm → 0–72 px. */ +export function barHeight(mae: number | null, px = 72, top = 5): number { + if (mae === null) return 0 + return Math.round(Math.max(0, Math.min(1, mae / top)) * px) +} + +/* ─── Nights ───────────────────────────────────────────────────────────── */ + +/** "2026-10-11" → { weekday: 'Sun', day: 11 }, read as a calendar date. */ +export function nightLabel(cohortId: string) { + const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(cohortId) + if (!m) return { weekday: cohortId, day: '' } + const date = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))) + return { weekday: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getUTCDay()], day: String(date.getUTCDate()) } +} + +/** Reference sources whose time span overlaps the night, e.g. "watch + ring". */ +export function nightSources(night: { startMs: number, endMs: number }, imports: Array<{ source: string, startMs: number | null, endMs: number | null }>) { + const sources = imports + .filter(i => i.startMs !== null && i.endMs !== null && i.startMs < night.endMs && i.endMs > night.startMs) + .map(i => i.source) + return [...new Set(sources)].join(' + ') +} + +/* ─── Trace ────────────────────────────────────────────────────────────── */ + +export const TRACE = { width: 364, height: 150, minBpm: 40, maxBpm: 100 } + +/** SVG path for timestamped values; gaps over `gapMs` break the line. */ +export function tracePath(points: Array<{ t: number, v: number | null }>, startMs: number, endMs: number, gapMs = 10 * 60_000) { + const span = Math.max(1, endMs - startMs) + let d = '', last: number | null = null + for (const p of points) { + if (p.v === null || p.t < startMs || p.t > endMs) { + last = null + continue + } + const x = ((p.t - startMs) / span) * TRACE.width + const clamped = Math.max(TRACE.minBpm, Math.min(TRACE.maxBpm, p.v)) + const y = TRACE.height - ((clamped - TRACE.minBpm) / (TRACE.maxBpm - TRACE.minBpm)) * TRACE.height + d += `${last === null || p.t - last > gapMs ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)} ` + last = p.t + } + return d.trim() +} + +/** Session start, every two hours on the hour, session end. */ +export function traceTicks(startMs: number, endMs: number) { + const ticks = [startMs] + const hour = 3_600_000 + let t = Math.ceil(startMs / (2 * hour)) * 2 * hour + for (; t < endMs; t += 2 * hour) { + if (t - startMs > 40 * 60_000 && endMs - t > 40 * 60_000) ticks.push(t) + } + ticks.push(endMs) + return ticks +} + +/* ─── Diffs ────────────────────────────────────────────────────────────── */ + +/** "trackingToleranceBpm 15 → 12" for each numeric parameter that moved. */ +export function paramDiffs(before: BiometricConfig, after: BiometricConfig) { + return PARAM_PATHS + .filter(path => readParam(before, path) !== readParam(after, path)) + .map(path => `${PARAMS[path].key} ${readParam(before, path)} → ${readParam(after, path)}`) +} + +/* ─── Clock labels ─────────────────────────────────────────────────────── */ + +function zoned(ms: number, timeZone: string) { + try { + const parts = new Intl.DateTimeFormat('en-US', { timeZone, hour: 'numeric', minute: '2-digit', hourCycle: 'h23', month: 'short', day: 'numeric' }).formatToParts(ms) + const get = (type: string) => parts.find(p => p.type === type)?.value ?? '' + return { hour: Number(get('hour')), minute: Number(get('minute')), month: get('month'), day: get('day') } + } + catch { + return zoned(ms, 'UTC') + } +} + +/** Axis label: "11:15p", "1a" (12h) or "23:15", "01:00" (24h). */ +export function clockLabel(ms: number, timeZone: string, h24: boolean) { + const { hour, minute } = zoned(ms, timeZone) + const mm = String(minute).padStart(2, '0') + if (h24) return `${String(hour).padStart(2, '0')}:${mm}` + const h = hour % 12 || 12 + return `${h}${minute ? `:${mm}` : ''}${hour < 12 ? 'a' : 'p'}` +} + +/** Relative under a day, else "Oct 9 · 11:43 PM". */ +export function activityTime(ms: number, nowMs: number, timeZone: string, h24: boolean) { + if (nowMs - ms < 86_400_000) return relativeTime(ms, nowMs) + const { hour, minute, month, day } = zoned(ms, timeZone) + const mm = String(minute).padStart(2, '0') + const clock = h24 ? `${String(hour).padStart(2, '0')}:${mm}` : `${hour % 12 || 12}:${mm} ${hour < 12 ? 'AM' : 'PM'}` + return `${month} ${day} · ${clock}` +} diff --git a/src/components/biometrics/Calibration/tests/BiometricCalibration.test.tsx b/src/components/biometrics/Calibration/tests/BiometricCalibration.test.tsx index 77c6928e..9c022292 100644 --- a/src/components/biometrics/Calibration/tests/BiometricCalibration.test.tsx +++ b/src/components/biometrics/Calibration/tests/BiometricCalibration.test.tsx @@ -1,78 +1,321 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render, screen, within } from '@testing-library/react' import { setupI18n } from '@lingui/core' import { I18nProvider } from '@lingui/react' import { beforeEach, expect, it, vi } from 'vitest' +import { defaultConfig } from '@/src/biometrics/algorithms/registry-core' import { BiometricCalibration } from '../BiometricCalibration' -const mock = vi.hoisted(() => ({ overview: undefined as unknown, overviewError: null as Error | null, activation: undefined as unknown, - history: { items: [] as unknown[], nextGeneration: null as number | null }, historyQuery: vi.fn(), sideChange: vi.fn() })) -vi.mock('@/src/utils/trpc', () => ({ trpc: { - biometricTuning: { - getProfileOverview: { useQuery: () => ({ data: mock.overview, error: mock.overviewError }) }, - getActivationStatus: { useQuery: () => ({ data: mock.activation }) }, - listActivationHistory: { useQuery: (input: unknown) => { - mock.historyQuery(input) - return { data: mock.history } - } }, - }, - settings: { getAll: { useQuery: () => ({ data: { device: { timezone: 'UTC' } } }) } }, -} })) -vi.mock('../ProfileRecovery', () => ({ ProfileRecovery: () =>

Recovery controls

})) + +const HASH = 'a4f2'.padEnd(62, '0') + '9c' +const mock = vi.hoisted(() => ({ + now: 0, + overview: undefined as unknown, + overviewError: null as Error | null, + revisions: {} as Record, + status: undefined as unknown, + history: { items: [] as unknown[], nextGeneration: null }, + jobs: [] as unknown[], + summaries: [] as unknown[], + datasets: [] as unknown[], + imports: [] as unknown[], + diffData: undefined as unknown, + traces: {} as Record, + referenceTrace: undefined as unknown, + create: vi.fn(), + activate: vi.fn(), + enqueue: vi.fn(), + diff: vi.fn(), + reset: vi.fn(), + rollback: vi.fn(), + sideChange: vi.fn(), +})) +vi.mock('@/src/utils/trpc', () => { + const query = (data: () => unknown) => ({ useQuery: () => ({ data: data(), error: null }) }) + const mutation = (fn: (input: unknown) => unknown, data: () => unknown = () => undefined) => ({ useMutation: () => ({ mutateAsync: async (input: unknown) => fn(input), mutate: fn, isPending: false, error: null, data: data() }) }) + return { trpc: { + useUtils: () => new Proxy({}, { get: () => new Proxy({}, { get: () => ({ invalidate: vi.fn() }) }) }), + useQueries: () => mock.summaries.map(data => ({ data })), + settings: { getAll: query(() => ({ device: { timezone: 'UTC' }, sides: { left: { name: 'Alex' }, right: { name: 'Sam' } } })) }, + biometricTuning: { + getProfileOverview: { useQuery: () => ({ data: mock.overview, error: mock.overviewError }) }, + getProfileRevision: { useQuery: ({ id }: { id: string }) => ({ data: mock.revisions[id], error: null }) }, + getActivationStatus: query(() => mock.status), + listActivationHistory: query(() => mock.history), + listProfileRevisions: query(() => ({ items: Object.values(mock.revisions), nextCursor: null })), + listBacktests: query(() => ({ items: mock.jobs, nextCursor: null })), + listDatasets: query(() => ({ items: mock.datasets, nextCursor: null })), + listImports: query(() => ({ items: mock.imports, nextCursor: null })), + getDataset: query(() => undefined), + getBacktestNumericTrace: { useQuery: ({ configuration }: { configuration: string }) => ({ data: mock.traces[configuration], error: null }) }, + getBacktestReferenceTrace: query(() => mock.referenceTrace), + createProfileRevision: mutation(mock.create), + activateProfile: mutation(mock.activate), + enqueueBacktest: mutation(mock.enqueue), + diffProfileRevision: mutation(mock.diff, () => mock.diffData), + resetProfile: mutation(mock.reset), + rollbackProfile: mutation(mock.rollback), + }, + } } +}) +vi.mock('next/navigation', () => ({ usePathname: () => '/en/settings' })) +vi.mock('@/src/providers/TimeFormatProvider', () => ({ useTimeFormat: () => '12h' })) +vi.mock('../ProfileLibrary', () => ({ ProfileLibrary: ({ side }: { side: string }) =>

{`Revision library ${side}`}

})) vi.mock('../ShadowRuns', () => ({ ShadowRuns: ({ side }: { side: string }) =>

{`Prospective runs ${side}`}

})) -vi.mock('../ProfileLibrary', () => ({ ProfileLibrary: () =>

Candidate editor

})) -vi.mock('@/src/hooks/useSideNames', () => ({ useSideNames: () => ({ leftName: 'Left', rightName: 'Right' }) })) -vi.mock('@/src/components/Settings/SleepStageRules', () => ({ SleepStageRules: ({ side }: { side: string }) =>

{`Existing stage editor ${side}`}

})) +vi.mock('../CalibrationReferences', () => ({ CalibrationReferences: () =>

Reference import

})) +vi.mock('../ReferenceLibrary', () => ({ ReferenceLibrary: () => null })) +vi.mock('../CalibrationBacktests', () => ({ CalibrationBacktests: () =>

Dataset builder

})) + const i18n = setupI18n({ locale: 'en', messages: {} }) const view = (side: 'left' | 'right' = 'left') => +const min = 60_000 +const window = (mae: number) => ({ startMs: 0, endMs: 1, observationCount: 10, numeric: [{ metric: 'heart_rate', status: 'compared', metrics: { mae, medianAbsoluteError: mae, p95AbsoluteError: mae, signedBias: 0 }, + pairedCount: 10, referenceCoverage: 1, outputCoverage: 0.9, unmatchedReferences: 0, unmatchedObservations: 0, exclusions: {} }] }) +const night = (cohortId: string, previous: number, saved: number) => ({ cohortId, evaluations: [ + { label: 'current', configHash: 'h-current', windows: [window(previous)] }, + { label: 'candidate', configHash: 'h-saved', windows: [window(saved)] }, +] }) + beforeEach(() => { vi.clearAllMocks() + mock.now = Date.now() + const saved = structuredClone(defaultConfig) + saved.heart_rate.trackingToleranceBpm = 12 mock.overviewError = null - mock.overview = { desired: { isDefault: false, hash: 'exact-config', binding: { generation: 2 }, warning: 'Using compatible fallback' }, - runtimeHealth: { state: 'stale', components: [{ component: 'heart_rate', state: 'stale' }] }, - stageView: { provenance: { warning: null } }, sensorCalibration: [], limitations: [] } - mock.activation = { observedState: 'pending_boundary' } - mock.history = { items: [], nextGeneration: null } + mock.overview = { desired: { binding: { side: 'left', generation: 3, desiredRevisionId: 'saved-id', previousRevisionId: null, updatedAtMs: mock.now - 14 * min }, + resolved: saved, hash: HASH, isDefault: false, warning: null }, runtimeStatus: 'receipts_available', sensorCalibration: [] } + mock.revisions = { 'saved-id': { id: 'saved-id', side: 'left', origin: 'agent', requester: 'tuner', overrides: [{ path: 'heart_rate.trackingToleranceBpm', value: 12 }], + resolved: saved, resolvedHash: 'h-saved', createdAtMs: mock.now - 16 * min } } + const event = { id: 'event', side: 'left', generation: 3, action: 'activate', previousRevisionId: null, nextRevisionId: 'saved-id', previousHash: 'h-default', nextHash: 'h-saved', + requester: 'tuner', state: 'pending_boundary', reason: null, createdAtMs: mock.now - 14 * min, updatedAtMs: mock.now - 14 * min } + mock.status = { desiredGeneration: 3, isCurrent: true, event, observedState: 'pending_boundary' } + mock.history = { items: [event], nextGeneration: null } + const configurations = [{ label: 'default', revisionId: null, hash: 'h-default' }, { label: 'current', revisionId: null, hash: 'h-current' }, { label: 'candidate', revisionId: 'saved-id', hash: 'h-saved' }] + mock.jobs = [ + { id: 'b42a-tuning', experimentId: 'exp', datasetId: 'ds', split: 'tuning', state: 'succeeded', createdAtMs: mock.now - 15 * min, updatedAtMs: mock.now - 15 * min + 38_000, configurations, evaluationScope: 'original_readings_comparison', error: null }, + { id: 'b42b-holdout', experimentId: 'exp', datasetId: 'ds', split: 'holdout', state: 'succeeded', createdAtMs: mock.now - 15 * min, updatedAtMs: mock.now - 15 * min + 30_000, configurations, evaluationScope: 'original_readings_comparison', error: null }, + ] + mock.summaries = [ + { contentHash: 'c1', available: true, report: { nights: [night('2026-10-10', 3.1, 2.4), night('2026-10-11', 2.9, 2.2)] } }, + { contentHash: 'c2', available: true, report: { nights: [night('2026-10-12', 3.4, 2.6)] } }, + ] + mock.datasets = [{ id: 'ds', side: 'left', available: true, createdAtMs: Date.UTC(2026, 9, 12), byteLength: 2.1 * 1024 * 1024, manifest: { splitProtocol: { assignments: [ + { cohortId: '2026-10-10', split: 'tuning', startMs: 1, endMs: 2, nightLabels: [], sessionIds: [1] }, + { cohortId: '2026-10-11', split: 'tuning', startMs: 3, endMs: 4, nightLabels: [], sessionIds: [2] }, + { cohortId: '2026-10-12', split: 'holdout', startMs: 5, endMs: 6, nightLabels: [], sessionIds: [3] }, + ] } } }] + mock.imports = [{ id: 'imp', available: true, createdAtMs: mock.now - 2 * 86_400_000, summary: { source: 'watch', startMs: 0, endMs: 10, accepted: 120 } }] + mock.create.mockResolvedValue({ id: 'new-id', resolvedHash: 'n'.repeat(64) }) + mock.activate.mockResolvedValue({ id: 'event-2', generation: 4 }) + mock.diff.mockImplementation(() => undefined) + mock.diffData = undefined + mock.traces = {} + mock.referenceTrace = undefined }) -it('distinguishes saved personalization, pending adoption and stale health', () => { + +it('shows who saved what, that it is still waiting, and what the processor runs', () => { render(view()) - expect(screen.getByText('Personalized profile')).toBeTruthy() - expect(screen.getByText('Waiting for a processing boundary')).toBeTruthy() - expect(screen.getAllByText('Stale')).toHaveLength(2) - expect(screen.queryByText('Applied')).toBeNull() - expect(screen.getByText('Using compatible fallback')).toBeTruthy() - expect(screen.getByText('No sensor calibration has been reported for this side.')).toBeTruthy() - expect(screen.getByText(/not clinical beat-to-beat RMSSD/)).toBeTruthy() -}) -it('reuses the existing stage editor and keeps manual activation semantics visible', () => { + const strip = screen.getByRole('group', { name: 'Profile lifecycle' }) + expect(within(strip).getByText('by tuner · 16 min ago')).toBeTruthy() + expect(within(strip).getByText('waiting 14 min')).toBeTruthy() + expect(within(strip).getByText('running system defaults')).toBeTruthy() + expect(screen.getByText('trackingToleranceBpm · previous: 15 · range 5–30')).toBeTruthy() + expect(screen.getByText(/window IBI variability, not clinical RMSSD/)).toBeTruthy() + expect(screen.getByText('Measured sensor calibration for this side:', { exact: false })).toBeTruthy() + expect(screen.getByText('a4f2…9c')).toBeTruthy() + expect((screen.getByRole('button', { name: 'Keep saved profile' }) as HTMLButtonElement).disabled).toBe(true) + expect(screen.getByText('Nothing changes until you move a control or restore.')).toBeTruthy() +}) + +it('reads the backtest per night and per split, with holdout the one to trust', () => { render(view()) - fireEvent.click(screen.getByRole('button', { name: 'Profiles' })) - expect(screen.getByText('Existing stage editor left')).toBeTruthy() - expect(screen.getByText(/unvalidated manual settings/)).toBeTruthy() + expect(screen.getByText('done · 38 s')).toBeTruthy() + expect(screen.getByRole('button', { name: 'Mon 12: previous 3.4, saved 2.6 bpm' })).toBeTruthy() + expect(screen.getByText('HR error · holdout')).toBeTruthy() + expect(screen.getAllByText('3.4').length).toBeGreaterThan(0) + expect(screen.getByText('frozen Oct 12 · 2.1 MB')).toBeTruthy() + expect(screen.getAllByText('watch').length).toBe(3) + expect(screen.getByText('holdout −0.8 bpm · covered 90%')).toBeTruthy() + expect(screen.getAllByText('unchanged').length).toBe(3) + const nights = screen.getByRole('listbox', { name: 'Dataset nights' }) + fireEvent.click(within(nights).getByRole('option', { name: /Sat 10/ })) + expect(within(nights).getByRole('option', { name: /Sat 10/ }).getAttribute('aria-selected')).toBe('true') + expect(screen.getByText(/compared stored readings/)).toBeTruthy() +}) + +it('lists agent and pod activity newest first', () => { + render(view()) + const activity = screen.getByText('Activity').closest('div')?.parentElement as HTMLElement + const items = within(activity).getAllByRole('listitem').map(li => li.textContent) + expect(items[0]).toContain('Requested activation · generation 3') + expect(items.some(text => text?.includes('tuner Saved the profile') && text.includes('trackingToleranceBpm 15 → 12'))).toBe(true) + expect(items.some(text => text?.includes('Finished a backtest on holdout nights') && text.includes('holdout HR error 3.4 → 2.6 bpm'))).toBe(true) +}) + +it('saves a moved control as a new revision, requests it, and re-runs tuning and validation', async () => { + render(view()) + fireEvent.change(screen.getByLabelText('Minimum IBI windows'), { target: { value: '14' } }) + expect(screen.getByText('tested on save')).toBeTruthy() + expect(screen.getByText('Overwrites the saved profile and requests it; the current one becomes previous.')).toBeTruthy() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Save and request' })) + }) + expect(mock.create).toHaveBeenCalledWith(expect.objectContaining({ side: 'left', parentRevisionId: 'saved-id', origin: 'manual', requester: 'web-calibration', + overrides: [{ path: 'heart_rate.trackingToleranceBpm', value: 12 }, { path: 'hrv_index.minimumWindows', value: 14 }] })) + expect(mock.activate).toHaveBeenCalledWith(expect.objectContaining({ revisionId: 'new-id', expectedGeneration: 3, expectedResolvedHash: 'n'.repeat(64), boundary: 'next_session', allowUnvalidated: true })) + expect(mock.enqueue.mock.calls.map(([input]) => [input.split, input.experimentId, input.expectedBindingGeneration, input.candidateRevisionId])).toEqual([['tuning', 'exp', 4, 'new-id']]) }) -it('opens prospective observations for the selected side', () => { - const rendered = render(view()) + +it('reviews a restore before requesting system defaults', async () => { + const result = render(view()) + fireEvent.click(screen.getByRole('button', { name: 'Restore… ▾' })) + fireEvent.click(screen.getByRole('menuitem', { name: 'System defaults' })) + expect(mock.diff).toHaveBeenCalledWith({ side: 'left', revisionId: null, againstRevisionId: 'saved-id' }) + expect(screen.getByRole('dialog', { name: 'Restore system defaults' })).toBeTruthy() + expect((screen.getByRole('button', { name: 'Restore and request' }) as HTMLButtonElement).disabled).toBe(true) + result.unmount() +}) + +it('opens workbench views from the page and returns', () => { + render(view()) + fireEvent.click(screen.getByRole('button', { name: 'Import reference' })) + expect(screen.getByText('Reference import')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'Backtests' })) + expect(screen.getByText('Dataset builder')).toBeTruthy() fireEvent.click(screen.getByRole('button', { name: 'Observations' })) expect(screen.getByText('Prospective runs left')).toBeTruthy() - rendered.rerender(view('right')) - expect(screen.getByText('Prospective runs right')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'Profiles' })) + expect(screen.getByText('Revision library left')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'Calibration' })) + expect(screen.getByRole('group', { name: 'Profile lifecycle' })).toBeTruthy() }) -it('resets history pagination on side changes and labels historical acknowledgements', () => { - mock.history = { items: [{ id: 'event', generation: 4, action: 'rollback', state: 'applied', createdAtMs: 1791673200000, - previousHash: 'before', nextHash: 'after', requester: 'fixture', reason: null }], nextGeneration: 4 } - const result = render(view()) - fireEvent.click(screen.getByRole('button', { name: 'History' })) - expect(screen.getByText(/Past acknowledgements/)).toBeTruthy() - expect(screen.getByText('Restore previous profile')).toBeTruthy() - expect(screen.getByText('Times shown in UTC')).toBeTruthy() - fireEvent.click(screen.getByRole('button', { name: 'Older changes' })) - expect(mock.historyQuery).toHaveBeenLastCalledWith({ side: 'left', beforeGeneration: 4 }) - result.rerender(view('right')) - expect(mock.historyQuery).toHaveBeenLastCalledWith({ side: 'right', beforeGeneration: undefined }) -}) -it('shows loading or errors without substituting healthy state', () => { + +it('shows loading or errors without substituting a healthy state', () => { mock.overview = undefined const result = render(view()) - expect(screen.queryByText('Applied')).toBeNull() + expect(screen.queryByRole('group', { name: 'Profile lifecycle' })).toBeNull() mock.overviewError = new Error('Processor data unavailable') result.rerender(view()) expect(screen.getByText('Processor data unavailable')).toBeTruthy() }) + +it('confirms a reviewed restore of the previous profile with the reviewed hash', async () => { + mock.overview = { ...(mock.overview as object), desired: { ...(mock.overview as { desired: object }).desired, + binding: { side: 'left', generation: 3, desiredRevisionId: 'saved-id', previousRevisionId: 'saved-id', updatedAtMs: mock.now } } } + mock.diffData = { afterHash: 'p'.repeat(64), changes: [{ path: 'heart_rate.trackingToleranceBpm', before: 12, after: 15 }, { path: 'sleep_stages.ruleset.fallback', before: 'deep', after: 'light' }] } + render(view()) + fireEvent.click(screen.getByRole('button', { name: 'Restore previous' })) + const dialog = screen.getByRole('dialog', { name: 'Restore the previous profile' }) + expect(within(dialog).getByText('heart_rate.trackingToleranceBpm')).toBeTruthy() + expect(within(dialog).getByText('12 → 15')).toBeTruthy() + expect(within(dialog).getByText('1 sleep-stage values')).toBeTruthy() + await act(async () => { + fireEvent.click(within(dialog).getByRole('button', { name: 'Restore and request' })) + }) + expect(mock.rollback).toHaveBeenCalledWith(expect.objectContaining({ side: 'left', revisionId: 'saved-id', expectedGeneration: 3, + expectedResolvedHash: 'p'.repeat(64), boundary: 'next_session', allowUnvalidated: true })) + expect(screen.queryByRole('dialog')).toBeNull() +}) + +it('restores defaults from the menu and says when nothing would change', async () => { + mock.diffData = { afterHash: 'd'.repeat(64), changes: [] } + render(view()) + fireEvent.click(screen.getByRole('button', { name: 'Restore… ▾' })) + fireEvent.keyDown(document, { key: 'Escape' }) + expect(screen.queryByRole('menu')).toBeNull() + fireEvent.click(screen.getByRole('button', { name: 'Restore… ▾' })) + fireEvent.click(screen.getByRole('menuitem', { name: 'System defaults' })) + expect(screen.getByText('Identical to the saved profile; restoring only re-requests it.')).toBeTruthy() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Restore and request' })) + }) + expect(mock.reset).toHaveBeenCalledWith(expect.objectContaining({ expectedResolvedHash: 'd'.repeat(64), expectedGeneration: 3 })) + fireEvent.click(screen.getByRole('button', { name: 'Restore… ▾' })) + fireEvent.mouseDown(document.body) + expect(screen.queryByRole('menu')).toBeNull() +}) + +it('draws the selected night from recomputed and reference traces', () => { + for (const job of mock.jobs as Array<{ evaluationScope: string }>) job.evaluationScope = 'retained_waveform_replay' + const at = (minutes: number) => Date.UTC(2026, 9, 12, 7) + minutes * min + const readings = (bpm: number) => ({ items: [0, 1, 2, 30].map(m => ({ timestampMs: at(m), heartRate: bpm, side: 'left', qualityScore: 0.84 })), nextCursor: null }) + mock.traces = { candidate: readings(62), current: readings(66) } + mock.referenceTrace = { items: [{ startMs: at(0), value: 61, compatibility: null }, { startMs: at(1), value: null, compatibility: 'unscorable' }], nextCursor: { offset: 500 } } + const assignments = (mock.datasets[0] as { manifest: { splitProtocol: { assignments: Array<{ startMs: number, endMs: number }> } } }).manifest.splitProtocol.assignments + for (const a of assignments) Object.assign(a, { startMs: at(-45), endMs: at(8 * 60) }) + ;(mock.imports[0] as { summary: object }).summary = { source: 'watch', startMs: at(-60), endMs: at(9 * 60), accepted: 1 } + const { container } = render(view()) + expect(screen.getByText('Heart rate · Mon 12')).toBeTruthy() + expect(screen.getByText('watch · quality 0.84')).toBeTruthy() + expect(screen.getByText('first 500 readings')).toBeTruthy() + const paths = [...container.querySelectorAll('svg[aria-label="Heart-rate trace"] path')].map(p => p.getAttribute('d')) + expect(paths.every(d => d && d.startsWith('M'))).toBe(true) + expect(screen.getByText('6:15a')).toBeTruthy() +}) + +it('explains missing evidence instead of drawing empty charts', () => { + mock.jobs = [] + mock.summaries = [] + mock.datasets = [] + mock.imports = [] + mock.history = { items: [], nextGeneration: null } + mock.revisions = {} + mock.overview = { ...(mock.overview as object), desired: { ...(mock.overview as { desired: object }).desired, + binding: { side: 'left', generation: 0, desiredRevisionId: null, previousRevisionId: null, updatedAtMs: 0 } }, runtimeStatus: 'unreported', + sensorCalibration: [{ id: 1, sensorType: 'piezo', status: 'completed', freshness: 'expired' }] } + mock.status = { desiredGeneration: 0, isCurrent: false, event: null, observedState: 'unreported' } + render(view()) + expect(screen.getByText(/No backtest has replayed the saved profile yet/)).toBeTruthy() + expect(screen.getByText(/No dataset is frozen for this side yet/)).toBeTruthy() + expect(screen.getByText('Nothing has happened on this side yet.')).toBeTruthy() + expect(screen.getByText('piezo expired')).toBeTruthy() + expect((screen.getByRole('button', { name: 'Restore previous' }) as HTMLButtonElement).disabled).toBe(true) + const strip = screen.getByRole('group', { name: 'Profile lifecycle' }) + expect(within(strip).getByText('not reported')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'show' })) + expect(screen.getByText(HASH)).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'hide' })) + expect(screen.getByText('a4f2…9c')).toBeTruthy() +}) + +it('reports in-flight and failed runs and pages through long activity', () => { + ;(mock.jobs[0] as { state: string }).state = 'running' + const extra = Array.from({ length: 6 }, (_, i) => ({ id: `ds-${i}`, side: 'left', available: true, createdAtMs: mock.now - (i + 2) * 86_400_000, byteLength: 10, + manifest: { splitProtocol: { assignments: [] } } })) + mock.datasets = [...mock.datasets, ...extra] + const result = render(view()) + expect(screen.getByText('running')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: /Show all \d+/ })) + expect(screen.getByRole('button', { name: 'Show recent' })).toBeTruthy() + ;(mock.jobs[0] as { state: string }).state = 'failed' + ;(mock.jobs[0] as { error: string }).error = 'Worker exited' + result.rerender(view()) + expect(screen.getAllByText('failed').length).toBeGreaterThan(0) + expect(screen.getByText('Worker exited')).toBeTruthy() + ;(mock.jobs[0] as { state: string }).state = 'queued' + result.rerender(view()) + expect(screen.getByText('queued')).toBeTruthy() +}) + +it('surfaces a failed save without losing the edit', async () => { + mock.create.mockRejectedValue(new Error('STALE_BINDING: inspect the current profile')) + render(view()) + fireEvent.change(screen.getByLabelText('Tracking history length'), { target: { value: '7' } }) + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Save and request' })) + }) + expect(screen.getByText('STALE_BINDING: inspect the current profile')).toBeTruthy() + expect(screen.getByRole('button', { name: 'Save and request' })).toBeTruthy() + expect(mock.activate).not.toHaveBeenCalled() +}) + +it('edits both breathing cut-offs on one axis, each kept inside its own range', () => { + render(view()) + const shortest = screen.getByLabelText('Shortest breathing interval (× median)') as HTMLInputElement + const longest = screen.getByLabelText('Longest breathing interval (× median)') as HTMLInputElement + expect([shortest.min, shortest.max, longest.min, longest.max]).toEqual(['0.25', '2', '0.25', '2']) + fireEvent.change(shortest, { target: { value: '1.4' } }) + fireEvent.change(longest, { target: { value: '0.6' } }) + expect(screen.getByText('0.9×')).toBeTruthy() + expect(screen.getByText('1.1×')).toBeTruthy() + expect(screen.getByText('1×')).toBeTruthy() + expect(screen.getByRole('button', { name: 'Save and request' })).toBeTruthy() +}) diff --git a/src/components/biometrics/Calibration/tests/CalibrationOverview.test.tsx b/src/components/biometrics/Calibration/tests/CalibrationOverview.test.tsx deleted file mode 100644 index b587ee5a..00000000 --- a/src/components/biometrics/Calibration/tests/CalibrationOverview.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { setupI18n } from '@lingui/core' -import { I18nProvider } from '@lingui/react' -import { beforeEach, expect, it, vi } from 'vitest' -import { CalibrationOverview } from '../CalibrationOverview' -const mock = vi.hoisted(() => ({ overview: { data: undefined as unknown, error: null as Error | null }, activation: { data: undefined as unknown, error: null as Error | null } })) -vi.mock('@/src/utils/trpc', () => ({ trpc: { biometricTuning: { - getProfileOverview: { useQuery: () => mock.overview }, - getActivationStatus: { useQuery: () => mock.activation }, -} } })) -const i18n = setupI18n({ locale: 'en', messages: {} }) -const element = () => -beforeEach(() => { - mock.overview = { data: { desired: { isDefault: true, hash: 'hash', binding: { generation: 1 }, warning: null }, - runtimeHealth: { state: 'current', components: [{ component: 'heart_rate', state: 'current' }, { component: 'custom', state: 'failed' }] }, - stageView: { provenance: { warning: 'Recomputed stages' } }, - sensorCalibration: [{ id: 'a', sensorType: 'piezo', status: 'ok', freshness: 'current' }, { id: 'b', sensorType: 'cap', status: 'ok', freshness: 'expired' }, { id: 'c', sensorType: 'temp', status: 'ok', freshness: 'other' }], - limitations: ['Limited to one night'] }, error: null } - mock.activation = { data: undefined, error: new Error('Activation unavailable') } -}) -it('shows the error or skeleton before data arrives', () => { - mock.overview = { data: undefined, error: new Error('Overview unavailable') } - const result = render(element()) - expect(screen.getByText('Overview unavailable')).toBeTruthy() - mock.overview = { data: undefined, error: null } - result.rerender(element()) - expect(screen.queryByText('Overview unavailable')).toBeNull() - expect(screen.queryByText('Saved profile')).toBeNull() -}) -it('falls back to raw component names, activation errors and sensor freshness labels', () => { - render(element()) - expect(screen.getByText('System defaults')).toBeTruthy() - expect(screen.getByText('Not reported')).toBeTruthy() - expect(screen.getByText('Activation unavailable')).toBeTruthy() - expect(screen.getByText('Heart rate')).toBeTruthy() - expect(screen.getByText('custom')).toBeTruthy() - expect(screen.getByText('Failed')).toBeTruthy() - expect(screen.getByText('Recomputed stages')).toBeTruthy() - expect(screen.getByText('Expired')).toBeTruthy() - expect(screen.getByText('Freshness unknown')).toBeTruthy() - expect(screen.getAllByText('Current')).toHaveLength(3) - expect(screen.getByText('Limited to one night')).toBeTruthy() - expect(screen.getByText('Generation 1')).toBeTruthy() -}) diff --git a/src/components/biometrics/Calibration/tests/ProfileRecovery.test.tsx b/src/components/biometrics/Calibration/tests/ProfileRecovery.test.tsx deleted file mode 100644 index c35b6f0a..00000000 --- a/src/components/biometrics/Calibration/tests/ProfileRecovery.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import { setupI18n } from '@lingui/core' -import { I18nProvider } from '@lingui/react' -import { beforeEach, expect, it, vi } from 'vitest' -import { ProfileRecovery } from '../ProfileRecovery' -const mock = vi.hoisted(() => ({ generation: 3, previousRevisionId: 'previous-id' as string | null, comparison: vi.fn(), activation: vi.fn(), warning: null as string | null, hash: 'default-hash' })) -vi.mock('@/src/utils/trpc', () => ({ trpc: { biometricTuning: { getProfileOverview: { useQuery: () => ({ data: { desired: { warning: mock.warning, hash: mock.hash, binding: { generation: mock.generation, desiredRevisionId: 'unavailable-revision', previousRevisionId: mock.previousRevisionId } } } }) } } } })) -vi.mock('../ProfileComparison', () => ({ ProfileComparison: (props: unknown) => { - mock.comparison(props) - return

Review changes

-} })) -vi.mock('../ManualProfileActivation', () => ({ ManualProfileActivation: (props: unknown) => { - mock.activation(props) - return

Explicit reset controls

-} })) -const i18n = setupI18n({ locale: 'en', messages: {} }) -beforeEach(() => { - vi.clearAllMocks() - mock.warning = null - mock.hash = 'default-hash' - mock.generation = 3 - mock.previousRevisionId = 'previous-id' -}) -const view = () => render() -it('selects defaults separately from the exact previous revision without making writes', () => { - view() - fireEvent.click(screen.getByRole('button', { name: 'Review previous profile' })) - expect(mock.comparison).toHaveBeenLastCalledWith({ side: 'left', action: 'rollback', revisionId: 'previous-id', selectionGeneration: 3 }) - fireEvent.click(screen.getByRole('button', { name: 'Review system defaults' })) - expect(mock.comparison).toHaveBeenLastCalledWith({ side: 'left', action: 'reset', revisionId: null, selectionGeneration: 3 }) -}) -it('disables rollback before any activation but allows rollback to previous defaults', () => { - mock.generation = 0 - const result = view() - expect((screen.getByRole('button', { name: 'Review previous profile' }) as HTMLButtonElement).disabled).toBe(true) - result.unmount() - mock.generation = 1 - mock.previousRevisionId = null - view() - fireEvent.click(screen.getByRole('button', { name: 'Review previous profile' })) - expect(mock.comparison).toHaveBeenLastCalledWith({ side: 'left', action: 'rollback', revisionId: null, selectionGeneration: 1 }) -}) - -it('reviews fallback defaults honestly and invalidates recovery after a concurrent generation change', () => { - mock.warning = 'Pinned engine unavailable' - const rendered = view() - fireEvent.click(screen.getByRole('button', { name: 'Review system defaults' })) - expect(screen.getByText(/a valid before-and-after comparison is unavailable/)).toBeTruthy() - expect(screen.getByText('unavailable-revision')).toBeTruthy() - expect(mock.comparison).not.toHaveBeenCalled() - expect(mock.activation).toHaveBeenLastCalledWith({ side: 'left', action: 'reset', revisionId: null, review: { generation: 3, hash: 'default-hash' } }) - mock.generation = 4 - mock.warning = null - rendered.rerender() - expect(mock.activation).toHaveBeenLastCalledWith({ side: 'left', action: 'reset', revisionId: null, review: undefined }) - expect(screen.getByText('Explicit reset controls')).toBeTruthy() -}) diff --git a/src/components/biometrics/Calibration/tests/calibration-model.test.ts b/src/components/biometrics/Calibration/tests/calibration-model.test.ts new file mode 100644 index 00000000..dde83a6b --- /dev/null +++ b/src/components/biometrics/Calibration/tests/calibration-model.test.ts @@ -0,0 +1,265 @@ +import { describe, expect, it } from 'vitest' +import { defaultConfig, type BiometricConfig } from '@/src/biometrics/algorithms/registry-core' +import { + activityTime, + actorOf, + aggregate, + barHeight, + baselineLabel, + changedPaths, + clockLabel, + compareTone, + coverage, + draftFrom, + lifecycle, + nightLabel, + nightScores, + overridesFor, + paramDiffs, + roundParam, + runStatus, + savedBacktest, + shortDuration, + stageOverrides, + tracePath, + traceTicks, + type JobLike, +} from '../calibration-model' +import type { NumericReportView } from '../report-view' + +const withConfig = (patch: (c: BiometricConfig) => void) => { + const c = structuredClone(defaultConfig) as BiometricConfig + patch(c) + return c +} +const job = (over: Partial): JobLike => ({ + id: 'j', experimentId: 'e', datasetId: 'd', split: 'tuning', state: 'succeeded', createdAtMs: 0, updatedAtMs: 1000, + configurations: [{ label: 'default', revisionId: null, hash: 'h-default' }, { label: 'current', revisionId: null, hash: 'h-current' }, { label: 'candidate', revisionId: 'saved', hash: 'h-saved' }], + evaluationScope: 'retained_waveform_replay', ...over, +}) + +describe('draft and overrides', () => { + it('reads the five tunable values and detects edits', () => { + const draft = draftFrom(defaultConfig) + expect(draft['heart_rate.trackingToleranceBpm']).toBe(15) + expect(changedPaths(draft, defaultConfig)).toEqual([]) + expect(changedPaths({ ...draft, 'hrv_index.minimumWindows': 12 }, defaultConfig)).toEqual(['hrv_index.minimumWindows']) + }) + + it('trims slider float noise to the parameter precision', () => { + expect(roundParam('breathing_rate.intervalLowerRatio', 0.1 + 0.2 + 0.15000000000000002)).toBe(0.45) + expect(roundParam('heart_rate.trackingHistoryLength', 4.6)).toBe(5) + }) + + it('keeps unrelated overrides, replaces edited ones and drops values back at default', () => { + const saved = [ + { path: 'sleep_stages.ruleset.rules.0.when.0.value', value: 240 }, + { path: 'heart_rate.trackingToleranceBpm', value: 12 }, + ] + const draft = { ...draftFrom(defaultConfig), 'heart_rate.trackingToleranceBpm': 15, 'hrv_index.minimumWindows': 14 } + expect(overridesFor(saved, draft, defaultConfig)).toEqual([ + { path: 'sleep_stages.ruleset.rules.0.when.0.value', value: 240 }, + { path: 'hrv_index.minimumWindows', value: 14 }, + ]) + }) + + it('describes numeric changes as key old → new', () => { + const after = withConfig((c) => { + c.heart_rate.trackingToleranceBpm = 12 + }) + expect(paramDiffs(defaultConfig, after)).toEqual(['trackingToleranceBpm 15 → 12']) + }) +}) + +describe('actors and time', () => { + it('classifies web, agent and pod requesters', () => { + expect(actorOf('web-calibration')).toEqual({ kind: 'you', name: 'you' }) + expect(actorOf('tuner', 'agent')).toEqual({ kind: 'agent', name: 'tuner' }) + expect(actorOf('tuner')).toEqual({ kind: 'agent', name: 'tuner' }) + expect(actorOf('anything', 'migration').kind).toBe('pod') + expect(actorOf('someone', 'manual').kind).toBe('you') + }) + + it('formats durations and relative times', () => { + expect(shortDuration(38_000)).toBe('38 s') + expect(shortDuration(14 * 60_000)).toBe('14 min') + expect(shortDuration(3 * 3_600_000)).toBe('3 h') + expect(shortDuration(3 * 86_400_000)).toBe('3 d') + expect(activityTime(1000, 1000 + 16 * 60_000, 'UTC', false)).toBe('16 min ago') + expect(activityTime(1000, 1000 + 10_000, 'UTC', false)).toBe('just now') + expect(activityTime(Date.UTC(2026, 9, 9, 23, 43), Date.UTC(2026, 9, 12), 'UTC', false)).toBe('Oct 9 · 11:43 PM') + expect(activityTime(Date.UTC(2026, 9, 9, 23, 43), Date.UTC(2026, 9, 12), 'UTC', true)).toBe('Oct 9 · 23:43') + }) + + it('labels clock ticks compactly in the device timezone', () => { + expect(clockLabel(Date.UTC(2026, 9, 12, 6, 15), 'America/Los_Angeles', false)).toBe('11:15p') + expect(clockLabel(Date.UTC(2026, 9, 12, 8, 0), 'America/Los_Angeles', false)).toBe('1a') + expect(clockLabel(Date.UTC(2026, 9, 12, 8, 0), 'America/Los_Angeles', true)).toBe('01:00') + expect(clockLabel(Date.UTC(2026, 9, 12, 8, 0), 'Not/AZone', true)).toBe('08:00') + }) +}) + +describe('lifecycle', () => { + const now = 10 * 3_600_000 + const base = { nowMs: now, bindingUpdatedAtMs: now - 60_000, generation: 3, runtimeReported: true } + + it('shows a pending request with the processor still on the previous profile', () => { + const strip = lifecycle({ ...base, saved: { actor: { kind: 'agent', name: 'tuner' }, atMs: now - 16 * 60_000 }, + request: { observedState: 'pending_boundary', createdAtMs: now - 14 * 60_000, updatedAtMs: now, previousIsDefaults: false } }) + expect(strip.saved).toEqual({ tone: 'ok', value: 'by tuner · 16 min ago' }) + expect(strip.requested).toEqual({ tone: 'warn', value: 'waiting 14 min' }) + expect(strip.processor).toEqual({ tone: 'ok', value: 'running previous' }) + }) + + it('names system defaults when the previous profile was the shipped one', () => { + const strip = lifecycle({ ...base, saved: { actor: { kind: 'you', name: 'you' }, atMs: now }, + request: { observedState: 'pending_delivery', createdAtMs: now, updatedAtMs: now, previousIsDefaults: true } }) + expect(strip.processor.value).toBe('running system defaults') + }) + + it('reports acknowledgement and failure', () => { + const acked = lifecycle({ ...base, saved: null, request: { observedState: 'applied', createdAtMs: now - 120_000, updatedAtMs: now - 60_000, previousIsDefaults: false } }) + expect(acked.requested).toEqual({ tone: 'ok', value: 'acknowledged 1 min ago' }) + expect(acked.processor.value).toBe('running saved') + expect(acked.saved.value).toBe('system defaults · 1 min ago') + const failed = lifecycle({ ...base, saved: null, request: { observedState: 'failed', createdAtMs: now, updatedAtMs: now, previousIsDefaults: false } }) + expect(failed.requested.tone).toBe('danger') + expect(failed.processor.tone).toBe('danger') + }) + + it('does not claim a running profile the processor never reported', () => { + const fresh = lifecycle({ ...base, generation: 0, runtimeReported: false, saved: null, request: null }) + expect(fresh.saved.value).toBe('system defaults') + expect(fresh.requested).toEqual({ tone: 'muted', value: 'none' }) + expect(fresh.processor).toEqual({ tone: 'muted', value: 'not reported' }) + expect(lifecycle({ ...base, saved: null, request: null }).processor.value).toBe('running system defaults') + }) +}) + +describe('sleep-stage overrides', () => { + it('pairs changed condition values rule by rule', () => { + const saved = withConfig((c) => { + if (c.sleep_stages.engine === 'rules') c.sleep_stages.ruleset.rules[0].when[0].value = 240 + }) + const result = stageOverrides(defaultConfig.sleep_stages, saved.sleep_stages) + expect(result.ruleCount).toBe(5) + expect(result.restructured).toBe(false) + expect(result.overrides).toEqual([{ rule: 1, stage: 'wake', metric: 'movement', op: '>', before: 200, after: 240 }]) + }) + + it('flags a reshaped rule list instead of guessing pairs', () => { + const saved = withConfig((c) => { + if (c.sleep_stages.engine === 'rules') { + c.sleep_stages.ruleset.rules.pop() + c.sleep_stages.ruleset.fallback = 'deep' + } + }) + const result = stageOverrides(defaultConfig.sleep_stages, saved.sleep_stages) + expect(result).toMatchObject({ ruleCount: 4, restructured: true, fallbackChanged: true, overrides: [] }) + }) +}) + +describe('backtests', () => { + it('groups the latest runs of the saved revision by split on one dataset', () => { + const jobs = [ + job({ id: 'old-tuning', split: 'tuning', createdAtMs: 1 }), + job({ id: 'tuning', split: 'tuning', createdAtMs: 5 }), + job({ id: 'holdout', split: 'holdout', createdAtMs: 4 }), + job({ id: 'failed-retry', split: 'holdout', createdAtMs: 6, state: 'failed' }), + job({ id: 'other-dataset', datasetId: 'x', split: 'validation', createdAtMs: 2 }), + job({ id: 'other-revision', split: 'validation', createdAtMs: 9, configurations: [{ label: 'candidate', revisionId: 'other', hash: 'z' }] }), + ] + const group = savedBacktest(jobs, 'saved') + expect(group?.latest.id).toBe('failed-retry') + expect(group?.bySplit.tuning?.id).toBe('tuning') + expect(group?.bySplit.holdout?.id).toBe('holdout') + expect(group?.bySplit.validation).toBeUndefined() + expect(savedBacktest(jobs, null)).toBeNull() + expect(savedBacktest(jobs, 'missing')).toBeNull() + }) + + it('summarizes run state, preferring in-flight work', () => { + expect(runStatus([])).toBeNull() + expect(runStatus([job({ createdAtMs: 0, updatedAtMs: 38_000 })])).toEqual({ tone: 'ok', kind: 'done', durationMs: 38_000 }) + expect(runStatus([job({}), job({ state: 'running' })])?.kind).toBe('running') + expect(runStatus([job({}), job({ state: 'queued' })])?.kind).toBe('queued') + expect(runStatus([job({}), job({ state: 'failed' })])?.tone).toBe('danger') + }) + + it('matches the previous profile by hash, else the runtime baseline', () => { + expect(baselineLabel(job({}), 'h-default')).toBe('default') + expect(baselineLabel(job({}), 'unknown')).toBe('current') + expect(baselineLabel(job({ configurations: [{ label: 'default', revisionId: null, hash: 'a' }] }), null)).toBe('default') + }) + + const window = (metric: 'heart_rate' | 'breathing_rate' | 'window_ibi_variability_v1', mae: number | null, paired: number, cover = 0.9) => ({ + metric, status: mae === null ? 'no_reference' as const : 'compared' as const, + metrics: mae === null ? null : { mae, medianAbsoluteError: mae, p95AbsoluteError: mae, signedBias: 0 }, + pairedCount: paired, referenceCoverage: 1, outputCoverage: cover, unmatchedReferences: 0, unmatchedObservations: 0, exclusions: {}, + }) + const report: NumericReportView = { nights: [ + { cohortId: '2026-10-11', evaluations: [ + { label: 'current', configHash: 'h-current', windows: [{ startMs: 0, endMs: 1, observationCount: 10, numeric: [window('heart_rate', 3, 10, 0.8), window('breathing_rate', 1, 5)] }] }, + { label: 'candidate', configHash: 'h-saved', windows: [ + { startMs: 0, endMs: 1, observationCount: 10, numeric: [window('heart_rate', 2, 10, 0.9)] }, + { startMs: 1, endMs: 2, observationCount: 30, numeric: [window('heart_rate', 4, 30, 0.5)] }, + ] }, + ] }, + { cohortId: '2026-10-12', evaluations: [ + { label: 'current', configHash: 'h-current', windows: [{ startMs: 0, endMs: 1, observationCount: 10, numeric: [window('heart_rate', null, 0)] }] }, + { label: 'candidate', configHash: 'h-saved', windows: [{ startMs: 0, endMs: 1, observationCount: 10, numeric: [window('heart_rate', null, 0)] }] }, + ] }, + ] } + + it('weights per-night error by paired references and excludes nights without them', () => { + const nights = nightScores(report, 'holdout', 'current') + expect(nights[0].saved.heart_rate.mae).toBeCloseTo(3.5) + expect(nights[0].saved.heart_rate.coverage).toBeCloseTo((0.9 * 10 + 0.5 * 30) / 40) + expect(nights[0].previous.breathing_rate.mae).toBe(1) + expect(nights[0].hasReference).toBe(true) + expect(nights[1].hasReference).toBe(false) + expect(aggregate(nights, 'heart_rate', 'holdout')).toEqual({ previous: 3, saved: 3.5, nights: 1 }) + expect(aggregate(nights, 'heart_rate', 'tuning').nights).toBe(0) + expect(coverage(nights).previous).toBeCloseTo(0.85) + }) + + it('colours only real improvements green and any regression amber', () => { + expect(compareTone(3, 2.9)).toBe('better') + expect(compareTone(3, 2.97)).toBe('same') + expect(compareTone(3, 3.01)).toBe('worse') + expect(compareTone(0.9, 0.95, true)).toBe('better') + expect(compareTone(0.9, 0.903, true)).toBe('same') + expect(compareTone(3, 2.9, false, 0.2)).toBe('same') + expect(compareTone(0.9, 0.8, true)).toBe('worse') + expect(compareTone(null, 1)).toBe('same') + }) + + it('scales bars 0–5 bpm onto 72 px and clamps', () => { + expect(barHeight(null)).toBe(0) + expect(barHeight(2.5)).toBe(36) + expect(barHeight(9)).toBe(72) + expect(barHeight(-1)).toBe(0) + }) +}) + +describe('nights and traces', () => { + it('labels a night from its calendar date', () => { + expect(nightLabel('2026-10-11')).toEqual({ weekday: 'Sun', day: '11' }) + expect(nightLabel('cohort-x')).toEqual({ weekday: 'cohort-x', day: '' }) + }) + + it('draws gaps instead of bridging missing readings', () => { + const d = tracePath([{ t: 0, v: 70 }, { t: 60_000, v: 70 }, { t: 120_000, v: null }, { t: 180_000, v: 100 }, { t: 3_600_000, v: 40 }], 0, 3_600_000) + expect(d.match(/M/g)).toHaveLength(3) + expect(d.startsWith('M0.0 75.0 L')).toBe(true) + expect(d).toContain('364.0 150.0') + }) + + it('ticks session start, every two hours, and session end', () => { + const start = Date.UTC(2026, 9, 12, 6, 15), end = Date.UTC(2026, 9, 12, 14, 0) + const ticks = traceTicks(start, end) + expect(ticks[0]).toBe(start) + expect(ticks[ticks.length - 1]).toBe(end) + expect(ticks.slice(1, -1).map(t => new Date(t).getUTCHours())).toEqual([8, 10, 12]) + }) +}) diff --git a/src/lib/i18n/locales/en.po b/src/lib/i18n/locales/en.po index fea68843..3158a94e 100644 --- a/src/lib/i18n/locales/en.po +++ b/src/lib/i18n/locales/en.po @@ -13,6 +13,18 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:169 +msgid ", with {count} overrides" +msgstr ", with {count} overrides" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:169 +msgid ", with 1 override" +msgstr ", with 1 override" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:369 +msgid ". Separate from the profile." +msgstr ". Separate from the profile." + #. placeholder {0}: item.summary.source #. placeholder {1}: item.summary.subjectId #. placeholder {2}: item.revision @@ -20,6 +32,31 @@ msgstr "" msgid "{0} · {1} · revision {2}" msgstr "{0} · {1} · revision {2}" +#. placeholder {0}: label.weekday +#. placeholder {1}: label.day +#. placeholder {2}: fmt1(prev) +#. placeholder {3}: fmt1(saved) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:198 +msgid "{0} {1}: previous {2}, saved {3} bpm" +msgstr "{0} {1}: previous {2}, saved {3} bpm" + +#. placeholder {0}: splitLabel[job.split] +#. placeholder {1}: avg(n => n.previous.heart_rate.mae as number) +#. placeholder {2}: avg(n => n.saved.heart_rate.mae as number) +#. placeholder {3}: shortDuration(job.updatedAtMs - job.createdAtMs) +#: src/components/biometrics/Calibration/CalibrationPage.tsx:216 +msgid "{0} HR error {1} → {2} bpm · {3}" +msgstr "{0} HR error {1} → {2} bpm · {3}" + +#. placeholder {0}: i.summary.accepted +#: src/components/biometrics/Calibration/CalibrationPage.tsx:227 +msgid "{0} records" +msgstr "{0} records" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:160 +msgid "{stages} sleep-stage values" +msgstr "{stages} sleep-stage values" + #: src/components/biometrics/Calibration/NumericReport.tsx:175 msgid "95th percentile absolute error" msgstr "95th percentile absolute error" @@ -32,6 +69,18 @@ msgstr "A saved request does not mean execution has started or succeeded. Inspec msgid "A saved revision is not evidence of validation or processor adoption." msgstr "A saved revision is not evidence of validation or processor adoption." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:109 +msgid "Accept an estimate when it is within" +msgstr "Accept an estimate when it is within" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:156 +msgid "accepted 10-second IBI windows" +msgstr "accepted 10-second IBI windows" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:123 +msgid "accepted estimates in the tracker" +msgstr "accepted estimates in the tracker" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:87 msgid "Accepted records are not necessarily usable for validation. Incompatible HRV definitions remain unscorable." msgstr "Accepted records are not necessarily usable for validation. Incompatible HRV definitions remain unscorable." @@ -53,10 +102,6 @@ msgstr "Accepted: {0}; rejected: {1}; duplicates: {2}; unscorable: {3}" msgid "Accepted: {0}; rejected: {1}; duplicates: {2}; unscorable: {3}; conflicts: {4}" msgstr "Accepted: {0}; rejected: {1}; duplicates: {2}; unscorable: {3}; conflicts: {4}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:21 -msgid "Activation history" -msgstr "Activation history" - #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:76 msgid "Activation request saved. Processor acknowledgement is reported separately below." msgstr "Activation request saved. Processor acknowledgement is reported separately below." @@ -80,6 +125,10 @@ msgstr "Active generations: left {0}, right {1}." msgid "Active profile hash" msgstr "Active profile hash" +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:21 +msgid "Activity" +msgstr "Activity" + #: src/components/biometrics/Calibration/NumericReport.tsx:111 msgid "Added readings" msgstr "Added readings" @@ -122,8 +171,6 @@ msgstr "An additional report issue needs review." msgid "Another revision from this reference family is selected." msgstr "Another revision from this reference family is selected." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Applied" msgstr "Applied" @@ -132,10 +179,6 @@ msgstr "Applied" msgid "Apply evaluated revision" msgstr "Apply evaluated revision" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Apply profile" -msgstr "Apply profile" - #: src/components/biometrics/Calibration/ValidatedProfileActivation.tsx:32 msgid "Apply this exact candidate using the selected evaluation. Evidence and current settings are checked again when applying." msgstr "Apply this exact candidate using the selected evaluation. Evidence and current settings are checked again when applying." @@ -172,10 +215,20 @@ msgstr "Attempt {0}" msgid "Available reference records by metric (across revisions)" msgstr "Available reference records by metric (across revisions)" +#. placeholder {0}: group.latest.id.slice(0, 4) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:181 +msgid "Backtest {0}. Frozen dataset replayed through the previous and saved profiles; nothing here changes the pod." +msgstr "Backtest {0}. Frozen dataset replayed through the previous and saved profiles; nothing here changes the pod." + #: src/components/biometrics/Calibration/BacktestJobs.tsx:21 msgid "Backtest jobs" msgstr "Backtest jobs" +#. placeholder {0}: splitLabel[job.split] +#: src/components/biometrics/Calibration/CalibrationPage.tsx:220 +msgid "Backtest on {0} nights failed" +msgstr "Backtest on {0} nights failed" + #. placeholder {0}: enqueue.data.id #: src/components/biometrics/Calibration/BacktestSubmission.tsx:93 msgid "Backtest request saved: {0}" @@ -185,7 +238,7 @@ msgstr "Backtest request saved: {0}" msgid "Backtest submission" msgstr "Backtest submission" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:29 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:30 msgid "Backtests" msgstr "Backtests" @@ -201,11 +254,26 @@ msgstr "Before" msgid "Before configuration hash" msgstr "Before configuration hash" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:24 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:68 msgid "Biometric calibration" msgstr "Biometric calibration" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:264 +msgid "Biometric calibration side" +msgstr "Biometric calibration side" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:113 +msgid "bpm of the tracker" +msgstr "bpm of the tracker" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:128 +msgid "BREATHING" +msgstr "BREATHING" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:173 +msgid "Breathing error" +msgstr "Breathing error" + #: src/components/biometrics/Calibration/ShadowReport.tsx:30 msgid "Breathing rate" msgstr "Breathing rate" @@ -216,10 +284,18 @@ msgstr "Breathing rate" msgid "Breathing rate (breaths/min)" msgstr "Breathing rate (breaths/min)" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:25 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:38 +msgid "Calibration" +msgstr "Calibration" + +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:34 msgid "Calibration views" msgstr "Calibration views" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:136 +msgid "Cancel" +msgstr "Cancel" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:79 msgid "Cancel backtest" msgstr "Cancel backtest" @@ -318,6 +394,14 @@ msgstr "Candidate sleep-stage rules" msgid "Candidate spike threshold" msgstr "Candidate spike threshold" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:94 +msgid "Changed values are white; the previous profile (System defaults) sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." +msgstr "Changed values are white; the previous profile (System defaults) sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:95 +msgid "Changed values are white; the previous profile sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." +msgstr "Changed values are white; the previous profile sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:86 msgid "Check Overview for current processor health; a past acknowledgement does not prove current health." msgstr "Check Overview for current processor health; a past acknowledgement does not prove current health." @@ -350,9 +434,9 @@ msgstr "Circles are recomputed readings; squares are reference points; horizonta msgid "Clear this night’s selection" msgstr "Clear this night’s selection" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 -msgid "Clock mismatch" -msgstr "Clock mismatch" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:187 +msgid "click a night" +msgstr "click a night" #. placeholder {0}: minutes(score.pairedDurationMs) #: src/components/biometrics/Calibration/StageReport.tsx:17 @@ -443,10 +527,6 @@ msgstr "Comparison shared MAE" msgid "Compatibility" msgstr "Compatibility" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Compatibility fallback" -msgstr "Compatibility fallback" - #: src/components/biometrics/Calibration/ReferenceOverlay.tsx:90 msgid "Compatible" msgstr "Compatible" @@ -464,15 +544,15 @@ msgstr "Completed nights" msgid "Compressed bytes: {0}" msgstr "Compressed bytes: {0}" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:372 +msgid "config id" +msgstr "config id" + #: src/components/biometrics/Calibration/BacktestReport.tsx:98 #: src/components/biometrics/Calibration/StageReport.tsx:107 msgid "Configuration" msgstr "Configuration" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:36 -msgid "Configuration identity" -msgstr "Configuration identity" - #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:58 msgid "Confirm import deletion" msgstr "Confirm import deletion" @@ -506,10 +586,13 @@ msgstr "Coverage reduced" msgid "Coverage reduced in at least one evaluated window" msgstr "Coverage reduced in at least one evaluated window" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Current" -msgstr "Current" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:61 +msgid "covered" +msgstr "covered" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "current" +msgstr "current" #: src/components/biometrics/Calibration/BacktestReport.tsx:60 #: src/components/biometrics/Calibration/NumericReport.tsx:26 @@ -543,6 +626,10 @@ msgstr "Dataset" msgid "Dataset hash" msgstr "Dataset hash" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:84 +msgid "Dataset nights" +msgstr "Dataset nights" + #. placeholder {0}: create.data.id #: src/components/biometrics/Calibration/DatasetBuilder.tsx:57 msgid "Dataset saved: {0}" @@ -557,10 +644,6 @@ msgstr "Dataset sleeper identifier" msgid "Deep" msgstr "Deep" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:31 -msgid "Default recovery configuration hash" -msgstr "Default recovery configuration hash" - #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:52 msgid "Delete import" msgstr "Delete import" @@ -577,13 +660,10 @@ msgstr "Deleting an import removes its source measurements. Previously frozen da msgid "Descriptive evaluation only" msgstr "Descriptive evaluation only" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 -msgid "Different versions reported" -msgstr "Different versions reported" - -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:57 -msgid "Direct sleep-stage changes are unvalidated manual settings. Saving requests a profile change; processor adoption is reported separately in Overview." -msgstr "Direct sleep-stage changes are unvalidated manual settings. Saving requests a profile change; processor adoption is reported separately in Overview." +#. placeholder {0}: shortDuration(run.durationMs) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "done · {0}" +msgstr "done · {0}" #: src/components/biometrics/Calibration/NumericReport.tsx:151 msgid "Dropping readings can lower own-support error. Duplicate timestamps are excluded from shared support. These counts do not measure continuous time coverage." @@ -601,6 +681,10 @@ msgstr "Each configuration on its own available intervals" msgid "Edit a new candidate from this revision" msgstr "Edit a new candidate from this revision" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:192 +msgid "Edit overrides" +msgstr "Edit overrides" + #: src/components/biometrics/Calibration/ProfileLibrary.tsx:20 msgid "Editing from revision {revisionId}" msgstr "Editing from revision {revisionId}" @@ -660,6 +744,10 @@ msgstr "Estimate exploratory whole-night uncertainty" msgid "Evaluated minutes: {0}" msgstr "Evaluated minutes: {0}" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:75 +msgid "Evaluation dataset" +msgstr "Evaluation dataset" + #: src/components/biometrics/Calibration/ExperimentBuilder.tsx:47 msgid "Evaluation mode" msgstr "Evaluation mode" @@ -693,10 +781,6 @@ msgstr "Exact normalized timestamps are shown in UTC. Values are not interpolate msgid "Exact processed-data coverage" msgstr "Exact processed-data coverage" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:45 -msgid "Exact profile transition" -msgstr "Exact profile transition" - #: src/components/biometrics/Calibration/NumericTrace.tsx:93 msgid "Exact recomputed readings, without reference clock adjustment" msgstr "Exact recomputed readings, without reference clock adjustment" @@ -728,9 +812,9 @@ msgstr "Execution state is separate from the strength of biometric evidence." msgid "Experiment saved: {0}" msgstr "Experiment saved: {0}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Expired" -msgstr "Expired" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "expired" +msgstr "expired" #: src/components/biometrics/Calibration/CandidateStageFields.tsx:39 msgid "Explicit stage overrides" @@ -757,19 +841,38 @@ msgstr "Exploratory interval estimated" msgid "F1" msgstr "F1" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "failed" +msgstr "failed" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 #: src/components/biometrics/Calibration/ShadowRuns.tsx:12 msgid "Failed" msgstr "Failed" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:184 +msgid "Fallback" +msgstr "Fallback" + #: src/components/biometrics/Calibration/CandidateStageFields.tsx:21 #: src/components/biometrics/Calibration/CandidateStageFields.tsx:21 msgid "Fallback stage" msgstr "Fallback stage" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:202 +msgid "Fell back to a compatible profile" +msgstr "Fell back to a compatible profile" + +#. placeholder {0}: splitLabel[job.split] +#: src/components/biometrics/Calibration/CalibrationPage.tsx:220 +msgid "Finished a backtest on {0} nights" +msgstr "Finished a backtest on {0} nights" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:314 +msgid "first 500 readings" +msgstr "first 500 readings" + #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:125 msgid "First archive page" msgstr "First archive page" @@ -794,6 +897,10 @@ msgstr "Fixed offset added to pod timestamps (ms, ±3600000)" msgid "Flags" msgstr "Flags" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:128 +msgid "Freeze a new dataset" +msgstr "Freeze a new dataset" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:33 msgid "Freeze evaluation inputs" msgstr "Freeze evaluation inputs" @@ -802,9 +909,31 @@ msgstr "Freeze evaluation inputs" msgid "Freeze selected inputs" msgstr "Freeze selected inputs" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Freshness unknown" -msgstr "Freshness unknown" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "freshness unknown" +msgstr "freshness unknown" + +#. placeholder {0}: d.manifest.splitProtocol.assignments.length +#: src/components/biometrics/Calibration/CalibrationPage.tsx:224 +msgid "Froze a dataset of {0} nights" +msgstr "Froze a dataset of {0} nights" + +#. placeholder {0}: formatDay(dataset.createdAtMs, timezone) +#. placeholder {1}: formatBytes(dataset.byteLength) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:79 +msgid "frozen {0} · {1}" +msgstr "frozen {0} · {1}" + +#. placeholder {0}: nights.length +#. placeholder {1}: sources.join(' + ') +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:69 +msgid "Frozen copy of {0} nights and {1} reference. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." +msgstr "Frozen copy of {0} nights and {1} reference. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." + +#. placeholder {0}: nights.length +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:70 +msgid "Frozen copy of {0} nights. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." +msgstr "Frozen copy of {0} nights. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." #: src/components/biometrics/Calibration/DatasetLibrary.tsx:85 msgid "Frozen coverage and capabilities" @@ -838,26 +967,32 @@ msgstr "Frozen source records in UTC; incompatible records are listed but not pl msgid "Frozen stage objective thresholds" msgstr "Frozen stage objective thresholds" -#. placeholder {0}: data.desired.binding.generation -#. placeholder {0}: event.generation -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:41 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:39 -msgid "Generation {0}" -msgstr "Generation {0}" - #: src/components/biometrics/Calibration/NumericReport.tsx:44 msgid "Groups use native reference timestamps and record types, independently of candidate output. Half-window groups exclude intervals crossing the midpoint. Groups overlap and are not independent experiments. These are not motion or artifact subgroups." msgstr "Groups use native reference timestamps and record types, independently of candidate output. Half-window groups exclude intervals crossing the midpoint. Groups overlap and are not independent experiments. These are not motion or artifact subgroups." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:228 +msgid "H holdout" +msgstr "H holdout" + #: src/components/biometrics/Calibration/ReplayResources.tsx:49 msgid "Health was sampled at admission checks, normally five seconds apart. Unobserved intervals are not continuous health measurements." msgstr "Health was sampled at admission checks, normally five seconds apart. Unobserved intervals are not continuous health measurements." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 #: src/components/biometrics/Calibration/ShadowReport.tsx:30 msgid "Heart rate" msgstr "Heart rate" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:106 +msgid "HEART RATE" +msgstr "HEART RATE" + +#. placeholder {0}: label.weekday +#. placeholder {1}: label.day +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:291 +msgid "Heart rate · {0} {1}" +msgstr "Heart rate · {0} {1}" + #: src/components/biometrics/Calibration/NumericReport.tsx:27 #: src/components/biometrics/Calibration/NumericTrace.tsx:38 #: src/components/biometrics/Calibration/ShadowComparison.tsx:44 @@ -868,10 +1003,18 @@ msgstr "Heart rate (bpm)" msgid "Heart-rate spike threshold (standard deviations)" msgstr "Heart-rate spike threshold (standard deviations)" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:295 +msgid "Heart-rate trace" +msgstr "Heart-rate trace" + #: app/[lang]/health/HealthcheckDemo.tsx:13 msgid "Hello, tRPC test page!" msgstr "Hello, tRPC test page!" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:377 +msgid "hide" +msgstr "hide" + #. placeholder {0}: summary.improvedAgreementNights #. placeholder {1}: summary.regressedAgreementNights #. placeholder {2}: summary.unchangedAgreementNights @@ -879,13 +1022,10 @@ msgstr "Hello, tRPC test page!" msgid "Higher agreement: {0}; lower: {1}; unchanged: {2}" msgstr "Higher agreement: {0}; lower: {1}; unchanged: {2}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:46 -msgid "Historical sleep stages are recomputed with the current classifier. Original stored vitals remain separate from replay results." -msgstr "Historical sleep stages are recomputed with the current classifier. Original stored vitals remain separate from replay results." - -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:30 -msgid "History" -msgstr "History" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:120 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "holdout" +msgstr "holdout" #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 @@ -900,6 +1040,26 @@ msgstr "Holdout" msgid "Holdout accesses recorded for this experiment: {0}" msgstr "Holdout accesses recorded for this experiment: {0}" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:258 +msgid "Holdout is the number to trust; tuning nights were used to pick these values." +msgstr "Holdout is the number to trust; tuning nights were used to pick these values." + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:170 +msgid "HR error · holdout" +msgstr "HR error · holdout" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:168 +msgid "HR error · tuning" +msgstr "HR error · tuning" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:169 +msgid "HR error · validation" +msgstr "HR error · validation" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:186 +msgid "HR error per night · bpm vs reference" +msgstr "HR error per night · bpm vs reference" + #: src/components/biometrics/BiometricsPage.tsx:532 msgid "HRV definition" msgstr "HRV definition" @@ -908,9 +1068,13 @@ msgstr "HRV definition" msgid "HRV definitions differ or are partly unknown. A combined HRV average is unavailable." msgstr "HRV definitions differ or are partly unknown. A combined HRV average is unavailable." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 -msgid "HRV index" -msgstr "HRV index" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:149 +msgid "HRV INDEX" +msgstr "HRV INDEX" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:172 +msgid "HRV index error" +msgstr "HRV index error" #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:60 msgid "I intend to apply this exact revision without validation evidence." @@ -928,6 +1092,10 @@ msgstr "I intend to restore system defaults without validation evidence." msgid "I intend to restore the exact previous profile without validation evidence." msgstr "I intend to restore the exact previous profile without validation evidence." +#: src/components/biometrics/Calibration/CalibrationControls.tsx:147 +msgid "Identical to the saved profile; restoring only re-requests it." +msgstr "Identical to the saved profile; restoring only re-requests it." + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:53 #: src/components/biometrics/Calibration/ValidatedProfileActivation.tsx:38 msgid "Immediate processing boundary" @@ -941,6 +1109,10 @@ msgstr "Import accepted records and omit rejected records" msgid "Import preview" msgstr "Import preview" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:126 +msgid "Import reference" +msgstr "Import reference" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:53 msgid "Import reference measurements" msgstr "Import reference measurements" @@ -972,6 +1144,11 @@ msgstr "Ingestion samples: {0} of {1} admission checks" msgid "Inherited default" msgstr "Inherited default" +#. placeholder {0}: stages.ruleCount +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:166 +msgid "Inherits the {0} rules and fallback from" +msgstr "Inherits the {0} rules and fallback from" + #: src/components/biometrics/Calibration/DatasetLibrary.tsx:31 msgid "Inputs released; metadata retained" msgstr "Inputs released; metadata retained" @@ -1063,6 +1240,14 @@ msgstr "Keep report" msgid "Keep retained inputs" msgstr "Keep retained inputs" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Keep saved profile" +msgstr "Keep saved profile" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:119 +msgid "Keep the last" +msgstr "Keep the last" + #. placeholder {0}: display(operations.ingestion.maximumObservationGapMs, 1000) #: src/components/biometrics/Calibration/ReplayResources.tsx:47 msgid "Largest observation gap: {0} seconds" @@ -1095,6 +1280,10 @@ msgstr "Light" msgid "Limitations" msgstr "Limitations" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:144 +msgid "Longest breathing interval (× median)" +msgstr "Longest breathing interval (× median)" + #: src/components/biometrics/Calibration/NumericReport.tsx:110 msgid "Lost readings" msgstr "Lost readings" @@ -1144,9 +1333,9 @@ msgstr "Mean absolute error on shared timestamps: active {0}, candidate {1}." msgid "Measured Python processes: {0} of {1}" msgstr "Measured Python processes: {0} of {1}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:53 -msgid "Measured sensor calibration" -msgstr "Measured sensor calibration" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:366 +msgid "Measured sensor calibration for this side:" +msgstr "Measured sensor calibration for this side:" #: src/components/biometrics/Calibration/NumericReport.tsx:174 msgid "Median absolute error" @@ -1176,9 +1365,13 @@ msgstr "Metric window: {0} seconds." msgid "Metrics: {0}" msgstr "Metrics: {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Migrate settings" -msgstr "Migrate settings" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:202 +msgid "Migrated sleep-stage settings into the profile" +msgstr "Migrated sleep-stage settings into the profile" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:159 +msgid "Minimum IBI windows" +msgstr "Minimum IBI windows" #: src/components/biometrics/Calibration/ExperimentBuilder.tsx:97 msgid "Minimum paired nights (3–60)" @@ -1205,10 +1398,6 @@ msgstr "New archive work is currently deferred." msgid "New observation plan" msgstr "New observation plan" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:62 -msgid "Newest" -msgstr "Newest" - #: src/components/biometrics/Calibration/DatasetLibrary.tsx:41 msgid "Newest datasets" msgstr "Newest datasets" @@ -1280,6 +1469,10 @@ msgstr "Next trace page" msgid "Night {0}" msgstr "Night {0}" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:171 +msgid "Night covered" +msgstr "Night covered" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:74 msgid "Night progress is not available yet." msgstr "Night progress is not available yet." @@ -1288,9 +1481,9 @@ msgstr "Night progress is not available yet." msgid "No accepted time range." msgstr "No accepted time range." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:30 -msgid "No activation history for this side." -msgstr "No activation history for this side." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:158 +msgid "No backtest has replayed the saved profile yet. Saving a change re-runs the latest experiment on its tuning and validation nights; nothing here changes the pod." +msgstr "No backtest has replayed the saved profile yet. Saving a change re-runs the latest experiment on its tuning and validation nights; nothing here changes the pod." #: src/components/biometrics/Calibration/BacktestJobs.tsx:28 msgid "No backtest jobs for this side." @@ -1312,6 +1505,10 @@ msgstr "No completed worker resource receipt is available." msgid "No coverage reduction detected" msgstr "No coverage reduction detected" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:71 +msgid "No dataset is frozen for this side yet. Freeze recorded nights with an imported reference to backtest a profile." +msgstr "No dataset is frozen for this side yet. Freeze recorded nights with an imported reference to backtest a profile." + #: src/components/biometrics/Calibration/BacktestReport.tsx:62 #: src/components/biometrics/Calibration/NumericReport.tsx:28 msgid "No estimable variation" @@ -1366,10 +1563,18 @@ msgstr "No prospective runs for this side." msgid "No recorded nights for this side." msgstr "No recorded nights for this side." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:101 +msgid "no reference" +msgstr "no reference" + #: src/components/biometrics/Calibration/NumericReport.tsx:29 msgid "No reference" msgstr "No reference" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:286 +msgid "no reference device · excluded from error" +msgstr "no reference device · excluded from error" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:91 #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:35 msgid "No reference imports for this side." @@ -1404,10 +1609,6 @@ msgstr "No saved candidate revisions." msgid "No saved experiments for this side." msgstr "No saved experiments for this side." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:55 -msgid "No sensor calibration has been reported for this side." -msgstr "No sensor calibration has been reported for this side." - #: src/components/biometrics/Calibration/StageTrace.tsx:84 msgid "No stage intervals were produced for this selection." msgstr "No stage intervals were produced for this selection." @@ -1434,8 +1635,10 @@ msgstr "None" msgid "Normalized reference measurements" msgstr "Normalized reference measurements" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:26 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:362 +msgid "not reported" +msgstr "not reported" + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Not reported" msgstr "Not reported" @@ -1453,6 +1656,19 @@ msgstr "Not requested" msgid "Not scored" msgstr "Not scored" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:55 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:57 +msgid "not tested" +msgstr "not tested" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:330 +msgid "Nothing changes until you move a control or restore." +msgstr "Nothing changes until you move a control or restore." + +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:24 +msgid "Nothing has happened on this side yet." +msgstr "Nothing has happened on this side yet." + #: src/components/biometrics/Calibration/NumericTrace.tsx:158 msgid "Numeric trace configuration" msgstr "Numeric trace configuration" @@ -1523,10 +1739,6 @@ msgstr "Observed process-group peak RSS: {0} MiB" msgid "Observing" msgstr "Observing" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:63 -msgid "Older changes" -msgstr "Older changes" - #: src/components/biometrics/Calibration/DatasetLibrary.tsx:42 msgid "Older datasets" msgstr "Older datasets" @@ -1558,6 +1770,10 @@ msgstr "Older profiles" msgid "Only the current page is plotted. Dots represent individual readings; gaps and missing values are not interpolated. Original stored readings remain unchanged." msgstr "Only the current page is plotted. Dots represent individual readings; gaps and missing values are not interpolated. Original stored readings remain unchanged." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:135 +msgid "or longer than" +msgstr "or longer than" + #: src/components/biometrics/Calibration/StageTrace.tsx:61 msgid "Original replay timestamps, without reference clock adjustment" msgstr "Original replay timestamps, without reference clock adjustment" @@ -1601,10 +1817,14 @@ msgstr "Overridden" msgid "Override intent can change even when the resolved value stays the same." msgstr "Override intent can change even when the resolved value stays the same." -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:26 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:27 msgid "Overview" msgstr "Overview" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:330 +msgid "Overwrites the saved profile and requests it; the current one becomes previous." +msgstr "Overwrites the saved profile and requests it; the current one becomes previous." + #: src/components/biometrics/Calibration/NumericReport.tsx:109 msgid "Own-support MAE difference" msgstr "Own-support MAE difference" @@ -1630,10 +1850,6 @@ msgstr "Parent revision" msgid "Partition timezone: {0}" msgstr "Partition timezone: {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:21 -msgid "Past acknowledgements do not establish which profile is running now." -msgstr "Past acknowledgements do not establish which profile is running now." - #: src/components/biometrics/Calibration/ShadowComparison.tsx:50 msgid "Paste JSON records or CSV with recordKey, metric, value, unit, and timestamp (or start and end). Use offset-bearing ISO times or Unix milliseconds. This comparison does not save a reference import." msgstr "Paste JSON records or CSV with recordKey, metric, value, unit, and timestamp (or start and end). Use offset-bearing ISO times or Unix milliseconds. This comparison does not save a reference import." @@ -1654,9 +1870,9 @@ msgstr "Permanently release inputs" msgid "Permanently release report" msgstr "Permanently release report" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 -msgid "Personalized profile" -msgstr "Personalized profile" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:238 +msgid "Pick a scored night to see its heart-rate trace." +msgstr "Pick a scored night to see its heart-rate trace." #: src/components/biometrics/Calibration/ProfileLibrary.tsx:38 msgid "Pinned configuration and overrides" @@ -1733,10 +1949,6 @@ msgstr "Prepared, not admitted. The pod checks current qualification and health msgid "Presence and pump context reflect host observations, not proof of the live processor’s cached state." msgstr "Presence and pump context reflect host observations, not proof of the live processor’s cached state." -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:29 -msgid "Preserved incompatible revision" -msgstr "Preserved incompatible revision" - #: src/components/biometrics/Calibration/CalibrationReferences.tsx:78 msgid "Preview import" msgstr "Preview import" @@ -1745,6 +1957,18 @@ msgstr "Preview import" msgid "Preview normalized records before saving them on the pod." msgstr "Preview normalized records before saving them on the pod." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:222 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:245 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:312 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:65 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:141 +msgid "previous" +msgstr "previous" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:77 +msgid "Previous" +msgstr "Previous" + #: src/components/biometrics/Calibration/NumericTrace.tsx:134 msgid "Previous numeric page" msgstr "Previous numeric page" @@ -1770,19 +1994,33 @@ msgstr "Processing failed." msgid "Processing run" msgstr "Processing run" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:27 -msgid "Processor health" -msgstr "Processor health" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:19 +msgid "Processor" +msgstr "Processor" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:24 -msgid "Profile and processor" -msgstr "Profile and processor" +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:203 +msgid "Processor acknowledged generation {0}" +msgstr "Processor acknowledged generation {0}" + +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:204 +msgid "Processor rejected generation {0}" +msgstr "Processor rejected generation {0}" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:22 +msgid "Profile lifecycle" +msgstr "Profile lifecycle" #: src/components/biometrics/Calibration/ProfileCandidateEditor.tsx:69 msgid "Profile resolution failed." msgstr "Profile resolution failed." -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:28 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:84 +msgid "Profile revisions here are candidates. Applying one requests it like Save and request does; the overview shows when the processor acknowledges it." +msgstr "Profile revisions here are candidates. Applying one requests it like Save and request does; the overview shows when the processor acknowledges it." + +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:29 msgid "Profiles" msgstr "Profiles" @@ -1804,6 +2042,10 @@ msgstr "Prospective observations" msgid "Protocol hash" msgstr "Protocol hash" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:287 +msgid "quality" +msgstr "quality" + #: src/components/biometrics/Calibration/NumericTrace.tsx:98 msgid "Quality" msgstr "Quality" @@ -1812,10 +2054,18 @@ msgstr "Quality" msgid "Queue backtest" msgstr "Queue backtest" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "queued" +msgstr "queued" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 msgid "Queued" msgstr "Queued" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:65 +msgid "range" +msgstr "range" + #: src/components/biometrics/Calibration/StageReport.tsx:52 msgid "Recall" msgstr "Recall" @@ -1854,6 +2104,11 @@ msgstr "Recorded configurations: {0}; readings with unknown configuration: {1}." msgid "Recorded nights" msgstr "Recorded nights" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:313 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:128 +msgid "reference" +msgstr "reference" + #: src/components/biometrics/Calibration/StageReport.tsx:31 msgid "Reference / predicted" msgstr "Reference / predicted" @@ -1974,7 +2229,7 @@ msgstr "Reference source" msgid "Reference-defined subgroup comparisons" msgstr "Reference-defined subgroup comparisons" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:27 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:28 msgid "References" msgstr "References" @@ -1986,6 +2241,10 @@ msgstr "Refresh report" msgid "Regression under the evaluation policy" msgstr "Regression under the evaluation policy" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:131 +msgid "Reject intervals shorter than" +msgstr "Reject intervals shorter than" + #: src/components/biometrics/Calibration/BacktestReport.tsx:162 msgid "Release detailed report" msgstr "Release detailed report" @@ -2002,6 +2261,10 @@ msgstr "Release retained inputs" msgid "Release these retained inputs permanently? Existing reports and audit metadata remain. Active jobs must finish or be cancelled first." msgstr "Release these retained inputs permanently? Existing reports and audit metadata remain. Active jobs must finish or be cancelled first." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:79 +msgid "released" +msgstr "released" + #: src/components/biometrics/Calibration/StageReport.tsx:12 #: src/components/biometrics/Calibration/StageTrace.tsx:30 msgid "REM" @@ -2059,6 +2322,10 @@ msgstr "Report hash" msgid "Report identity" msgstr "Report identity" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:152 +msgid "Report the index only after" +msgstr "Report the index only after" + #: src/components/biometrics/Calibration/ShadowRuns.tsx:51 msgid "Reports describe observed coverage and reference agreement; they do not authorize activation." msgstr "Reports describe observed coverage and reference agreement; they do not authorize activation." @@ -2067,14 +2334,18 @@ msgstr "Reports describe observed coverage and reference agreement; they do not msgid "Reproducible sampling seed" msgstr "Reproducible sampling seed" -#. placeholder {0}: event.requester -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:54 -msgid "Requested by {0}" -msgstr "Requested by {0}" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:18 +msgid "Requested" +msgstr "Requested" + +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:198 +msgid "Requested activation · generation {0}" +msgstr "Requested activation · generation {0}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:26 -msgid "Requested change" -msgstr "Requested change" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:135 +msgid "Requesting…" +msgstr "Requesting…" #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:117 msgid "Requires re-verification" @@ -2100,21 +2371,41 @@ msgstr "Resource observations are partial. Worker health reflects the last valid msgid "Restart archive scan" msgstr "Restart archive scan" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:17 -msgid "Restore a profile" -msgstr "Restore a profile" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:135 +msgid "Restore and request" +msgstr "Restore and request" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:326 +msgid "Restore previous" +msgstr "Restore previous" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:62 #: src/components/biometrics/Calibration/ProfileComparison.tsx:28 msgid "Restore previous profile" msgstr "Restore previous profile" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:127 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:62 #: src/components/biometrics/Calibration/ProfileComparison.tsx:28 msgid "Restore system defaults" msgstr "Restore system defaults" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:127 +msgid "Restore the previous profile" +msgstr "Restore the previous profile" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:72 +msgid "Restore… ▾" +msgstr "Restore… ▾" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:201 +msgid "Restored system defaults and requested them" +msgstr "Restored system defaults and requested them" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:200 +msgid "Restored the previous profile and requested it" +msgstr "Restored the previous profile and requested it" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:33 msgid "Retain a copy of the selected nights and reference revisions. Future edits or source deletion do not alter this dataset." msgstr "Retain a copy of the selected nights and reference revisions. Future edits or source deletion do not alter this dataset." @@ -2161,10 +2452,6 @@ msgstr "Retry same scheduling request" msgid "Review candidate" msgstr "Review candidate" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:17 -msgid "Review changes before requesting system defaults or the exact previous profile." -msgstr "Review changes before requesting system defaults or the exact previous profile." - #: src/components/biometrics/Calibration/ShadowRuns.tsx:88 msgid "Review observation report" msgstr "Review observation report" @@ -2173,18 +2460,10 @@ msgstr "Review observation report" msgid "Review prepared plan" msgstr "Review prepared plan" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:21 -msgid "Review previous profile" -msgstr "Review previous profile" - #: src/components/biometrics/Calibration/BacktestJobs.tsx:81 msgid "Review report" msgstr "Review report" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:20 -msgid "Review system defaults" -msgstr "Review system defaults" - #. placeholder {0}: profile.revision #. placeholder {1}: profile.origin #. placeholder {2}: profile.requester @@ -2209,6 +2488,15 @@ msgstr "Right" msgid "Row {0}: {1}" msgstr "Row {0}: {1}" +#. placeholder {0}: o.rule +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:174 +msgid "Rule {0}" +msgstr "Rule {0}" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "running" +msgstr "running" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 msgid "Running" msgstr "Running" @@ -2222,6 +2510,10 @@ msgstr "Sampled worker memory peak: {0} MiB. Short peaks between samples may be msgid "Save a candidate for evaluation. This does not change live processing." msgstr "Save a candidate for evaluation. This does not change live processing." +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Save and request" +msgstr "Save and request" + #: src/components/biometrics/Calibration/ProfileCandidateEditor.tsx:111 msgid "Save candidate without applying" msgstr "Save candidate without applying" @@ -2230,11 +2522,21 @@ msgstr "Save candidate without applying" msgid "Save evaluation protocol" msgstr "Save evaluation protocol" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:174 +msgid "Save failed." +msgstr "Save failed." + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:103 msgid "Save previewed import" msgstr "Save previewed import" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:226 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:246 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:311 +msgid "saved" +msgstr "saved" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:17 msgid "Saved" msgstr "Saved" @@ -2251,9 +2553,9 @@ msgstr "Saved experiments" msgid "Saved observations: {0}" msgstr "Saved observations: {0}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 -msgid "Saved profile" -msgstr "Saved profile" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:92 +msgid "Saved profile against previous" +msgstr "Saved profile against previous" #: src/components/biometrics/Calibration/ProfileLibrary.tsx:19 msgid "Saved profile revisions" @@ -2265,9 +2567,17 @@ msgstr "Saved profile revisions" msgid "Saved sample times: {0}. Planned observation intervals: {1}." msgstr "Saved sample times: {0}. Planned observation intervals: {1}." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:24 -msgid "Saved settings and processor reports are separate." -msgstr "Saved settings and processor reports are separate." +#: src/components/biometrics/Calibration/CalibrationPage.tsx:196 +msgid "Saved the profile" +msgstr "Saved the profile" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:141 +msgid "Saves it as the profile and requests it; the current saved profile becomes previous. The processor keeps running what it has until it acknowledges the request." +msgstr "Saves it as the profile and requests it; the current saved profile becomes previous. The processor keeps running what it has until it acknowledges the request." + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Saving…" +msgstr "Saving…" #: src/components/biometrics/Calibration/ShadowSchedule.tsx:129 #: src/components/biometrics/Calibration/ShadowSchedule.tsx:130 @@ -2339,10 +2649,6 @@ msgstr "Selected reference revisions: {0} / 20" msgid "Selected sessions: {0} / 60" msgstr "Selected sessions: {0} / 60" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:53 -msgid "Sensor measurements are separate from algorithm personalization." -msgstr "Sensor measurements are separate from algorithm personalization." - #. placeholder {0}: cohort.sessionIds.join(', ') #: src/components/biometrics/Calibration/DatasetLibrary.tsx:78 msgid "Sessions: {0}" @@ -2370,6 +2676,23 @@ msgstr "Shared-timestamp comparisons are unavailable in this report." msgid "Shared-timestamp comparisons. Differences are comparison minus baseline; negative error differences mean lower error on shared observations." msgstr "Shared-timestamp comparisons. Differences are comparison minus baseline; negative error differences mean lower error on shared observations." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:143 +msgid "Shortest breathing interval (× median)" +msgstr "Shortest breathing interval (× median)" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:377 +msgid "show" +msgstr "show" + +#. placeholder {0}: rows.length +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:42 +msgid "Show all {0}" +msgstr "Show all {0}" + +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:42 +msgid "Show recent" +msgstr "Show recent" + #: src/components/biometrics/Calibration/ReplaySessionPicker.tsx:67 msgid "Signal replay unavailable: raw waveforms and processing context have not been retained by this service." msgstr "Signal replay unavailable: raw waveforms and processing context have not been retained by this service." @@ -2382,6 +2705,14 @@ msgstr "Signed bias" msgid "Simulated demo: generated readings, no hardware execution or qualification." msgstr "Simulated demo: generated readings, no hardware execution or qualification." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:168 +msgid "Sleep stages" +msgstr "Sleep stages" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:162 +msgid "SLEEP STAGES" +msgstr "SLEEP STAGES" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:59 msgid "Sleeper identifier" msgstr "Sleeper identifier" @@ -2464,10 +2795,6 @@ msgstr "Stage rules on original readings" msgid "Stage trace for the displayed intervals; exact times and stages follow in the table." msgstr "Stage trace for the displayed intervals; exact times and stages follow in the table." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -msgid "Stale" -msgstr "Stale" - #: src/components/biometrics/Calibration/ReferenceOverlay.tsx:67 #: src/components/biometrics/Calibration/StageTrace.tsx:64 msgid "Start" @@ -2498,6 +2825,11 @@ msgstr "Stop observation" msgid "Stop recorded" msgstr "Stop recorded" +#. placeholder {0}: i.summary.source +#: src/components/biometrics/Calibration/CalibrationPage.tsx:227 +msgid "Stored a {0} reference import" +msgstr "Stored a {0} reference import" + #. placeholder {0}: window.observationCount #: src/components/biometrics/Calibration/NumericReport.tsx:166 msgid "Stored observations: {0}" @@ -2515,7 +2847,8 @@ msgstr "Stored-reading comparison available" #: src/components/biometrics/Calibration/BacktestJobs.tsx:64 #: src/components/biometrics/Calibration/BacktestReport.tsx:60 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 +#: src/components/biometrics/Calibration/CalibrationControls.tsx:80 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:103 #: src/components/biometrics/Calibration/NumericReport.tsx:26 #: src/components/biometrics/Calibration/NumericTrace.tsx:147 #: src/components/biometrics/Calibration/ProfileComparison.tsx:29 @@ -2525,6 +2858,10 @@ msgstr "Stored-reading comparison available" msgid "System defaults" msgstr "System defaults" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:54 +msgid "tested on save" +msgstr "tested on save" + #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:84 msgid "The archive directory is unavailable." msgstr "The archive directory is unavailable." @@ -2538,14 +2875,14 @@ msgstr "The current pod algorithm reports a window IBI variability index, not cl msgid "The frozen offset of {clockOffsetMs} ms is added to recomputed timestamps in this overlay. Reference times stay unchanged. Point matching tolerance: {0} ms." msgstr "The frozen offset of {clockOffsetMs} ms is added to recomputed timestamps in this overlay. Reference times stay unchanged. Point matching tolerance: {0} ms." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:45 -msgid "The HRV index measures window IBI variability. It is not clinical beat-to-beat RMSSD, and incompatible reference metrics cannot validate it." -msgstr "The HRV index measures window IBI variability. It is not clinical beat-to-beat RMSSD, and incompatible reference metrics cannot validate it." - #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:83 msgid "The last archive was unavailable. Continue to inspect the next archive." msgstr "The last archive was unavailable. Continue to inspect the next archive." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:139 +msgid "the median" +msgstr "the median" + #: src/components/biometrics/Calibration/ShadowReport.tsx:19 msgid "The observation gap exceeded the planned limit." msgstr "The observation gap exceeded the planned limit." @@ -2554,10 +2891,6 @@ msgstr "The observation gap exceeded the planned limit." msgid "The observation window was missed." msgstr "The observation window was missed." -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:27 -msgid "The pinned profile is incompatible. Its revision will be preserved, but a valid before-and-after comparison is unavailable." -msgstr "The pinned profile is incompatible. Its revision will be preserved, but a valid before-and-after comparison is unavailable." - #: src/components/biometrics/Calibration/ShadowSchedule.tsx:77 msgid "The plan could not be prepared. Check the selected profile, window and installed qualification." msgstr "The plan could not be prepared. Check the selected profile, window and installed qualification." @@ -2579,12 +2912,15 @@ msgstr "The request could not be saved for recovery. This attempt was not sent. msgid "The retained inputs are unavailable for new evaluations." msgstr "The retained inputs are unavailable for new evaluations." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:171 +msgid "The rule list itself differs from the shipped rules." +msgstr "The rule list itself differs from the shipped rules." + #: src/components/biometrics/Calibration/ProfileComparison.tsx:33 msgid "The saved profile changed. Compare again before reviewing these changes." msgstr "The saved profile changed. Compare again before reviewing these changes." #: src/components/biometrics/Calibration/ProfileComparison.tsx:33 -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:34 msgid "The saved profile changed. Select a recovery action again to make a new request." msgstr "The saved profile changed. Select a recovery action again to make a new request." @@ -2636,6 +2972,10 @@ msgstr "These scores compare original stored readings. Identical numerical resul msgid "These scores compare readings recomputed from retained waveforms. Shared-timestamp scores use observations available in every configuration. Differences describe agreement with the reference and do not establish validated improvement." msgstr "These scores compare readings recomputed from retained waveforms. Shared-timestamp scores use observations available in every configuration. Differences describe agreement with the reference and do not establish validated improvement." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:237 +msgid "This backtest compared stored readings, so there is no recomputed heart-rate trace to draw." +msgstr "This backtest compared stored readings, so there is no recomputed heart-rate trace to draw." + #: src/components/biometrics/Calibration/StageTrace.tsx:41 msgid "This chart shows only the current page of replay intervals. Gaps are left empty; no stages are inferred between intervals." msgstr "This chart shows only the current page of replay intervals. Gaps are left empty; no stages are inferred between intervals." @@ -2709,7 +3049,6 @@ msgstr "Time" msgid "Times shown in {0}" msgstr "Times shown in {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:23 #: src/components/biometrics/Calibration/NumericReport.tsx:85 #: src/components/biometrics/Calibration/StageTrace.tsx:43 msgid "Times shown in {timezone}" @@ -2744,6 +3083,19 @@ msgstr "Trace intervals {first}–{last} of {0}" msgid "Trace night" msgstr "Trace night" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:125 +msgid "Tracking history length" +msgstr "Tracking history length" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:115 +msgid "Tracking tolerance (bpm)" +msgstr "Tracking tolerance (bpm)" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:112 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "tuning" +msgstr "tuning" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 #: src/components/biometrics/Calibration/BacktestSubmission.tsx:73 @@ -2763,6 +3115,10 @@ msgstr "Unavailable" msgid "Unavailable or belongs to a different sleeper." msgstr "Unavailable or belongs to a different sleeper." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:53 +msgid "unchanged" +msgstr "unchanged" + #. placeholder {0}: parameter.unit #. placeholder {1}: parameter.minimum #. placeholder {2}: parameter.maximum @@ -2790,10 +3146,6 @@ msgstr "Unmatched references" msgid "Use offset-bearing ISO timestamps or Unix milliseconds. Each record needs recordKey, metric, value, unit, and timestamp (or start and end). JSON uses an array; CSV uses these field names as headers. Imports are limited to 2 MiB and 2000 records." msgstr "Use offset-bearing ISO timestamps or Unix milliseconds. Each record needs recordKey, metric, value, unit, and timestamp (or start and end). JSON uses an array; CSV uses these field names as headers. Imports are limited to 2 MiB and 2000 records." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Use system defaults" -msgstr "Use system defaults" - #: src/components/biometrics/Calibration/ShadowSchedule.tsx:145 msgid "Use the same sleeper identity as the reference records you intend to compare." msgstr "Use the same sleeper identity as the reference records you intend to compare." @@ -2802,6 +3154,11 @@ msgstr "Use the same sleeper identity as the reference records you intend to com msgid "Validated activation" msgstr "Validated activation" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:116 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "validation" +msgstr "validation" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 #: src/components/biometrics/Calibration/BacktestSubmission.tsx:74 @@ -2827,8 +3184,6 @@ msgstr "Value / unit" msgid "Vitals with unknown original processing provenance: {0}" msgstr "Vitals with unknown original processing provenance: {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Waiting for a processing boundary" msgstr "Waiting for a processing boundary" @@ -2837,8 +3192,6 @@ msgstr "Waiting for a processing boundary" msgid "Waiting for a worker and resource admission. No execution is implied by a queued request." msgstr "Waiting for a worker and resource admission. No execution is implied by a queued request." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Waiting for delivery" msgstr "Waiting for delivery" @@ -2856,9 +3209,14 @@ msgstr "Wake" msgid "Waveform dataset retained: {published}" msgstr "Waveform dataset retained: {published}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:44 -msgid "What these metrics mean" -msgstr "What these metrics mean" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:157 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:180 +msgid "What the saved profile did" +msgstr "What the saved profile did" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:176 +msgid "when" +msgstr "when" #: src/components/biometrics/Calibration/DatasetBuilder.tsx:59 #: src/components/biometrics/Calibration/DatasetLibrary.tsx:70 @@ -2875,6 +3233,10 @@ msgstr "Window IBI variability index" msgid "Window IBI variability index (ms)" msgstr "Window IBI variability index (ms)" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:158 +msgid "window IBI variability, not clinical RMSSD" +msgstr "window IBI variability, not clinical RMSSD" + #: src/components/biometrics/Calibration/ShadowReport.tsx:34 msgid "Within the observed limits." msgstr "Within the observed limits." @@ -2883,6 +3245,10 @@ msgstr "Within the observed limits." msgid "Work was deferred; your selection and retry key are preserved." msgstr "Work was deferred; your selection and retry key are preserved." +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:41 +msgid "WORKBENCH" +msgstr "WORKBENCH" + #: src/components/biometrics/Calibration/ShadowReport.tsx:18 msgid "Worker cleanup has not been confirmed." msgstr "Worker cleanup has not been confirmed." diff --git a/src/lib/i18n/locales/es.po b/src/lib/i18n/locales/es.po index 6abcc6ce..2c683f25 100644 --- a/src/lib/i18n/locales/es.po +++ b/src/lib/i18n/locales/es.po @@ -13,6 +13,18 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:169 +msgid ", with {count} overrides" +msgstr ", con {count} ajustes propios" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:169 +msgid ", with 1 override" +msgstr ", con 1 ajuste propio" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:369 +msgid ". Separate from the profile." +msgstr ". Independiente del perfil." + #. placeholder {0}: item.summary.source #. placeholder {1}: item.summary.subjectId #. placeholder {2}: item.revision @@ -20,6 +32,31 @@ msgstr "" msgid "{0} · {1} · revision {2}" msgstr "{0} · {1} · revisión {2}" +#. placeholder {0}: label.weekday +#. placeholder {1}: label.day +#. placeholder {2}: fmt1(prev) +#. placeholder {3}: fmt1(saved) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:198 +msgid "{0} {1}: previous {2}, saved {3} bpm" +msgstr "{0} {1}: anterior {2}, guardado {3} lpm" + +#. placeholder {0}: splitLabel[job.split] +#. placeholder {1}: avg(n => n.previous.heart_rate.mae as number) +#. placeholder {2}: avg(n => n.saved.heart_rate.mae as number) +#. placeholder {3}: shortDuration(job.updatedAtMs - job.createdAtMs) +#: src/components/biometrics/Calibration/CalibrationPage.tsx:216 +msgid "{0} HR error {1} → {2} bpm · {3}" +msgstr "{0} error de FC {1} → {2} lpm · {3}" + +#. placeholder {0}: i.summary.accepted +#: src/components/biometrics/Calibration/CalibrationPage.tsx:227 +msgid "{0} records" +msgstr "{0} registros" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:160 +msgid "{stages} sleep-stage values" +msgstr "{stages} valores de fases del sueño" + #: src/components/biometrics/Calibration/NumericReport.tsx:175 msgid "95th percentile absolute error" msgstr "Percentil 95 del error absoluto" @@ -32,6 +69,18 @@ msgstr "Una solicitud guardada no significa que la ejecución haya comenzado o f msgid "A saved revision is not evidence of validation or processor adoption." msgstr "Una revisión guardada no demuestra validación ni adopción por el procesador." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:109 +msgid "Accept an estimate when it is within" +msgstr "Aceptar una estimación cuando esté a menos de" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:156 +msgid "accepted 10-second IBI windows" +msgstr "ventanas IBI de 10 segundos aceptadas" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:123 +msgid "accepted estimates in the tracker" +msgstr "estimaciones aceptadas en el seguimiento" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:87 msgid "Accepted records are not necessarily usable for validation. Incompatible HRV definitions remain unscorable." msgstr "Los registros aceptados no son necesariamente válidos para la validación. Las definiciones incompatibles de VFC no se pueden evaluar." @@ -53,10 +102,6 @@ msgstr "Aceptados: {0}; rechazados: {1}; duplicados: {2}; no evaluables: {3}" msgid "Accepted: {0}; rejected: {1}; duplicates: {2}; unscorable: {3}; conflicts: {4}" msgstr "Aceptados: {0}; rechazados: {1}; duplicados: {2}; no evaluables: {3}; conflictos: {4}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:21 -msgid "Activation history" -msgstr "Historial de activación" - #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:76 msgid "Activation request saved. Processor acknowledgement is reported separately below." msgstr "Solicitud de activación guardada. La confirmación del procesador se indica por separado a continuación." @@ -80,6 +125,10 @@ msgstr "Generaciones activas: izquierda {0}, derecha {1}." msgid "Active profile hash" msgstr "Hash del perfil activo" +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:21 +msgid "Activity" +msgstr "Actividad" + #: src/components/biometrics/Calibration/NumericReport.tsx:111 msgid "Added readings" msgstr "Lecturas añadidas" @@ -122,8 +171,6 @@ msgstr "Hay otro problema en el informe que requiere revisión." msgid "Another revision from this reference family is selected." msgstr "Se ha seleccionado otra revisión de esta familia de referencias." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Applied" msgstr "Aplicado" @@ -132,10 +179,6 @@ msgstr "Aplicado" msgid "Apply evaluated revision" msgstr "Aplicar la revisión evaluada" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Apply profile" -msgstr "Aplicar perfil" - #: src/components/biometrics/Calibration/ValidatedProfileActivation.tsx:32 msgid "Apply this exact candidate using the selected evaluation. Evidence and current settings are checked again when applying." msgstr "Aplica este candidato exacto usando la evaluación seleccionada. La evidencia y la configuración actual se comprueban de nuevo al aplicarlo." @@ -172,10 +215,20 @@ msgstr "Intento {0}" msgid "Available reference records by metric (across revisions)" msgstr "Registros de referencia disponibles por métrica (entre revisiones)" +#. placeholder {0}: group.latest.id.slice(0, 4) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:181 +msgid "Backtest {0}. Frozen dataset replayed through the previous and saved profiles; nothing here changes the pod." +msgstr "Prueba retrospectiva {0}. El conjunto congelado se reprodujo con los perfiles anterior y guardado; nada aquí cambia el pod." + #: src/components/biometrics/Calibration/BacktestJobs.tsx:21 msgid "Backtest jobs" msgstr "Trabajos de evaluación retrospectiva" +#. placeholder {0}: splitLabel[job.split] +#: src/components/biometrics/Calibration/CalibrationPage.tsx:220 +msgid "Backtest on {0} nights failed" +msgstr "Falló la prueba retrospectiva en noches de {0}" + #. placeholder {0}: enqueue.data.id #: src/components/biometrics/Calibration/BacktestSubmission.tsx:93 msgid "Backtest request saved: {0}" @@ -185,7 +238,7 @@ msgstr "Solicitud de evaluación retrospectiva guardada: {0}" msgid "Backtest submission" msgstr "Envío de evaluación retrospectiva" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:29 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:30 msgid "Backtests" msgstr "Evaluaciones retrospectivas" @@ -201,11 +254,26 @@ msgstr "Antes" msgid "Before configuration hash" msgstr "Hash de configuración anterior" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:24 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:68 msgid "Biometric calibration" msgstr "Calibración biométrica" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:264 +msgid "Biometric calibration side" +msgstr "Lado de la calibración biométrica" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:113 +msgid "bpm of the tracker" +msgstr "lpm del seguimiento" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:128 +msgid "BREATHING" +msgstr "RESPIRACIÓN" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:173 +msgid "Breathing error" +msgstr "Error de respiración" + #: src/components/biometrics/Calibration/ShadowReport.tsx:30 msgid "Breathing rate" msgstr "Frecuencia respiratoria" @@ -216,10 +284,18 @@ msgstr "Frecuencia respiratoria" msgid "Breathing rate (breaths/min)" msgstr "Frecuencia respiratoria (respiraciones/min)" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:25 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:38 +msgid "Calibration" +msgstr "Calibración" + +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:34 msgid "Calibration views" msgstr "Vistas de calibración" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:136 +msgid "Cancel" +msgstr "Cancelar" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:79 msgid "Cancel backtest" msgstr "Cancelar evaluación retrospectiva" @@ -318,6 +394,14 @@ msgstr "Reglas de fases del sueño del candidato" msgid "Candidate spike threshold" msgstr "Umbral de picos del candidato" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:94 +msgid "Changed values are white; the previous profile (System defaults) sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." +msgstr "Los valores cambiados aparecen en blanco; el perfil anterior (valores del sistema) queda debajo. Mover un control edita el perfil guardado; guardarlo de nuevo lo vuelve a solicitar. El procesador adopta un perfil solo después de confirmar una solicitud." + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:95 +msgid "Changed values are white; the previous profile sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." +msgstr "Los valores cambiados aparecen en blanco; el perfil anterior queda debajo. Mover un control edita el perfil guardado; guardarlo de nuevo lo vuelve a solicitar. El procesador adopta un perfil solo después de confirmar una solicitud." + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:86 msgid "Check Overview for current processor health; a past acknowledgement does not prove current health." msgstr "Consulte Resumen para ver el estado actual del procesador; una confirmación anterior no demuestra el estado actual." @@ -350,9 +434,9 @@ msgstr "Los círculos son lecturas recalculadas; los cuadrados son puntos de ref msgid "Clear this night’s selection" msgstr "Borrar la selección de esta noche" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 -msgid "Clock mismatch" -msgstr "Desajuste de reloj" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:187 +msgid "click a night" +msgstr "elige una noche" #. placeholder {0}: minutes(score.pairedDurationMs) #: src/components/biometrics/Calibration/StageReport.tsx:17 @@ -443,10 +527,6 @@ msgstr "MAE de la comparación con observaciones comunes" msgid "Compatibility" msgstr "Compatibilidad" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Compatibility fallback" -msgstr "Configuración alternativa compatible" - #: src/components/biometrics/Calibration/ReferenceOverlay.tsx:90 msgid "Compatible" msgstr "Compatible" @@ -464,15 +544,15 @@ msgstr "Noches completadas" msgid "Compressed bytes: {0}" msgstr "Bytes comprimidos: {0}" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:372 +msgid "config id" +msgstr "id de configuración" + #: src/components/biometrics/Calibration/BacktestReport.tsx:98 #: src/components/biometrics/Calibration/StageReport.tsx:107 msgid "Configuration" msgstr "Configuración" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:36 -msgid "Configuration identity" -msgstr "Identidad de la configuración" - #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:58 msgid "Confirm import deletion" msgstr "Confirmar la eliminación de la importación" @@ -506,10 +586,13 @@ msgstr "Cobertura reducida" msgid "Coverage reduced in at least one evaluated window" msgstr "Cobertura reducida en al menos una ventana evaluada" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Current" -msgstr "Actual" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:61 +msgid "covered" +msgstr "cubierto" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "current" +msgstr "vigente" #: src/components/biometrics/Calibration/BacktestReport.tsx:60 #: src/components/biometrics/Calibration/NumericReport.tsx:26 @@ -543,6 +626,10 @@ msgstr "Conjunto de datos" msgid "Dataset hash" msgstr "Hash del conjunto de datos" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:84 +msgid "Dataset nights" +msgstr "Noches del conjunto" + #. placeholder {0}: create.data.id #: src/components/biometrics/Calibration/DatasetBuilder.tsx:57 msgid "Dataset saved: {0}" @@ -557,10 +644,6 @@ msgstr "Identificador de la persona del conjunto de datos" msgid "Deep" msgstr "Profundo" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:31 -msgid "Default recovery configuration hash" -msgstr "Hash de la configuración predeterminada de recuperación" - #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:52 msgid "Delete import" msgstr "Eliminar importación" @@ -577,13 +660,10 @@ msgstr "Eliminar una importación borra sus mediciones de origen. Los conjuntos msgid "Descriptive evaluation only" msgstr "Solo evaluación descriptiva" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 -msgid "Different versions reported" -msgstr "Se han informado versiones diferentes" - -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:57 -msgid "Direct sleep-stage changes are unvalidated manual settings. Saving requests a profile change; processor adoption is reported separately in Overview." -msgstr "Los cambios directos de las fases del sueño son ajustes manuales sin validar. Al guardar se solicita un cambio de perfil; su adopción por el procesador se informa por separado en Resumen." +#. placeholder {0}: shortDuration(run.durationMs) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "done · {0}" +msgstr "listo · {0}" #: src/components/biometrics/Calibration/NumericReport.tsx:151 msgid "Dropping readings can lower own-support error. Duplicate timestamps are excluded from shared support. These counts do not measure continuous time coverage." @@ -601,6 +681,10 @@ msgstr "Cada configuración en sus propios intervalos disponibles" msgid "Edit a new candidate from this revision" msgstr "Editar un nuevo candidato a partir de esta revisión" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:192 +msgid "Edit overrides" +msgstr "Editar ajustes" + #: src/components/biometrics/Calibration/ProfileLibrary.tsx:20 msgid "Editing from revision {revisionId}" msgstr "Editando a partir de la revisión {revisionId}" @@ -660,6 +744,10 @@ msgstr "Estimar incertidumbre exploratoria por noches completas" msgid "Evaluated minutes: {0}" msgstr "Minutos evaluados: {0}" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:75 +msgid "Evaluation dataset" +msgstr "Conjunto de evaluación" + #: src/components/biometrics/Calibration/ExperimentBuilder.tsx:47 msgid "Evaluation mode" msgstr "Modo de evaluación" @@ -693,10 +781,6 @@ msgstr "Las fechas normalizadas exactas se muestran en UTC. Los valores no se in msgid "Exact processed-data coverage" msgstr "Cobertura exacta de los datos procesados" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:45 -msgid "Exact profile transition" -msgstr "Cambio exacto de perfil" - #: src/components/biometrics/Calibration/NumericTrace.tsx:93 msgid "Exact recomputed readings, without reference clock adjustment" msgstr "Lecturas recalculadas exactas, sin ajuste del reloj de referencia" @@ -728,9 +812,9 @@ msgstr "El estado de ejecución es independiente de la solidez de la evidencia b msgid "Experiment saved: {0}" msgstr "Experimento guardado: {0}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Expired" -msgstr "Caducada" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "expired" +msgstr "caducada" #: src/components/biometrics/Calibration/CandidateStageFields.tsx:39 msgid "Explicit stage overrides" @@ -757,19 +841,38 @@ msgstr "Intervalo exploratorio estimado" msgid "F1" msgstr "F1" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "failed" +msgstr "falló" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 #: src/components/biometrics/Calibration/ShadowRuns.tsx:12 msgid "Failed" msgstr "Fallido" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:184 +msgid "Fallback" +msgstr "Fase por defecto" + #: src/components/biometrics/Calibration/CandidateStageFields.tsx:21 #: src/components/biometrics/Calibration/CandidateStageFields.tsx:21 msgid "Fallback stage" msgstr "Fase alternativa" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:202 +msgid "Fell back to a compatible profile" +msgstr "Se recurrió a un perfil compatible" + +#. placeholder {0}: splitLabel[job.split] +#: src/components/biometrics/Calibration/CalibrationPage.tsx:220 +msgid "Finished a backtest on {0} nights" +msgstr "Terminó una prueba retrospectiva en noches de {0}" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:314 +msgid "first 500 readings" +msgstr "primeras 500 lecturas" + #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:125 msgid "First archive page" msgstr "Primera página de archivos" @@ -794,6 +897,10 @@ msgstr "Desplazamiento fijo añadido a las fechas del pod (ms, ±3600000)" msgid "Flags" msgstr "Indicadores" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:128 +msgid "Freeze a new dataset" +msgstr "Congelar un conjunto nuevo" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:33 msgid "Freeze evaluation inputs" msgstr "Fijar datos de evaluación" @@ -802,9 +909,31 @@ msgstr "Fijar datos de evaluación" msgid "Freeze selected inputs" msgstr "Fijar datos seleccionados" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Freshness unknown" -msgstr "Vigencia desconocida" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "freshness unknown" +msgstr "vigencia desconocida" + +#. placeholder {0}: d.manifest.splitProtocol.assignments.length +#: src/components/biometrics/Calibration/CalibrationPage.tsx:224 +msgid "Froze a dataset of {0} nights" +msgstr "Congeló un conjunto de {0} noches" + +#. placeholder {0}: formatDay(dataset.createdAtMs, timezone) +#. placeholder {1}: formatBytes(dataset.byteLength) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:79 +msgid "frozen {0} · {1}" +msgstr "congelado {0} · {1}" + +#. placeholder {0}: nights.length +#. placeholder {1}: sources.join(' + ') +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:69 +msgid "Frozen copy of {0} nights and {1} reference. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." +msgstr "Copia congelada de {0} noches y la referencia de {1}. Las ediciones o eliminaciones posteriores no la afectan. Las noches se reparten enteras antes de puntuar; un reparto no es evidencia por sí solo." + +#. placeholder {0}: nights.length +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:70 +msgid "Frozen copy of {0} nights. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." +msgstr "Copia congelada de {0} noches. Las ediciones o eliminaciones posteriores no la afectan. Las noches se reparten enteras antes de puntuar; un reparto no es evidencia por sí solo." #: src/components/biometrics/Calibration/DatasetLibrary.tsx:85 msgid "Frozen coverage and capabilities" @@ -838,26 +967,32 @@ msgstr "Registros de origen fijados en UTC; los registros incompatibles se enume msgid "Frozen stage objective thresholds" msgstr "Umbrales fijados de los objetivos de fases del sueño" -#. placeholder {0}: data.desired.binding.generation -#. placeholder {0}: event.generation -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:41 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:39 -msgid "Generation {0}" -msgstr "Generación {0}" - #: src/components/biometrics/Calibration/NumericReport.tsx:44 msgid "Groups use native reference timestamps and record types, independently of candidate output. Half-window groups exclude intervals crossing the midpoint. Groups overlap and are not independent experiments. These are not motion or artifact subgroups." msgstr "Los grupos usan las marcas de tiempo y los tipos de registro originales de la referencia, independientemente de la salida del candidato. Los grupos de medio período excluyen los intervalos que cruzan el punto medio. Los grupos se solapan y no son experimentos independientes. No son subgrupos de movimiento ni de artefactos." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:228 +msgid "H holdout" +msgstr "H reserva" + #: src/components/biometrics/Calibration/ReplayResources.tsx:49 msgid "Health was sampled at admission checks, normally five seconds apart. Unobserved intervals are not continuous health measurements." msgstr "El estado se muestreó en las comprobaciones de admisión, normalmente cada cinco segundos. Los intervalos sin observaciones no constituyen mediciones continuas del estado." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 #: src/components/biometrics/Calibration/ShadowReport.tsx:30 msgid "Heart rate" msgstr "Frecuencia cardíaca" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:106 +msgid "HEART RATE" +msgstr "FRECUENCIA CARDÍACA" + +#. placeholder {0}: label.weekday +#. placeholder {1}: label.day +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:291 +msgid "Heart rate · {0} {1}" +msgstr "Frecuencia cardíaca · {0} {1}" + #: src/components/biometrics/Calibration/NumericReport.tsx:27 #: src/components/biometrics/Calibration/NumericTrace.tsx:38 #: src/components/biometrics/Calibration/ShadowComparison.tsx:44 @@ -868,10 +1003,18 @@ msgstr "Frecuencia cardíaca (latidos/min)" msgid "Heart-rate spike threshold (standard deviations)" msgstr "Umbral de picos de frecuencia cardíaca (desviaciones estándar)" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:295 +msgid "Heart-rate trace" +msgstr "Trazo de frecuencia cardíaca" + #: app/[lang]/health/HealthcheckDemo.tsx:13 msgid "Hello, tRPC test page!" msgstr "¡Hola, tRPC test page!" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:377 +msgid "hide" +msgstr "ocultar" + #. placeholder {0}: summary.improvedAgreementNights #. placeholder {1}: summary.regressedAgreementNights #. placeholder {2}: summary.unchangedAgreementNights @@ -879,13 +1022,10 @@ msgstr "¡Hola, tRPC test page!" msgid "Higher agreement: {0}; lower: {1}; unchanged: {2}" msgstr "Mayor concordancia: {0}; menor: {1}; sin cambios: {2}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:46 -msgid "Historical sleep stages are recomputed with the current classifier. Original stored vitals remain separate from replay results." -msgstr "Las fases históricas del sueño se recalculan con el clasificador actual. Los signos vitales originales almacenados se mantienen separados de los resultados del reprocesamiento." - -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:30 -msgid "History" -msgstr "Historial" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:120 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "holdout" +msgstr "reserva" #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 @@ -900,6 +1040,26 @@ msgstr "Reserva" msgid "Holdout accesses recorded for this experiment: {0}" msgstr "Accesos a la reserva registrados para este experimento: {0}" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:258 +msgid "Holdout is the number to trust; tuning nights were used to pick these values." +msgstr "La reserva es la cifra fiable; las noches de ajuste se usaron para elegir estos valores." + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:170 +msgid "HR error · holdout" +msgstr "Error de FC · reserva" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:168 +msgid "HR error · tuning" +msgstr "Error de FC · ajuste" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:169 +msgid "HR error · validation" +msgstr "Error de FC · validación" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:186 +msgid "HR error per night · bpm vs reference" +msgstr "Error de FC por noche · lpm frente a la referencia" + #: src/components/biometrics/BiometricsPage.tsx:532 msgid "HRV definition" msgstr "Definición de VFC" @@ -908,9 +1068,13 @@ msgstr "Definición de VFC" msgid "HRV definitions differ or are partly unknown. A combined HRV average is unavailable." msgstr "Las definiciones de VFC difieren o se desconocen en parte. No se puede calcular un promedio combinado de VFC." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 -msgid "HRV index" -msgstr "Índice de VFC" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:149 +msgid "HRV INDEX" +msgstr "ÍNDICE DE VFC" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:172 +msgid "HRV index error" +msgstr "Error del índice de VFC" #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:60 msgid "I intend to apply this exact revision without validation evidence." @@ -928,6 +1092,10 @@ msgstr "Quiero restaurar los valores predeterminados del sistema sin evidencia d msgid "I intend to restore the exact previous profile without validation evidence." msgstr "Quiero restaurar el perfil anterior exacto sin evidencia de validación." +#: src/components/biometrics/Calibration/CalibrationControls.tsx:147 +msgid "Identical to the saved profile; restoring only re-requests it." +msgstr "Idéntico al perfil guardado; restaurar solo lo vuelve a solicitar." + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:53 #: src/components/biometrics/Calibration/ValidatedProfileActivation.tsx:38 msgid "Immediate processing boundary" @@ -941,6 +1109,10 @@ msgstr "Importar los registros aceptados y omitir los rechazados" msgid "Import preview" msgstr "Vista previa de la importación" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:126 +msgid "Import reference" +msgstr "Importar referencia" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:53 msgid "Import reference measurements" msgstr "Importar mediciones de referencia" @@ -972,6 +1144,11 @@ msgstr "Muestras de recepción de datos: {0} de {1} comprobaciones de admisión" msgid "Inherited default" msgstr "Valor predeterminado heredado" +#. placeholder {0}: stages.ruleCount +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:166 +msgid "Inherits the {0} rules and fallback from" +msgstr "Hereda las {0} reglas y la fase por defecto de" + #: src/components/biometrics/Calibration/DatasetLibrary.tsx:31 msgid "Inputs released; metadata retained" msgstr "Datos liberados; metadatos conservados" @@ -1063,6 +1240,14 @@ msgstr "Conservar informe" msgid "Keep retained inputs" msgstr "Conservar los datos retenidos" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Keep saved profile" +msgstr "Mantener el perfil guardado" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:119 +msgid "Keep the last" +msgstr "Conservar las últimas" + #. placeholder {0}: display(operations.ingestion.maximumObservationGapMs, 1000) #: src/components/biometrics/Calibration/ReplayResources.tsx:47 msgid "Largest observation gap: {0} seconds" @@ -1095,6 +1280,10 @@ msgstr "Ligero" msgid "Limitations" msgstr "Limitaciones" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:144 +msgid "Longest breathing interval (× median)" +msgstr "Intervalo respiratorio más largo (× mediana)" + #: src/components/biometrics/Calibration/NumericReport.tsx:110 msgid "Lost readings" msgstr "Lecturas perdidas" @@ -1144,9 +1333,9 @@ msgstr "Error absoluto medio en marcas de tiempo comunes: activo {0}, candidato msgid "Measured Python processes: {0} of {1}" msgstr "Procesos Python medidos: {0} de {1}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:53 -msgid "Measured sensor calibration" -msgstr "Calibración medida de los sensores" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:366 +msgid "Measured sensor calibration for this side:" +msgstr "Calibración medida del sensor para este lado:" #: src/components/biometrics/Calibration/NumericReport.tsx:174 msgid "Median absolute error" @@ -1176,9 +1365,13 @@ msgstr "Ventana de la métrica: {0} segundos." msgid "Metrics: {0}" msgstr "Métricas: {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Migrate settings" -msgstr "Migrar ajustes" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:202 +msgid "Migrated sleep-stage settings into the profile" +msgstr "Migró los ajustes de fases del sueño al perfil" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:159 +msgid "Minimum IBI windows" +msgstr "Ventanas IBI mínimas" #: src/components/biometrics/Calibration/ExperimentBuilder.tsx:97 msgid "Minimum paired nights (3–60)" @@ -1205,10 +1398,6 @@ msgstr "El procesamiento de nuevos archivos está aplazado por ahora." msgid "New observation plan" msgstr "Nuevo plan de observación" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:62 -msgid "Newest" -msgstr "Más recientes" - #: src/components/biometrics/Calibration/DatasetLibrary.tsx:41 msgid "Newest datasets" msgstr "Conjuntos de datos recientes" @@ -1280,6 +1469,10 @@ msgstr "Siguiente página de la traza" msgid "Night {0}" msgstr "Noche {0}" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:171 +msgid "Night covered" +msgstr "Noche cubierta" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:74 msgid "Night progress is not available yet." msgstr "El progreso por noches aún no está disponible." @@ -1288,9 +1481,9 @@ msgstr "El progreso por noches aún no está disponible." msgid "No accepted time range." msgstr "No hay un intervalo de tiempo aceptado." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:30 -msgid "No activation history for this side." -msgstr "No hay historial de activación para este lado." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:158 +msgid "No backtest has replayed the saved profile yet. Saving a change re-runs the latest experiment on its tuning and validation nights; nothing here changes the pod." +msgstr "Ninguna prueba retrospectiva ha reproducido aún el perfil guardado. Guardar un cambio vuelve a ejecutar el último experimento en sus noches de ajuste y validación; nada aquí cambia el pod." #: src/components/biometrics/Calibration/BacktestJobs.tsx:28 msgid "No backtest jobs for this side." @@ -1312,6 +1505,10 @@ msgstr "No hay ningún comprobante de recursos de un proceso de trabajo completa msgid "No coverage reduction detected" msgstr "No se detectó reducción de cobertura" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:71 +msgid "No dataset is frozen for this side yet. Freeze recorded nights with an imported reference to backtest a profile." +msgstr "Aún no hay un conjunto congelado para este lado. Congela noches grabadas con una referencia importada para probar un perfil." + #: src/components/biometrics/Calibration/BacktestReport.tsx:62 #: src/components/biometrics/Calibration/NumericReport.tsx:28 msgid "No estimable variation" @@ -1366,10 +1563,18 @@ msgstr "No hay ejecuciones prospectivas para este lado." msgid "No recorded nights for this side." msgstr "No hay noches registradas para este lado." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:101 +msgid "no reference" +msgstr "sin referencia" + #: src/components/biometrics/Calibration/NumericReport.tsx:29 msgid "No reference" msgstr "Sin referencia" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:286 +msgid "no reference device · excluded from error" +msgstr "sin dispositivo de referencia · excluida del error" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:91 #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:35 msgid "No reference imports for this side." @@ -1404,10 +1609,6 @@ msgstr "No hay revisiones de candidatos guardadas." msgid "No saved experiments for this side." msgstr "No hay experimentos guardados para este lado." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:55 -msgid "No sensor calibration has been reported for this side." -msgstr "No se ha informado ninguna calibración de sensores para este lado." - #: src/components/biometrics/Calibration/StageTrace.tsx:84 msgid "No stage intervals were produced for this selection." msgstr "No se generaron intervalos de fases para esta selección." @@ -1434,8 +1635,10 @@ msgstr "Ninguno" msgid "Normalized reference measurements" msgstr "Mediciones de referencia normalizadas" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:26 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:362 +msgid "not reported" +msgstr "sin informar" + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Not reported" msgstr "Sin información" @@ -1453,6 +1656,19 @@ msgstr "No solicitado" msgid "Not scored" msgstr "Sin puntuación" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:55 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:57 +msgid "not tested" +msgstr "sin probar" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:330 +msgid "Nothing changes until you move a control or restore." +msgstr "Nada cambia hasta que muevas un control o restaures." + +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:24 +msgid "Nothing has happened on this side yet." +msgstr "Aún no ha pasado nada en este lado." + #: src/components/biometrics/Calibration/NumericTrace.tsx:158 msgid "Numeric trace configuration" msgstr "Configuración de la traza numérica" @@ -1523,10 +1739,6 @@ msgstr "Pico observado de RSS del grupo de procesos: {0} MiB" msgid "Observing" msgstr "Observando" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:63 -msgid "Older changes" -msgstr "Cambios anteriores" - #: src/components/biometrics/Calibration/DatasetLibrary.tsx:42 msgid "Older datasets" msgstr "Conjuntos de datos anteriores" @@ -1558,6 +1770,10 @@ msgstr "Perfiles anteriores" msgid "Only the current page is plotted. Dots represent individual readings; gaps and missing values are not interpolated. Original stored readings remain unchanged." msgstr "Solo se representa la página actual. Los puntos son lecturas individuales; los huecos y los valores faltantes no se interpolan. Las lecturas originales almacenadas no se modifican." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:135 +msgid "or longer than" +msgstr "o más largos que" + #: src/components/biometrics/Calibration/StageTrace.tsx:61 msgid "Original replay timestamps, without reference clock adjustment" msgstr "Marcas de tiempo originales del reprocesamiento, sin ajuste al reloj de referencia" @@ -1601,10 +1817,14 @@ msgstr "Personalizado" msgid "Override intent can change even when the resolved value stays the same." msgstr "La intención de personalización puede cambiar aunque el valor resuelto sea el mismo." -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:26 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:27 msgid "Overview" msgstr "Resumen" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:330 +msgid "Overwrites the saved profile and requests it; the current one becomes previous." +msgstr "Sobrescribe el perfil guardado y lo solicita; el actual pasa a ser el anterior." + #: src/components/biometrics/Calibration/NumericReport.tsx:109 msgid "Own-support MAE difference" msgstr "Diferencia de MAE con observaciones propias" @@ -1630,10 +1850,6 @@ msgstr "Revisión de origen" msgid "Partition timezone: {0}" msgstr "Zona horaria de las particiones: {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:21 -msgid "Past acknowledgements do not establish which profile is running now." -msgstr "Las confirmaciones anteriores no indican qué perfil se está ejecutando ahora." - #: src/components/biometrics/Calibration/ShadowComparison.tsx:50 msgid "Paste JSON records or CSV with recordKey, metric, value, unit, and timestamp (or start and end). Use offset-bearing ISO times or Unix milliseconds. This comparison does not save a reference import." msgstr "Pega registros JSON o CSV con recordKey, metric, value, unit y timestamp (o start y end). Usa fechas ISO con desfase horario o milisegundos Unix. Esta comparación no guarda una importación de referencia." @@ -1654,9 +1870,9 @@ msgstr "Liberar datos permanentemente" msgid "Permanently release report" msgstr "Liberar informe permanentemente" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 -msgid "Personalized profile" -msgstr "Perfil personalizado" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:238 +msgid "Pick a scored night to see its heart-rate trace." +msgstr "Elige una noche puntuada para ver su trazo de frecuencia cardíaca." #: src/components/biometrics/Calibration/ProfileLibrary.tsx:38 msgid "Pinned configuration and overrides" @@ -1733,10 +1949,6 @@ msgstr "Preparado, aún no admitido. El pod comprueba de nuevo la cualificación msgid "Presence and pump context reflect host observations, not proof of the live processor’s cached state." msgstr "El contexto de presencia y de la bomba refleja observaciones del host, no demuestra el estado almacenado en la caché del procesador en vivo." -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:29 -msgid "Preserved incompatible revision" -msgstr "Revisión incompatible conservada" - #: src/components/biometrics/Calibration/CalibrationReferences.tsx:78 msgid "Preview import" msgstr "Vista previa de la importación" @@ -1745,6 +1957,18 @@ msgstr "Vista previa de la importación" msgid "Preview normalized records before saving them on the pod." msgstr "Revise los registros normalizados antes de guardarlos en el pod." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:222 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:245 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:312 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:65 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:141 +msgid "previous" +msgstr "anterior" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:77 +msgid "Previous" +msgstr "Anterior" + #: src/components/biometrics/Calibration/NumericTrace.tsx:134 msgid "Previous numeric page" msgstr "Página numérica anterior" @@ -1770,19 +1994,33 @@ msgstr "El procesamiento falló." msgid "Processing run" msgstr "Ejecución de procesamiento" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:27 -msgid "Processor health" -msgstr "Estado del procesador" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:19 +msgid "Processor" +msgstr "Procesador" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:24 -msgid "Profile and processor" -msgstr "Perfil y procesador" +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:203 +msgid "Processor acknowledged generation {0}" +msgstr "El procesador confirmó la generación {0}" + +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:204 +msgid "Processor rejected generation {0}" +msgstr "El procesador rechazó la generación {0}" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:22 +msgid "Profile lifecycle" +msgstr "Ciclo del perfil" #: src/components/biometrics/Calibration/ProfileCandidateEditor.tsx:69 msgid "Profile resolution failed." msgstr "Error al resolver el perfil." -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:28 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:84 +msgid "Profile revisions here are candidates. Applying one requests it like Save and request does; the overview shows when the processor acknowledges it." +msgstr "Las revisiones de perfil aquí son candidatas. Aplicar una la solicita igual que Guardar y solicitar; la vista general muestra cuándo la confirma el procesador." + +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:29 msgid "Profiles" msgstr "Perfiles" @@ -1804,6 +2042,10 @@ msgstr "Observaciones prospectivas" msgid "Protocol hash" msgstr "Hash del protocolo" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:287 +msgid "quality" +msgstr "calidad" + #: src/components/biometrics/Calibration/NumericTrace.tsx:98 msgid "Quality" msgstr "Calidad" @@ -1812,10 +2054,18 @@ msgstr "Calidad" msgid "Queue backtest" msgstr "Poner evaluación retrospectiva en cola" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "queued" +msgstr "en cola" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 msgid "Queued" msgstr "En cola" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:65 +msgid "range" +msgstr "rango" + #: src/components/biometrics/Calibration/StageReport.tsx:52 msgid "Recall" msgstr "Sensibilidad" @@ -1854,6 +2104,11 @@ msgstr "Configuraciones registradas: {0}; lecturas con configuración desconocid msgid "Recorded nights" msgstr "Noches registradas" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:313 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:128 +msgid "reference" +msgstr "referencia" + #: src/components/biometrics/Calibration/StageReport.tsx:31 msgid "Reference / predicted" msgstr "Referencia / predicción" @@ -1974,7 +2229,7 @@ msgstr "Fuente de referencia" msgid "Reference-defined subgroup comparisons" msgstr "Comparaciones de subgrupos definidos por la referencia" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:27 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:28 msgid "References" msgstr "Referencias" @@ -1986,6 +2241,10 @@ msgstr "Actualizar informe" msgid "Regression under the evaluation policy" msgstr "Empeoramiento según la política de evaluación" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:131 +msgid "Reject intervals shorter than" +msgstr "Rechazar intervalos más cortos que" + #: src/components/biometrics/Calibration/BacktestReport.tsx:162 msgid "Release detailed report" msgstr "Liberar informe detallado" @@ -2002,6 +2261,10 @@ msgstr "Liberar datos conservados" msgid "Release these retained inputs permanently? Existing reports and audit metadata remain. Active jobs must finish or be cancelled first." msgstr "¿Liberar estos datos conservados permanentemente? Se mantienen los informes existentes y los metadatos de auditoría. Los trabajos activos deben finalizar o cancelarse primero." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:79 +msgid "released" +msgstr "liberado" + #: src/components/biometrics/Calibration/StageReport.tsx:12 #: src/components/biometrics/Calibration/StageTrace.tsx:30 msgid "REM" @@ -2059,6 +2322,10 @@ msgstr "Hash del informe" msgid "Report identity" msgstr "Identidad del informe" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:152 +msgid "Report the index only after" +msgstr "Informar el índice solo después de" + #: src/components/biometrics/Calibration/ShadowRuns.tsx:51 msgid "Reports describe observed coverage and reference agreement; they do not authorize activation." msgstr "Los informes describen la cobertura observada y la concordancia con la referencia; no autorizan la activación." @@ -2067,14 +2334,18 @@ msgstr "Los informes describen la cobertura observada y la concordancia con la r msgid "Reproducible sampling seed" msgstr "Semilla de muestreo reproducible" -#. placeholder {0}: event.requester -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:54 -msgid "Requested by {0}" -msgstr "Solicitado por {0}" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:18 +msgid "Requested" +msgstr "Solicitado" + +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:198 +msgid "Requested activation · generation {0}" +msgstr "Solicitó la activación · generación {0}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:26 -msgid "Requested change" -msgstr "Cambio solicitado" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:135 +msgid "Requesting…" +msgstr "Solicitando…" #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:117 msgid "Requires re-verification" @@ -2100,21 +2371,41 @@ msgstr "Las observaciones de recursos son parciales. El estado del proceso refle msgid "Restart archive scan" msgstr "Reiniciar la exploración de archivos" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:17 -msgid "Restore a profile" -msgstr "Restaurar un perfil" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:135 +msgid "Restore and request" +msgstr "Restaurar y solicitar" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:326 +msgid "Restore previous" +msgstr "Restaurar el anterior" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:62 #: src/components/biometrics/Calibration/ProfileComparison.tsx:28 msgid "Restore previous profile" msgstr "Restaurar el perfil anterior" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:127 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:62 #: src/components/biometrics/Calibration/ProfileComparison.tsx:28 msgid "Restore system defaults" msgstr "Restaurar valores predeterminados del sistema" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:127 +msgid "Restore the previous profile" +msgstr "Restaurar el perfil anterior" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:72 +msgid "Restore… ▾" +msgstr "Restaurar… ▾" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:201 +msgid "Restored system defaults and requested them" +msgstr "Restauró los valores del sistema y los solicitó" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:200 +msgid "Restored the previous profile and requested it" +msgstr "Restauró el perfil anterior y lo solicitó" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:33 msgid "Retain a copy of the selected nights and reference revisions. Future edits or source deletion do not alter this dataset." msgstr "Conserve una copia de las noches y revisiones de referencia seleccionadas. Los cambios posteriores o la eliminación del origen no alteran este conjunto de datos." @@ -2161,10 +2452,6 @@ msgstr "Reintentar la misma solicitud de programación" msgid "Review candidate" msgstr "Revisar candidato" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:17 -msgid "Review changes before requesting system defaults or the exact previous profile." -msgstr "Revise los cambios antes de solicitar los valores predeterminados del sistema o el perfil anterior exacto." - #: src/components/biometrics/Calibration/ShadowRuns.tsx:88 msgid "Review observation report" msgstr "Revisar informe de observación" @@ -2173,18 +2460,10 @@ msgstr "Revisar informe de observación" msgid "Review prepared plan" msgstr "Revisar plan preparado" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:21 -msgid "Review previous profile" -msgstr "Revisar perfil anterior" - #: src/components/biometrics/Calibration/BacktestJobs.tsx:81 msgid "Review report" msgstr "Revisar informe" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:20 -msgid "Review system defaults" -msgstr "Revisar valores predeterminados del sistema" - #. placeholder {0}: profile.revision #. placeholder {1}: profile.origin #. placeholder {2}: profile.requester @@ -2209,6 +2488,15 @@ msgstr "Derecha" msgid "Row {0}: {1}" msgstr "Fila {0}: {1}" +#. placeholder {0}: o.rule +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:174 +msgid "Rule {0}" +msgstr "Regla {0}" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "running" +msgstr "en curso" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 msgid "Running" msgstr "En ejecución" @@ -2222,6 +2510,10 @@ msgstr "Pico de memoria muestreado del proceso de trabajo: {0} MiB. Los picos br msgid "Save a candidate for evaluation. This does not change live processing." msgstr "Guarde un candidato para evaluarlo. Esto no cambia el procesamiento en vivo." +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Save and request" +msgstr "Guardar y solicitar" + #: src/components/biometrics/Calibration/ProfileCandidateEditor.tsx:111 msgid "Save candidate without applying" msgstr "Guardar candidato sin aplicar" @@ -2230,11 +2522,21 @@ msgstr "Guardar candidato sin aplicar" msgid "Save evaluation protocol" msgstr "Guardar protocolo de evaluación" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:174 +msgid "Save failed." +msgstr "No se pudo guardar." + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:103 msgid "Save previewed import" msgstr "Guardar la importación revisada" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:226 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:246 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:311 +msgid "saved" +msgstr "guardado" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:17 msgid "Saved" msgstr "Guardado" @@ -2251,9 +2553,9 @@ msgstr "Experimentos guardados" msgid "Saved observations: {0}" msgstr "Observaciones guardadas: {0}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 -msgid "Saved profile" -msgstr "Perfil guardado" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:92 +msgid "Saved profile against previous" +msgstr "Perfil guardado frente al anterior" #: src/components/biometrics/Calibration/ProfileLibrary.tsx:19 msgid "Saved profile revisions" @@ -2265,9 +2567,17 @@ msgstr "Revisiones de perfiles guardadas" msgid "Saved sample times: {0}. Planned observation intervals: {1}." msgstr "Marcas de tiempo de muestras guardadas: {0}. Intervalos de observación previstos: {1}." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:24 -msgid "Saved settings and processor reports are separate." -msgstr "Los ajustes guardados y los informes del procesador son independientes." +#: src/components/biometrics/Calibration/CalibrationPage.tsx:196 +msgid "Saved the profile" +msgstr "Guardó el perfil" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:141 +msgid "Saves it as the profile and requests it; the current saved profile becomes previous. The processor keeps running what it has until it acknowledges the request." +msgstr "Lo guarda como perfil y lo solicita; el perfil guardado actual pasa a ser el anterior. El procesador sigue con lo que tiene hasta confirmar la solicitud." + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Saving…" +msgstr "Guardando…" #: src/components/biometrics/Calibration/ShadowSchedule.tsx:129 #: src/components/biometrics/Calibration/ShadowSchedule.tsx:130 @@ -2339,10 +2649,6 @@ msgstr "Revisiones de referencia seleccionadas: {0} / 20" msgid "Selected sessions: {0} / 60" msgstr "Sesiones seleccionadas: {0} / 60" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:53 -msgid "Sensor measurements are separate from algorithm personalization." -msgstr "Las mediciones de los sensores son independientes de la personalización del algoritmo." - #. placeholder {0}: cohort.sessionIds.join(', ') #: src/components/biometrics/Calibration/DatasetLibrary.tsx:78 msgid "Sessions: {0}" @@ -2370,6 +2676,23 @@ msgstr "Este informe no dispone de comparaciones en marcas de tiempo comunes." msgid "Shared-timestamp comparisons. Differences are comparison minus baseline; negative error differences mean lower error on shared observations." msgstr "Comparaciones en marcas de tiempo comunes. Las diferencias se calculan como comparación menos base; las diferencias negativas de error indican un menor error en las observaciones comunes." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:143 +msgid "Shortest breathing interval (× median)" +msgstr "Intervalo respiratorio más corto (× mediana)" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:377 +msgid "show" +msgstr "mostrar" + +#. placeholder {0}: rows.length +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:42 +msgid "Show all {0}" +msgstr "Mostrar los {0}" + +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:42 +msgid "Show recent" +msgstr "Mostrar recientes" + #: src/components/biometrics/Calibration/ReplaySessionPicker.tsx:67 msgid "Signal replay unavailable: raw waveforms and processing context have not been retained by this service." msgstr "Reprocesamiento de señales no disponible: este servicio no ha conservado las formas de onda originales ni el contexto de procesamiento." @@ -2382,6 +2705,14 @@ msgstr "Sesgo con signo" msgid "Simulated demo: generated readings, no hardware execution or qualification." msgstr "Demostración simulada: lecturas generadas, sin ejecución en hardware ni cualificación." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:168 +msgid "Sleep stages" +msgstr "Fases del sueño" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:162 +msgid "SLEEP STAGES" +msgstr "FASES DEL SUEÑO" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:59 msgid "Sleeper identifier" msgstr "Identificador de la persona" @@ -2464,10 +2795,6 @@ msgstr "Reglas de fases sobre lecturas originales" msgid "Stage trace for the displayed intervals; exact times and stages follow in the table." msgstr "Traza de fases de los intervalos mostrados; las horas exactas y las fases figuran en la tabla siguiente." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -msgid "Stale" -msgstr "Desactualizado" - #: src/components/biometrics/Calibration/ReferenceOverlay.tsx:67 #: src/components/biometrics/Calibration/StageTrace.tsx:64 msgid "Start" @@ -2498,6 +2825,11 @@ msgstr "Detener observación" msgid "Stop recorded" msgstr "Parada registrada" +#. placeholder {0}: i.summary.source +#: src/components/biometrics/Calibration/CalibrationPage.tsx:227 +msgid "Stored a {0} reference import" +msgstr "Guardó una importación de referencia de {0}" + #. placeholder {0}: window.observationCount #: src/components/biometrics/Calibration/NumericReport.tsx:166 msgid "Stored observations: {0}" @@ -2515,7 +2847,8 @@ msgstr "Comparación de lecturas almacenadas disponible" #: src/components/biometrics/Calibration/BacktestJobs.tsx:64 #: src/components/biometrics/Calibration/BacktestReport.tsx:60 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 +#: src/components/biometrics/Calibration/CalibrationControls.tsx:80 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:103 #: src/components/biometrics/Calibration/NumericReport.tsx:26 #: src/components/biometrics/Calibration/NumericTrace.tsx:147 #: src/components/biometrics/Calibration/ProfileComparison.tsx:29 @@ -2525,6 +2858,10 @@ msgstr "Comparación de lecturas almacenadas disponible" msgid "System defaults" msgstr "Valores predeterminados del sistema" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:54 +msgid "tested on save" +msgstr "se prueba al guardar" + #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:84 msgid "The archive directory is unavailable." msgstr "El directorio de archivos no está disponible." @@ -2538,14 +2875,14 @@ msgstr "El algoritmo actual del pod informa un índice de variabilidad de los in msgid "The frozen offset of {clockOffsetMs} ms is added to recomputed timestamps in this overlay. Reference times stay unchanged. Point matching tolerance: {0} ms." msgstr "El desfase fijado de {clockOffsetMs} ms se añade a las marcas de tiempo recalculadas en esta superposición. Las horas de referencia no cambian. Tolerancia de emparejamiento de puntos: {0} ms." -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:45 -msgid "The HRV index measures window IBI variability. It is not clinical beat-to-beat RMSSD, and incompatible reference metrics cannot validate it." -msgstr "El índice de VFC mide la variabilidad de los intervalos entre latidos dentro de una ventana. No es la RMSSD clínica entre latidos consecutivos y no puede validarse con métricas de referencia incompatibles." - #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:83 msgid "The last archive was unavailable. Continue to inspect the next archive." msgstr "El último archivo no estaba disponible. Continúa con la inspección del siguiente archivo." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:139 +msgid "the median" +msgstr "la mediana" + #: src/components/biometrics/Calibration/ShadowReport.tsx:19 msgid "The observation gap exceeded the planned limit." msgstr "El intervalo sin observaciones superó el límite previsto." @@ -2554,10 +2891,6 @@ msgstr "El intervalo sin observaciones superó el límite previsto." msgid "The observation window was missed." msgstr "Se perdió el período de observación." -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:27 -msgid "The pinned profile is incompatible. Its revision will be preserved, but a valid before-and-after comparison is unavailable." -msgstr "El perfil fijado es incompatible. Se conservará su revisión, pero no se puede realizar una comparación válida del antes y el después." - #: src/components/biometrics/Calibration/ShadowSchedule.tsx:77 msgid "The plan could not be prepared. Check the selected profile, window and installed qualification." msgstr "No se pudo preparar el plan. Comprueba el perfil y el período seleccionados y la cualificación instalada." @@ -2579,12 +2912,15 @@ msgstr "No se pudo guardar la solicitud para recuperarla. Este intento no se env msgid "The retained inputs are unavailable for new evaluations." msgstr "Los datos conservados no están disponibles para nuevas evaluaciones." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:171 +msgid "The rule list itself differs from the shipped rules." +msgstr "La lista de reglas difiere de las reglas de fábrica." + #: src/components/biometrics/Calibration/ProfileComparison.tsx:33 msgid "The saved profile changed. Compare again before reviewing these changes." msgstr "El perfil guardado ha cambiado. Vuelva a comparar antes de revisar estos cambios." #: src/components/biometrics/Calibration/ProfileComparison.tsx:33 -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:34 msgid "The saved profile changed. Select a recovery action again to make a new request." msgstr "El perfil guardado ha cambiado. Seleccione de nuevo una acción de restauración para realizar una nueva solicitud." @@ -2636,6 +2972,10 @@ msgstr "Estas puntuaciones comparan las lecturas originales almacenadas. Los res msgid "These scores compare readings recomputed from retained waveforms. Shared-timestamp scores use observations available in every configuration. Differences describe agreement with the reference and do not establish validated improvement." msgstr "Estas puntuaciones comparan lecturas recalculadas a partir de formas de onda conservadas. Las puntuaciones en marcas de tiempo comunes usan observaciones disponibles en todas las configuraciones. Las diferencias describen la concordancia con la referencia y no demuestran una mejora validada." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:237 +msgid "This backtest compared stored readings, so there is no recomputed heart-rate trace to draw." +msgstr "Esta prueba comparó lecturas almacenadas, así que no hay un trazo de frecuencia cardíaca recalculado que dibujar." + #: src/components/biometrics/Calibration/StageTrace.tsx:41 msgid "This chart shows only the current page of replay intervals. Gaps are left empty; no stages are inferred between intervals." msgstr "Este gráfico muestra solo la página actual de intervalos reprocesados. Los huecos quedan vacíos; no se infieren fases entre intervalos." @@ -2709,7 +3049,6 @@ msgstr "Hora" msgid "Times shown in {0}" msgstr "Horas mostradas en {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:23 #: src/components/biometrics/Calibration/NumericReport.tsx:85 #: src/components/biometrics/Calibration/StageTrace.tsx:43 msgid "Times shown in {timezone}" @@ -2744,6 +3083,19 @@ msgstr "Intervalos de la traza {first}–{last} de {0}" msgid "Trace night" msgstr "Noche de la traza" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:125 +msgid "Tracking history length" +msgstr "Longitud del historial de seguimiento" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:115 +msgid "Tracking tolerance (bpm)" +msgstr "Tolerancia de seguimiento (lpm)" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:112 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "tuning" +msgstr "ajuste" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 #: src/components/biometrics/Calibration/BacktestSubmission.tsx:73 @@ -2763,6 +3115,10 @@ msgstr "No disponible" msgid "Unavailable or belongs to a different sleeper." msgstr "No disponible o pertenece a otra persona." +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:53 +msgid "unchanged" +msgstr "sin cambios" + #. placeholder {0}: parameter.unit #. placeholder {1}: parameter.minimum #. placeholder {2}: parameter.maximum @@ -2790,10 +3146,6 @@ msgstr "Referencias sin emparejar" msgid "Use offset-bearing ISO timestamps or Unix milliseconds. Each record needs recordKey, metric, value, unit, and timestamp (or start and end). JSON uses an array; CSV uses these field names as headers. Imports are limited to 2 MiB and 2000 records." msgstr "Use fechas ISO con desplazamiento horario o milisegundos Unix. Cada registro requiere recordKey, metric, value, unit y timestamp (o start y end). JSON usa un array; CSV usa estos nombres como encabezados. Las importaciones se limitan a 2 MiB y 2000 registros." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Use system defaults" -msgstr "Usar los valores predeterminados del sistema" - #: src/components/biometrics/Calibration/ShadowSchedule.tsx:145 msgid "Use the same sleeper identity as the reference records you intend to compare." msgstr "Usa la misma identidad de la persona que duerme que en los registros de referencia que quieras comparar." @@ -2802,6 +3154,11 @@ msgstr "Usa la misma identidad de la persona que duerme que en los registros de msgid "Validated activation" msgstr "Activación validada" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:116 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "validation" +msgstr "validación" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 #: src/components/biometrics/Calibration/BacktestSubmission.tsx:74 @@ -2827,8 +3184,6 @@ msgstr "Valor / unidad" msgid "Vitals with unknown original processing provenance: {0}" msgstr "Signos vitales con procedencia de procesamiento original desconocida: {0}" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Waiting for a processing boundary" msgstr "Esperando un límite de procesamiento" @@ -2837,8 +3192,6 @@ msgstr "Esperando un límite de procesamiento" msgid "Waiting for a worker and resource admission. No execution is implied by a queued request." msgstr "Esperando un ejecutor y recursos disponibles. Una solicitud en cola no implica que se esté ejecutando." -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Waiting for delivery" msgstr "Esperando la entrega" @@ -2856,9 +3209,14 @@ msgstr "Vigilia" msgid "Waveform dataset retained: {published}" msgstr "Conjunto de datos de formas de onda conservado: {published}" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:44 -msgid "What these metrics mean" -msgstr "Qué significan estas métricas" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:157 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:180 +msgid "What the saved profile did" +msgstr "Qué hizo el perfil guardado" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:176 +msgid "when" +msgstr "cuando" #: src/components/biometrics/Calibration/DatasetBuilder.tsx:59 #: src/components/biometrics/Calibration/DatasetLibrary.tsx:70 @@ -2875,6 +3233,10 @@ msgstr "Índice de variabilidad IBI por ventana" msgid "Window IBI variability index (ms)" msgstr "Índice de variabilidad IBI por ventanas (ms)" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:158 +msgid "window IBI variability, not clinical RMSSD" +msgstr "variabilidad IBI por ventana, no RMSSD clínico" + #: src/components/biometrics/Calibration/ShadowReport.tsx:34 msgid "Within the observed limits." msgstr "Dentro de los límites observados." @@ -2883,6 +3245,10 @@ msgstr "Dentro de los límites observados." msgid "Work was deferred; your selection and retry key are preserved." msgstr "El trabajo se aplazó; se conservan tu selección y la clave de reintento." +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:41 +msgid "WORKBENCH" +msgstr "TALLER" + #: src/components/biometrics/Calibration/ShadowReport.tsx:18 msgid "Worker cleanup has not been confirmed." msgstr "No se ha confirmado la limpieza del proceso de trabajo." diff --git a/src/lib/i18n/locales/pseudo.po b/src/lib/i18n/locales/pseudo.po index e4e89d51..90f5d980 100644 --- a/src/lib/i18n/locales/pseudo.po +++ b/src/lib/i18n/locales/pseudo.po @@ -13,6 +13,18 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:169 +msgid ", with {count} overrides" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:169 +msgid ", with 1 override" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:369 +msgid ". Separate from the profile." +msgstr "" + #. placeholder {0}: item.summary.source #. placeholder {1}: item.summary.subjectId #. placeholder {2}: item.revision @@ -20,6 +32,31 @@ msgstr "" msgid "{0} · {1} · revision {2}" msgstr "" +#. placeholder {0}: label.weekday +#. placeholder {1}: label.day +#. placeholder {2}: fmt1(prev) +#. placeholder {3}: fmt1(saved) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:198 +msgid "{0} {1}: previous {2}, saved {3} bpm" +msgstr "" + +#. placeholder {0}: splitLabel[job.split] +#. placeholder {1}: avg(n => n.previous.heart_rate.mae as number) +#. placeholder {2}: avg(n => n.saved.heart_rate.mae as number) +#. placeholder {3}: shortDuration(job.updatedAtMs - job.createdAtMs) +#: src/components/biometrics/Calibration/CalibrationPage.tsx:216 +msgid "{0} HR error {1} → {2} bpm · {3}" +msgstr "" + +#. placeholder {0}: i.summary.accepted +#: src/components/biometrics/Calibration/CalibrationPage.tsx:227 +msgid "{0} records" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:160 +msgid "{stages} sleep-stage values" +msgstr "" + #: src/components/biometrics/Calibration/NumericReport.tsx:175 msgid "95th percentile absolute error" msgstr "" @@ -32,6 +69,18 @@ msgstr "" msgid "A saved revision is not evidence of validation or processor adoption." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:109 +msgid "Accept an estimate when it is within" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:156 +msgid "accepted 10-second IBI windows" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:123 +msgid "accepted estimates in the tracker" +msgstr "" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:87 msgid "Accepted records are not necessarily usable for validation. Incompatible HRV definitions remain unscorable." msgstr "" @@ -53,10 +102,6 @@ msgstr "" msgid "Accepted: {0}; rejected: {1}; duplicates: {2}; unscorable: {3}; conflicts: {4}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:21 -msgid "Activation history" -msgstr "" - #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:76 msgid "Activation request saved. Processor acknowledgement is reported separately below." msgstr "" @@ -80,6 +125,10 @@ msgstr "" msgid "Active profile hash" msgstr "" +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:21 +msgid "Activity" +msgstr "" + #: src/components/biometrics/Calibration/NumericReport.tsx:111 msgid "Added readings" msgstr "" @@ -122,8 +171,6 @@ msgstr "" msgid "Another revision from this reference family is selected." msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Applied" msgstr "" @@ -132,10 +179,6 @@ msgstr "" msgid "Apply evaluated revision" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Apply profile" -msgstr "" - #: src/components/biometrics/Calibration/ValidatedProfileActivation.tsx:32 msgid "Apply this exact candidate using the selected evaluation. Evidence and current settings are checked again when applying." msgstr "" @@ -172,10 +215,20 @@ msgstr "" msgid "Available reference records by metric (across revisions)" msgstr "" +#. placeholder {0}: group.latest.id.slice(0, 4) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:181 +msgid "Backtest {0}. Frozen dataset replayed through the previous and saved profiles; nothing here changes the pod." +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:21 msgid "Backtest jobs" msgstr "" +#. placeholder {0}: splitLabel[job.split] +#: src/components/biometrics/Calibration/CalibrationPage.tsx:220 +msgid "Backtest on {0} nights failed" +msgstr "" + #. placeholder {0}: enqueue.data.id #: src/components/biometrics/Calibration/BacktestSubmission.tsx:93 msgid "Backtest request saved: {0}" @@ -185,7 +238,7 @@ msgstr "" msgid "Backtest submission" msgstr "" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:29 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:30 msgid "Backtests" msgstr "" @@ -201,11 +254,26 @@ msgstr "" msgid "Before configuration hash" msgstr "" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:24 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:68 msgid "Biometric calibration" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:264 +msgid "Biometric calibration side" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:113 +msgid "bpm of the tracker" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:128 +msgid "BREATHING" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:173 +msgid "Breathing error" +msgstr "" + #: src/components/biometrics/Calibration/ShadowReport.tsx:30 msgid "Breathing rate" msgstr "" @@ -216,10 +284,18 @@ msgstr "" msgid "Breathing rate (breaths/min)" msgstr "" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:25 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:38 +msgid "Calibration" +msgstr "" + +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:34 msgid "Calibration views" msgstr "" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:136 +msgid "Cancel" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:79 msgid "Cancel backtest" msgstr "" @@ -318,6 +394,14 @@ msgstr "" msgid "Candidate spike threshold" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:94 +msgid "Changed values are white; the previous profile (System defaults) sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:95 +msgid "Changed values are white; the previous profile sits underneath. Moving a control edits the saved profile; saving again re-requests it. The processor adopts a profile only after it acknowledges a request." +msgstr "" + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:86 msgid "Check Overview for current processor health; a past acknowledgement does not prove current health." msgstr "" @@ -350,8 +434,8 @@ msgstr "" msgid "Clear this night’s selection" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 -msgid "Clock mismatch" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:187 +msgid "click a night" msgstr "" #. placeholder {0}: minutes(score.pairedDurationMs) @@ -443,10 +527,6 @@ msgstr "" msgid "Compatibility" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Compatibility fallback" -msgstr "" - #: src/components/biometrics/Calibration/ReferenceOverlay.tsx:90 msgid "Compatible" msgstr "" @@ -464,15 +544,15 @@ msgstr "" msgid "Compressed bytes: {0}" msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:372 +msgid "config id" +msgstr "" + #: src/components/biometrics/Calibration/BacktestReport.tsx:98 #: src/components/biometrics/Calibration/StageReport.tsx:107 msgid "Configuration" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:36 -msgid "Configuration identity" -msgstr "" - #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:58 msgid "Confirm import deletion" msgstr "" @@ -506,9 +586,12 @@ msgstr "" msgid "Coverage reduced in at least one evaluated window" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Current" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:61 +msgid "covered" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "current" msgstr "" #: src/components/biometrics/Calibration/BacktestReport.tsx:60 @@ -543,6 +626,10 @@ msgstr "" msgid "Dataset hash" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:84 +msgid "Dataset nights" +msgstr "" + #. placeholder {0}: create.data.id #: src/components/biometrics/Calibration/DatasetBuilder.tsx:57 msgid "Dataset saved: {0}" @@ -557,10 +644,6 @@ msgstr "" msgid "Deep" msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:31 -msgid "Default recovery configuration hash" -msgstr "" - #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:52 msgid "Delete import" msgstr "" @@ -577,12 +660,9 @@ msgstr "" msgid "Descriptive evaluation only" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 -msgid "Different versions reported" -msgstr "" - -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:57 -msgid "Direct sleep-stage changes are unvalidated manual settings. Saving requests a profile change; processor adoption is reported separately in Overview." +#. placeholder {0}: shortDuration(run.durationMs) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "done · {0}" msgstr "" #: src/components/biometrics/Calibration/NumericReport.tsx:151 @@ -601,6 +681,10 @@ msgstr "" msgid "Edit a new candidate from this revision" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:192 +msgid "Edit overrides" +msgstr "" + #: src/components/biometrics/Calibration/ProfileLibrary.tsx:20 msgid "Editing from revision {revisionId}" msgstr "" @@ -660,6 +744,10 @@ msgstr "" msgid "Evaluated minutes: {0}" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:75 +msgid "Evaluation dataset" +msgstr "" + #: src/components/biometrics/Calibration/ExperimentBuilder.tsx:47 msgid "Evaluation mode" msgstr "" @@ -693,10 +781,6 @@ msgstr "" msgid "Exact processed-data coverage" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:45 -msgid "Exact profile transition" -msgstr "" - #: src/components/biometrics/Calibration/NumericTrace.tsx:93 msgid "Exact recomputed readings, without reference clock adjustment" msgstr "" @@ -728,8 +812,8 @@ msgstr "" msgid "Experiment saved: {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Expired" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "expired" msgstr "" #: src/components/biometrics/Calibration/CandidateStageFields.tsx:39 @@ -757,19 +841,38 @@ msgstr "" msgid "F1" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "failed" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 #: src/components/biometrics/Calibration/ShadowRuns.tsx:12 msgid "Failed" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:184 +msgid "Fallback" +msgstr "" + #: src/components/biometrics/Calibration/CandidateStageFields.tsx:21 #: src/components/biometrics/Calibration/CandidateStageFields.tsx:21 msgid "Fallback stage" msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:202 +msgid "Fell back to a compatible profile" +msgstr "" + +#. placeholder {0}: splitLabel[job.split] +#: src/components/biometrics/Calibration/CalibrationPage.tsx:220 +msgid "Finished a backtest on {0} nights" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:314 +msgid "first 500 readings" +msgstr "" + #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:125 msgid "First archive page" msgstr "" @@ -794,6 +897,10 @@ msgstr "" msgid "Flags" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:128 +msgid "Freeze a new dataset" +msgstr "" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:33 msgid "Freeze evaluation inputs" msgstr "" @@ -802,8 +909,30 @@ msgstr "" msgid "Freeze selected inputs" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:58 -msgid "Freshness unknown" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:361 +msgid "freshness unknown" +msgstr "" + +#. placeholder {0}: d.manifest.splitProtocol.assignments.length +#: src/components/biometrics/Calibration/CalibrationPage.tsx:224 +msgid "Froze a dataset of {0} nights" +msgstr "" + +#. placeholder {0}: formatDay(dataset.createdAtMs, timezone) +#. placeholder {1}: formatBytes(dataset.byteLength) +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:79 +msgid "frozen {0} · {1}" +msgstr "" + +#. placeholder {0}: nights.length +#. placeholder {1}: sources.join(' + ') +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:69 +msgid "Frozen copy of {0} nights and {1} reference. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." +msgstr "" + +#. placeholder {0}: nights.length +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:70 +msgid "Frozen copy of {0} nights. Later edits or deletions do not touch it. Whole nights are split before scoring; a split is not evidence on its own." msgstr "" #: src/components/biometrics/Calibration/DatasetLibrary.tsx:85 @@ -838,26 +967,32 @@ msgstr "" msgid "Frozen stage objective thresholds" msgstr "" -#. placeholder {0}: data.desired.binding.generation -#. placeholder {0}: event.generation -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:41 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:39 -msgid "Generation {0}" -msgstr "" - #: src/components/biometrics/Calibration/NumericReport.tsx:44 msgid "Groups use native reference timestamps and record types, independently of candidate output. Half-window groups exclude intervals crossing the midpoint. Groups overlap and are not independent experiments. These are not motion or artifact subgroups." msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:228 +msgid "H holdout" +msgstr "" + #: src/components/biometrics/Calibration/ReplayResources.tsx:49 msgid "Health was sampled at admission checks, normally five seconds apart. Unobserved intervals are not continuous health measurements." msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 #: src/components/biometrics/Calibration/ShadowReport.tsx:30 msgid "Heart rate" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:106 +msgid "HEART RATE" +msgstr "" + +#. placeholder {0}: label.weekday +#. placeholder {1}: label.day +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:291 +msgid "Heart rate · {0} {1}" +msgstr "" + #: src/components/biometrics/Calibration/NumericReport.tsx:27 #: src/components/biometrics/Calibration/NumericTrace.tsx:38 #: src/components/biometrics/Calibration/ShadowComparison.tsx:44 @@ -868,10 +1003,18 @@ msgstr "" msgid "Heart-rate spike threshold (standard deviations)" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:295 +msgid "Heart-rate trace" +msgstr "" + #: app/[lang]/health/HealthcheckDemo.tsx:13 msgid "Hello, tRPC test page!" msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:377 +msgid "hide" +msgstr "" + #. placeholder {0}: summary.improvedAgreementNights #. placeholder {1}: summary.regressedAgreementNights #. placeholder {2}: summary.unchangedAgreementNights @@ -879,12 +1022,9 @@ msgstr "" msgid "Higher agreement: {0}; lower: {1}; unchanged: {2}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:46 -msgid "Historical sleep stages are recomputed with the current classifier. Original stored vitals remain separate from replay results." -msgstr "" - -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:30 -msgid "History" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:120 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "holdout" msgstr "" #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 @@ -900,6 +1040,26 @@ msgstr "" msgid "Holdout accesses recorded for this experiment: {0}" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:258 +msgid "Holdout is the number to trust; tuning nights were used to pick these values." +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:170 +msgid "HR error · holdout" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:168 +msgid "HR error · tuning" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:169 +msgid "HR error · validation" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:186 +msgid "HR error per night · bpm vs reference" +msgstr "" + #: src/components/biometrics/BiometricsPage.tsx:532 msgid "HRV definition" msgstr "" @@ -908,8 +1068,12 @@ msgstr "" msgid "HRV definitions differ or are partly unknown. A combined HRV average is unavailable." msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:18 -msgid "HRV index" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:149 +msgid "HRV INDEX" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:172 +msgid "HRV index error" msgstr "" #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:60 @@ -928,6 +1092,10 @@ msgstr "" msgid "I intend to restore the exact previous profile without validation evidence." msgstr "" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:147 +msgid "Identical to the saved profile; restoring only re-requests it." +msgstr "" + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:53 #: src/components/biometrics/Calibration/ValidatedProfileActivation.tsx:38 msgid "Immediate processing boundary" @@ -941,6 +1109,10 @@ msgstr "" msgid "Import preview" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:126 +msgid "Import reference" +msgstr "" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:53 msgid "Import reference measurements" msgstr "" @@ -972,6 +1144,11 @@ msgstr "" msgid "Inherited default" msgstr "" +#. placeholder {0}: stages.ruleCount +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:166 +msgid "Inherits the {0} rules and fallback from" +msgstr "" + #: src/components/biometrics/Calibration/DatasetLibrary.tsx:31 msgid "Inputs released; metadata retained" msgstr "" @@ -1063,6 +1240,14 @@ msgstr "" msgid "Keep retained inputs" msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Keep saved profile" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:119 +msgid "Keep the last" +msgstr "" + #. placeholder {0}: display(operations.ingestion.maximumObservationGapMs, 1000) #: src/components/biometrics/Calibration/ReplayResources.tsx:47 msgid "Largest observation gap: {0} seconds" @@ -1095,6 +1280,10 @@ msgstr "" msgid "Limitations" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:144 +msgid "Longest breathing interval (× median)" +msgstr "" + #: src/components/biometrics/Calibration/NumericReport.tsx:110 msgid "Lost readings" msgstr "" @@ -1144,8 +1333,8 @@ msgstr "" msgid "Measured Python processes: {0} of {1}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:53 -msgid "Measured sensor calibration" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:366 +msgid "Measured sensor calibration for this side:" msgstr "" #: src/components/biometrics/Calibration/NumericReport.tsx:174 @@ -1176,8 +1365,12 @@ msgstr "" msgid "Metrics: {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Migrate settings" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:202 +msgid "Migrated sleep-stage settings into the profile" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:159 +msgid "Minimum IBI windows" msgstr "" #: src/components/biometrics/Calibration/ExperimentBuilder.tsx:97 @@ -1205,10 +1398,6 @@ msgstr "" msgid "New observation plan" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:62 -msgid "Newest" -msgstr "" - #: src/components/biometrics/Calibration/DatasetLibrary.tsx:41 msgid "Newest datasets" msgstr "" @@ -1280,6 +1469,10 @@ msgstr "" msgid "Night {0}" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:171 +msgid "Night covered" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:74 msgid "Night progress is not available yet." msgstr "" @@ -1288,8 +1481,8 @@ msgstr "" msgid "No accepted time range." msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:30 -msgid "No activation history for this side." +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:158 +msgid "No backtest has replayed the saved profile yet. Saving a change re-runs the latest experiment on its tuning and validation nights; nothing here changes the pod." msgstr "" #: src/components/biometrics/Calibration/BacktestJobs.tsx:28 @@ -1312,6 +1505,10 @@ msgstr "" msgid "No coverage reduction detected" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:71 +msgid "No dataset is frozen for this side yet. Freeze recorded nights with an imported reference to backtest a profile." +msgstr "" + #: src/components/biometrics/Calibration/BacktestReport.tsx:62 #: src/components/biometrics/Calibration/NumericReport.tsx:28 msgid "No estimable variation" @@ -1366,10 +1563,18 @@ msgstr "" msgid "No recorded nights for this side." msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:101 +msgid "no reference" +msgstr "" + #: src/components/biometrics/Calibration/NumericReport.tsx:29 msgid "No reference" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:286 +msgid "no reference device · excluded from error" +msgstr "" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:91 #: src/components/biometrics/Calibration/ReferenceLibrary.tsx:35 msgid "No reference imports for this side." @@ -1404,10 +1609,6 @@ msgstr "" msgid "No saved experiments for this side." msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:55 -msgid "No sensor calibration has been reported for this side." -msgstr "" - #: src/components/biometrics/Calibration/StageTrace.tsx:84 msgid "No stage intervals were produced for this selection." msgstr "" @@ -1434,8 +1635,10 @@ msgstr "" msgid "Normalized reference measurements" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:26 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:362 +msgid "not reported" +msgstr "" + #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Not reported" msgstr "" @@ -1453,6 +1656,19 @@ msgstr "" msgid "Not scored" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:55 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:57 +msgid "not tested" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:330 +msgid "Nothing changes until you move a control or restore." +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:24 +msgid "Nothing has happened on this side yet." +msgstr "" + #: src/components/biometrics/Calibration/NumericTrace.tsx:158 msgid "Numeric trace configuration" msgstr "" @@ -1523,10 +1739,6 @@ msgstr "" msgid "Observing" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:63 -msgid "Older changes" -msgstr "" - #: src/components/biometrics/Calibration/DatasetLibrary.tsx:42 msgid "Older datasets" msgstr "" @@ -1558,6 +1770,10 @@ msgstr "" msgid "Only the current page is plotted. Dots represent individual readings; gaps and missing values are not interpolated. Original stored readings remain unchanged." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:135 +msgid "or longer than" +msgstr "" + #: src/components/biometrics/Calibration/StageTrace.tsx:61 msgid "Original replay timestamps, without reference clock adjustment" msgstr "" @@ -1601,10 +1817,14 @@ msgstr "" msgid "Override intent can change even when the resolved value stays the same." msgstr "" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:26 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:27 msgid "Overview" msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:330 +msgid "Overwrites the saved profile and requests it; the current one becomes previous." +msgstr "" + #: src/components/biometrics/Calibration/NumericReport.tsx:109 msgid "Own-support MAE difference" msgstr "" @@ -1630,10 +1850,6 @@ msgstr "" msgid "Partition timezone: {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:21 -msgid "Past acknowledgements do not establish which profile is running now." -msgstr "" - #: src/components/biometrics/Calibration/ShadowComparison.tsx:50 msgid "Paste JSON records or CSV with recordKey, metric, value, unit, and timestamp (or start and end). Use offset-bearing ISO times or Unix milliseconds. This comparison does not save a reference import." msgstr "" @@ -1654,8 +1870,8 @@ msgstr "" msgid "Permanently release report" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 -msgid "Personalized profile" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:238 +msgid "Pick a scored night to see its heart-rate trace." msgstr "" #: src/components/biometrics/Calibration/ProfileLibrary.tsx:38 @@ -1733,10 +1949,6 @@ msgstr "" msgid "Presence and pump context reflect host observations, not proof of the live processor’s cached state." msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:29 -msgid "Preserved incompatible revision" -msgstr "" - #: src/components/biometrics/Calibration/CalibrationReferences.tsx:78 msgid "Preview import" msgstr "" @@ -1745,6 +1957,18 @@ msgstr "" msgid "Preview normalized records before saving them on the pod." msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:222 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:245 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:312 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:65 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:141 +msgid "previous" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:77 +msgid "Previous" +msgstr "" + #: src/components/biometrics/Calibration/NumericTrace.tsx:134 msgid "Previous numeric page" msgstr "" @@ -1770,19 +1994,33 @@ msgstr "" msgid "Processing run" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:27 -msgid "Processor health" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:19 +msgid "Processor" +msgstr "" + +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:203 +msgid "Processor acknowledged generation {0}" +msgstr "" + +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:204 +msgid "Processor rejected generation {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:24 -msgid "Profile and processor" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:22 +msgid "Profile lifecycle" msgstr "" #: src/components/biometrics/Calibration/ProfileCandidateEditor.tsx:69 msgid "Profile resolution failed." msgstr "" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:28 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:84 +msgid "Profile revisions here are candidates. Applying one requests it like Save and request does; the overview shows when the processor acknowledges it." +msgstr "" + +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:29 msgid "Profiles" msgstr "" @@ -1804,6 +2042,10 @@ msgstr "" msgid "Protocol hash" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:287 +msgid "quality" +msgstr "" + #: src/components/biometrics/Calibration/NumericTrace.tsx:98 msgid "Quality" msgstr "" @@ -1812,10 +2054,18 @@ msgstr "" msgid "Queue backtest" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "queued" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 msgid "Queued" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:65 +msgid "range" +msgstr "" + #: src/components/biometrics/Calibration/StageReport.tsx:52 msgid "Recall" msgstr "" @@ -1854,6 +2104,11 @@ msgstr "" msgid "Recorded nights" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:313 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:128 +msgid "reference" +msgstr "" + #: src/components/biometrics/Calibration/StageReport.tsx:31 msgid "Reference / predicted" msgstr "" @@ -1974,7 +2229,7 @@ msgstr "" msgid "Reference-defined subgroup comparisons" msgstr "" -#: src/components/biometrics/Calibration/BiometricCalibration.tsx:27 +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:28 msgid "References" msgstr "" @@ -1986,6 +2241,10 @@ msgstr "" msgid "Regression under the evaluation policy" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:131 +msgid "Reject intervals shorter than" +msgstr "" + #: src/components/biometrics/Calibration/BacktestReport.tsx:162 msgid "Release detailed report" msgstr "" @@ -2002,6 +2261,10 @@ msgstr "" msgid "Release these retained inputs permanently? Existing reports and audit metadata remain. Active jobs must finish or be cancelled first." msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:79 +msgid "released" +msgstr "" + #: src/components/biometrics/Calibration/StageReport.tsx:12 #: src/components/biometrics/Calibration/StageTrace.tsx:30 msgid "REM" @@ -2059,6 +2322,10 @@ msgstr "" msgid "Report identity" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:152 +msgid "Report the index only after" +msgstr "" + #: src/components/biometrics/Calibration/ShadowRuns.tsx:51 msgid "Reports describe observed coverage and reference agreement; they do not authorize activation." msgstr "" @@ -2067,13 +2334,17 @@ msgstr "" msgid "Reproducible sampling seed" msgstr "" -#. placeholder {0}: event.requester -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:54 -msgid "Requested by {0}" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:18 +msgid "Requested" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:26 -msgid "Requested change" +#. placeholder {0}: event.generation +#: src/components/biometrics/Calibration/CalibrationPage.tsx:198 +msgid "Requested activation · generation {0}" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:135 +msgid "Requesting…" msgstr "" #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:117 @@ -2100,21 +2371,41 @@ msgstr "" msgid "Restart archive scan" msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:17 -msgid "Restore a profile" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:135 +msgid "Restore and request" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:326 +msgid "Restore previous" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:62 #: src/components/biometrics/Calibration/ProfileComparison.tsx:28 msgid "Restore previous profile" msgstr "" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:127 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:62 #: src/components/biometrics/Calibration/ProfileComparison.tsx:28 msgid "Restore system defaults" msgstr "" +#: src/components/biometrics/Calibration/CalibrationControls.tsx:127 +msgid "Restore the previous profile" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:72 +msgid "Restore… ▾" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:201 +msgid "Restored system defaults and requested them" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:200 +msgid "Restored the previous profile and requested it" +msgstr "" + #: src/components/biometrics/Calibration/DatasetBuilder.tsx:33 msgid "Retain a copy of the selected nights and reference revisions. Future edits or source deletion do not alter this dataset." msgstr "" @@ -2161,10 +2452,6 @@ msgstr "" msgid "Review candidate" msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:17 -msgid "Review changes before requesting system defaults or the exact previous profile." -msgstr "" - #: src/components/biometrics/Calibration/ShadowRuns.tsx:88 msgid "Review observation report" msgstr "" @@ -2173,18 +2460,10 @@ msgstr "" msgid "Review prepared plan" msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:21 -msgid "Review previous profile" -msgstr "" - #: src/components/biometrics/Calibration/BacktestJobs.tsx:81 msgid "Review report" msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:20 -msgid "Review system defaults" -msgstr "" - #. placeholder {0}: profile.revision #. placeholder {1}: profile.origin #. placeholder {2}: profile.requester @@ -2209,6 +2488,15 @@ msgstr "" msgid "Row {0}: {1}" msgstr "" +#. placeholder {0}: o.rule +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:174 +msgid "Rule {0}" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:150 +msgid "running" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:11 msgid "Running" msgstr "" @@ -2222,6 +2510,10 @@ msgstr "" msgid "Save a candidate for evaluation. This does not change live processing." msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Save and request" +msgstr "" + #: src/components/biometrics/Calibration/ProfileCandidateEditor.tsx:111 msgid "Save candidate without applying" msgstr "" @@ -2230,11 +2522,21 @@ msgstr "" msgid "Save evaluation protocol" msgstr "" +#: src/components/biometrics/Calibration/CalibrationPage.tsx:174 +msgid "Save failed." +msgstr "" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:103 msgid "Save previewed import" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:226 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:246 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:311 +msgid "saved" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:17 msgid "Saved" msgstr "" @@ -2251,8 +2553,8 @@ msgstr "" msgid "Saved observations: {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 -msgid "Saved profile" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:92 +msgid "Saved profile against previous" msgstr "" #: src/components/biometrics/Calibration/ProfileLibrary.tsx:19 @@ -2265,8 +2567,16 @@ msgstr "" msgid "Saved sample times: {0}. Planned observation intervals: {1}." msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:24 -msgid "Saved settings and processor reports are separate." +#: src/components/biometrics/Calibration/CalibrationPage.tsx:196 +msgid "Saved the profile" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationControls.tsx:141 +msgid "Saves it as the profile and requests it; the current saved profile becomes previous. The processor keeps running what it has until it acknowledges the request." +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:318 +msgid "Saving…" msgstr "" #: src/components/biometrics/Calibration/ShadowSchedule.tsx:129 @@ -2339,10 +2649,6 @@ msgstr "" msgid "Selected sessions: {0} / 60" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:53 -msgid "Sensor measurements are separate from algorithm personalization." -msgstr "" - #. placeholder {0}: cohort.sessionIds.join(', ') #: src/components/biometrics/Calibration/DatasetLibrary.tsx:78 msgid "Sessions: {0}" @@ -2370,6 +2676,23 @@ msgstr "" msgid "Shared-timestamp comparisons. Differences are comparison minus baseline; negative error differences mean lower error on shared observations." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:143 +msgid "Shortest breathing interval (× median)" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationPage.tsx:377 +msgid "show" +msgstr "" + +#. placeholder {0}: rows.length +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:42 +msgid "Show all {0}" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationActivity.tsx:42 +msgid "Show recent" +msgstr "" + #: src/components/biometrics/Calibration/ReplaySessionPicker.tsx:67 msgid "Signal replay unavailable: raw waveforms and processing context have not been retained by this service." msgstr "" @@ -2382,6 +2705,14 @@ msgstr "" msgid "Simulated demo: generated readings, no hardware execution or qualification." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:168 +msgid "Sleep stages" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:162 +msgid "SLEEP STAGES" +msgstr "" + #: src/components/biometrics/Calibration/CalibrationReferences.tsx:59 msgid "Sleeper identifier" msgstr "" @@ -2464,10 +2795,6 @@ msgstr "" msgid "Stage trace for the displayed intervals; exact times and stages follow in the table." msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:15 -msgid "Stale" -msgstr "" - #: src/components/biometrics/Calibration/ReferenceOverlay.tsx:67 #: src/components/biometrics/Calibration/StageTrace.tsx:64 msgid "Start" @@ -2498,6 +2825,11 @@ msgstr "" msgid "Stop recorded" msgstr "" +#. placeholder {0}: i.summary.source +#: src/components/biometrics/Calibration/CalibrationPage.tsx:227 +msgid "Stored a {0} reference import" +msgstr "" + #. placeholder {0}: window.observationCount #: src/components/biometrics/Calibration/NumericReport.tsx:166 msgid "Stored observations: {0}" @@ -2515,7 +2847,8 @@ msgstr "" #: src/components/biometrics/Calibration/BacktestJobs.tsx:64 #: src/components/biometrics/Calibration/BacktestReport.tsx:60 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:25 +#: src/components/biometrics/Calibration/CalibrationControls.tsx:80 +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:103 #: src/components/biometrics/Calibration/NumericReport.tsx:26 #: src/components/biometrics/Calibration/NumericTrace.tsx:147 #: src/components/biometrics/Calibration/ProfileComparison.tsx:29 @@ -2525,6 +2858,10 @@ msgstr "" msgid "System defaults" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:54 +msgid "tested on save" +msgstr "" + #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:84 msgid "The archive directory is unavailable." msgstr "" @@ -2538,14 +2875,14 @@ msgstr "" msgid "The frozen offset of {clockOffsetMs} ms is added to recomputed timestamps in this overlay. Reference times stay unchanged. Point matching tolerance: {0} ms." msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:45 -msgid "The HRV index measures window IBI variability. It is not clinical beat-to-beat RMSSD, and incompatible reference metrics cannot validate it." -msgstr "" - #: src/components/biometrics/Calibration/WaveformDatasetBuilder.tsx:83 msgid "The last archive was unavailable. Continue to inspect the next archive." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:139 +msgid "the median" +msgstr "" + #: src/components/biometrics/Calibration/ShadowReport.tsx:19 msgid "The observation gap exceeded the planned limit." msgstr "" @@ -2554,10 +2891,6 @@ msgstr "" msgid "The observation window was missed." msgstr "" -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:27 -msgid "The pinned profile is incompatible. Its revision will be preserved, but a valid before-and-after comparison is unavailable." -msgstr "" - #: src/components/biometrics/Calibration/ShadowSchedule.tsx:77 msgid "The plan could not be prepared. Check the selected profile, window and installed qualification." msgstr "" @@ -2579,12 +2912,15 @@ msgstr "" msgid "The retained inputs are unavailable for new evaluations." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:171 +msgid "The rule list itself differs from the shipped rules." +msgstr "" + #: src/components/biometrics/Calibration/ProfileComparison.tsx:33 msgid "The saved profile changed. Compare again before reviewing these changes." msgstr "" #: src/components/biometrics/Calibration/ProfileComparison.tsx:33 -#: src/components/biometrics/Calibration/ProfileRecovery.tsx:34 msgid "The saved profile changed. Select a recovery action again to make a new request." msgstr "" @@ -2636,6 +2972,10 @@ msgstr "" msgid "These scores compare readings recomputed from retained waveforms. Shared-timestamp scores use observations available in every configuration. Differences describe agreement with the reference and do not establish validated improvement." msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:237 +msgid "This backtest compared stored readings, so there is no recomputed heart-rate trace to draw." +msgstr "" + #: src/components/biometrics/Calibration/StageTrace.tsx:41 msgid "This chart shows only the current page of replay intervals. Gaps are left empty; no stages are inferred between intervals." msgstr "" @@ -2709,7 +3049,6 @@ msgstr "" msgid "Times shown in {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:23 #: src/components/biometrics/Calibration/NumericReport.tsx:85 #: src/components/biometrics/Calibration/StageTrace.tsx:43 msgid "Times shown in {timezone}" @@ -2744,6 +3083,19 @@ msgstr "" msgid "Trace night" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:125 +msgid "Tracking history length" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:115 +msgid "Tracking tolerance (bpm)" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:112 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "tuning" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 #: src/components/biometrics/Calibration/BacktestSubmission.tsx:73 @@ -2763,6 +3115,10 @@ msgstr "" msgid "Unavailable or belongs to a different sleeper." msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:53 +msgid "unchanged" +msgstr "" + #. placeholder {0}: parameter.unit #. placeholder {1}: parameter.minimum #. placeholder {2}: parameter.maximum @@ -2790,10 +3146,6 @@ msgstr "" msgid "Use offset-bearing ISO timestamps or Unix milliseconds. Each record needs recordKey, metric, value, unit, and timestamp (or start and end). JSON uses an array; CSV uses these field names as headers. Imports are limited to 2 MiB and 2000 records." msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:17 -msgid "Use system defaults" -msgstr "" - #: src/components/biometrics/Calibration/ShadowSchedule.tsx:145 msgid "Use the same sleeper identity as the reference records you intend to compare." msgstr "" @@ -2802,6 +3154,11 @@ msgstr "" msgid "Validated activation" msgstr "" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:116 +#: src/components/biometrics/Calibration/CalibrationPage.tsx:206 +msgid "validation" +msgstr "" + #: src/components/biometrics/Calibration/BacktestJobs.tsx:63 #: src/components/biometrics/Calibration/BacktestReport.tsx:61 #: src/components/biometrics/Calibration/BacktestSubmission.tsx:74 @@ -2827,8 +3184,6 @@ msgstr "" msgid "Vitals with unknown original processing provenance: {0}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:17 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Waiting for a processing boundary" msgstr "" @@ -2837,8 +3192,6 @@ msgstr "" msgid "Waiting for a worker and resource admission. No execution is implied by a queued request." msgstr "" -#: src/components/biometrics/Calibration/CalibrationHistory.tsx:18 -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:16 #: src/components/biometrics/Calibration/ManualProfileActivation.tsx:73 msgid "Waiting for delivery" msgstr "" @@ -2856,8 +3209,13 @@ msgstr "" msgid "Waveform dataset retained: {published}" msgstr "" -#: src/components/biometrics/Calibration/CalibrationOverview.tsx:44 -msgid "What these metrics mean" +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:157 +#: src/components/biometrics/Calibration/CalibrationEvidence.tsx:180 +msgid "What the saved profile did" +msgstr "" + +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:176 +msgid "when" msgstr "" #: src/components/biometrics/Calibration/DatasetBuilder.tsx:59 @@ -2875,6 +3233,10 @@ msgstr "" msgid "Window IBI variability index (ms)" msgstr "" +#: src/components/biometrics/Calibration/CalibrationProfileCard.tsx:158 +msgid "window IBI variability, not clinical RMSSD" +msgstr "" + #: src/components/biometrics/Calibration/ShadowReport.tsx:34 msgid "Within the observed limits." msgstr "" @@ -2883,6 +3245,10 @@ msgstr "" msgid "Work was deferred; your selection and retry key are preserved." msgstr "" +#: src/components/biometrics/Calibration/BiometricCalibration.tsx:41 +msgid "WORKBENCH" +msgstr "" + #: src/components/biometrics/Calibration/ShadowReport.tsx:18 msgid "Worker cleanup has not been confirmed." msgstr "" diff --git a/tests/browser/workflow.e2e.ts b/tests/browser/workflow.e2e.ts index 0ab87ce8..cc64692b 100644 --- a/tests/browser/workflow.e2e.ts +++ b/tests/browser/workflow.e2e.ts @@ -78,14 +78,15 @@ test('import, freeze, replay, review, manually apply, acknowledge, and roll back expect(configuration()).toContainEqual({ side: 'left', desired_revision_id: candidateId, generation: 1 }) await view(page, 'Overview') - await page.getByRole('button', { name: 'Review previous profile', exact: true }).click() - await page.getByRole('button', { name: 'Compare with saved profile', exact: true }).click() - await page.getByLabel('I intend to restore the exact previous profile without validation evidence.', { exact: true }).check() - await page.getByRole('button', { name: 'Restore previous profile', exact: true }).click() - const recovery = page.getByRole('region', { name: 'Manual activation' }) - await expect(recovery.getByText('Waiting for delivery', { exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Restore previous', exact: true }).click() + const review = page.getByRole('dialog', { name: 'Restore the previous profile' }) + await expect(review).toContainText('heart_rate.trackingToleranceBpm') + await review.getByRole('button', { name: 'Restore and request', exact: true }).click() + const recovery = page.getByRole('group', { name: 'Profile lifecycle' }) + await expect(recovery.locator('[data-segment="requested"]')).toContainText('waiting') await fixture('adopt') - await expect(recovery.getByText('Applied', { exact: true })).toBeVisible({ timeout: 15_000 }) + await expect(recovery.locator('[data-segment="requested"]')).toContainText('acknowledged', { timeout: 15_000 }) + await expect(recovery.locator('[data-segment="processor"]')).toContainText('running saved') expect(configuration()).toContainEqual({ side: 'left', desired_revision_id: null, generation: 2 }) expect(configuration().filter(row => row.side === 'right')).toEqual(baseline.filter(row => row.side === 'right')) await recovery.screenshot({ path: test.info().outputPath('rollback-receipt.png') })