openvibers/openvibe.network Commits
Admin review queue: a row says which kind of review it already had (#612)
Changes
@@@ -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),});}@@@ -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(?:'|')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');
@@ -75,12 +75,22 @@ function itemOf(raw, site, now) {
@@ -75,12 +75,22 @@ function itemOf(raw, site, now) {
const revision = raw.revision == null ? null : Number(raw.revision);
const revision = raw.revision == null ? null : Number(raw.revision);
if (revision != null && !Number.isInteger(revision)) return null;
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) })) : [];
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 {
return {
site: site.id, kind: raw.kind, id: raw.id, title: raw.title.slice(0, 300),
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,
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),
url: safeUrl(raw.url), editUrl: raw.editUrl == null ? null : safeUrl(raw.editUrl),
createdAt: createdAt.toISOString(), age: ageText(createdAt.toISOString(), now),
createdAt: createdAt.toISOString(), age: ageText(createdAt.toISOString(), now),
reasons: reasons.length ? reasons : [{ code: 'noindex_requested', text: 'Waiting for a person\'s review.' }],
reasons: reasons.length ? reasons : [{ code: 'noindex_requested', text: 'Waiting for a person\'s review.' }],
+ reviewed,
};
};
}
}
@@ -120,12 +130,21 @@ function itemRow(item, csrf) {
@@ -120,12 +130,21 @@ function itemRow(item, csrf) {
const href = item.editUrl || item.url;
const href = item.editUrl || item.url;
const title = `<a href="${esc(safeUrl(href))}">${esc(item.title)}</a>`;
const title = `<a href="${esc(safeUrl(href))}">${esc(item.title)}</a>`;
const why = item.reasons.map((r) => r.text).join(' · ');
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({
return kit.row({
titleHtml: title,
titleHtml: title,
badge: kit.badge(kindOf(item.kind)),
badge: kit.badge(kindOf(item.kind)),
- meta: item.age ? `${item.age} waiting` : '',
+ meta,
metaIso: item.createdAt,
metaIso: item.createdAt,
- sub: why,
+ sub: [why, reviewedText].filter(Boolean).join(' · '),
trail: actionForms(item, csrf),
trail: actionForms(item, csrf),
});
});
}
}
@@ -36,8 +36,11 @@ const item = (id, kind, createdAt, extra = {}) => ({
@@ -36,8 +36,11 @@ const item = (id, kind, createdAt, extra = {}) => ({
});
});
(async () => {
(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 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 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 };
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 = {}) => ({
@@ -73,6 +76,9 @@ const item = (id, kind, createdAt, extra = {}) => ({
assert.strictEqual(r.status, 200, r.text.slice(0, 300));
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('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('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(?:'|')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.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.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');
assert.strictEqual(r.text.split('Title pst_g').length, 2, 'the folded row appears once');