acpilot 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1164 -1031
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -21329,14 +21329,14 @@ var require_diagnostics = __commonJS({
|
|
|
21329
21329
|
"undici:client:beforeConnect",
|
|
21330
21330
|
(evt) => {
|
|
21331
21331
|
const {
|
|
21332
|
-
connectParams: { version, protocol, port, host }
|
|
21332
|
+
connectParams: { version: version2, protocol, port, host }
|
|
21333
21333
|
} = evt;
|
|
21334
21334
|
debugLog(
|
|
21335
21335
|
"connecting to %s%s using %s%s",
|
|
21336
21336
|
host,
|
|
21337
21337
|
port ? `:${port}` : "",
|
|
21338
21338
|
protocol,
|
|
21339
|
-
|
|
21339
|
+
version2
|
|
21340
21340
|
);
|
|
21341
21341
|
}
|
|
21342
21342
|
);
|
|
@@ -21344,14 +21344,14 @@ var require_diagnostics = __commonJS({
|
|
|
21344
21344
|
"undici:client:connected",
|
|
21345
21345
|
(evt) => {
|
|
21346
21346
|
const {
|
|
21347
|
-
connectParams: { version, protocol, port, host }
|
|
21347
|
+
connectParams: { version: version2, protocol, port, host }
|
|
21348
21348
|
} = evt;
|
|
21349
21349
|
debugLog(
|
|
21350
21350
|
"connected to %s%s using %s%s",
|
|
21351
21351
|
host,
|
|
21352
21352
|
port ? `:${port}` : "",
|
|
21353
21353
|
protocol,
|
|
21354
|
-
|
|
21354
|
+
version2
|
|
21355
21355
|
);
|
|
21356
21356
|
}
|
|
21357
21357
|
);
|
|
@@ -21359,7 +21359,7 @@ var require_diagnostics = __commonJS({
|
|
|
21359
21359
|
"undici:client:connectError",
|
|
21360
21360
|
(evt) => {
|
|
21361
21361
|
const {
|
|
21362
|
-
connectParams: { version, protocol, port, host },
|
|
21362
|
+
connectParams: { version: version2, protocol, port, host },
|
|
21363
21363
|
error
|
|
21364
21364
|
} = evt;
|
|
21365
21365
|
debugLog(
|
|
@@ -21367,7 +21367,7 @@ var require_diagnostics = __commonJS({
|
|
|
21367
21367
|
host,
|
|
21368
21368
|
port ? `:${port}` : "",
|
|
21369
21369
|
protocol,
|
|
21370
|
-
|
|
21370
|
+
version2,
|
|
21371
21371
|
error.message
|
|
21372
21372
|
);
|
|
21373
21373
|
}
|
|
@@ -43517,6 +43517,779 @@ var init_esm11 = __esm({
|
|
|
43517
43517
|
}
|
|
43518
43518
|
});
|
|
43519
43519
|
|
|
43520
|
+
// ../../src/lib/scanner/browser.ts
|
|
43521
|
+
var browser_exports = {};
|
|
43522
|
+
__export(browser_exports, {
|
|
43523
|
+
getPageAndHtml: () => getPageAndHtml,
|
|
43524
|
+
getRenderedHtml: () => getRenderedHtml
|
|
43525
|
+
});
|
|
43526
|
+
async function launchBrowser() {
|
|
43527
|
+
if (isDev) {
|
|
43528
|
+
const puppeteer = (await import("puppeteer")).default;
|
|
43529
|
+
return puppeteer.launch({ headless: "new", args: CHROME_ARGS });
|
|
43530
|
+
}
|
|
43531
|
+
const puppeteerCore = (await import("puppeteer-core")).default;
|
|
43532
|
+
const chromium = (await import("@sparticuz/chromium")).default;
|
|
43533
|
+
chromium.setGraphicsMode = false;
|
|
43534
|
+
return puppeteerCore.launch({
|
|
43535
|
+
args: [...chromium.args, ...CHROME_ARGS],
|
|
43536
|
+
defaultViewport: { width: 1280, height: 720 },
|
|
43537
|
+
executablePath: await chromium.executablePath(),
|
|
43538
|
+
headless: "new"
|
|
43539
|
+
});
|
|
43540
|
+
}
|
|
43541
|
+
async function waitForHydration(page, maxWait = 1e4) {
|
|
43542
|
+
const interval = 500;
|
|
43543
|
+
const attempts = Math.ceil(maxWait / interval);
|
|
43544
|
+
for (let i = 0; i < attempts; i++) {
|
|
43545
|
+
const ready = await page.evaluate(() => {
|
|
43546
|
+
const anchors = document.querySelectorAll("a[href]");
|
|
43547
|
+
const buttons = document.querySelectorAll("button");
|
|
43548
|
+
const bodyText = document.body?.innerText?.length || 0;
|
|
43549
|
+
return anchors.length > 0 || buttons.length > 0 || bodyText > 500;
|
|
43550
|
+
}).catch(() => false);
|
|
43551
|
+
if (ready) {
|
|
43552
|
+
if (isDev) console.log(`[Browser] Hydration complete after ${(i + 1) * interval}ms`);
|
|
43553
|
+
return;
|
|
43554
|
+
}
|
|
43555
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
43556
|
+
}
|
|
43557
|
+
if (isDev) console.log(`[Browser] Hydration not detected after ${maxWait}ms`);
|
|
43558
|
+
}
|
|
43559
|
+
async function autoScrollForLazyContent(page, maxScrollTime = 3e3) {
|
|
43560
|
+
const start = Date.now();
|
|
43561
|
+
await page.evaluate(async (maxTime) => {
|
|
43562
|
+
await new Promise((resolve) => {
|
|
43563
|
+
const viewportHeight = window.innerHeight;
|
|
43564
|
+
const maxScroll = document.body.scrollHeight;
|
|
43565
|
+
let currentScroll = 0;
|
|
43566
|
+
const step = Math.floor(viewportHeight * 0.8);
|
|
43567
|
+
const timer = setInterval(() => {
|
|
43568
|
+
if (Date.now() - window.__scrollStart > maxTime || currentScroll >= maxScroll) {
|
|
43569
|
+
clearInterval(timer);
|
|
43570
|
+
window.scrollTo(0, 0);
|
|
43571
|
+
resolve();
|
|
43572
|
+
return;
|
|
43573
|
+
}
|
|
43574
|
+
currentScroll += step;
|
|
43575
|
+
window.scrollTo(0, currentScroll);
|
|
43576
|
+
}, 150);
|
|
43577
|
+
window.__scrollStart = Date.now();
|
|
43578
|
+
});
|
|
43579
|
+
}, maxScrollTime);
|
|
43580
|
+
try {
|
|
43581
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: 5e3 });
|
|
43582
|
+
} catch {
|
|
43583
|
+
}
|
|
43584
|
+
if (isDev) console.log(`[Browser] Auto-scroll completed in ${Date.now() - start}ms`);
|
|
43585
|
+
}
|
|
43586
|
+
async function extractShadowDomHtml(page) {
|
|
43587
|
+
return page.evaluate(() => {
|
|
43588
|
+
const fragments = [];
|
|
43589
|
+
function extract3(root2) {
|
|
43590
|
+
const elements = Array.from(root2.querySelectorAll("*"));
|
|
43591
|
+
for (const el of elements) {
|
|
43592
|
+
if (el.shadowRoot) {
|
|
43593
|
+
fragments.push(el.shadowRoot.innerHTML);
|
|
43594
|
+
const tempDiv = document.createElement("div");
|
|
43595
|
+
tempDiv.innerHTML = el.shadowRoot.innerHTML;
|
|
43596
|
+
extract3(tempDiv);
|
|
43597
|
+
}
|
|
43598
|
+
}
|
|
43599
|
+
}
|
|
43600
|
+
extract3(document);
|
|
43601
|
+
return { count: fragments.length, html: fragments.join("\n") };
|
|
43602
|
+
});
|
|
43603
|
+
}
|
|
43604
|
+
function attachDebugListeners(page) {
|
|
43605
|
+
if (!isDev) return;
|
|
43606
|
+
page.on("pageerror", (err) => {
|
|
43607
|
+
console.log(`[Browser][JS Error] ${err.message || err}`);
|
|
43608
|
+
});
|
|
43609
|
+
page.on("requestfailed", (req) => {
|
|
43610
|
+
const url = req.url();
|
|
43611
|
+
if (/\.(js|mjs|css)(\?|$)/i.test(url) || url.includes("_next/")) {
|
|
43612
|
+
console.log(`[Browser][Request Failed] ${url} \u2014 ${req.failure()?.errorText || "unknown"}`);
|
|
43613
|
+
}
|
|
43614
|
+
});
|
|
43615
|
+
}
|
|
43616
|
+
async function getRenderedHtml(url) {
|
|
43617
|
+
let browser;
|
|
43618
|
+
try {
|
|
43619
|
+
browser = await launchBrowser();
|
|
43620
|
+
} catch (err) {
|
|
43621
|
+
throw new Error(`Browser launch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
43622
|
+
}
|
|
43623
|
+
try {
|
|
43624
|
+
const page = await browser.newPage();
|
|
43625
|
+
await page.setUserAgent(USER_AGENT);
|
|
43626
|
+
await page.setViewport({ width: 1280, height: 720 });
|
|
43627
|
+
attachDebugListeners(page);
|
|
43628
|
+
const response = await page.goto(url, { waitUntil: "networkidle0", timeout: 3e4 });
|
|
43629
|
+
const httpStatus = response?.status() ?? 0;
|
|
43630
|
+
await waitForHydration(page);
|
|
43631
|
+
await autoScrollForLazyContent(page);
|
|
43632
|
+
const shadow = await extractShadowDomHtml(page);
|
|
43633
|
+
if (isDev && shadow.count > 0) console.log(`[Browser] Extracted ${shadow.count} Shadow DOM roots`);
|
|
43634
|
+
let html3 = await page.content();
|
|
43635
|
+
const title = await page.title();
|
|
43636
|
+
if (shadow.html) {
|
|
43637
|
+
html3 = html3.replace("</body>", `<div data-shadow-content="true">${shadow.html}</div></body>`);
|
|
43638
|
+
}
|
|
43639
|
+
if (isDev) console.log(`[Browser] Rendered ${html3.length} chars, title: "${title}", status: ${httpStatus}`);
|
|
43640
|
+
return { html: html3, title, httpStatus };
|
|
43641
|
+
} finally {
|
|
43642
|
+
await browser.close();
|
|
43643
|
+
}
|
|
43644
|
+
}
|
|
43645
|
+
async function getPageAndHtml(url, options) {
|
|
43646
|
+
let browser;
|
|
43647
|
+
try {
|
|
43648
|
+
browser = await launchBrowser();
|
|
43649
|
+
} catch (err) {
|
|
43650
|
+
throw new Error(`Browser launch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
43651
|
+
}
|
|
43652
|
+
try {
|
|
43653
|
+
const page = await browser.newPage();
|
|
43654
|
+
await page.setUserAgent(USER_AGENT);
|
|
43655
|
+
await page.setViewport({ width: 1280, height: 720 });
|
|
43656
|
+
attachDebugListeners(page);
|
|
43657
|
+
if (options?.beforeNavigate) {
|
|
43658
|
+
options.beforeNavigate(page);
|
|
43659
|
+
}
|
|
43660
|
+
const response = await page.goto(url, { waitUntil: "networkidle0", timeout: 3e4 });
|
|
43661
|
+
const httpStatus = response?.status() ?? 0;
|
|
43662
|
+
const responseHeaders = response?.headers() ?? {};
|
|
43663
|
+
await waitForHydration(page);
|
|
43664
|
+
await autoScrollForLazyContent(page);
|
|
43665
|
+
const shadow = await extractShadowDomHtml(page);
|
|
43666
|
+
if (isDev && shadow.count > 0) console.log(`[Browser] Extracted ${shadow.count} Shadow DOM roots`);
|
|
43667
|
+
let html3 = await page.content();
|
|
43668
|
+
const title = await page.title();
|
|
43669
|
+
if (shadow.html) {
|
|
43670
|
+
html3 = html3.replace("</body>", `<div data-shadow-content="true">${shadow.html}</div></body>`);
|
|
43671
|
+
}
|
|
43672
|
+
if (isDev) console.log(`[Browser] Rendered ${html3.length} chars, title: "${title}", status: ${httpStatus}`);
|
|
43673
|
+
return {
|
|
43674
|
+
page,
|
|
43675
|
+
browser,
|
|
43676
|
+
html: html3,
|
|
43677
|
+
title,
|
|
43678
|
+
httpStatus,
|
|
43679
|
+
responseHeaders,
|
|
43680
|
+
cleanup: async () => {
|
|
43681
|
+
try {
|
|
43682
|
+
await browser.close();
|
|
43683
|
+
} catch {
|
|
43684
|
+
}
|
|
43685
|
+
}
|
|
43686
|
+
};
|
|
43687
|
+
} catch (err) {
|
|
43688
|
+
try {
|
|
43689
|
+
await browser.close();
|
|
43690
|
+
} catch {
|
|
43691
|
+
}
|
|
43692
|
+
throw err;
|
|
43693
|
+
}
|
|
43694
|
+
}
|
|
43695
|
+
var isDev, CHROME_ARGS, USER_AGENT;
|
|
43696
|
+
var init_browser = __esm({
|
|
43697
|
+
"../../src/lib/scanner/browser.ts"() {
|
|
43698
|
+
"use strict";
|
|
43699
|
+
isDev = process.env.NODE_ENV === "development";
|
|
43700
|
+
CHROME_ARGS = [
|
|
43701
|
+
"--no-sandbox",
|
|
43702
|
+
"--disable-setuid-sandbox",
|
|
43703
|
+
"--disable-dev-shm-usage",
|
|
43704
|
+
"--enable-features=NetworkService,NetworkServiceInProcess",
|
|
43705
|
+
"--disable-background-timer-throttling",
|
|
43706
|
+
"--disable-backgrounding-occluded-windows",
|
|
43707
|
+
"--disable-renderer-backgrounding"
|
|
43708
|
+
];
|
|
43709
|
+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
43710
|
+
}
|
|
43711
|
+
});
|
|
43712
|
+
|
|
43713
|
+
// ../../src/lib/network-tester/index.ts
|
|
43714
|
+
var network_tester_exports = {};
|
|
43715
|
+
__export(network_tester_exports, {
|
|
43716
|
+
NETWORK_PROFILES: () => NETWORK_PROFILES,
|
|
43717
|
+
runNetworkTest: () => runNetworkTest
|
|
43718
|
+
});
|
|
43719
|
+
function getTimeoutForProfile(profile) {
|
|
43720
|
+
if (profile.offline) return 1e4;
|
|
43721
|
+
if (profile.name === "slow3g") return 45e3;
|
|
43722
|
+
return 3e4;
|
|
43723
|
+
}
|
|
43724
|
+
function isThirdPartyUrl(requestUrl, pageOrigin) {
|
|
43725
|
+
try {
|
|
43726
|
+
const reqHost = new URL(requestUrl).hostname;
|
|
43727
|
+
const pageHost = new URL(pageOrigin).hostname;
|
|
43728
|
+
return !reqHost.endsWith(pageHost) && !pageHost.endsWith(reqHost);
|
|
43729
|
+
} catch {
|
|
43730
|
+
return false;
|
|
43731
|
+
}
|
|
43732
|
+
}
|
|
43733
|
+
function calculateNetworkScore(findings) {
|
|
43734
|
+
const penalties = {
|
|
43735
|
+
critical: 15,
|
|
43736
|
+
serious: 8,
|
|
43737
|
+
moderate: 4,
|
|
43738
|
+
minor: 1
|
|
43739
|
+
};
|
|
43740
|
+
let total = 0;
|
|
43741
|
+
for (const f of findings) {
|
|
43742
|
+
total += penalties[f.severity] || 1;
|
|
43743
|
+
}
|
|
43744
|
+
return Math.max(0, Math.min(100, 100 - Math.min(total, 85)));
|
|
43745
|
+
}
|
|
43746
|
+
async function setupNetworkTracking(client, pageUrl) {
|
|
43747
|
+
const requests = [];
|
|
43748
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
43749
|
+
const requestIdToIndex = /* @__PURE__ */ new Map();
|
|
43750
|
+
await client.send("Network.enable");
|
|
43751
|
+
client.on("Network.requestWillBeSent", (params) => {
|
|
43752
|
+
pendingRequests.set(params.requestId, {
|
|
43753
|
+
url: params.request.url,
|
|
43754
|
+
resourceType: params.type || "Other"
|
|
43755
|
+
});
|
|
43756
|
+
});
|
|
43757
|
+
client.on("Network.responseReceived", (params) => {
|
|
43758
|
+
const pending = pendingRequests.get(params.requestId);
|
|
43759
|
+
if (pending) {
|
|
43760
|
+
const idx = requests.length;
|
|
43761
|
+
requests.push({
|
|
43762
|
+
url: pending.url,
|
|
43763
|
+
transferSize: 0,
|
|
43764
|
+
// Will be set by loadingFinished
|
|
43765
|
+
failed: false,
|
|
43766
|
+
resourceType: pending.resourceType
|
|
43767
|
+
});
|
|
43768
|
+
requestIdToIndex.set(params.requestId, idx);
|
|
43769
|
+
}
|
|
43770
|
+
});
|
|
43771
|
+
client.on("Network.loadingFinished", (params) => {
|
|
43772
|
+
const idx = requestIdToIndex.get(params.requestId);
|
|
43773
|
+
if (idx !== void 0) {
|
|
43774
|
+
requests[idx].transferSize = params.encodedDataLength || 0;
|
|
43775
|
+
} else {
|
|
43776
|
+
const pending = pendingRequests.get(params.requestId);
|
|
43777
|
+
if (pending) {
|
|
43778
|
+
requests.push({
|
|
43779
|
+
url: pending.url,
|
|
43780
|
+
transferSize: params.encodedDataLength || 0,
|
|
43781
|
+
failed: false,
|
|
43782
|
+
resourceType: pending.resourceType
|
|
43783
|
+
});
|
|
43784
|
+
}
|
|
43785
|
+
}
|
|
43786
|
+
pendingRequests.delete(params.requestId);
|
|
43787
|
+
requestIdToIndex.delete(params.requestId);
|
|
43788
|
+
});
|
|
43789
|
+
client.on("Network.loadingFailed", (params) => {
|
|
43790
|
+
const pending = pendingRequests.get(params.requestId);
|
|
43791
|
+
if (pending) {
|
|
43792
|
+
requests.push({
|
|
43793
|
+
url: pending.url,
|
|
43794
|
+
transferSize: 0,
|
|
43795
|
+
failed: true,
|
|
43796
|
+
resourceType: pending.resourceType
|
|
43797
|
+
});
|
|
43798
|
+
}
|
|
43799
|
+
pendingRequests.delete(params.requestId);
|
|
43800
|
+
requestIdToIndex.delete(params.requestId);
|
|
43801
|
+
});
|
|
43802
|
+
return {
|
|
43803
|
+
getResults: () => {
|
|
43804
|
+
const thirdParty = requests.filter(
|
|
43805
|
+
(r) => !r.failed && isThirdPartyUrl(r.url, pageUrl)
|
|
43806
|
+
).length;
|
|
43807
|
+
return { requests, thirdParty };
|
|
43808
|
+
}
|
|
43809
|
+
};
|
|
43810
|
+
}
|
|
43811
|
+
async function testWithProfile(url, profile) {
|
|
43812
|
+
const start = Date.now();
|
|
43813
|
+
const timeout = getTimeoutForProfile(profile);
|
|
43814
|
+
let timeoutTimer;
|
|
43815
|
+
let trackingRef;
|
|
43816
|
+
try {
|
|
43817
|
+
const loadResult = await Promise.race([
|
|
43818
|
+
getPageAndHtml(url, {
|
|
43819
|
+
beforeNavigate: async (page) => {
|
|
43820
|
+
const client = await page.target().createCDPSession();
|
|
43821
|
+
trackingRef = await setupNetworkTracking(client, url);
|
|
43822
|
+
if (profile.downloadThroughput >= 0 || profile.offline) {
|
|
43823
|
+
await client.send("Network.emulateNetworkConditions", {
|
|
43824
|
+
offline: profile.offline || false,
|
|
43825
|
+
downloadThroughput: profile.downloadThroughput,
|
|
43826
|
+
uploadThroughput: profile.uploadThroughput,
|
|
43827
|
+
latency: profile.latency
|
|
43828
|
+
});
|
|
43829
|
+
}
|
|
43830
|
+
}
|
|
43831
|
+
}),
|
|
43832
|
+
new Promise((_, reject) => {
|
|
43833
|
+
timeoutTimer = setTimeout(
|
|
43834
|
+
() => reject(new Error("NETWORK_TEST_TIMEOUT")),
|
|
43835
|
+
timeout
|
|
43836
|
+
);
|
|
43837
|
+
})
|
|
43838
|
+
]);
|
|
43839
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
43840
|
+
const performanceTiming = await loadResult.page.evaluate(() => {
|
|
43841
|
+
const timing = performance.getEntriesByType("navigation")[0];
|
|
43842
|
+
if (!timing) return null;
|
|
43843
|
+
return {
|
|
43844
|
+
domContentLoaded: timing.domContentLoadedEventEnd - timing.startTime
|
|
43845
|
+
};
|
|
43846
|
+
}).catch(() => null);
|
|
43847
|
+
const fcpMs = await loadResult.page.evaluate(() => {
|
|
43848
|
+
const fcp = performance.getEntriesByName("first-contentful-paint")[0];
|
|
43849
|
+
return fcp ? fcp.startTime : null;
|
|
43850
|
+
}).catch(() => null);
|
|
43851
|
+
const networkData = trackingRef ? trackingRef.getResults() : { requests: [], thirdParty: 0 };
|
|
43852
|
+
const loadTimeMs = Date.now() - start;
|
|
43853
|
+
const totalTransferBytes = networkData.requests.reduce((sum, r) => sum + r.transferSize, 0);
|
|
43854
|
+
const failedRequests = networkData.requests.filter((r) => r.failed).length;
|
|
43855
|
+
const profileResult = {
|
|
43856
|
+
profile: profile.name,
|
|
43857
|
+
label: profile.label,
|
|
43858
|
+
loadTimeMs,
|
|
43859
|
+
firstContentfulPaintMs: typeof fcpMs === "number" ? Math.round(fcpMs) : null,
|
|
43860
|
+
domContentLoadedMs: performanceTiming ? Math.round(performanceTiming.domContentLoaded) : null,
|
|
43861
|
+
totalRequests: networkData.requests.length,
|
|
43862
|
+
totalTransferBytes,
|
|
43863
|
+
failedRequests,
|
|
43864
|
+
timedOut: false
|
|
43865
|
+
};
|
|
43866
|
+
return {
|
|
43867
|
+
result: profileResult,
|
|
43868
|
+
html: loadResult.html,
|
|
43869
|
+
page: loadResult.page,
|
|
43870
|
+
cleanup: loadResult.cleanup,
|
|
43871
|
+
networkData
|
|
43872
|
+
};
|
|
43873
|
+
} catch (err) {
|
|
43874
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
43875
|
+
const isTimeout = err instanceof Error && (err.message === "NETWORK_TEST_TIMEOUT" || err.message.includes("timeout") || err.message.includes("Navigation timeout"));
|
|
43876
|
+
if (isDev2) {
|
|
43877
|
+
console.log(
|
|
43878
|
+
`[NetworkTest] Profil "${profile.name}" ${isTimeout ? "Timeout" : "Fehler"}: ${err instanceof Error ? err.message : String(err)}`
|
|
43879
|
+
);
|
|
43880
|
+
}
|
|
43881
|
+
return {
|
|
43882
|
+
result: {
|
|
43883
|
+
profile: profile.name,
|
|
43884
|
+
label: profile.label,
|
|
43885
|
+
loadTimeMs: Date.now() - start,
|
|
43886
|
+
firstContentfulPaintMs: null,
|
|
43887
|
+
domContentLoadedMs: null,
|
|
43888
|
+
totalRequests: 0,
|
|
43889
|
+
totalTransferBytes: 0,
|
|
43890
|
+
failedRequests: 0,
|
|
43891
|
+
timedOut: isTimeout
|
|
43892
|
+
}
|
|
43893
|
+
};
|
|
43894
|
+
}
|
|
43895
|
+
}
|
|
43896
|
+
async function analyzeBaselineMetrics(html3, page, networkData) {
|
|
43897
|
+
const $2 = load(html3);
|
|
43898
|
+
let hasServiceWorker = false;
|
|
43899
|
+
if (page) {
|
|
43900
|
+
try {
|
|
43901
|
+
hasServiceWorker = await page.evaluate(
|
|
43902
|
+
() => "serviceWorker" in navigator && navigator.serviceWorker.controller !== null
|
|
43903
|
+
);
|
|
43904
|
+
} catch {
|
|
43905
|
+
hasServiceWorker = false;
|
|
43906
|
+
}
|
|
43907
|
+
}
|
|
43908
|
+
const hasFontDisplaySwap = html3.includes("font-display") && (html3.includes("swap") || html3.includes("optional"));
|
|
43909
|
+
const allImages = $2("img");
|
|
43910
|
+
const lazyImages = $2('img[loading="lazy"]');
|
|
43911
|
+
const imageCount = allImages.length;
|
|
43912
|
+
const imagesWithoutLazy = imageCount - lazyImages.length;
|
|
43913
|
+
const hasLazyLoading = lazyImages.length > 0;
|
|
43914
|
+
const hasCriticalCssInline = $2("head style").length > 0;
|
|
43915
|
+
const hasLoadingIndicators = $2(
|
|
43916
|
+
'[class*="skeleton"], [class*="loading"], [class*="spinner"], [class*="loader"], [aria-busy="true"], [class*="placeholder"], [class*="Skeleton"], [class*="Loading"], [class*="Spinner"], [class*="Loader"], [class*="Placeholder"]'
|
|
43917
|
+
).length > 0;
|
|
43918
|
+
const networkFontCount = networkData ? networkData.requests.filter(
|
|
43919
|
+
(r) => !r.failed && (r.resourceType === "Font" || /\.(woff2?|ttf|otf|eot)(\?|$)/i.test(r.url))
|
|
43920
|
+
).length : 0;
|
|
43921
|
+
let htmlFontCount = 0;
|
|
43922
|
+
if (networkFontCount === 0) {
|
|
43923
|
+
$2("link[href]").each((_, el) => {
|
|
43924
|
+
const href = $2(el).attr("href") || "";
|
|
43925
|
+
if (/\.(woff2?|ttf|otf|eot)(\?|$)/i.test(href) || /fonts\.(googleapis|gstatic|bunny)\.net/i.test(href) || /use\.typekit\.net/i.test(href)) {
|
|
43926
|
+
htmlFontCount++;
|
|
43927
|
+
}
|
|
43928
|
+
});
|
|
43929
|
+
const fontFaceMatches = html3.match(/@font-face/gi);
|
|
43930
|
+
if (fontFaceMatches) {
|
|
43931
|
+
htmlFontCount += fontFaceMatches.length;
|
|
43932
|
+
}
|
|
43933
|
+
}
|
|
43934
|
+
const webFontCount = networkFontCount > 0 ? networkFontCount : htmlFontCount;
|
|
43935
|
+
const totalPageSizeBytes = networkData ? networkData.requests.reduce((sum, r) => sum + r.transferSize, 0) : 0;
|
|
43936
|
+
const thirdPartyRequests = networkData ? networkData.thirdParty : 0;
|
|
43937
|
+
return {
|
|
43938
|
+
hasServiceWorker,
|
|
43939
|
+
hasFontDisplaySwap,
|
|
43940
|
+
hasLazyLoading,
|
|
43941
|
+
hasCriticalCssInline,
|
|
43942
|
+
hasLoadingIndicators,
|
|
43943
|
+
totalPageSizeBytes,
|
|
43944
|
+
imageCount,
|
|
43945
|
+
imagesWithoutLazy,
|
|
43946
|
+
webFontCount,
|
|
43947
|
+
thirdPartyRequests
|
|
43948
|
+
};
|
|
43949
|
+
}
|
|
43950
|
+
function generateFindings(profiles, metrics) {
|
|
43951
|
+
const findings = [];
|
|
43952
|
+
let findingId = 0;
|
|
43953
|
+
const nextId2 = () => `net-${++findingId}`;
|
|
43954
|
+
const baseline = profiles.find((p) => p.profile === "fast");
|
|
43955
|
+
const profile3g = profiles.find((p) => p.profile === "3g");
|
|
43956
|
+
const profileSlow3g = profiles.find((p) => p.profile === "slow3g");
|
|
43957
|
+
const profileOffline = profiles.find((p) => p.profile === "offline");
|
|
43958
|
+
if (profile3g) {
|
|
43959
|
+
if (profile3g.timedOut || profile3g.loadTimeMs > 3e4) {
|
|
43960
|
+
findings.push({
|
|
43961
|
+
id: nextId2(),
|
|
43962
|
+
category: "ladezeit",
|
|
43963
|
+
severity: "critical",
|
|
43964
|
+
title: "Seite nicht ladbar auf 3G-Verbindung",
|
|
43965
|
+
description: `Die Seite konnte innerhalb von 30 Sekunden auf einer 3G-Verbindung nicht geladen werden. Nutzer mit langsamer Mobilfunkverbindung k\xF6nnen die Seite nicht verwenden.`,
|
|
43966
|
+
value: profile3g.timedOut ? "Timeout" : `${Math.round(profile3g.loadTimeMs / 1e3)}s`,
|
|
43967
|
+
recommendation: "Seitengr\xF6\xDFe drastisch reduzieren. Kritische Ressourcen priorisieren, nicht ben\xF6tigte Skripte und Stylesheets entfernen oder versp\xE4tet laden."
|
|
43968
|
+
});
|
|
43969
|
+
} else if (profile3g.loadTimeMs > 1e4) {
|
|
43970
|
+
findings.push({
|
|
43971
|
+
id: nextId2(),
|
|
43972
|
+
category: "ladezeit",
|
|
43973
|
+
severity: "serious",
|
|
43974
|
+
title: "Seite l\xE4dt \xFCber 10 Sekunden auf 3G",
|
|
43975
|
+
description: `Die Ladezeit auf einer 3G-Verbindung betr\xE4gt ${Math.round(
|
|
43976
|
+
profile3g.loadTimeMs / 1e3
|
|
43977
|
+
)} Sekunden. Nutzer mit langsamer Verbindung erleben eine erhebliche Verz\xF6gerung.`,
|
|
43978
|
+
value: `${Math.round(profile3g.loadTimeMs / 1e3)}s`,
|
|
43979
|
+
recommendation: "Bilder komprimieren, CSS/JS minimieren und unn\xF6tige Third-Party-Ressourcen entfernen. Code-Splitting und Lazy Loading einsetzen."
|
|
43980
|
+
});
|
|
43981
|
+
} else if (profile3g.loadTimeMs > 5e3) {
|
|
43982
|
+
findings.push({
|
|
43983
|
+
id: nextId2(),
|
|
43984
|
+
category: "ladezeit",
|
|
43985
|
+
severity: "moderate",
|
|
43986
|
+
title: "Seite l\xE4dt \xFCber 5 Sekunden auf 3G",
|
|
43987
|
+
description: `Die Ladezeit auf einer 3G-Verbindung betr\xE4gt ${Math.round(
|
|
43988
|
+
profile3g.loadTimeMs / 1e3
|
|
43989
|
+
)} Sekunden. Nutzer mit langsamer Verbindung m\xFCssen deutlich warten.`,
|
|
43990
|
+
value: `${Math.round(profile3g.loadTimeMs / 1e3)}s`,
|
|
43991
|
+
recommendation: "Ressourcen optimieren: Bilder als WebP/AVIF bereitstellen, CSS und JavaScript b\xFCndeln und komprimieren."
|
|
43992
|
+
});
|
|
43993
|
+
}
|
|
43994
|
+
}
|
|
43995
|
+
if (profileSlow3g) {
|
|
43996
|
+
if (profileSlow3g.timedOut || profileSlow3g.loadTimeMs > 45e3) {
|
|
43997
|
+
findings.push({
|
|
43998
|
+
id: nextId2(),
|
|
43999
|
+
category: "ladezeit",
|
|
44000
|
+
severity: "serious",
|
|
44001
|
+
title: "Seite nicht ladbar auf langsamem 3G",
|
|
44002
|
+
description: "Die Seite konnte innerhalb von 45 Sekunden auf einer langsamen 3G-Verbindung nicht geladen werden.",
|
|
44003
|
+
value: "Timeout",
|
|
44004
|
+
recommendation: "Eine stark reduzierte Version der Seite f\xFCr langsame Verbindungen anbieten. Progressive Enhancement einsetzen."
|
|
44005
|
+
});
|
|
44006
|
+
}
|
|
44007
|
+
}
|
|
44008
|
+
if (baseline && baseline.loadTimeMs > 3e3) {
|
|
44009
|
+
findings.push({
|
|
44010
|
+
id: nextId2(),
|
|
44011
|
+
category: "ladezeit",
|
|
44012
|
+
severity: "moderate",
|
|
44013
|
+
title: "Baseline-Ladezeit \xFCber 3 Sekunden",
|
|
44014
|
+
description: `Selbst auf einer schnellen Verbindung betr\xE4gt die Ladezeit ${Math.round(
|
|
44015
|
+
baseline.loadTimeMs / 1e3
|
|
44016
|
+
)} Sekunden. Das deutet auf grundlegende Performanceprobleme hin.`,
|
|
44017
|
+
value: `${Math.round(baseline.loadTimeMs / 1e3)}s`,
|
|
44018
|
+
recommendation: "Server-Antwortzeit pr\xFCfen, Render-blockierende Ressourcen eliminieren, Caching-Header setzen."
|
|
44019
|
+
});
|
|
44020
|
+
}
|
|
44021
|
+
if (!metrics.hasServiceWorker) {
|
|
44022
|
+
findings.push({
|
|
44023
|
+
id: nextId2(),
|
|
44024
|
+
category: "offline",
|
|
44025
|
+
severity: "moderate",
|
|
44026
|
+
title: "Kein Service Worker registriert",
|
|
44027
|
+
description: "Die Seite verwendet keinen Service Worker. Ohne Service Worker ist keine Offline-Unterst\xFCtzung, kein Caching und keine Push-Benachrichtigungen m\xF6glich.",
|
|
44028
|
+
recommendation: "Einen Service Worker implementieren, der kritische Ressourcen cachet und eine Offline-Fallback-Seite bereitstellt."
|
|
44029
|
+
});
|
|
44030
|
+
}
|
|
44031
|
+
if (profileOffline) {
|
|
44032
|
+
const hasOfflineFallback = !profileOffline.timedOut && profileOffline.totalRequests > 0;
|
|
44033
|
+
if (profileOffline.timedOut && !hasOfflineFallback) {
|
|
44034
|
+
findings.push({
|
|
44035
|
+
id: nextId2(),
|
|
44036
|
+
category: "offline",
|
|
44037
|
+
severity: "moderate",
|
|
44038
|
+
title: "Keine Offline-Fallback-Seite",
|
|
44039
|
+
description: "Wenn die Netzwerkverbindung ausf\xE4llt, sieht der Nutzer nur eine Browser-Fehlermeldung. Eine benutzerdefinierte Offline-Seite verbessert die Nutzererfahrung erheblich.",
|
|
44040
|
+
recommendation: "Eine Offline-Fallback-Seite im Service Worker hinterlegen, die dem Nutzer erkl\xE4rt, dass keine Verbindung besteht und welche Inhalte offline verf\xFCgbar sind."
|
|
44041
|
+
});
|
|
44042
|
+
}
|
|
44043
|
+
}
|
|
44044
|
+
if (metrics.imagesWithoutLazy > 3) {
|
|
44045
|
+
findings.push({
|
|
44046
|
+
id: nextId2(),
|
|
44047
|
+
category: "bilder",
|
|
44048
|
+
severity: "moderate",
|
|
44049
|
+
title: `${metrics.imagesWithoutLazy} Bilder ohne Lazy Loading`,
|
|
44050
|
+
description: `Von ${metrics.imageCount} Bildern auf der Seite verwenden ${metrics.imagesWithoutLazy} kein loading="lazy"-Attribut. Alle Bilder werden sofort geladen, auch wenn sie nicht im sichtbaren Bereich sind.`,
|
|
44051
|
+
value: `${metrics.imagesWithoutLazy}/${metrics.imageCount}`,
|
|
44052
|
+
recommendation: 'Allen Bildern au\xDFerhalb des sichtbaren Bereichs (below the fold) das Attribut loading="lazy" hinzuf\xFCgen. Bilder im sofort sichtbaren Bereich k\xF6nnen ohne Lazy Loading bleiben.'
|
|
44053
|
+
});
|
|
44054
|
+
}
|
|
44055
|
+
if (metrics.totalPageSizeBytes > 5 * 1024 * 1024) {
|
|
44056
|
+
findings.push({
|
|
44057
|
+
id: nextId2(),
|
|
44058
|
+
category: "groesse",
|
|
44059
|
+
severity: "critical",
|
|
44060
|
+
title: "Seitengr\xF6\xDFe \xFCber 5 MB",
|
|
44061
|
+
description: `Die Gesamtgr\xF6\xDFe aller Ressourcen betr\xE4gt ${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB. Das f\xFChrt zu extrem langen Ladezeiten auf mobilen Verbindungen und verbraucht das Datenvolumen der Nutzer.`,
|
|
44062
|
+
value: `${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB`,
|
|
44063
|
+
recommendation: "Bilder als moderne Formate (WebP/AVIF) bereitstellen, Videos nur bei Bedarf laden, JavaScript-Bundles aufteilen und Tree-Shaking einsetzen."
|
|
44064
|
+
});
|
|
44065
|
+
} else if (metrics.totalPageSizeBytes > 3 * 1024 * 1024) {
|
|
44066
|
+
findings.push({
|
|
44067
|
+
id: nextId2(),
|
|
44068
|
+
category: "groesse",
|
|
44069
|
+
severity: "serious",
|
|
44070
|
+
title: "Seitengr\xF6\xDFe \xFCber 3 MB",
|
|
44071
|
+
description: `Die Gesamtgr\xF6\xDFe aller Ressourcen betr\xE4gt ${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB. Auf langsameren Verbindungen f\xFChrt das zu sp\xFCrbaren Wartezeiten.`,
|
|
44072
|
+
value: `${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB`,
|
|
44073
|
+
recommendation: "Bilder komprimieren, ungenutztes CSS/JS entfernen, Schriftarten auf ben\xF6tigte Zeichen beschr\xE4nken (Subsetting)."
|
|
44074
|
+
});
|
|
44075
|
+
}
|
|
44076
|
+
if (!metrics.hasFontDisplaySwap && metrics.webFontCount > 0) {
|
|
44077
|
+
findings.push({
|
|
44078
|
+
id: nextId2(),
|
|
44079
|
+
category: "schriften",
|
|
44080
|
+
severity: "moderate",
|
|
44081
|
+
title: "Webfonts blockieren Rendering",
|
|
44082
|
+
description: `Es werden ${metrics.webFontCount} Webfonts geladen, aber font-display: swap fehlt. Bis die Schriften geladen sind, wird kein Text angezeigt (Flash of Invisible Text).`,
|
|
44083
|
+
value: `${metrics.webFontCount} Fonts`,
|
|
44084
|
+
recommendation: "In allen @font-face-Regeln font-display: swap oder font-display: optional setzen, damit Text sofort mit einer Ersatzschrift angezeigt wird."
|
|
44085
|
+
});
|
|
44086
|
+
}
|
|
44087
|
+
if (metrics.webFontCount > 4) {
|
|
44088
|
+
findings.push({
|
|
44089
|
+
id: nextId2(),
|
|
44090
|
+
category: "schriften",
|
|
44091
|
+
severity: "minor",
|
|
44092
|
+
title: "Viele Webfonts geladen",
|
|
44093
|
+
description: `Es werden ${metrics.webFontCount} Webfont-Dateien geladen. Jede zus\xE4tzliche Schrift verl\xE4ngert die Ladezeit.`,
|
|
44094
|
+
value: `${metrics.webFontCount} Fonts`,
|
|
44095
|
+
recommendation: "Anzahl der Schriftvarianten reduzieren. Variable Fonts verwenden, um mehrere Schnitte in einer Datei zu b\xFCndeln."
|
|
44096
|
+
});
|
|
44097
|
+
}
|
|
44098
|
+
if (!metrics.hasCriticalCssInline && baseline && baseline.loadTimeMs > 3e3) {
|
|
44099
|
+
findings.push({
|
|
44100
|
+
id: nextId2(),
|
|
44101
|
+
category: "rendering",
|
|
44102
|
+
severity: "minor",
|
|
44103
|
+
title: "Kein kritisches CSS inline",
|
|
44104
|
+
description: "Es befindet sich kein eingebettetes CSS im <head>-Bereich. Der Browser muss erst externe Stylesheets laden, bevor er die Seite rendern kann, was den First Paint verz\xF6gert.",
|
|
44105
|
+
recommendation: "Kritisches CSS (Above-the-fold-Styles) direkt im <head> als <style>-Block einbetten. Restliches CSS asynchron nachladen."
|
|
44106
|
+
});
|
|
44107
|
+
}
|
|
44108
|
+
if (!metrics.hasLoadingIndicators) {
|
|
44109
|
+
findings.push({
|
|
44110
|
+
id: nextId2(),
|
|
44111
|
+
category: "ux",
|
|
44112
|
+
severity: "moderate",
|
|
44113
|
+
title: "Keine Lade-Indikatoren erkannt",
|
|
44114
|
+
description: "Die Seite scheint keine Skeleton Screens, Spinner oder andere Ladezustandsanzeigen zu verwenden. Bei langsamen Verbindungen sieht der Nutzer m\xF6glicherweise eine leere oder unvollst\xE4ndige Seite.",
|
|
44115
|
+
recommendation: 'Skeleton Screens oder Spinner f\xFCr asynchron geladene Inhalte einsetzen. Das Attribut aria-busy="true" verwenden, damit Screenreader den Ladezustand erkennen.'
|
|
44116
|
+
});
|
|
44117
|
+
}
|
|
44118
|
+
if (metrics.thirdPartyRequests > 20) {
|
|
44119
|
+
findings.push({
|
|
44120
|
+
id: nextId2(),
|
|
44121
|
+
category: "third-party",
|
|
44122
|
+
severity: "serious",
|
|
44123
|
+
title: "\xDCber 20 Third-Party-Requests",
|
|
44124
|
+
description: `Die Seite l\xE4dt ${metrics.thirdPartyRequests} Ressourcen von Drittanbieter-Domains. Jeder zus\xE4tzliche DNS-Lookup und TLS-Handshake verl\xE4ngert die Ladezeit und erh\xF6ht das Risiko von Single Points of Failure.`,
|
|
44125
|
+
value: `${metrics.thirdPartyRequests} Requests`,
|
|
44126
|
+
recommendation: "Third-Party-Skripte auf Notwendigkeit pr\xFCfen. Nicht ben\xF6tigte Tracker und Widgets entfernen. F\xFCr unvermeidbare Drittanbieter Resource Hints (dns-prefetch, preconnect) setzen."
|
|
44127
|
+
});
|
|
44128
|
+
} else if (metrics.thirdPartyRequests > 10) {
|
|
44129
|
+
findings.push({
|
|
44130
|
+
id: nextId2(),
|
|
44131
|
+
category: "third-party",
|
|
44132
|
+
severity: "moderate",
|
|
44133
|
+
title: "Viele Third-Party-Requests",
|
|
44134
|
+
description: `Die Seite l\xE4dt ${metrics.thirdPartyRequests} Ressourcen von Drittanbieter-Domains. Das kann die Ladezeit negativ beeinflussen.`,
|
|
44135
|
+
value: `${metrics.thirdPartyRequests} Requests`,
|
|
44136
|
+
recommendation: "Pr\xFCfen, ob alle eingebundenen Drittanbieter-Skripte wirklich ben\xF6tigt werden. Resource Hints (dns-prefetch, preconnect) f\xFCr wichtige Drittanbieter setzen."
|
|
44137
|
+
});
|
|
44138
|
+
}
|
|
44139
|
+
return findings;
|
|
44140
|
+
}
|
|
44141
|
+
async function runNetworkTest(url) {
|
|
44142
|
+
if (isDev2) console.log(`[NetworkTest] Starte Netzwerk-Resilienztest f\xFCr: ${url}`);
|
|
44143
|
+
const findings = [];
|
|
44144
|
+
const profileResults = [];
|
|
44145
|
+
let baselineHtml;
|
|
44146
|
+
let baselinePage;
|
|
44147
|
+
let baselineCleanup;
|
|
44148
|
+
let baselineNetworkData;
|
|
44149
|
+
const fastProfile = NETWORK_PROFILES.find((p) => p.name === "fast");
|
|
44150
|
+
try {
|
|
44151
|
+
if (isDev2) console.log(`[NetworkTest] Teste Profil: ${fastProfile.label}`);
|
|
44152
|
+
const baselineResult = await testWithProfile(url, fastProfile);
|
|
44153
|
+
profileResults.push(baselineResult.result);
|
|
44154
|
+
baselineHtml = baselineResult.html;
|
|
44155
|
+
baselinePage = baselineResult.page;
|
|
44156
|
+
baselineCleanup = baselineResult.cleanup;
|
|
44157
|
+
baselineNetworkData = baselineResult.networkData;
|
|
44158
|
+
} catch (err) {
|
|
44159
|
+
if (isDev2) {
|
|
44160
|
+
console.log(
|
|
44161
|
+
`[NetworkTest] Baseline-Test fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`
|
|
44162
|
+
);
|
|
44163
|
+
}
|
|
44164
|
+
profileResults.push({
|
|
44165
|
+
profile: fastProfile.name,
|
|
44166
|
+
label: fastProfile.label,
|
|
44167
|
+
loadTimeMs: 0,
|
|
44168
|
+
firstContentfulPaintMs: null,
|
|
44169
|
+
domContentLoadedMs: null,
|
|
44170
|
+
totalRequests: 0,
|
|
44171
|
+
totalTransferBytes: 0,
|
|
44172
|
+
failedRequests: 0,
|
|
44173
|
+
timedOut: true
|
|
44174
|
+
});
|
|
44175
|
+
}
|
|
44176
|
+
if (baselineCleanup) {
|
|
44177
|
+
try {
|
|
44178
|
+
await baselineCleanup();
|
|
44179
|
+
} catch {
|
|
44180
|
+
}
|
|
44181
|
+
baselineCleanup = void 0;
|
|
44182
|
+
baselinePage = void 0;
|
|
44183
|
+
}
|
|
44184
|
+
const throttledProfiles = NETWORK_PROFILES.filter((p) => p.name !== "fast");
|
|
44185
|
+
for (const profile of throttledProfiles) {
|
|
44186
|
+
try {
|
|
44187
|
+
if (isDev2) console.log(`[NetworkTest] Teste Profil: ${profile.label}`);
|
|
44188
|
+
const testResult = await testWithProfile(url, profile);
|
|
44189
|
+
profileResults.push(testResult.result);
|
|
44190
|
+
if (testResult.cleanup) {
|
|
44191
|
+
try {
|
|
44192
|
+
await testResult.cleanup();
|
|
44193
|
+
} catch {
|
|
44194
|
+
}
|
|
44195
|
+
}
|
|
44196
|
+
} catch (err) {
|
|
44197
|
+
if (isDev2) {
|
|
44198
|
+
console.log(
|
|
44199
|
+
`[NetworkTest] Profil "${profile.label}" fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`
|
|
44200
|
+
);
|
|
44201
|
+
}
|
|
44202
|
+
profileResults.push({
|
|
44203
|
+
profile: profile.name,
|
|
44204
|
+
label: profile.label,
|
|
44205
|
+
loadTimeMs: 0,
|
|
44206
|
+
firstContentfulPaintMs: null,
|
|
44207
|
+
domContentLoadedMs: null,
|
|
44208
|
+
totalRequests: 0,
|
|
44209
|
+
totalTransferBytes: 0,
|
|
44210
|
+
failedRequests: 0,
|
|
44211
|
+
timedOut: true
|
|
44212
|
+
});
|
|
44213
|
+
}
|
|
44214
|
+
}
|
|
44215
|
+
const metrics = await analyzeBaselineMetrics(
|
|
44216
|
+
baselineHtml || "<html><body></body></html>",
|
|
44217
|
+
void 0,
|
|
44218
|
+
// Page ist bereits geschlossen
|
|
44219
|
+
baselineNetworkData
|
|
44220
|
+
);
|
|
44221
|
+
const offlineProfileResult = profileResults.find((p) => p.profile === "offline");
|
|
44222
|
+
const hasOfflineFallback = metrics.hasServiceWorker && offlineProfileResult !== void 0 && !offlineProfileResult.timedOut && offlineProfileResult.totalRequests > 0;
|
|
44223
|
+
const generatedFindings = generateFindings(profileResults, metrics);
|
|
44224
|
+
findings.push(...generatedFindings);
|
|
44225
|
+
const score = calculateNetworkScore(findings);
|
|
44226
|
+
if (isDev2) {
|
|
44227
|
+
console.log(
|
|
44228
|
+
`[NetworkTest] Abgeschlossen. Score: ${score}/100, ${findings.length} Befunde`
|
|
44229
|
+
);
|
|
44230
|
+
}
|
|
44231
|
+
return {
|
|
44232
|
+
url,
|
|
44233
|
+
testedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
44234
|
+
score,
|
|
44235
|
+
profiles: profileResults,
|
|
44236
|
+
findings,
|
|
44237
|
+
metrics: {
|
|
44238
|
+
hasServiceWorker: metrics.hasServiceWorker,
|
|
44239
|
+
hasOfflineFallback,
|
|
44240
|
+
hasLoadingIndicators: metrics.hasLoadingIndicators,
|
|
44241
|
+
hasFontDisplaySwap: metrics.hasFontDisplaySwap,
|
|
44242
|
+
hasLazyLoading: metrics.hasLazyLoading,
|
|
44243
|
+
hasCriticalCssInline: metrics.hasCriticalCssInline,
|
|
44244
|
+
totalPageSizeBytes: metrics.totalPageSizeBytes,
|
|
44245
|
+
imageCount: metrics.imageCount,
|
|
44246
|
+
imagesWithoutLazy: metrics.imagesWithoutLazy,
|
|
44247
|
+
webFontCount: metrics.webFontCount,
|
|
44248
|
+
thirdPartyRequests: metrics.thirdPartyRequests
|
|
44249
|
+
}
|
|
44250
|
+
};
|
|
44251
|
+
}
|
|
44252
|
+
var isDev2, NETWORK_PROFILES;
|
|
44253
|
+
var init_network_tester = __esm({
|
|
44254
|
+
"../../src/lib/network-tester/index.ts"() {
|
|
44255
|
+
"use strict";
|
|
44256
|
+
init_esm11();
|
|
44257
|
+
init_browser();
|
|
44258
|
+
isDev2 = process.env.NODE_ENV === "development";
|
|
44259
|
+
NETWORK_PROFILES = [
|
|
44260
|
+
{
|
|
44261
|
+
name: "fast",
|
|
44262
|
+
label: "Schnelles Netz (4G/WiFi)",
|
|
44263
|
+
downloadThroughput: -1,
|
|
44264
|
+
uploadThroughput: -1,
|
|
44265
|
+
latency: 0
|
|
44266
|
+
},
|
|
44267
|
+
{
|
|
44268
|
+
name: "3g",
|
|
44269
|
+
label: "3G Mobilfunk",
|
|
44270
|
+
downloadThroughput: 1500 * 1024 / 8,
|
|
44271
|
+
uploadThroughput: 400 * 1024 / 8,
|
|
44272
|
+
latency: 300
|
|
44273
|
+
},
|
|
44274
|
+
{
|
|
44275
|
+
name: "slow3g",
|
|
44276
|
+
label: "Langsames 3G",
|
|
44277
|
+
downloadThroughput: 500 * 1024 / 8,
|
|
44278
|
+
uploadThroughput: 100 * 1024 / 8,
|
|
44279
|
+
latency: 2e3
|
|
44280
|
+
},
|
|
44281
|
+
{
|
|
44282
|
+
name: "offline",
|
|
44283
|
+
label: "Offline",
|
|
44284
|
+
downloadThroughput: 0,
|
|
44285
|
+
uploadThroughput: 0,
|
|
44286
|
+
latency: 0,
|
|
44287
|
+
offline: true
|
|
44288
|
+
}
|
|
44289
|
+
];
|
|
44290
|
+
}
|
|
44291
|
+
});
|
|
44292
|
+
|
|
43520
44293
|
// ../../src/lib/scanner/crawler.ts
|
|
43521
44294
|
var crawler_exports = {};
|
|
43522
44295
|
__export(crawler_exports, {
|
|
@@ -52564,199 +53337,6 @@ var init_network_privacy = __esm({
|
|
|
52564
53337
|
}
|
|
52565
53338
|
});
|
|
52566
53339
|
|
|
52567
|
-
// ../../src/lib/scanner/browser.ts
|
|
52568
|
-
var browser_exports = {};
|
|
52569
|
-
__export(browser_exports, {
|
|
52570
|
-
getPageAndHtml: () => getPageAndHtml,
|
|
52571
|
-
getRenderedHtml: () => getRenderedHtml
|
|
52572
|
-
});
|
|
52573
|
-
async function launchBrowser() {
|
|
52574
|
-
if (isDev) {
|
|
52575
|
-
const puppeteer = (await import("puppeteer")).default;
|
|
52576
|
-
return puppeteer.launch({ headless: "new", args: CHROME_ARGS });
|
|
52577
|
-
}
|
|
52578
|
-
const puppeteerCore = (await import("puppeteer-core")).default;
|
|
52579
|
-
const chromium = (await import("@sparticuz/chromium")).default;
|
|
52580
|
-
chromium.setGraphicsMode = false;
|
|
52581
|
-
return puppeteerCore.launch({
|
|
52582
|
-
args: [...chromium.args, ...CHROME_ARGS],
|
|
52583
|
-
defaultViewport: { width: 1280, height: 720 },
|
|
52584
|
-
executablePath: await chromium.executablePath(),
|
|
52585
|
-
headless: "new"
|
|
52586
|
-
});
|
|
52587
|
-
}
|
|
52588
|
-
async function waitForHydration(page, maxWait = 1e4) {
|
|
52589
|
-
const interval = 500;
|
|
52590
|
-
const attempts = Math.ceil(maxWait / interval);
|
|
52591
|
-
for (let i = 0; i < attempts; i++) {
|
|
52592
|
-
const ready = await page.evaluate(() => {
|
|
52593
|
-
const anchors = document.querySelectorAll("a[href]");
|
|
52594
|
-
const buttons = document.querySelectorAll("button");
|
|
52595
|
-
const bodyText = document.body?.innerText?.length || 0;
|
|
52596
|
-
return anchors.length > 0 || buttons.length > 0 || bodyText > 500;
|
|
52597
|
-
}).catch(() => false);
|
|
52598
|
-
if (ready) {
|
|
52599
|
-
if (isDev) console.log(`[Browser] Hydration complete after ${(i + 1) * interval}ms`);
|
|
52600
|
-
return;
|
|
52601
|
-
}
|
|
52602
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
52603
|
-
}
|
|
52604
|
-
if (isDev) console.log(`[Browser] Hydration not detected after ${maxWait}ms`);
|
|
52605
|
-
}
|
|
52606
|
-
async function autoScrollForLazyContent(page, maxScrollTime = 3e3) {
|
|
52607
|
-
const start = Date.now();
|
|
52608
|
-
await page.evaluate(async (maxTime) => {
|
|
52609
|
-
await new Promise((resolve) => {
|
|
52610
|
-
const viewportHeight = window.innerHeight;
|
|
52611
|
-
const maxScroll = document.body.scrollHeight;
|
|
52612
|
-
let currentScroll = 0;
|
|
52613
|
-
const step = Math.floor(viewportHeight * 0.8);
|
|
52614
|
-
const timer = setInterval(() => {
|
|
52615
|
-
if (Date.now() - window.__scrollStart > maxTime || currentScroll >= maxScroll) {
|
|
52616
|
-
clearInterval(timer);
|
|
52617
|
-
window.scrollTo(0, 0);
|
|
52618
|
-
resolve();
|
|
52619
|
-
return;
|
|
52620
|
-
}
|
|
52621
|
-
currentScroll += step;
|
|
52622
|
-
window.scrollTo(0, currentScroll);
|
|
52623
|
-
}, 150);
|
|
52624
|
-
window.__scrollStart = Date.now();
|
|
52625
|
-
});
|
|
52626
|
-
}, maxScrollTime);
|
|
52627
|
-
try {
|
|
52628
|
-
await page.waitForNetworkIdle({ idleTime: 500, timeout: 5e3 });
|
|
52629
|
-
} catch {
|
|
52630
|
-
}
|
|
52631
|
-
if (isDev) console.log(`[Browser] Auto-scroll completed in ${Date.now() - start}ms`);
|
|
52632
|
-
}
|
|
52633
|
-
async function extractShadowDomHtml(page) {
|
|
52634
|
-
return page.evaluate(() => {
|
|
52635
|
-
const fragments = [];
|
|
52636
|
-
function extract3(root2) {
|
|
52637
|
-
const elements = Array.from(root2.querySelectorAll("*"));
|
|
52638
|
-
for (const el of elements) {
|
|
52639
|
-
if (el.shadowRoot) {
|
|
52640
|
-
fragments.push(el.shadowRoot.innerHTML);
|
|
52641
|
-
const tempDiv = document.createElement("div");
|
|
52642
|
-
tempDiv.innerHTML = el.shadowRoot.innerHTML;
|
|
52643
|
-
extract3(tempDiv);
|
|
52644
|
-
}
|
|
52645
|
-
}
|
|
52646
|
-
}
|
|
52647
|
-
extract3(document);
|
|
52648
|
-
return { count: fragments.length, html: fragments.join("\n") };
|
|
52649
|
-
});
|
|
52650
|
-
}
|
|
52651
|
-
function attachDebugListeners(page) {
|
|
52652
|
-
if (!isDev) return;
|
|
52653
|
-
page.on("pageerror", (err) => {
|
|
52654
|
-
console.log(`[Browser][JS Error] ${err.message || err}`);
|
|
52655
|
-
});
|
|
52656
|
-
page.on("requestfailed", (req) => {
|
|
52657
|
-
const url = req.url();
|
|
52658
|
-
if (/\.(js|mjs|css)(\?|$)/i.test(url) || url.includes("_next/")) {
|
|
52659
|
-
console.log(`[Browser][Request Failed] ${url} \u2014 ${req.failure()?.errorText || "unknown"}`);
|
|
52660
|
-
}
|
|
52661
|
-
});
|
|
52662
|
-
}
|
|
52663
|
-
async function getRenderedHtml(url) {
|
|
52664
|
-
let browser;
|
|
52665
|
-
try {
|
|
52666
|
-
browser = await launchBrowser();
|
|
52667
|
-
} catch (err) {
|
|
52668
|
-
throw new Error(`Browser launch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
52669
|
-
}
|
|
52670
|
-
try {
|
|
52671
|
-
const page = await browser.newPage();
|
|
52672
|
-
await page.setUserAgent(USER_AGENT);
|
|
52673
|
-
await page.setViewport({ width: 1280, height: 720 });
|
|
52674
|
-
attachDebugListeners(page);
|
|
52675
|
-
const response = await page.goto(url, { waitUntil: "networkidle0", timeout: 3e4 });
|
|
52676
|
-
const httpStatus = response?.status() ?? 0;
|
|
52677
|
-
await waitForHydration(page);
|
|
52678
|
-
await autoScrollForLazyContent(page);
|
|
52679
|
-
const shadow = await extractShadowDomHtml(page);
|
|
52680
|
-
if (isDev && shadow.count > 0) console.log(`[Browser] Extracted ${shadow.count} Shadow DOM roots`);
|
|
52681
|
-
let html3 = await page.content();
|
|
52682
|
-
const title = await page.title();
|
|
52683
|
-
if (shadow.html) {
|
|
52684
|
-
html3 = html3.replace("</body>", `<div data-shadow-content="true">${shadow.html}</div></body>`);
|
|
52685
|
-
}
|
|
52686
|
-
if (isDev) console.log(`[Browser] Rendered ${html3.length} chars, title: "${title}", status: ${httpStatus}`);
|
|
52687
|
-
return { html: html3, title, httpStatus };
|
|
52688
|
-
} finally {
|
|
52689
|
-
await browser.close();
|
|
52690
|
-
}
|
|
52691
|
-
}
|
|
52692
|
-
async function getPageAndHtml(url, options) {
|
|
52693
|
-
let browser;
|
|
52694
|
-
try {
|
|
52695
|
-
browser = await launchBrowser();
|
|
52696
|
-
} catch (err) {
|
|
52697
|
-
throw new Error(`Browser launch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
52698
|
-
}
|
|
52699
|
-
try {
|
|
52700
|
-
const page = await browser.newPage();
|
|
52701
|
-
await page.setUserAgent(USER_AGENT);
|
|
52702
|
-
await page.setViewport({ width: 1280, height: 720 });
|
|
52703
|
-
attachDebugListeners(page);
|
|
52704
|
-
if (options?.beforeNavigate) {
|
|
52705
|
-
options.beforeNavigate(page);
|
|
52706
|
-
}
|
|
52707
|
-
const response = await page.goto(url, { waitUntil: "networkidle0", timeout: 3e4 });
|
|
52708
|
-
const httpStatus = response?.status() ?? 0;
|
|
52709
|
-
const responseHeaders = response?.headers() ?? {};
|
|
52710
|
-
await waitForHydration(page);
|
|
52711
|
-
await autoScrollForLazyContent(page);
|
|
52712
|
-
const shadow = await extractShadowDomHtml(page);
|
|
52713
|
-
if (isDev && shadow.count > 0) console.log(`[Browser] Extracted ${shadow.count} Shadow DOM roots`);
|
|
52714
|
-
let html3 = await page.content();
|
|
52715
|
-
const title = await page.title();
|
|
52716
|
-
if (shadow.html) {
|
|
52717
|
-
html3 = html3.replace("</body>", `<div data-shadow-content="true">${shadow.html}</div></body>`);
|
|
52718
|
-
}
|
|
52719
|
-
if (isDev) console.log(`[Browser] Rendered ${html3.length} chars, title: "${title}", status: ${httpStatus}`);
|
|
52720
|
-
return {
|
|
52721
|
-
page,
|
|
52722
|
-
browser,
|
|
52723
|
-
html: html3,
|
|
52724
|
-
title,
|
|
52725
|
-
httpStatus,
|
|
52726
|
-
responseHeaders,
|
|
52727
|
-
cleanup: async () => {
|
|
52728
|
-
try {
|
|
52729
|
-
await browser.close();
|
|
52730
|
-
} catch {
|
|
52731
|
-
}
|
|
52732
|
-
}
|
|
52733
|
-
};
|
|
52734
|
-
} catch (err) {
|
|
52735
|
-
try {
|
|
52736
|
-
await browser.close();
|
|
52737
|
-
} catch {
|
|
52738
|
-
}
|
|
52739
|
-
throw err;
|
|
52740
|
-
}
|
|
52741
|
-
}
|
|
52742
|
-
var isDev, CHROME_ARGS, USER_AGENT;
|
|
52743
|
-
var init_browser = __esm({
|
|
52744
|
-
"../../src/lib/scanner/browser.ts"() {
|
|
52745
|
-
"use strict";
|
|
52746
|
-
isDev = process.env.NODE_ENV === "development";
|
|
52747
|
-
CHROME_ARGS = [
|
|
52748
|
-
"--no-sandbox",
|
|
52749
|
-
"--disable-setuid-sandbox",
|
|
52750
|
-
"--disable-dev-shm-usage",
|
|
52751
|
-
"--enable-features=NetworkService,NetworkServiceInProcess",
|
|
52752
|
-
"--disable-background-timer-throttling",
|
|
52753
|
-
"--disable-backgrounding-occluded-windows",
|
|
52754
|
-
"--disable-renderer-backgrounding"
|
|
52755
|
-
];
|
|
52756
|
-
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
52757
|
-
}
|
|
52758
|
-
});
|
|
52759
|
-
|
|
52760
53340
|
// ../../src/lib/scanner/cookie-storage-audit.ts
|
|
52761
53341
|
var cookie_storage_audit_exports = {};
|
|
52762
53342
|
__export(cookie_storage_audit_exports, {
|
|
@@ -59787,785 +60367,211 @@ async function scanHtml(html3, url = "local") {
|
|
|
59787
60367
|
try {
|
|
59788
60368
|
rawFindings.push(...rule.run(html3, url, $2));
|
|
59789
60369
|
} catch (ruleErr) {
|
|
59790
|
-
console.error(`[ScanHtml] Rule "${rule.id}" failed:`, ruleErr instanceof Error ? ruleErr.message : ruleErr);
|
|
59791
|
-
}
|
|
59792
|
-
}
|
|
59793
|
-
for (const f of rawFindings) {
|
|
59794
|
-
if (f.legalBasis !== "DSGVO") {
|
|
59795
|
-
f.en301549Reference = getEN301549Reference(f.wcagReference);
|
|
59796
|
-
}
|
|
59797
|
-
}
|
|
59798
|
-
if (detectAccessibilityWidget(html3, $2)) {
|
|
59799
|
-
downgradeContrastFindings(rawFindings);
|
|
59800
|
-
}
|
|
59801
|
-
const allFindings = deduplicateFindings(rawFindings);
|
|
59802
|
-
const findings = allFindings.filter((f) => f.legalBasis !== "DSGVO");
|
|
59803
|
-
const dsgvoFindings = allFindings.filter((f) => f.legalBasis === "DSGVO");
|
|
59804
|
-
const score = calculateScore(rawFindings.filter((f) => f.legalBasis !== "DSGVO"));
|
|
59805
|
-
const dsgvoScore = calculateDsgvoScore(dsgvoFindings);
|
|
59806
|
-
const critical = findings.filter((f) => f.severity === "critical").length;
|
|
59807
|
-
const serious = findings.filter((f) => f.severity === "serious").length;
|
|
59808
|
-
const moderate = findings.filter((f) => f.severity === "moderate").length;
|
|
59809
|
-
const minor = findings.filter((f) => f.severity === "minor").length;
|
|
59810
|
-
return {
|
|
59811
|
-
url,
|
|
59812
|
-
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59813
|
-
score,
|
|
59814
|
-
totalFindings: findings.length,
|
|
59815
|
-
critical,
|
|
59816
|
-
serious,
|
|
59817
|
-
moderate,
|
|
59818
|
-
minor,
|
|
59819
|
-
findings,
|
|
59820
|
-
pageTitle: $2("title").text() || "Kein Titel",
|
|
59821
|
-
loadTimeMs: Date.now() - start,
|
|
59822
|
-
dsgvoFindings: dsgvoFindings.length > 0 ? dsgvoFindings : void 0,
|
|
59823
|
-
dsgvoScore
|
|
59824
|
-
};
|
|
59825
|
-
}
|
|
59826
|
-
function deduplicateFindings(findings) {
|
|
59827
|
-
const MAIN_RELATED_RULES = /* @__PURE__ */ new Set([
|
|
59828
|
-
"axe-landmark-main-is-top-level",
|
|
59829
|
-
"axe-landmark-no-duplicate-main"
|
|
59830
|
-
]);
|
|
59831
|
-
const mainRelated = [];
|
|
59832
|
-
const rest = [];
|
|
59833
|
-
for (const f of findings) {
|
|
59834
|
-
const isMainDuplicateId = f.rule === "structure" && f.id.startsWith("structure-duplicate-id") && f.element?.includes("<main");
|
|
59835
|
-
if (MAIN_RELATED_RULES.has(f.rule) || isMainDuplicateId) {
|
|
59836
|
-
mainRelated.push(f);
|
|
59837
|
-
} else {
|
|
59838
|
-
rest.push(f);
|
|
59839
|
-
}
|
|
59840
|
-
}
|
|
59841
|
-
if (mainRelated.length > 1) {
|
|
59842
|
-
mainRelated.sort((a, b) => {
|
|
59843
|
-
const order = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
59844
|
-
return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
|
|
59845
|
-
});
|
|
59846
|
-
const merged = { ...mainRelated[0] };
|
|
59847
|
-
merged.description = `Mehrere <main>-Elemente auf der Seite (${mainRelated.length} zusammenh\xE4ngende Findings)`;
|
|
59848
|
-
merged.explanation = `Die Seite enth\xE4lt mehrere <main>-Elemente. Dies verursacht zusammenh\xE4ngende Probleme: doppelte IDs, Landmark-Struktur und Screenreader-Navigation. L\xF6sung: Verwenden Sie nur ein einziges <main>-Element pro Seite.`;
|
|
59849
|
-
merged.fix = "Entfernen Sie \xFCberfl\xFCssige <main>-Elemente, sodass nur eines auf der Seite verbleibt.";
|
|
59850
|
-
rest.push(merged);
|
|
59851
|
-
} else {
|
|
59852
|
-
rest.push(...mainRelated);
|
|
59853
|
-
}
|
|
59854
|
-
const groups = {};
|
|
59855
|
-
for (const f of rest) {
|
|
59856
|
-
if (f.id.startsWith("language-all-caps")) {
|
|
59857
|
-
const key = "language::all-caps-group";
|
|
59858
|
-
if (!groups[key]) groups[key] = [];
|
|
59859
|
-
groups[key].push(f);
|
|
59860
|
-
} else {
|
|
59861
|
-
const normalizedDesc = f.description.replace(/\s*\([\d.,:/]+.*?\)\s*$/, "");
|
|
59862
|
-
const key = `${f.rule}::${normalizedDesc}`;
|
|
59863
|
-
if (!groups[key]) groups[key] = [];
|
|
59864
|
-
groups[key].push(f);
|
|
59865
|
-
}
|
|
59866
|
-
}
|
|
59867
|
-
const severityOrder = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
59868
|
-
const result = [];
|
|
59869
|
-
for (const key of Object.keys(groups)) {
|
|
59870
|
-
const group = groups[key];
|
|
59871
|
-
if (key === "language::all-caps-group" && group.length > 1) {
|
|
59872
|
-
const first2 = { ...group[0] };
|
|
59873
|
-
first2.description = `Text in Gro\xDFbuchstaben (${group.length}x auf dieser Seite)`;
|
|
59874
|
-
first2.explanation = `${group.length} Textstellen auf dieser Seite sind vollst\xE4ndig in Gro\xDFbuchstaben geschrieben. Screenreader k\xF6nnen solchen Text als Abk\xFCrzung interpretieren und Buchstabe f\xFCr Buchstabe vorlesen. Verwenden Sie CSS text-transform: uppercase f\xFCr die visuelle Darstellung.`;
|
|
59875
|
-
result.push(first2);
|
|
59876
|
-
} else if (group.length <= 1) {
|
|
59877
|
-
result.push(...group);
|
|
59878
|
-
} else {
|
|
59879
|
-
group.sort((a, b) => (severityOrder[a.severity] ?? 4) - (severityOrder[b.severity] ?? 4));
|
|
59880
|
-
const first2 = { ...group[0] };
|
|
59881
|
-
const baseDesc = first2.description.replace(/\s*\([\d.,:/]+.*?\)\s*$/, "");
|
|
59882
|
-
first2.description = `${baseDesc} (${group.length}x auf dieser Seite)`;
|
|
59883
|
-
first2.explanation = `${first2.explanation}
|
|
59884
|
-
|
|
59885
|
-
Dieses Problem tritt ${group.length} Mal auf dieser Seite auf.`;
|
|
59886
|
-
result.push(first2);
|
|
59887
|
-
}
|
|
59888
|
-
}
|
|
59889
|
-
return result;
|
|
59890
|
-
}
|
|
59891
|
-
function detectAccessibilityWidget(html3, $2) {
|
|
59892
|
-
if ($2('script[src*="acpilot"]').length > 0 || $2('script[src*="accesspilot"]').length > 0) return true;
|
|
59893
|
-
if ($2("[data-acpilot]").length > 0 || $2("[data-accesspilot]").length > 0) return true;
|
|
59894
|
-
if ($2('script[src*="userway"]').length > 0) return true;
|
|
59895
|
-
if ($2('script[src*="accessibe"]').length > 0) return true;
|
|
59896
|
-
if ($2('script[src*="equalweb"]').length > 0) return true;
|
|
59897
|
-
if ($2("#accessibilityWidget, .accessibility-widget, [data-accessibility-widget]").length > 0) return true;
|
|
59898
|
-
if (html3.includes("acpilot") || html3.includes("accesspilot-widget")) return true;
|
|
59899
|
-
return false;
|
|
59900
|
-
}
|
|
59901
|
-
function downgradeContrastFindings(findings) {
|
|
59902
|
-
const WIDGET_HINT = "\n\n\u{1F4A1} Diese Website hat ein Barrierefreiheits-Widget eingebunden, \xFCber das Nutzer den Kontrast manuell anpassen k\xF6nnen. Das Problem wird daher herabgestuft.";
|
|
59903
|
-
for (const f of findings) {
|
|
59904
|
-
const isContrastRule = f.rule === "contrast" || f.rule === "color-contrast" || f.rule === "non-text-contrast" || f.rule.includes("color-contrast");
|
|
59905
|
-
if (!isContrastRule) continue;
|
|
59906
|
-
if (f.severity === "critical") {
|
|
59907
|
-
f.severity = "moderate";
|
|
59908
|
-
} else if (f.severity === "serious" || f.severity === "moderate") {
|
|
59909
|
-
f.severity = "minor";
|
|
59910
|
-
}
|
|
59911
|
-
f.explanation += WIDGET_HINT;
|
|
59912
|
-
}
|
|
59913
|
-
}
|
|
59914
|
-
function calculateDsgvoScore(dsgvoFindings) {
|
|
59915
|
-
if (dsgvoFindings.length === 0) return 100;
|
|
59916
|
-
const penalties = { critical: 20, serious: 12, moderate: 5, minor: 2 };
|
|
59917
|
-
const sorted = [...dsgvoFindings].sort((a, b) => (penalties[b.severity] || 0) - (penalties[a.severity] || 0));
|
|
59918
|
-
let totalPenalty = 0;
|
|
59919
|
-
sorted.forEach((f, i) => {
|
|
59920
|
-
const base = penalties[f.severity] || 3;
|
|
59921
|
-
if (i < 3) {
|
|
59922
|
-
totalPenalty += base;
|
|
59923
|
-
} else if (i < 6) {
|
|
59924
|
-
totalPenalty += base * 0.5;
|
|
59925
|
-
} else {
|
|
59926
|
-
totalPenalty += base * 0.25;
|
|
59927
|
-
}
|
|
59928
|
-
});
|
|
59929
|
-
const cappedPenalty = Math.min(totalPenalty, 92);
|
|
59930
|
-
return Math.max(0, Math.min(100, Math.round(100 - cappedPenalty)));
|
|
59931
|
-
}
|
|
59932
|
-
function calculateCombinedScore(wcagScore, logicScore, srIssueCount) {
|
|
59933
|
-
let srScore = 100;
|
|
59934
|
-
if (srIssueCount != null && srIssueCount > 0) {
|
|
59935
|
-
srScore = Math.max(15, Math.round(100 * Math.exp(-0.1 * srIssueCount)));
|
|
59936
|
-
}
|
|
59937
|
-
if (logicScore != null && srIssueCount != null) {
|
|
59938
|
-
return Math.round(wcagScore * 0.5 + logicScore * 0.25 + srScore * 0.25);
|
|
59939
|
-
}
|
|
59940
|
-
if (logicScore != null) {
|
|
59941
|
-
return Math.round(wcagScore * 0.65 + logicScore * 0.35);
|
|
59942
|
-
}
|
|
59943
|
-
if (srIssueCount != null) {
|
|
59944
|
-
return Math.round(wcagScore * 0.65 + srScore * 0.35);
|
|
59945
|
-
}
|
|
59946
|
-
return wcagScore;
|
|
59947
|
-
}
|
|
59948
|
-
function calculateScore(findings) {
|
|
59949
|
-
const penalties = { critical: 8, serious: 4, moderate: 1.5, minor: 0.5 };
|
|
59950
|
-
const byRule = {};
|
|
59951
|
-
for (const f of findings) {
|
|
59952
|
-
if (!byRule[f.rule]) byRule[f.rule] = [];
|
|
59953
|
-
byRule[f.rule].push(f);
|
|
59954
|
-
}
|
|
59955
|
-
let totalPenalty = 0;
|
|
59956
|
-
for (const rule of Object.keys(byRule)) {
|
|
59957
|
-
const ruleFindings = byRule[rule];
|
|
59958
|
-
ruleFindings.sort((a, b) => penalties[b.severity] - penalties[a.severity]);
|
|
59959
|
-
ruleFindings.forEach((f, i) => {
|
|
59960
|
-
const basePenalty = penalties[f.severity] || 0.5;
|
|
59961
|
-
if (i === 0) {
|
|
59962
|
-
totalPenalty += basePenalty;
|
|
59963
|
-
} else if (i < 3) {
|
|
59964
|
-
totalPenalty += basePenalty * 0.4;
|
|
59965
|
-
} else {
|
|
59966
|
-
totalPenalty += 0.3;
|
|
59967
|
-
}
|
|
59968
|
-
});
|
|
59969
|
-
}
|
|
59970
|
-
const cappedPenalty = Math.min(totalPenalty, 95);
|
|
59971
|
-
return Math.max(0, Math.min(100, Math.round(100 - cappedPenalty)));
|
|
59972
|
-
}
|
|
59973
|
-
var init_scanner = __esm({
|
|
59974
|
-
"../../src/lib/scanner/index.ts"() {
|
|
59975
|
-
"use strict";
|
|
59976
|
-
init_esm11();
|
|
59977
|
-
init_rules();
|
|
59978
|
-
init_en301549_mapping();
|
|
59979
|
-
init_tab_order();
|
|
59980
|
-
init_color_contrast();
|
|
59981
|
-
init_axe_integration();
|
|
59982
|
-
init_network_privacy();
|
|
59983
|
-
init_rules();
|
|
59984
|
-
}
|
|
59985
|
-
});
|
|
59986
|
-
|
|
59987
|
-
// ../../src/lib/network-tester/index.ts
|
|
59988
|
-
var network_tester_exports = {};
|
|
59989
|
-
__export(network_tester_exports, {
|
|
59990
|
-
NETWORK_PROFILES: () => NETWORK_PROFILES,
|
|
59991
|
-
runNetworkTest: () => runNetworkTest
|
|
59992
|
-
});
|
|
59993
|
-
function getTimeoutForProfile(profile) {
|
|
59994
|
-
if (profile.offline) return 1e4;
|
|
59995
|
-
if (profile.name === "slow3g") return 45e3;
|
|
59996
|
-
return 3e4;
|
|
59997
|
-
}
|
|
59998
|
-
function isThirdPartyUrl(requestUrl, pageOrigin) {
|
|
59999
|
-
try {
|
|
60000
|
-
const reqHost = new URL(requestUrl).hostname;
|
|
60001
|
-
const pageHost = new URL(pageOrigin).hostname;
|
|
60002
|
-
return !reqHost.endsWith(pageHost) && !pageHost.endsWith(reqHost);
|
|
60003
|
-
} catch {
|
|
60004
|
-
return false;
|
|
60005
|
-
}
|
|
60006
|
-
}
|
|
60007
|
-
function calculateNetworkScore(findings) {
|
|
60008
|
-
const penalties = {
|
|
60009
|
-
critical: 15,
|
|
60010
|
-
serious: 8,
|
|
60011
|
-
moderate: 4,
|
|
60012
|
-
minor: 1
|
|
60013
|
-
};
|
|
60014
|
-
let total = 0;
|
|
60015
|
-
for (const f of findings) {
|
|
60016
|
-
total += penalties[f.severity] || 1;
|
|
60017
|
-
}
|
|
60018
|
-
return Math.max(0, Math.min(100, 100 - Math.min(total, 85)));
|
|
60019
|
-
}
|
|
60020
|
-
async function setupNetworkTracking(client, pageUrl) {
|
|
60021
|
-
const requests = [];
|
|
60022
|
-
const pendingRequests = /* @__PURE__ */ new Map();
|
|
60023
|
-
const requestIdToIndex = /* @__PURE__ */ new Map();
|
|
60024
|
-
await client.send("Network.enable");
|
|
60025
|
-
client.on("Network.requestWillBeSent", (params) => {
|
|
60026
|
-
pendingRequests.set(params.requestId, {
|
|
60027
|
-
url: params.request.url,
|
|
60028
|
-
resourceType: params.type || "Other"
|
|
60029
|
-
});
|
|
60030
|
-
});
|
|
60031
|
-
client.on("Network.responseReceived", (params) => {
|
|
60032
|
-
const pending = pendingRequests.get(params.requestId);
|
|
60033
|
-
if (pending) {
|
|
60034
|
-
const idx = requests.length;
|
|
60035
|
-
requests.push({
|
|
60036
|
-
url: pending.url,
|
|
60037
|
-
transferSize: 0,
|
|
60038
|
-
// Will be set by loadingFinished
|
|
60039
|
-
failed: false,
|
|
60040
|
-
resourceType: pending.resourceType
|
|
60041
|
-
});
|
|
60042
|
-
requestIdToIndex.set(params.requestId, idx);
|
|
60043
|
-
}
|
|
60044
|
-
});
|
|
60045
|
-
client.on("Network.loadingFinished", (params) => {
|
|
60046
|
-
const idx = requestIdToIndex.get(params.requestId);
|
|
60047
|
-
if (idx !== void 0) {
|
|
60048
|
-
requests[idx].transferSize = params.encodedDataLength || 0;
|
|
60049
|
-
} else {
|
|
60050
|
-
const pending = pendingRequests.get(params.requestId);
|
|
60051
|
-
if (pending) {
|
|
60052
|
-
requests.push({
|
|
60053
|
-
url: pending.url,
|
|
60054
|
-
transferSize: params.encodedDataLength || 0,
|
|
60055
|
-
failed: false,
|
|
60056
|
-
resourceType: pending.resourceType
|
|
60057
|
-
});
|
|
60058
|
-
}
|
|
60059
|
-
}
|
|
60060
|
-
pendingRequests.delete(params.requestId);
|
|
60061
|
-
requestIdToIndex.delete(params.requestId);
|
|
60062
|
-
});
|
|
60063
|
-
client.on("Network.loadingFailed", (params) => {
|
|
60064
|
-
const pending = pendingRequests.get(params.requestId);
|
|
60065
|
-
if (pending) {
|
|
60066
|
-
requests.push({
|
|
60067
|
-
url: pending.url,
|
|
60068
|
-
transferSize: 0,
|
|
60069
|
-
failed: true,
|
|
60070
|
-
resourceType: pending.resourceType
|
|
60071
|
-
});
|
|
60072
|
-
}
|
|
60073
|
-
pendingRequests.delete(params.requestId);
|
|
60074
|
-
requestIdToIndex.delete(params.requestId);
|
|
60075
|
-
});
|
|
60076
|
-
return {
|
|
60077
|
-
getResults: () => {
|
|
60078
|
-
const thirdParty = requests.filter(
|
|
60079
|
-
(r) => !r.failed && isThirdPartyUrl(r.url, pageUrl)
|
|
60080
|
-
).length;
|
|
60081
|
-
return { requests, thirdParty };
|
|
60082
|
-
}
|
|
60083
|
-
};
|
|
60084
|
-
}
|
|
60085
|
-
async function testWithProfile(url, profile) {
|
|
60086
|
-
const start = Date.now();
|
|
60087
|
-
const timeout = getTimeoutForProfile(profile);
|
|
60088
|
-
let timeoutTimer;
|
|
60089
|
-
let trackingRef;
|
|
60090
|
-
try {
|
|
60091
|
-
const loadResult = await Promise.race([
|
|
60092
|
-
getPageAndHtml(url, {
|
|
60093
|
-
beforeNavigate: async (page) => {
|
|
60094
|
-
const client = await page.target().createCDPSession();
|
|
60095
|
-
trackingRef = await setupNetworkTracking(client, url);
|
|
60096
|
-
if (profile.downloadThroughput >= 0 || profile.offline) {
|
|
60097
|
-
await client.send("Network.emulateNetworkConditions", {
|
|
60098
|
-
offline: profile.offline || false,
|
|
60099
|
-
downloadThroughput: profile.downloadThroughput,
|
|
60100
|
-
uploadThroughput: profile.uploadThroughput,
|
|
60101
|
-
latency: profile.latency
|
|
60102
|
-
});
|
|
60103
|
-
}
|
|
60104
|
-
}
|
|
60105
|
-
}),
|
|
60106
|
-
new Promise((_, reject) => {
|
|
60107
|
-
timeoutTimer = setTimeout(
|
|
60108
|
-
() => reject(new Error("NETWORK_TEST_TIMEOUT")),
|
|
60109
|
-
timeout
|
|
60110
|
-
);
|
|
60111
|
-
})
|
|
60112
|
-
]);
|
|
60113
|
-
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
60114
|
-
const performanceTiming = await loadResult.page.evaluate(() => {
|
|
60115
|
-
const timing = performance.getEntriesByType("navigation")[0];
|
|
60116
|
-
if (!timing) return null;
|
|
60117
|
-
return {
|
|
60118
|
-
domContentLoaded: timing.domContentLoadedEventEnd - timing.startTime
|
|
60119
|
-
};
|
|
60120
|
-
}).catch(() => null);
|
|
60121
|
-
const fcpMs = await loadResult.page.evaluate(() => {
|
|
60122
|
-
const fcp = performance.getEntriesByName("first-contentful-paint")[0];
|
|
60123
|
-
return fcp ? fcp.startTime : null;
|
|
60124
|
-
}).catch(() => null);
|
|
60125
|
-
const networkData = trackingRef ? trackingRef.getResults() : { requests: [], thirdParty: 0 };
|
|
60126
|
-
const loadTimeMs = Date.now() - start;
|
|
60127
|
-
const totalTransferBytes = networkData.requests.reduce((sum, r) => sum + r.transferSize, 0);
|
|
60128
|
-
const failedRequests = networkData.requests.filter((r) => r.failed).length;
|
|
60129
|
-
const profileResult = {
|
|
60130
|
-
profile: profile.name,
|
|
60131
|
-
label: profile.label,
|
|
60132
|
-
loadTimeMs,
|
|
60133
|
-
firstContentfulPaintMs: typeof fcpMs === "number" ? Math.round(fcpMs) : null,
|
|
60134
|
-
domContentLoadedMs: performanceTiming ? Math.round(performanceTiming.domContentLoaded) : null,
|
|
60135
|
-
totalRequests: networkData.requests.length,
|
|
60136
|
-
totalTransferBytes,
|
|
60137
|
-
failedRequests,
|
|
60138
|
-
timedOut: false
|
|
60139
|
-
};
|
|
60140
|
-
return {
|
|
60141
|
-
result: profileResult,
|
|
60142
|
-
html: loadResult.html,
|
|
60143
|
-
page: loadResult.page,
|
|
60144
|
-
cleanup: loadResult.cleanup,
|
|
60145
|
-
networkData
|
|
60146
|
-
};
|
|
60147
|
-
} catch (err) {
|
|
60148
|
-
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
60149
|
-
const isTimeout = err instanceof Error && (err.message === "NETWORK_TEST_TIMEOUT" || err.message.includes("timeout") || err.message.includes("Navigation timeout"));
|
|
60150
|
-
if (isDev2) {
|
|
60151
|
-
console.log(
|
|
60152
|
-
`[NetworkTest] Profil "${profile.name}" ${isTimeout ? "Timeout" : "Fehler"}: ${err instanceof Error ? err.message : String(err)}`
|
|
60153
|
-
);
|
|
60154
|
-
}
|
|
60155
|
-
return {
|
|
60156
|
-
result: {
|
|
60157
|
-
profile: profile.name,
|
|
60158
|
-
label: profile.label,
|
|
60159
|
-
loadTimeMs: Date.now() - start,
|
|
60160
|
-
firstContentfulPaintMs: null,
|
|
60161
|
-
domContentLoadedMs: null,
|
|
60162
|
-
totalRequests: 0,
|
|
60163
|
-
totalTransferBytes: 0,
|
|
60164
|
-
failedRequests: 0,
|
|
60165
|
-
timedOut: isTimeout
|
|
60166
|
-
}
|
|
60167
|
-
};
|
|
60168
|
-
}
|
|
60169
|
-
}
|
|
60170
|
-
async function analyzeBaselineMetrics(html3, page, networkData) {
|
|
60171
|
-
const $2 = load(html3);
|
|
60172
|
-
let hasServiceWorker = false;
|
|
60173
|
-
if (page) {
|
|
60174
|
-
try {
|
|
60175
|
-
hasServiceWorker = await page.evaluate(
|
|
60176
|
-
() => "serviceWorker" in navigator && navigator.serviceWorker.controller !== null
|
|
60177
|
-
);
|
|
60178
|
-
} catch {
|
|
60179
|
-
hasServiceWorker = false;
|
|
60180
|
-
}
|
|
60181
|
-
}
|
|
60182
|
-
const hasFontDisplaySwap = html3.includes("font-display") && (html3.includes("swap") || html3.includes("optional"));
|
|
60183
|
-
const allImages = $2("img");
|
|
60184
|
-
const lazyImages = $2('img[loading="lazy"]');
|
|
60185
|
-
const imageCount = allImages.length;
|
|
60186
|
-
const imagesWithoutLazy = imageCount - lazyImages.length;
|
|
60187
|
-
const hasLazyLoading = lazyImages.length > 0;
|
|
60188
|
-
const hasCriticalCssInline = $2("head style").length > 0;
|
|
60189
|
-
const hasLoadingIndicators = $2(
|
|
60190
|
-
'[class*="skeleton"], [class*="loading"], [class*="spinner"], [class*="loader"], [aria-busy="true"], [class*="placeholder"], [class*="Skeleton"], [class*="Loading"], [class*="Spinner"], [class*="Loader"], [class*="Placeholder"]'
|
|
60191
|
-
).length > 0;
|
|
60192
|
-
const networkFontCount = networkData ? networkData.requests.filter(
|
|
60193
|
-
(r) => !r.failed && (r.resourceType === "Font" || /\.(woff2?|ttf|otf|eot)(\?|$)/i.test(r.url))
|
|
60194
|
-
).length : 0;
|
|
60195
|
-
let htmlFontCount = 0;
|
|
60196
|
-
if (networkFontCount === 0) {
|
|
60197
|
-
$2("link[href]").each((_, el) => {
|
|
60198
|
-
const href = $2(el).attr("href") || "";
|
|
60199
|
-
if (/\.(woff2?|ttf|otf|eot)(\?|$)/i.test(href) || /fonts\.(googleapis|gstatic|bunny)\.net/i.test(href) || /use\.typekit\.net/i.test(href)) {
|
|
60200
|
-
htmlFontCount++;
|
|
60201
|
-
}
|
|
60202
|
-
});
|
|
60203
|
-
const fontFaceMatches = html3.match(/@font-face/gi);
|
|
60204
|
-
if (fontFaceMatches) {
|
|
60205
|
-
htmlFontCount += fontFaceMatches.length;
|
|
60206
|
-
}
|
|
60207
|
-
}
|
|
60208
|
-
const webFontCount = networkFontCount > 0 ? networkFontCount : htmlFontCount;
|
|
60209
|
-
const totalPageSizeBytes = networkData ? networkData.requests.reduce((sum, r) => sum + r.transferSize, 0) : 0;
|
|
60210
|
-
const thirdPartyRequests = networkData ? networkData.thirdParty : 0;
|
|
60211
|
-
return {
|
|
60212
|
-
hasServiceWorker,
|
|
60213
|
-
hasFontDisplaySwap,
|
|
60214
|
-
hasLazyLoading,
|
|
60215
|
-
hasCriticalCssInline,
|
|
60216
|
-
hasLoadingIndicators,
|
|
60217
|
-
totalPageSizeBytes,
|
|
60218
|
-
imageCount,
|
|
60219
|
-
imagesWithoutLazy,
|
|
60220
|
-
webFontCount,
|
|
60221
|
-
thirdPartyRequests
|
|
60222
|
-
};
|
|
60223
|
-
}
|
|
60224
|
-
function generateFindings(profiles, metrics) {
|
|
60225
|
-
const findings = [];
|
|
60226
|
-
let findingId = 0;
|
|
60227
|
-
const nextId2 = () => `net-${++findingId}`;
|
|
60228
|
-
const baseline = profiles.find((p) => p.profile === "fast");
|
|
60229
|
-
const profile3g = profiles.find((p) => p.profile === "3g");
|
|
60230
|
-
const profileSlow3g = profiles.find((p) => p.profile === "slow3g");
|
|
60231
|
-
const profileOffline = profiles.find((p) => p.profile === "offline");
|
|
60232
|
-
if (profile3g) {
|
|
60233
|
-
if (profile3g.timedOut || profile3g.loadTimeMs > 3e4) {
|
|
60234
|
-
findings.push({
|
|
60235
|
-
id: nextId2(),
|
|
60236
|
-
category: "ladezeit",
|
|
60237
|
-
severity: "critical",
|
|
60238
|
-
title: "Seite nicht ladbar auf 3G-Verbindung",
|
|
60239
|
-
description: `Die Seite konnte innerhalb von 30 Sekunden auf einer 3G-Verbindung nicht geladen werden. Nutzer mit langsamer Mobilfunkverbindung k\xF6nnen die Seite nicht verwenden.`,
|
|
60240
|
-
value: profile3g.timedOut ? "Timeout" : `${Math.round(profile3g.loadTimeMs / 1e3)}s`,
|
|
60241
|
-
recommendation: "Seitengr\xF6\xDFe drastisch reduzieren. Kritische Ressourcen priorisieren, nicht ben\xF6tigte Skripte und Stylesheets entfernen oder versp\xE4tet laden."
|
|
60242
|
-
});
|
|
60243
|
-
} else if (profile3g.loadTimeMs > 1e4) {
|
|
60244
|
-
findings.push({
|
|
60245
|
-
id: nextId2(),
|
|
60246
|
-
category: "ladezeit",
|
|
60247
|
-
severity: "serious",
|
|
60248
|
-
title: "Seite l\xE4dt \xFCber 10 Sekunden auf 3G",
|
|
60249
|
-
description: `Die Ladezeit auf einer 3G-Verbindung betr\xE4gt ${Math.round(
|
|
60250
|
-
profile3g.loadTimeMs / 1e3
|
|
60251
|
-
)} Sekunden. Nutzer mit langsamer Verbindung erleben eine erhebliche Verz\xF6gerung.`,
|
|
60252
|
-
value: `${Math.round(profile3g.loadTimeMs / 1e3)}s`,
|
|
60253
|
-
recommendation: "Bilder komprimieren, CSS/JS minimieren und unn\xF6tige Third-Party-Ressourcen entfernen. Code-Splitting und Lazy Loading einsetzen."
|
|
60254
|
-
});
|
|
60255
|
-
} else if (profile3g.loadTimeMs > 5e3) {
|
|
60256
|
-
findings.push({
|
|
60257
|
-
id: nextId2(),
|
|
60258
|
-
category: "ladezeit",
|
|
60259
|
-
severity: "moderate",
|
|
60260
|
-
title: "Seite l\xE4dt \xFCber 5 Sekunden auf 3G",
|
|
60261
|
-
description: `Die Ladezeit auf einer 3G-Verbindung betr\xE4gt ${Math.round(
|
|
60262
|
-
profile3g.loadTimeMs / 1e3
|
|
60263
|
-
)} Sekunden. Nutzer mit langsamer Verbindung m\xFCssen deutlich warten.`,
|
|
60264
|
-
value: `${Math.round(profile3g.loadTimeMs / 1e3)}s`,
|
|
60265
|
-
recommendation: "Ressourcen optimieren: Bilder als WebP/AVIF bereitstellen, CSS und JavaScript b\xFCndeln und komprimieren."
|
|
60266
|
-
});
|
|
60370
|
+
console.error(`[ScanHtml] Rule "${rule.id}" failed:`, ruleErr instanceof Error ? ruleErr.message : ruleErr);
|
|
60267
60371
|
}
|
|
60268
60372
|
}
|
|
60269
|
-
|
|
60270
|
-
if (
|
|
60271
|
-
|
|
60272
|
-
id: nextId2(),
|
|
60273
|
-
category: "ladezeit",
|
|
60274
|
-
severity: "serious",
|
|
60275
|
-
title: "Seite nicht ladbar auf langsamem 3G",
|
|
60276
|
-
description: "Die Seite konnte innerhalb von 45 Sekunden auf einer langsamen 3G-Verbindung nicht geladen werden.",
|
|
60277
|
-
value: "Timeout",
|
|
60278
|
-
recommendation: "Eine stark reduzierte Version der Seite f\xFCr langsame Verbindungen anbieten. Progressive Enhancement einsetzen."
|
|
60279
|
-
});
|
|
60373
|
+
for (const f of rawFindings) {
|
|
60374
|
+
if (f.legalBasis !== "DSGVO") {
|
|
60375
|
+
f.en301549Reference = getEN301549Reference(f.wcagReference);
|
|
60280
60376
|
}
|
|
60281
60377
|
}
|
|
60282
|
-
if (
|
|
60283
|
-
|
|
60284
|
-
id: nextId2(),
|
|
60285
|
-
category: "ladezeit",
|
|
60286
|
-
severity: "moderate",
|
|
60287
|
-
title: "Baseline-Ladezeit \xFCber 3 Sekunden",
|
|
60288
|
-
description: `Selbst auf einer schnellen Verbindung betr\xE4gt die Ladezeit ${Math.round(
|
|
60289
|
-
baseline.loadTimeMs / 1e3
|
|
60290
|
-
)} Sekunden. Das deutet auf grundlegende Performanceprobleme hin.`,
|
|
60291
|
-
value: `${Math.round(baseline.loadTimeMs / 1e3)}s`,
|
|
60292
|
-
recommendation: "Server-Antwortzeit pr\xFCfen, Render-blockierende Ressourcen eliminieren, Caching-Header setzen."
|
|
60293
|
-
});
|
|
60294
|
-
}
|
|
60295
|
-
if (!metrics.hasServiceWorker) {
|
|
60296
|
-
findings.push({
|
|
60297
|
-
id: nextId2(),
|
|
60298
|
-
category: "offline",
|
|
60299
|
-
severity: "moderate",
|
|
60300
|
-
title: "Kein Service Worker registriert",
|
|
60301
|
-
description: "Die Seite verwendet keinen Service Worker. Ohne Service Worker ist keine Offline-Unterst\xFCtzung, kein Caching und keine Push-Benachrichtigungen m\xF6glich.",
|
|
60302
|
-
recommendation: "Einen Service Worker implementieren, der kritische Ressourcen cachet und eine Offline-Fallback-Seite bereitstellt."
|
|
60303
|
-
});
|
|
60378
|
+
if (detectAccessibilityWidget(html3, $2)) {
|
|
60379
|
+
downgradeContrastFindings(rawFindings);
|
|
60304
60380
|
}
|
|
60305
|
-
|
|
60306
|
-
|
|
60307
|
-
|
|
60308
|
-
|
|
60309
|
-
|
|
60310
|
-
|
|
60311
|
-
|
|
60312
|
-
|
|
60313
|
-
|
|
60314
|
-
|
|
60315
|
-
|
|
60381
|
+
const allFindings = deduplicateFindings(rawFindings);
|
|
60382
|
+
const findings = allFindings.filter((f) => f.legalBasis !== "DSGVO");
|
|
60383
|
+
const dsgvoFindings = allFindings.filter((f) => f.legalBasis === "DSGVO");
|
|
60384
|
+
const score = calculateScore(rawFindings.filter((f) => f.legalBasis !== "DSGVO"));
|
|
60385
|
+
const dsgvoScore = calculateDsgvoScore(dsgvoFindings);
|
|
60386
|
+
const critical = findings.filter((f) => f.severity === "critical").length;
|
|
60387
|
+
const serious = findings.filter((f) => f.severity === "serious").length;
|
|
60388
|
+
const moderate = findings.filter((f) => f.severity === "moderate").length;
|
|
60389
|
+
const minor = findings.filter((f) => f.severity === "minor").length;
|
|
60390
|
+
return {
|
|
60391
|
+
url,
|
|
60392
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
60393
|
+
score,
|
|
60394
|
+
totalFindings: findings.length,
|
|
60395
|
+
critical,
|
|
60396
|
+
serious,
|
|
60397
|
+
moderate,
|
|
60398
|
+
minor,
|
|
60399
|
+
findings,
|
|
60400
|
+
pageTitle: $2("title").text() || "Kein Titel",
|
|
60401
|
+
loadTimeMs: Date.now() - start,
|
|
60402
|
+
dsgvoFindings: dsgvoFindings.length > 0 ? dsgvoFindings : void 0,
|
|
60403
|
+
dsgvoScore
|
|
60404
|
+
};
|
|
60405
|
+
}
|
|
60406
|
+
function deduplicateFindings(findings) {
|
|
60407
|
+
const MAIN_RELATED_RULES = /* @__PURE__ */ new Set([
|
|
60408
|
+
"axe-landmark-main-is-top-level",
|
|
60409
|
+
"axe-landmark-no-duplicate-main"
|
|
60410
|
+
]);
|
|
60411
|
+
const mainRelated = [];
|
|
60412
|
+
const rest = [];
|
|
60413
|
+
for (const f of findings) {
|
|
60414
|
+
const isMainDuplicateId = f.rule === "structure" && f.id.startsWith("structure-duplicate-id") && f.element?.includes("<main");
|
|
60415
|
+
if (MAIN_RELATED_RULES.has(f.rule) || isMainDuplicateId) {
|
|
60416
|
+
mainRelated.push(f);
|
|
60417
|
+
} else {
|
|
60418
|
+
rest.push(f);
|
|
60316
60419
|
}
|
|
60317
60420
|
}
|
|
60318
|
-
if (
|
|
60319
|
-
|
|
60320
|
-
|
|
60321
|
-
|
|
60322
|
-
severity: "moderate",
|
|
60323
|
-
title: `${metrics.imagesWithoutLazy} Bilder ohne Lazy Loading`,
|
|
60324
|
-
description: `Von ${metrics.imageCount} Bildern auf der Seite verwenden ${metrics.imagesWithoutLazy} kein loading="lazy"-Attribut. Alle Bilder werden sofort geladen, auch wenn sie nicht im sichtbaren Bereich sind.`,
|
|
60325
|
-
value: `${metrics.imagesWithoutLazy}/${metrics.imageCount}`,
|
|
60326
|
-
recommendation: 'Allen Bildern au\xDFerhalb des sichtbaren Bereichs (below the fold) das Attribut loading="lazy" hinzuf\xFCgen. Bilder im sofort sichtbaren Bereich k\xF6nnen ohne Lazy Loading bleiben.'
|
|
60421
|
+
if (mainRelated.length > 1) {
|
|
60422
|
+
mainRelated.sort((a, b) => {
|
|
60423
|
+
const order = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
60424
|
+
return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
|
|
60327
60425
|
});
|
|
60426
|
+
const merged = { ...mainRelated[0] };
|
|
60427
|
+
merged.description = `Mehrere <main>-Elemente auf der Seite (${mainRelated.length} zusammenh\xE4ngende Findings)`;
|
|
60428
|
+
merged.explanation = `Die Seite enth\xE4lt mehrere <main>-Elemente. Dies verursacht zusammenh\xE4ngende Probleme: doppelte IDs, Landmark-Struktur und Screenreader-Navigation. L\xF6sung: Verwenden Sie nur ein einziges <main>-Element pro Seite.`;
|
|
60429
|
+
merged.fix = "Entfernen Sie \xFCberfl\xFCssige <main>-Elemente, sodass nur eines auf der Seite verbleibt.";
|
|
60430
|
+
rest.push(merged);
|
|
60431
|
+
} else {
|
|
60432
|
+
rest.push(...mainRelated);
|
|
60328
60433
|
}
|
|
60329
|
-
|
|
60330
|
-
|
|
60331
|
-
|
|
60332
|
-
|
|
60333
|
-
|
|
60334
|
-
|
|
60335
|
-
|
|
60336
|
-
|
|
60337
|
-
|
|
60338
|
-
|
|
60339
|
-
|
|
60340
|
-
|
|
60341
|
-
id: nextId2(),
|
|
60342
|
-
category: "groesse",
|
|
60343
|
-
severity: "serious",
|
|
60344
|
-
title: "Seitengr\xF6\xDFe \xFCber 3 MB",
|
|
60345
|
-
description: `Die Gesamtgr\xF6\xDFe aller Ressourcen betr\xE4gt ${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB. Auf langsameren Verbindungen f\xFChrt das zu sp\xFCrbaren Wartezeiten.`,
|
|
60346
|
-
value: `${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB`,
|
|
60347
|
-
recommendation: "Bilder komprimieren, ungenutztes CSS/JS entfernen, Schriftarten auf ben\xF6tigte Zeichen beschr\xE4nken (Subsetting)."
|
|
60348
|
-
});
|
|
60434
|
+
const groups = {};
|
|
60435
|
+
for (const f of rest) {
|
|
60436
|
+
if (f.id.startsWith("language-all-caps")) {
|
|
60437
|
+
const key = "language::all-caps-group";
|
|
60438
|
+
if (!groups[key]) groups[key] = [];
|
|
60439
|
+
groups[key].push(f);
|
|
60440
|
+
} else {
|
|
60441
|
+
const normalizedDesc = f.description.replace(/\s*\([\d.,:/]+.*?\)\s*$/, "");
|
|
60442
|
+
const key = `${f.rule}::${normalizedDesc}`;
|
|
60443
|
+
if (!groups[key]) groups[key] = [];
|
|
60444
|
+
groups[key].push(f);
|
|
60445
|
+
}
|
|
60349
60446
|
}
|
|
60350
|
-
|
|
60351
|
-
|
|
60352
|
-
|
|
60353
|
-
|
|
60354
|
-
|
|
60355
|
-
|
|
60356
|
-
description
|
|
60357
|
-
|
|
60358
|
-
|
|
60359
|
-
})
|
|
60447
|
+
const severityOrder = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
60448
|
+
const result = [];
|
|
60449
|
+
for (const key of Object.keys(groups)) {
|
|
60450
|
+
const group = groups[key];
|
|
60451
|
+
if (key === "language::all-caps-group" && group.length > 1) {
|
|
60452
|
+
const first2 = { ...group[0] };
|
|
60453
|
+
first2.description = `Text in Gro\xDFbuchstaben (${group.length}x auf dieser Seite)`;
|
|
60454
|
+
first2.explanation = `${group.length} Textstellen auf dieser Seite sind vollst\xE4ndig in Gro\xDFbuchstaben geschrieben. Screenreader k\xF6nnen solchen Text als Abk\xFCrzung interpretieren und Buchstabe f\xFCr Buchstabe vorlesen. Verwenden Sie CSS text-transform: uppercase f\xFCr die visuelle Darstellung.`;
|
|
60455
|
+
result.push(first2);
|
|
60456
|
+
} else if (group.length <= 1) {
|
|
60457
|
+
result.push(...group);
|
|
60458
|
+
} else {
|
|
60459
|
+
group.sort((a, b) => (severityOrder[a.severity] ?? 4) - (severityOrder[b.severity] ?? 4));
|
|
60460
|
+
const first2 = { ...group[0] };
|
|
60461
|
+
const baseDesc = first2.description.replace(/\s*\([\d.,:/]+.*?\)\s*$/, "");
|
|
60462
|
+
first2.description = `${baseDesc} (${group.length}x auf dieser Seite)`;
|
|
60463
|
+
first2.explanation = `${first2.explanation}
|
|
60464
|
+
|
|
60465
|
+
Dieses Problem tritt ${group.length} Mal auf dieser Seite auf.`;
|
|
60466
|
+
result.push(first2);
|
|
60467
|
+
}
|
|
60360
60468
|
}
|
|
60361
|
-
|
|
60362
|
-
|
|
60363
|
-
|
|
60364
|
-
|
|
60365
|
-
|
|
60366
|
-
|
|
60367
|
-
|
|
60368
|
-
|
|
60369
|
-
|
|
60370
|
-
|
|
60469
|
+
return result;
|
|
60470
|
+
}
|
|
60471
|
+
function detectAccessibilityWidget(html3, $2) {
|
|
60472
|
+
if ($2('script[src*="acpilot"]').length > 0 || $2('script[src*="accesspilot"]').length > 0) return true;
|
|
60473
|
+
if ($2("[data-acpilot]").length > 0 || $2("[data-accesspilot]").length > 0) return true;
|
|
60474
|
+
if ($2('script[src*="userway"]').length > 0) return true;
|
|
60475
|
+
if ($2('script[src*="accessibe"]').length > 0) return true;
|
|
60476
|
+
if ($2('script[src*="equalweb"]').length > 0) return true;
|
|
60477
|
+
if ($2("#accessibilityWidget, .accessibility-widget, [data-accessibility-widget]").length > 0) return true;
|
|
60478
|
+
if (html3.includes("acpilot") || html3.includes("accesspilot-widget")) return true;
|
|
60479
|
+
return false;
|
|
60480
|
+
}
|
|
60481
|
+
function downgradeContrastFindings(findings) {
|
|
60482
|
+
const WIDGET_HINT = "\n\n\u{1F4A1} Diese Website hat ein Barrierefreiheits-Widget eingebunden, \xFCber das Nutzer den Kontrast manuell anpassen k\xF6nnen. Das Problem wird daher herabgestuft.";
|
|
60483
|
+
for (const f of findings) {
|
|
60484
|
+
const isContrastRule = f.rule === "contrast" || f.rule === "color-contrast" || f.rule === "non-text-contrast" || f.rule.includes("color-contrast");
|
|
60485
|
+
if (!isContrastRule) continue;
|
|
60486
|
+
if (f.severity === "critical") {
|
|
60487
|
+
f.severity = "moderate";
|
|
60488
|
+
} else if (f.severity === "serious" || f.severity === "moderate") {
|
|
60489
|
+
f.severity = "minor";
|
|
60490
|
+
}
|
|
60491
|
+
f.explanation += WIDGET_HINT;
|
|
60371
60492
|
}
|
|
60372
|
-
|
|
60373
|
-
|
|
60374
|
-
|
|
60375
|
-
|
|
60376
|
-
|
|
60377
|
-
|
|
60378
|
-
|
|
60379
|
-
|
|
60380
|
-
|
|
60493
|
+
}
|
|
60494
|
+
function calculateDsgvoScore(dsgvoFindings) {
|
|
60495
|
+
if (dsgvoFindings.length === 0) return 100;
|
|
60496
|
+
const penalties = { critical: 20, serious: 12, moderate: 5, minor: 2 };
|
|
60497
|
+
const sorted = [...dsgvoFindings].sort((a, b) => (penalties[b.severity] || 0) - (penalties[a.severity] || 0));
|
|
60498
|
+
let totalPenalty = 0;
|
|
60499
|
+
sorted.forEach((f, i) => {
|
|
60500
|
+
const base = penalties[f.severity] || 3;
|
|
60501
|
+
if (i < 3) {
|
|
60502
|
+
totalPenalty += base;
|
|
60503
|
+
} else if (i < 6) {
|
|
60504
|
+
totalPenalty += base * 0.5;
|
|
60505
|
+
} else {
|
|
60506
|
+
totalPenalty += base * 0.25;
|
|
60507
|
+
}
|
|
60508
|
+
});
|
|
60509
|
+
const cappedPenalty = Math.min(totalPenalty, 92);
|
|
60510
|
+
return Math.max(0, Math.min(100, Math.round(100 - cappedPenalty)));
|
|
60511
|
+
}
|
|
60512
|
+
function calculateCombinedScore(wcagScore, logicScore, srIssueCount) {
|
|
60513
|
+
let srScore = 100;
|
|
60514
|
+
if (srIssueCount != null && srIssueCount > 0) {
|
|
60515
|
+
srScore = Math.max(15, Math.round(100 * Math.exp(-0.1 * srIssueCount)));
|
|
60381
60516
|
}
|
|
60382
|
-
if (
|
|
60383
|
-
|
|
60384
|
-
id: nextId2(),
|
|
60385
|
-
category: "ux",
|
|
60386
|
-
severity: "moderate",
|
|
60387
|
-
title: "Keine Lade-Indikatoren erkannt",
|
|
60388
|
-
description: "Die Seite scheint keine Skeleton Screens, Spinner oder andere Ladezustandsanzeigen zu verwenden. Bei langsamen Verbindungen sieht der Nutzer m\xF6glicherweise eine leere oder unvollst\xE4ndige Seite.",
|
|
60389
|
-
recommendation: 'Skeleton Screens oder Spinner f\xFCr asynchron geladene Inhalte einsetzen. Das Attribut aria-busy="true" verwenden, damit Screenreader den Ladezustand erkennen.'
|
|
60390
|
-
});
|
|
60517
|
+
if (logicScore != null && srIssueCount != null) {
|
|
60518
|
+
return Math.round(wcagScore * 0.5 + logicScore * 0.25 + srScore * 0.25);
|
|
60391
60519
|
}
|
|
60392
|
-
if (
|
|
60393
|
-
|
|
60394
|
-
id: nextId2(),
|
|
60395
|
-
category: "third-party",
|
|
60396
|
-
severity: "serious",
|
|
60397
|
-
title: "\xDCber 20 Third-Party-Requests",
|
|
60398
|
-
description: `Die Seite l\xE4dt ${metrics.thirdPartyRequests} Ressourcen von Drittanbieter-Domains. Jeder zus\xE4tzliche DNS-Lookup und TLS-Handshake verl\xE4ngert die Ladezeit und erh\xF6ht das Risiko von Single Points of Failure.`,
|
|
60399
|
-
value: `${metrics.thirdPartyRequests} Requests`,
|
|
60400
|
-
recommendation: "Third-Party-Skripte auf Notwendigkeit pr\xFCfen. Nicht ben\xF6tigte Tracker und Widgets entfernen. F\xFCr unvermeidbare Drittanbieter Resource Hints (dns-prefetch, preconnect) setzen."
|
|
60401
|
-
});
|
|
60402
|
-
} else if (metrics.thirdPartyRequests > 10) {
|
|
60403
|
-
findings.push({
|
|
60404
|
-
id: nextId2(),
|
|
60405
|
-
category: "third-party",
|
|
60406
|
-
severity: "moderate",
|
|
60407
|
-
title: "Viele Third-Party-Requests",
|
|
60408
|
-
description: `Die Seite l\xE4dt ${metrics.thirdPartyRequests} Ressourcen von Drittanbieter-Domains. Das kann die Ladezeit negativ beeinflussen.`,
|
|
60409
|
-
value: `${metrics.thirdPartyRequests} Requests`,
|
|
60410
|
-
recommendation: "Pr\xFCfen, ob alle eingebundenen Drittanbieter-Skripte wirklich ben\xF6tigt werden. Resource Hints (dns-prefetch, preconnect) f\xFCr wichtige Drittanbieter setzen."
|
|
60411
|
-
});
|
|
60520
|
+
if (logicScore != null) {
|
|
60521
|
+
return Math.round(wcagScore * 0.65 + logicScore * 0.35);
|
|
60412
60522
|
}
|
|
60413
|
-
|
|
60414
|
-
|
|
60415
|
-
async function runNetworkTest(url) {
|
|
60416
|
-
if (isDev2) console.log(`[NetworkTest] Starte Netzwerk-Resilienztest f\xFCr: ${url}`);
|
|
60417
|
-
const findings = [];
|
|
60418
|
-
const profileResults = [];
|
|
60419
|
-
let baselineHtml;
|
|
60420
|
-
let baselinePage;
|
|
60421
|
-
let baselineCleanup;
|
|
60422
|
-
let baselineNetworkData;
|
|
60423
|
-
const fastProfile = NETWORK_PROFILES.find((p) => p.name === "fast");
|
|
60424
|
-
try {
|
|
60425
|
-
if (isDev2) console.log(`[NetworkTest] Teste Profil: ${fastProfile.label}`);
|
|
60426
|
-
const baselineResult = await testWithProfile(url, fastProfile);
|
|
60427
|
-
profileResults.push(baselineResult.result);
|
|
60428
|
-
baselineHtml = baselineResult.html;
|
|
60429
|
-
baselinePage = baselineResult.page;
|
|
60430
|
-
baselineCleanup = baselineResult.cleanup;
|
|
60431
|
-
baselineNetworkData = baselineResult.networkData;
|
|
60432
|
-
} catch (err) {
|
|
60433
|
-
if (isDev2) {
|
|
60434
|
-
console.log(
|
|
60435
|
-
`[NetworkTest] Baseline-Test fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`
|
|
60436
|
-
);
|
|
60437
|
-
}
|
|
60438
|
-
profileResults.push({
|
|
60439
|
-
profile: fastProfile.name,
|
|
60440
|
-
label: fastProfile.label,
|
|
60441
|
-
loadTimeMs: 0,
|
|
60442
|
-
firstContentfulPaintMs: null,
|
|
60443
|
-
domContentLoadedMs: null,
|
|
60444
|
-
totalRequests: 0,
|
|
60445
|
-
totalTransferBytes: 0,
|
|
60446
|
-
failedRequests: 0,
|
|
60447
|
-
timedOut: true
|
|
60448
|
-
});
|
|
60523
|
+
if (srIssueCount != null) {
|
|
60524
|
+
return Math.round(wcagScore * 0.65 + srScore * 0.35);
|
|
60449
60525
|
}
|
|
60450
|
-
|
|
60451
|
-
|
|
60452
|
-
|
|
60453
|
-
|
|
60454
|
-
|
|
60455
|
-
|
|
60456
|
-
|
|
60526
|
+
return wcagScore;
|
|
60527
|
+
}
|
|
60528
|
+
function calculateScore(findings) {
|
|
60529
|
+
const penalties = { critical: 8, serious: 4, moderate: 1.5, minor: 0.5 };
|
|
60530
|
+
const byRule = {};
|
|
60531
|
+
for (const f of findings) {
|
|
60532
|
+
if (!byRule[f.rule]) byRule[f.rule] = [];
|
|
60533
|
+
byRule[f.rule].push(f);
|
|
60457
60534
|
}
|
|
60458
|
-
|
|
60459
|
-
for (const
|
|
60460
|
-
|
|
60461
|
-
|
|
60462
|
-
|
|
60463
|
-
|
|
60464
|
-
if (
|
|
60465
|
-
|
|
60466
|
-
|
|
60467
|
-
|
|
60468
|
-
|
|
60469
|
-
|
|
60470
|
-
} catch (err) {
|
|
60471
|
-
if (isDev2) {
|
|
60472
|
-
console.log(
|
|
60473
|
-
`[NetworkTest] Profil "${profile.label}" fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`
|
|
60474
|
-
);
|
|
60535
|
+
let totalPenalty = 0;
|
|
60536
|
+
for (const rule of Object.keys(byRule)) {
|
|
60537
|
+
const ruleFindings = byRule[rule];
|
|
60538
|
+
ruleFindings.sort((a, b) => penalties[b.severity] - penalties[a.severity]);
|
|
60539
|
+
ruleFindings.forEach((f, i) => {
|
|
60540
|
+
const basePenalty = penalties[f.severity] || 0.5;
|
|
60541
|
+
if (i === 0) {
|
|
60542
|
+
totalPenalty += basePenalty;
|
|
60543
|
+
} else if (i < 3) {
|
|
60544
|
+
totalPenalty += basePenalty * 0.4;
|
|
60545
|
+
} else {
|
|
60546
|
+
totalPenalty += 0.3;
|
|
60475
60547
|
}
|
|
60476
|
-
|
|
60477
|
-
profile: profile.name,
|
|
60478
|
-
label: profile.label,
|
|
60479
|
-
loadTimeMs: 0,
|
|
60480
|
-
firstContentfulPaintMs: null,
|
|
60481
|
-
domContentLoadedMs: null,
|
|
60482
|
-
totalRequests: 0,
|
|
60483
|
-
totalTransferBytes: 0,
|
|
60484
|
-
failedRequests: 0,
|
|
60485
|
-
timedOut: true
|
|
60486
|
-
});
|
|
60487
|
-
}
|
|
60488
|
-
}
|
|
60489
|
-
const metrics = await analyzeBaselineMetrics(
|
|
60490
|
-
baselineHtml || "<html><body></body></html>",
|
|
60491
|
-
void 0,
|
|
60492
|
-
// Page ist bereits geschlossen
|
|
60493
|
-
baselineNetworkData
|
|
60494
|
-
);
|
|
60495
|
-
const offlineProfileResult = profileResults.find((p) => p.profile === "offline");
|
|
60496
|
-
const hasOfflineFallback = metrics.hasServiceWorker && offlineProfileResult !== void 0 && !offlineProfileResult.timedOut && offlineProfileResult.totalRequests > 0;
|
|
60497
|
-
const generatedFindings = generateFindings(profileResults, metrics);
|
|
60498
|
-
findings.push(...generatedFindings);
|
|
60499
|
-
const score = calculateNetworkScore(findings);
|
|
60500
|
-
if (isDev2) {
|
|
60501
|
-
console.log(
|
|
60502
|
-
`[NetworkTest] Abgeschlossen. Score: ${score}/100, ${findings.length} Befunde`
|
|
60503
|
-
);
|
|
60548
|
+
});
|
|
60504
60549
|
}
|
|
60505
|
-
|
|
60506
|
-
|
|
60507
|
-
testedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
60508
|
-
score,
|
|
60509
|
-
profiles: profileResults,
|
|
60510
|
-
findings,
|
|
60511
|
-
metrics: {
|
|
60512
|
-
hasServiceWorker: metrics.hasServiceWorker,
|
|
60513
|
-
hasOfflineFallback,
|
|
60514
|
-
hasLoadingIndicators: metrics.hasLoadingIndicators,
|
|
60515
|
-
hasFontDisplaySwap: metrics.hasFontDisplaySwap,
|
|
60516
|
-
hasLazyLoading: metrics.hasLazyLoading,
|
|
60517
|
-
hasCriticalCssInline: metrics.hasCriticalCssInline,
|
|
60518
|
-
totalPageSizeBytes: metrics.totalPageSizeBytes,
|
|
60519
|
-
imageCount: metrics.imageCount,
|
|
60520
|
-
imagesWithoutLazy: metrics.imagesWithoutLazy,
|
|
60521
|
-
webFontCount: metrics.webFontCount,
|
|
60522
|
-
thirdPartyRequests: metrics.thirdPartyRequests
|
|
60523
|
-
}
|
|
60524
|
-
};
|
|
60550
|
+
const cappedPenalty = Math.min(totalPenalty, 95);
|
|
60551
|
+
return Math.max(0, Math.min(100, Math.round(100 - cappedPenalty)));
|
|
60525
60552
|
}
|
|
60526
|
-
var
|
|
60527
|
-
|
|
60528
|
-
"../../src/lib/network-tester/index.ts"() {
|
|
60553
|
+
var init_scanner = __esm({
|
|
60554
|
+
"../../src/lib/scanner/index.ts"() {
|
|
60529
60555
|
"use strict";
|
|
60530
60556
|
init_esm11();
|
|
60531
|
-
|
|
60532
|
-
|
|
60533
|
-
|
|
60534
|
-
|
|
60535
|
-
|
|
60536
|
-
|
|
60537
|
-
|
|
60538
|
-
uploadThroughput: -1,
|
|
60539
|
-
latency: 0
|
|
60540
|
-
},
|
|
60541
|
-
{
|
|
60542
|
-
name: "3g",
|
|
60543
|
-
label: "3G Mobilfunk",
|
|
60544
|
-
downloadThroughput: 1500 * 1024 / 8,
|
|
60545
|
-
uploadThroughput: 400 * 1024 / 8,
|
|
60546
|
-
latency: 300
|
|
60547
|
-
},
|
|
60548
|
-
{
|
|
60549
|
-
name: "slow3g",
|
|
60550
|
-
label: "Langsames 3G",
|
|
60551
|
-
downloadThroughput: 500 * 1024 / 8,
|
|
60552
|
-
uploadThroughput: 100 * 1024 / 8,
|
|
60553
|
-
latency: 2e3
|
|
60554
|
-
},
|
|
60555
|
-
{
|
|
60556
|
-
name: "offline",
|
|
60557
|
-
label: "Offline",
|
|
60558
|
-
downloadThroughput: 0,
|
|
60559
|
-
uploadThroughput: 0,
|
|
60560
|
-
latency: 0,
|
|
60561
|
-
offline: true
|
|
60562
|
-
}
|
|
60563
|
-
];
|
|
60557
|
+
init_rules();
|
|
60558
|
+
init_en301549_mapping();
|
|
60559
|
+
init_tab_order();
|
|
60560
|
+
init_color_contrast();
|
|
60561
|
+
init_axe_integration();
|
|
60562
|
+
init_network_privacy();
|
|
60563
|
+
init_rules();
|
|
60564
60564
|
}
|
|
60565
60565
|
});
|
|
60566
60566
|
|
|
60567
60567
|
// src/cli.ts
|
|
60568
60568
|
var readline = __toESM(require("readline"));
|
|
60569
|
+
|
|
60570
|
+
// package.json
|
|
60571
|
+
var version = "2.1.0";
|
|
60572
|
+
|
|
60573
|
+
// src/cli.ts
|
|
60574
|
+
var PKG_VERSION = version;
|
|
60569
60575
|
var RESET = "\x1B[0m";
|
|
60570
60576
|
var BOLD = "\x1B[1m";
|
|
60571
60577
|
var DIM = "\x1B[2m";
|
|
@@ -60585,16 +60591,16 @@ function scoreColor(score) {
|
|
|
60585
60591
|
}
|
|
60586
60592
|
function printLogo() {
|
|
60587
60593
|
console.log("");
|
|
60588
|
-
console.log(`${BLUE} \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${RESET}`);
|
|
60589
|
-
console.log(`${BLUE} \u2551${RESET}
|
|
60590
|
-
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588${WHITE}\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588${RESET}
|
|
60591
|
-
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588
|
|
60592
|
-
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588
|
|
60593
|
-
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588
|
|
60594
|
-
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588${RESET}
|
|
60595
|
-
console.log(`${BLUE} \u2551${RESET}
|
|
60596
|
-
console.log(`${BLUE} \u2551${RESET} ${DIM}${ITALIC} Barrierefreiheit
|
|
60597
|
-
console.log(`${BLUE} \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET}`);
|
|
60594
|
+
console.log(`${BLUE} \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${RESET}`);
|
|
60595
|
+
console.log(`${BLUE} \u2551${RESET} ${BLUE}\u2551${RESET}`);
|
|
60596
|
+
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 ${WHITE}\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588${RESET} ${BLUE}\u2551${RESET}`);
|
|
60597
|
+
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588${RESET} ${BLUE}\u2551${RESET}`);
|
|
60598
|
+
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588${RESET} ${BLUE}\u2551${RESET}`);
|
|
60599
|
+
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588${RESET} ${BLUE}\u2551${RESET}`);
|
|
60600
|
+
console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588${RESET} ${BLUE}\u2551${RESET}`);
|
|
60601
|
+
console.log(`${BLUE} \u2551${RESET} ${BLUE}\u2551${RESET}`);
|
|
60602
|
+
console.log(`${BLUE} \u2551${RESET} ${DIM}${ITALIC} Barrierefreiheit fuer Entwickler${RESET} ${DIM}v${PKG_VERSION}${RESET} ${BLUE}\u2551${RESET}`);
|
|
60603
|
+
console.log(`${BLUE} \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET}`);
|
|
60598
60604
|
console.log("");
|
|
60599
60605
|
}
|
|
60600
60606
|
function printDivider(label) {
|
|
@@ -60628,6 +60634,44 @@ function askChoice(rl, question, choices) {
|
|
|
60628
60634
|
});
|
|
60629
60635
|
});
|
|
60630
60636
|
}
|
|
60637
|
+
function suppressLogs() {
|
|
60638
|
+
const origLog = console.log;
|
|
60639
|
+
const origError = console.error;
|
|
60640
|
+
const origWarn = console.warn;
|
|
60641
|
+
const noisePatterns = [
|
|
60642
|
+
/^\[ScanFast\]/,
|
|
60643
|
+
/^\[Browser\]/,
|
|
60644
|
+
/^\[DSGVO\]/,
|
|
60645
|
+
/^\[Crawler\]/,
|
|
60646
|
+
/^\[Axe\]/,
|
|
60647
|
+
/^\[Impressum\]/,
|
|
60648
|
+
/^\[NetworkTest\]/,
|
|
60649
|
+
/^\[Logic\]/,
|
|
60650
|
+
/^\[Screenreader\]/,
|
|
60651
|
+
/^\[SR\]/
|
|
60652
|
+
];
|
|
60653
|
+
function isNoisy(args) {
|
|
60654
|
+
if (args.length === 0) return false;
|
|
60655
|
+
const first2 = String(args[0]);
|
|
60656
|
+
return noisePatterns.some((p) => p.test(first2));
|
|
60657
|
+
}
|
|
60658
|
+
console.log = (...args) => {
|
|
60659
|
+
if (!isNoisy(args)) origLog(...args);
|
|
60660
|
+
};
|
|
60661
|
+
console.error = (...args) => {
|
|
60662
|
+
if (!isNoisy(args)) origError(...args);
|
|
60663
|
+
};
|
|
60664
|
+
console.warn = (...args) => {
|
|
60665
|
+
if (!isNoisy(args)) origWarn(...args);
|
|
60666
|
+
};
|
|
60667
|
+
return {
|
|
60668
|
+
restore() {
|
|
60669
|
+
console.log = origLog;
|
|
60670
|
+
console.error = origError;
|
|
60671
|
+
console.warn = origWarn;
|
|
60672
|
+
}
|
|
60673
|
+
};
|
|
60674
|
+
}
|
|
60631
60675
|
function parseArgs() {
|
|
60632
60676
|
const args = process.argv.slice(2);
|
|
60633
60677
|
const opts = {};
|
|
@@ -60652,7 +60696,8 @@ function parseArgs() {
|
|
|
60652
60696
|
json: flags.has("--json"),
|
|
60653
60697
|
noCrawl: flags.has("--no-crawl"),
|
|
60654
60698
|
interactive: !opts["--url"],
|
|
60655
|
-
networkTest: flags.has("--network-test")
|
|
60699
|
+
networkTest: flags.has("--network-test"),
|
|
60700
|
+
scanMode: "full"
|
|
60656
60701
|
};
|
|
60657
60702
|
}
|
|
60658
60703
|
function printHelp() {
|
|
@@ -60668,9 +60713,14 @@ function printHelp() {
|
|
|
60668
60713
|
console.log(` ${CYAN}--max-depth${RESET} <n> Crawl-Tiefe ${DIM}(Standard: 3)${RESET}`);
|
|
60669
60714
|
console.log(` ${CYAN}--project${RESET} <id> Projekt-ID ${DIM}(optional)${RESET}`);
|
|
60670
60715
|
console.log(` ${CYAN}--no-crawl${RESET} Nur eine URL scannen`);
|
|
60671
|
-
console.log(` ${CYAN}--network-test${RESET} Netzwerk-Resilienz Test
|
|
60716
|
+
console.log(` ${CYAN}--network-test${RESET} Netzwerk-Resilienz Test einschliessen`);
|
|
60672
60717
|
console.log(` ${CYAN}--json${RESET} JSON-Ausgabe`);
|
|
60673
60718
|
console.log("");
|
|
60719
|
+
console.log(` ${BOLD}API-Key:${RESET}`);
|
|
60720
|
+
console.log(` ${DIM}Per Flag:${RESET} --api-key AP_xxx`);
|
|
60721
|
+
console.log(` ${DIM}Per Env:${RESET} export ACCESSPILOT_API_KEY=AP_xxx`);
|
|
60722
|
+
console.log(` ${DIM}Holen:${RESET} ${CYAN}https://acpilot.de/dashboard/dev${RESET}`);
|
|
60723
|
+
console.log("");
|
|
60674
60724
|
console.log(` ${BOLD}Engine:${RESET} ${DIM}Volle ACPilot Engine lokal \u2014 identisch mit acpilot.de${RESET}`);
|
|
60675
60725
|
console.log(` ${DIM}28 WCAG-Regeln \xB7 axe-core \xB7 Color Contrast \xB7 Logic Tester${RESET}`);
|
|
60676
60726
|
console.log(` ${DIM}Screenreader Sim \xB7 DSGVO-Check \xB7 Netzwerk-Test${RESET}`);
|
|
@@ -60713,10 +60763,22 @@ async function main() {
|
|
|
60713
60763
|
console.log(` ${DIM} axe-core \xB7 Color Contrast \xB7 Tab Order \xB7 DSGVO \xB7 Netzwerk${RESET}`);
|
|
60714
60764
|
} else {
|
|
60715
60765
|
console.log(` ${YELLOW}\u26A0${RESET} Puppeteer nicht installiert \u2014 ${BOLD}Lite-Modus${RESET}`);
|
|
60716
|
-
console.log(` ${DIM}
|
|
60766
|
+
console.log(` ${DIM} Fuer volle Power: npm install -g puppeteer${RESET}`);
|
|
60717
60767
|
}
|
|
60718
60768
|
}
|
|
60719
|
-
if (
|
|
60769
|
+
if (!apiKey) {
|
|
60770
|
+
console.log("");
|
|
60771
|
+
console.error(` ${RED}\u2717${RESET} API-Key nicht gefunden.`);
|
|
60772
|
+
console.error("");
|
|
60773
|
+
console.error(` ${BOLD}Setze deinen API-Key auf eine der folgenden Weisen:${RESET}`);
|
|
60774
|
+
console.error(` ${CYAN}1.${RESET} Flag: ${GREEN}npx acpilot --api-key AP_xxx${RESET}`);
|
|
60775
|
+
console.error(` ${CYAN}2.${RESET} Env: ${GREEN}export ACCESSPILOT_API_KEY=AP_xxx${RESET}`);
|
|
60776
|
+
console.error("");
|
|
60777
|
+
console.error(` ${DIM}API-Key erstellen:${RESET} ${CYAN}https://acpilot.de/dashboard/dev${RESET}`);
|
|
60778
|
+
console.error("");
|
|
60779
|
+
process.exit(1);
|
|
60780
|
+
}
|
|
60781
|
+
if (args.interactive || !url) {
|
|
60720
60782
|
const rl = createRL();
|
|
60721
60783
|
console.log("");
|
|
60722
60784
|
console.log(` ${DIM}Scannt deine lokale Website mit der vollen ACPilot Engine.${RESET}`);
|
|
@@ -60726,35 +60788,43 @@ async function main() {
|
|
|
60726
60788
|
new URL(url);
|
|
60727
60789
|
} catch {
|
|
60728
60790
|
console.error(`
|
|
60729
|
-
${RED}\u2717${RESET}
|
|
60730
|
-
rl.close();
|
|
60731
|
-
process.exit(1);
|
|
60732
|
-
}
|
|
60733
|
-
if (!apiKey) {
|
|
60734
|
-
apiKey = await ask(rl, `${BOLD}API-Key${RESET} ${DIM}(von acpilot.de/dashboard/dev)${RESET}`);
|
|
60735
|
-
}
|
|
60736
|
-
if (!apiKey) {
|
|
60737
|
-
console.error(`
|
|
60738
|
-
${RED}\u2717${RESET} API-Key erforderlich \u2192 ${CYAN}https://acpilot.de/dashboard/dev${RESET}`);
|
|
60791
|
+
${RED}\u2717${RESET} Ungueltige URL: ${url}`);
|
|
60739
60792
|
rl.close();
|
|
60740
60793
|
process.exit(1);
|
|
60741
60794
|
}
|
|
60742
60795
|
console.log("");
|
|
60743
|
-
const modeChoice = await askChoice(rl, `${BOLD}Scan-Modus
|
|
60744
|
-
{ key: "1", label: "
|
|
60745
|
-
{ key: "2", label: "
|
|
60796
|
+
const modeChoice = await askChoice(rl, `${BOLD}Scan-Modus waehlen${RESET}`, [
|
|
60797
|
+
{ key: "1", label: "Komplett-Scan", desc: "\u2014 WCAG + Logic + Screenreader + DSGVO (crawlt alle Seiten)" },
|
|
60798
|
+
{ key: "2", label: "Einzelseite", desc: "\u2014 WCAG + Logic + Screenreader + DSGVO (nur eine URL)" },
|
|
60799
|
+
{ key: "3", label: "Nur DSGVO-Check", desc: "\u2014 Datenschutz-Pruefung einer einzelnen Seite" },
|
|
60800
|
+
{ key: "4", label: "Nur Netzwerk-Test", desc: "\u2014 Netzwerk-Resilienz testen" }
|
|
60746
60801
|
]);
|
|
60747
|
-
|
|
60748
|
-
|
|
60802
|
+
if (modeChoice === "1") {
|
|
60803
|
+
args.scanMode = "full";
|
|
60804
|
+
args.noCrawl = false;
|
|
60805
|
+
} else if (modeChoice === "2") {
|
|
60806
|
+
args.scanMode = "single";
|
|
60807
|
+
args.noCrawl = true;
|
|
60808
|
+
} else if (modeChoice === "3") {
|
|
60809
|
+
args.scanMode = "dsgvo";
|
|
60810
|
+
args.noCrawl = true;
|
|
60811
|
+
} else if (modeChoice === "4") {
|
|
60812
|
+
args.scanMode = "network";
|
|
60813
|
+
args.noCrawl = true;
|
|
60814
|
+
}
|
|
60815
|
+
if (args.scanMode === "full") {
|
|
60749
60816
|
const pagesStr = await ask(rl, "Max. Seitenanzahl", "50");
|
|
60750
60817
|
maxPages = parseInt(pagesStr, 10) || 50;
|
|
60751
60818
|
const depthStr = await ask(rl, "Max. Crawl-Tiefe", "3");
|
|
60752
60819
|
maxDepth = parseInt(depthStr, 10) || 3;
|
|
60753
60820
|
}
|
|
60754
|
-
if (puppeteerAvailable) {
|
|
60821
|
+
if ((args.scanMode === "full" || args.scanMode === "single") && puppeteerAvailable) {
|
|
60755
60822
|
const netChoice = await ask(rl, `Netzwerk-Resilienz Test? ${DIM}(j/n)${RESET}`, "n");
|
|
60756
60823
|
args.networkTest = netChoice.toLowerCase().startsWith("j") || netChoice.toLowerCase() === "y";
|
|
60757
60824
|
}
|
|
60825
|
+
if (args.scanMode === "network") {
|
|
60826
|
+
args.networkTest = true;
|
|
60827
|
+
}
|
|
60758
60828
|
rl.close();
|
|
60759
60829
|
args.url = url;
|
|
60760
60830
|
args.apiKey = apiKey;
|
|
@@ -60766,6 +60836,37 @@ async function main() {
|
|
|
60766
60836
|
async function runScan(args) {
|
|
60767
60837
|
const isJson = args.json;
|
|
60768
60838
|
const puppeteerAvailable = await hasPuppeteer();
|
|
60839
|
+
const logGuard = suppressLogs();
|
|
60840
|
+
try {
|
|
60841
|
+
return await runScanInner(args, isJson, puppeteerAvailable);
|
|
60842
|
+
} finally {
|
|
60843
|
+
logGuard.restore();
|
|
60844
|
+
}
|
|
60845
|
+
}
|
|
60846
|
+
async function runScanInner(args, isJson, puppeteerAvailable) {
|
|
60847
|
+
if (args.scanMode === "network") {
|
|
60848
|
+
if (!isJson) {
|
|
60849
|
+
printDivider("Netzwerk-Resilienz");
|
|
60850
|
+
process.stdout.write(` ${CYAN}\u27F3${RESET} Teste unter verschiedenen Netzwerkbedingungen... `);
|
|
60851
|
+
}
|
|
60852
|
+
try {
|
|
60853
|
+
const { runNetworkTest: runNetworkTest2 } = await Promise.resolve().then(() => (init_network_tester(), network_tester_exports));
|
|
60854
|
+
const networkResult2 = await runNetworkTest2(args.url);
|
|
60855
|
+
if (!isJson) {
|
|
60856
|
+
console.log(`${GREEN}\u2713${RESET}`);
|
|
60857
|
+
console.log(` ${BOLD}Netzwerk-Score:${RESET} ${scoreColor(networkResult2.score)}${networkResult2.score}/100${RESET}`);
|
|
60858
|
+
for (const p of networkResult2.profiles || []) {
|
|
60859
|
+
const timeColor = p.timedOut ? RED : p.loadTimeMs < 3e3 ? GREEN : p.loadTimeMs < 1e4 ? YELLOW : RED;
|
|
60860
|
+
console.log(` ${timeColor}${p.timedOut ? "TIMEOUT" : p.loadTimeMs + "ms"}${RESET} ${p.label}`);
|
|
60861
|
+
}
|
|
60862
|
+
} else {
|
|
60863
|
+
console.log(JSON.stringify({ networkResult: networkResult2 }, null, 2));
|
|
60864
|
+
}
|
|
60865
|
+
} catch (err) {
|
|
60866
|
+
if (!isJson) console.log(`${RED}\u2717${RESET} ${DIM}${err instanceof Error ? err.message : "Fehler"}${RESET}`);
|
|
60867
|
+
}
|
|
60868
|
+
return;
|
|
60869
|
+
}
|
|
60769
60870
|
if (!isJson) printDivider("Verbindung");
|
|
60770
60871
|
if (!isJson) process.stdout.write(` ${CYAN}\u27F3${RESET} API-Key wird verifiziert... `);
|
|
60771
60872
|
try {
|
|
@@ -60776,9 +60877,9 @@ async function runScan(args) {
|
|
|
60776
60877
|
});
|
|
60777
60878
|
const verifyData = await verifyRes.json();
|
|
60778
60879
|
if (!verifyData.valid) {
|
|
60779
|
-
if (!isJson) console.log(`${RED}\u2717
|
|
60880
|
+
if (!isJson) console.log(`${RED}\u2717 ungueltig${RESET}`);
|
|
60780
60881
|
console.error(`
|
|
60781
|
-
${RED}Fehler:${RESET} ${verifyData.error || "
|
|
60882
|
+
${RED}Fehler:${RESET} ${verifyData.error || "Ungueltiger API-Key"}`);
|
|
60782
60883
|
process.exit(1);
|
|
60783
60884
|
}
|
|
60784
60885
|
if (!isJson) console.log(`${GREEN}\u2713 verifiziert${RESET}`);
|
|
@@ -60833,7 +60934,12 @@ async function runScan(args) {
|
|
|
60833
60934
|
}
|
|
60834
60935
|
if (!isJson) {
|
|
60835
60936
|
printDivider("Scanning");
|
|
60836
|
-
|
|
60937
|
+
let engineDesc;
|
|
60938
|
+
if (args.scanMode === "dsgvo") {
|
|
60939
|
+
engineDesc = "DSGVO-Check";
|
|
60940
|
+
} else {
|
|
60941
|
+
engineDesc = puppeteerAvailable ? "28 Regeln + axe-core + Contrast + Logic + Screenreader + DSGVO" : "28 Regeln + Logic + Screenreader (Lite \u2014 kein Puppeteer)";
|
|
60942
|
+
}
|
|
60837
60943
|
console.log(` ${DIM}${engineDesc}${RESET}
|
|
60838
60944
|
`);
|
|
60839
60945
|
}
|
|
@@ -60853,36 +60959,45 @@ async function runScan(args) {
|
|
|
60853
60959
|
const path = chunk[j].replace(origin, "") || "/";
|
|
60854
60960
|
if (s.status === "fulfilled") {
|
|
60855
60961
|
const r = s.value;
|
|
60856
|
-
|
|
60857
|
-
|
|
60858
|
-
|
|
60859
|
-
|
|
60860
|
-
|
|
60861
|
-
|
|
60862
|
-
|
|
60863
|
-
|
|
60962
|
+
if (args.scanMode !== "dsgvo") {
|
|
60963
|
+
try {
|
|
60964
|
+
const { runLogicTest: runLogicTest2 } = await Promise.resolve().then(() => (init_logic_tester(), logic_tester_exports));
|
|
60965
|
+
const htmlRes = await fetch(chunk[j], { signal: AbortSignal.timeout(5e3) });
|
|
60966
|
+
if (htmlRes.ok) {
|
|
60967
|
+
const html3 = await htmlRes.text();
|
|
60968
|
+
const logicResult = runLogicTest2(html3, chunk[j]);
|
|
60969
|
+
r.logicScore = logicResult.overallScore;
|
|
60970
|
+
r.logicIssues = logicResult.summary.totalIssues;
|
|
60971
|
+
}
|
|
60972
|
+
} catch {
|
|
60864
60973
|
}
|
|
60865
|
-
} catch {
|
|
60866
60974
|
}
|
|
60867
|
-
|
|
60868
|
-
|
|
60869
|
-
|
|
60870
|
-
|
|
60871
|
-
|
|
60872
|
-
|
|
60873
|
-
|
|
60874
|
-
|
|
60975
|
+
if (args.scanMode !== "dsgvo") {
|
|
60976
|
+
try {
|
|
60977
|
+
const { simulateScreenreader: simulateScreenreader2 } = await Promise.resolve().then(() => (init_screenreader_simulator(), screenreader_simulator_exports));
|
|
60978
|
+
const htmlRes = await fetch(chunk[j], { signal: AbortSignal.timeout(5e3) });
|
|
60979
|
+
if (htmlRes.ok) {
|
|
60980
|
+
const html3 = await htmlRes.text();
|
|
60981
|
+
const srResult = simulateScreenreader2(html3, chunk[j]);
|
|
60982
|
+
r.srStats = srResult.stats;
|
|
60983
|
+
r.srIssueCount = srResult.issues.length;
|
|
60984
|
+
}
|
|
60985
|
+
} catch {
|
|
60875
60986
|
}
|
|
60876
|
-
} catch {
|
|
60877
60987
|
}
|
|
60878
60988
|
results.push(r);
|
|
60879
60989
|
if (!isJson) {
|
|
60880
|
-
|
|
60881
|
-
|
|
60882
|
-
|
|
60883
|
-
|
|
60884
|
-
|
|
60885
|
-
|
|
60990
|
+
if (args.scanMode === "dsgvo") {
|
|
60991
|
+
const dsgvo = r.dsgvoFindings?.length ? `${YELLOW}${r.dsgvoFindings.length} DSGVO-Findings${RESET}` : `${GREEN}keine DSGVO-Findings${RESET}`;
|
|
60992
|
+
console.log(` ${path.padEnd(28)} ${dsgvo}`);
|
|
60993
|
+
} else {
|
|
60994
|
+
const sc = scoreColor(r.score);
|
|
60995
|
+
const severity = r.critical > 0 ? `${RED}${r.critical} krit.${RESET}` : r.serious > 0 ? `${YELLOW}${r.serious} ernst${RESET}` : `${GREEN}sauber${RESET}`;
|
|
60996
|
+
const logic = r.logicScore != null ? ` ${DIM}L:${r.logicScore}${RESET}` : "";
|
|
60997
|
+
const sr = r.srIssueCount != null && r.srIssueCount > 0 ? ` ${DIM}SR:${r.srIssueCount}${RESET}` : "";
|
|
60998
|
+
const dsgvo = r.dsgvoFindings?.length ? ` ${DIM}DSGVO:${r.dsgvoFindings.length}${RESET}` : "";
|
|
60999
|
+
console.log(` ${sc}${String(r.score).padStart(3)}${RESET} ${path.padEnd(28)} ${severity}${logic}${sr}${dsgvo} ${DIM}${r.loadTimeMs}ms${RESET}`);
|
|
61000
|
+
}
|
|
60886
61001
|
}
|
|
60887
61002
|
} else {
|
|
60888
61003
|
if (!isJson) {
|
|
@@ -60941,29 +61056,47 @@ async function runScan(args) {
|
|
|
60941
61056
|
console.log(JSON.stringify({ ...pushData, results, networkResult }, null, 2));
|
|
60942
61057
|
return;
|
|
60943
61058
|
}
|
|
60944
|
-
|
|
60945
|
-
|
|
60946
|
-
|
|
60947
|
-
|
|
60948
|
-
|
|
60949
|
-
|
|
60950
|
-
|
|
60951
|
-
|
|
60952
|
-
|
|
60953
|
-
|
|
60954
|
-
|
|
60955
|
-
|
|
60956
|
-
|
|
60957
|
-
|
|
60958
|
-
|
|
60959
|
-
|
|
60960
|
-
|
|
60961
|
-
|
|
60962
|
-
|
|
60963
|
-
|
|
60964
|
-
|
|
60965
|
-
|
|
60966
|
-
|
|
61059
|
+
if (args.scanMode === "dsgvo") {
|
|
61060
|
+
const dsgvoCount = results.reduce((s, r) => s + (r.dsgvoFindings?.length ?? 0), 0);
|
|
61061
|
+
printDivider("DSGVO-Ergebnis");
|
|
61062
|
+
console.log("");
|
|
61063
|
+
console.log(` ${BOLD}Seiten gescannt:${RESET} ${results.length}`);
|
|
61064
|
+
console.log(` ${BOLD}DSGVO Findings:${RESET} ${dsgvoCount > 0 ? `${YELLOW}${dsgvoCount}${RESET}` : `${GREEN}0${RESET}`}`);
|
|
61065
|
+
for (const r of results) {
|
|
61066
|
+
if (r.dsgvoFindings?.length) {
|
|
61067
|
+
const path = r.url?.replace(new URL(args.url).origin, "") || "/";
|
|
61068
|
+
console.log(`
|
|
61069
|
+
${BOLD}${path}${RESET}`);
|
|
61070
|
+
for (const f of r.dsgvoFindings) {
|
|
61071
|
+
console.log(` ${YELLOW}\u25CF${RESET} ${f.title || f.message || f}`);
|
|
61072
|
+
}
|
|
61073
|
+
}
|
|
61074
|
+
}
|
|
61075
|
+
} else {
|
|
61076
|
+
const avgScore = Math.round(results.reduce((s, r) => s + r.score, 0) / results.length);
|
|
61077
|
+
const totalFindings = results.reduce((s, r) => s + r.totalFindings, 0);
|
|
61078
|
+
const totalCritical = results.reduce((s, r) => s + r.critical, 0);
|
|
61079
|
+
const totalSerious = results.reduce((s, r) => s + r.serious, 0);
|
|
61080
|
+
const totalModerate = results.reduce((s, r) => s + r.moderate, 0);
|
|
61081
|
+
const totalMinor = results.reduce((s, r) => s + r.minor, 0);
|
|
61082
|
+
const logicScores = results.filter((r) => r.logicScore != null).map((r) => r.logicScore);
|
|
61083
|
+
const avgLogic = logicScores.length > 0 ? Math.round(logicScores.reduce((a, b) => a + b, 0) / logicScores.length) : null;
|
|
61084
|
+
const totalSrIssues = results.reduce((s, r) => s + (r.srIssueCount ?? 0), 0);
|
|
61085
|
+
const dsgvoCount = results.reduce((s, r) => s + (r.dsgvoFindings?.length ?? 0), 0);
|
|
61086
|
+
printDivider("Ergebnis");
|
|
61087
|
+
console.log(bigScore(avgScore));
|
|
61088
|
+
console.log("");
|
|
61089
|
+
console.log(` ${BOLD}Seiten gescannt:${RESET} ${results.length}`);
|
|
61090
|
+
console.log(` ${BOLD}WCAG Findings:${RESET} ${totalFindings}`);
|
|
61091
|
+
if (totalCritical > 0) console.log(` ${RED}\u25CF Kritisch:${RESET} ${totalCritical}`);
|
|
61092
|
+
if (totalSerious > 0) console.log(` ${YELLOW}\u25CF Ernst:${RESET} ${totalSerious}`);
|
|
61093
|
+
if (totalModerate > 0) console.log(` ${CYAN}\u25CF Moderat:${RESET} ${totalModerate}`);
|
|
61094
|
+
if (totalMinor > 0) console.log(` ${GRAY}\u25CF Gering:${RESET} ${totalMinor}`);
|
|
61095
|
+
if (avgLogic != null) console.log(` ${BOLD}Logic Score:${RESET} ${scoreColor(avgLogic)}${avgLogic}/100${RESET}`);
|
|
61096
|
+
if (totalSrIssues > 0) console.log(` ${BOLD}Screenreader:${RESET} ${totalSrIssues} Probleme`);
|
|
61097
|
+
if (dsgvoCount > 0) console.log(` ${BOLD}DSGVO Findings:${RESET} ${dsgvoCount}`);
|
|
61098
|
+
if (networkResult) console.log(` ${BOLD}Netzwerk-Score:${RESET} ${scoreColor(networkResult.score)}${networkResult.score}/100${RESET}`);
|
|
61099
|
+
}
|
|
60967
61100
|
console.log("");
|
|
60968
61101
|
console.log(` ${CYAN}\u2197${RESET} ${BOLD}${args.server}${pushData.dashboard_url}${RESET}`);
|
|
60969
61102
|
console.log("");
|