Entries now immutable once journal is generated: - Edit/delete returns ENTRY_IMMUTABLE error if journal exists - Frontend shows lock message and hides delete button - Delete Journal button to unlock entries Task logging now stores full JSON: - request: full JSON request sent to AI provider - response: full JSON response from AI provider - prompt: formatted human-readable prompt Prompt structure: 1. System prompt 2. Previous diary entries (journals) 3. Today's entries
177 lines
5.0 KiB
TypeScript
177 lines
5.0 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { HonoEnv } from '../lib/types';
|
|
|
|
export const entriesRoutes = new Hono<HonoEnv>();
|
|
|
|
entriesRoutes.post('/', async (c) => {
|
|
const userId = c.get('userId');
|
|
const prisma = c.get('prisma');
|
|
const mediaDir = c.env.MEDIA_DIR || './data/media';
|
|
|
|
const body = await c.req.json();
|
|
const { date, type, content, metadata } = body;
|
|
|
|
if (!date || !type || !content) {
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'date, type, and content are required' } }, 400);
|
|
}
|
|
|
|
const validTypes = ['text', 'voice', 'photo', 'health', 'location'];
|
|
if (!validTypes.includes(type)) {
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: `type must be one of: ${validTypes.join(', ')}` } }, 400);
|
|
}
|
|
|
|
const entry = await prisma.entry.create({
|
|
data: {
|
|
userId,
|
|
date,
|
|
type,
|
|
content,
|
|
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
},
|
|
});
|
|
|
|
return c.json({ data: entry, error: null }, 201);
|
|
});
|
|
|
|
entriesRoutes.get('/:id', async (c) => {
|
|
const userId = c.get('userId');
|
|
const { id } = c.req.param();
|
|
const prisma = c.get('prisma');
|
|
|
|
const entry = await prisma.entry.findFirst({
|
|
where: { id, userId },
|
|
});
|
|
|
|
if (!entry) {
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
|
|
}
|
|
|
|
return c.json({ data: entry, error: null });
|
|
});
|
|
|
|
entriesRoutes.put('/:id', async (c) => {
|
|
const userId = c.get('userId');
|
|
const { id } = c.req.param();
|
|
const prisma = c.get('prisma');
|
|
|
|
const body = await c.req.json();
|
|
const { content, metadata } = body;
|
|
|
|
const existing = await prisma.entry.findFirst({
|
|
where: { id, userId },
|
|
});
|
|
|
|
if (!existing) {
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
|
|
}
|
|
|
|
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
|
|
if (journal) {
|
|
return c.json({ data: null, error: { code: 'ENTRY_IMMUTABLE', message: 'Cannot edit entry: journal already generated. Delete the journal first.' } }, 400);
|
|
}
|
|
|
|
const entry = await prisma.entry.update({
|
|
where: { id },
|
|
data: {
|
|
content: content ?? existing.content,
|
|
metadata: metadata !== undefined ? JSON.stringify(metadata) : existing.metadata,
|
|
},
|
|
});
|
|
|
|
return c.json({ data: entry, error: null });
|
|
});
|
|
|
|
entriesRoutes.delete('/:id', async (c) => {
|
|
const userId = c.get('userId');
|
|
const { id } = c.req.param();
|
|
const prisma = c.get('prisma');
|
|
|
|
const existing = await prisma.entry.findFirst({
|
|
where: { id, userId },
|
|
});
|
|
|
|
if (!existing) {
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
|
|
}
|
|
|
|
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
|
|
if (journal) {
|
|
return c.json({ data: null, error: { code: 'ENTRY_IMMUTABLE', message: 'Cannot delete entry: journal already generated. Delete the journal first.' } }, 400);
|
|
}
|
|
|
|
await prisma.entry.delete({ where: { id } });
|
|
|
|
return c.json({ data: { deleted: true }, error: null });
|
|
});
|
|
|
|
entriesRoutes.post('/:id/photo', async (c) => {
|
|
const userId = c.get('userId');
|
|
const { id } = c.req.param();
|
|
const prisma = c.get('prisma');
|
|
const mediaDir = c.env.MEDIA_DIR || './data/media';
|
|
|
|
const entry = await prisma.entry.findFirst({
|
|
where: { id, userId },
|
|
});
|
|
|
|
if (!entry) {
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
|
|
}
|
|
|
|
const body = await c.req.parseBody();
|
|
const file = body.file;
|
|
|
|
if (!file || !(file instanceof File)) {
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'No file provided' } }, 400);
|
|
}
|
|
|
|
const ext = file.name.split('.').pop() || 'jpg';
|
|
const fileName = `${id}.${ext}`;
|
|
const userMediaDir = `${mediaDir}/${userId}/${entry.date}`;
|
|
const filePath = `${userMediaDir}/${fileName}`;
|
|
|
|
await Bun.write(filePath, file);
|
|
|
|
await prisma.entry.update({
|
|
where: { id },
|
|
data: { mediaPath: filePath },
|
|
});
|
|
|
|
return c.json({ data: { mediaPath: filePath }, error: null }, 201);
|
|
});
|
|
|
|
entriesRoutes.post('/:id/voice', async (c) => {
|
|
const userId = c.get('userId');
|
|
const { id } = c.req.param();
|
|
const prisma = c.get('prisma');
|
|
const mediaDir = c.env.MEDIA_DIR || './data/media';
|
|
|
|
const entry = await prisma.entry.findFirst({
|
|
where: { id, userId },
|
|
});
|
|
|
|
if (!entry) {
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
|
|
}
|
|
|
|
const body = await c.req.parseBody();
|
|
const file = body.file;
|
|
|
|
if (!file || !(file instanceof File)) {
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'No audio file provided' } }, 400);
|
|
}
|
|
|
|
const fileName = `${id}.webm`;
|
|
const userMediaDir = `${mediaDir}/${userId}/${entry.date}`;
|
|
const filePath = `${userMediaDir}/${fileName}`;
|
|
|
|
await Bun.write(filePath, file);
|
|
|
|
await prisma.entry.update({
|
|
where: { id },
|
|
data: { mediaPath: filePath },
|
|
});
|
|
|
|
return c.json({ data: { mediaPath: filePath }, error: null }, 201);
|
|
});
|