diff --git a/src/components/Schedule/CurveEditor.tsx b/src/components/Schedule/CurveEditor.tsx index 0674fe11..2bc84d3a 100644 --- a/src/components/Schedule/CurveEditor.tsx +++ b/src/components/Schedule/CurveEditor.tsx @@ -117,7 +117,7 @@ export function CurveEditor({ side }: CurveEditorProps) { await Promise.all(deletePromises) // Create new temperature schedule entries - const createPromises = Object.entries(scheduleTemps).map(([time, temperature]) => + const createPromises: Promise[] = Object.entries(scheduleTemps).map(([time, temperature]) => createTempSchedule.mutateAsync({ side, dayOfWeek: day, @@ -136,7 +136,7 @@ export function CurveEditor({ side }: CurveEditorProps) { offTime: wakeTime, onTemperature: Math.max(55, Math.min(110, 80 + curvePoints[0].tempOffset)), enabled: true, - }) as Promise as Promise<{ id: number }>, + }), ) await Promise.all(createPromises) diff --git a/src/components/Sensors/BedTempMatrix.tsx b/src/components/Sensors/BedTempMatrix.tsx index 2944cabd..013baa17 100644 --- a/src/components/Sensors/BedTempMatrix.tsx +++ b/src/components/Sensors/BedTempMatrix.tsx @@ -165,7 +165,7 @@ export function BedTempMatrix() { // Stored data already converted to user's unit by the tRPC endpoint return { source: 'stored' as const, - timestamp: stored.timestamp ? Math.floor(new Date(stored.timestamp as string).getTime() / 1000) : undefined, + timestamp: stored.timestamp ? Math.floor(stored.timestamp.getTime() / 1000) : undefined, ambientTemp: formatConverted(stored.ambientTemp), mcuTemp: formatConverted(stored.mcuTemp), humidity: stored.humidity != null ? `${Math.round(stored.humidity)}%` : undefined, diff --git a/src/components/status/WaterLevelCard.tsx b/src/components/status/WaterLevelCard.tsx index 6971ec60..fa5eba80 100644 --- a/src/components/status/WaterLevelCard.tsx +++ b/src/components/status/WaterLevelCard.tsx @@ -4,9 +4,9 @@ import { useState, useCallback, useMemo } from 'react' import { trpc } from '@/src/utils/trpc' import { Droplets, Play, X, AlertTriangle, TrendingDown, TrendingUp, Minus, Loader2 } from 'lucide-react' -function trendIcon(direction: string) { - if (direction === 'falling') return - if (direction === 'rising') return +function trendIcon(trend: string) { + if (trend === 'declining') return + if (trend === 'rising') return return } @@ -84,7 +84,7 @@ export function WaterLevelCard() { Water Level - {trend && trendIcon(trend.direction)} + {trend && trendIcon(trend.trend)} {/* Current level */} @@ -99,9 +99,7 @@ export function WaterLevelCard() {

- {typeof latest.levelPercent === 'number' - ? `${Math.round(latest.levelPercent)}%` - : '--'} + {latest.level === 'ok' ? 'OK' : 'Low'}

{new Date(latest.timestamp).toLocaleTimeString([], { @@ -112,14 +110,16 @@ export function WaterLevelCard() {

{trend && (
- {trend.direction !== 'stable' && ( + {trend.trend === 'stable' && Stable} + {trend.trend === 'declining' && ( - {(trend.changePercent ?? 0) > 0 ? '+' : ''} - {(trend.changePercent ?? 0).toFixed(1)} - % / 24h + Declining ( + {trend.lowPercent} + % low) )} - {trend.direction === 'stable' && Stable} + {trend.trend === 'rising' && Rising} + {trend.trend === 'unknown' && Insufficient data}
)}
@@ -134,7 +134,7 @@ export function WaterLevelCard() { {/* Active alerts */} {activeAlerts.length > 0 && (
- {activeAlerts.map((alert: { id: number, alertType: string, message: string }) => ( + {activeAlerts.map(alert => (
- if (direction === 'rising') return +function trendIcon(trend: string) { + if (trend === 'declining') return + if (trend === 'rising') return return } @@ -99,9 +99,7 @@ export function WaterModal({ open, onClose }: { open: boolean, onClose: () => vo

- {typeof latest.levelPercent === 'number' - ? `${Math.round(latest.levelPercent)}%` - : '--'} + {latest.level === 'ok' ? 'OK' : 'Low'}

{new Date(latest.timestamp).toLocaleTimeString([], { @@ -112,11 +110,12 @@ export function WaterModal({ open, onClose }: { open: boolean, onClose: () => vo

{trend && (
- {trendIcon(trend.direction)} + {trendIcon(trend.trend)} - {trend.direction !== 'stable' - ? `${(trend.changePercent ?? 0) > 0 ? '+' : ''}${(trend.changePercent ?? 0).toFixed(1)}% / 24h` - : 'Stable'} + {trend.trend === 'stable' && 'Stable'} + {trend.trend === 'declining' && `Declining (${trend.lowPercent}% low)`} + {trend.trend === 'rising' && 'Rising'} + {trend.trend === 'unknown' && 'Insufficient data'}
)} @@ -132,7 +131,7 @@ export function WaterModal({ open, onClose }: { open: boolean, onClose: () => vo {/* Active alerts */} {activeAlerts.length > 0 && (
- {activeAlerts.map((alert: { id: number, alertType: string, message: string }) => ( + {activeAlerts.map(alert => (
{alert.message} diff --git a/src/hooks/useSchedules.ts b/src/hooks/useSchedules.ts index a66d0dca..5ddba085 100644 --- a/src/hooks/useSchedules.ts +++ b/src/hooks/useSchedules.ts @@ -12,8 +12,8 @@ export interface TemperatureSchedule { time: string temperature: number enabled: boolean - createdAt: string | Date - updatedAt: string | Date + createdAt: Date + updatedAt: Date } export interface PowerSchedule { @@ -24,6 +24,8 @@ export interface PowerSchedule { offTime: string onTemperature: number enabled: boolean + createdAt: Date + updatedAt: Date } export interface AlarmSchedule { @@ -31,11 +33,13 @@ export interface AlarmSchedule { side: 'left' | 'right' dayOfWeek: DayOfWeek time: string - vibrationPattern: string + vibrationPattern: 'double' | 'rise' vibrationIntensity: number duration: number alarmTemperature: number enabled: boolean + createdAt: Date + updatedAt: Date } export interface ScheduleData { diff --git a/src/server/routers/biometrics.ts b/src/server/routers/biometrics.ts index a293bf9d..264ec531 100644 --- a/src/server/routers/biometrics.ts +++ b/src/server/routers/biometrics.ts @@ -55,7 +55,17 @@ export const biometricsRouter = router({ */ getSleepRecords: publicProcedure .meta({ openapi: { method: 'GET', path: '/biometrics/sleep-records', protect: false, tags: ['Biometrics'] } }) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + side: sideSchema, + enteredBedAt: z.date(), + leftBedAt: z.date(), + sleepDurationSeconds: z.number(), + timesExitedBed: z.number(), + presentIntervals: z.unknown(), + notPresentIntervals: z.unknown(), + createdAt: z.date(), + }))) .input( z .object({ @@ -133,7 +143,14 @@ export const biometricsRouter = router({ */ getVitals: publicProcedure .meta({ openapi: { method: 'GET', path: '/biometrics/vitals', protect: false, tags: ['Biometrics'] } }) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + side: sideSchema, + timestamp: z.date(), + heartRate: z.number().nullable(), + hrv: z.number().nullable(), + breathingRate: z.number().nullable(), + }))) .input( z .object({ @@ -210,7 +227,12 @@ export const biometricsRouter = router({ */ getMovement: publicProcedure .meta({ openapi: { method: 'GET', path: '/biometrics/movement', protect: false, tags: ['Biometrics'] } }) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + side: sideSchema, + timestamp: z.date(), + totalMovement: z.number(), + }))) .input( z .object({ @@ -285,7 +307,17 @@ export const biometricsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + id: z.number(), + side: sideSchema, + enteredBedAt: z.date(), + leftBedAt: z.date(), + sleepDurationSeconds: z.number(), + timesExitedBed: z.number(), + presentIntervals: z.unknown(), + notPresentIntervals: z.unknown(), + createdAt: z.date(), + }).nullable()) .query(async ({ input }) => { try { const [record] = await biometricsDb @@ -330,7 +362,14 @@ export const biometricsRouter = router({ */ getVitalsSummary: publicProcedure .meta({ openapi: { method: 'GET', path: '/biometrics/vitals/summary', protect: false, tags: ['Biometrics'] } }) - .output(z.any()) + .output(z.object({ + avgHeartRate: z.number().nullable(), + minHeartRate: z.number().nullable(), + maxHeartRate: z.number().nullable(), + avgHRV: z.number().nullable(), + avgBreathingRate: z.number().nullable(), + recordCount: z.number(), + }).nullable()) .input( z .object({ @@ -530,7 +569,17 @@ export const biometricsRouter = router({ timesExitedBed: z.number().int().min(0).optional(), }).strict() ) - .output(z.any()) + .output(z.object({ + id: z.number(), + side: sideSchema, + enteredBedAt: z.date(), + leftBedAt: z.date(), + sleepDurationSeconds: z.number(), + timesExitedBed: z.number(), + presentIntervals: z.unknown(), + notPresentIntervals: z.unknown(), + createdAt: z.date(), + })) .mutation(async ({ input }) => { const { id, ...updates } = input diff --git a/src/server/routers/calibration.ts b/src/server/routers/calibration.ts index 26215eaa..253c6de8 100644 --- a/src/server/routers/calibration.ts +++ b/src/server/routers/calibration.ts @@ -80,7 +80,21 @@ export const calibrationRouter = router({ sensorType: sensorTypeSchema.optional(), limit: z.number().int().min(1).max(50).default(10), }).strict()) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + side: z.enum(['left', 'right']), + sensorType: z.enum(['piezo', 'capacitance', 'temperature']), + status: z.enum(['completed', 'failed']), + parameters: z.unknown(), + qualityScore: z.number().nullable(), + sourceWindowStart: z.number().nullable(), + sourceWindowEnd: z.number().nullable(), + samplesUsed: z.number().nullable(), + errorMessage: z.string().nullable(), + durationMs: z.number().nullable(), + triggeredBy: z.enum(['daily', 'manual', 'startup']), + createdAt: z.date(), + }))) .query(async ({ input }) => { const conditions = [eq(calibrationRuns.side, input.side)] if (input.sensorType) { @@ -194,7 +208,16 @@ export const calibrationRouter = router({ endDate: z.date().optional(), limit: z.number().int().min(1).max(500).default(100), }).strict()) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + vitalsId: z.number(), + side: z.enum(['left', 'right']), + timestamp: z.date(), + qualityScore: z.number(), + flags: z.unknown(), + hrRaw: z.number().nullable(), + createdAt: z.date(), + }))) .query(async ({ input }) => { const conditions = [eq(vitalsQuality.side, input.side)] if (input.startDate) { diff --git a/src/server/routers/device.ts b/src/server/routers/device.ts index 73075f76..50a67a63 100644 --- a/src/server/routers/device.ts +++ b/src/server/routers/device.ts @@ -87,7 +87,38 @@ export const deviceRouter = router({ getStatus: publicProcedure .meta({ openapi: { method: 'GET', path: '/device/status', protect: false, tags: ['Device'] } }) .input(z.object({ unit: z.enum(['F', 'C']).default('F') }).strict()) - .output(z.any()) + .output(z.object({ + leftSide: z.object({ + currentTemperature: z.number(), + targetTemperature: z.number(), + currentLevel: z.number(), + targetLevel: z.number(), + heatingDuration: z.number(), + isAlarmVibrating: z.boolean().optional(), + }), + rightSide: z.object({ + currentTemperature: z.number(), + targetTemperature: z.number(), + currentLevel: z.number(), + targetLevel: z.number(), + heatingDuration: z.number(), + isAlarmVibrating: z.boolean().optional(), + }), + waterLevel: z.enum(['low', 'ok']), + isPriming: z.boolean(), + podVersion: z.enum(['H00', 'I00', 'J00']), + sensorLabel: z.string(), + gestures: z.object({ + doubleTap: z.object({ l: z.number(), r: z.number() }).optional(), + tripleTap: z.object({ l: z.number(), r: z.number() }).optional(), + quadTap: z.object({ l: z.number(), r: z.number() }).optional(), + }).optional(), + primeCompletedNotification: z.object({ timestamp: z.number() }).optional(), + snooze: z.object({ + left: z.object({ active: z.boolean(), snoozeUntil: z.number().nullable() }), + right: z.object({ active: z.boolean(), snoozeUntil: z.number().nullable() }), + }), + })) .query(async ({ input }) => { return withHardwareClient(async (client) => { const status = await client.getDeviceStatus() @@ -557,7 +588,12 @@ export const deviceRouter = router({ command: z.enum(['SET_TEMP', 'SET_ALARM', 'ALARM_LEFT', 'ALARM_RIGHT', 'SET_SETTINGS', 'PRIME', 'DEVICE_STATUS', 'ALARM_CLEAR']), args: z.string().optional(), }).strict()) - .output(z.any()) + .output(z.object({ + command: z.string(), + args: z.string().nullable(), + response: z.unknown(), + disclaimer: z.string(), + })) .mutation(async ({ input }) => { const hwCommand = COMMAND_MAP[input.command] diff --git a/src/server/routers/environment.ts b/src/server/routers/environment.ts index b1b7c879..da028baa 100644 --- a/src/server/routers/environment.ts +++ b/src/server/routers/environment.ts @@ -28,7 +28,19 @@ export const environmentRouter = router({ getBedTemp: publicProcedure .meta({ openapi: { method: 'GET', path: '/environment/bed-temp', protect: false, tags: ['Environment'] } }) .input(dateRangeInput) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + timestamp: z.date(), + ambientTemp: z.number().nullable(), + mcuTemp: z.number().nullable(), + humidity: z.number().nullable(), + leftOuterTemp: z.number().nullable(), + leftCenterTemp: z.number().nullable(), + leftInnerTemp: z.number().nullable(), + rightOuterTemp: z.number().nullable(), + rightCenterTemp: z.number().nullable(), + rightInnerTemp: z.number().nullable(), + }))) .query(async ({ input }) => { try { if (input.startDate && input.endDate && !validateDateRange(input.startDate, input.endDate)) { @@ -72,7 +84,14 @@ export const environmentRouter = router({ getFreezerTemp: publicProcedure .meta({ openapi: { method: 'GET', path: '/environment/freezer-temp', protect: false, tags: ['Environment'] } }) .input(dateRangeInput) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + timestamp: z.date(), + ambientTemp: z.number().nullable(), + heatsinkTemp: z.number().nullable(), + leftWaterTemp: z.number().nullable(), + rightWaterTemp: z.number().nullable(), + }))) .query(async ({ input }) => { try { if (input.startDate && input.endDate && !validateDateRange(input.startDate, input.endDate)) { @@ -111,7 +130,19 @@ export const environmentRouter = router({ getLatestBedTemp: publicProcedure .meta({ openapi: { method: 'GET', path: '/environment/bed-temp/latest', protect: false, tags: ['Environment'] } }) .input(z.object({ unit: z.enum(['F', 'C']).default('F') }).strict()) - .output(z.any()) + .output(z.object({ + id: z.number(), + timestamp: z.date(), + ambientTemp: z.number().nullable(), + mcuTemp: z.number().nullable(), + humidity: z.number().nullable(), + leftOuterTemp: z.number().nullable(), + leftCenterTemp: z.number().nullable(), + leftInnerTemp: z.number().nullable(), + rightOuterTemp: z.number().nullable(), + rightCenterTemp: z.number().nullable(), + rightInnerTemp: z.number().nullable(), + }).nullable()) .query(async ({ input }) => { try { const [row] = await biometricsDb @@ -148,7 +179,14 @@ export const environmentRouter = router({ getLatestFreezerTemp: publicProcedure .meta({ openapi: { method: 'GET', path: '/environment/freezer-temp/latest', protect: false, tags: ['Environment'] } }) .input(z.object({ unit: z.enum(['F', 'C']).default('F') }).strict()) - .output(z.any()) + .output(z.object({ + id: z.number(), + timestamp: z.date(), + ambientTemp: z.number().nullable(), + heatsinkTemp: z.number().nullable(), + leftWaterTemp: z.number().nullable(), + rightWaterTemp: z.number().nullable(), + }).nullable()) .query(async ({ input }) => { try { const [row] = await biometricsDb @@ -179,7 +217,24 @@ export const environmentRouter = router({ getSummary: publicProcedure .meta({ openapi: { method: 'GET', path: '/environment/summary', protect: false, tags: ['Environment'] } }) - .output(z.any()) + .output(z.object({ + bedTemp: z.object({ + avgAmbientTemp: z.number().nullable(), + minAmbientTemp: z.number().nullable(), + maxAmbientTemp: z.number().nullable(), + avgHumidity: z.number().nullable(), + avgLeftCenterTemp: z.number().nullable(), + avgRightCenterTemp: z.number().nullable(), + recordCount: z.number(), + }).nullable(), + freezerTemp: z.object({ + avgAmbientTemp: z.number().nullable(), + avgHeatsinkTemp: z.number().nullable(), + avgLeftWaterTemp: z.number().nullable(), + avgRightWaterTemp: z.number().nullable(), + recordCount: z.number(), + }).nullable(), + })) .input( z.object({ startDate: z.date(), @@ -272,7 +327,11 @@ export const environmentRouter = router({ endDate: z.date().optional(), limit: z.number().int().min(1).max(1440).default(1440), }).strict()) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + timestamp: z.date(), + lux: z.number().nullable(), + }))) .query(async ({ input }) => { try { if (input.startDate && input.endDate && !validateDateRange(input.startDate, input.endDate)) { @@ -302,7 +361,11 @@ export const environmentRouter = router({ getLatestAmbientLight: publicProcedure .meta({ openapi: { method: 'GET', path: '/environment/ambient-light/latest', protect: false, tags: ['Environment'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + id: z.number(), + timestamp: z.date(), + lux: z.number().nullable(), + }).nullable()) .query(async () => { try { const [row] = await biometricsDb @@ -327,7 +390,12 @@ export const environmentRouter = router({ startDate: z.date(), endDate: z.date(), }).strict()) - .output(z.any()) + .output(z.object({ + avgLux: z.number().nullable(), + minLux: z.number().nullable(), + maxLux: z.number().nullable(), + recordCount: z.number(), + }).nullable()) .query(async ({ input }) => { try { if (!validateDateRange(input.startDate, input.endDate)) { diff --git a/src/server/routers/health.ts b/src/server/routers/health.ts index 067284ab..4b43a38b 100644 --- a/src/server/routers/health.ts +++ b/src/server/routers/health.ts @@ -33,7 +33,25 @@ export const healthRouter = router({ scheduler: publicProcedure .meta({ openapi: { method: 'GET', path: '/health/scheduler', protect: false, tags: ['Health'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + enabled: z.boolean(), + jobCounts: z.object({ + temperature: z.number(), + powerOn: z.number(), + powerOff: z.number(), + alarm: z.number(), + prime: z.number(), + reboot: z.number(), + total: z.number(), + }), + upcomingJobs: z.array(z.object({ + id: z.string(), + type: z.string(), + side: z.string().optional(), + nextRun: z.string().nullable(), + })), + healthy: z.boolean(), + })) .query(async () => { try { const jobManager = await getJobManager() @@ -114,7 +132,28 @@ export const healthRouter = router({ system: publicProcedure .meta({ openapi: { method: 'GET', path: '/health/system', protect: false, tags: ['Health'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + status: z.enum(['ok', 'degraded']), + timestamp: z.string(), + database: z.object({ + status: z.enum(['ok', 'degraded']), + latencyMs: z.number(), + error: z.string().optional(), + }), + scheduler: z.object({ + enabled: z.boolean(), + jobCount: z.number(), + drift: z.object({ + dbScheduleCount: z.number(), + schedulerJobCount: z.number(), + drifted: z.boolean(), + }).optional(), + }), + iptables: z.object({ + ok: z.boolean(), + missing: z.array(z.string()), + }), + })) .query(async () => { let overallStatus: 'ok' | 'degraded' = 'ok' @@ -244,7 +283,11 @@ export const healthRouter = router({ dacMonitor: publicProcedure .meta({ openapi: { method: 'GET', path: '/health/dac-monitor', protect: false, tags: ['Health'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + status: z.string(), + podVersion: z.string().nullable(), + gesturesSupported: z.boolean(), + })) .query(() => { const monitor = getDacMonitorIfRunning() if (!monitor) { @@ -264,7 +307,12 @@ export const healthRouter = router({ hardware: publicProcedure .meta({ openapi: { method: 'GET', path: '/health/hardware', protect: false, tags: ['Health'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + status: z.enum(['ok', 'degraded']), + socketPath: z.string(), + latencyMs: z.number(), + error: z.string().optional(), + })) .query(async () => { let status: 'ok' | 'degraded' = 'ok' let latencyMs = 0 diff --git a/src/server/routers/raw.ts b/src/server/routers/raw.ts index dafda55b..e83f761a 100644 --- a/src/server/routers/raw.ts +++ b/src/server/routers/raw.ts @@ -41,7 +41,11 @@ export const rawRouter = router({ files: publicProcedure .meta({ openapi: { method: 'GET', path: '/raw/files', protect: false, tags: ['Raw'] } }) .input(z.object({})) - .output(z.any()) + .output(z.array(z.object({ + name: z.string(), + sizeBytes: z.number(), + modifiedAt: z.string(), + }))) .query(async () => { try { return await listRawFiles() @@ -103,7 +107,13 @@ export const rawRouter = router({ diskUsage: publicProcedure .meta({ openapi: { method: 'GET', path: '/raw/disk-usage', protect: false, tags: ['Raw'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + totalBytes: z.number(), + usedBytes: z.number(), + availableBytes: z.number(), + rawFileCount: z.number(), + rawBytes: z.number(), + })) .query(async () => { try { const files = await listRawFiles() diff --git a/src/server/routers/runOnce.ts b/src/server/routers/runOnce.ts index 762a2951..fe1c24af 100644 --- a/src/server/routers/runOnce.ts +++ b/src/server/routers/runOnce.ts @@ -125,7 +125,15 @@ export const runOnceRouter = router({ getActive: publicProcedure .meta({ openapi: { method: 'GET', path: '/run-once/active', protect: false, tags: ['RunOnce'] } }) .input(z.object({ side: sideSchema }).strict()) - .output(z.any()) + .output(z.object({ + id: z.number(), + side: z.enum(['left', 'right']), + setPoints: z.unknown(), + wakeTime: z.string(), + startedAt: z.number(), + expiresAt: z.number(), + status: z.enum(['active', 'completed', 'cancelled']), + }).nullable()) .query(async ({ input }) => { const [session] = await db .select() diff --git a/src/server/routers/scheduleGroups.ts b/src/server/routers/scheduleGroups.ts index cc42e390..83ce81d6 100644 --- a/src/server/routers/scheduleGroups.ts +++ b/src/server/routers/scheduleGroups.ts @@ -48,7 +48,14 @@ export const scheduleGroupsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + side: sideSchema, + name: z.string(), + days: z.array(z.string()), + createdAt: z.date(), + updatedAt: z.date(), + }))) .query(async ({ input }) => { try { const groups = db @@ -82,7 +89,14 @@ export const scheduleGroupsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + id: z.number(), + side: sideSchema, + name: z.string(), + days: z.array(z.string()), + createdAt: z.date(), + updatedAt: z.date(), + })) .mutation(async ({ input }) => { try { const conflict = findConflictingGroup(input.side, input.days) @@ -140,7 +154,14 @@ export const scheduleGroupsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + id: z.number(), + side: sideSchema, + name: z.string(), + days: z.array(z.string()), + createdAt: z.date(), + updatedAt: z.date(), + })) .mutation(async ({ input }) => { try { // Fetch existing group first @@ -374,7 +395,14 @@ export const scheduleGroupsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + id: z.number(), + side: sideSchema, + name: z.string(), + days: z.array(z.string()), + createdAt: z.date(), + updatedAt: z.date(), + }).nullable()) .query(async ({ input }) => { try { const groups = db diff --git a/src/server/routers/schedules.ts b/src/server/routers/schedules.ts index f332d5c5..7cefb425 100644 --- a/src/server/routers/schedules.ts +++ b/src/server/routers/schedules.ts @@ -18,6 +18,49 @@ import { vibrationPatternSchema, alarmDurationSchema, } from '@/src/server/validation-schemas' + +const temperatureScheduleOutput = z.object({ + id: z.number(), + side: sideSchema, + dayOfWeek: dayOfWeekSchema, + time: z.string(), + temperature: z.number(), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +}) + +const powerScheduleOutput = z.object({ + id: z.number(), + side: sideSchema, + dayOfWeek: dayOfWeekSchema, + onTime: z.string(), + offTime: z.string(), + onTemperature: z.number(), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +}) + +const alarmScheduleOutput = z.object({ + id: z.number(), + side: sideSchema, + dayOfWeek: dayOfWeekSchema, + time: z.string(), + vibrationIntensity: z.number(), + vibrationPattern: vibrationPatternSchema, + duration: z.number(), + alarmTemperature: z.number(), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +}) + +const schedulesCollectionOutput = z.object({ + temperature: z.array(temperatureScheduleOutput), + power: z.array(powerScheduleOutput), + alarm: z.array(alarmScheduleOutput), +}) import { getJobManager } from '@/src/scheduler' import { toC } from '@/src/lib/tempUtils' @@ -61,7 +104,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(schedulesCollectionOutput) .query(async ({ input }) => { try { const temperatureSchedulesList = db @@ -111,7 +154,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(temperatureScheduleOutput) .mutation(async ({ input }) => { try { const created = db.transaction((tx) => { @@ -161,7 +204,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(temperatureScheduleOutput) .mutation(async ({ input }) => { try { const { id, ...updates } = input @@ -269,7 +312,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(powerScheduleOutput) .mutation(async ({ input }) => { try { const created = db.transaction((tx) => { @@ -320,7 +363,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(powerScheduleOutput) .mutation(async ({ input }) => { try { const { id, ...updates } = input @@ -430,7 +473,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(alarmScheduleOutput) .mutation(async ({ input }) => { try { const created = db.transaction((tx) => { @@ -483,7 +526,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(alarmScheduleOutput) .mutation(async ({ input }) => { try { const { id, ...updates } = input @@ -718,7 +761,7 @@ export const schedulesRouter = router({ }) .strict() ) - .output(z.any()) + .output(schedulesCollectionOutput) .query(async ({ input }) => { try { const temperatureSchedulesList = db diff --git a/src/server/routers/settings.ts b/src/server/routers/settings.ts index 3717d93d..324048a5 100644 --- a/src/server/routers/settings.ts +++ b/src/server/routers/settings.ts @@ -50,7 +50,78 @@ export const settingsRouter = router({ getAll: publicProcedure .meta({ openapi: { method: 'GET', path: '/settings', protect: false, tags: ['Settings'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + device: z.object({ + id: z.number(), + timezone: z.string(), + temperatureUnit: temperatureUnitSchema, + rebootDaily: z.boolean(), + rebootTime: z.string().nullable(), + primePodDaily: z.boolean(), + primePodTime: z.string().nullable(), + ledNightModeEnabled: z.boolean(), + ledDayBrightness: z.number(), + ledNightBrightness: z.number(), + ledNightStartTime: z.string().nullable(), + ledNightEndTime: z.string().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + }), + sides: z.object({ + left: z.object({ + side: sideSchema, + name: z.string(), + awayMode: z.boolean(), + alwaysOn: z.boolean(), + autoOffEnabled: z.boolean(), + autoOffMinutes: z.number(), + awayStart: z.string().nullable().optional(), + awayReturn: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + }), + right: z.object({ + side: sideSchema, + name: z.string(), + awayMode: z.boolean(), + alwaysOn: z.boolean(), + autoOffEnabled: z.boolean(), + autoOffMinutes: z.number(), + awayStart: z.string().nullable().optional(), + awayReturn: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + }), + }), + gestures: z.object({ + left: z.array(z.object({ + id: z.number(), + side: sideSchema, + tapType: tapTypeSchema, + actionType: z.enum(['temperature', 'alarm']), + temperatureChange: z.enum(['increment', 'decrement']).nullable(), + temperatureAmount: z.number().nullable(), + alarmBehavior: z.enum(['snooze', 'dismiss']).nullable(), + alarmSnoozeDuration: z.number().nullable(), + alarmInactiveBehavior: z.enum(['power', 'none']).nullable(), + createdAt: z.date(), + updatedAt: z.date(), + })), + right: z.array(z.object({ + id: z.number(), + side: sideSchema, + tapType: tapTypeSchema, + actionType: z.enum(['temperature', 'alarm']), + temperatureChange: z.enum(['increment', 'decrement']).nullable(), + temperatureAmount: z.number().nullable(), + alarmBehavior: z.enum(['snooze', 'dismiss']).nullable(), + alarmSnoozeDuration: z.number().nullable(), + alarmInactiveBehavior: z.enum(['power', 'none']).nullable(), + createdAt: z.date(), + updatedAt: z.date(), + })), + }), + })) .query(async () => { try { const [device] = await db.select().from(deviceSettings).limit(1) @@ -115,7 +186,22 @@ export const settingsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + id: z.number(), + timezone: z.string(), + temperatureUnit: temperatureUnitSchema, + rebootDaily: z.boolean(), + rebootTime: z.string().nullable(), + primePodDaily: z.boolean(), + primePodTime: z.string().nullable(), + ledNightModeEnabled: z.boolean(), + ledDayBrightness: z.number(), + ledNightBrightness: z.number(), + ledNightStartTime: z.string().nullable(), + ledNightEndTime: z.string().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + })) .mutation(async ({ input }) => { try { const updated = db.transaction((tx) => { @@ -211,7 +297,18 @@ export const settingsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + side: sideSchema, + name: z.string(), + awayMode: z.boolean(), + alwaysOn: z.boolean(), + autoOffEnabled: z.boolean(), + autoOffMinutes: z.number(), + awayStart: z.string().nullable(), + awayReturn: z.string().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + })) .mutation(async ({ input }) => { try { const { side, ...updates } = input @@ -324,7 +421,18 @@ export const settingsRouter = router({ }) .strict() ) - .output(z.any()) + .output(z.object({ + side: sideSchema, + name: z.string(), + awayMode: z.boolean(), + alwaysOn: z.boolean(), + autoOffEnabled: z.boolean(), + autoOffMinutes: z.number(), + awayStart: z.string().nullable(), + awayReturn: z.string().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + })) .mutation(async ({ input }) => { try { const updated = db.transaction((tx) => { diff --git a/src/server/routers/tests/scheduleGroups.test.ts b/src/server/routers/tests/scheduleGroups.test.ts index d2a5a1cf..521c8ee6 100644 --- a/src/server/routers/tests/scheduleGroups.test.ts +++ b/src/server/routers/tests/scheduleGroups.test.ts @@ -184,7 +184,7 @@ describe('scheduleGroups', () => { }) const result = await caller.getByDay({ side: 'left', dayOfWeek: 'tuesday' }) - expect(result).not.toBeNull() + if (result === null) throw new Error('Expected non-null result') expect(result.name).toBe('Weekdays') expect(result.days).toContain('tuesday') }) diff --git a/src/server/routers/tests/schedules.test.ts b/src/server/routers/tests/schedules.test.ts index b17f3958..9e59ce6b 100644 --- a/src/server/routers/tests/schedules.test.ts +++ b/src/server/routers/tests/schedules.test.ts @@ -161,7 +161,8 @@ describe('schedules.batchUpdate', () => { const after = await caller.getAll({ side: 'left' }) expect(after.temperature).toHaveLength(2) - const tuesday = after.temperature.find((t: any) => t.dayOfWeek === 'tuesday') + const tuesday = after.temperature.find(t => t.dayOfWeek === 'tuesday') + if (!tuesday) throw new Error('Expected to find tuesday schedule') expect(tuesday.time).toBe('22:00') expect(tuesday.temperature).toBe(68) }) diff --git a/src/server/routers/waterLevel.ts b/src/server/routers/waterLevel.ts index 2a4f2f88..b40948f9 100644 --- a/src/server/routers/waterLevel.ts +++ b/src/server/routers/waterLevel.ts @@ -17,7 +17,11 @@ export const waterLevelRouter = router({ endDate: z.date().optional(), limit: z.number().int().min(1).max(10000).default(1440), }).strict()) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + timestamp: z.date(), + level: z.enum(['low', 'ok']), + }))) .query(async ({ input }) => { try { if (input.startDate && input.endDate && !validateDateRange(input.startDate, input.endDate)) { @@ -50,7 +54,11 @@ export const waterLevelRouter = router({ getLatest: publicProcedure .meta({ openapi: { method: 'GET', path: '/water-level/latest', protect: false, tags: ['Water Level'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + id: z.number(), + timestamp: z.date(), + level: z.enum(['low', 'ok']), + }).nullable()) .query(async () => { try { const [row] = await biometricsDb @@ -78,7 +86,12 @@ export const waterLevelRouter = router({ .input(z.object({ hours: z.number().int().min(1).max(168).default(24), }).strict()) - .output(z.any()) + .output(z.object({ + totalReadings: z.number(), + okPercent: z.number(), + lowPercent: z.number(), + trend: z.enum(['stable', 'declining', 'rising', 'unknown']), + })) .query(async ({ input }) => { try { const now = Date.now() @@ -166,7 +179,14 @@ export const waterLevelRouter = router({ getAlerts: publicProcedure .meta({ openapi: { method: 'GET', path: '/water-level/alerts', protect: false, tags: ['Water Level'] } }) .input(z.object({})) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + type: z.enum(['low_sustained', 'rapid_change', 'leak_suspected']), + startedAt: z.date(), + dismissedAt: z.date().nullable(), + message: z.string().nullable(), + createdAt: z.date(), + }))) .query(async () => { try { return await biometricsDb @@ -226,7 +246,14 @@ export const waterLevelRouter = router({ .input(z.object({ hours: z.number().int().min(1).max(168).default(24), }).strict()) - .output(z.any()) + .output(z.array(z.object({ + id: z.number(), + timestamp: z.date(), + leftFlowrateCd: z.number().nullable(), + rightFlowrateCd: z.number().nullable(), + leftPumpRpm: z.number().nullable(), + rightPumpRpm: z.number().nullable(), + }))) .query(async ({ input }) => { try { const since = new Date(Date.now() - input.hours * 60 * 60 * 1000) @@ -252,7 +279,14 @@ export const waterLevelRouter = router({ getLatestFlowReading: publicProcedure .meta({ openapi: { method: 'GET', path: '/water-level/flow/latest', protect: false, tags: ['Water Level'] } }) .input(z.object({})) - .output(z.any()) + .output(z.object({ + id: z.number(), + timestamp: z.date(), + leftFlowrateCd: z.number().nullable(), + rightFlowrateCd: z.number().nullable(), + leftPumpRpm: z.number().nullable(), + rightPumpRpm: z.number().nullable(), + }).nullable()) .query(async () => { try { const [row] = await biometricsDb