import { avatarUrl } from '$lib/avatar'; import { recordIsacAudit } from '$lib/server/audit'; import { getPermissions, requirePermission } from '$lib/server/auth/authorization'; import { getSessionUser } from '$lib/server/auth/session'; import { getDatabase } from '$lib/server/database'; import { renderUserMarkdown } from '$lib/markdown'; import { createNotification } from '$lib/server/notifications'; import { getPagination, getRegistrySearch, getRegistrySort } from '$lib/server/pagination'; import { error, fail } from '@sveltejs/kit'; import { randomUUID } from 'node:crypto'; import type { Actions, PageServerLoad } from './$types'; import { agentProfileHref } from '$lib/agents'; const COMMENT_PERMISSION = 'community.comments.moderate'; const PROFILE_PERMISSION = 'community.profiles.moderate'; const REMOVED_RETENTION_DAYS = 30; const has = (permissions: string[], permission: string) => permissions.includes('*') || permissions.includes(permission); const actionFailure = (status: number, message: string) => fail(status, { communityError: message, communityResultToken: randomUUID() }); const requiredReason = (value: FormDataEntryValue | null) => { const reason = String(value ?? '').normalize('NFKC').trim().slice(0, 500); return reason.length >= 3 ? reason : null; }; const targetHref = (targetType: string, targetId: unknown, slug: unknown, publicSlug?: unknown) => targetType === 'profile' ? agentProfileHref({ id: String(targetId), publicSlug: typeof publicSlug === 'string' ? publicSlug : null }, 'profile-comments') : `/builds/${encodeURIComponent(String(slug ?? ''))}#comments`; const parseAuditSnapshot = (value: unknown) => { if (typeof value !== 'string') return null; try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record : null; } catch { return null; } }; type QueueContentType = 'comment' | 'profile_bio' | 'guide'; type QueueSource = 'scan' | 'report'; const contentPermission = (contentType: QueueContentType) => contentType === 'profile_bio' ? PROFILE_PERMISSION : COMMENT_PERMISSION; const parseJsonObject = (value: unknown) => { if (typeof value !== 'string') return {}; try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record : {}; } catch { return {}; } }; const parseJsonArray = (value: unknown) => { if (typeof value !== 'string') return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }; const queueTargetHref = ( contentType: QueueContentType, contentId: unknown, targetType: unknown, targetId: unknown, slug: unknown, publicSlug?: unknown ) => { if (contentType === 'profile_bio') { return agentProfileHref({ id: String(contentId), publicSlug: typeof publicSlug === 'string' ? publicSlug : null }); } if (contentType === 'guide') { return slug ? `/builds/${encodeURIComponent(String(slug))}` : `/builds/${encodeURIComponent(String(contentId))}`; } return targetHref(String(targetType), targetId, slug, publicSlug); }; const queueActionFailure = (status: number, message: string) => fail(status, { communityQueueError: message, communityResultToken: randomUUID() }); const allowedQueueTypes = (canModerateComments: boolean, canModerateProfiles: boolean) => [ ...(canModerateComments ? ['comment', 'guide'] as QueueContentType[] : []), ...(canModerateProfiles ? ['profile_bio'] as QueueContentType[] : []) ]; const assertQueuePermission = async ( operator: NonNullable>>, contentType: QueueContentType ) => { const granted = await getPermissions(operator); if (!has(granted, contentPermission(contentType))) { error(403, contentType === 'profile_bio' ? 'Profile moderation permission required' : 'Comment moderation permission required'); } }; const scanQueueRecord = async (database: Awaited>, id: string) => (await database.prepare(`SELECT scan.*,revision.body_markdown AS revision_body, comments.body_markdown AS comment_body,comments.status AS comment_status, comments.thread_id,thread.target_type,thread.target_id,target_build.slug,target_build.title AS target_title, target_build.guide_body AS current_guide_body,target_build.visibility AS current_guide_visibility, target_build.owner_id AS guide_owner_id, profile.bio_markdown AS current_bio,profile.bio_updated_at AS current_bio_updated_at, profile.updated_at AS profile_updated_at,COALESCE(profile.display_name,users.username) AS display_name FROM content_moderation_scans scan LEFT JOIN content_moderation_reviews review ON review.scan_id=scan.id LEFT JOIN moderated_content_revisions revision ON revision.id=scan.revision_id LEFT JOIN comments ON scan.content_type='comment' AND comments.id=scan.content_id LEFT JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN builds target_build ON (scan.content_type='guide' AND target_build.id=scan.content_id) OR (scan.content_type='comment' AND thread.target_type IN ('build','guide') AND target_build.id=thread.target_id) LEFT JOIN user_profiles profile ON profile.user_id=scan.author_id LEFT JOIN users ON users.id=scan.author_id WHERE scan.id=? AND scan.content_type IN ('comment','profile_bio','guide') AND scan.status IN ('review','priority_review','error') AND review.id IS NULL`)).get(id); const reportQueueRecord = async (database: Awaited>, id: string) => (await database.prepare(`SELECT report.*, comments.body_markdown AS comment_body,comments.status AS comment_status, comments.author_id AS comment_author_id,comments.thread_id, thread.target_type,thread.target_id,target_build.slug,target_build.title AS target_title, target_build.guide_body AS current_guide_body,target_build.visibility AS current_guide_visibility, target_build.owner_id AS guide_owner_id, profile.bio_markdown AS current_bio,profile.is_public AS profile_is_public, COALESCE(moderation.bio_hidden,0) AS bio_hidden FROM user_content_reports report LEFT JOIN comments ON report.content_type='comment' AND comments.id=report.content_id LEFT JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN builds target_build ON (report.content_type='guide' AND target_build.id=report.content_id) OR (report.content_type='comment' AND thread.target_type IN ('build','guide') AND target_build.id=thread.target_id) LEFT JOIN user_profiles profile ON report.content_type='profile_bio' AND profile.user_id=report.content_id LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id WHERE report.id=? AND report.status='pending' AND report.content_type IN ('comment','profile_bio','guide')`)).get(id); export const load: PageServerLoad = async ({ cookies, url }) => { const operator = await getSessionUser(cookies); if (!operator) error(401, 'Sign in required'); const granted = await getPermissions(operator); const canModerateComments = has(granted, COMMENT_PERMISSION); const canModerateProfiles = has(granted, PROFILE_PERMISSION); if (!canModerateComments && !canModerateProfiles) error(403, 'Community Oversight permission required'); const requestedTab = url.searchParams.get('tab'); const tab = requestedTab === 'queue' ? 'queue' : requestedTab === 'bios' ? 'bios' : requestedTab === 'comments' ? 'comments' : 'queue'; if (tab === 'comments' && !canModerateComments) error(403, 'Comment moderation permission required'); if (tab === 'bios' && !canModerateProfiles) error(403, 'Profile moderation permission required'); const allowedStatuses = tab === 'queue' ? ['all', 'priority', 'automated', 'reported'] : tab === 'comments' ? ['all', 'visible', 'removed'] : ['all', 'visible', 'hidden']; const status = allowedStatuses.includes(String(url.searchParams.get('status'))) ? String(url.searchParams.get('status')) : tab === 'queue' ? 'all' : 'visible'; const { search, pattern } = getRegistrySearch(url); const database = await getDatabase(); const removedCutoff = new Date(Date.now() - REMOVED_RETENTION_DAYS * 86_400_000).toISOString(); const commentSummaryRows = canModerateComments ? await (await database.prepare(`SELECT status,COUNT(*) AS count FROM comments WHERE status='visible' OR (status='removed' AND removed_at>=?) GROUP BY status`)).all(removedCutoff) : []; const visibleBios = canModerateProfiles ? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id WHERE profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0 AND length(trim(profile.bio_markdown))>0`)).get())?.count ?? 0) : 0; const hiddenBios = canModerateProfiles ? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile JOIN profile_moderation moderation ON moderation.user_id=profile.user_id WHERE moderation.bio_hidden=1 AND length(trim(profile.bio_markdown))>0`)).get())?.count ?? 0) : 0; const commentSummary = Object.fromEntries( commentSummaryRows.map((row) => [String(row.status), Number(row.count)]) ); const queueTypes = allowedQueueTypes(canModerateComments, canModerateProfiles); const queueTypePlaceholders = queueTypes.map(() => '?').join(','); const [scanQueueSummary, reportQueueSummary] = await Promise.all([ (await database.prepare(`SELECT COUNT(*) AS total, SUM(CASE WHEN status='priority_review' THEN 1 ELSE 0 END) AS priority, SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors FROM content_moderation_scans scan WHERE scan.content_type IN (${queueTypePlaceholders}) AND scan.status IN ('review','priority_review','error') AND NOT EXISTS( SELECT 1 FROM content_moderation_reviews review WHERE review.scan_id=scan.id )`)).get(...queueTypes), (await database.prepare(`SELECT COUNT(*) AS total FROM user_content_reports WHERE content_type IN (${queueTypePlaceholders}) AND status='pending'`)).get(...queueTypes) ]); const queueSummary = { automated: Number(scanQueueSummary?.total ?? 0), reported: Number(reportQueueSummary?.total ?? 0), priority: Number(scanQueueSummary?.priority ?? 0) + Number(scanQueueSummary?.errors ?? 0) }; const summary = { queue: { ...queueSummary, total: queueSummary.automated + queueSummary.reported }, comments: { visible: Number(commentSummary.visible ?? 0), removed: Number(commentSummary.removed ?? 0) }, bios: { visible: visibleBios, hidden: hiddenBios } }; if (tab === 'queue') { const scanQueue = `SELECT scan.id AS queue_id,'scan' AS item_source,scan.content_type,scan.content_id, scan.author_id,COALESCE(author_profile.display_name,author.username,'Deleted Agent') AS display_name, author_profile.public_slug AS author_public_slug, author_avatar.source AS avatar_source,author_avatar.value AS avatar_value, author_avatar.updated_at AS avatar_updated_at, COALESCE(revision.body_markdown,comments.body_markdown,target_build.guide_body,'') AS body_markdown, scan.created_at AS queued_at,scan.status AS scan_status,scan.risk_level,scan.reason, NULL AS category,scan.provider,scan.model,scan.provider_request_id, scan.scores_json,scan.flags_json,scan.rule_matches_json,NULL AS reporter_name, NULL AS details,thread.target_type,thread.target_id, CASE WHEN scan.content_type='profile_bio' THEN COALESCE(author_profile.display_name,author.username,'Agent profile') WHEN scan.content_type='guide' THEN COALESCE(target_build.title,'Build guide') ELSE COALESCE(target_build.title,target_profile.display_name,target_user.username,'Community content') END AS target_title, target_build.slug,COALESCE(target_profile.public_slug,author_profile.public_slug) AS target_public_slug FROM content_moderation_scans scan LEFT JOIN content_moderation_reviews review ON review.scan_id=scan.id LEFT JOIN moderated_content_revisions revision ON revision.id=scan.revision_id LEFT JOIN comments ON scan.content_type='comment' AND comments.id=scan.content_id LEFT JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN builds target_build ON (scan.content_type='guide' AND target_build.id=scan.content_id) OR (scan.content_type='comment' AND thread.target_type IN ('build','guide') AND target_build.id=thread.target_id) LEFT JOIN users target_user ON scan.content_type='comment' AND thread.target_type='profile' AND target_user.id=thread.target_id LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id LEFT JOIN users author ON author.id=scan.author_id LEFT JOIN user_profiles author_profile ON author_profile.user_id=author.id LEFT JOIN user_avatars author_avatar ON author_avatar.user_id=author.id WHERE scan.content_type IN (${queueTypePlaceholders}) AND scan.status IN ('review','priority_review','error') AND review.id IS NULL`; const reportQueue = `SELECT report.id AS queue_id,'report' AS item_source,report.content_type,report.content_id, COALESCE(comments.author_id,profile.user_id,target_build.owner_id) AS author_id, COALESCE(author_profile.display_name,author.username,'Deleted Agent') AS display_name, author_profile.public_slug AS author_public_slug, author_avatar.source AS avatar_source,author_avatar.value AS avatar_value, author_avatar.updated_at AS avatar_updated_at, COALESCE(comments.body_markdown,profile.bio_markdown,target_build.guide_body,'') AS body_markdown, report.created_at AS queued_at,NULL AS scan_status,'reported' AS risk_level, COALESCE(report.details,'A community member requested Oversight review.') AS reason, report.category,NULL AS provider,NULL AS model,NULL AS provider_request_id, '{}' AS scores_json,'{}' AS flags_json,'[]' AS rule_matches_json, COALESCE(reporter_profile.display_name,reporter.username,'Community member') AS reporter_name, report.details,thread.target_type,thread.target_id, CASE WHEN report.content_type='profile_bio' THEN COALESCE(profile.display_name,author.username,'Agent profile') WHEN report.content_type='guide' THEN COALESCE(target_build.title,'Build guide') ELSE COALESCE(target_build.title,target_profile.display_name,target_user.username,'Community content') END AS target_title, target_build.slug,COALESCE(target_profile.public_slug,profile.public_slug) AS target_public_slug FROM user_content_reports report LEFT JOIN comments ON report.content_type='comment' AND comments.id=report.content_id LEFT JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN user_profiles profile ON report.content_type='profile_bio' AND profile.user_id=report.content_id LEFT JOIN builds target_build ON (report.content_type='guide' AND target_build.id=report.content_id) OR (report.content_type='comment' AND thread.target_type IN ('build','guide') AND target_build.id=thread.target_id) LEFT JOIN users target_user ON report.content_type='comment' AND thread.target_type='profile' AND target_user.id=thread.target_id LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id LEFT JOIN users author ON author.id=COALESCE(comments.author_id,profile.user_id,target_build.owner_id) LEFT JOIN user_profiles author_profile ON author_profile.user_id=author.id LEFT JOIN user_avatars author_avatar ON author_avatar.user_id=author.id LEFT JOIN users reporter ON reporter.id=report.reporter_id LEFT JOIN user_profiles reporter_profile ON reporter_profile.user_id=reporter.id WHERE report.content_type IN (${queueTypePlaceholders}) AND report.status='pending'`; const union = status === 'automated' ? scanQueue : status === 'reported' ? reportQueue : `${scanQueue} UNION ALL ${reportQueue}`; const args: unknown[] = status === 'automated' || status === 'reported' ? [...queueTypes] : [...queueTypes, ...queueTypes]; const conditions: string[] = []; if (status === 'priority') { conditions.push("item_source='scan' AND scan_status IN ('priority_review','error')"); } if (search) { conditions.push(`(body_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE OR display_name LIKE ? ESCAPE '\\' COLLATE NOCASE OR target_title LIKE ? ESCAPE '\\' COLLATE NOCASE OR COALESCE(reason,'') LIKE ? ESCAPE '\\' COLLATE NOCASE OR COALESCE(category,'') LIKE ? ESCAPE '\\' COLLATE NOCASE)`); args.push(pattern, pattern, pattern, pattern, pattern); } const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''; const total = Number((await (await database.prepare( `WITH queue_items AS (${union}) SELECT COUNT(*) AS count FROM queue_items ${where}` )).get(...args))?.count ?? 0); const pagination = getPagination(url, total); const rows = await (await database.prepare(`WITH queue_items AS (${union}) SELECT * FROM queue_items ${where} ORDER BY CASE WHEN scan_status='priority_review' THEN 0 WHEN scan_status='error' THEN 1 WHEN item_source='report' THEN 2 ELSE 3 END,queued_at DESC LIMIT ? OFFSET ?`)) .all(...args, pagination.perPage, pagination.offset); return { tab, status, search, summary, items: rows.map((row) => { const contentType = String(row.content_type) as QueueContentType; const scores = parseJsonObject(row.scores_json); const flags = parseJsonObject(row.flags_json); return { id: String(row.queue_id), source: String(row.item_source) as QueueSource, contentType, contentId: String(row.content_id), bodyHtml: renderUserMarkdown(row.body_markdown), queuedAt: String(row.queued_at), scanStatus: row.scan_status ? String(row.scan_status) : null, riskLevel: String(row.risk_level), reason: String(row.reason), category: row.category ? String(row.category) : null, provider: row.provider ? String(row.provider) : null, model: row.model ? String(row.model) : null, providerRequestId: row.provider_request_id ? String(row.provider_request_id) : null, reporterName: row.reporter_name ? String(row.reporter_name) : null, scores: Object.entries(scores) .map(([category, score]) => ({ category, score: Number(score) })) .filter((entry) => Number.isFinite(entry.score) && entry.score > 0) .sort((left, right) => right.score - left.score) .slice(0, 5), flags: Object.entries(flags) .filter(([, flagged]) => Boolean(flagged)) .map(([category]) => category), ruleMatches: parseJsonArray(row.rule_matches_json).flatMap((match) => { if (!match || typeof match !== 'object' || Array.isArray(match)) return []; const record = match as Record; return [{ label: String(record.label ?? record.kind ?? 'LOCAL RULE'), excerpt: String(record.excerpt ?? '') }]; }), targetType: contentType === 'comment' ? String(row.target_type ?? 'content') : contentType, targetTitle: String(row.target_title), targetHref: queueTargetHref( contentType, row.content_id, row.target_type, row.target_id, row.slug, row.target_public_slug ), author: { id: String(row.author_id ?? ''), displayName: String(row.display_name), publicSlug: row.author_public_slug ? String(row.author_public_slug) : null, avatarUrl: avatarUrl( row.avatar_source, row.avatar_value, String(row.author_id ?? ''), row.avatar_updated_at ) } }; }), pagination, permissions: { comments: canModerateComments, profiles: canModerateProfiles } }; } if (tab === 'comments') { const sort = getRegistrySort(url, { created: { asc: 'comments.created_at ASC', desc: 'comments.created_at DESC', defaultDirection: 'desc' }, updated: { asc: 'comments.updated_at ASC', desc: 'comments.updated_at DESC', defaultDirection: 'desc' }, author: { asc: 'display_name COLLATE NOCASE ASC', desc: 'display_name COLLATE NOCASE DESC' } }, 'created', 'desc'); const conditions: string[] = []; const args: unknown[] = []; if (status === 'all') { conditions.push("(comments.status='visible' OR (comments.status='removed' AND comments.removed_at>=?))"); args.push(removedCutoff); } else { conditions.push('comments.status=?'); args.push(status); } if (status === 'removed') { conditions.push('comments.removed_at>=?'); args.push(removedCutoff); } if (search) { conditions.push(`(comments.body_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE OR COALESCE(profile.display_name,users.username,'Deleted Agent') LIKE ? ESCAPE '\\' COLLATE NOCASE OR COALESCE(builds.title,target_profile.display_name,target_user.username,'') LIKE ? ESCAPE '\\' COLLATE NOCASE)`); args.push(pattern, pattern, pattern); } const joins = `FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN users ON users.id=comments.author_id LEFT JOIN user_profiles profile ON profile.user_id=users.id LEFT JOIN user_avatars avatar ON avatar.user_id=users.id LEFT JOIN builds ON builds.id=thread.target_id AND thread.target_type IN ('build','guide') LEFT JOIN users target_user ON target_user.id=thread.target_id AND thread.target_type='profile' LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id`; const where = `WHERE ${conditions.join(' AND ')}`; const total = Number((await (await database.prepare( `SELECT COUNT(*) AS count ${joins} ${where}` )).get(...args))?.count ?? 0); const pagination = getPagination(url, total); const rows = await (await database.prepare(`SELECT comments.id,comments.body_markdown,comments.status, comments.created_at,comments.updated_at,comments.removal_reason, comments.author_id,COALESCE(profile.display_name,users.username,'Deleted Agent') AS display_name, profile.public_slug AS author_public_slug,target_profile.public_slug AS target_public_slug, avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at, thread.target_type,thread.target_id,builds.slug, COALESCE(builds.title,target_profile.display_name,target_user.username,'Community content') AS target_title ${joins} ${where} ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`)) .all(...args, pagination.perPage, pagination.offset); return { tab, status, search, summary, items: rows.map((row) => ({ id: String(row.id), bodyHtml: renderUserMarkdown(row.body_markdown), status: String(row.status), createdAt: String(row.created_at), updatedAt: String(row.updated_at), moderationReason: row.removal_reason ? String(row.removal_reason) : null, targetType: String(row.target_type), targetTitle: String(row.target_title), targetHref: targetHref(String(row.target_type), row.target_id, row.slug, row.target_public_slug), author: { id: String(row.author_id), displayName: String(row.display_name), publicSlug: row.author_public_slug ? String(row.author_public_slug) : null, avatarUrl: avatarUrl(row.avatar_source, row.avatar_value, String(row.author_id), row.avatar_updated_at) } })), pagination, sort: { key: sort.key, direction: sort.direction }, permissions: { comments: canModerateComments, profiles: canModerateProfiles } }; } const sort = getRegistrySort(url, { updated: { asc: 'profile.updated_at ASC', desc: 'profile.updated_at DESC', defaultDirection: 'desc' }, agent: { asc: 'display_name COLLATE NOCASE ASC', desc: 'display_name COLLATE NOCASE DESC' } }, 'updated', 'desc'); const conditions = [ status === 'all' ? "((profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0) OR moderation.bio_hidden=1)" : status === 'visible' ? 'profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0' : 'moderation.bio_hidden=1', 'length(trim(profile.bio_markdown))>0' ]; const args: unknown[] = []; if (search) { conditions.push(`(profile.bio_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE OR COALESCE(profile.display_name,users.username) LIKE ? ESCAPE '\\' COLLATE NOCASE)`); args.push(pattern, pattern); } const where = `WHERE ${conditions.join(' AND ')}`; const total = Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile JOIN users ON users.id=profile.user_id LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id ${where}`)).get(...args))?.count ?? 0); const pagination = getPagination(url, total); const rows = await (await database.prepare(`SELECT profile.user_id,profile.public_slug,profile.bio_markdown,profile.is_public, profile.updated_at,COALESCE(profile.display_name,users.username) AS display_name, avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at, moderation.reason AS moderation_reason FROM user_profiles profile JOIN users ON users.id=profile.user_id LEFT JOIN user_avatars avatar ON avatar.user_id=users.id LEFT JOIN profile_moderation moderation ON moderation.user_id=users.id ${where} ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`)) .all(...args, pagination.perPage, pagination.offset); return { tab, status, search, summary, items: rows.map((row) => ({ id: String(row.user_id), bodyHtml: renderUserMarkdown(row.bio_markdown), status, updatedAt: String(row.updated_at), moderationReason: row.moderation_reason ? String(row.moderation_reason) : null, agent: { id: String(row.user_id), displayName: String(row.display_name), publicSlug: row.public_slug ? String(row.public_slug) : null, avatarUrl: avatarUrl(row.avatar_source, row.avatar_value, String(row.user_id), row.avatar_updated_at) } })), pagination, sort: { key: sort.key, direction: sort.direction }, permissions: { comments: canModerateComments, profiles: canModerateProfiles } }; }; export const actions: Actions = { approveQueue: async ({ cookies, request }) => { const operator = await getSessionUser(cookies); if (!operator) return queueActionFailure(401, 'Sign in to review community content.'); const form = await request.formData(); const id = String(form.get('id') ?? '').trim(); const source = String(form.get('source') ?? '') as QueueSource; const reason = requiredReason(form.get('reason')); if (!id || !['scan', 'report'].includes(source) || !reason) { return queueActionFailure(400, 'Choose a queue item and provide a brief approval reason.'); } const database = await getDatabase(); if (source === 'report') { const report = await reportQueueRecord(database, id); if (!report) return queueActionFailure(404, 'That user report is no longer pending.'); const contentType = String(report.content_type) as QueueContentType; await assertQueuePermission(operator, contentType); const now = new Date().toISOString(); await database.exec('BEGIN IMMEDIATE'); try { const result = await (await database.prepare(`UPDATE user_content_reports SET status='dismissed',reviewed_at=?,reviewed_by=?,resolution_reason=? WHERE id=? AND status='pending'`)).run(now, operator.id, reason, id); if (!result.changes) { await database.exec('ROLLBACK'); return queueActionFailure(409, 'That report changed before it could be dismissed.'); } await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: 'content_report.dismissed', entityId: id, before: { status: 'pending', contentType, contentId: String(report.content_id), category: String(report.category), reporterId: String(report.reporter_id) }, after: { status: 'dismissed', reason, reviewedAt: now, reviewedBy: operator.id } }); await database.exec('COMMIT'); } catch (cause) { await database.exec('ROLLBACK'); throw cause; } return { communitySaved: true, communityResultToken: randomUUID() }; } const scan = await scanQueueRecord(database, id); if (!scan) return queueActionFailure(404, 'That automated review is no longer pending.'); const contentType = String(scan.content_type) as QueueContentType; await assertQueuePermission(operator, contentType); const body = contentType === 'comment' ? String(scan.comment_body ?? '') : String(scan.revision_body ?? ''); if (!body.trim()) return queueActionFailure(409, 'The queued content is no longer available.'); const now = new Date().toISOString(); const reviewId = randomUUID(); let targetOwnerId: string | null = null; await database.exec('BEGIN IMMEDIATE'); try { const claim = await (await database.prepare(`INSERT INTO content_moderation_reviews (id,scan_id,reviewer_id,decision,reason,created_at) SELECT ?,?,?,'approved',?,? WHERE NOT EXISTS(SELECT 1 FROM content_moderation_reviews WHERE scan_id=?)`)) .run(reviewId, id, operator.id, reason, now, id); if (!claim.changes) { await database.exec('ROLLBACK'); return queueActionFailure(409, 'Another reviewer already handled this scan.'); } if (contentType === 'comment') { const result = await (await database.prepare(`UPDATE comments SET status='visible',removed_at=NULL,removed_by=NULL,removal_reason=NULL,updated_at=? WHERE id=? AND status='pending_review'`)).run(now, scan.content_id); if (!result.changes) throw new Error('The pending comment changed before approval.'); targetOwnerId = String(scan.target_type) === 'profile' ? String(scan.target_id) : scan.guide_owner_id ? String(scan.guide_owner_id) : null; } else if (contentType === 'profile_bio') { await (await database.prepare(`INSERT INTO user_profiles (user_id,display_name,bio_markdown,is_public,bio_updated_at,updated_at) VALUES(?,?,?,1,?,?) ON CONFLICT(user_id) DO UPDATE SET bio_markdown=excluded.bio_markdown, bio_updated_at=excluded.bio_updated_at, updated_at=excluded.updated_at`)) .run(scan.content_id, String(scan.display_name ?? 'Agent'), body, now, now); await (await database.prepare(`UPDATE profile_moderation SET bio_hidden=0, reason=?,moderated_at=?,moderated_by=? WHERE user_id=?`)) .run(reason, now, operator.id, scan.content_id); } else { const result = await (await database.prepare(`UPDATE builds SET guide_body=?,updated_at=? WHERE id=? AND owner_id=?`)) .run(body, now, scan.content_id, scan.author_id); if (!result.changes) throw new Error('The queued guide changed before approval.'); } await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: `${contentType}.review_approved`, entityId: String(scan.content_id), before: { scanId: id, status: String(scan.status), riskLevel: String(scan.risk_level), reason: String(scan.reason), policyVersion: Number(scan.policy_version), provider: String(scan.provider), model: String(scan.model), currentStatus: contentType === 'comment' ? String(scan.comment_status) : contentType === 'profile_bio' ? 'revision_pending' : 'revision_pending' }, after: { scanId: id, reviewId, decision: 'approved', reason, reviewedAt: now, reviewedBy: operator.id, published: true } }); await database.exec('COMMIT'); } catch (cause) { await database.exec('ROLLBACK'); if (cause instanceof Error && cause.message.includes('changed before approval')) { return queueActionFailure(409, cause.message); } throw cause; } const authorId = String(scan.author_id); const link = queueTargetHref( contentType, scan.content_id, scan.target_type, scan.target_id, scan.slug ); await createNotification({ userId: authorId, category: 'isac', source: 'ISAC COMMUNITY OVERSIGHT', title: contentType === 'comment' ? 'Comment approved' : contentType === 'profile_bio' ? 'Agent bio approved' : 'Guide approved', body: `Community Oversight approved your ${contentType === 'profile_bio' ? 'Agent bio' : contentType}. Reason: ${reason}`, link, actions: [{ label: 'VIEW CONTENT', href: link }], mandatory: true }); if (contentType === 'comment' && targetOwnerId && targetOwnerId !== authorId) { await createNotification({ userId: targetOwnerId, category: 'comments', source: 'agent', title: 'New comment', body: `${String(scan.display_name ?? 'An Agent')} posted a comment on your ${String(scan.target_type) === 'profile' ? 'Agent profile' : 'build'}.`, link, actions: [{ label: 'VIEW COMMENT', href: link }] }); } return { communitySaved: true, communityResultToken: randomUUID() }; }, rejectQueue: async ({ cookies, request }) => { const operator = await getSessionUser(cookies); if (!operator) return queueActionFailure(401, 'Sign in to review community content.'); const form = await request.formData(); const id = String(form.get('id') ?? '').trim(); const source = String(form.get('source') ?? '') as QueueSource; const reason = requiredReason(form.get('reason')); if (!id || !['scan', 'report'].includes(source) || !reason) { return queueActionFailure(400, 'Choose a queue item and provide a brief rejection reason.'); } const database = await getDatabase(); if (source === 'report') { const report = await reportQueueRecord(database, id); if (!report) return queueActionFailure(404, 'That user report is no longer pending.'); const contentType = String(report.content_type) as QueueContentType; await assertQueuePermission(operator, contentType); const authorId = contentType === 'comment' ? String(report.comment_author_id ?? '') : contentType === 'profile_bio' ? String(report.content_id) : String(report.guide_owner_id ?? ''); if (!authorId) return queueActionFailure(409, 'The reported content author is no longer available.'); const now = new Date().toISOString(); const before = { reportId: id, contentType, contentId: String(report.content_id), reportCategory: String(report.category), reporterId: String(report.reporter_id), contentStatus: contentType === 'comment' ? String(report.comment_status ?? 'missing') : contentType === 'profile_bio' ? Boolean(report.bio_hidden) ? 'hidden' : 'visible' : String(report.current_guide_visibility ?? 'missing') }; await database.exec('BEGIN IMMEDIATE'); try { if (contentType === 'comment') { const result = await (await database.prepare(`UPDATE comments SET status='removed', removed_at=?,removed_by=?,removal_reason=?,updated_at=? WHERE id=? AND status='visible'`)) .run(now, operator.id, reason, now, report.content_id); if (!result.changes) throw new Error('The reported comment is no longer visible.'); } else if (contentType === 'profile_bio') { if (!String(report.current_bio ?? '').trim() || Boolean(report.bio_hidden)) { throw new Error('The reported biography is no longer visible.'); } await (await database.prepare(`INSERT INTO profile_moderation (user_id,bio_hidden,reason,moderated_at,moderated_by) VALUES(?,1,?,?,?) ON CONFLICT(user_id) DO UPDATE SET bio_hidden=1,reason=excluded.reason, moderated_at=excluded.moderated_at,moderated_by=excluded.moderated_by`)) .run(report.content_id, reason, now, operator.id); } else { if (!String(report.current_guide_body ?? '').trim() || report.current_guide_visibility === 'private') { throw new Error('The reported guide is no longer publicly available.'); } const result = await (await database.prepare(`UPDATE builds SET visibility='private',updated_at=? WHERE id=? AND visibility<>'private'`)) .run(now, report.content_id); if (!result.changes) throw new Error('The reported guide changed before it could be hidden.'); } const resolved = await (await database.prepare(`UPDATE user_content_reports SET status='resolved',reviewed_at=?,reviewed_by=?,resolution_reason=? WHERE content_type=? AND content_id=? AND status='pending'`)) .run(now, operator.id, reason, contentType, report.content_id); if (!resolved.changes) throw new Error('The pending reports changed before resolution.'); await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: contentType === 'comment' ? 'comment.report_removed' : contentType === 'profile_bio' ? 'profile_bio.report_hidden' : 'guide.report_hidden', entityId: String(report.content_id), before, after: { status: contentType === 'comment' ? 'removed' : 'hidden', reason, reviewedAt: now, reviewedBy: operator.id, resolvedReports: Number(resolved.changes) } }); await database.exec('COMMIT'); } catch (cause) { await database.exec('ROLLBACK'); if (cause instanceof Error && ( cause.message.includes('no longer') || cause.message.includes('changed before') )) return queueActionFailure(409, cause.message); throw cause; } const link = queueTargetHref( contentType, report.content_id, report.target_type, report.target_id, report.slug ); await createNotification({ userId: authorId, category: 'isac', source: 'ISAC COMMUNITY OVERSIGHT', title: contentType === 'comment' ? 'Comment removed' : contentType === 'profile_bio' ? 'Agent bio hidden' : 'Guide hidden', body: `Community Oversight ${contentType === 'comment' ? 'removed' : 'hid'} your ${contentType === 'profile_bio' ? 'Agent bio' : contentType}. Reason: ${reason}`, link: contentType === 'profile_bio' ? '/account' : link, actions: [{ label: 'REVIEW CONTENT', href: contentType === 'profile_bio' ? '/account' : link }], mandatory: true }); return { communitySaved: true, communityResultToken: randomUUID() }; } const scan = await scanQueueRecord(database, id); if (!scan) return queueActionFailure(404, 'That automated review is no longer pending.'); const contentType = String(scan.content_type) as QueueContentType; await assertQueuePermission(operator, contentType); const now = new Date().toISOString(); const reviewId = randomUUID(); await database.exec('BEGIN IMMEDIATE'); try { const claim = await (await database.prepare(`INSERT INTO content_moderation_reviews (id,scan_id,reviewer_id,decision,reason,created_at) SELECT ?,?,?,'rejected',?,? WHERE NOT EXISTS(SELECT 1 FROM content_moderation_reviews WHERE scan_id=?)`)) .run(reviewId, id, operator.id, reason, now, id); if (!claim.changes) { await database.exec('ROLLBACK'); return queueActionFailure(409, 'Another reviewer already handled this scan.'); } if (contentType === 'comment') { const result = await (await database.prepare(`UPDATE comments SET status='removed', removed_at=?,removed_by=?,removal_reason=?,updated_at=? WHERE id=? AND status='pending_review'`)) .run(now, operator.id, reason, now, scan.content_id); if (!result.changes) throw new Error('The pending comment changed before rejection.'); } await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: `${contentType}.review_rejected`, entityId: String(scan.content_id), before: { scanId: id, status: String(scan.status), riskLevel: String(scan.risk_level), reason: String(scan.reason), policyVersion: Number(scan.policy_version), provider: String(scan.provider), model: String(scan.model) }, after: { scanId: id, reviewId, decision: 'rejected', reason, reviewedAt: now, reviewedBy: operator.id, published: false } }); await database.exec('COMMIT'); } catch (cause) { await database.exec('ROLLBACK'); if (cause instanceof Error && cause.message.includes('changed before rejection')) { return queueActionFailure(409, cause.message); } throw cause; } await createNotification({ userId: String(scan.author_id), category: 'isac', source: 'ISAC COMMUNITY OVERSIGHT', title: contentType === 'comment' ? 'Comment rejected' : contentType === 'profile_bio' ? 'Agent bio revision rejected' : 'Guide revision rejected', body: `Community Oversight rejected your ${contentType === 'profile_bio' ? 'Agent bio revision' : contentType}. Reason: ${reason}`, link: '/account', actions: [{ label: 'REVIEW ACCOUNT', href: '/account' }], mandatory: true }); return { communitySaved: true, communityResultToken: randomUUID() }; }, removeComment: async ({ cookies, request }) => { const operator = await requirePermission(await getSessionUser(cookies), COMMENT_PERMISSION); const form = await request.formData(); const id = String(form.get('id') ?? '').trim(); const reason = requiredReason(form.get('reason')); if (!id || !reason) return actionFailure(400, 'Choose a comment and provide a brief removal reason.'); const database = await getDatabase(); const comment = await (await database.prepare(`SELECT comments.*,thread.target_type,thread.target_id,builds.slug FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN builds ON builds.id=thread.target_id AND thread.target_type IN ('build','guide') WHERE comments.id=? AND comments.status='visible'`)).get(id); if (!comment) return actionFailure(404, 'That visible comment no longer exists.'); const before = { status: 'visible', bodyMarkdown: String(comment.body_markdown), authorId: String(comment.author_id), threadId: String(comment.thread_id), targetType: String(comment.target_type), targetId: String(comment.target_id), updatedAt: String(comment.updated_at) }; const now = new Date().toISOString(); const result = await (await database.prepare(`UPDATE comments SET status='removed',removed_at=?, removed_by=?,removal_reason=?,updated_at=? WHERE id=? AND status='visible'`)) .run(now, operator.id, reason, now, id); if (!result.changes) return actionFailure(409, 'That comment changed before it could be removed.'); const after = { ...before, status: 'removed', removedAt: now, removedBy: operator.id, reason, updatedAt: now }; await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: 'comment.removed', entityId: id, before, after }); await createNotification({ userId: String(comment.author_id), category: 'isac', source: 'ISAC COMMUNITY OVERSIGHT', title: 'Comment removed', body: `Community Oversight removed one of your comments. Reason: ${reason}`, link: targetHref(String(comment.target_type), comment.target_id, comment.slug), actions: [{ label: 'VIEW DISCUSSION', href: targetHref(String(comment.target_type), comment.target_id, comment.slug) }], mandatory: true }); return { communitySaved: true, communityResultToken: randomUUID() }; }, restoreComment: async ({ cookies, request }) => { const operator = await requirePermission(await getSessionUser(cookies), COMMENT_PERMISSION); const form = await request.formData(); const id = String(form.get('id') ?? '').trim(); const reason = requiredReason(form.get('reason')); if (!id || !reason) return actionFailure(400, 'Choose a comment and provide a brief restoration reason.'); const database = await getDatabase(); const comment = await (await database.prepare(`SELECT * FROM comments WHERE id=? AND status='removed'`)).get(id); if (!comment) return actionFailure(404, 'That removed comment no longer exists.'); const audit = await (await database.prepare(`SELECT after_json FROM audit_log WHERE entity_type='community_moderation' AND entity_id=? AND action='comment.removed' ORDER BY created_at DESC LIMIT 1`)).get(id); const expected = parseAuditSnapshot(audit?.after_json); if ( !expected || expected.status !== 'removed' || String(expected.removedAt ?? '') !== String(comment.removed_at ?? '') || String(expected.updatedAt ?? '') !== String(comment.updated_at ?? '') ) return actionFailure(409, 'This comment was not removed by Community Oversight or changed after removal.'); const before = { status: 'removed', bodyMarkdown: String(comment.body_markdown), authorId: String(comment.author_id), threadId: String(comment.thread_id), removedAt: String(comment.removed_at), removedBy: String(comment.removed_by), removalReason: String(comment.removal_reason), updatedAt: String(comment.updated_at) }; const now = new Date().toISOString(); const result = await (await database.prepare(`UPDATE comments SET status='visible',removed_at=NULL, removed_by=NULL,removal_reason=NULL,updated_at=? WHERE id=? AND status='removed' AND updated_at=?`)) .run(now, id, comment.updated_at); if (!result.changes) return actionFailure(409, 'That comment changed before it could be restored.'); await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: 'comment.restored', entityId: id, before, after: { status: 'visible', bodyMarkdown: String(comment.body_markdown), authorId: String(comment.author_id), threadId: String(comment.thread_id), restorationReason: reason, updatedAt: now } }); return { communitySaved: true, communityResultToken: randomUUID() }; }, hideBio: async ({ cookies, request }) => { const operator = await requirePermission(await getSessionUser(cookies), PROFILE_PERMISSION); const form = await request.formData(); const id = String(form.get('id') ?? '').trim(); const reason = requiredReason(form.get('reason')); if (!id || !reason) return actionFailure(400, 'Choose a profile and provide a brief reason for hiding it.'); const database = await getDatabase(); const profile = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown, profile.is_public,profile.bio_updated_at,profile.updated_at, COALESCE(moderation.bio_hidden,0) AS bio_hidden,moderation.reason, moderation.moderated_at,moderation.moderated_by FROM user_profiles profile LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id WHERE profile.user_id=? AND profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0 AND length(trim(profile.bio_markdown))>0`)).get(id); if (!profile) return actionFailure(404, 'That visible profile biography no longer exists.'); const before = { bioMarkdown: String(profile.bio_markdown), isPublic: true, bioHidden: false, bioUpdatedAt: profile.bio_updated_at ? String(profile.bio_updated_at) : null, profileUpdatedAt: String(profile.updated_at), moderation: profile.moderated_at ? { reason: profile.reason ? String(profile.reason) : null, moderatedAt: String(profile.moderated_at), moderatedBy: profile.moderated_by ? String(profile.moderated_by) : null } : null }; const now = new Date().toISOString(); const result = await (await database.prepare(`INSERT INTO profile_moderation (user_id,bio_hidden,reason,moderated_at,moderated_by) VALUES(?,1,?,?,?) ON CONFLICT(user_id) DO UPDATE SET bio_hidden=1,reason=excluded.reason, moderated_at=excluded.moderated_at,moderated_by=excluded.moderated_by WHERE profile_moderation.bio_hidden=0`)).run(id, reason, now, operator.id); if (!result.changes) return actionFailure(409, 'That profile changed before it could be hidden.'); const after = { ...before, bioHidden: true, reason, moderatedAt: now, moderatedBy: operator.id }; await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: 'profile.hidden', entityId: id, before, after }); await createNotification({ userId: id, category: 'isac', source: 'ISAC COMMUNITY OVERSIGHT', title: 'Agent profile hidden', body: `Community Oversight hid your public Agent profile. Your biography is preserved but is not publicly visible. Reason: ${reason}`, link: '/account', actions: [{ label: 'REVIEW PROFILE', href: '/account' }], mandatory: true }); return { communitySaved: true, communityResultToken: randomUUID() }; }, restoreBio: async ({ cookies, request }) => { const operator = await requirePermission(await getSessionUser(cookies), PROFILE_PERMISSION); const form = await request.formData(); const id = String(form.get('id') ?? '').trim(); const reason = requiredReason(form.get('reason')); if (!id || !reason) return actionFailure(400, 'Choose a profile and provide a brief restoration reason.'); const database = await getDatabase(); const profile = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown, profile.is_public,profile.bio_updated_at,profile.updated_at, moderation.bio_hidden,moderation.reason,moderation.moderated_at,moderation.moderated_by FROM user_profiles profile JOIN profile_moderation moderation ON moderation.user_id=profile.user_id WHERE profile.user_id=? AND moderation.bio_hidden=1 AND length(trim(profile.bio_markdown))>0`)).get(id); if (!profile) return actionFailure(404, 'That hidden profile biography no longer exists.'); const audit = await (await database.prepare(`SELECT after_json FROM audit_log WHERE entity_type='community_moderation' AND entity_id=? AND action='profile.hidden' ORDER BY created_at DESC LIMIT 1`)).get(id); const expected = parseAuditSnapshot(audit?.after_json); if ( !expected || expected.bioHidden !== true || String(expected.bioMarkdown ?? '') !== String(profile.bio_markdown) || String(expected.bioUpdatedAt ?? '') !== String(profile.bio_updated_at ?? '') || String(expected.profileUpdatedAt ?? '') !== String(profile.updated_at) || String(expected.moderatedAt ?? '') !== String(profile.moderated_at) || String(expected.moderatedBy ?? '') !== String(profile.moderated_by) ) return actionFailure(409, 'This profile was not hidden by Community Oversight or changed after it was hidden.'); const before = { bioMarkdown: String(profile.bio_markdown), isPublic: Boolean(profile.is_public), bioHidden: true, bioUpdatedAt: profile.bio_updated_at ? String(profile.bio_updated_at) : null, profileUpdatedAt: String(profile.updated_at), reason: String(profile.reason), moderatedAt: String(profile.moderated_at), moderatedBy: String(profile.moderated_by) }; const now = new Date().toISOString(); const result = await (await database.prepare(`UPDATE profile_moderation SET bio_hidden=0,reason=?,moderated_at=?,moderated_by=? WHERE user_id=? AND bio_hidden=1 AND moderated_at=?`)) .run(reason, now, operator.id, id, profile.moderated_at); if (!result.changes) return actionFailure(409, 'That profile changed before it could be restored.'); await recordIsacAudit({ actorId: operator.id, category: 'community_moderation', action: 'profile.restored', entityId: id, before, after: { ...before, bioHidden: false, restorationReason: reason, moderatedAt: now, moderatedBy: operator.id } }); return { communitySaved: true, communityResultToken: randomUUID() }; } };