Skip to content

Commit 4869c34

Browse files
sametbasbugclaude
andcommitted
Give the platform tools their own page
Moderation, announcements, quota and backups were four separate jobs folded into one <details> accordion underneath the personal account settings. None of them is "my account", and the accordion made them look like a footnote to it. They live at /dashboard/platform now, full width. The old two-column admin grid stretched every card to its tallest neighbour, so the media budget card was 990px tall around 80px of content while the announcement form was squeezed into a 310px ribbon beside it. The backup card was 34 identical "daily · succeeded" rows: one failure among them carried exactly the same visual weight as the successes. It is a status line now — last run, last success, how many of how many — followed by the failures alone, with their error codes. Raw enums are gone. A Turkish admin screen was printing database values: "daily · succeeded", "info · all_agents · active". The dictionaries pass unknown values through untranslated, so a new server-side kind shows up as itself rather than as a silently wrong Turkish label. The page is not a security boundary and says so in a comment: hiding it grants nothing, every endpoint is still checked server-side. The denied state exists so someone without the role does not get a screen full of buttons that will fail. Shared helpers moved to dashboard-shared.js rather than being copied into the second page. Three mutations fail the suite — listing the successful backups, reverting a label dictionary, dropping align-items from the pair grid. The last threshold was measured, not guessed: the gap is 82px when correct and 189px when broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0f3cbc9 commit 4869c34

8 files changed

Lines changed: 844 additions & 356 deletions

File tree

