Files
proxmox/scripts/verify/playwright-audit-info-defi-oracle.mjs
defiQUG dbd517b279 Sync workspace: config, docs, scripts, CI, operator rules, and submodule pointers.
- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains
- Omit embedded publish git dirs and empty placeholders from index

Made-with: Cursor
2026-04-12 06:12:20 -07:00

241 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 (010 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);
});