'use strict'; /** * preflight — boot visibility (Session 65, Standard §A4). * * A healthcheck that only pings /api/health can't tell "booted degraded" * from "crashed and rolled back" — which is exactly how a missing garnish * file (content/stark-lines.json) served a 14-hour-old image silently. This * runs once at boot and prints ONE unambiguous line: `[preflight] OK` when * everything required is present, or `[preflight] DEGRADED` naming exactly * what is missing. It NEVER throws and NEVER blocks boot — its only job is * to make the container's state legible in the logs. * * Philosophy (matches the app's own doctrine): content garnish missing is * NOTED, not fatal (absent beats crashed). Missing critical env is named so * an operator sees it immediately instead of debugging a symptom later. */ const fs = require('fs'); const path = require('path'); // Optional content — the app runs fine without these; we just name them so a // missing one is visible (and never silently crashes a require chain again). const OPTIONAL_CONTENT = [ { label: 'stark-lines.json', file: path.join(__dirname, '..', 'content', 'stark-lines.json'), note: 'media posts drop the Stark kicker' }, { label: 'coaches.json', file: path.join(__dirname, 'config', 'coaches.json'), note: 'coach-impact feature uses no seed' }, ]; // Critical env — the app boots without these but key surfaces degrade; named // loudly so an operator fixes them before hunting a downstream symptom. const CRITICAL_ENV = ['SUPABASE_URL', 'REDIS_URL', 'VYNDR_INTERNAL_KEY']; // Either service-role name satisfies the Supabase write path. const ENV_EITHER = [['SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_SERVICE_KEY']]; function runPreflight(log = console) { try { const missingContent = []; for (const c of OPTIONAL_CONTENT) { let ok = false; try { fs.accessSync(c.file, fs.constants.R_OK); ok = true; } catch { ok = false; } if (!ok) missingContent.push(`${c.label} (${c.note})`); } const missingEnv = CRITICAL_ENV.filter((k) => !process.env[k]); for (const group of ENV_EITHER) { if (!group.some((k) => process.env[k])) missingEnv.push(group.join('|')); } // Operational visibility for the pipeline knobs (not failures — states). const cron = process.env.SNAPSHOT_CRON === '1' ? 'armed' : 'off'; const interval = Number(process.env.SNAPSHOT_EXPECTED_INTERVAL) > 0 ? `${process.env.SNAPSHOT_EXPECTED_INTERVAL}s` : 'default(18000s)'; if (missingContent.length === 0 && missingEnv.length === 0) { log.log(`[preflight] OK — content present, critical env present, snapshot cron ${cron}, expected-interval ${interval}`); } else { log.warn(`[preflight] DEGRADED — snapshot cron ${cron}, expected-interval ${interval}`); if (missingEnv.length) log.warn(`[preflight] missing env: ${missingEnv.join(', ')}`); if (missingContent.length) log.warn(`[preflight] missing content (non-fatal): ${missingContent.join(', ')}`); } return { ok: missingContent.length === 0 && missingEnv.length === 0, missingContent, missingEnv }; } catch (e) { // Preflight itself must never break boot. try { log.warn(`[preflight] check errored (non-fatal): ${e.message}`); } catch { /* ignore */ } return { ok: false, error: e.message }; } } module.exports = { runPreflight, __internals: { OPTIONAL_CONTENT, CRITICAL_ENV, ENV_EITHER } };