‎scripts/orbit-browser-tests.mjs‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,58 @@ if (errors.length === 0) {
208208
* rolü platform araçlarını da yüklüyor ve onların uçları burada
209209
* mocklanmadığı için panel hata durumuna düşerdi. Ölçtüğümüz şey
210210
* ajan listesi, o yüzden sade hesap yeterli. */
211+
/* Platform yöneticisi. Yedek listesi bilerek karışık: otuz dört özdeş
212+
* satırın içinde tek bir başarısızlık, kartın çözmesi gereken asıl
213+
* problemdi. */
214+
if (request.headers.cookie?.includes('orbit-platform-test=1')) {
215+
const platformJson = (body) => {
216+
response.writeHead(200, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' });
217+
response.end(JSON.stringify(body));
218+
};
219+
if (pathname === '/v1/me') {
220+
platformJson({
221+
account: {
222+
roles: ['platform_owner'], agentQuota: -1, displayName: 'Orbit Owner',
223+
handle: 'orbit-owner', avatarUrl: null, announcementEmails: true,
224+
},
225+
session: { id: 'session-owner' },
226+
agentQuota: { limit: -1, used: 0, remaining: -1, canCreate: true },
227+
sponsoredAgents: [],
228+
});
229+
return;
230+
}
231+
if (pathname === '/v1/approvals') { platformJson({ reviews: [] }); return; }
232+
if (pathname === '/v1/admin/announcements') {
233+
platformJson({ announcements: [{
234+
id: 'ann-1', title: 'Bakım penceresi', severity: 'critical',
235+
audienceType: 'all_agents', status: 'active',
236+
}] });
237+
return;
238+
}
239+
if (pathname === '/v1/admin/announcements/email-budget') {
240+
platformJson({ emailBudget: { recipients: 3, recipientCap: 60, remainingToday: 90, dailyBudget: 90 } });
241+
return;
242+
}
243+
if (pathname === '/v1/admin/media-transform-usage') {
244+
platformJson({ usage: {
245+
monthUtc: '2026-08', attemptedCount: 1, safetyLimit: 4500,
246+
succeededCount: 1, failedCount: 0, alert: false,
247+
} });
248+
return;
249+
}
250+
if (pathname === '/v1/admin/backups') {
251+
const runs = Array.from({ length: 33 }, (_, index) => ({
252+
backupKind: 'daily', status: 'succeeded',
253+
startedAt: Date.UTC(2026, 7, 14 - index, 6, 17), errorCode: null,
254+
}));
255+
runs.splice(4, 0, {
256+
backupKind: 'weekly', status: 'failed',
257+
startedAt: Date.UTC(2026, 7, 9, 6, 17), errorCode: 'r2_upload_timeout',
258+
});
259+
platformJson({ backups: runs });
260+
return;
261+
}
262+
}
211263
if (request.headers.cookie?.includes('orbit-sponsor-test=1')) {
212264
const sponsorJson = (body) => {
213265
response.writeHead(200, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' });
@@ -226,6 +278,13 @@ if (errors.length === 0) {
226278
return;
227279
}
228280
if (pathname === '/v1/sessions') { sponsorJson({ sessions: [] }); return; }
281+
/* Sponsorun platform yetkisi yok: sayfa "kapalı" hâlini göstermeli
282+
* ve hiçbir yönetici ucuna dokunmamalı. */
283+
if (pathname.startsWith('/v1/admin/') || pathname === '/v1/approvals') {
284+
response.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
285+
response.end(JSON.stringify({ error: { code: 'forbidden', message: 'no' } }));
286+
return;
287+
}
229288
if (pathname === '/v1/mcp/authorizations') { sponsorJson({ authorizations: [] }); return; }
230289
if (pathname === '/v1/me/connected-sites') { sponsorJson({ connectedSites: [] }); return; }
231290
const manage = /^\/v1\/agents\/([^/]+)\/manage$/u.exec(pathname);
@@ -844,6 +903,7 @@ if (errors.length === 0) {
844903
underlinedButtons: [...document.querySelectorAll('a.dashboard-button')]
845904
.filter((a) => getComputedStyle(a).textDecorationLine.includes('underline')).length,
846905
deadCards: document.querySelectorAll('#messages-card, #following-card, #agent-detail').length,
906+
platformLinkHidden: document.querySelector('#platform-link')?.classList.contains('hidden'),
847907
};
848908
});
849909

@@ -875,6 +935,10 @@ if (errors.length === 0) {
875935
);
876936
check(panel.deadCards === 0, `${label}: kaldırılan mesaj/takip kartları ya da eski detay kabı hâlâ basılıyor.`);
877937
check(panel.underlinedButtons === 0, `${label}: bağlantı olarak çizilen düğmenin metni altı çizili (${panel.underlinedButtons}).`);
938+
/* Sponsorun platform yetkisi yok; bağlantı gizli olmalı. Gizlemek
939+
* yetki kaldırmıyor — uçlar sunucuda denetleniyor — ama çalışmayan
940+
* bir kapıyı göstermenin de anlamı yok. */
941+
check(panel.platformLinkHidden === true, `${label}: yetkisi olmayan hesaba platform bağlantısı gösteriliyor.`);
878942

879943
/* İkinci ajana geçiş: detay taşınmalı, çoğalmamalı. */
880944
await page.locator('.agent-row').nth(1).locator('.agent-list-item').click();
@@ -899,6 +963,107 @@ if (errors.length === 0) {
899963
await context.close();
900964
}
901965

966+
/* Platform araçları sayfası. Dört ayrı iş bir dönem kişisel hesap
967+
* ayarlarının altında bir akordeonun içindeydi. */
968+
{
969+
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: 'light' });
970+
await context.addCookies([{ name: 'orbit-platform-test', value: '1', url: baseUrl }]);
971+
const page = await context.newPage();
972+
const pageErrors = [];
973+
page.on('pageerror', (error) => pageErrors.push(error.message));
974+
await page.goto(`${baseUrl}/dashboard/platform`, { waitUntil: 'load' });
975+
await page.waitForSelector('#platform:not(.hidden)');
976+
await page.waitForSelector('.backup-summary');
977+
978+
const platform = await page.evaluate(() => {
979+
const box = (selector) => {
980+
const element = document.querySelector(selector);
981+
if (!element) return null;
982+
const rect = element.getBoundingClientRect();
983+
return { width: Math.round(rect.width), height: Math.round(rect.height) };
984+
};
985+
return {
986+
deniedHidden: document.querySelector('#platform-denied')?.classList.contains('hidden'),
987+
announcementMeta: document.querySelector('#announcements .meta')?.textContent,
988+
backupSummary: document.querySelector('.backup-summary')?.textContent?.replace(/\s+/gu, ' ').trim(),
989+
backupFailing: document.querySelector('.backup-summary')?.classList.contains('is-failing'),
990+
backupRows: document.querySelectorAll('#backups .dashboard-item').length,
991+
backupRowText: document.querySelector('#backups .dashboard-item')?.textContent?.replace(/\s+/gu, ' ').trim(),
992+
mediaCard: box('#media-transform-card'),
993+
mediaContent: box('#media-transform-usage'),
994+
announcementForm: box('#announcement-form'),
995+
};
996+
});
997+
998+
check(platform.deniedHidden === true, 'Platform: yetkili hesaba "kapalı" ekranı gösteriliyor.');
999+
/* Ham enum sızıntısı. `info · all_agents · active` yerine Türkçe. */
1000+
check(
1001+
platform.announcementMeta === 'Kritik · Tüm ajanlar · Yayında',
1002+
`Platform: duyuru satırı ham enum yazıyor (${platform.announcementMeta}).`,
1003+
);
1004+
/* Yedek kartı: 34 satır yerine bir durum ve yalnız başarısız olan. */
1005+
check(platform.backupRows === 1, `Platform: yedek kartı başarılı çalışmaları da listeliyor (${platform.backupRows}).`);
1006+
check(platform.backupFailing === true, 'Platform: başarısız yedek varken durum satırı uyarı rengine geçmiyor.');
1007+
check(
1008+
platform.backupSummary?.includes('1 yedek çalışması başarısız'),
1009+
`Platform: yedek durumu başarısızlığı söylemiyor (${platform.backupSummary}).`,
1010+
);
1011+
check(
1012+
platform.backupSummary?.includes('34 çalışmanın 33'),
1013+
`Platform: yedek durumu toplamı yanlış (${platform.backupSummary}).`,
1014+
);
1015+
check(
1016+
platform.backupRowText?.includes('Haftalık · Başarısız') && platform.backupRowText?.includes('r2_upload_timeout'),
1017+
`Platform: başarısız yedek satırı türü ya da hata kodunu yazmıyor (${platform.backupRowText}).`,
1018+
);
1019+
/* Boş kutu: kart bir dönem 990px yüksekliğindeydi ve içinde 80px
1020+
* içerik vardı, çünkü yanındaki uzun kartla eşit boya çekiliyordu. */
1021+
/* Boş kutu. Kart bir dönem 990px yüksekliğindeydi ve içinde 80px
1022+
* içerik vardı: `1fr 1fr` bir ızgarada komşusu uzun olduğu için
1023+
* onunla eşit boya çekiliyordu.
1024+
*
1025+
* Eşik ölçülerek seçildi: `align-items: start` varken kart ile
1026+
* içeriği arasındaki fark 82px (dolgu ve başlık), kaldırıldığında
1027+
* 189px'e çıkıyor. 120 ikisini ayırıyor. */
1028+
check(
1029+
platform.mediaCard.height - platform.mediaContent.height < 120,
1030+
`Platform: görsel bütçe kartı içeriğinden çok daha uzun (kart ${platform.mediaCard.height}px, içerik ${platform.mediaContent.height}px).`,
1031+
);
1032+
/* Duyuru formu bir dönem 310px'lik bir şeritti. */
1033+
check(
1034+
platform.announcementForm.width > 500,
1035+
`Platform: duyuru formu dar bir şeride sıkışmış (${platform.announcementForm.width}px).`,
1036+
);
1037+
check(
1038+
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
1039+
'Platform: sayfa yatay taşıyor.',
1040+
);
1041+
check(pageErrors.length === 0, `Platform: sayfa hatası: ${pageErrors.join(' | ')}`);
1042+
await context.close();
1043+
}
1044+
1045+
/* Yetkisi olmayan hesap. Sayfa bir kapı değil — uçlar sunucuda
1046+
* denetleniyor — ama çalışmayan düğmelerle dolu bir ekran da açmamalı. */
1047+
{
1048+
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: 'light' });
1049+
await context.addCookies([{ name: 'orbit-sponsor-test', value: '1', url: baseUrl }]);
1050+
const page = await context.newPage();
1051+
const pageErrors = [];
1052+
page.on('pageerror', (error) => pageErrors.push(error.message));
1053+
await page.goto(`${baseUrl}/dashboard/platform`, { waitUntil: 'load' });
1054+
await page.waitForSelector('#platform-denied:not(.hidden)');
1055+
const denied = await page.evaluate(() => ({
1056+
toolsHidden: document.querySelector('#platform')?.classList.contains('hidden'),
1057+
approvals: document.querySelectorAll('#approvals .dashboard-item').length,
1058+
backups: document.querySelectorAll('#backups .dashboard-item, .backup-summary').length,
1059+
}));
1060+
check(denied.toolsHidden === true, 'Platform: yetkisiz hesaba araçlar gösteriliyor.');
1061+
check(denied.approvals === 0, 'Platform: yetkisiz hesap için inceleme kuyruğu yüklenmiş.');
1062+
check(denied.backups === 0, 'Platform: yetkisiz hesap için yedek listesi yüklenmiş.');
1063+
check(pageErrors.length === 0, `Platform yetkisiz: sayfa hatası: ${pageErrors.join(' | ')}`);
1064+
await context.close();
1065+
}
1066+
9021067
{
9031068
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme: 'light' });
9041069
const page = await context.newPage();

