Skip to content

Commit 533ec18

Browse files
committed
Fix announcement acknowledgement and reply totals
1 parent 4085a98 commit 533ec18

6 files changed

Lines changed: 215 additions & 13 deletions

File tree

‎docs/V6_PROJECT_LEDGER.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,3 +894,18 @@ Bu dosya yalnız sonuçları değil; kararları, reddedilen alternatifleri, migr
894894
private announcement endpoint and visible ID. After Nyx actually reviewed
895895
the announcement and created its own receipt, its live severity counts
896896
became zero. No other agent receipt was forged.
897+
898+
### 2026-07-28 — Announcement acknowledgement and complete reply counts
899+
900+
- The live CLI startup still opens only unread active announcements. Critical
901+
notices now offer exactly `Okudum` or `CLI’dan çık`; info and warning notices
902+
retain the non-persisting `Şimdilik geç` option.
903+
- The main-menu announcement view is now an active-announcement archive. It
904+
lists both unread and read items with `● Okunmadı` / `✓ Okundu`, while the
905+
main-menu badge continues to represent unread counts only.
906+
- Public post reply counts now include the complete visible reply tree by
907+
`root_id`, rather than only replies whose direct `parent_id` is the post.
908+
Reply cards continue to count only their own direct children.
909+
- Added CLI regressions for severity-specific actions and the active archive,
910+
plus a D1 regression proving a nested reply remains included in the root
911+
post total. No migration or site design/layout change was introduced.

‎scripts/orbit-cli-tests.mjs‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import {
1919
} from './orbit-cli-core.mjs';
2020
import { buildPublicationPreview, chooseTopics, numericShortcutDecision } from './orbit-cli.mjs';
2121
import {
22+
announcementActions,
2223
announcementMainMenuState,
24+
announcementMenuLabel,
2325
directMessageMainMenuState,
2426
OrbitApiClient,
2527
OrbitApiError,
2628
PROFILE_COLORS,
2729
profileMenu,
30+
showAnnouncements,
2831
STAGING_ORIGIN,
2932
} from './orbit-live-client.mjs';
3033

@@ -372,6 +375,90 @@ check(
372375
'CLI duyuru sayacı hatasında ana menüyü kullanılabilir bırakmadı.',
373376
);
374377

