From ef59b2e91ac694c60d1eced0d7a6665cbdef1b2c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 14:28:45 +0000 Subject: [PATCH] Add "Force recap now" button to tracked items Lets an admin trigger a tracked item's recap immediately instead of waiting out its recapIntervalHours cadence. Still summarizes only the real window of new constituent articles since the last recap (or the last 24h if never recapped), returning a friendly not-published reason when there's nothing new rather than fabricating content. --- backend/src/api/admin.ts | 35 ++++++++++++ frontend/src/lib/adminApi.ts | 9 ++- frontend/src/lib/adminTypes.ts | 9 +++ .../src/lib/components/admin/EventsTab.svelte | 55 ++++++++++++++++++- 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/backend/src/api/admin.ts b/backend/src/api/admin.ts index 7b83f3c..2f94ffc 100644 --- a/backend/src/api/admin.ts +++ b/backend/src/api/admin.ts @@ -2,11 +2,13 @@ import type { FastifyInstance } from 'fastify'; import * as settingsDb from '../storage/db/settings.js'; import * as sourcesDb from '../storage/db/sources.js'; import * as eventsDb from '../storage/db/events.js'; +import * as articlesDb from '../storage/db/articles.js'; import * as categoriesDb from '../storage/db/categories.js'; import * as installedWidgetsDb from '../storage/db/installedWidgets.js'; import { clearSourceContent, reissueSourceContent, reissueArticle, clearAllArticles, clearAllMedia } from '../storage/contentCascade.js'; import { totalStorageBytes } from '../storage/media/index.js'; import { OllamaProvider } from '../inference/ollama-provider.js'; +import { publishEventRecap } from '../pipeline/publish.js'; import { pollSourceNow } from '../ingestion/poller.js'; import { logger, listLogs } from '../storage/db/logs.js'; import * as backlogStats from '../queue/backlogStats.js'; @@ -187,6 +189,39 @@ export async function registerAdminRoutes(app: FastifyInstance) { return reply.code(204).send(); }); + // Forces one tracked item's recap to run right now, ignoring its recapIntervalHours + // cadence entirely (even if recaps are turned off for it) — for "I want a wrap-up + // right now" rather than waiting out the timer. Still summarizes the same real + // window eventsRecap.ts would (everything published since lastRecapAt, or the last + // 24h if it's never recapped) rather than some arbitrary admin-chosen range, and + // still requires that window to actually contain something — an AI call with zero + // source material to summarize would just hallucinate content it wasn't given. + app.post('/api/admin/events/:id/recap-now', async (req, reply) => { + const { id } = req.params as { id: string }; + const event = eventsDb.getEvent(id); + if (!event) return reply.code(404).send({ error: 'not found' }); + if (event.sourceIds.length === 0) { + return { published: false, reason: 'No sources assigned to this item yet.' }; + } + + const since = event.lastRecapAt ?? new Date(Date.now() - 24 * 3600_000).toISOString(); + const constituents = articlesDb.articlesForEventSince(event.id, since); + if (constituents.length === 0) { + return { published: false, reason: 'Nothing new published under this item since its last recap.' }; + } + + const settings = settingsDb.getSettings(); + const provider = new OllamaProvider(settings.aiServiceHost, settings.aiServicePort); + try { + const article = await publishEventRecap(provider, settings, event, constituents); + eventsDb.markRecapped(event.id); + logger.info('events', `Manually forced recap for "${event.name}" from ${constituents.length} article(s)`); + return { published: true, title: article.title }; + } catch (err) { + return reply.code(502).send({ error: `Recap failed: ${(err as Error).message}` }); + } + }); + // --- Models / AI service (fetched live from the configured Ollama host) --- app.get('/api/admin/models', async (_req, reply) => { const settings = settingsDb.getSettings(); diff --git a/frontend/src/lib/adminApi.ts b/frontend/src/lib/adminApi.ts index a77d98d..6043012 100644 --- a/frontend/src/lib/adminApi.ts +++ b/frontend/src/lib/adminApi.ts @@ -18,7 +18,8 @@ import type { InstalledWidget, WidgetUploadManifest, PipelineStats, - ModelContextInfo + ModelContextInfo, + ForceRecapResult } from './adminTypes'; async function request(path: string, options: RequestInit = {}, fetchFn: typeof fetch = fetch): Promise { @@ -136,6 +137,12 @@ export const updateEvent = (id: string, patch: Partial, fetch export const deleteEvent = (id: string, fetchFn?: typeof fetch) => request(`/api/admin/events/${id}`, { method: 'DELETE' }, fetchFn); +// Runs this item's recap immediately, ignoring its recapIntervalHours cadence — still +// summarizes the same real window (everything since lastRecapAt) and still requires +// there to actually be something new to summarize (see the backend route). +export const forceRecap = (id: string, fetchFn?: typeof fetch) => + request(`/api/admin/events/${id}/recap-now`, { method: 'POST' }, fetchFn); + // Models / AI service export const getModels = (fetchFn?: typeof fetch) => request('/api/admin/models', {}, fetchFn); diff --git a/frontend/src/lib/adminTypes.ts b/frontend/src/lib/adminTypes.ts index a94956b..5c905e5 100644 --- a/frontend/src/lib/adminTypes.ts +++ b/frontend/src/lib/adminTypes.ts @@ -199,6 +199,15 @@ export interface AdminTrackedEvent { recapCustomInstructions: string; } +/** Response from POST /api/admin/events/:id/recap-now. */ +export interface ForceRecapResult { + published: boolean; + /** Set when published is true. */ + title?: string; + /** Set when published is false — why nothing was generated (no sources assigned, nothing new since last recap). */ + reason?: string; +} + export interface ModelCatalog { embedding: string[]; image: string[]; diff --git a/frontend/src/lib/components/admin/EventsTab.svelte b/frontend/src/lib/components/admin/EventsTab.svelte index 44a6a6a..80d5c91 100644 --- a/frontend/src/lib/components/admin/EventsTab.svelte +++ b/frontend/src/lib/components/admin/EventsTab.svelte @@ -1,6 +1,6 @@