From 615b86fe27abde87c21f745f19b4300a96eaa9fe 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 ecdd2a5..0ec1887 100644 --- a/backend/src/api/admin.ts +++ b/backend/src/api/admin.ts @@ -2,6 +2,7 @@ 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 stocksDb from '../storage/db/stocks.js'; import * as bookmarksDb from '../storage/db/bookmarks.js'; @@ -9,6 +10,7 @@ import * as poe2WatchlistDb from '../storage/db/poe2Watchlist.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'; @@ -183,6 +185,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 d1f4010..22bbeb1 100644 --- a/frontend/src/lib/adminApi.ts +++ b/frontend/src/lib/adminApi.ts @@ -15,7 +15,8 @@ import type { Poe2BrowseEntry, AdminPoe2Entry, PipelineStats, - ModelContextInfo + ModelContextInfo, + ForceRecapResult } from './adminTypes'; async function request(path: string, options: RequestInit = {}, fetchFn: typeof fetch = fetch): Promise { @@ -133,6 +134,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 e0a599d..f7041c9 100644 --- a/frontend/src/lib/adminTypes.ts +++ b/frontend/src/lib/adminTypes.ts @@ -186,6 +186,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 @@