import { i18n } from './i18n' import { sdk } from './sdk' import { uiPort } from './utils' import { configFile } from './file-models/config.json' // ── System SMTP → config.json sync ─────────────────────────────────────── // The StartOS SDK exposes shared SMTP credentials via effects.getSystemSmtp. // Passing a callback subscribes us — StartOS re-invokes the callback when // the operator changes their System → SMTP settings. We mirror the values // into our own config.json so the server (which polls the JSON via // server/config.js) can pick them up without needing direct SDK access. // // `password` can be null in the StartOS payload — coerce to empty string // so the Zod schema (z.string()) accepts it. The server treats "" the // same as "auth disabled". // // Only meaningful in multi-tenant cloud mode (recap_mode === 'multi'), // where the server sends magic-link sign-in emails. In single mode the // fields exist in config.json but nothing reads them. async function syncSystemSmtpToConfig(effects: any) { try { const smtp = await effects.getSystemSmtp({ callback: () => { // Re-fire ourselves on change. Effects callbacks fire on every // mutation of the underlying value, so this stays in sync. syncSystemSmtpToConfig(effects).catch((err) => { console.warn('[smtp] sync callback failed:', err) }) }, }) if (!smtp) { // No System SMTP configured — clear stale values so the server // doesn't try to send mail through a transport we no longer have // credentials for. await configFile.merge(effects, { smtp_host: '', smtp_port: 0, smtp_security: 'tls', smtp_username: '', smtp_password: '', smtp_from: '', }) return } await configFile.merge(effects, { smtp_host: smtp.host || '', smtp_port: smtp.port || 0, smtp_security: smtp.security || 'tls', smtp_username: smtp.username || '', smtp_password: smtp.password || '', smtp_from: smtp.from || '', }) } catch (err) { console.warn('[smtp] initial sync failed:', err) } } export const main = sdk.setupMain(async ({ effects }) => { console.info(i18n('Starting Recap...')) // Subscribe to System SMTP changes before the daemon starts so the // server boots with credentials already in config.json (no race where // a magic-link request arrives before the first sync). await syncSystemSmtpToConfig(effects) // Read current config to determine which mode to boot in. The // RECAP_MODE env var is passed to the container so the Node server // can branch on it without re-reading the config file at startup. const cfg = await configFile.read().once() const recapMode = cfg?.recap_mode === 'multi' ? 'multi' : 'single' return sdk.Daemons.of(effects).addDaemon('primary', { subcontainer: await sdk.SubContainer.of( effects, { imageId: 'main' }, sdk.Mounts.of().mountVolume({ volumeId: 'main', subpath: null, mountpoint: '/data', readonly: false, }), 'recap-sub', ), exec: { command: [ 'dumb-init', '--', '/usr/local/bin/docker_entrypoint.sh', ], env: { RECAP_MODE: recapMode, }, }, ready: { display: i18n('Web Interface'), fn: () => sdk.healthCheck.checkPortListening(effects, uiPort, { successMessage: i18n('Recap is ready'), errorMessage: i18n('Recap is not responding'), }), }, requires: [], }) })