diff --git a/backend/src/lib/cosmos-init.ts b/backend/src/lib/cosmos-init.ts index 0021e41..59486e5 100644 --- a/backend/src/lib/cosmos-init.ts +++ b/backend/src/lib/cosmos-init.ts @@ -1,5 +1,6 @@ import { initializeAllContainers, registerContainers } from '@bytelyst/cosmos'; import type { ContainerConfig } from '@bytelyst/cosmos'; +import type { Logger } from '@bytelyst/logger'; import { config } from './config.js'; const CONTAINER_DEFS: Record = { @@ -27,7 +28,9 @@ const CONTAINER_DEFS: Record = { palace_diaries: { partitionKeyPath: '/userId' }, }; -export async function initCosmosIfNeeded(): Promise { +type CosmosInitLogger = Pick; + +export async function initCosmosIfNeeded(logger?: CosmosInitLogger): Promise { registerContainers(CONTAINER_DEFS); const shouldInit = config.NODE_ENV !== 'production' || process.env.COSMOS_AUTO_INIT === 'true'; @@ -37,9 +40,8 @@ export async function initCosmosIfNeeded(): Promise { try { await initializeAllContainers(); - process.stdout.write('[notelett-backend] Cosmos containers ensured\n'); + logger?.info('Cosmos containers ensured'); } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`[notelett-backend] Cosmos init failed: ${msg}\n`); + logger?.error('Cosmos init failed', err); } } diff --git a/backend/src/modules/note-prompts/scheduler.ts b/backend/src/modules/note-prompts/scheduler.ts index 157e323..70e39b9 100644 --- a/backend/src/modules/note-prompts/scheduler.ts +++ b/backend/src/modules/note-prompts/scheduler.ts @@ -22,6 +22,16 @@ import { stripHtmlForEmbedding } from '../../lib/embeddings.js'; // ── Types ────────────────────────────────────────────────────────── +type SchedulerLogger = { + error: (details: Record, message: string) => void; +}; + +let schedulerLogger: SchedulerLogger = { error: () => undefined }; + +export function setSchedulerLogger(logger: SchedulerLogger): void { + schedulerLogger = logger; +} + export interface PromptScheduleDoc { id: string; productId: string; @@ -216,15 +226,17 @@ export async function runSchedulerTick(): Promise { trackEvent('scheduled_action_fired', schedule.userId, { scheduleId: schedule.id, templateSlug: template?.slug ?? schedule.templateId }); ran++; } catch (err: unknown) { - const msg = err instanceof Error ? err.message : 'Unknown scheduler error'; - process.stderr.write(`[scheduler] Failed to run schedule ${schedule.id}: ${msg}\n`); + schedulerLogger.error({ err, scheduleId: schedule.id }, 'Failed to run scheduled prompt'); } } return ran; } -export function startSchedulerLoop(intervalMs = 60_000): void { +export function startSchedulerLoop(intervalMs = 60_000, logger?: SchedulerLogger): void { + if (logger) { + setSchedulerLogger(logger); + } if (schedulerInterval) return; schedulerInterval = setInterval(() => { void runSchedulerTick(); diff --git a/backend/src/server.test.ts b/backend/src/server.test.ts index eeed50f..a1fa761 100644 --- a/backend/src/server.test.ts +++ b/backend/src/server.test.ts @@ -6,12 +6,18 @@ const startServiceMock = vi.fn(async () => undefined); const initCosmosIfNeededMock = vi.fn(async () => undefined); const initDatastoreMock = vi.fn(() => undefined); const diagnosticsRoutesMock = vi.fn(async () => undefined); +const startSchedulerLoopMock = vi.fn(); +const stopSchedulerLoopMock = vi.fn(); const appMock = { register: vi.fn(async () => undefined), get: vi.fn(), post: vi.fn(), addHook: vi.fn(), + log: { + error: vi.fn(), + info: vi.fn(), + }, }; vi.mock('@bytelyst/fastify-core', () => ({ @@ -36,8 +42,8 @@ vi.mock('./modules/ecosystem-phase3/routes.js', () => ({ ecosystemPhase3Routes: vi.mock('./modules/note-prompts/routes.js', () => ({ notePromptRoutes: vi.fn() })); vi.mock('./modules/note-prompts/scheduler.js', () => ({ promptSchedulerRoutes: vi.fn(), - startSchedulerLoop: vi.fn(), - stopSchedulerLoop: vi.fn(), + startSchedulerLoop: startSchedulerLoopMock, + stopSchedulerLoop: stopSchedulerLoopMock, })); vi.mock('./modules/intake/routes.js', () => ({ intakeRoutes: vi.fn() })); vi.mock('./modules/note-collaborators/routes.js', () => ({ noteCollaboratorRoutes: vi.fn() })); @@ -51,6 +57,7 @@ vi.mock('./lib/config.js', () => ({ PORT: 4016, HOST: '0.0.0.0', JWT_SECRET: 'test-secret', + NODE_ENV: 'test', }, })); vi.mock('./lib/product-config.js', () => ({ @@ -78,12 +85,16 @@ describe('server bootstrap', () => { it('initializes app, routes, and starts service', async () => { await import('./server.js'); - expect(initCosmosIfNeededMock).toHaveBeenCalledOnce(); + expect(initCosmosIfNeededMock).toHaveBeenCalledWith(expect.objectContaining({ + error: expect.any(Function), + info: expect.any(Function), + })); expect(initDatastoreMock).toHaveBeenCalledOnce(); expect(createServiceAppMock).toHaveBeenCalledOnce(); expect(registerOptionalJwtContextMock).toHaveBeenCalledOnce(); expect(appMock.register).toHaveBeenCalledTimes(14); expect(diagnosticsRoutesMock).toHaveBeenCalledWith(appMock); + expect(startSchedulerLoopMock).toHaveBeenCalledWith(60_000, appMock.log); expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4016, host: '0.0.0.0' }); }); }); diff --git a/backend/src/server.ts b/backend/src/server.ts index 39c92b1..dc3458a 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,4 +1,5 @@ import { createServiceApp, registerOptionalJwtContext, startService } from '@bytelyst/fastify-core'; +import { createLogger } from '@bytelyst/logger'; import { jwtVerify } from 'jose'; import { noteAgentActionRoutes } from './modules/note-agent-actions/routes.js'; import { ecosystemPhase1Routes } from './modules/ecosystem-phase1/routes.js'; @@ -27,8 +28,12 @@ import { findShareByToken } from './modules/note-shares/repository.js'; import * as noteRepo from './modules/notes/repository.js'; const jwtSecret = new TextEncoder().encode(config.JWT_SECRET); +const startupLogger = createLogger({ + service: config.SERVICE_NAME, + isDev: config.NODE_ENV !== 'production', +}); -await initCosmosIfNeeded(); +await initCosmosIfNeeded(startupLogger); initDatastore(); const app = await createServiceApp({ @@ -74,7 +79,7 @@ await registerApiPlugin(noteCollaboratorRoutes); await registerApiPlugin(palaceRoutes); // ── Start scheduler loop (F25) ──────────────────────────────────── -startSchedulerLoop(); +startSchedulerLoop(60_000, app.log); initWebhookSubscriber(); app.addHook('onClose', async () => { stopSchedulerLoop(); stopWebhookSubscriber(); });