feat: terminology fix (Entry→Event), diary page generation, settings refactor

- Rename Entry→Event throughout frontend and backend
- Change 'journal' terminology to 'diary page'
- Add professional footer with links
- Redirect to diary page after generation
- Error handling for generation failures
- Fix settings to store per-provider configs in providerSettings
- Backend reads API key from providerSettings
- Use prisma db push instead of migrate for schema sync
- Clean up duplicate entries.ts file
This commit is contained in:
lotherk
2026-03-26 23:10:33 +00:00
parent 754fea73c6
commit deaf496a7d
18 changed files with 289 additions and 256 deletions

View File

@@ -147,7 +147,7 @@ app.get('/api/v1/days', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const days = await prisma.entry.groupBy({
const days = await prisma.event.groupBy({
by: ['date'],
where: { userId },
_count: { id: true },
@@ -163,7 +163,7 @@ app.get('/api/v1/days', async (c) => {
const result = days.map(day => ({
date: day.date,
entryCount: day._count.id,
eventCount: day._count.id,
hasJournal: journalMap.has(day.date),
journalGeneratedAt: journalMap.get(day.date)?.generatedAt,
}));
@@ -181,12 +181,12 @@ app.get('/api/v1/days/:date', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'Invalid date format. Use YYYY-MM-DD' } }, 400);
}
const [entries, journal] = await Promise.all([
prisma.entry.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
const [events, journal] = await Promise.all([
prisma.event.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
prisma.journal.findFirst({ where: { userId, date } }),
]);
return c.json({ data: { date, entries, journal }, error: null });
return c.json({ data: { date, events, journal }, error: null });
});
app.delete('/api/v1/days/:date', async (c) => {
@@ -196,15 +196,31 @@ app.delete('/api/v1/days/:date', async (c) => {
const { date } = c.req.param();
await prisma.$transaction([
prisma.entry.deleteMany({ where: { userId, date } }),
prisma.event.deleteMany({ where: { userId, date } }),
prisma.journal.deleteMany({ where: { userId, date } }),
]);
return c.json({ data: { deleted: true }, error: null });
});
// Entries routes
app.post('/api/v1/entries', async (c) => {
// Delete journal only (keeps events)
app.delete('/api/v1/journal/:date', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { date } = c.req.param();
const journal = await prisma.journal.findFirst({ where: { userId, date } });
if (!journal) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Journal not found' } }, 404);
}
await prisma.journal.delete({ where: { id: journal.id } });
return c.json({ data: { deleted: true }, error: null });
});
// Events routes
app.post('/api/v1/events', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
@@ -214,45 +230,45 @@ app.post('/api/v1/entries', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'date, type, and content are required' } }, 400);
}
const validTypes = ['text', 'voice', 'photo', 'health', 'location'];
const validTypes = ['event'];
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({
const event = await prisma.event.create({
data: { userId, date, type, content, metadata: metadata ? JSON.stringify(metadata) : null },
});
return c.json({ data: entry, error: null }, 201);
return c.json({ data: event, error: null }, 201);
});
app.get('/api/v1/entries/:id', async (c) => {
app.get('/api/v1/events/:id', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { id } = c.req.param();
const entry = await prisma.entry.findFirst({ where: { id, userId } });
const event = await prisma.event.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 });
if (!event) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
return c.json({ data: event, error: null });
});
app.put('/api/v1/entries/:id', async (c) => {
app.put('/api/v1/events/:id', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { id } = c.req.param();
const { content, metadata } = await c.req.json();
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 existing = await prisma.event.findFirst({ where: { id, userId } });
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event 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);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot edit event: journal already generated. Delete the journal first.' } }, 400);
}
const entry = await prisma.entry.update({
const event = await prisma.event.update({
where: { id },
data: {
content: content ?? existing.content,
@@ -260,23 +276,23 @@ app.put('/api/v1/entries/:id', async (c) => {
},
});
return c.json({ data: entry, error: null });
return c.json({ data: event, error: null });
});
app.delete('/api/v1/entries/:id', async (c) => {
app.delete('/api/v1/events/:id', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { id } = c.req.param();
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 existing = await prisma.event.findFirst({ where: { id, userId } });
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event 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);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot delete event: journal already generated. Delete the journal first.' } }, 400);
}
await prisma.entry.delete({ where: { id } });
await prisma.event.delete({ where: { id } });
return c.json({ data: { deleted: true }, error: null });
});
@@ -287,27 +303,30 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
const { date } = c.req.param();
const [entries, settings] = await Promise.all([
prisma.entry.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
const [events, settings] = await Promise.all([
prisma.event.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
prisma.settings.findUnique({ where: { userId } }),
]);
if (entries.length === 0) {
return c.json({ data: null, error: { code: 'NO_ENTRIES', message: 'No entries found for this date' } }, 400);
if (events.length === 0) {
return c.json({ data: null, error: { code: 'NO_EVENTS', message: 'No events found for this date' } }, 400);
}
const provider = settings?.aiProvider || 'groq';
const providerSettings = settings?.providerSettings ? JSON.parse(settings.providerSettings) : {};
const providerConfig = providerSettings[provider] || {};
const apiKey = providerConfig.apiKey || settings?.aiApiKey;
if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq') && !settings?.aiApiKey) {
if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq') && !apiKey) {
return c.json({ data: null, error: { code: 'NO_AI_CONFIG', message: 'AI not configured. Please set up your API key in settings.' } }, 400);
}
// Build entries text
const entriesText = entries.map(entry => {
let text = `[${entry.type.toUpperCase()}] ${entry.createdAt.toISOString()}\n${entry.content}`;
if (entry.metadata) {
// Build events text
const eventsText = events.map(event => {
let text = `[EVENT] ${event.createdAt.toISOString()}\n${event.content}`;
if (event.metadata) {
try {
const meta = JSON.parse(entry.metadata);
const meta = JSON.parse(event.metadata);
if (meta.location) text += `\nLocation: ${meta.location.lat}, ${meta.location.lng}`;
if (meta.duration) text += `\nDuration: ${meta.duration}s`;
} catch {}
@@ -342,15 +361,15 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
}
}
// Build prompts: 1. system prompt, 2. previous journals, 3. today's entries
// Build prompts: 1. system prompt, 2. previous journals, 3. today's events
const systemPrompt = settings?.journalPrompt || 'You are a thoughtful journal writer.';
const userPrompt = `${previousJournalsText}ENTRIES FROM TODAY (${date}):\n${entriesText}\n\nWrite a thoughtful, reflective journal entry based on the entries above.`;
const userPrompt = `${previousJournalsText}EVENTS FROM TODAY (${date}):\n${eventsText}\n\nWrite a thoughtful, reflective journal entry based on the events above.`;
console.log(`[Journal Generate] Date: ${date}, Context days: ${contextDays}, Entries: ${entries.length}`);
console.log(`[Journal Generate] Date: ${date}, Context days: ${contextDays}, Events: ${events.length}`);
// Create placeholder journal and task
const placeholderJournal = await prisma.journal.create({
data: { userId, date, content: 'Generating...', entryCount: entries.length },
data: { userId, date, content: 'Generating...', eventCount: events.length },
});
const task = await prisma.task.create({
@@ -376,11 +395,14 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
try {
console.log(`[Journal Generate] Using provider: ${provider}`);
const model = providerConfig.model || settings?.aiModel;
const baseUrl = providerConfig.baseUrl || settings?.aiBaseUrl;
const aiProvider = createAIProvider({
provider: provider as 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq',
apiKey: settings?.aiApiKey || '',
model: settings?.aiModel || undefined,
baseUrl: (provider === 'ollama' || provider === 'lmstudio') ? settings?.aiBaseUrl || undefined : undefined,
apiKey: apiKey,
model: model || undefined,
baseUrl: (provider === 'ollama' || provider === 'lmstudio') ? baseUrl : undefined,
});
console.log(`[Journal Generate] AI Provider created: ${aiProvider.provider}`);

View File

@@ -7,7 +7,7 @@ daysRoutes.get('/', async (c) => {
const userId = c.get('userId');
const prisma = c.get('prisma');
const days = await prisma.entry.groupBy({
const days = await prisma.event.groupBy({
by: ['date'],
where: { userId },
_count: { id: true },
@@ -16,14 +16,14 @@ daysRoutes.get('/', async (c) => {
const journals = await prisma.journal.findMany({
where: { userId },
select: { date: true, generatedAt: true, entryCount: true },
select: { date: true, generatedAt: true, eventCount: true },
});
const journalMap = new Map(journals.map(j => [j.date, j]));
const result = days.map(day => ({
date: day.date,
entryCount: day._count.id,
eventCount: day._count.id,
hasJournal: journalMap.has(day.date),
journalGeneratedAt: journalMap.get(day.date)?.generatedAt,
}));
@@ -41,8 +41,8 @@ daysRoutes.get('/:date', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'Invalid date format. Use YYYY-MM-DD' } }, 400);
}
const [entries, journal] = await Promise.all([
prisma.entry.findMany({
const [events, journal] = await Promise.all([
prisma.event.findMany({
where: { userId, date },
orderBy: { createdAt: 'asc' },
}),
@@ -51,7 +51,7 @@ daysRoutes.get('/:date', async (c) => {
}),
]);
return c.json({ data: { date, entries, journal }, error: null });
return c.json({ data: { date, events, journal }, error: null });
});
daysRoutes.delete('/:date', async (c) => {
@@ -60,9 +60,29 @@ daysRoutes.delete('/:date', async (c) => {
const prisma = c.get('prisma');
await prisma.$transaction([
prisma.entry.deleteMany({ where: { userId, date } }),
prisma.event.deleteMany({ where: { userId, date } }),
prisma.journal.deleteMany({ where: { userId, date } }),
]);
return c.json({ data: { deleted: true }, error: null });
});
daysRoutes.delete('/:date/journal', async (c) => {
const userId = c.get('userId');
const { date } = c.req.param();
const prisma = c.get('prisma');
const journal = await prisma.journal.findUnique({
where: { userId_date: { userId, date } },
});
if (!journal) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Journal not found' } }, 404);
}
await prisma.journal.delete({
where: { id: journal.id },
});
return c.json({ data: { deleted: true }, error: null });
});

View File

@@ -1,9 +1,9 @@
import { Hono } from 'hono';
import { HonoEnv } from '../lib/types';
export const entriesRoutes = new Hono<HonoEnv>();
export const eventsRoutes = new Hono<HonoEnv>();
entriesRoutes.post('/', async (c) => {
eventsRoutes.post('/', async (c) => {
const userId = c.get('userId');
const prisma = c.get('prisma');
const mediaDir = c.env.MEDIA_DIR || './data/media';
@@ -15,12 +15,12 @@ entriesRoutes.post('/', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'date, type, and content are required' } }, 400);
}
const validTypes = ['text', 'voice', 'photo', 'health', 'location'];
const validTypes = ['event', 'text', 'photo', 'voice', 'health'];
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({
const event = await prisma.event.create({
data: {
userId,
date,
@@ -30,26 +30,26 @@ entriesRoutes.post('/', async (c) => {
},
});
return c.json({ data: entry, error: null }, 201);
return c.json({ data: event, error: null }, 201);
});
entriesRoutes.get('/:id', async (c) => {
eventsRoutes.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({
const event = await prisma.event.findFirst({
where: { id, userId },
});
if (!entry) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
if (!event) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
}
return c.json({ data: entry, error: null });
return c.json({ data: event, error: null });
});
entriesRoutes.put('/:id', async (c) => {
eventsRoutes.put('/:id', async (c) => {
const userId = c.get('userId');
const { id } = c.req.param();
const prisma = c.get('prisma');
@@ -57,20 +57,20 @@ entriesRoutes.put('/:id', async (c) => {
const body = await c.req.json();
const { content, metadata } = body;
const existing = await prisma.entry.findFirst({
const existing = await prisma.event.findFirst({
where: { id, userId },
});
if (!existing) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event 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);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot edit event: diary already generated. Delete the diary first.' } }, 400);
}
const entry = await prisma.entry.update({
const event = await prisma.event.update({
where: { id },
data: {
content: content ?? existing.content,
@@ -78,44 +78,44 @@ entriesRoutes.put('/:id', async (c) => {
},
});
return c.json({ data: entry, error: null });
return c.json({ data: event, error: null });
});
entriesRoutes.delete('/:id', async (c) => {
eventsRoutes.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({
const existing = await prisma.event.findFirst({
where: { id, userId },
});
if (!existing) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event 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);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot delete event: diary already generated. Delete the diary first.' } }, 400);
}
await prisma.entry.delete({ where: { id } });
await prisma.event.delete({ where: { id } });
return c.json({ data: { deleted: true }, error: null });
});
entriesRoutes.post('/:id/photo', async (c) => {
eventsRoutes.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({
const event = await prisma.event.findFirst({
where: { id, userId },
});
if (!entry) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
if (!event) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 400);
}
const body = await c.req.parseBody();
@@ -127,12 +127,12 @@ entriesRoutes.post('/:id/photo', async (c) => {
const ext = file.name.split('.').pop() || 'jpg';
const fileName = `${id}.${ext}`;
const userMediaDir = `${mediaDir}/${userId}/${entry.date}`;
const userMediaDir = `${mediaDir}/${userId}/${event.date}`;
const filePath = `${userMediaDir}/${fileName}`;
await Bun.write(filePath, file);
await prisma.entry.update({
await prisma.event.update({
where: { id },
data: { mediaPath: filePath },
});
@@ -140,18 +140,18 @@ entriesRoutes.post('/:id/photo', async (c) => {
return c.json({ data: { mediaPath: filePath }, error: null }, 201);
});
entriesRoutes.post('/:id/voice', async (c) => {
eventsRoutes.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({
const event = await prisma.event.findFirst({
where: { id, userId },
});
if (!entry) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
if (!event) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 400);
}
const body = await c.req.parseBody();
@@ -162,12 +162,12 @@ entriesRoutes.post('/:id/voice', async (c) => {
}
const fileName = `${id}.webm`;
const userMediaDir = `${mediaDir}/${userId}/${entry.date}`;
const userMediaDir = `${mediaDir}/${userId}/${event.date}`;
const filePath = `${userMediaDir}/${fileName}`;
await Bun.write(filePath, file);
await prisma.entry.update({
await prisma.event.update({
where: { id },
data: { mediaPath: filePath },
});

View File

@@ -9,8 +9,8 @@ journalRoutes.post('/generate/:date', async (c) => {
const { date } = c.req.param();
const prisma = c.get('prisma');
const [entries, settings] = await Promise.all([
prisma.entry.findMany({
const [events, settings] = await Promise.all([
prisma.event.findMany({
where: { userId, date },
orderBy: { createdAt: 'asc' },
}),
@@ -19,8 +19,8 @@ journalRoutes.post('/generate/:date', async (c) => {
}),
]);
if (entries.length === 0) {
return c.json({ data: null, error: { code: 'NO_ENTRIES', message: 'No entries found for this date' } }, 400);
if (events.length === 0) {
return c.json({ data: null, error: { code: 'NO_EVENTS', message: 'No events found for this date' } }, 400);
}
if (!settings?.aiApiKey) {
@@ -34,11 +34,11 @@ journalRoutes.post('/generate/:date', async (c) => {
baseUrl: settings.aiBaseUrl,
});
const entriesText = entries.map(entry => {
let text = `[${entry.type.toUpperCase()}] ${entry.createdAt.toISOString()}\n${entry.content}`;
if (entry.metadata) {
const eventsText = events.map(event => {
let text = `[${event.type.toUpperCase()}] ${event.createdAt.toISOString()}\n${event.content}`;
if (event.metadata) {
try {
const meta = JSON.parse(entry.metadata);
const meta = JSON.parse(event.metadata);
if (meta.location) text += `\nLocation: ${meta.location.lat}, ${meta.location.lng}`;
if (meta.duration) text += `\nDuration: ${meta.duration}s`;
} catch {}
@@ -46,17 +46,17 @@ journalRoutes.post('/generate/:date', async (c) => {
return text;
}).join('\n\n');
const prompt = `The following entries were captured throughout the day (${date}). Write a thoughtful, reflective journal entry that:
const prompt = `The following events were captured throughout the day (${date}). Write a thoughtful, reflective diary page that:
1. Summarizes the key moments and activities
2. Reflects on any patterns, feelings, or insights
3. Ends with a forward-looking thought
Use a warm, personal tone. The journal should flow naturally as prose.
ENTRIES:
${entriesText}
EVENTS:
${eventsText}
JOURNAL:`;
DIARY PAGE:`;
try {
const content = await provider.generate(prompt, settings.journalPrompt);
@@ -67,11 +67,11 @@ JOURNAL:`;
userId,
date,
content,
entryCount: entries.length,
eventCount: events.length,
},
update: {
content,
entryCount: entries.length,
eventCount: events.length,
generatedAt: new Date(),
},
});