You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/src/server.ts

75 lines
3.1 KiB
TypeScript

import express from 'express'
import fs from 'node:fs'
import type http from 'node:http'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { apiRouter } from './api'
import { maybePullAwsCosts } from './aws-costs'
import { pollBounces } from './bounces'
import { openDb, type DB } from './db'
import { renderPdf } from './pdf'
import { runDailyScan } from './scheduler'
import { seedIfEmpty } from './seed'
// dist/server.cjs (esbuild CJS bundle) has __dirname; vitest/tsx run this file as ESM.
// Both src/ and dist/ sit one level under apps/hq, so ../../hq-web/dist works from either.
const moduleDir = typeof __dirname === 'undefined'
? path.dirname(fileURLToPath(import.meta.url))
: __dirname
function schedulerDeps(db: DB) {
const company = (): Record<string, string> =>
Object.fromEntries((db.prepare(`SELECT key, value FROM setting WHERE key LIKE 'company.%'`)
.all() as { key: string; value: string }[]).map((r) => [r.key, r.value]))
const gmail = {
f: fetch,
clientId: process.env['GOOGLE_CLIENT_ID'] ?? '',
clientSecret: process.env['GOOGLE_CLIENT_SECRET'] ?? '',
keyHex: process.env['HQ_SECRET_KEY'] ?? '',
}
return { gmail, renderPdf, company }
}
/** Boot the scan + bounce poll now and every 6h. unref() so it never blocks exit. */
export function startScheduler(db: DB): NodeJS.Timeout {
const deps = schedulerDeps(db)
const awsCreds = {
accessKeyId: process.env['AWS_ACCESS_KEY_ID'] ?? '',
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'] ?? '',
}
const today = (): string => new Date().toISOString().slice(0, 10)
const tick = (): void => {
void runDailyScan(db, deps, today()).catch((e: unknown) => console.error('[scheduler] scan failed', e))
void pollBounces(db, deps).catch((e: unknown) => console.error('[scheduler] bounce poll failed', e))
// AWS cost pull only when credentials are configured — env-gated, once per month.
if (awsCreds.accessKeyId !== '' && awsCreds.secretAccessKey !== '') {
void maybePullAwsCosts(db, { f: fetch, creds: awsCreds }, today())
.catch((e: unknown) => console.error('[scheduler] aws cost pull failed', e))
}
}
tick()
const handle = setInterval(tick, 6 * 60 * 60 * 1000)
handle.unref()
return handle
}
export function startServer(port: number, opts: { scheduler?: boolean } = {}): http.Server {
const app = express()
const db = openDb(process.env['HQ_DATA_DIR'])
seedIfEmpty(db) // first boot prints the owner password once
app.locals['db'] = db
app.use(express.json({ limit: '2mb' }))
app.use('/api', apiRouter(db))
const webDist = path.resolve(moduleDir, '../../hq-web/dist')
if (fs.existsSync(webDist)) app.use('/', express.static(webDist))
if (opts.scheduler === true) startScheduler(db)
return app.listen(port)
}
// Direct launch (bundled dist/server.cjs); vitest imports startServer instead.
if (process.argv[1]?.endsWith('server.cjs') || process.argv[1]?.endsWith('server.ts')) {
const PORT = Number(process.env['HQ_PORT'] ?? 5182)
startServer(PORT, { scheduler: true })
console.log(`SiMS HQ console on http://localhost:${PORT}`)
}