#!/usr/bin/env node /** * Playwright audit for info.defi-oracle.io (or INFO_SITE_URL). * Grades structure, agent surfaces, API reachability from the browser, console/network hygiene, basic perf. * * Usage: * pnpm exec playwright install chromium # once * node scripts/verify/playwright-audit-info-defi-oracle.mjs * INFO_SITE_URL=http://192.168.11.218 node scripts/verify/playwright-audit-info-defi-oracle.mjs */ import { chromium } from 'playwright'; const BASE = (process.env.INFO_SITE_URL || 'https://info.defi-oracle.io').replace(/\/$/, ''); const ROUTES = [ '/', '/tokens', '/pools', '/swap', '/routing', '/governance', '/ecosystem', '/documentation', '/solacenet', '/agents', '/disclosures', ]; function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); } function scoreFromRatio(ok, total) { if (total === 0) return 10; return clamp((ok / total) * 10, 0, 10); } async function collectPageMetrics(page) { return page.evaluate(() => { const h1s = [...document.querySelectorAll('h1')].map((el) => el.textContent?.trim() || ''); const nav = document.querySelector('nav'); const main = document.querySelector('main') || document.querySelector('[role="main"]'); const title = document.title || ''; const metaDesc = document.querySelector('meta[name="description"]')?.getAttribute('content') || ''; const jsonLd = [...document.querySelectorAll('script[type="application/ld+json"]')].length; const navLinks = nav ? nav.querySelectorAll('a').length : 0; let ttfbMs = null; let domCompleteMs = null; const navEntry = performance.getEntriesByType('navigation')[0]; if (navEntry) { ttfbMs = Math.round(navEntry.responseStart); domCompleteMs = Math.round(navEntry.domComplete); } return { title, metaDesc, jsonLdScripts: jsonLd, h1Count: h1s.length, h1Texts: h1s.slice(0, 3), hasMain: !!main, navLinkCount: navLinks, ttfbMs, domCompleteMs, }; }); } async function main() { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, ignoreHTTPSErrors: true, }); const page = await context.newPage(); const consoleErrors = []; const consoleWarnings = []; page.on('console', (msg) => { const t = msg.type(); const text = msg.text(); if (t === 'error') consoleErrors.push(text); if (t === 'warning') consoleWarnings.push(text); }); const failedRequests = []; page.on('requestfailed', (req) => { failedRequests.push({ url: req.url(), failure: req.failure()?.errorText || 'unknown' }); }); const pages = []; for (const path of ROUTES) { const url = `${BASE}${path === '/' ? '/' : path}`; const res = await page.goto(url, { waitUntil: 'load', timeout: 60_000 }); const status = res?.status() ?? 0; await page.waitForTimeout(1500); const metrics = await collectPageMetrics(page); const bodySample = await page.evaluate(() => document.body?.innerText?.slice(0, 2000) || ''); pages.push({ path, url, status, metrics, bodySampleLength: bodySample.length }); } const staticChecks = []; for (const p of ['/llms.txt', '/robots.txt', '/sitemap.xml', '/agent-hints.json']) { const r = await page.request.get(`${BASE}${p}`); staticChecks.push({ path: p, status: r.status(), ok: r.ok(), contentType: r.headers()['content-type'] || '', }); } let tokenAggProbe = { ok: false, status: 0, snippet: '' }; try { const taUrl = `${BASE}/token-aggregation/api/v1/networks?refresh=1`; const tr = await page.request.get(taUrl); tokenAggProbe.status = tr.status(); tokenAggProbe.ok = tr.ok(); const ct = (tr.headers()['content-type'] || '').toLowerCase(); if (ct.includes('json')) { const j = await tr.json().catch(() => null); tokenAggProbe.snippet = j && typeof j === 'object' ? JSON.stringify(j).slice(0, 120) : ''; } else { tokenAggProbe.snippet = (await tr.text()).slice(0, 80); } } catch (e) { tokenAggProbe.error = String(e); } await browser.close(); const routesOk = pages.filter((p) => p.status >= 200 && p.status < 400).length; const staticOk = staticChecks.filter((s) => s.ok).length; const h1Ok = pages.filter((p) => p.metrics.h1Count === 1).length; const mainOk = pages.filter((p) => p.metrics.hasMain).length; const uniqueConsoleBuckets = new Set( consoleErrors.map((t) => { if (t.includes('CORS policy')) return 'cors-blocked-cross-origin-api'; if (t.includes('502')) return 'http-502'; if (t.includes('429')) return 'http-429'; if (t.includes('Failed to load resource')) return 'failed-resource-generic'; return t.slice(0, 100); }), ); const distinctConsoleIssues = uniqueConsoleBuckets.size; const scores = { availability: scoreFromRatio(routesOk, pages.length), staticAgentSurfaces: scoreFromRatio(staticOk, staticChecks.length), documentStructure: clamp( (scoreFromRatio(h1Ok, pages.length) * 0.5 + scoreFromRatio(mainOk, pages.length) * 0.5), 0, 10, ), tokenAggregationApi: tokenAggProbe.ok ? 10 : tokenAggProbe.status === 429 ? 4 : 2, consoleHygiene: consoleErrors.length === 0 ? 10 : clamp(10 - distinctConsoleIssues * 2.5, 0, 10), networkHygiene: failedRequests.length === 0 ? 10 : clamp(10 - Math.min(failedRequests.length, 8) * 1.1, 0, 10), homeMetaAndSeo: (() => { const home = pages[0]; let s = 0; if (home?.metrics.title?.length > 10) s += 3; if (home?.metrics.metaDesc?.length > 20) s += 3; if (home?.metrics.jsonLdScripts > 0) s += 4; return clamp(s, 0, 10); })(), performanceHint: (() => { const ttfb = pages[0]?.metrics?.ttfbMs; if (ttfb == null || Number.isNaN(ttfb)) return 7; if (ttfb < 300) return 10; if (ttfb < 800) return 8; if (ttfb < 1800) return 6; return 4; })(), }; const weights = { availability: 0.18, staticAgentSurfaces: 0.12, documentStructure: 0.12, tokenAggregationApi: 0.18, consoleHygiene: 0.1, networkHygiene: 0.1, homeMetaAndSeo: 0.1, performanceHint: 0.1, }; let overall = 0; for (const [k, w] of Object.entries(weights)) { overall += scores[k] * w; } overall = Math.round(overall * 10) / 10; const report = { baseUrl: BASE, generatedAt: new Date().toISOString(), overallScore: overall, scores, weights, pages: pages.map((p) => ({ path: p.path, url: p.url, status: p.status, metrics: p.metrics, bodyTextChars: p.bodySampleLength, })), staticChecks, tokenAggregationProbe: tokenAggProbe, consoleErrors, distinctConsoleIssues, consoleWarnings: consoleWarnings.slice(0, 20), failedRequests: failedRequests.slice(0, 30), rubricNotes: [ 'Scores are heuristic (0–10 per axis); overall is weighted sum.', 'tokenAggregationApi uses same-origin /token-aggregation from BASE (expects JSON 200).', 'Exactly one h1 per route is preferred for documentStructure.', 'performanceHint uses Navigation Timing responseStart (TTFB) on home only.', ], }; const grade = overall >= 9 ? 'A' : overall >= 8 ? 'B' : overall >= 7 ? 'C' : overall >= 6 ? 'D' : 'F'; report.letterGrade = grade; console.log(JSON.stringify(report, null, 2)); } main().catch((e) => { console.error(e); process.exit(1); });