Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
output/
125 changes: 125 additions & 0 deletions app/api/(auth)/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Authentication Route Group

## Current Status: No Authentication Required

This route group uses Next.js App Router's [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups) pattern (parentheses notation) to organize all API endpoints that **will eventually require authentication**.

### Why No Auth Currently?

**Deployment Context: Local Hardware Only**

This application runs exclusively on local hardware (SleepyPod device) with **no external network exposure**:

- Server runs on localhost only
- No internet-facing endpoints
- No remote API access
- Single-user, single-device deployment
- No multi-tenant concerns
- Health/biometric data never leaves the device

**Security Posture:**
- Physical access control (device is in user's home)
- Network isolation (no external exposure)
- Direct hardware control (USB/local socket communication)
- No authentication needed for local-only deployment

### Benefits of (auth) Route Group Structure

Despite not implementing auth yet, this structure provides:

1. **Future-Ready Architecture**: All sensitive endpoints are grouped
2. **Clear Intent**: The `(auth)` name signals these routes handle sensitive operations
3. **Easy Migration Path**: When auth is needed, add middleware in one place
4. **Route Organization**: Parentheses folder doesn't affect URL paths
- Routes stay at `/api/trpc/*` (not `/api/(auth)/trpc/*`)

### When to Add Authentication

Consider implementing auth if any of these change:

- [ ] Device becomes accessible over network/WiFi
- [ ] Cloud sync or remote monitoring features added
- [ ] Mobile app connects from different devices
- [ ] Multi-user support per device (e.g., left/right side different users)
- [ ] Web UI accessible from other machines on network
- [ ] Data export to external services

### How to Add Authentication Later

When the time comes, adding auth is straightforward:

**Option 1: Middleware at Route Group Level**
```typescript
// app/api/(auth)/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
// Validate session, JWT, or API key
const token = request.headers.get('authorization')

if (!isValidToken(token)) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}

return NextResponse.next()
}

export const config = {
matcher: '/api/(auth)/:path*'
}
```

**Option 2: tRPC Context-Based Auth**
```typescript
// src/server/trpc.ts
import { TRPCError } from '@trpc/server'

const protectedProcedure = publicProcedure.use(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({ ctx: { ...ctx, user: ctx.user } })
})

// Apply to routers
export const deviceRouter = router({
setTemperature: protectedProcedure.input(...).mutation(...)
})
```

### Current API Endpoints in This Group

All tRPC procedures are in this route group:

**Device Control** (`/api/trpc`)
- Hardware temperature, power, alarm, priming operations
- Direct physical device control

**Biometrics** (`/api/trpc`)
- Sleep records, vitals, movement data
- Personal health information (PHI)

**Schedules** (`/api/trpc`)
- Temperature, power, and alarm schedules
- Recurring automation

**Settings** (`/api/trpc`)
- Device configuration, side settings, tap gestures

See [tRPC API Documentation](../../../src/server/routers/README.md) for full endpoint details.

### Related Documentation

- [tRPC Review Report](../../../docs/trpc-review-2026-02-23.md) - Comprehensive security analysis
- [Hardware Integration](../../../src/hardware/README.md) - Physical device communication
- [Database Schema](../../../src/db/README.md) - Data storage architecture

---

**Last Updated:** 2026-02-23
**Auth Status:** Not implemented (local hardware deployment)
**Requires Auth:** No (subject to change with network features)
14 changes: 14 additions & 0 deletions app/api/(auth)/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { appRouter } from '@/src/server/routers/app'
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'

const handler = (req: Request) => {
console.log('tRPC incoming:', req.url)
return fetchRequestHandler({
Comment on lines +4 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging full tRPC request URLs (PII risk).
Line 5 logs req.url, which can include serialized inputs in the query string. This risks leaking sensitive data into logs. Consider logging only the pathname or removing the log line.

πŸ”’ Suggested fix (sanitize logged URL)
 const handler = (req: Request) => {
-  console.log('tRPC incoming:', req.url)
+  const { pathname } = new URL(req.url)
+  console.log('tRPC incoming:', pathname)
   return fetchRequestHandler({
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handler = (req: Request) => {
console.log('tRPC incoming:', req.url)
return fetchRequestHandler({
const handler = (req: Request) => {
const { pathname } = new URL(req.url)
console.log('tRPC incoming:', pathname)
return fetchRequestHandler({
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/`(auth)/trpc/[trpc]/route.ts around lines 4 - 6, The handler function
currently logs the full request URL (req.url) which can contain sensitive query
data; update the logging in handler to avoid PII by either removing the
console.log or logging only safe parts such as new URL(req.url).pathname (or
req.nextUrl?.pathname) and minimal context like the HTTP method and route
identifier; change the log call in handler accordingly and ensure no other
references to req.url remain that output the full URL.

endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({}),
})
}

export { handler as GET, handler as POST }
12 changes: 12 additions & 0 deletions app/api/(auth)/trpc/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { appRouter } from '@/src/server/routers/app'
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'

const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({}),
})

export { handler as GET, handler as POST }
207 changes: 207 additions & 0 deletions instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* Scheduler initialization and process lifecycle management
*
* Handles:
* - Job scheduler initialization with retry logic
* - Centralized signal handling (SIGTERM/SIGINT)
* - Global unhandled rejection/exception handlers
* - Hardware pre-flight validation
* - Graceful shutdown sequencing
*
* USAGE:
* - If your Next.js version supports instrumentation hooks, this will be called automatically
* - Otherwise, call `initializeScheduler()` from your app startup (e.g., in a layout or API route)
*/

import { getJobManager, shutdownJobManager } from '@/src/scheduler'
import { closeDatabase } from '@/src/db'
import { createHardwareClient } from '@/src/hardware/client'

const DAC_SOCK_PATH = process.env.DAC_SOCK_PATH || '/run/dac.sock'

let isInitialized = false
let isShuttingDown = false
let handlersRegistered = false

/**
* Centralized graceful shutdown coordinator.
* Sequences: wait for in-flight jobs β†’ shutdown scheduler β†’ close database β†’ exit
*/
async function gracefulShutdown(signal: string): Promise<void> {
if (isShuttingDown) return
isShuttingDown = true

console.log(`Received ${signal}, starting graceful shutdown...`)

// Force exit after 10s if graceful shutdown hangs
const forceExitTimer = setTimeout(() => {
console.error('Graceful shutdown timed out after 10s, forcing exit')
process.exit(1)
}, 10_000)
forceExitTimer.unref()

// Step 1: Shutdown scheduler (waits for in-flight jobs internally)
try {
await shutdownJobManager()
}
catch (error) {
console.error('Error shutting down scheduler:', error)
}

// Step 2: Close database connection
try {
closeDatabase()
}
catch (error) {
console.error('Error closing database:', error)
}

process.exit(0)
}

/**
* Register global process handlers (signal handlers, error handlers).
* Safe to call multiple times - only registers once.
*/
function registerGlobalHandlers(): void {
if (handlersRegistered) return
handlersRegistered = true

// Centralized signal handlers
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))

// Global unhandled rejection handler - log but don't crash
process.on('unhandledRejection', (reason: unknown) => {
console.error('Unhandled promise rejection:', reason)
// Don't exit - let the process continue serving other requests
})

// Global uncaught exception handler - log and attempt graceful shutdown
process.on('uncaughtException', (error: Error) => {
console.error('Uncaught exception:', error)
// Process state may be corrupted, attempt graceful shutdown
gracefulShutdown('uncaughtException')
})
}

/**
* Retry a function with exponential backoff.
*/
async function withRetry<T>(
fn: () => Promise<T>,
label: string,
maxAttempts: number = 3,
baseDelayMs: number = 500
): Promise<T> {
let lastError: unknown
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
}
catch (error) {
lastError = error
if (attempt < maxAttempts) {
const delay = baseDelayMs * Math.pow(2, attempt - 1)
console.warn(
`${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms:`,
error instanceof Error ? error.message : error
)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError
}

/**
* Validate hardware daemon connectivity on startup.
* Logs a warning if unavailable but does not crash.
*/
async function validateHardware(): Promise<void> {
try {
const client = await withRetry(
() => createHardwareClient({ socketPath: DAC_SOCK_PATH, connectionTimeout: 5000 }),
'Hardware validation',
3,
1000
)
client.disconnect()
console.log('Hardware daemon connectivity verified')
}
catch (error) {
console.warn(
'WARNING: Hardware daemon is not available at',
DAC_SOCK_PATH,
'-',
error instanceof Error ? error.message : error
)
console.warn('Scheduled jobs that require hardware will fail until the daemon is running')
}
}

/**
* Initialize the job scheduler
* Safe to call multiple times - will only initialize once
*/
export async function initializeScheduler(): Promise<void> {
if (isInitialized) return

try {
console.log('Initializing job scheduler...')
const jobManager = await withRetry(
() => getJobManager(),
'Job manager initialization'
)
const scheduler = jobManager.getScheduler()
const jobs = scheduler.getJobs()

console.log(`Job scheduler initialized with ${jobs.length} scheduled jobs`)

// Log next scheduled jobs for visibility
const upcomingJobs = jobs
.map((job) => {
const nextRun = scheduler.getNextInvocation(job.id)
return {
id: job.id,
type: job.type,
nextRun: nextRun ? nextRun.toISOString() : 'N/A',
}
})
.filter(job => job.nextRun !== 'N/A')
.sort((a, b) => {
if (a.nextRun === 'N/A' || b.nextRun === 'N/A') return 0
return new Date(a.nextRun).getTime() - new Date(b.nextRun).getTime()
})
.slice(0, 5)

if (upcomingJobs.length > 0) {
console.log('Next scheduled jobs:')
for (const job of upcomingJobs) {
console.log(` - ${job.id}: ${job.nextRun}`)
}
}

isInitialized = true

// Validate hardware connectivity (non-blocking, runs after scheduler is ready)
validateHardware()
}
catch (error) {
console.error('Failed to initialize job scheduler:', error)
// Don't crash the app if scheduler fails to initialize
}
}

/**
* Next.js instrumentation hook (if supported)
* Automatically called by Next.js on server startup
*/
export async function register(): Promise<void> {
// Only run on server
if (process.env.NEXT_RUNTIME === 'nodejs' || typeof window === 'undefined') {
// Register global handlers first (before any initialization that could fail)
registerGlobalHandlers()
await initializeScheduler()
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"lucide-react": "^0.562.0",
"negotiator": "^1.0.0",
"next": "^16.1.6",
"node-schedule": "^2.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"shadcn": "^3.8.5",
Expand Down Expand Up @@ -67,6 +68,7 @@
"@types/jsdom": "^27.0.0",
"@types/negotiator": "^0.6.4",
"@types/node": "^25.3.0",
"@types/node-schedule": "^2.1.8",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
Expand Down
Loading