openvibers/openvibe.media Commits

Pastes: expose an AI work queue and a write-back route

OP OpenVibers committed 19 Aug 2026 · cae7bef657 · 1 file, +33 −0

Paste AI stopped at the migration. Pastes moved here, but the analysing app kept
querying its OWN pastes table — which no longer receives rows — so everything
created since had no summary: 274 legacy pastes carry AI, and nothing after
id 509 does. There was also no route to store a summary even if one existed.

Media has no LLM, so it hands the owning app a queue instead:
  GET  /pastes?needs_ai=1   pastes with no ai_summary (app-key only)
  POST /pastes/:slug/ai     store ai_summary + ai_tags (app-key only)

needs_ai deliberately spans visibility, so it is app-key only and can never be
reached with a user token.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/server/pastes/routes.js b/server/pastes/routes.js
iindex 8304edc..9337a0e 100644
---- a/server/pastes/routes.js
++++ b/server/pastes/routes.js
@@@ -269,6 +269,12 @@ router.get('/', tenantAuth({ allowUser: true }), (req, res) => {
if (type === 'paste' || type === 'screenshot') { sql += ` AND type = ?`; params.push(type); }
if (userId != null) { sql += ` AND user_id = ?`; params.push(userId); }
++ // Work queue for the owning app's AI pass. Media holds the pastes but has no LLM;
++ // the app polls this, analyses, and posts results back to /:slug/ai. App-key only —
++ // this deliberately spans visibility, so it must never be reachable by a user token.
++ if (req.authType === 'app' && (req.query.needs_ai === '1' || req.query.needs_ai === 'true')) {
++ sql = sql.replace(visClause, `AND COALESCE(ai_summary, '') = ''`);
++ }
if (search) { sql += ` AND (title LIKE ? OR content LIKE ?)`; params.push(search, search); }
const dir = req.query.sort === 'oldest' ? 'ASC' : 'DESC';
@@@ -533,6 +539,33 @@ router.post('/', tenantAuth({ allowUser: true }), screenshotUpload.single('scree
}
});
++/**
++ * Store AI results for a paste. App-key only: Media owns the pastes but has no LLM, so
++ * the owning app analyses and writes back here.
++ *
++ * Paste AI silently stopped at the migration — the analysis job still queried the app's
++ * OWN pastes table, which no longer receives rows, so every paste created since had no
++ * summary. There was no route to write one either; this is it.
++ */
++router.post('/:slug/ai', tenantAuth(), (req, res) => {
++ try {
++ const paste = _getPasteScoped(req, res);
++ if (!paste) return;
++ const { ai_summary, ai_tags } = req.body || {};
++ const summary = ai_summary == null ? null : String(ai_summary).slice(0, 2000);
++ const tags = ai_tags == null ? null
++ : (typeof ai_tags === 'string' ? ai_tags : JSON.stringify(ai_tags)).slice(0, 2000);
++ db.run(
++ 'UPDATE pastes SET ai_summary = ?, ai_tags = ?, ai_analyzed_at = CURRENT_TIMESTAMP WHERE id = ?',
++ [summary, tags, paste.id]
++ );
++ res.json({ ok: true, paste: pastePublic(db.getPasteBySlug(paste.slug, req.appId)) });
++ } catch (err) {
++ console.error('[Pastes] AI update error:', err.message);
++ res.status(500).json({ error: 'Failed to store paste AI' });
++ }
++});
++
// ── Update paste ────────────────────────────────────────────
router.put('/:slug', tenantAuth({ allowUser: true }), (req, res) => {
try {