Add song-level report triage
This commit is contained in:
10
server/db.js
10
server/db.js
@@ -493,11 +493,15 @@ export async function listFlagsForVideo(videoId, status = 'open') {
|
||||
}
|
||||
|
||||
// Newest first across every video; status 'all' returns both.
|
||||
export async function listAllFlags({ status = 'open', limit = 200 } = {}) {
|
||||
const where = status === 'all' ? '' : 'WHERE status = ?';
|
||||
export async function listAllFlags({ status = 'open', limit = 200, olderThan = 0 } = {}) {
|
||||
const filters = [];
|
||||
const args = [];
|
||||
if (status !== 'all') { filters.push('status = ?'); args.push(status); }
|
||||
if (Number(olderThan) > 0) { filters.push('created_at <= ?'); args.push(Math.floor(Number(olderThan))); }
|
||||
const where = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
|
||||
const r = await db.execute({
|
||||
sql: `SELECT * FROM lyric_flags ${where} ORDER BY created_at DESC, id DESC LIMIT ?`,
|
||||
args: status === 'all' ? [limit] : [status, limit],
|
||||
args: [...args, limit],
|
||||
});
|
||||
return r.rows.map(flagRow);
|
||||
}
|
||||
|
||||
@@ -762,12 +762,24 @@ export function registerNoteRoutes(app, deps) {
|
||||
|
||||
app.get('/api/admin/flags', requireAdminOrToken, async (c) => {
|
||||
const status = ['open', 'resolved', 'all'].includes(c.req.query('status')) ? c.req.query('status') : 'open';
|
||||
const olderThanDays = Math.max(0, Math.min(3650, Number(c.req.query('olderThanDays')) || 0));
|
||||
const olderThan = olderThanDays ? Math.floor(Date.now() / 1000) - olderThanDays * 86400 : 0;
|
||||
const cache = new Map();
|
||||
const flags = [];
|
||||
for (const f of await db.listAllFlags({ status, limit: 300 })) {
|
||||
for (const f of await db.listAllFlags({ status, limit: 300, olderThan })) {
|
||||
flags.push({ ...f, ...(await songLabel(f.videoId, cache)) });
|
||||
}
|
||||
return c.json({ ok: true, open: await db.countOpenFlags(), flags });
|
||||
return c.json({ ok: true, open: await db.countOpenFlags(), flags, olderThanDays });
|
||||
});
|
||||
|
||||
app.post('/api/admin/flags/song/:videoId/resolve', requireAdmin, async (c) => {
|
||||
const videoId = c.req.param('videoId');
|
||||
if (badId(videoId)) return c.json({ ok: false, error: 'invalid video id' }, 400);
|
||||
const flags = await db.listFlagsForVideo(videoId, 'open');
|
||||
let resolved = 0;
|
||||
for (const flag of flags) if (await db.setFlagStatus(flag.id, 'resolved', 'admin')) resolved++;
|
||||
if (resolved) await audit(db, 'admin', 'report.song.resolve', videoId, { resolved });
|
||||
return c.json({ ok: true, resolved, open: await db.countOpenFlags() });
|
||||
});
|
||||
|
||||
app.post('/api/admin/flags/:fid', requireAdmin, async (c) => {
|
||||
|
||||
@@ -284,3 +284,18 @@ test('admin song list includes metadata-only phone songs for lyrics lookup', asy
|
||||
expect((await app.request(`/api/admin/media/${id}/redownload`, { method: 'POST', headers: { Cookie: cookie } })).status).toBe(202);
|
||||
expect((await app.request('/api/admin/media/phoneSong01/redownload', { method: 'POST' })).status).toBe(401);
|
||||
});
|
||||
|
||||
test('report triage filters aging flags and resolves every open report for one song', async () => {
|
||||
const id = VID;
|
||||
const a = await dbmod.upsertFlag({ videoId: id, index: 0, text: 'Flagged line', rev: 1, reason: 'words', note: '', reporter: 'listener-a' });
|
||||
const b = await dbmod.upsertFlag({ videoId: id, index: 0, text: 'Flagged line', rev: 1, reason: 'typo', note: '', reporter: 'listener-b' });
|
||||
await dbmod.db.execute({ sql: 'UPDATE lyric_flags SET created_at=? WHERE id IN (?,?)', args: [Math.floor(Date.now() / 1000) - 10 * 86400, a.id, b.id] });
|
||||
const cookie = await adminCookie();
|
||||
const aged = await (await app.request('/api/admin/flags?status=open&olderThanDays=7', { headers: { Cookie: cookie } })).json();
|
||||
expect(aged.flags.filter(flag => flag.videoId === id)).toHaveLength(2);
|
||||
const unauthorized = await app.request(`/api/admin/flags/song/${id}/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
expect(unauthorized.status).toBe(401);
|
||||
const response = await app.request(`/api/admin/flags/song/${id}/resolve`, json('POST', {}, { Cookie: cookie }));
|
||||
expect(await response.json()).toMatchObject({ ok: true, resolved: 2 });
|
||||
expect((await dbmod.listFlagsForVideo(id, 'open'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user