Scaffold Phase 1 hvac app — NewBook-driven room TRV heating scheduler

Ports the state machine, retry/backoff, and guest-override detection from
the retired homeassistant-newbook-heating-component, without depending on
Home Assistant. Backend (Fastify/pg) + frontend (React/Vite/TS) following
standard stack conventions; LXC 128 (127 was already taken by utilities).

MHI/Midea/Daikin/boiler drivers and the shared MQTT broker (LXC 104) are
later phases/infra, not included here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-26 18:00:42 +00:00
commit 276c04f8c6
42 changed files with 5461 additions and 0 deletions

57
backend/src/index.js Normal file
View file

@ -0,0 +1,57 @@
import Fastify from 'fastify'
import cookie from '@fastify/cookie'
import cors from '@fastify/cors'
import multipart from '@fastify/multipart'
import staticFiles from '@fastify/static'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import { initDb } from './db.js'
import { connect as connectMqtt } from './lib/mqtt.js'
import { startScheduler } from './lib/scheduler.js'
import { zoneRoutes } from './routes/zones.js'
import { deviceRoutes } from './routes/devices.js'
import { statusRoutes } from './routes/status.js'
import { overrideRoutes } from './routes/override.js'
import { configRoutes } from './routes/config.js'
import { settingsRoutes } from './routes/settings.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
const UPLOADS_DIR = join(__dirname, '..', 'uploads')
const app = Fastify({ logger: true, trustProxy: true })
const startedAt = Date.now()
await app.register(cookie)
await app.register(cors, {
origin: process.env.CORS_ORIGIN || false,
credentials: true,
})
await app.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } })
await app.register(staticFiles, {
root: UPLOADS_DIR,
prefix: '/api/uploads/',
decorateReply: false,
})
app.get('/health', async () => ({ status: 'healthy', version: process.env.BUILD_VERSION || String(startedAt) }))
await app.register(zoneRoutes)
await app.register(deviceRoutes, { uploadsDir: UPLOADS_DIR })
await app.register(statusRoutes)
await app.register(overrideRoutes)
await app.register(configRoutes)
await app.register(settingsRoutes)
try {
await initDb()
// MQTT broker (shared infra, LXC 104) may not exist yet — connect() handles that
// gracefully with its own backoff and never blocks startup.
connectMqtt().catch(err => app.log.warn(`MQTT connect failed at startup: ${err.message}`))
await startScheduler()
await app.listen({ port: 3001, host: '0.0.0.0' })
} catch (err) {
app.log.error(err)
process.exit(1)
}