feat(journal): add JournalView UI with /journal route + nav link
Restores a working browser surface for the journal feature, which was left UI-less when Stage F of the original plan was deferred. JournalView is a fresh write (not a rename of BriefingView) — drops the briefing- specific weather panel, news cards, RSS reactions, article-discuss button, and setup wizard, since none of those have journal-backend equivalents. What it does: - Fetches /api/journal/today on mount; shows today's daily-prep card (rendered from msg_metadata.kind === 'daily_prep' sections) - Day picker via /api/journal/days lets you switch to past days - Refresh-prep button hits /api/journal/trigger-prep - Center is the existing ChatPanel pinned to today's journal conversation (so the chat input + SSE + tool-call cards inherit unchanged) - Right sidebar keeps the upcoming-events list (uses the generic /api/events endpoint, not a briefing-specific one) Also: - Replace the dead Briefing API client functions with Journal equivalents (getJournalConfig, saveJournalConfig, getJournalToday, getJournalDay, getJournalDays, triggerJournalPrep, list/update/deleteJournalMoment). - Remove NewsView.vue — it was orphaned (no route, no nav) and depended on the deleted /api/briefing/* endpoints. If a standalone news surface is wanted later it'll need to be rebuilt against new endpoints. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+65
-89
@@ -300,130 +300,106 @@ export function apiSSEStream(
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Briefing
|
||||
// Journal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface BriefingLocation {
|
||||
label: string;
|
||||
address: string;
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
export interface JournalConfig {
|
||||
prep_enabled: boolean;
|
||||
prep_hour: number;
|
||||
prep_minute: number;
|
||||
day_rollover_hour: number;
|
||||
morning_end_hour?: number;
|
||||
midday_end_hour?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface BriefingSlots {
|
||||
compilation: boolean;
|
||||
morning: boolean;
|
||||
midday: boolean;
|
||||
afternoon: boolean;
|
||||
}
|
||||
|
||||
export interface BriefingConfig {
|
||||
enabled: boolean;
|
||||
locations: {
|
||||
home?: BriefingLocation;
|
||||
work?: BriefingLocation;
|
||||
};
|
||||
use_caldav_event_locations: boolean;
|
||||
work_days: number[];
|
||||
slots: BriefingSlots;
|
||||
notifications: boolean;
|
||||
temp_unit: 'C' | 'F';
|
||||
}
|
||||
|
||||
export interface BriefingFeed {
|
||||
export interface JournalConversation {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
category: string | null;
|
||||
last_fetched_at: string | null;
|
||||
}
|
||||
|
||||
export interface BriefingConversation {
|
||||
id: number;
|
||||
title: string;
|
||||
briefing_date: string | null;
|
||||
model: string;
|
||||
conversation_type: string;
|
||||
day_date: string | null;
|
||||
rag_project_id: number | null;
|
||||
message_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface BriefingMessage {
|
||||
export interface JournalMessage {
|
||||
id: number;
|
||||
conversation_id: number;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
status: string;
|
||||
context_note_id: number | null;
|
||||
tool_calls: unknown[] | null;
|
||||
metadata: Record<string, unknown> | null;
|
||||
created_at: string;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const DEFAULT_BRIEFING_CONFIG: BriefingConfig = {
|
||||
enabled: false,
|
||||
locations: {},
|
||||
use_caldav_event_locations: false,
|
||||
work_days: [1, 2, 3, 4, 5],
|
||||
slots: { compilation: true, morning: true, midday: false, afternoon: false },
|
||||
notifications: true,
|
||||
temp_unit: 'C',
|
||||
};
|
||||
|
||||
export async function getBriefingConfig(): Promise<BriefingConfig> {
|
||||
try {
|
||||
const data = await apiGet<BriefingConfig>('/api/briefing/config');
|
||||
return { ...DEFAULT_BRIEFING_CONFIG, ...data };
|
||||
} catch {
|
||||
return { ...DEFAULT_BRIEFING_CONFIG };
|
||||
}
|
||||
export interface JournalDayPayload {
|
||||
day_date: string;
|
||||
conversation: JournalConversation | null;
|
||||
messages: JournalMessage[];
|
||||
}
|
||||
|
||||
export async function saveBriefingConfig(config: BriefingConfig): Promise<void> {
|
||||
await apiPut('/api/briefing/config', config);
|
||||
export interface JournalMoment {
|
||||
id: number;
|
||||
user_id: number;
|
||||
conversation_id: number | null;
|
||||
source_message_id: number | null;
|
||||
day_date: string;
|
||||
occurred_at: string;
|
||||
recorded_at: string;
|
||||
content: string;
|
||||
raw_excerpt: string | null;
|
||||
tags: string[];
|
||||
pinned: boolean;
|
||||
people: { id: number; title: string }[];
|
||||
places: { id: number; title: string }[];
|
||||
task_ids: number[];
|
||||
note_ids: number[];
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export async function getBriefingFeeds(): Promise<BriefingFeed[]> {
|
||||
const data = await apiGet<BriefingFeed[]>('/api/briefing/feeds');
|
||||
return data;
|
||||
export async function getJournalConfig(): Promise<JournalConfig> {
|
||||
return apiGet<JournalConfig>('/api/journal/config');
|
||||
}
|
||||
|
||||
export async function createBriefingFeed(url: string, category?: string): Promise<BriefingFeed> {
|
||||
const body: Record<string, string> = { url };
|
||||
if (category?.trim()) body.category = category.trim();
|
||||
const data = await apiPost<{ id: number; url: string; title: string; category: string | null }>('/api/briefing/feeds', body);
|
||||
return { ...data, last_fetched_at: null };
|
||||
export async function saveJournalConfig(config: JournalConfig): Promise<void> {
|
||||
await apiPut('/api/journal/config', config);
|
||||
}
|
||||
|
||||
export async function refreshBriefingFeeds(): Promise<{ feeds_refreshed: number; new_items: number }> {
|
||||
return apiPost('/api/briefing/feeds/refresh', {});
|
||||
export async function getJournalToday(): Promise<JournalDayPayload> {
|
||||
return apiGet<JournalDayPayload>('/api/journal/today');
|
||||
}
|
||||
|
||||
export async function deleteBriefingFeed(id: number): Promise<void> {
|
||||
await apiDelete(`/api/briefing/feeds/${id}`);
|
||||
export async function getJournalDay(isoDate: string): Promise<JournalDayPayload> {
|
||||
return apiGet<JournalDayPayload>(`/api/journal/day/${isoDate}`);
|
||||
}
|
||||
|
||||
export async function getBriefingConversations(): Promise<BriefingConversation[]> {
|
||||
const data = await apiGet<{ conversations: BriefingConversation[] }>('/api/briefing/conversations');
|
||||
return data.conversations;
|
||||
export async function getJournalDays(): Promise<string[]> {
|
||||
const data = await apiGet<{ days: string[] }>('/api/journal/days');
|
||||
return data.days;
|
||||
}
|
||||
|
||||
export async function getBriefingToday(): Promise<{ id: number; title: string; messages: BriefingMessage[] }> {
|
||||
return apiGet('/api/briefing/conversations/today');
|
||||
export async function triggerJournalPrep(date?: string): Promise<{ ok: boolean; message_id: number }> {
|
||||
return apiPost('/api/journal/trigger-prep', date ? { date } : {});
|
||||
}
|
||||
|
||||
export async function getBriefingConvMessages(id: number): Promise<BriefingMessage[]> {
|
||||
const data = await apiGet<{ messages: BriefingMessage[] }>(`/api/briefing/conversations/${id}/messages`);
|
||||
return data.messages;
|
||||
export async function listJournalMoments(params: Record<string, string | number | boolean> = {}): Promise<JournalMoment[]> {
|
||||
const qs = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(params)) qs.set(k, String(v));
|
||||
const data = await apiGet<{ moments: JournalMoment[] }>(`/api/journal/moments?${qs}`);
|
||||
return data.moments;
|
||||
}
|
||||
|
||||
export async function triggerBriefingSlot(slot: string): Promise<void> {
|
||||
await apiPost('/api/briefing/trigger', { slot });
|
||||
export async function updateJournalMoment(id: number, patch: Partial<JournalMoment>): Promise<JournalMoment> {
|
||||
return apiPatch<JournalMoment>(`/api/journal/moments/${id}`, patch);
|
||||
}
|
||||
|
||||
export async function postRssReaction(
|
||||
rssItemId: number,
|
||||
reaction: 'up' | 'down'
|
||||
): Promise<{ ok: boolean; action: string }> {
|
||||
return apiPost('/api/briefing/rss-reactions', { rss_item_id: rssItemId, reaction });
|
||||
}
|
||||
|
||||
export async function deleteRssReaction(rssItemId: number): Promise<void> {
|
||||
return apiDelete(`/api/briefing/rss-reactions/${rssItemId}`);
|
||||
export async function deleteJournalMoment(id: number): Promise<void> {
|
||||
await apiDelete(`/api/journal/moments/${id}`);
|
||||
}
|
||||
|
||||
export async function openArticleInChat(
|
||||
@@ -434,7 +410,7 @@ export async function openArticleInChat(
|
||||
|
||||
export async function geocodeAddress(address: string): Promise<{ lat: number; lon: number; display_name: string } | null> {
|
||||
try {
|
||||
const r = await apiPost<{ lat: number; lon: number; label: string }>('/api/briefing/weather/geocode', { query: address });
|
||||
const r = await apiPost<{ lat: number; lon: number; label: string }>('/api/geocode', { query: address });
|
||||
return { lat: r.lat, lon: r.lon, display_name: r.label };
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user