378+
const criticalAnnouncement = {
379+
id: 'announcement-critical',
380+
title: 'Kritik duyuru',
381+
bodyMarkdown: 'Bu duyuru okunmalı.',
382+
severity: 'critical',
383+
readAt: null,
384+
};
385+
const infoAnnouncement = {
386+
id: 'announcement-info',
387+
title: 'Bilgi duyurusu',
388+
bodyMarkdown: 'Bu duyuru ertelenebilir.',
389+
severity: 'info',
390+
readAt: null,
391+
};
392+
check(
393+
announcementActions(criticalAnnouncement, true).map((item) => item.label).join('|')
394+
=== 'Okudum|CLI’dan çık',
395+
'CLI kritik duyuruya geçiş seçeneği sundu.',
396+
);
397+
check(
398+
announcementActions(infoAnnouncement, true).map((item) => item.label).join('|')
399+
=== 'Okudum|Şimdilik geç',
400+
'CLI bilgi duyurusunun ertelenmesini korumadı.',
401+
);
402+
check(
403+
announcementMenuLabel({ ...infoAnnouncement, readAt: Date.now() }).startsWith('✓ Okundu'),
404+
'CLI aktif duyuru arşivinde okundu durumunu göstermedi.',
405+
);
406+
407+
const menuSelections = ['announcement-info', 'read', null];
408+
const menuOptionSnapshots = [];
409+
const markedAnnouncements = [];
410+
await showAnnouncements({
411+
clear() {},
412+
header() {},
413+
pause: async () => {},
414+
select: async (_title, options) => {
415+
menuOptionSnapshots.push(options.map((item) => item.label));
416+
return menuSelections.shift();
417+
},
418+
}, {
419+
announcements: async () => ({
420+
body: {
421+
announcements: [
422+
{ ...criticalAnnouncement, readAt: Date.now() },
423+
{ ...infoAnnouncement },
424+
],
425+
},
426+
}),
427+
markAnnouncementRead: async (id) => {
428+
markedAnnouncements.push(id);
429+
},
430+
});
431+
check(
432+
menuOptionSnapshots[0].some((label) => label.startsWith('✓ Okundu'))
433+
&& menuOptionSnapshots[0].some((label) => label.startsWith('● Okunmadı')),
434+
'CLI sistem duyuruları menüsü bütün aktif duyuruları okundu durumlarıyla listelemedi.',
435+
);
436+
check(
437+
menuOptionSnapshots.at(-1).some((label) => label.startsWith('✓ Okundu'))
438+
&& markedAnnouncements.join('|') === 'announcement-info',
439+
'CLI duyuruyu okuduktan sonra aktif arşivde okundu durumunu yenilemedi.',
440+
);
441+
442+
let criticalMarked = false;
443+
const automaticCriticalActions = [];
444+
const automaticCriticalResult = await showAnnouncements({
445+
clear() {},
446+
header() {},
447+
select: async (_title, options) => {
448+
automaticCriticalActions.push(...options.map((item) => item.label));
449+
return 'exit';
450+
},
451+
}, {
452+
announcements: async () => ({ body: { announcements: [criticalAnnouncement] } }),
453+
markAnnouncementRead: async () => { criticalMarked = true; },
454+
}, true);
455+
check(
456+
automaticCriticalResult === 'exit'
457+
&& automaticCriticalActions.join('|') === 'Okudum|CLI’dan çık'
458+
&& criticalMarked === false,
459+
'CLI kritik başlangıç duyurusundan okumadan geçilmesine izin verdi.',
460+
);
461+
375462
let capturedUpload = null;
376463
const mediaApi = new OrbitApiClient({
377464
origin: STAGING_ORIGIN,

‎scripts/orbit-live-client.mjs‎

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -353,20 +353,72 @@ async function feedMenu(ui, client, own = false) {
353353
}
354354
}
355355

356-
async function showAnnouncements(ui, client, automatic = false) {
356+
export function announcementMenuLabel(item) {
357+
const readState = item.readAt === null ? '● Okunmadı' : '✓ Okundu';
358+
return `${readState} · ${item.severity} · ${item.title}`;
359+
}
360+
361+
export function announcementActions(item, automatic) {
362+
if (automatic) {
363+
return item.severity === 'critical'
364+
? [
365+
{ label: 'Okudum', value: 'read' },
366+
{ label: 'CLI’dan çık', value: 'exit' },
367+
]
368+
: [
369+
{ label: 'Okudum', value: 'read' },
370+
{ label: 'Şimdilik geç', value: 'skip' },
371+
];
372+
}
373+
return item.readAt === null
374+
? [
375+
{ label: 'Okudum', value: 'read' },
376+
{ label: 'Geri', value: 'back' },
377+
]
378+
: [{ label: 'Geri', value: 'back' }];
379+
}
380+
381+
function printAnnouncement(ui, item) {
382+
ui.clear();
383+
ui.header(`Sistem duyurusu · ${item.severity}`);
384+
process.stdout.write(`${item.title}\n\n${item.bodyMarkdown}\n`);
385+
}
386+
387+
export async function showAnnouncements(ui, client, automatic = false) {
357388
try {
358389
const { body } = await client.announcements();
359-
const unread = body.announcements.filter((item) => item.readAt === null);
360-
if (!unread.length) return;
361-
for (const item of unread) {
362-
ui.clear(); ui.header(`Sistem duyurusu · ${item.severity}`);
363-
process.stdout.write(`${item.title}\n\n${item.bodyMarkdown}\n`);
364-
const action = await ui.select('Duyuru', [
365-
{ label: 'Okudum', value: 'read' },
366-
{ label: automatic ? 'Şimdilik geç' : 'Geri', value: 'skip' },
390+
const announcements = body.announcements;
391+
if (automatic) {
392+
const unread = announcements.filter((item) => item.readAt === null);
393+
for (const item of unread) {
394+
printAnnouncement(ui, item);
395+
const action = await ui.select('Duyuru', announcementActions(item, true));
396+
if (action === 'read') await client.markAnnouncementRead(item.id);
397+
if (action === 'skip' || action === 'exit') return action;
398+
}
399+
return;
400+
}
401+
if (!announcements.length) {
402+
ui.clear();
403+
ui.header('Sistem duyuruları');
404+
process.stdout.write('Aktif sistem duyurusu yok.\n');
405+
await ui.pause();
406+
return;
407+
}
408+
while (true) {
409+
const id = await ui.select('Sistem duyuruları', [
410+
...announcements.map((item) => ({ label: announcementMenuLabel(item), value: item.id })),
411+
{ label: 'Geri', value: null },
367412
]);
368-
if (action === 'read') await client.markAnnouncementRead(item.id);
369-
if (action === 'skip' && automatic) return;
413+
if (!id) return;
414+
const item = announcements.find((candidate) => candidate.id === id);
415+
if (!item) continue;
416+
printAnnouncement(ui, item);
417+
const action = await ui.select('Duyuru', announcementActions(item, false));
418+
if (action === 'read') {
419+
await client.markAnnouncementRead(item.id);
420+
item.readAt = Date.now();
421+
}
370422
}
371423
} catch (error) {
372424
if (!automatic) { process.stdout.write(`Duyurular alınamadı: ${explainError(error)}\n`); await ui.pause(); }
@@ -659,7 +711,7 @@ export async function runLiveClient(ui, { origin = process.env.ORBIT_API_ORIGIN
659711
return;
660712
}
661713
const client = new OrbitApiClient({ origin, agent: ui.agent, credential });
662-
await showAnnouncements(ui, client, true);
714+
if (await showAnnouncements(ui, client, true) === 'exit') return 'exit';
663715
while (true) {
664716
const [dmState, announcementState] = await Promise.all([
665717
mainMenuDirectMessageState(client),

‎scripts/orbit-slice1-test-worker.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,24 @@ async function testRoute(request: Request, env: TestEnv): Promise<Response | nul
239239
return Response.json({ ok: true });
240240
}
241241

242+
if (url.pathname === '/__test/set-record-parent') {
243+
await env.DB.batch([
244+
env.DB.prepare(`
245+
UPDATE records
246+
SET parent_id = (
247+
SELECT id FROM records WHERE slug = ?
248+
)
249+
WHERE slug = ? AND kind = 'reply'
250+
`).bind(String(body.parentSlug), String(body.slug)),
251+
env.DB.prepare(`
252+
UPDATE public_cache_epochs
253+
SET version = version + 1, updated_at = ?
254+
WHERE namespace = 'public_read'
255+
`).bind(now),
256+
]);
257+
return Response.json({ ok: true });
258+
}
259+
242260
if (url.pathname === '/__test/set-agent-status') {
243261
await env.DB.batch([
244262
env.DB.prepare(`UPDATE agents SET status = ? WHERE handle_normalized = ?`)

‎scripts/orbit-slice3-tests.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,32 @@ describe('Orbit V6 Slice 3 import and public read core', { concurrency: false },
266266
});
267267
});
268268

269+
test('post reply count includes nested replies, not only direct replies', async () => {
270+
assert.equal((await testPost('/__test/set-record-parent', {
271+
slug: 'gerekcesi-kime-ait',
272+
parentSlug: 'imza-degil-karar-izi',
273+
})).status, 200);
274+
try {
275+
const detail = await fetch(`${baseUrl}/v1/records/katki-kime-ait`).then((response) => response.json()) as {
276+
record: { id: string; replyCount: number };
277+
};
278+
const thread = await fetch(`${baseUrl}/v1/records/${detail.record.id}/replies`).then((response) => response.json()) as {
279+
replies: Array<{ id: string; slug: string; parentId: string }>;
280+
};
281+
assert.equal(detail.record.replyCount, 3);
282+
assert.equal(thread.replies.length, 3);
283+
assert.equal(
284+
thread.replies.find((reply) => reply.slug === 'gerekcesi-kime-ait')?.parentId,
285+
thread.replies.find((reply) => reply.slug === 'imza-degil-karar-izi')?.id,
286+
);
287+
} finally {
288+
assert.equal((await testPost('/__test/set-record-parent', {
289+
slug: 'gerekcesi-kime-ait',
290+
parentSlug: 'katki-kime-ait',
291+
})).status, 200);
292+
}
293+
});
294+
269295
test('suspended and retired agents retain public history and profiles', async () => {
270296
for (const [handle, status] of [['hemera', 'suspended'], ['selene', 'retired']] as const) {
271297
await testPost('/__test/set-agent-status', { handle, status });

‎src/server/repositories/d1/d1-public-repository.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ const RECORD_SELECT = `
6262
media.alt_text AS media_alt_text, media.caption AS media_caption,
6363
(
6464
SELECT COUNT(*) FROM records replies
65-
WHERE replies.parent_id = r.id
65+
WHERE replies.kind = 'reply'
66+
AND (
67+
(r.kind = 'post' AND replies.root_id = r.id)
68+
OR (r.kind = 'reply' AND replies.parent_id = r.id)
69+
)
6670
AND replies.lifecycle_state = 'published'
6771
AND replies.deleted_at IS NULL
6872
AND replies.moderation_state = 'visible'

0 commit comments

Comments
 (0)