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.
Files changed (2) hide show
  1. package/dist/cli.js +1164 -1031
  2. 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
- version
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
- version
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
- version,
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
- if (profileSlow3g) {
60270
- if (profileSlow3g.timedOut || profileSlow3g.loadTimeMs > 45e3) {
60271
- findings.push({
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 (baseline && baseline.loadTimeMs > 3e3) {
60283
- findings.push({
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
- if (profileOffline) {
60306
- const hasOfflineFallback = !profileOffline.timedOut && profileOffline.totalRequests > 0;
60307
- if (profileOffline.timedOut && !hasOfflineFallback) {
60308
- findings.push({
60309
- id: nextId2(),
60310
- category: "offline",
60311
- severity: "moderate",
60312
- title: "Keine Offline-Fallback-Seite",
60313
- description: "Wenn die Netzwerkverbindung ausf\xE4llt, sieht der Nutzer nur eine Browser-Fehlermeldung. Eine benutzerdefinierte Offline-Seite verbessert die Nutzererfahrung erheblich.",
60314
- 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."
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 (metrics.imagesWithoutLazy > 3) {
60319
- findings.push({
60320
- id: nextId2(),
60321
- category: "bilder",
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
- if (metrics.totalPageSizeBytes > 5 * 1024 * 1024) {
60330
- findings.push({
60331
- id: nextId2(),
60332
- category: "groesse",
60333
- severity: "critical",
60334
- title: "Seitengr\xF6\xDFe \xFCber 5 MB",
60335
- 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.`,
60336
- value: `${(metrics.totalPageSizeBytes / (1024 * 1024)).toFixed(1)} MB`,
60337
- recommendation: "Bilder als moderne Formate (WebP/AVIF) bereitstellen, Videos nur bei Bedarf laden, JavaScript-Bundles aufteilen und Tree-Shaking einsetzen."
60338
- });
60339
- } else if (metrics.totalPageSizeBytes > 3 * 1024 * 1024) {
60340
- findings.push({
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
- if (!metrics.hasFontDisplaySwap && metrics.webFontCount > 0) {
60351
- findings.push({
60352
- id: nextId2(),
60353
- category: "schriften",
60354
- severity: "moderate",
60355
- title: "Webfonts blockieren Rendering",
60356
- 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).`,
60357
- value: `${metrics.webFontCount} Fonts`,
60358
- recommendation: "In allen @font-face-Regeln font-display: swap oder font-display: optional setzen, damit Text sofort mit einer Ersatzschrift angezeigt wird."
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
- if (metrics.webFontCount > 4) {
60362
- findings.push({
60363
- id: nextId2(),
60364
- category: "schriften",
60365
- severity: "minor",
60366
- title: "Viele Webfonts geladen",
60367
- description: `Es werden ${metrics.webFontCount} Webfont-Dateien geladen. Jede zus\xE4tzliche Schrift verl\xE4ngert die Ladezeit.`,
60368
- value: `${metrics.webFontCount} Fonts`,
60369
- recommendation: "Anzahl der Schriftvarianten reduzieren. Variable Fonts verwenden, um mehrere Schnitte in einer Datei zu b\xFCndeln."
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
- if (!metrics.hasCriticalCssInline && baseline && baseline.loadTimeMs > 3e3) {
60373
- findings.push({
60374
- id: nextId2(),
60375
- category: "rendering",
60376
- severity: "minor",
60377
- title: "Kein kritisches CSS inline",
60378
- 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.",
60379
- recommendation: "Kritisches CSS (Above-the-fold-Styles) direkt im <head> als <style>-Block einbetten. Restliches CSS asynchron nachladen."
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 (!metrics.hasLoadingIndicators) {
60383
- findings.push({
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 (metrics.thirdPartyRequests > 20) {
60393
- findings.push({
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
- return findings;
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
- if (baselineCleanup) {
60451
- try {
60452
- await baselineCleanup();
60453
- } catch {
60454
- }
60455
- baselineCleanup = void 0;
60456
- baselinePage = void 0;
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
- const throttledProfiles = NETWORK_PROFILES.filter((p) => p.name !== "fast");
60459
- for (const profile of throttledProfiles) {
60460
- try {
60461
- if (isDev2) console.log(`[NetworkTest] Teste Profil: ${profile.label}`);
60462
- const testResult = await testWithProfile(url, profile);
60463
- profileResults.push(testResult.result);
60464
- if (testResult.cleanup) {
60465
- try {
60466
- await testResult.cleanup();
60467
- } catch {
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
- profileResults.push({
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
- return {
60506
- url,
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 isDev2, NETWORK_PROFILES;
60527
- var init_network_tester = __esm({
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
- init_browser();
60532
- isDev2 = process.env.NODE_ENV === "development";
60533
- NETWORK_PROFILES = [
60534
- {
60535
- name: "fast",
60536
- label: "Schnelles Netz (4G/WiFi)",
60537
- downloadThroughput: -1,
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} ${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} ${BLUE}\u2551${RESET}`);
60591
- 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}`);
60592
- 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${RESET} ${BLUE}\u2551${RESET}`);
60593
- console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588 \u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588${RESET} ${BLUE}\u2551${RESET}`);
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} ${BLUE}\u2551${RESET}`);
60595
- console.log(`${BLUE} \u2551${RESET} ${BLUE}\u2551${RESET}`);
60596
- console.log(`${BLUE} \u2551${RESET} ${DIM}${ITALIC} Barrierefreiheit f\xFCr Entwickler${RESET} ${DIM}v2.0.0${RESET} ${BLUE}\u2551${RESET}`);
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 einschlie\xDFen`);
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} F\xFCr volle Power: npm install -g puppeteer${RESET}`);
60766
+ console.log(` ${DIM} Fuer volle Power: npm install -g puppeteer${RESET}`);
60717
60767
  }
60718
60768
  }
60719
- if (args.interactive || !url || !apiKey) {
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} Ung\xFCltige URL: ${url}`);
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 w\xE4hlen${RESET}`, [
60744
- { key: "1", label: "Komplette Website", desc: "\u2014 Crawlt alle Unterseiten" },
60745
- { key: "2", label: "Einzelne Seite", desc: "\u2014 Nur die eingegebene URL" }
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
- args.noCrawl = modeChoice === "2";
60748
- if (!args.noCrawl) {
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 ung\xFCltig${RESET}`);
60880
+ if (!isJson) console.log(`${RED}\u2717 ungueltig${RESET}`);
60780
60881
  console.error(`
60781
- ${RED}Fehler:${RESET} ${verifyData.error || "Ung\xFCltiger API-Key"}`);
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
- const engineDesc = puppeteerAvailable ? "28 Regeln + axe-core + Contrast + Logic + Screenreader + DSGVO" : "28 Regeln + Logic + Screenreader (Lite \u2014 kein Puppeteer)";
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
- try {
60857
- const { runLogicTest: runLogicTest2 } = await Promise.resolve().then(() => (init_logic_tester(), logic_tester_exports));
60858
- const htmlRes = await fetch(chunk[j], { signal: AbortSignal.timeout(5e3) });
60859
- if (htmlRes.ok) {
60860
- const html3 = await htmlRes.text();
60861
- const logicResult = runLogicTest2(html3, chunk[j]);
60862
- r.logicScore = logicResult.overallScore;
60863
- r.logicIssues = logicResult.summary.totalIssues;
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
- try {
60868
- const { simulateScreenreader: simulateScreenreader2 } = await Promise.resolve().then(() => (init_screenreader_simulator(), screenreader_simulator_exports));
60869
- const htmlRes = await fetch(chunk[j], { signal: AbortSignal.timeout(5e3) });
60870
- if (htmlRes.ok) {
60871
- const html3 = await htmlRes.text();
60872
- const srResult = simulateScreenreader2(html3, chunk[j]);
60873
- r.srStats = srResult.stats;
60874
- r.srIssueCount = srResult.issues.length;
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
- const sc = scoreColor(r.score);
60881
- const severity = r.critical > 0 ? `${RED}${r.critical} krit.${RESET}` : r.serious > 0 ? `${YELLOW}${r.serious} ernst${RESET}` : `${GREEN}sauber${RESET}`;
60882
- const logic = r.logicScore != null ? ` ${DIM}L:${r.logicScore}${RESET}` : "";
60883
- const sr = r.srIssueCount != null && r.srIssueCount > 0 ? ` ${DIM}SR:${r.srIssueCount}${RESET}` : "";
60884
- const dsgvo = r.dsgvoFindings?.length ? ` ${DIM}DSGVO:${r.dsgvoFindings.length}${RESET}` : "";
60885
- console.log(` ${sc}${String(r.score).padStart(3)}${RESET} ${path.padEnd(28)} ${severity}${logic}${sr}${dsgvo} ${DIM}${r.loadTimeMs}ms${RESET}`);
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
- const avgScore = Math.round(results.reduce((s, r) => s + r.score, 0) / results.length);
60945
- const totalFindings = results.reduce((s, r) => s + r.totalFindings, 0);
60946
- const totalCritical = results.reduce((s, r) => s + r.critical, 0);
60947
- const totalSerious = results.reduce((s, r) => s + r.serious, 0);
60948
- const totalModerate = results.reduce((s, r) => s + r.moderate, 0);
60949
- const totalMinor = results.reduce((s, r) => s + r.minor, 0);
60950
- const logicScores = results.filter((r) => r.logicScore != null).map((r) => r.logicScore);
60951
- const avgLogic = logicScores.length > 0 ? Math.round(logicScores.reduce((a, b) => a + b, 0) / logicScores.length) : null;
60952
- const totalSrIssues = results.reduce((s, r) => s + (r.srIssueCount ?? 0), 0);
60953
- const dsgvoCount = results.reduce((s, r) => s + (r.dsgvoFindings?.length ?? 0), 0);
60954
- printDivider("Ergebnis");
60955
- console.log(bigScore(avgScore));
60956
- console.log("");
60957
- console.log(` ${BOLD}Seiten gescannt:${RESET} ${results.length}`);
60958
- console.log(` ${BOLD}WCAG Findings:${RESET} ${totalFindings}`);
60959
- if (totalCritical > 0) console.log(` ${RED}\u25CF Kritisch:${RESET} ${totalCritical}`);
60960
- if (totalSerious > 0) console.log(` ${YELLOW}\u25CF Ernst:${RESET} ${totalSerious}`);
60961
- if (totalModerate > 0) console.log(` ${CYAN}\u25CF Moderat:${RESET} ${totalModerate}`);
60962
- if (totalMinor > 0) console.log(` ${GRAY}\u25CF Gering:${RESET} ${totalMinor}`);
60963
- if (avgLogic != null) console.log(` ${BOLD}Logic Score:${RESET} ${scoreColor(avgLogic)}${avgLogic}/100${RESET}`);
60964
- if (totalSrIssues > 0) console.log(` ${BOLD}Screenreader:${RESET} ${totalSrIssues} Probleme`);
60965
- if (dsgvoCount > 0) console.log(` ${BOLD}DSGVO Findings:${RESET} ${dsgvoCount}`);
60966
- if (networkResult) console.log(` ${BOLD}Netzwerk-Score:${RESET} ${scoreColor(networkResult.score)}${networkResult.score}/100${RESET}`);
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("");