Skip to content

Commit ceaf171

Browse files
sametbasbugclaude
andcommitted
Tell the dashboard what the agents actually did
The panel answered only one question: what can I revoke? Every button on it was a revoke button, and nothing anywhere said how many records an agent had written or when it last wrote one. Most of that was not a missing query. `publicAgent()` already emits the stats block, but only when handed a PublicAgentProfileView, and `/v1/me` was handing it the plain profile — so the field was silently dropped. Quota was computed server-side too, and never returned. - listSponsoredAgentsWithStats() joins the public select for the panel. The cheap list stays for the quota and MCP-consent paths, which would otherwise pay three correlated subqueries per row for nothing. - getReviewCountsForAgents() groups the pending counts into one query instead of one call per agent. - sponsoredAgentQuota() is now the single place the quota is counted, and canCreateSponsoredAgent() reads it. A screen that says one thing while the door counts another is worse than no number at all. - The remaining quota moved next to the button that spends it; it used to sit in the other column, showing the raw ceiling rather than what was left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent d40f9e7 commit ceaf171

8 files changed

Lines changed: 257 additions & 11 deletions

File tree

‎scripts/orbit-slice1-tests.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,36 @@ let firstCredentialToken = '';
663663
const me = await request('/v1/me', {
664664
headers: authenticatedHeaders(sponsorCookies),
665665
}, NOW + 46);
666-
const meBody = await me.json() as { sponsoredAgents: Array<{ id: string }> };
666+
const meBody = await me.json() as {
667+
agentQuota: { limit: number; used: number; remaining: number; canCreate: boolean };
668+
sponsoredAgents: Array<{
669+
id: string;
670+
stats: { postCount: number; replyCount: number; latestActivityAt: number | null };
671+
reviewCounts: { pending: number; pendingReview: number };
672+
}>;
673+
};
667674
assert.deepEqual(meBody.sponsoredAgents.map((agent) => agent.id), [sponsoredAgentId]);
675+
676+
/* Panel ajan başına sayı gösteriyor; bu alanlar bir dönem hiç
677+
* doğmuyordu çünkü `/v1/me` istatistiksiz görünümü döndürüyordu ve
678+
* serileştirici onları sessizce atlıyordu. Sessiz atlamayı yakalayan
679+
* tek şey bu iddia. */
680+
assert.deepEqual(meBody.sponsoredAgents[0].stats, {
681+
postCount: 0,
682+
replyCount: 0,
683+
latestActivityAt: null,
684+
});
685+
assert.deepEqual(meBody.sponsoredAgents[0].reviewCounts, { pending: 0, pendingReview: 0 });
686+
687+
/* Kotayı ekran da kapı da aynı yerden okuyor. Varsayılan hak 1 ve o
688+
* hak bu ajanla dolmuş durumda: panelin "1 hakkın kaldı" demesi ile
689+
* ikinci kaydın 409 alması aynı sayıdan çıkmalı. */
690+
assert.deepEqual(meBody.agentQuota, {
691+
limit: 1,
692+
used: 1,
693+
remaining: 0,
694+
canCreate: false,
695+
});
668696
});
669697

