diff --git a/scripts/bin/sp-update b/scripts/bin/sp-update index 7d3983b3a..1b16e3058 100644 --- a/scripts/bin/sp-update +++ b/scripts/bin/sp-update @@ -681,6 +681,11 @@ if [ -f "$INSTALL_DIR/scripts/lib/storage-helpers" ]; then install_storage_cleanup_sudoers fi +if [ -f "$INSTALL_DIR/scripts/lib/journald-helpers" ]; then + source "$INSTALL_DIR/scripts/lib/journald-helpers" + install_journald_config +fi + # Re-block WAN before starting service if [ "$WAN_WAS_BLOCKED" = true ]; then reload_iptables_helpers diff --git a/scripts/install b/scripts/install index 26029a48c..b13f39aab 100755 --- a/scripts/install +++ b/scripts/install @@ -279,9 +279,9 @@ reclaim_rootfs_caches() { rm -rf /var/lib/dnf fi - # Journal vacuum. The journald config below caps SystemMaxUse=50M, but - # only enforces it on rotation. A one-shot vacuum brings the on-disk - # size in line immediately; the cap keeps growth bounded after that. + # Journal vacuum. The journald config below keeps new logs in RAM, so + # the on-disk journal stops growing; a one-shot vacuum trims what the + # firmware's persistent journal already holds. if command -v journalctl &>/dev/null; then echo " Vacuuming systemd journal to 20M..." journalctl --vacuum-size=20M >/dev/null 2>&1 || true @@ -1494,15 +1494,9 @@ RuntimeDirectory=dac WantedBy=multi-user.target EOF -# Cap journal size to prevent unbounded log growth on limited disk -mkdir -p /etc/systemd/journald.conf.d -cat > /etc/systemd/journald.conf.d/sleepypod.conf << 'JOURNALD' -[Journal] -SystemMaxUse=50M -SystemKeepFree=200M -MaxFileSec=1week -JOURNALD -systemctl restart systemd-journald 2>/dev/null || true +# Keep the journal in RAM so firmware log chatter doesn't wear the eMMC +source "$INSTALL_DIR/scripts/lib/journald-helpers" +install_journald_config # Install CLI tools from scripts/bin/ BEFORE the service starts. The systemd # unit's ExecStartPre references /usr/local/bin/sp-maintenance — if these diff --git a/scripts/lib/journald-helpers b/scripts/lib/journald-helpers new file mode 100644 index 000000000..34e90bf9c --- /dev/null +++ b/scripts/lib/journald-helpers @@ -0,0 +1,45 @@ +# shellcheck shell=bash +# Keep the systemd journal in RAM (/run/log/journal). Eight's firmware sets +# Storage=persistent and links /var/log/journal to /persistent/journal, so +# frank's steady log stream otherwise lands on the eMMC around the clock. A +# drop-in overrides journald.conf; logs from earlier boots are the cost. +# Shared by install and sp-update so existing pods pick it up on update. +install_journald_config() { + local file="/etc/systemd/journald.conf.d/sleepypod.conf" tmp backup="" + mkdir -p /etc/systemd/journald.conf.d + tmp=$(mktemp) + cat > "$tmp" <<'JOURNALD' +[Journal] +Storage=volatile +RuntimeMaxUse=48M +RuntimeKeepFree=100M +MaxFileSec=1week +JOURNALD + if cmp -s "$tmp" "$file"; then + rm -f "$tmp" + return 0 + fi + if [ -f "$file" ]; then + backup=$(mktemp) + cp -fp "$file" "$backup" + fi + install -m 0644 "$tmp" "$file" + rm -f "$tmp" + # journald keeps service stdout streams in its fd store across a restart. + # If it won't restart, put the previous config back and bring journald up + # on it, so the next install or update retries. Non-fatal either way. + if ! systemctl restart systemd-journald 2>/dev/null; then + if [ -n "$backup" ]; then + install -m 0644 "$backup" "$file" + else + rm -f "$file" + fi + systemctl restart systemd-journald 2>/dev/null || true + if systemctl is-active --quiet systemd-journald; then + echo "Warning: systemd-journald rejected the RAM-only journal config — kept the previous one, will retry on next update" >&2 + else + echo "Warning: systemd-journald is not running after restoring its previous config — check 'systemctl status systemd-journald'" >&2 + fi + fi + rm -f "$backup" +} diff --git a/src/components/System/StorageTab.tsx b/src/components/System/StorageTab.tsx index 441a74400..d12ae5d95 100644 --- a/src/components/System/StorageTab.tsx +++ b/src/components/System/StorageTab.tsx @@ -1,15 +1,17 @@ 'use client' import { useState } from 'react' -import { HardDrive, Trash2 } from 'lucide-react' +import { Gauge, HardDrive, Trash2 } from 'lucide-react' import type { inferRouterOutputs } from '@trpc/server' import type { AppRouter } from '@/src/server/routers/app' import { trpc } from '@/src/utils/trpc' -import { Button, Card, CardHeader, InlineError, Skeleton, Toggle } from '@/src/components/ds' +import { Button, Card, CardHeader, InlineError, KeyValue, Skeleton, StatusDot, Toggle } from '@/src/components/ds' import { ConfirmDialog } from '@/src/components/Schedule/ConfirmDialog' import { formatBytes } from '@/src/components/status/SystemInfoCard' type Storage = inferRouterOutputs['system']['getStorage'] +type Flash = NonNullable['system']['getFlashHealth']> +type Wear = Flash['lifeTime'][number] type SegmentKey = Storage['segments'][number]['key'] const SEGMENTS: Record = { @@ -26,6 +28,60 @@ function shortPath(p: string): string { return p.replace(/^\/persistent\//, '') } +const PRE_EOL = { + normal: { label: 'Normal', tone: 'ok' }, + warning: { label: 'Warning', tone: 'warn' }, + urgent: { label: 'Urgent', tone: 'danger' }, +} as const + +/** JEDEC reports wear in 10% steps; "exceeded" has no upper bound. */ +function wearLabel(w: NonNullable): string { + return w.maxPercent == null ? 'Past rated life' : `${w.minPercent}–${w.maxPercent}%` +} + +/** Card tone from the chip's own estimates: 80%+ worn or reserve blocks low is a warning. */ +function flashTone(f: Flash): 'warn' | 'danger' | undefined { + const worst = Math.max(...f.lifeTime.map(w => w?.minPercent ?? 0)) + if (f.preEol === 'urgent' || worst >= 100) return 'danger' + if (f.preEol === 'warning' || worst >= 80) return 'warn' + return undefined +} + +/** + * eMMC endurance: how worn the chip says it is, and how fast the pod is + * writing to it. Hidden off-pod, where there's no eMMC to read. + */ +function FlashCard({ flash }: { flash: Flash }) { + const [slc, main] = flash.lifeTime + const eol = flash.preEol && PRE_EOL[flash.preEol] + return ( + + } + /> +
+ + {main && } + {slc && } + + +
+

+ {flash.perDay + ? `Averaged over the last ${flash.perDay.coveredHours < 48 ? `${flash.perDay.coveredHours} h` : `${Math.round(flash.perDay.coveredHours / 24)} days`}. ` + : 'Writes per day appear after an hour of samples. '} + {'Covers everything on the chip — Eight\'s firmware, sleepypod and the OS. Wear is the chip\'s own estimate, in 10% steps.'} +

