import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; import { db } from '../db/connection.js'; import { contexts, topics, historyEntries } from '../db/schema.js'; import { and, eq, sql, max } from 'drizzle-orm'; import { isLocked } from '../lib/ai-export-service.js'; import { ensureDailyLog } from '../lib/sync-service.js'; import type { AuthEnv } from '../middleware/auth.js'; import { randomUUID } from 'crypto'; const JOURNAL_TOPIC_ID = 'daily-log-journal'; const MAX_TITLE_LENGTH = 22; function now(): string { return new Date().toISOString(); } function normalizeTitle(title: string, body: string): { title: string; text: string } { if (title.length <= MAX_TITLE_LENGTH) { const text = body.trim() ? `${title}\n\n${body}` : title; return { title, text }; } const short = title.slice(0, MAX_TITLE_LENGTH) + '…'; const fullBody = body.trim() ? `${title}\n\n${body}` : title; return { title: short, text: `${short}\n\n${fullBody}` }; } async function ensureJournalTopic(userId: string): Promise { const existing = await db.select({ id: topics.id }).from(topics) .where(and(eq(topics.id, JOURNAL_TOPIC_ID), eq(topics.userId, userId))) .get(); if (existing) return; await db.insert(topics).values({ id: JOURNAL_TOPIC_ID, userId, contextId: 'daily-log', title: '__journal__', status: 'active', snoozeUntil: null, sortOrder: -1, isNew: false, updatedAt: now(), deletedAt: null, version: 1, }); } const DailyLogBody = z.object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD'), scope: z.enum(['private', 'company']), title: z.string().min(1).max(200), body: z.string(), }); const DailyLogResponse = z.object({ contextId: z.string(), topicId: z.string(), historyEntryId: z.string(), }); const pushDailyLogRoute = createRoute({ method: 'post', path: '/daily-log', summary: 'Create a new daily log entry', description: 'Pushes a new journal entry (historyEntry) into the daily-log context. Returns 409 if an entry with the same title already exists on that date.', security: [{ BearerAuth: [] }], request: { body: { content: { 'application/json': { schema: DailyLogBody } }, required: true, }, }, responses: { 201: { description: 'Entry created', content: { 'application/json': { schema: DailyLogResponse } }, }, 409: { description: 'Duplicate: entry with this title already exists on this date', content: { 'application/json': { schema: z.object({ error: z.string() }) } }, }, 423: { description: 'AI lock active', content: { 'application/json': { schema: z.object({ error: z.string() }) } }, }, }, }); const push = new OpenAPIHono(); push.openapi(pushDailyLogRoute, async (c) => { const { userId } = c.get('auth'); const lock = await isLocked(userId); if (lock) return c.json({ error: 'AI lock active — unlock at /api/ai/unlock' }, 423); await ensureDailyLog(userId); await ensureJournalTopic(userId); const { date, scope, title: rawTitle, body } = c.req.valid('json'); const isPrivate = scope === 'private'; const { title, text } = normalizeTitle(rawTitle, body); // Duplicate check: same date + title as first line (plain or legacy ## prefix) const existing = await db.select({ id: historyEntries.id }) .from(historyEntries) .where(and( eq(historyEntries.userId, userId), eq(historyEntries.topicId, JOURNAL_TOPIC_ID), eq(historyEntries.date, date), sql`(${historyEntries.text} LIKE ${title + '%'} OR ${historyEntries.text} LIKE ${'## ' + title + '%'})`, )) .get(); if (existing) { return c.json({ error: `Entry with title "${title}" already exists for ${date}` }, 409); } const maxSortOrder = await db.select({ val: max(historyEntries.sortOrder) }) .from(historyEntries) .where(and(eq(historyEntries.userId, userId), eq(historyEntries.topicId, JOURNAL_TOPIC_ID))) .get(); const sortOrder = (maxSortOrder?.val ?? 0) + 1; const id = randomUUID(); await db.insert(historyEntries).values({ id, userId, topicId: JOURNAL_TOPIC_ID, date, text, sortOrder, linkedContextId: null, doneAt: null, wiedervorlageDate: null, wiedervorlageResolvedAt: null, isPrivate, updatedAt: now(), deletedAt: null, version: 1, }); return c.json({ contextId: 'daily-log', topicId: JOURNAL_TOPIC_ID, historyEntryId: id }, 201); }); export default push;