openvibers/openvibe.network Commits

Admin review queue: a row says which kind of review it already had (#612)

OP OpenVibers committed 6 days ago · 165c681263 · 2 files, +29 −4

Changes

ddiff --git a/server/admin/review-queue.js b/server/admin/review-queue.js
iindex e68ed991..b993b190 100644
---- a/server/admin/review-queue.js
++++ b/server/admin/review-queue.js
@@@ -75,12 +75,22 @@ function itemOf(raw, site, now) {
const revision = raw.revision == null ? null : Number(raw.revision);
if (revision != null && !Number.isInteger(revision)) return null;
const reasons = Array.isArray(raw.reasons) ? raw.reasons.filter((r) => r && typeof r.text === 'string').slice(0, 6).map((r) => ({ code: String(r.code || ''), text: r.text.slice(0, 300) })) : [];
++ // Which kind of review an item already had (a service adds this): the AI editor's decision, or a
++ // person's. Anything else is ignored — an item is still shown, just without the fact.
++ const kind = raw.reviewed && typeof raw.reviewed === 'object' ? raw.reviewed.kind : null;
++ const decision = raw.reviewed && typeof raw.reviewed === 'object' ? raw.reviewed.decision : null;
++ const reviewed = (kind === 'ai_editor' || kind === 'person') && (decision === 'approved' || decision === 'rejected') ? {
++ kind, decision,
++ by: typeof raw.reviewed.by === 'string' ? raw.reviewed.by.slice(0, 120) : null,
++ label: typeof raw.reviewed.label === 'string' ? raw.reviewed.label.slice(0, 300) : null,
++ } : null;
return {
site: site.id, kind: raw.kind, id: raw.id, title: raw.title.slice(0, 300),
revision, state: typeof raw.state === 'string' ? raw.state : null, ref: typeof raw.ref === 'string' ? raw.ref : null,
url: safeUrl(raw.url), editUrl: raw.editUrl == null ? null : safeUrl(raw.editUrl),
createdAt: createdAt.toISOString(), age: ageText(createdAt.toISOString(), now),
reasons: reasons.length ? reasons : [{ code: 'noindex_requested', text: 'Waiting for a person\'s review.' }],
++ reviewed,
};
}
@@@ -120,12 +130,21 @@ function itemRow(item, csrf) {
const href = item.editUrl || item.url;
const title = `<a href="${esc(safeUrl(href))}">${esc(item.title)}</a>`;
const why = item.reasons.map((r) => r.text).join(' · ');
++ // Which kind of review the item already had: a person's, or the AI editor's (with its date).
++ const who = item.reviewed ? (item.reviewed.kind === 'ai_editor' ? 'the AI editor' : 'a person') : null;
++ const reviewedText = item.reviewed
++ ? (item.reviewed.label || `${item.reviewed.decision === 'approved' ? 'Checked' : 'Kept hidden'} by ${who}`)
++ : '';
++ const meta = [
++ item.age ? `${item.age} waiting` : '',
++ who ? `last review by ${who}` : '',
++ ].filter(Boolean).join(' · ');
return kit.row({
titleHtml: title,
badge: kit.badge(kindOf(item.kind)),
-- meta: item.age ? `${item.age} waiting` : '',
++ meta,
metaIso: item.createdAt,
-- sub: why,
++ sub: [why, reviewedText].filter(Boolean).join(' · '),
trail: actionForms(item, csrf),
});
}
ddiff --git a/test/review-queue.test.js b/test/review-queue.test.js
iindex c04e9069..2e8ffdbc 100644
---- a/test/review-queue.test.js
++++ b/test/review-queue.test.js
@@@ -36,8 +36,11 @@ const item = (id, kind, createdAt, extra = {}) => ({
});
(async () => {
-- const wiki = await upstream('wiki', [item('pg_a', 'wiki_page', '2026-10-01T00:00:00Z'), item('pg_b', 'wiki_page', '2026-10-03T00:00:00Z')]);
-- const blog = await upstream('blog', [item('pst_a', 'blog_draft', '2026-10-02T00:00:00Z'), item('pst_b', 'blog_draft', '2026-10-04T00:00:00Z'), item('pst_c', 'blog_draft', '2026-10-05T00:00:00Z'), item('pst_d', 'blog_draft', '2026-10-06T00:00:00Z'), item('pst_e', 'blog_draft', '2026-10-07T00:00:00Z'), item('pst_f', 'blog_draft', '2026-10-08T00:00:00Z'), item('pst_g', 'blog_draft', '2026-10-09T00:00:00Z')]);
++ const wiki = await upstream('wiki', [item('pg_a', 'wiki_page', '2026-10-01T00:00:00Z'), item('pg_b', 'wiki_page', '2026-10-03T00:00:00Z', {
++ // A page the AI editor kept hidden: the queue names which kind of review it had.
++ reviewed: { kind: 'ai_editor', decision: 'rejected', by: 'svc:orchestrator', at: '2026-10-02T00:00:00Z', label: "Kept hidden by OpenVibe's AI editor on 2026-10-02" },
++ })]);
++ const blog = await upstream('blog', [item('pst_a', 'blog_draft', '2026-10-02T00:00:00Z'), item('pst_b', 'blog_draft', '2026-10-04T00:00:00Z', { reviewed: { kind: 'robot', decision: 'approved', label: 'not a kind' } }), item('pst_c', 'blog_draft', '2026-10-05T00:00:00Z'), item('pst_d', 'blog_draft', '2026-10-06T00:00:00Z'), item('pst_e', 'blog_draft', '2026-10-07T00:00:00Z'), item('pst_f', 'blog_draft', '2026-10-08T00:00:00Z'), item('pst_g', 'blog_draft', '2026-10-09T00:00:00Z')]);
const news = await upstream('news', [item('clu_a', 'ai_brief', '2026-09-30T00:00:00Z')]);
const trade = await upstream('trade', [item('ins_a', 'company_page', '2026-09-29T00:00:00Z', { ref: 'ACME' })]);
const env = { OV_WIKI_INTERNAL_URL: wiki.url, OV_BLOG_INTERNAL_URL: blog.url, OV_NEWS_INTERNAL_URL: news.url, OV_TRADE_INTERNAL_URL: trade.url };
@@@ -73,6 +76,9 @@ const item = (id, kind, createdAt, extra = {}) => ({
assert.strictEqual(r.status, 200, r.text.slice(0, 300));
assert.ok(r.text.includes('OpenVibe.Wiki') && r.text.includes('OpenVibe.Blog') && r.text.includes('OpenVibe.News') && r.text.includes('OpenVibe.Trade'));
assert.ok(r.text.includes('More (1)'), 'the 7th Blog row is folded behind More (1)');
++ assert.ok(r.text.includes('last review by the AI editor'), 'the queue shows which kind of review the item had');
++ assert.match(r.text, /Kept hidden by OpenVibe(?:&#39;|')s AI editor on 2026-10-02/, 'and what it said');
++ assert.ok(!r.text.includes('not a kind'), 'an unknown review kind is ignored, never rendered');
assert.ok(r.text.includes('Title clu_a') && r.text.includes('AI brief'));
assert.ok(r.text.indexOf('More (1)') < r.text.indexOf('Title pst_g'), 'the 7th row is inside the fold');
assert.strictEqual(r.text.split('Title pst_g').length, 2, 'the folded row appears once');