feat: immutable entries + full task logging

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
This commit is contained in:
lotherk
2026-03-26 22:05:52 +00:00
parent 5c217853de
commit 754fea73c6
10 changed files with 197 additions and 98 deletions

View File

@@ -3,9 +3,10 @@ import type { Entry } from '../lib/api';
interface Props {
entries: Entry[];
onDelete: (id: string) => void;
readOnly?: boolean;
}
export default function EntryList({ entries, onDelete }: Props) {
export default function EntryList({ entries, onDelete, readOnly }: Props) {
const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
};
@@ -34,6 +35,11 @@ export default function EntryList({ entries, onDelete }: Props) {
return (
<div className="space-y-3">
{readOnly && (
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3 text-sm text-amber-400">
🔒 Entries are locked because a journal has been generated. Delete the journal to edit entries.
</div>
)}
{entries.map((entry) => (
<div
key={entry.id}
@@ -63,12 +69,14 @@ export default function EntryList({ entries, onDelete }: Props) {
</div>
)}
</div>
<button
onClick={() => onDelete(entry.id)}
className="text-slate-500 hover:text-red-400 text-sm transition"
>
×
</button>
{!readOnly && (
<button
onClick={() => onDelete(entry.id)}
className="text-slate-500 hover:text-red-400 text-sm transition"
>
×
</button>
)}
</div>
</div>
))}

View File

@@ -9,6 +9,7 @@ export default function Day() {
const [entries, setEntries] = useState<Entry[]>([]);
const [loading, setLoading] = useState(true);
const [hasJournal, setHasJournal] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
useEffect(() => {
if (date) loadEntries();
@@ -35,9 +36,21 @@ export default function Day() {
};
const handleDeleteEntry = async (id: string) => {
setDeleteError(null);
const res = await api.deleteEntry(id);
if (res.data) {
setEntries((prev) => prev.filter((e) => e.id !== id));
} else if (res.error?.code === 'ENTRY_IMMUTABLE') {
setDeleteError(res.error.message);
}
};
const handleDeleteJournal = async () => {
if (!date) return;
if (!confirm('Delete journal? This will unlock entries for editing.')) return;
const res = await api.deleteDay(date);
if (res.data) {
setHasJournal(false);
}
};
@@ -56,12 +69,26 @@ export default function Day() {
<h1 className="text-2xl font-bold">{formatDate(date)}</h1>
</div>
{hasJournal && (
<a href={`/journal/${date}`} className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg font-medium transition">
View Journal
</a>
<div className="flex gap-2">
<button
onClick={handleDeleteJournal}
className="px-3 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg text-sm transition"
>
Delete Journal
</button>
<a href={`/journal/${date}`} className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg font-medium transition">
View Journal
</a>
</div>
)}
</div>
{deleteError && (
<div className="bg-red-500/20 border border-red-500/30 rounded-lg p-3 mb-4 text-sm text-red-400">
{deleteError}
</div>
)}
<EntryInput onSubmit={handleAddEntry} />
{loading ? (
@@ -71,7 +98,7 @@ export default function Day() {
<p className="text-slate-400">No entries for this day</p>
</div>
) : (
<EntryList entries={entries} onDelete={handleDeleteEntry} />
<EntryList entries={entries} onDelete={handleDeleteEntry} readOnly={hasJournal} />
)}
</div>
);