670698
test('MCP authorization uses CSRF, a versioned permission bundle, one-time exchange and revocation', async () => {

‎src/pages/dashboard.astro‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ import { LEGAL_LAST_UPDATED } from '../data/legal';
117117
<p class="section-label">Sponsor alanı</p>
118118
<h2>Merhaba, <span id="welcome-name">yeniden hoş geldin</span>.</h2>
119119
<p>Ajan erişimleri burada. Ajanların kimliklerini ve yayınlarını kendileri yönetir.</p>
120+
{/*
121+
Sayfanın "ne oldu?" satırı. Panel uzun süre yalnız "neyi
122+
kapatabilirim?" sorusuna cevap veriyordu: her düğme bir iptal
123+
düğmesiydi ve ajanların ne yaptığı hiçbir yerde yazmıyordu.
124+
*/}
125+
<div id="activity-summary" class="activity-summary hidden"></div>
120126
</header>
121127

122128
<article id="agent-card" class="dashboard-card section-card">
@@ -125,7 +131,12 @@ import { LEGAL_LAST_UPDATED } from '../data/legal';
125131
<p class="public-attribution-note"><Icon name="info" size={15} /> Ajan kaydı tamamlandığında Orbit adın ve profil fotoğrafın, ajanın public profilinde “İnsanı” olarak görünür.</p>
126132
<div class="dashboard-row agent-create-form">
127133
<button id="registration-code-create" class="dashboard-button primary" type="button">Ajanım için kayıt kodu oluştur</button>
128-
<span class="muted">Kod 10 dakika geçerlidir ve bir ajan kotası ayırır.</span>
134+
{/*
135+
Kalan hak, onu harcayan düğmenin yanında. Eskiden sağ rayda,
136+
profil kartının içinde küçük gri bir satırdı — yani kararı veren
137+
yerde değil, bambaşka bir sütunda duruyordu.
138+
*/}
139+
<span class="muted" id="quota-note">Kod 10 dakika geçerlidir ve bir ajan kotası ayırır.</span>
129140
</div>
130141
<div id="agent-list" class="agent-list" aria-label="Ajanların"></div>
131142
<div id="agent-detail"></div>
@@ -319,6 +330,18 @@ import { LEGAL_LAST_UPDATED } from '../data/legal';
319330
.agent-list-item span:first-child { min-width: 0; }
320331
.agent-list-item strong, .agent-list-item small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
321332
.agent-list-item small { margin-top: .15rem; color: var(--muted); font-size: .7rem; }
333+
.agent-list-flags { display: flex; align-items: center; flex: 0 0 auto; gap: .35rem; }
334+
/* Bekleyen inceleme sayısı. Durum rozetinin yanında ama onun rengiyle
335+
değil: biri hesabın durumu, diğeri bir iş listesi. Aynı renkte olsalar
336+
"Aktif" ile "3 incelemede" tek bir etikete karışırdı. */
337+
.agent-waiting { padding: .22rem .5rem; border: 1px solid #e8cea1; border-radius: 999px; color: #8a5a12; background: #fff8e9; font-size: .66rem; font-weight: 800; white-space: nowrap; }
338+
/* "Ne oldu?" satırı. Bekleyen iş yoksa sessiz bir meta satırı; varsa
339+
uyarı rengine geçiyor, çünkü o hâlde okunması gereken bir şey var. */
340+
.activity-summary { margin: .6rem 0 0; color: var(--muted); font-size: .8rem; }
341+
.activity-summary b { color: var(--text); font-weight: 750; }
342+
.activity-summary.is-waiting { width: fit-content; padding: .45rem .7rem; border: 1px solid color-mix(in srgb,var(--warning-ink) 34%,var(--line)); border-radius: .7rem; color: var(--warning-ink); background: var(--warning-surface); }
343+
.activity-summary.is-waiting b { color: inherit; }
344+
html[data-theme="dark"] .agent-waiting { color: #e8cea1; border-color: #5c4a2a; background: #2a2216; }
322345
.agent-heading { display: grid; gap: .35rem; }
323346
.agent-state { width: fit-content; padding: .22rem .5rem; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); background: var(--paper); font-size: .66rem; font-weight: 800; }
324347
.agent-state.active { color: #116947; border-color: #adddc9; background: #edf9f4; }

‎src/scripts/dashboard.js‎

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,92 @@ async function completeSignup() {
245245
söylemeyen arayüz sinir bozucu olur; o yüzden buton kapalı DEĞİL, basınca
246246
sebebi söylüyor. Kapalı bir buton "site bozuk" gibi okunur. */
247247

248+
/* Panelde tarih değil, tazelik önemli: "3 gün önce" bir şeyin durduğunu
249+
söyler, "11.08.2026" söylemez. Kesin tarih başlık olarak duruyor, yani
250+
bilgi kaybolmuyor — yalnız ikinci plana geçiyor. */
251+
const RELATIVE_UNITS = [
252+
['year', 365 * 24 * 60 * 60 * 1000],
253+
['month', 30 * 24 * 60 * 60 * 1000],
254+
['day', 24 * 60 * 60 * 1000],
255+
['hour', 60 * 60 * 1000],
256+
['minute', 60 * 1000],
257+
];
258+
259+
function relativeTime(value) {
260+
const elapsed = value - Date.now();
261+
const formatter = new Intl.RelativeTimeFormat('tr-TR', { numeric: 'auto' });
262+
for (const [unit, size] of RELATIVE_UNITS) {
263+
if (Math.abs(elapsed) >= size) return formatter.format(Math.round(elapsed / size), unit);
264+
}
265+
return 'az önce';
266+
}
267+
268+
function absoluteTime(value) {
269+
return new Date(value).toLocaleString('tr-TR');
270+
}
271+
272+
/* Ajanın istatistikleri artık `/v1/me` ile geliyor. Bir dönem gelmiyordu ve
273+
panel bunu fark edemezdi: alan yoksa `undefined` okunur, ekranda "NaN
274+
gönderi" yazardı. O yüzden burada sayı olduğu doğrulanıyor. */
275+
function agentNumbers(agent) {
276+
const posts = Number(agent.stats?.postCount ?? 0);
277+
const replies = Number(agent.stats?.replyCount ?? 0);
278+
const latest = agent.stats?.latestActivityAt ?? null;
279+
const waiting = Number(agent.reviewCounts?.pending ?? 0)
280+
+ Number(agent.reviewCounts?.pendingReview ?? 0);
281+
return { posts, replies, latest, waiting, records: posts + replies };
282+
}
283+
284+
/* Sayfanın tepesindeki tek satır. İki soruya cevap veriyor: bir şey oldu mu,
285+
ve benim müdahalemi bekleyen bir şey var mı. İkincisi varsa satır rengini
286+
değiştiriyor — aynı cümleyi aynı renkte okumak, bekleyeni fark etmemek
287+
demek. */
288+
function renderActivitySummary() {
289+
const host = byId('activity-summary');
290+
const agents = me.sponsoredAgents ?? [];
291+
if (agents.length === 0) {
292+
host.classList.add('hidden');
293+
return;
294+
}
295+
const totals = agents.map(agentNumbers);
296+
const records = totals.reduce((sum, agent) => sum + agent.records, 0);
297+
const waiting = totals.reduce((sum, agent) => sum + agent.waiting, 0);
298+
const latest = totals.reduce(
299+
(newest, agent) => (agent.latest && (!newest || agent.latest > newest) ? agent.latest : newest),
300+
null,
301+
);
302+
303+
const parts = [`<b>${agents.length}</b> ajan`, `<b>${records}</b> kayıt`];
304+
if (latest) {
305+
parts.push(`son aktivite <b title="${escapeHtml(absoluteTime(latest))}">${escapeHtml(relativeTime(latest))}</b>`);
306+
}
307+
host.innerHTML = waiting > 0
308+
? `${parts.join(' · ')} · <b>${waiting}</b> kayıt incelemede bekliyor`
309+
: parts.join(' · ');
310+
host.classList.toggle('is-waiting', waiting > 0);
311+
host.classList.remove('hidden');
312+
}
313+
314+
/* Kotayı sunucu hesaplıyor, panel yalnız yazıyor. Sayıyı burada yeniden
315+
türetmek, kapının saydığından farklı bir sayı göstermeye açık olurdu. */
316+
function renderQuota() {
317+
const host = byId('quota-note');
318+
const quota = me.agentQuota;
319+
if (!quota) return;
320+
if (quota.limit < 0) {
321+
host.textContent = 'Sınırsız ajan hakkın var. Kod 10 dakika geçerlidir.';
322+
return;
323+
}
324+
host.textContent = quota.remaining > 0
325+
? `${quota.remaining}/${quota.limit} ajan hakkın kaldı. Kod 10 dakika geçerlidir ve bir hak ayırır.`
326+
: `Ajan hakkın dolu (${quota.used}/${quota.limit}). Yeni kod almak için bir ajanı emekliye ayırman gerekiyor.`;
327+
}
328+
248329
function renderAccount() {
249-
const quota = me.account.agentQuota === -1 ? 'Sınırsız ajan hakkı' : `${me.account.agentQuota} ajan hakkı`;
330+
/* Kota buradan kalktı: artık onu harcayan düğmenin yanında yazıyor ve
331+
`me.agentQuota` üzerinden geliyor. İki yerde iki ayrı kaynaktan yazmak,
332+
birinin eskimesi demekti — bu satır ham tavanı gösteriyordu, kaç hakkın
333+
kaldığını değil. */
250334
const accountRole = me.account.roles.includes('platform_owner')
251335
? 'Platform yöneticisi'
252336
: me.account.roles.includes('moderator') ? 'Moderatör' : 'Sponsor';
@@ -264,7 +348,7 @@ function renderAccount() {
264348
${me.account.avatarUrl ? `<img class="dashboard-avatar" src="${escapeHtml(me.account.avatarUrl)}" alt="" />` : ''}
265349
<div><strong>${escapeHtml(me.account.displayName)}</strong><div class="meta">@${escapeHtml(handle)}</div></div>
266350
</div>
267-
<div class="meta">${accountRole} · ${escapeHtml(quota)}</div>`;
351+
<div class="meta">${accountRole}</div>`;
268352
}
269353

270354
async function loadSessions() {
@@ -446,10 +530,22 @@ function renderAgentList() {
446530
for (const agent of me.sponsoredAgents ?? []) {
447531
const state = agent.status === 'active' ? agent.onboardingState : agent.status;
448532
const label = state === 'active' ? 'Aktif' : state === 'pending' ? 'Beklemede' : state === 'suspended' ? 'Askıda' : 'Emekli';
533+
const numbers = agentNumbers(agent);
534+
/* İkinci satır: ajanın ne yaptığı. Rozet ne yaptığını değil, hesabın
535+
durumunu söylüyor — ikisi farklı sorular ve bir dönem yalnız
536+
ikincisinin cevabı vardı. */
537+
const activity = numbers.records > 0
538+
? `${numbers.posts} gönderi · ${numbers.replies} yanıt${numbers.latest
539+
? ` · ${escapeHtml(relativeTime(numbers.latest))}`
540+
: ''}`
541+
: 'Henüz kayıt yok';
449542
const button = document.createElement('button');
450543
button.type = 'button';
451544
button.className = `agent-list-item${agent.id === selectedAgentId ? ' selected' : ''}`;
452-
button.innerHTML = `<span><strong>@${escapeHtml(agent.handle)}</strong></span><span class="agent-state ${escapeHtml(state)}">${label}</span>`;
545+
if (numbers.latest) button.title = `Son aktivite · ${absoluteTime(numbers.latest)}`;
546+
button.innerHTML = `<span><strong>@${escapeHtml(agent.handle)}</strong><small>${activity}</small></span><span class="agent-list-flags">${
547+
numbers.waiting > 0 ? `<span class="agent-waiting">${numbers.waiting} incelemede</span>` : ''
548+
}<span class="agent-state ${escapeHtml(state)}">${label}</span></span>`;
453549
button.addEventListener('click', async () => {
454550
selectedAgentId = agent.id;
455551
await loadAgent();
@@ -618,6 +714,8 @@ async function load() {
618714
}
619715
showPrimaryView('dashboard');
620716
renderAccount();
717+
renderActivitySummary();
718+
renderQuota();
621719
await Promise.all([loadSessions(), loadAgent(), loadMcpAuthorizations(), loadConnectedSites()]);
622720
const publicationReviewer = me.account.roles.includes('platform_owner') || me.account.roles.includes('moderator');
623721
if (publicationReviewer) {

‎src/server/http/api.ts‎

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5076,13 +5076,35 @@ function isMcpPendingAgent(agent: AgentProfileView): boolean {
50765076
&& agent.handle.startsWith(MCP_PENDING_HANDLE_PREFIX);
50775077
}
50785078

5079-
function canCreateSponsoredAgent(account: AccountView, agents: AgentProfileView[], now: number): boolean {
5080-
if (account.agentQuota < 0) return true;
5081-
const activeReservations = agents.filter((agent) => (
5079+
/* Kotanın tek hesaplandığı yer. Panel de buradan okuyor: kapının saydığı
5080+
* sayı ile ekrandaki sayı ayrışırsa, kişi "bir hakkım var" yazan bir
5081+
* ekranda 409 yer ve hatayı arıza sanar.
5082+
*
5083+
* `limit: -1` sınırsız demek; o durumda `remaining` de -1 kalıyor, sıfır
5084+
* ya da büyük bir sayı uydurmuyoruz. */
5085+
function sponsoredAgentQuota(account: AccountView, agents: AgentProfileView[], now: number): {
5086+
limit: number;
5087+
used: number;
5088+
remaining: number;
5089+
canCreate: boolean;
5090+
} {
5091+
const used = agents.filter((agent) => (
50825092
agent.status !== 'retired'
50835093
&& !(isMcpPendingAgent(agent) && agent.createdAt + MCP_NATIVE_ONBOARDING_TTL_MS <= now)
50845094
)).length;
5085-
return activeReservations < account.agentQuota;
5095+
if (account.agentQuota < 0) {
5096+
return { limit: -1, used, remaining: -1, canCreate: true };
5097+
}
5098+
return {
5099+
limit: account.agentQuota,
5100+
used,
5101+
remaining: Math.max(0, account.agentQuota - used),
5102+
canCreate: used < account.agentQuota,
5103+
};
5104+
}
5105+
5106+
function canCreateSponsoredAgent(account: AccountView, agents: AgentProfileView[], now: number): boolean {
5107+
return sponsoredAgentQuota(account, agents, now).canCreate;
50865108
}
50875109

50885110
async function cleanupAbandonedMcpOnboarding(
@@ -6896,14 +6918,27 @@ export async function handleApiRequest(
68966918
}
68976919
if (request.method === 'GET' && path === '/v1/me') {
68986920
const auth = await authenticateHuman(request, env, repository, now, false);
6899-
const sponsoredAgents = await agentRepository.listSponsoredAgents(auth.account.id);
6921+
/* İstatistikli sürüm: panel ajan başına gönderi, yanıt ve son
6922+
* aktiviteyi gösteriyor. `publicAgent` bu alanları zaten tanıyor,
6923+
* yalnız kendisine istatistiksiz bir görünüm verildiğinde
6924+
* atlıyordu. */
6925+
const sponsoredAgents = await agentRepository.listSponsoredAgentsWithStats(auth.account.id);
6926+
/* Bekleyen inceleme sayısı tek sorguda, ajan başına gruplanmış. */
6927+
const reviewCounts = await publicationRepository.getReviewCountsForAgents(
6928+
sponsoredAgents.map((agent) => agent.id),
6929+
);
69006930
return json({ account: auth.account, session: {
69016931
id: auth.session.sessionId,
69026932
createdAt: auth.session.createdAt,
69036933
lastSeenAt: auth.session.lastSeenAt,
69046934
idleExpiresAt: auth.session.idleExpiresAt,
69056935
absoluteExpiresAt: auth.session.absoluteExpiresAt,
6906-
}, sponsoredAgents: sponsoredAgents.map(publicAgent) });
6936+
},
6937+
agentQuota: sponsoredAgentQuota(auth.account, sponsoredAgents, now),
6938+
sponsoredAgents: sponsoredAgents.map((agent) => ({
6939+
...publicAgent(agent),
6940+
reviewCounts: reviewCounts.get(agent.id) ?? { pending: 0, pendingReview: 0 },
6941+
})) });
69076942
}
69086943
/* Duyuru postaları kapatılabilir; hesap, moderasyon ve güvenlik
69096944
* postaları kapatılamaz ve bu ucun onları kapatacak bir alanı yok.

‎src/server/repositories/agent-repository.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export interface AgentRegistrationGrantView {
8282

8383
export interface AgentRepository {
8484
listSponsoredAgents(accountId: string): Promise<AgentProfileView[]>;
85+
/* Aynı listenin istatistikli hâli. Ayrı bir metot, çünkü istatistikler
86+
* satır başına üç bağıntılı alt sorgu demek: panelin buna ihtiyacı var,
87+
* kotayı sayan ve MCP onay ekranını dolduran yollar için ise boşuna
88+
* ödenen bir bedel. */
89+
listSponsoredAgentsWithStats(accountId: string): Promise<PublicAgentProfileView[]>;
8590
listPublicAgents(): Promise<PublicAgentProfileView[]>;
8691
listPublicAgentsPage(input: {
8792
limit: number;

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ export class D1AgentRepository implements AgentRepository {
193193
return result.results.map(profileFromSql);
194194
}
195195

196+
/* PUBLIC_AGENT_SELECT sponsorluk üyeliğine zaten LEFT JOIN yapıyor ve o
197+
* join `primary_sponsor` + iptal edilmemiş koşullarını taşıyor; hesabı
198+
* WHERE'e koymak onu inner join'e çeviriyor. Sıralama ve süzgeç yokluğu
199+
* `listSponsoredAgents` ile birebir aynı: bekleyen, askıdaki ve emekli
200+
* ajanlar da dönüyor — panel hepsini gösteriyor. */
201+
async listSponsoredAgentsWithStats(accountId: string): Promise<PublicAgentProfileView[]> {
202+
const result = await this.#db.prepare(`
203+
${PUBLIC_AGENT_SELECT}
204+
WHERE membership.account_id = ?
205+
ORDER BY a.created_at, a.id
206+
`).bind(accountId).all<PublicAgentSqlRow>();
207+
return result.results.map(publicProfileFromSql);
208+
}
209+
196210
/* Dizin askıdaki ajanı da sayar. Askı silme değil; listeden düşürmek,
197211
* profilde "kayıtları yerinde duruyor" derken ajanın kendisini ortadan
198212
* kaldırmak olurdu. Kartın üzerinde durumu yazıyor, yani görünürlük

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
AgentRecordReviewStatus,
99
AgentRecordRevisionView,
1010
AgentRecordView,
11+
AgentReviewCounts,
1112
ControlledDictionary,
1213
IdempotencyReplay,
1314
MutationRecord,
@@ -544,6 +545,34 @@ export class D1PublicationRepository implements PublicationRepository {
544545
};
545546
}
546547

548+
async getReviewCountsForAgents(
549+
agentIds: readonly string[],
550+
): Promise<Map<string, AgentReviewCounts>> {
551+
const counts = new Map<string, AgentReviewCounts>();
552+
if (agentIds.length === 0) return counts;
553+
const placeholders = agentIds.map(() => '?').join(', ');
554+
const result = await this.#db.prepare(`
555+
SELECT
556+
author_agent_id,
557+
COALESCE(SUM(CASE WHEN lifecycle_state = 'pending' THEN 1 ELSE 0 END), 0) AS pending,
558+
COALESCE(SUM(CASE WHEN pending_revision_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS pending_review
559+
FROM records
560+
WHERE author_agent_id IN (${placeholders})
561+
GROUP BY author_agent_id
562+
`).bind(...agentIds).all<{
563+
author_agent_id: string;
564+
pending: number;
565+
pending_review: number;
566+
}>();
567+
for (const row of result.results) {
568+
counts.set(row.author_agent_id, {
569+
pending: Number(row.pending ?? 0),
570+
pendingReview: Number(row.pending_review ?? 0),
571+
});
572+
}
573+
return counts;
574+
}
575+
547576
async listAgentRecords(input: {
548577
agentId: string;
549578
limit: number;

‎src/server/repositories/publication-repository.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ export interface AgentRecordCounts {
148148
moderated: number;
149149
}
150150

151+
/* Panelin ajan başına gösterdiği iki sayı. `AgentRecordCounts`'un yedi
152+
* alanını her ajan için çekmek gereksiz: panel "kaç şey benim müdahalemi
153+
* bekliyor" sorusunu soruyor, kaydın tam yaşam döngüsünü değil. */
154+
export interface AgentReviewCounts {
155+
/* Henüz hiç yayımlanmamış, incelemede duran kayıt. */
156+
pending: number;
157+
/* Yayımlanmış ama bekleyen bir revizyonu olan kayıt. */
158+
pendingReview: number;
159+
}
160+
151161
export interface AgentRecordPage {
152162
items: AgentRecordView[];
153163
hasMore: boolean;
@@ -165,6 +175,10 @@ export interface PublicationRepository {
165175
getRecord(idOrSlug: string): Promise<MutationRecord | null>;
166176
getAgentRecord(agentId: string, idOrSlug: string): Promise<AgentRecordView | null>;
167177
getAgentRecordCounts(agentId: string): Promise<AgentRecordCounts>;
178+
/* Tek sorguda, ajan başına gruplanmış. Panelde beş ajan varken beş ayrı
179+
* sorgu atmamak için var. Kaydı olmayan ajan sonuçta hiç görünmez;
180+
* çağıran taraf eksik anahtarı sıfır sayar. */
181+
getReviewCountsForAgents(agentIds: readonly string[]): Promise<Map<string, AgentReviewCounts>>;
168182
listAgentRecords(input: {
169183
agentId: string;
170184
limit: number;

0 commit comments

Comments
 (0)