‎scripts/orbit-email-tests.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ describe('giden posta', { concurrency: false }, () => {
9191
* tutuyor. Kopya ayrışırsa kutu açık kalır, kişi postalanacağını
9292
* sanır ve sunucu 400 döner — yani kullanıcı hatası gibi görünen bir
9393
* bizim hatamız. Kaynak dosyadan okuyup karşılaştırıyoruz. */
94-
const client = readFileSync(new URL('../src/scripts/dashboard.js', import.meta.url), 'utf8');
94+
/* Liste duyuru formuyla birlikte platform sayfasına taşındı. Yol
95+
* güncellenmezse test "panelde bulunamadı" diye kırılır — sessizce
96+
* geçmez, ama yine de doğru dosyaya bakmalı. */
97+
const client = readFileSync(new URL('../src/scripts/dashboard-platform.js', import.meta.url), 'utf8');
9598
const match = client.match(/const ANNOUNCEMENT_EMAIL_SEVERITIES = \[([^\]]*)\]/u);
9699
assert.ok(match, 'panelde seviye listesi bulunamadı');
97100
const clientList = match[1].split(',')

‎scripts/orbit-site-tests.mjs‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -574,8 +574,12 @@ if (fs.existsSync(dashboardFile)) {
574574
check(dashboardHtml.includes('Google hesabımla devam et'), 'Dashboard sponsor giriş akışını taşımıyor.');
575575
check(dashboardHtml.includes('Ajanım için kayıt kodu oluştur'), 'Dashboard tek kullanımlık kayıt kodu akışını taşımıyor.');
576576
check(dashboardHtml.includes('public profilinde “İnsanı” olarak görünür'), 'Dashboard, seçilen adın ajan profilinde görüneceğini açıklamıyor.');
577-
check(dashboardHtml.includes('Yayın incelemeleri'), 'Dashboard moderator yayın kuyruğunu taşımıyor.');
578-
check(dashboardHtml.includes('Metin değiştirilemez'), 'Dashboard moderatorün içeriği düzenleyemeyeceğini açıklamıyor.');
577+
check(dashboardHtml.includes('/dashboard/platform'), 'Dashboard platform araçları sayfasına bağlanmıyor.');
578+
/* Metne değil kaba bakıyoruz: "Yayın incelemeleri" ifadesi platform
579+
* bağlantısının açıklamasında da geçiyor. Taşınan şey kuyruğun kendisi. */
580+
check(!dashboardHtml.includes('id="approvals"'), 'Dashboard yayın kuyruğunu hâlâ kendi içinde taşıyor.');
581+
check(!dashboardHtml.includes('id="announcement-form"'), 'Dashboard duyuru formunu hâlâ kendi içinde taşıyor.');
582+
check(!dashboardHtml.includes('id="backups"'), 'Dashboard yedek listesini hâlâ kendi içinde taşıyor.');
579583
check(dashboardHtml.includes('Bağlantıyı onayla'), 'Dashboard MCP yetkilendirme ekranını taşımıyor.');
580584
check(dashboardHtml.includes('id="mcp-agent-select"'), 'Dashboard MCP ajan seçimini taşımıyor.');
581585
check(dashboardHtml.includes('uzun ömürlü API anahtarı'), 'Dashboard MCP credential güvenlik sınırını açıklamıyor.');
@@ -584,10 +588,48 @@ if (fs.existsSync(dashboardFile)) {
584588
check(!dashboardHtml.includes('orb_agent_v1_'), 'Dashboard build çıktısı ajan credential kalıbı içeriyor.');
585589
}
586590
const dashboardScript = fs.readFileSync(path.join(ROOT, 'src', 'scripts', 'dashboard.js'), 'utf8');
587-
check(dashboardScript.includes("roles.includes('moderator')"), 'Dashboard moderator rolünü yayın incelemesine bağlamıyor.');
588-
check(dashboardScript.includes("loadApprovals()"), 'Dashboard moderator yayın kuyruğunu yüklemiyor.');
589-
check(dashboardScript.includes("review-approve').addEventListener"), 'Dashboard yayın onay düğmesini bağlamıyor.');
590-
check(dashboardScript.includes("review-reject').addEventListener"), 'Dashboard yayın ret düğmesini bağlamıyor.');
591+
check(dashboardScript.includes("roles.includes('moderator')"), 'Dashboard moderator rolüne platform bağlantısını göstermiyor.');
592+
593+
/* Platform araçları ayrı sayfada. Moderasyon iddiaları oraya taşındı:
594+
* aynı cümleleri dashboard'da aramak, taşındıkları için değil hiç var
595+
* olmadıkları için geçen bir test bırakırdı. */
596+
const platformFile = path.join(DIST_DIR, 'dashboard', 'platform', 'index.html');
597+
check(fs.existsSync(platformFile), 'Platform araçları rotası build çıktısında yok.');
598+
if (fs.existsSync(platformFile)) {
599+
const platformHtml = fs.readFileSync(platformFile, 'utf8');
600+
check(platformHtml.includes('Yayın incelemeleri'), 'Platform sayfası moderator yayın kuyruğunu taşımıyor.');
601+
check(platformHtml.includes('Metin değiştirilemez'), 'Platform sayfası moderatorün içeriği düzenleyemeyeceğini açıklamıyor.');
602+
check(platformHtml.includes('Sistem duyuruları'), 'Platform sayfası duyuru formunu taşımıyor.');
603+
check(platformHtml.includes('Yedek durumu'), 'Platform sayfası yedek durumunu taşımıyor.');
604+
check(platformHtml.includes('id="platform-denied"'), 'Platform sayfası yetkisiz hâli taşımıyor.');
605+
check(!platformHtml.includes('orb_agent_v1_'), 'Platform build çıktısı ajan credential kalıbı içeriyor.');
606+
check(
607+
/<meta name="robots" content="noindex, nofollow, noarchive"/u.test(platformHtml),
608+
'Platform sayfası noindex değil.',
609+
);
610+
}
611+
const platformScript = fs.readFileSync(path.join(ROOT, 'src', 'scripts', 'dashboard-platform.js'), 'utf8');
612+
check(platformScript.includes("roles.includes('moderator')"), 'Platform sayfası moderator rolünü yayın incelemesine bağlamıyor.');
613+
check(platformScript.includes("loadApprovals()"), 'Platform sayfası yayın kuyruğunu yüklemiyor.');
614+
check(platformScript.includes("review-approve').addEventListener"), 'Platform sayfası yayın onay düğmesini bağlamıyor.');
615+
check(platformScript.includes("review-reject').addEventListener"), 'Platform sayfası yayın ret düğmesini bağlamıyor.');
616+
/* Ham enum sızıntısı: panel bir dönem `daily · succeeded` ve
617+
* `info · all_agents · active` yazıyordu — Türkçe bir yönetim ekranında
618+
* veritabanı değerleri. Sözlükler ortak modülde duruyor. */
619+
const dashboardShared = fs.readFileSync(path.join(ROOT, 'src', 'scripts', 'dashboard-shared.js'), 'utf8');
620+
for (const [raw, turkish] of [
621+
['succeeded', 'Başarılı'],
622+
['failed', 'Başarısız'],
623+
['daily', 'Günlük'],
624+
['all_agents', 'Tüm ajanlar'],
625+
['withdrawn', 'Geri çekildi'],
626+
['critical', 'Kritik'],
627+
]) {
628+
check(
629+
new RegExp(`${raw}:\\s*'${turkish}'`, 'u').test(dashboardShared),
630+
`Panel ${raw} değerini Türkçeye çevirmiyor.`,
631+
);
632+
}
591633
check(dashboardScript.includes("history.replaceState"), 'Dashboard MCP ticket fragmentını adres çubuğundan temizlemiyor.');
592634
check(dashboardScript.includes("sessionStorage.setItem(MCP_TICKET_STORAGE_KEY"), 'Dashboard MCP ticketını yalnız sekme oturumunda korumuyor.');
593635
check(!dashboardScript.includes("localStorage.setItem(MCP_TICKET_STORAGE_KEY"), 'Dashboard MCP ticketını kalıcı depolamaya yazıyor.');

0 commit comments

Comments
 (0)