Add searchable index health and repair tools
This commit is contained in:
@@ -4,7 +4,7 @@ const clientOf = db => typeof db?.execute === 'function' ? db : db?.db;
|
||||
const parse = value => { try { return JSON.parse(value || '{}'); } catch { return {}; } };
|
||||
const number = value => Number(value) || 0;
|
||||
|
||||
export function registerAdminToolsRoutes(app, { db, adminAuth, media = null, backupDir = null }) {
|
||||
export function registerAdminToolsRoutes(app, { db, adminAuth, media = null, backupDir = null, backfillStep = null }) {
|
||||
const sql = clientOf(db);
|
||||
|
||||
app.get('/api/admin/lyrics/quality', adminAuth, async c => {
|
||||
@@ -97,4 +97,53 @@ export function registerAdminToolsRoutes(app, { db, adminAuth, media = null, bac
|
||||
return c.json({ ok: true, pinned: body.pinned });
|
||||
});
|
||||
|
||||
app.get('/api/admin/index/health', adminAuth, async c => {
|
||||
const [stats, sources, latest, backfill] = await Promise.all([
|
||||
sql.execute(`SELECT COUNT(*) AS total,SUM(CASE WHEN url IS NOT NULL AND url<>'' THEN 1 ELSE 0 END) AS urls,
|
||||
SUM(CASE WHEN EXISTS(SELECT 1 FROM video_details d WHERE d.video_id=video_meta.id) THEN 1 ELSE 0 END) AS details,
|
||||
SUM(CASE WHEN EXISTS(SELECT 1 FROM video_thumbnails t WHERE t.video_id=video_meta.id AND t.data IS NOT NULL) THEN 1 ELSE 0 END) AS thumbnails FROM video_meta`),
|
||||
sql.execute('SELECT source,COUNT(*) AS videos,SUM(discoveries) AS discoveries,MAX(last_seen) AS lastSeen FROM video_meta_sources GROUP BY source ORDER BY videos DESC'),
|
||||
sql.execute('SELECT id,status,created_at,updated_at,state,error FROM deep_search_jobs ORDER BY created_at DESC LIMIT 1'),
|
||||
sql.execute('SELECT source,cursor,ceiling FROM catalog_backfill ORDER BY source'),
|
||||
]);
|
||||
const row = stats.rows[0] || {}, total = number(row.total);
|
||||
const last = latest.rows[0];
|
||||
return c.json({ ok: true, total, urlCount: number(row.urls), detailCount: number(row.details), thumbnailCount: number(row.thumbnails),
|
||||
urlCoverage: total ? number(row.urls) / total : 0, detailCoverage: total ? number(row.details) / total : 0, thumbnailCoverage: total ? number(row.thumbnails) / total : 0,
|
||||
sources: sources.rows.map(x => ({ source: x.source, videos: number(x.videos), discoveries: number(x.discoveries), lastSeen: number(x.lastSeen) })),
|
||||
lastRun: last ? { id: last.id, status: last.status, createdAt: number(last.created_at), updatedAt: number(last.updated_at), error: last.error || null, progress: parse(last.state) } : null,
|
||||
backfill: backfill.rows.map(x => ({ source: x.source, cursor: number(x.cursor), ceiling: number(x.ceiling), done: number(x.cursor) >= number(x.ceiling) })) }, 200, { 'Cache-Control': 'no-store' });
|
||||
});
|
||||
|
||||
app.post('/api/admin/index/backfill', adminAuth, async c => {
|
||||
if (!backfillStep) return c.json({ ok: false, error: 'Catalog backfill is unavailable.' }, 503);
|
||||
const processed = await backfillStep(100);
|
||||
const state = (await sql.execute('SELECT source,cursor,ceiling FROM catalog_backfill ORDER BY source')).rows;
|
||||
await audit(db, 'admin', 'index.backfill', '', { processed });
|
||||
return c.json({ ok: true, processed, backfill: state.map(x => ({ source: x.source, cursor: number(x.cursor), ceiling: number(x.ceiling), done: number(x.cursor) >= number(x.ceiling) })) });
|
||||
});
|
||||
|
||||
app.post('/api/admin/index/cleanup', adminAuth, async c => {
|
||||
let body = {}; try { body = await c.req.json(); } catch {}
|
||||
const days = Math.max(180, Math.min(3650, Number(body.days) || 365));
|
||||
const cutoff = Date.now() - days * 86400_000;
|
||||
const candidates = await sql.execute({ sql: `SELECT COUNT(*) AS n FROM video_meta m WHERE m.updated_at<?
|
||||
AND NOT EXISTS(SELECT 1 FROM video_meta_sources s WHERE s.video_id=m.id)
|
||||
AND NOT EXISTS(SELECT 1 FROM media_cache c WHERE c.video_id=m.id)
|
||||
AND NOT EXISTS(SELECT 1 FROM listening_daily l WHERE l.video_id=m.id)`, args: [cutoff] });
|
||||
const removed = number(candidates.rows[0]?.n);
|
||||
await sql.execute({ sql: `DELETE FROM video_meta WHERE updated_at<?
|
||||
AND NOT EXISTS(SELECT 1 FROM video_meta_sources s WHERE s.video_id=video_meta.id)
|
||||
AND NOT EXISTS(SELECT 1 FROM media_cache c WHERE c.video_id=video_meta.id)
|
||||
AND NOT EXISTS(SELECT 1 FROM listening_daily l WHERE l.video_id=video_meta.id)`, args: [cutoff] });
|
||||
await sql.batch([
|
||||
'DELETE FROM video_meta_sources WHERE video_id NOT IN (SELECT id FROM video_meta)',
|
||||
'DELETE FROM video_thumbnails WHERE video_id NOT IN (SELECT id FROM video_meta)',
|
||||
'DELETE FROM video_channels WHERE video_id NOT IN (SELECT id FROM video_meta)',
|
||||
'DELETE FROM video_details WHERE video_id NOT IN (SELECT id FROM video_meta)',
|
||||
], 'write');
|
||||
await audit(db, 'admin', 'index.cleanup', '', { days, removed });
|
||||
return c.json({ ok: true, removed, days });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user