+
+ ) +} + /** * System → Storage: what fills /persistent, how much raw history that leaves * once the pruner trims to its target, and a one-button cleanup of @@ -34,6 +90,7 @@ function shortPath(p: string): string { export function StorageTab() { const utils = trpc.useUtils() const storage = trpc.system.getStorage.useQuery({}, { refetchInterval: 60_000 }) + const flash = trpc.system.getFlashHealth.useQuery({}, { refetchInterval: 60_000 }) const [includeBackups, setIncludeBackups] = useState(false) const [confirming, setConfirming] = useState(false) const free = trpc.system.freeStorage.useMutation({ @@ -127,6 +184,8 @@ export function StorageTab() {

+ {flash.data && } + ({ mutate: vi.fn(), result: undefined as unknown, pending: false, + flash: null as unknown, })) vi.mock('@/src/utils/trpc', () => ({ @@ -15,6 +16,7 @@ vi.mock('@/src/utils/trpc', () => ({ useUtils: () => ({ system: { getStorage: { invalidate: vi.fn() } } }), system: { getStorage: { useQuery: () => ({ data: mocks.pending ? undefined : mocks.data, isPending: mocks.pending, isLoading: false, error: null }) }, + getFlashHealth: { useQuery: () => ({ data: mocks.flash }) }, freeStorage: { useMutation: () => ({ mutate: mocks.mutate, isPending: false, error: null, data: mocks.result }) }, }, }, @@ -26,6 +28,7 @@ beforeEach(() => { mocks.mutate.mockReset() mocks.result = undefined mocks.pending = false + mocks.flash = null mocks.data = { persistent: { totalBytes: 15 * GB, usedBytes: 12 * GB, availableBytes: 3 * GB, usedPercent: 80 }, prunerTargetPercent: 80, @@ -96,4 +99,47 @@ describe('StorageTab', () => { expect(screen.getByText(/Cleanup tool not installed/)).toBeTruthy() expect(screen.queryByRole('button', { name: /Free up/ })).toBeNull() }) + + describe('flash endurance', () => { + const flash = (over: Record = {}) => ({ + device: 'mmcblk0', + name: 'C9A551', + capacityBytes: 29 * GB, + lifeTime: [{ minPercent: 30, maxPercent: 40 }, null], + preEol: 'normal', + sinceBoot: { bytes: 1.5 * GB, uptimeSeconds: 2600 }, + perDay: { bytes: 3.1 * GB, coveredHours: 168 }, + ...over, + }) + + it('is hidden off-pod', () => { + render() + expect(screen.queryByTestId('flash-card')).toBeNull() + }) + + it('shows wear, reserve blocks and writes per day', () => { + mocks.flash = flash() + render() + expect(screen.getByText('C9A551 · 29.0 GB eMMC')).toBeTruthy() + expect(screen.getByText('30–40%')).toBeTruthy() + expect(screen.queryByText('Wear (main area)')).toBeNull() + expect(screen.getByText('3.1 GB')).toBeTruthy() + expect(screen.getByText(/Averaged over the last 7 days/)).toBeTruthy() + expect(screen.getAllByText('Normal').length).toBeGreaterThan(0) + }) + + it('says it is still measuring before an hour of samples', () => { + mocks.flash = flash({ perDay: null }) + render() + expect(screen.getByText('Measuring…')).toBeTruthy() + expect(screen.getByText(/appear after an hour of samples/)).toBeTruthy() + }) + + it('flags a chip past rated life', () => { + mocks.flash = flash({ lifeTime: [null, { minPercent: 100, maxPercent: null }], preEol: 'urgent' }) + render() + expect(screen.getByText('Past rated life')).toBeTruthy() + expect(screen.getByTestId('flash-card').className).toContain('border-danger-line') + }) + }) }) diff --git a/src/db/biometrics-migrations/0031_wide_black_knight.sql b/src/db/biometrics-migrations/0031_wide_black_knight.sql new file mode 100644 index 000000000..670381943 --- /dev/null +++ b/src/db/biometrics-migrations/0031_wide_black_knight.sql @@ -0,0 +1,8 @@ +CREATE TABLE `flash_write_samples` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `timestamp` integer NOT NULL, + `boot_id` text NOT NULL, + `sectors_written` integer NOT NULL +); +--> statement-breakpoint +CREATE INDEX `idx_flash_write_samples_timestamp` ON `flash_write_samples` (`timestamp`); \ No newline at end of file diff --git a/src/db/biometrics-migrations/meta/0031_snapshot.json b/src/db/biometrics-migrations/meta/0031_snapshot.json new file mode 100644 index 000000000..b97b4ff95 --- /dev/null +++ b/src/db/biometrics-migrations/meta/0031_snapshot.json @@ -0,0 +1,3006 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "53709beb-0cba-4645-9770-0ef733082ef6", + "prevId": "8642f917-fd30-4e9e-8b99-de14b5ea5dca", + "tables": { + "ambient_light": { + "name": "ambient_light", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lux": { + "name": "lux", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_ambient_light_timestamp": { + "name": "idx_ambient_light_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bed_temp": { + "name": "bed_temp", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ambient_temp": { + "name": "ambient_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcu_temp": { + "name": "mcu_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "humidity": { + "name": "humidity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_outer_temp": { + "name": "left_outer_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_center_temp": { + "name": "left_center_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_inner_temp": { + "name": "left_inner_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_outer_temp": { + "name": "right_outer_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_center_temp": { + "name": "right_center_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_inner_temp": { + "name": "right_inner_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_bed_temp_timestamp": { + "name": "idx_bed_temp_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_backtest_jobs": { + "name": "biometric_backtest_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "experiment_id": { + "name": "experiment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lease_expires_at_ms": { + "name": "lease_expires_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fencing_token": { + "name": "fencing_token", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "heartbeat_at_ms": { + "name": "heartbeat_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "progress": { + "name": "progress", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cancellation_requested_at_ms": { + "name": "cancellation_requested_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at_ms": { + "name": "updated_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "biometric_backtest_jobs_idempotency_key_unique": { + "name": "biometric_backtest_jobs_idempotency_key_unique", + "columns": [ + "idempotency_key" + ], + "isUnique": true + }, + "idx_biometric_job_state_created": { + "name": "idx_biometric_job_state_created", + "columns": [ + "state", + "created_at_ms" + ], + "isUnique": false + }, + "idx_biometric_job_experiment": { + "name": "idx_biometric_job_experiment", + "columns": [ + "experiment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "biometric_backtest_jobs_experiment_id_biometric_experiments_id_fk": { + "name": "biometric_backtest_jobs_experiment_id_biometric_experiments_id_fk", + "tableFrom": "biometric_backtest_jobs", + "tableTo": "biometric_experiments", + "columnsFrom": [ + "experiment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_backtest_results": { + "name": "biometric_backtest_results", + "columns": { + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "fencing_token": { + "name": "fencing_token", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "report": { + "name": "report", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at_ms": { + "name": "completed_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "released_at_ms": { + "name": "released_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "biometric_backtest_results_job_id_biometric_backtest_jobs_id_fk": { + "name": "biometric_backtest_results_job_id_biometric_backtest_jobs_id_fk", + "tableFrom": "biometric_backtest_results", + "tableTo": "biometric_backtest_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_dataset_raw_artifacts": { + "name": "biometric_dataset_raw_artifacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "dataset_id": { + "name": "dataset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_hash": { + "name": "artifact_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "released_at_ms": { + "name": "released_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_biometric_dataset_raw_identity": { + "name": "idx_biometric_dataset_raw_identity", + "columns": [ + "dataset_id", + "artifact_hash" + ], + "isUnique": true + }, + "idx_biometric_dataset_raw_position": { + "name": "idx_biometric_dataset_raw_position", + "columns": [ + "dataset_id", + "position" + ], + "isUnique": true + }, + "idx_biometric_raw_artifact_references": { + "name": "idx_biometric_raw_artifact_references", + "columns": [ + "artifact_hash", + "released_at_ms" + ], + "isUnique": false + } + }, + "foreignKeys": { + "biometric_dataset_raw_artifacts_dataset_id_biometric_datasets_id_fk": { + "name": "biometric_dataset_raw_artifacts_dataset_id_biometric_datasets_id_fk", + "tableFrom": "biometric_dataset_raw_artifacts", + "tableTo": "biometric_datasets", + "columnsFrom": [ + "dataset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "biometric_dataset_raw_artifacts_artifact_hash_biometric_raw_artifacts_content_hash_fk": { + "name": "biometric_dataset_raw_artifacts_artifact_hash_biometric_raw_artifacts_content_hash_fk", + "tableFrom": "biometric_dataset_raw_artifacts", + "tableTo": "biometric_raw_artifacts", + "columnsFrom": [ + "artifact_hash" + ], + "columnsTo": [ + "content_hash" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_datasets": { + "name": "biometric_datasets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manifest": { + "name": "manifest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot": { + "name": "snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "released_at_ms": { + "name": "released_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "biometric_datasets_idempotency_key_unique": { + "name": "biometric_datasets_idempotency_key_unique", + "columns": [ + "idempotency_key" + ], + "isUnique": true + }, + "idx_biometric_dataset_side_created": { + "name": "idx_biometric_dataset_side_created", + "columns": [ + "side", + "created_at_ms", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_experiments": { + "name": "biometric_experiments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dataset_id": { + "name": "dataset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_hash": { + "name": "protocol_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "holdout_accesses": { + "name": "holdout_accesses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "biometric_experiments_idempotency_key_unique": { + "name": "biometric_experiments_idempotency_key_unique", + "columns": [ + "idempotency_key" + ], + "isUnique": true + }, + "idx_biometric_experiment_dataset": { + "name": "idx_biometric_experiment_dataset", + "columns": [ + "dataset_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "biometric_experiments_dataset_id_biometric_datasets_id_fk": { + "name": "biometric_experiments_dataset_id_biometric_datasets_id_fk", + "tableFrom": "biometric_experiments", + "tableTo": "biometric_datasets", + "columnsFrom": [ + "dataset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_holdout_accesses": { + "name": "biometric_holdout_accesses", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_ms": { + "name": "start_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_ms": { + "name": "end_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessed_at_ms": { + "name": "accessed_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_biometric_holdout_side_start": { + "name": "idx_biometric_holdout_side_start", + "columns": [ + "side", + "start_ms" + ], + "isUnique": false + } + }, + "foreignKeys": { + "biometric_holdout_accesses_job_id_biometric_backtest_jobs_id_fk": { + "name": "biometric_holdout_accesses_job_id_biometric_backtest_jobs_id_fk", + "tableFrom": "biometric_holdout_accesses", + "tableTo": "biometric_backtest_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_processing_runs": { + "name": "biometric_processing_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bundle": { + "name": "bundle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_hash": { + "name": "config_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "calibration_hash": { + "name": "calibration_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_identity": { + "name": "input_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at_ms": { + "name": "started_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at_ms": { + "name": "ended_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_biometric_processing_side_start": { + "name": "idx_biometric_processing_side_start", + "columns": [ + "side", + "started_at_ms" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_raw_artifacts": { + "name": "biometric_raw_artifacts", + "columns": { + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compressed_bytes": { + "name": "compressed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expanded_bytes": { + "name": "expanded_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "released_at_ms": { + "name": "released_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "biometric_raw_artifacts_name_unique": { + "name": "biometric_raw_artifacts_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_raw_sources": { + "name": "biometric_raw_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_name": { + "name": "source_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_hash": { + "name": "artifact_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "descriptor": { + "name": "descriptor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inventory_version": { + "name": "inventory_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inventory": { + "name": "inventory", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_event_ms": { + "name": "first_event_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_event_ms": { + "name": "last_event_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspected_at_ms": { + "name": "inspected_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unavailable_at_ms": { + "name": "unavailable_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_biometric_raw_source_identity": { + "name": "idx_biometric_raw_source_identity", + "columns": [ + "source_name", + "artifact_hash", + "inventory_version" + ], + "isUnique": true + }, + "idx_biometric_raw_source_window": { + "name": "idx_biometric_raw_source_window", + "columns": [ + "first_event_ms", + "last_event_ms" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_runtime_configs": { + "name": "biometric_runtime_configs", + "columns": { + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_hash": { + "name": "resolved_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boundary": { + "name": "boundary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_biometric_runtime_config": { + "name": "uq_biometric_runtime_config", + "columns": [ + "side", + "generation" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_runtime_receipts": { + "name": "biometric_runtime_receipts", + "columns": { + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "component": { + "name": "component", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_hash": { + "name": "applied_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effective_sample_time_ms": { + "name": "effective_sample_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "heartbeat_at_ms": { + "name": "heartbeat_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_biometric_runtime_receipt": { + "name": "uq_biometric_runtime_receipt", + "columns": [ + "side", + "component", + "generation" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_shadow_runs": { + "name": "biometric_shadow_runs", + "columns": { + "worker_receipt": { + "name": "worker_receipt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worker_receipt_hash": { + "name": "worker_receipt_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_hash": { + "name": "plan_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lease_until_ms": { + "name": "lease_until_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "heartbeat_at_ms": { + "name": "heartbeat_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at_ms": { + "name": "ended_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "point_count": { + "name": "point_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "byte_count": { + "name": "byte_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "biometric_shadow_runs_idempotency_key_unique": { + "name": "biometric_shadow_runs_idempotency_key_unique", + "columns": [ + "idempotency_key" + ], + "isUnique": true + }, + "idx_biometric_shadow_side_state": { + "name": "idx_biometric_shadow_side_state", + "columns": [ + "side", + "state" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "biometric_shadow_samples": { + "name": "biometric_shadow_samples", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp_ms": { + "name": "timestamp_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_biometric_shadow_sample": { + "name": "uq_biometric_shadow_sample", + "columns": [ + "run_id", + "sequence" + ], + "isUnique": true + }, + "idx_biometric_shadow_sample_time": { + "name": "idx_biometric_shadow_sample_time", + "columns": [ + "run_id", + "timestamp_ms" + ], + "isUnique": false + } + }, + "foreignKeys": { + "biometric_shadow_samples_run_id_biometric_shadow_runs_id_fk": { + "name": "biometric_shadow_samples_run_id_biometric_shadow_runs_id_fk", + "tableFrom": "biometric_shadow_samples", + "tableTo": "biometric_shadow_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calibration_profiles": { + "name": "calibration_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quality_score": { + "name": "quality_score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_start": { + "name": "source_window_start", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_end": { + "name": "source_window_end", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "samples_used": { + "name": "samples_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_cal_type_status": { + "name": "idx_cal_type_status", + "columns": [ + "sensor_type", + "status" + ], + "isUnique": false + }, + "uq_cal_side_type_active": { + "name": "uq_cal_side_type_active", + "columns": [ + "side", + "sensor_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calibration_runs": { + "name": "calibration_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quality_score": { + "name": "quality_score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_start": { + "name": "source_window_start", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_window_end": { + "name": "source_window_end", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "samples_used": { + "name": "samples_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_cal_runs_side_type": { + "name": "idx_cal_runs_side_type", + "columns": [ + "side", + "sensor_type", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cap_sense_frames": { + "name": "cap_sense_frames", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "zones": { + "name": "zones", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max": { + "name": "max", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mean": { + "name": "mean", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spread": { + "name": "spread", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "peak_zone": { + "name": "peak_zone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frame_count": { + "name": "frame_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_counts": { + "name": "status_counts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_cap_sense_frames_side_ts": { + "name": "uq_cap_sense_frames_side_ts", + "columns": [ + "side", + "timestamp" + ], + "isUnique": true + }, + "idx_cap_sense_frames_timestamp": { + "name": "idx_cap_sense_frames_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "flash_write_samples": { + "name": "flash_write_samples", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sectors_written": { + "name": "sectors_written", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_flash_write_samples_timestamp": { + "name": "idx_flash_write_samples_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "flow_readings": { + "name": "flow_readings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "left_flowrate_cd": { + "name": "left_flowrate_cd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_flowrate_cd": { + "name": "right_flowrate_cd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_pump_rpm": { + "name": "left_pump_rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_pump_rpm": { + "name": "right_pump_rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_flow_readings_timestamp": { + "name": "idx_flow_readings_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "freezer_temp": { + "name": "freezer_temp", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ambient_temp": { + "name": "ambient_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "heatsink_temp": { + "name": "heatsink_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "left_water_temp": { + "name": "left_water_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_water_temp": { + "name": "right_water_temp", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_freezer_temp_timestamp": { + "name": "idx_freezer_temp_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "health_runs": { + "name": "health_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_health_runs_check_started": { + "name": "idx_health_runs_check_started", + "columns": [ + "check_id", + "started_at" + ], + "isUnique": false + }, + "idx_health_runs_last_seen": { + "name": "idx_health_runs_last_seen", + "columns": [ + "last_seen_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "movement": { + "name": "movement", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_movement": { + "name": "total_movement", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_movement_side_timestamp": { + "name": "idx_movement_side_timestamp", + "columns": [ + "side", + "timestamp" + ], + "isUnique": false + }, + "idx_movement_timestamp": { + "name": "idx_movement_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prime_events": { + "name": "prime_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_prime_events_timestamp": { + "name": "idx_prime_events_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pump_alerts": { + "name": "pump_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "flowrate_cd": { + "name": "flowrate_cd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "restore_target_temperature": { + "name": "restore_target_temperature", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "restore_duration_seconds": { + "name": "restore_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_pump_alerts_timestamp": { + "name": "idx_pump_alerts_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_pump_alerts_acknowledged": { + "name": "idx_pump_alerts_acknowledged", + "columns": [ + "acknowledged_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reference_imports": { + "name": "reference_imports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supersedes_import_id": { + "name": "supersedes_import_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "adapter_version": { + "name": "adapter_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_policy": { + "name": "time_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overlap_policy": { + "name": "overlap_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'exclude_conflicts'" + }, + "accepted_count": { + "name": "accepted_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rejected_count": { + "name": "rejected_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at_ms": { + "name": "deleted_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_reference_import_active_root": { + "name": "uq_reference_import_active_root", + "columns": [ + "content_hash" + ], + "isUnique": true, + "where": "\"reference_imports\".\"deleted_at_ms\" IS NULL AND \"reference_imports\".\"supersedes_import_id\" IS NULL" + }, + "uq_reference_import_active_correction": { + "name": "uq_reference_import_active_correction", + "columns": [ + "content_hash", + "supersedes_import_id" + ], + "isUnique": true, + "where": "\"reference_imports\".\"deleted_at_ms\" IS NULL" + }, + "uq_reference_import_revision": { + "name": "uq_reference_import_revision", + "columns": [ + "family_id", + "revision" + ], + "isUnique": true + }, + "idx_reference_import_side_created": { + "name": "idx_reference_import_side_created", + "columns": [ + "side", + "created_at_ms", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reference_measurements": { + "name": "reference_measurements", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "import_id": { + "name": "import_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_key": { + "name": "record_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_ms": { + "name": "start_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_ms": { + "name": "end_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "temporal_kind": { + "name": "temporal_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "aggregation": { + "name": "aggregation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_ms": { + "name": "window_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quality": { + "name": "quality", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scorable": { + "name": "scorable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original": { + "name": "original", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_reference_measurement_key": { + "name": "uq_reference_measurement_key", + "columns": [ + "import_id", + "record_key" + ], + "isUnique": true + }, + "idx_reference_measurement_side_metric_start": { + "name": "idx_reference_measurement_side_metric_start", + "columns": [ + "side", + "metric", + "start_ms" + ], + "isUnique": false + }, + "idx_reference_measurement_import_id": { + "name": "idx_reference_measurement_import_id", + "columns": [ + "import_id", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reference_measurements_import_id_reference_imports_id_fk": { + "name": "reference_measurements_import_id_reference_imports_id_fk", + "tableFrom": "reference_measurements", + "tableTo": "reference_imports", + "columnsFrom": [ + "import_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reference_stages": { + "name": "reference_stages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end": { + "name": "end", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_id": { + "name": "import_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_reference_stages_side_start_source": { + "name": "uq_reference_stages_side_start_source", + "columns": [ + "side", + "start", + "source" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reference_vitals": { + "name": "reference_vitals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heart_rate": { + "name": "heart_rate", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_id": { + "name": "import_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_reference_vitals_side_timestamp_source": { + "name": "uq_reference_vitals_side_timestamp_source", + "columns": [ + "side", + "timestamp", + "source" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sleep_records": { + "name": "sleep_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entered_bed_at": { + "name": "entered_bed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "left_bed_at": { + "name": "left_bed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sleep_duration_seconds": { + "name": "sleep_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "times_exited_bed": { + "name": "times_exited_bed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "present_intervals": { + "name": "present_intervals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_present_intervals": { + "name": "not_present_intervals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_sleep_records_side_entered": { + "name": "idx_sleep_records_side_entered", + "columns": [ + "side", + "entered_bed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thermal_state": { + "name": "thermal_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_powered": { + "name": "is_powered", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_temp_f": { + "name": "target_temp_f", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_temp_f": { + "name": "current_temp_f", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_thermal_state_timestamp": { + "name": "idx_thermal_state_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vitals": { + "name": "vitals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heart_rate": { + "name": "heart_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hrv": { + "name": "hrv", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "breathing_rate": { + "name": "breathing_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_vitals_side_timestamp": { + "name": "uq_vitals_side_timestamp", + "columns": [ + "side", + "timestamp" + ], + "isUnique": true + }, + "idx_vitals_timestamp": { + "name": "idx_vitals_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vitals_quality": { + "name": "vitals_quality", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "vitals_id": { + "name": "vitals_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quality_score": { + "name": "quality_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "flags": { + "name": "flags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hr_raw": { + "name": "hr_raw", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_vq_vitals_id": { + "name": "idx_vq_vitals_id", + "columns": [ + "vitals_id" + ], + "isUnique": false + }, + "idx_vq_side_ts": { + "name": "idx_vq_side_ts", + "columns": [ + "side", + "timestamp" + ], + "isUnique": false + }, + "idx_vq_timestamp": { + "name": "idx_vq_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "water_level_alerts": { + "name": "water_level_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_water_level_alerts_dismissed": { + "name": "idx_water_level_alerts_dismissed", + "columns": [ + "dismissed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "water_level_readings": { + "name": "water_level_readings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw": { + "name": "raw", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "calibrated_empty": { + "name": "calibrated_empty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "calibrated_full": { + "name": "calibrated_full", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_water_level_timestamp": { + "name": "idx_water_level_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/biometrics-migrations/meta/_journal.json b/src/db/biometrics-migrations/meta/_journal.json index abb988136..1d0dba727 100644 --- a/src/db/biometrics-migrations/meta/_journal.json +++ b/src/db/biometrics-migrations/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1791878980742, "tag": "0030_icy_maginty", "breakpoints": true + }, + { + "idx": 31, + "version": "6", + "when": 1792274814771, + "tag": "0031_wide_black_knight", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/biometrics-schema.ts b/src/db/biometrics-schema.ts index fbacef079..89878ba7e 100644 --- a/src/db/biometrics-schema.ts +++ b/src/db/biometrics-schema.ts @@ -231,6 +231,20 @@ export const healthRuns = sqliteTable('health_runs', { index('idx_health_runs_last_seen').on(t.lastSeenAt), ]) +/** + * Hourly readings of the eMMC's cumulative sectors-written counter + * (/sys/block//stat) for System → Storage's writes-per-day. The kernel + * resets the counter at boot, so each row carries the boot it was read in. + */ +export const flashWriteSamples = sqliteTable('flash_write_samples', { + id: integer('id').primaryKey({ autoIncrement: true }), + timestamp: integer('timestamp', { mode: 'timestamp' }).notNull(), + bootId: text('boot_id').notNull(), + sectorsWritten: integer('sectors_written').notNull(), +}, t => [ + index('idx_flash_write_samples_timestamp').on(t.timestamp), +]) + /** * Externally scored sleep stages (Apple Watch SleepAnalysis posted by the iOS * app) used as a named comparison source for the pod's stage classifier. One diff --git a/src/db/retention.ts b/src/db/retention.ts index 921b7ebc0..6b9364d6e 100644 --- a/src/db/retention.ts +++ b/src/db/retention.ts @@ -4,6 +4,7 @@ import { biometricsDb } from './biometrics' import { ambientLight, bedTemp, + flashWriteSamples, flowReadings, freezerTemp, healthRuns, @@ -26,7 +27,7 @@ import { * Tables covered (all write at ≥1/minute and have no referential joins): * vitals, movement, bed_temp, freezer_temp, flow_readings, * ambient_light, water_level_readings, pump_alerts, vitals_quality, - * thermal_state, health_runs + * thermal_state, health_runs, flash_write_samples * * vitals_quality shares vitals' timestamp cutoff so each quality row dies * with its paired vitals row (vitals_id is a logical, unenforced reference — @@ -52,6 +53,7 @@ const RETENTION_TABLES = [ { table: pumpAlerts, column: pumpAlerts.timestamp, name: 'pump_alerts' }, { table: thermalState, column: thermalState.timestamp, name: 'thermal_state' }, { table: healthRuns, column: healthRuns.lastSeenAt, name: 'health_runs' }, + { table: flashWriteSamples, column: flashWriteSamples.timestamp, name: 'flash_write_samples' }, ] as const /** Tables the daily retention pass prunes, for System → Databases. */ diff --git a/src/db/tests/__snapshots__/schema.test.ts.snap b/src/db/tests/__snapshots__/schema.test.ts.snap index f1737ba6b..5ea1312bb 100644 --- a/src/db/tests/__snapshots__/schema.test.ts.snap +++ b/src/db/tests/__snapshots__/schema.test.ts.snap @@ -415,6 +415,23 @@ exports[`complete Drizzle schema contracts > pins every biometrics table, column "primaryKeys": [], "uniqueConstraints": [], }, + { + "checks": 0, + "columns": [ + "id | sql:integer | logical:number/SQLiteInteger | not-null | default:auto | primary-key | not-unique | autoincrement | enum:none", + "timestamp | sql:integer | logical:date/SQLiteTimestamp | not-null | default:none | not-primary-key | not-unique | not-autoincrement | enum:none", + "boot_id | sql:text | logical:string/SQLiteText | not-null | default:none | not-primary-key | not-unique | not-autoincrement | enum:none", + "sectors_written | sql:integer | logical:number/SQLiteInteger | not-null | default:none | not-primary-key | not-unique | not-autoincrement | enum:none", + ], + "exportName": "flashWriteSamples", + "foreignKeys": [], + "indexes": [ + "idx_flash_write_samples_timestamp | non-unique | columns:timestamp | where:none", + ], + "name": "flash_write_samples", + "primaryKeys": [], + "uniqueConstraints": [], + }, { "checks": 0, "columns": [ diff --git a/src/db/tests/retention.test.ts b/src/db/tests/retention.test.ts index 7eff879e0..c81be533b 100644 --- a/src/db/tests/retention.test.ts +++ b/src/db/tests/retention.test.ts @@ -142,9 +142,14 @@ describe('pruneOldBiometrics', () => { { checkId: 'dac', status: 'ok', startedAt: old, lastSeenAt: fresh }, ]).run() + db.insert(schema.flashWriteSamples).values([ + { timestamp: old, bootId: 'a', sectorsWritten: 1 }, + { timestamp: fresh, bootId: 'a', sectorsWritten: 2 }, + ]).run() + const result = pruneOldBiometrics(cutoff, db) - expect(result.rowsDeleted).toBe(11) + expect(result.rowsDeleted).toBe(12) expect(result.perTable).toEqual({ vitals: 1, vitals_quality: 1, @@ -157,6 +162,7 @@ describe('pruneOldBiometrics', () => { pump_alerts: 1, thermal_state: 1, health_runs: 1, + flash_write_samples: 1, }) // Verify fresh rows remain @@ -265,6 +271,7 @@ describe('pruneOldBiometrics', () => { pump_alerts: 0, thermal_state: 0, health_runs: 0, + flash_write_samples: 0, }) }) diff --git a/src/demo/handlers/system.ts b/src/demo/handlers/system.ts index 2a24b66ce..b1c1bd7c4 100644 --- a/src/demo/handlers/system.ts +++ b/src/demo/handlers/system.ts @@ -154,6 +154,16 @@ export const system: DemoHandlers<'system'> = { getStorage: () => storage(), + getFlashHealth: () => ({ + device: 'mmcblk0', + name: 'C9A551', + capacityBytes: Math.round(29.1 * GB), + lifeTime: [{ minPercent: 0, maxPercent: 10 }, { minPercent: 10, maxPercent: 20 }], + preEol: 'normal', + sinceBoot: { bytes: Math.round(1.4 * GB), uptimeSeconds: Math.round((Date.now() - loadedAt) / 1000) + 9 * 3600 }, + perDay: { bytes: Math.round(3.1 * GB), coveredHours: 168 }, + }), + freeStorage: (input) => { const removed = reclaimable.filter(i => input.includeDbBackups || i.kind !== 'backup') reclaimable = reclaimable.filter(i => !removed.includes(i)) diff --git a/src/lib/flashHealth.ts b/src/lib/flashHealth.ts new file mode 100644 index 000000000..92d9e4315 --- /dev/null +++ b/src/lib/flashHealth.ts @@ -0,0 +1,165 @@ +import { readFile } from 'node:fs/promises' +import { asc, gte } from 'drizzle-orm' +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' +import { flashWriteSamples } from '@/src/db/biometrics-schema' +import type * as biometricsSchema from '@/src/db/biometrics-schema' + +/** + * eMMC endurance for System → Storage: the chip's own wear estimates + * (JEDEC life_time / pre_eol_info) and how much the pod writes per day. + * + * The kernel has no per-process I/O accounting on the pods, so writes are + * the whole device — firmware, sleepypod and the OS together. + */ + +type Db = BetterSQLite3Database + +export const FLASH_DEVICE = process.env.SP_FLASH_DEVICE || 'mmcblk0' +/** /sys/block//stat counts in 512-byte sectors regardless of the device's block size. */ +const SECTOR_BYTES = 512 +const DAY_MS = 86_400_000 +const RATE_WINDOW_MS = 7 * DAY_MS +/** Shortest stretch of samples worth turning into a per-day rate. */ +const MIN_COVERAGE_MS = 3_600_000 + +/** One JEDEC life_time estimate: 0x01 = 0–10% of rated endurance used … 0x0A = 90–100%, 0x0B = exceeded. */ +export interface WearRange { minPercent: number, maxPercent: number | null } + +export type PreEol = 'normal' | 'warning' | 'urgent' + +export interface FlashReading { + name: string | null + capacityBytes: number | null + /** [type A (SLC area), type B (MLC area)]; null where the chip doesn't report one. */ + lifeTime: [WearRange | null, WearRange | null] + preEol: PreEol | null + sectorsWritten: number + /** null when /proc/sys/kernel/random/boot_id is unreadable; such readings never feed the rate. */ + bootId: string | null + uptimeSeconds: number +} + +export interface FlashReport { + device: string + name: string | null + capacityBytes: number | null + lifeTime: [WearRange | null, WearRange | null] + preEol: PreEol | null + sinceBoot: { bytes: number, uptimeSeconds: number } + /** Average over the last week of samples, null until they cover an hour. */ + perDay: { bytes: number, coveredHours: number } | null +} + +export function parseWear(code: string | undefined): WearRange | null { + const n = Number.parseInt(code ?? '', 16) + if (!Number.isFinite(n) || n < 1 || n > 11) return null + if (n === 11) return { minPercent: 100, maxPercent: null } + return { minPercent: (n - 1) * 10, maxPercent: n * 10 } +} + +export function parseLifeTime(text: string): [WearRange | null, WearRange | null] { + const [a, b] = text.trim().split(/\s+/) + return [parseWear(a), parseWear(b)] +} + +export function parsePreEol(text: string): PreEol | null { + switch (Number.parseInt(text.trim(), 16)) { + case 1: return 'normal' + case 2: return 'warning' + case 3: return 'urgent' + default: return null + } +} + +/** Field 7 of /sys/block//stat: sectors written since boot. */ +export function parseSectorsWritten(stat: string): number | null { + const n = Number(stat.trim().split(/\s+/)[6]) + return Number.isFinite(n) ? n : null +} + +async function readText(path: string): Promise { + try { + return await readFile(path, 'utf-8') + } + catch { + return null + } +} + +/** Read the device's sysfs entries, or null when it isn't there (dev machines, non-eMMC storage). */ +export async function readFlash(device = FLASH_DEVICE): Promise { + const base = `/sys/block/${device}` + const [stat, name, size, lifeTime, preEol, bootId, uptime] = await Promise.all([ + readText(`${base}/stat`), + readText(`${base}/device/name`), + readText(`${base}/size`), + readText(`${base}/device/life_time`), + readText(`${base}/device/pre_eol_info`), + readText('/proc/sys/kernel/random/boot_id'), + readText('/proc/uptime'), + ]) + const sectorsWritten = stat == null ? null : parseSectorsWritten(stat) + if (sectorsWritten == null) return null + const sizeSectors = Number(size?.trim()) + return { + name: name?.trim() || null, + capacityBytes: sizeSectors > 0 ? sizeSectors * SECTOR_BYTES : null, + lifeTime: lifeTime == null ? [null, null] : parseLifeTime(lifeTime), + preEol: preEol == null ? null : parsePreEol(preEol), + sectorsWritten, + bootId: bootId?.trim() || null, + uptimeSeconds: Number(uptime?.trim().split(/\s+/)[0]) || 0, + } +} + +export interface CounterSample { at: number, bootId: string, sectorsWritten: number } + +/** + * Bytes per day across consecutive samples. Within a boot the delta is the + * counter's growth; across a reboot the counter restarted from zero, so the + * new reading is all writes since that boot. Writes between the last sample + * of a boot and its shutdown are never seen. + */ +export function writeRate(samples: CounterSample[]): { bytesPerDay: number, coveredMs: number } | null { + if (samples.length < 2) return null + let sectors = 0 + for (let i = 1; i < samples.length; i++) { + const prev = samples[i - 1] + const cur = samples[i] + // A counter that went backwards also restarted. + const sameBoot = cur.bootId === prev.bootId && cur.sectorsWritten >= prev.sectorsWritten + sectors += sameBoot ? cur.sectorsWritten - prev.sectorsWritten : cur.sectorsWritten + } + const coveredMs = samples[samples.length - 1].at - samples[0].at + if (coveredMs < MIN_COVERAGE_MS) return null + return { bytesPerDay: (sectors * SECTOR_BYTES * DAY_MS) / coveredMs, coveredMs } +} + +export function recordFlashSample(db: Db, reading: FlashReading, now: number): void { + // Without a boot id a reboot can't be told apart from growth. + if (reading.bootId == null) return + db.insert(flashWriteSamples) + .values({ timestamp: new Date(now), bootId: reading.bootId, sectorsWritten: reading.sectorsWritten }) + .run() +} + +/** Average over the last week of samples; the live reading closes the window at now. */ +export function buildFlashReport(db: Db, reading: FlashReading, now: number, device = FLASH_DEVICE): FlashReport { + const rows = db.select().from(flashWriteSamples) + .where(gte(flashWriteSamples.timestamp, new Date(now - RATE_WINDOW_MS))) + .orderBy(asc(flashWriteSamples.timestamp)) + .all() + const rate = writeRate([ + ...rows.map(r => ({ at: r.timestamp.getTime(), bootId: r.bootId, sectorsWritten: r.sectorsWritten })), + ...(reading.bootId == null ? [] : [{ at: now, bootId: reading.bootId, sectorsWritten: reading.sectorsWritten }]), + ]) + return { + device, + name: reading.name, + capacityBytes: reading.capacityBytes, + lifeTime: reading.lifeTime, + preEol: reading.preEol, + sinceBoot: { bytes: reading.sectorsWritten * SECTOR_BYTES, uptimeSeconds: Math.round(reading.uptimeSeconds) }, + perDay: rate && { bytes: Math.round(rate.bytesPerDay), coveredHours: Math.round((rate.coveredMs / 3_600_000) * 10) / 10 }, + } +} diff --git a/src/lib/flashSampler.ts b/src/lib/flashSampler.ts new file mode 100644 index 000000000..144bf41c5 --- /dev/null +++ b/src/lib/flashSampler.ts @@ -0,0 +1,44 @@ +import { biometricsDb } from '@/src/db' +import { buildFlashReport, readFlash, recordFlashSample, type FlashReport } from '@/src/lib/flashHealth' + +const SAMPLE_INTERVAL_MS = 3_600_000 + +let timer: NodeJS.Timeout | null = null + +async function sampleOnce(): Promise { + const reading = await readFlash() + if (reading) recordFlashSample(biometricsDb as never, reading, Date.now()) +} + +/** System → Storage's flash report, or null when the pod's eMMC isn't visible. */ +export async function getFlashReport(): Promise { + const reading = await readFlash() + return reading && buildFlashReport(biometricsDb as never, reading, Date.now()) +} + +/** + * Record the eMMC's sectors-written counter hourly so System → Storage can + * average writes per day across reboots. A row an hour is the whole cost. + */ +export function startFlashSampler(initialDelayMs = 60_000): void { + if (timer) return + const tick = () => { + sampleOnce().catch((err) => { + console.warn('[flash] sample failed:', err instanceof Error ? err.message : err) + }) + } + timer = setTimeout(() => { + tick() + timer = setInterval(tick, SAMPLE_INTERVAL_MS) + timer.unref() + }, initialDelayMs) + timer.unref() +} + +export function stopFlashSampler(): void { + if (timer) { + clearTimeout(timer) + clearInterval(timer) + timer = null + } +} diff --git a/src/lib/tests/flashHealth.test.ts b/src/lib/tests/flashHealth.test.ts new file mode 100644 index 000000000..3167dcf12 --- /dev/null +++ b/src/lib/tests/flashHealth.test.ts @@ -0,0 +1,193 @@ +import path from 'node:path' +import Database from 'better-sqlite3' +import { drizzle } from 'drizzle-orm/better-sqlite3' +import { migrate } from 'drizzle-orm/better-sqlite3/migrator' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import * as schema from '@/src/db/biometrics-schema' +import { flashWriteSamples } from '@/src/db/biometrics-schema' +import type { FlashReading } from '../flashHealth' + +const files = vi.hoisted(() => new Map()) +vi.mock('node:fs/promises', () => { + const readFile = vi.fn(async (p: string) => { + const v = files.get(p) + if (v == null) throw new Error('ENOENT') + return v + }) + return { readFile, default: { readFile } } +}) + +const { + buildFlashReport, parseLifeTime, parsePreEol, parseSectorsWritten, readFlash, recordFlashSample, writeRate, +} = await import('../flashHealth') + +const HOUR = 3_600_000 +const DAY = 24 * HOUR +const T0 = Date.UTC(2026, 9, 17, 0, 0, 0) +/** sectors for n MiB */ +const mib = (n: number) => (n * 1024 * 1024) / 512 + +// Captured from a Pod 5 (FORESEE eMMC) — field 7 is sectors written. +const STAT = ' 43154 9400 2711852 39177 21804 18522 3248408 1139238 0 80564 1205189 5197 166 2954288 26227 665 545\n' + +function pod(overrides: Record = {}) { + files.clear() + const base = '/sys/block/mmcblk0' + const entries: Record = { + [`${base}/stat`]: STAT, + [`${base}/size`]: '61071360\n', + [`${base}/device/name`]: 'C9A551\n', + [`${base}/device/life_time`]: '0x04 0x00\n', + [`${base}/device/pre_eol_info`]: '0x01\n', + '/proc/sys/kernel/random/boot_id': 'boot-a\n', + '/proc/uptime': '2604.89 6984.84\n', + ...overrides, + } + for (const [k, v] of Object.entries(entries)) files.set(k, v) +} + +describe('parsers', () => { + it('maps JEDEC life_time codes to 10% bands, exceeded and undefined', () => { + expect(parseLifeTime('0x04 0x00\n')).toEqual([{ minPercent: 30, maxPercent: 40 }, null]) + expect(parseLifeTime('0x01 0x0A')).toEqual([{ minPercent: 0, maxPercent: 10 }, { minPercent: 90, maxPercent: 100 }]) + expect(parseLifeTime('0x0B 0x0c')).toEqual([{ minPercent: 100, maxPercent: null }, null]) + expect(parseLifeTime('')).toEqual([null, null]) + }) + + it('maps pre_eol_info', () => { + expect(parsePreEol('0x01\n')).toBe('normal') + expect(parsePreEol('0x02')).toBe('warning') + expect(parsePreEol('0x03')).toBe('urgent') + expect(parsePreEol('0x00')).toBeNull() + }) + + it('reads sectors written from field 7 of the block stat', () => { + expect(parseSectorsWritten(STAT)).toBe(3248408) + expect(parseSectorsWritten('1 2 3')).toBeNull() + }) +}) + +describe('readFlash', () => { + it('reads the chip and counters', async () => { + pod() + expect(await readFlash()).toEqual({ + name: 'C9A551', + capacityBytes: 61071360 * 512, + lifeTime: [{ minPercent: 30, maxPercent: 40 }, null], + preEol: 'normal', + sectorsWritten: 3248408, + bootId: 'boot-a', + uptimeSeconds: 2604.89, + }) + }) + + it('is null without the block device (off-pod)', async () => { + files.clear() + expect(await readFlash()).toBeNull() + }) + + it('reports a null boot id when it is unreadable', async () => { + pod() + files.delete('/proc/sys/kernel/random/boot_id') + expect((await readFlash())?.bootId).toBeNull() + }) + + it('still reports writes when the chip hides its health registers', async () => { + pod() + files.delete('/sys/block/mmcblk0/device/life_time') + files.delete('/sys/block/mmcblk0/device/pre_eol_info') + const r = await readFlash() + expect(r?.lifeTime).toEqual([null, null]) + expect(r?.preEol).toBeNull() + expect(r?.sectorsWritten).toBe(3248408) + }) +}) + +describe('writeRate', () => { + it('needs an hour of samples', () => { + expect(writeRate([{ at: T0, bootId: 'a', sectorsWritten: 0 }])).toBeNull() + expect(writeRate([ + { at: T0, bootId: 'a', sectorsWritten: 0 }, + { at: T0 + HOUR - 1, bootId: 'a', sectorsWritten: mib(100) }, + ])).toBeNull() + }) + + it('scales counter growth within a boot to a day', () => { + const r = writeRate([ + { at: T0, bootId: 'a', sectorsWritten: mib(1000) }, + { at: T0 + 2 * HOUR, bootId: 'a', sectorsWritten: mib(1100) }, + ]) + expect(r).toEqual({ bytesPerDay: 1200 * 1024 * 1024, coveredMs: 2 * HOUR }) + }) + + it('counts a new boot from zero instead of going negative', () => { + const r = writeRate([ + { at: T0, bootId: 'a', sectorsWritten: mib(5000) }, + { at: T0 + 12 * HOUR, bootId: 'b', sectorsWritten: mib(300) }, + { at: T0 + DAY, bootId: 'b', sectorsWritten: mib(700) }, + ]) + expect(r?.bytesPerDay).toBe(700 * 1024 * 1024) + }) + + it('treats a counter that went backwards as a reboot', () => { + const r = writeRate([ + { at: T0, bootId: 'a', sectorsWritten: mib(5000) }, + { at: T0 + DAY, bootId: 'a', sectorsWritten: mib(700) }, + ]) + expect(r?.bytesPerDay).toBe(700 * 1024 * 1024) + }) +}) + +describe('buildFlashReport', () => { + let raw: Database.Database + let db: ReturnType> + + beforeEach(() => { + raw = new Database(':memory:') + db = drizzle(raw, { schema }) + migrate(db, { migrationsFolder: path.resolve(process.cwd(), 'src/db/biometrics-migrations') }) + }) + + afterEach(() => raw.close()) + + const reading = (sectorsWritten: number, bootId = 'boot-a'): FlashReading => ({ + name: 'C9A551', + capacityBytes: 29 * 1024 ** 3, + lifeTime: [{ minPercent: 30, maxPercent: 40 }, null], + preEol: 'normal', + sectorsWritten, + bootId, + uptimeSeconds: 7200, + }) + + it('has no per-day rate before samples cover an hour', () => { + const report = buildFlashReport(db, reading(mib(50)), T0) + expect(report.perDay).toBeNull() + expect(report.sinceBoot).toEqual({ bytes: 50 * 1024 * 1024, uptimeSeconds: 7200 }) + }) + + it('records nothing and leaves the live reading out of the rate without a boot id', () => { + recordFlashSample(db, reading(mib(0)), T0) + recordFlashSample(db, { ...reading(mib(100)), bootId: null }, T0 + HOUR) + expect(db.select().from(flashWriteSamples).all()).toHaveLength(1) + const report = buildFlashReport(db, { ...reading(mib(400)), bootId: null }, T0 + 4 * HOUR) + expect(report.perDay).toBeNull() + expect(report.sinceBoot.bytes).toBe(400 * 1024 * 1024) + }) + + it('averages stored samples up to the live reading', () => { + recordFlashSample(db, reading(mib(0)), T0) + recordFlashSample(db, reading(mib(100)), T0 + HOUR) + const report = buildFlashReport(db, reading(mib(400)), T0 + 4 * HOUR) + expect(report.perDay).toEqual({ bytes: 2400 * 1024 * 1024, coveredHours: 4 }) + expect(report.device).toBe('mmcblk0') + }) + + it('ignores samples older than a week', () => { + recordFlashSample(db, reading(0, 'old'), T0 - 8 * DAY) + recordFlashSample(db, reading(mib(100)), T0) + const report = buildFlashReport(db, reading(mib(200)), T0 + 2 * HOUR) + expect(report.perDay).toEqual({ bytes: 1200 * 1024 * 1024, coveredHours: 2 }) + expect(db.select().from(flashWriteSamples).all()).toHaveLength(2) + }) +}) diff --git a/src/server/instrumentation-node.ts b/src/server/instrumentation-node.ts index 9bf4e1bf3..de098df71 100644 --- a/src/server/instrumentation-node.ts +++ b/src/server/instrumentation-node.ts @@ -23,6 +23,7 @@ import { closeDatabase, closeBiometricsDatabase } from '@/src/db' import { startBiometricsRetention, stopBiometricsRetention } from '@/src/db/retention' import { startAutomationRunsRetention, stopAutomationRunsRetention } from '@/src/db/automationRunsRetention' import { startHealthSampler, stopHealthSampler } from '@/src/lib/healthSampler' +import { startFlashSampler, stopFlashSampler } from '@/src/lib/flashSampler' import { stopBacktestHost } from '@/src/biometrics/backtest/host' import { stopBiometricCoordinator } from '@/src/biometrics/backtest/coordinator-service' import { stopBiometricRuntime } from '@/src/biometrics/profiles/runtime-host' @@ -149,6 +150,7 @@ async function gracefulShutdown(signal: string): Promise { stopBiometricsRetention() stopAutomationRunsRetention() stopHealthSampler() + stopFlashSampler() stopBiometricRuntime() } catch (error) { @@ -358,6 +360,8 @@ async function initializeBackgroundServices(): Promise { // Started before the scheduler so a scheduler that fails to load still // leaves a history of the failure. startHealthSampler() + // Hourly eMMC write counter for System → Storage's writes-per-day. + startFlashSampler() const schedulerStartedAt = performance.now() console.log('Initializing job scheduler...') diff --git a/src/server/routers/system.ts b/src/server/routers/system.ts index 9e5b0f5c6..0202b2f5a 100644 --- a/src/server/routers/system.ts +++ b/src/server/routers/system.ts @@ -7,6 +7,7 @@ import { accessSync, constants } from 'node:fs' import { readdir, readFile, stat } from 'node:fs/promises' import { promisify } from 'node:util' import { getStorageReport, runStorageCleanup } from '@/src/lib/podStorage' +import { getFlashReport } from '@/src/lib/flashSampler' import { classifyFirmware, expectedTransport, @@ -259,6 +260,8 @@ async function blockWan(): Promise { * - `setInternetAccess` — toggle WAN block on/off * - `triggerUpdate` — kick off sp-update (or a deploy-driven install) */ +const wearRange = z.object({ minPercent: z.number(), maxPercent: z.number().nullable() }).nullable() + export const systemRouter = router({ /** * Returns whether WAN access is currently blocked by iptables. @@ -651,6 +654,24 @@ export const systemRouter = router({ })) .query(() => getStorageReport()), + /** + * eMMC endurance (System → Storage): the chip's own wear estimates and + * average writes per day for the whole device. Null off-pod. + */ + getFlashHealth: publicProcedure + .meta({ openapi: { method: 'GET', path: '/system/flash', protect: false, tags: ['System'] } }) + .input(z.object({})) + .output(z.object({ + device: z.string(), + name: z.string().nullable(), + capacityBytes: z.number().nullable(), + lifeTime: z.tuple([wearRange, wearRange]), + preEol: z.enum(['normal', 'warning', 'urgent']).nullable(), + sinceBoot: z.object({ bytes: z.number(), uptimeSeconds: z.number() }), + perDay: z.object({ bytes: z.number(), coveredHours: z.number() }).nullable(), + }).nullable()) + .query(() => getFlashReport()), + /** * Delete sleepypod's own leftovers on /persistent (never the live install, * databases, raw archive, swap or HomeKit data). Old database backups only