diff --git a/backend/.env.example b/backend/.env.example index 37bf508..dd0845b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -16,3 +16,7 @@ FIELD_ENCRYPT_KEY_PROVIDER=memory FIELD_ENCRYPT_KEY= FIELD_ENCRYPT_MEK_NAME=chronomind-mek AZURE_KEYVAULT_URL= + +# Platform + extraction services (optional — for AI context messages) +PLATFORM_SERVICE_URL=http://localhost:4003 +EXTRACTION_SERVICE_URL=http://localhost:4005 diff --git a/backend/src/modules/agent-actions/routes.ts b/backend/src/modules/agent-actions/routes.ts index dc02bd7..e235642 100644 --- a/backend/src/modules/agent-actions/routes.ts +++ b/backend/src/modules/agent-actions/routes.ts @@ -19,8 +19,7 @@ import { BatchApproveSchema, type AgentActionDoc, } from './types.js'; - -const PRODUCT_ID = 'chronomind'; +import { PRODUCT_ID } from '../../lib/product-config.js'; export async function agentActionRoutes(app: FastifyInstance) { // ── Feature flag gate ───────────────────────────────────── diff --git a/backend/src/modules/timers/routes.ts b/backend/src/modules/timers/routes.ts index be13ef1..a070d60 100644 --- a/backend/src/modules/timers/routes.ts +++ b/backend/src/modules/timers/routes.ts @@ -233,13 +233,17 @@ export async function timerRoutes(app: FastifyInstance) { } // Fetch all timers in the window (active or upcoming) - const { items: timers } = await repo.listTimers(auth.sub, PRODUCT_ID, { + const { items: allTimers } = await repo.listTimers(auth.sub, PRODUCT_ID, { limit: 100, offset: 0, sortBy: 'targetTime', sortOrder: 'asc', }); + // Exclude timers that no longer occupy time slots + const INACTIVE_STATES = new Set(['dismissed', 'completed', 'fired']); + const timers = allTimers.filter(t => !INACTIVE_STATES.has(t.state)); + // Build occupied intervals from timers that overlap the window const occupied: Array<{ start: number; end: number }> = []; for (const t of timers) { diff --git a/backend/src/server.ts b/backend/src/server.ts index 8b061de..90e1d7f 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -17,8 +17,9 @@ import { initCosmosIfNeeded } from './lib/cosmos-init.js'; import { initDatastore } from './lib/datastore.js'; import { initEncryption } from './lib/field-encrypt.js'; import { config } from './lib/config.js'; -import { getAllFlags } from './lib/feature-flags.js'; -import { generateContextMessage } from './lib/ai-context.js'; +import { getAllFlags, isFeatureEnabled } from './lib/feature-flags.js'; +import { generateContextMessage, type ContextMessageInput } from './lib/ai-context.js'; +import { z } from 'zod'; import { getBufferedEvents, flushEvents } from './lib/telemetry.js'; import { PRODUCT_ID, productConfig } from './lib/product-config.js'; @@ -58,19 +59,25 @@ await app.register(webhookRoutes, { prefix: '/api' }); await app.register(agentActionRoutes, { prefix: '/api' }); // ── Phase A.4: Context-aware AI messages ───────────────────────── -app.post('/api/context-message', async req => { - const body = req.body as { - timerLabel: string; - category?: string; - urgency?: string; - minutesBefore: number; - timeOfDay?: string; - recentTimerLabels?: string[]; - }; - if (!body.timerLabel || typeof body.minutesBefore !== 'number') { +const ContextMessageSchema = z.object({ + timerLabel: z.string().min(1).max(500), + category: z.string().max(128).optional(), + urgency: z.string().max(64).optional(), + minutesBefore: z.number().min(0).max(10080), + timeOfDay: z.string().max(64).optional(), + recentTimerLabels: z.array(z.string().max(500)).max(10).optional(), +}); + +app.post('/api/context-message', async (req, reply) => { + if (!isFeatureEnabled('ai_context_messages.enabled')) { return { message: 'Get ready!', source: 'generic' as const }; } - return generateContextMessage(body); + const parsed = ContextMessageSchema.safeParse(req.body); + if (!parsed.success) { + reply.code(400); + return { message: 'Get ready!', source: 'generic' as const }; + } + return generateContextMessage(parsed.data as ContextMessageInput); }); // ── Bootstrap (no auth) ──────────────────────────────────────────