From 83e30eb9e0a49243241cf048b3c7b60132cd5c03 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:09:11 +0400 Subject: [PATCH 01/22] security: pin patched sharp with npm overrides --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 63f038d..e2d1767 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "@11ty/eleventy-plugin-rss": "^2.0.4", "markdown-it": "^14.3.0", "markdown-it-anchor": "^9.2.1" + }, + "overrides": { + "sharp": "0.35.5" } } From 0c7a7c1d90e7d4ede266704b74ad3e7c766f732c Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:09:40 +0400 Subject: [PATCH 02/22] security: disallow inline JavaScript in CSP --- src/_includes/base.njk | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/_includes/base.njk b/src/_includes/base.njk index b0686fa..e2e5085 100644 --- a/src/_includes/base.njk +++ b/src/_includes/base.njk @@ -3,24 +3,8 @@ - - + + {{ title or site.title }} From 6d44ffcd525d9c71831c80b12b1ab55bf8e42511 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:09:42 +0400 Subject: [PATCH 03/22] security: externalize early theme initialization --- src/assets/js/theme-init.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/assets/js/theme-init.js diff --git a/src/assets/js/theme-init.js b/src/assets/js/theme-init.js new file mode 100644 index 0000000..b2565bc --- /dev/null +++ b/src/assets/js/theme-init.js @@ -0,0 +1,15 @@ +/* Set theme before paint to avoid a flash: saved choice, else system preference. + Also emit a matching theme-color meta so the mobile browser chrome matches. */ +(function () { + document.documentElement.classList.add('js'); + var theme = 'dark'; + try { + var saved = localStorage.getItem('theme'); + theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); + } catch (e) {} + document.documentElement.dataset.theme = theme; + var meta = document.createElement('meta'); + meta.name = 'theme-color'; + meta.content = theme === 'light' ? '#eef2f8' : '#070d19'; + document.head.appendChild(meta); +})(); From 2418cdccdadaf2b0bddcea44986a5146ac3d0375 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:17:10 +0400 Subject: [PATCH 04/22] ci: temporarily generate npm lockfile for security update --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c798ad8..f407060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,14 @@ jobs: node-version: 20 cache: npm + - name: Refresh lockfile for PR repair (temporary) + run: | + npm install --package-lock-only --ignore-scripts --no-audit --no-fund + echo "LOCKFILE_BASE64_BEGIN" + base64 -w0 package-lock.json + echo + echo "LOCKFILE_BASE64_END" + - name: Install dependencies run: npm ci From d825efeec0e64428fc6ebff77bd8664a5a263c22 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:19:35 +0400 Subject: [PATCH 05/22] security: regenerate npm lockfile for patched sharp --- package-lock.json | 467 ++++++++++++++++++++++++++-------------------- 1 file changed, 263 insertions(+), 204 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a19251..c842bce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -264,9 +264,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", "optional": true, @@ -274,10 +274,20 @@ "tslib": "^2.4.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -288,19 +298,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -311,19 +321,39 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -338,9 +368,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -355,16 +385,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -375,16 +402,47 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -395,16 +453,13 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -415,16 +470,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -435,16 +487,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -455,16 +504,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -475,185 +521,250 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@img/sharp-wasm32": "0.35.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -664,16 +775,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -684,7 +795,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -959,51 +1070,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -1546,13 +1612,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "dev": true, - "license": "MIT" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2241,53 +2300,53 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "dev": true, - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/slash": { From 9ab78507a8c87505e4c525374f3b81cc656f5c2f Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:19:48 +0400 Subject: [PATCH 06/22] ci: restore standard npm clean-install build workflow --- .github/workflows/ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f407060..c798ad8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,14 +20,6 @@ jobs: node-version: 20 cache: npm - - name: Refresh lockfile for PR repair (temporary) - run: | - npm install --package-lock-only --ignore-scripts --no-audit --no-fund - echo "LOCKFILE_BASE64_BEGIN" - base64 -w0 package-lock.json - echo - echo "LOCKFILE_BASE64_END" - - name: Install dependencies run: npm ci From a24332a7ba0cdf3d4fda36f21019a36e9aa71733 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:52:01 +0400 Subject: [PATCH 07/22] ci: temporarily audit dependencies and verify browser test capability --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c798ad8..48d7165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,12 @@ jobs: - name: Build site with Eleventy run: npm run build + + - name: Audit and browser readiness (temporary) + shell: bash + run: | + npm audit --json > /tmp/npm-security-audit.json || true + node -e 'const a=require("/tmp/npm-security-audit.json"); console.log("AUDIT_SUMMARY",JSON.stringify({counts:a.metadata?.vulnerabilities,affected:Object.entries(a.vulnerabilities||{}).map(([name,v])=>({name,severity:v.severity,via:v.via.map(x=>typeof x==="string"?x:{name:x.name,title:x.title,range:x.range})}))}));' + echo "CHROME_BINARY_START" + command -v google-chrome || command -v chromium || command -v chromium-browser || true + echo "CHROME_BINARY_END" From cfecb2cabfb2ca5aee4b350bb863c1db8dc43ba9 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:53:50 +0400 Subject: [PATCH 08/22] test: temporarily smoke-test generated site under Chromium --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48d7165..991c583 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,3 +34,24 @@ jobs: echo "CHROME_BINARY_START" command -v google-chrome || command -v chromium || command -v chromium-browser || true echo "CHROME_BINARY_END" + + - name: Chromium smoke test (temporary) + shell: bash + run: | + set -euo pipefail + test -s _site/assets/js/theme-init.js + python3 -m http.server 8765 --directory _site >/tmp/portfolio-http.log 2>&1 & + SERVER_PID=$! + trap 'kill $SERVER_PID || true' EXIT + sleep 2 + curl -fsS http://127.0.0.1:8765/ > /tmp/site-home.html + grep -F 'script-src '' /tmp/site-home.html >/dev/null || grep -F "script-src 'self'" /tmp/site-home.html >/dev/null + google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --virtual-time-budget=2500 --dump-dom http://127.0.0.1:8765/ > /tmp/site-rendered.html 2>/tmp/chrome-stderr.log + grep -Eq ']*class="[^"]*js' /tmp/site-rendered.html + grep -Eq ']*data-theme="(dark|light)"' /tmp/site-rendered.html + grep -q 'name="theme-color"' /tmp/site-rendered.html + grep -q 'class="theme-toggle"' /tmp/site-rendered.html + echo "BROWSER_SMOKE_SUCCESS" + if grep -Ei '(Refused to execute|Content Security Policy|ERR_|Uncaught)' /tmp/chrome-stderr.log; then + echo "BROWSER_CONSOLE_WARNING_FOUND" + fi From f9f35762693fb94d857bc4acf4f1a26c82299cfe Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:55:12 +0400 Subject: [PATCH 09/22] ci: restore original workflow after security audit and Chromium smoke test --- .github/workflows/ci.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 991c583..c798ad8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,33 +25,3 @@ jobs: - name: Build site with Eleventy run: npm run build - - - name: Audit and browser readiness (temporary) - shell: bash - run: | - npm audit --json > /tmp/npm-security-audit.json || true - node -e 'const a=require("/tmp/npm-security-audit.json"); console.log("AUDIT_SUMMARY",JSON.stringify({counts:a.metadata?.vulnerabilities,affected:Object.entries(a.vulnerabilities||{}).map(([name,v])=>({name,severity:v.severity,via:v.via.map(x=>typeof x==="string"?x:{name:x.name,title:x.title,range:x.range})}))}));' - echo "CHROME_BINARY_START" - command -v google-chrome || command -v chromium || command -v chromium-browser || true - echo "CHROME_BINARY_END" - - - name: Chromium smoke test (temporary) - shell: bash - run: | - set -euo pipefail - test -s _site/assets/js/theme-init.js - python3 -m http.server 8765 --directory _site >/tmp/portfolio-http.log 2>&1 & - SERVER_PID=$! - trap 'kill $SERVER_PID || true' EXIT - sleep 2 - curl -fsS http://127.0.0.1:8765/ > /tmp/site-home.html - grep -F 'script-src '' /tmp/site-home.html >/dev/null || grep -F "script-src 'self'" /tmp/site-home.html >/dev/null - google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --virtual-time-budget=2500 --dump-dom http://127.0.0.1:8765/ > /tmp/site-rendered.html 2>/tmp/chrome-stderr.log - grep -Eq ']*class="[^"]*js' /tmp/site-rendered.html - grep -Eq ']*data-theme="(dark|light)"' /tmp/site-rendered.html - grep -q 'name="theme-color"' /tmp/site-rendered.html - grep -q 'class="theme-toggle"' /tmp/site-rendered.html - echo "BROWSER_SMOKE_SUCCESS" - if grep -Ei '(Refused to execute|Content Security Policy|ERR_|Uncaught)' /tmp/chrome-stderr.log; then - echo "BROWSER_CONSOLE_WARNING_FOUND" - fi From 2a98d854a69cb0172f702e34f2a9c09eaacbd5f3 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:07:09 +0400 Subject: [PATCH 10/22] test: add temporary PR37 interactive browser regression harness --- scripts/pr37-qa.html | 147 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 scripts/pr37-qa.html diff --git a/scripts/pr37-qa.html b/scripts/pr37-qa.html new file mode 100644 index 0000000..22c4bd7 --- /dev/null +++ b/scripts/pr37-qa.html @@ -0,0 +1,147 @@ + + +PR37 temporary browser validation +

PR37 Chromium validation

Running...
+ + From f1244626a768813e371e7c447438ba08b458e01a Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:07:27 +0400 Subject: [PATCH 11/22] ci: temporarily run interactive Chromium security, theme and image validation --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c798ad8..c4cda61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,23 @@ jobs: - name: Build site with Eleventy run: npm run build + + - name: PR37 interactive browser and image tests (temporary) + shell: bash + run: | + set -euo pipefail + cp scripts/pr37-qa.html _site/pr37-qa.html + python3 -m http.server 8765 --directory _site >/tmp/pr37-server.log 2>&1 & + SERVER_PID=$! + trap 'kill $SERVER_PID || true' EXIT + sleep 2 + google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --disable-background-networking --virtual-time-budget=14000 --dump-dom http://127.0.0.1:8765/pr37-qa.html >/tmp/pr37-qa-dom.html 2>/tmp/pr37-chrome.log + node - <<'NODE' + const fs=require('fs'); + const html=fs.readFileSync('/tmp/pr37-qa-dom.html','utf8'); + const m=html.match(/data-qa-report="([^"]+)"/); + if(!m){console.log('CHROMIUM_DOM_TAIL',html.slice(-1000));process.exit(1)} + const report=JSON.parse(Buffer.from(m[1],'base64').toString()); + console.log('PR37_VALIDATION_REPORT',JSON.stringify(report)); + if(report.failed||report.fatal||report.passed!==9) process.exit(1); + NODE From f2e408e2973cf5c6dca9ae05b9861bedc3d713db Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:07:34 +0400 Subject: [PATCH 12/22] ci: temporarily execute browser regression harness for PR37 --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4cda61..64d3efb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,3 +45,23 @@ jobs: console.log('PR37_VALIDATION_REPORT',JSON.stringify(report)); if(report.failed||report.fatal||report.passed!==9) process.exit(1); NODE + + - name: PR37 browser validation (temporary) + shell: bash + run: | + set -euo pipefail + cp scripts/pr37-qa.html _site/pr37-qa.html + python3 -m http.server 8765 --directory _site >/tmp/pr37-http.log 2>&1 & + SERVER_PID=$! + trap 'kill $SERVER_PID || true' EXIT + sleep 2 + google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --virtual-time-budget=18000 --dump-dom http://127.0.0.1:8765/pr37-qa.html > /tmp/pr37-dom.html 2> /tmp/pr37-chrome.log + python3 - <<'PY' + import re,base64,json,html + text=open('/tmp/pr37-dom.html').read() + m=re.search(r'data-qa-report="([^"]+)"',text) + if not m: raise SystemExit("Browser test did not finish; tail of output: "+text[-1000:]) + report=json.loads(base64.b64decode(html.unescape(m.group(1)))) + print("PR37_QA_REPORT "+json.dumps(report)) + if report.get("failed",1): raise SystemExit("Interactive regression test failure") + PY From 68be28a459fbcaac7a1839e1433f7026032ab7af Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:09:16 +0400 Subject: [PATCH 13/22] test: bound persisted theme reload validation without iframe onload race --- scripts/pr37-qa.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/pr37-qa.html b/scripts/pr37-qa.html index 22c4bd7..b4f63c7 100644 --- a/scripts/pr37-qa.html +++ b/scripts/pr37-qa.html @@ -92,9 +92,11 @@ }); await perform('Persisted theme survives reload', async () => { const before = frame.contentDocument.documentElement.dataset.theme; - const loaded = new Promise(resolve => {frame.onload = resolve;}); - frame.src = '/?qa-pr37-reload=1'; - await loaded; + frame.contentWindow.location.replace('/?qa-pr37-reload=1'); + await waitUntil(() => { + try {return frame.contentWindow.location.search.includes('qa-pr37-reload') && + frame.contentDocument.documentElement.classList.contains('js');} catch (_) {return false;} + }, 6000); assert(frame.contentDocument.documentElement.dataset.theme === before, 'Theme lost after reload'); assert(frame.contentDocument.documentElement.classList.contains('js'), 'Initializer missing after reload'); return 'persisted ' + before; From d4ff7c02b0b69c6c93d08d9b7423c15be5944180 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:09:42 +0400 Subject: [PATCH 14/22] test: use independent iframe for theme persistence check --- scripts/pr37-qa.html | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/scripts/pr37-qa.html b/scripts/pr37-qa.html index b4f63c7..aed4c74 100644 --- a/scripts/pr37-qa.html +++ b/scripts/pr37-qa.html @@ -92,13 +92,9 @@ }); await perform('Persisted theme survives reload', async () => { const before = frame.contentDocument.documentElement.dataset.theme; - frame.contentWindow.location.replace('/?qa-pr37-reload=1'); - await waitUntil(() => { - try {return frame.contentWindow.location.search.includes('qa-pr37-reload') && - frame.contentDocument.documentElement.classList.contains('js');} catch (_) {return false;} - }, 6000); - assert(frame.contentDocument.documentElement.dataset.theme === before, 'Theme lost after reload'); - assert(frame.contentDocument.documentElement.classList.contains('js'), 'Initializer missing after reload'); + const reloaded = await load('/?qa-pr37-reload=1', 390); + assert(reloaded.contentDocument.documentElement.dataset.theme === before, 'Theme lost after reload'); + assert(reloaded.contentDocument.documentElement.classList.contains('js'), 'Initializer missing after reload'); return 'persisted ' + before; }); await perform('Mobile nav opens and Escape closes', async () => { From 76fc6070f7153169e76414c343d55240b05a602d Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:09:50 +0400 Subject: [PATCH 15/22] test: rerun single Chromium validation with longer budget --- .github/workflows/ci.yml | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64d3efb..08bb63b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: SERVER_PID=$! trap 'kill $SERVER_PID || true' EXIT sleep 2 - google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --disable-background-networking --virtual-time-budget=14000 --dump-dom http://127.0.0.1:8765/pr37-qa.html >/tmp/pr37-qa-dom.html 2>/tmp/pr37-chrome.log + google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --disable-background-networking --virtual-time-budget=30000 --dump-dom http://127.0.0.1:8765/pr37-qa.html >/tmp/pr37-qa-dom.html 2>/tmp/pr37-chrome.log node - <<'NODE' const fs=require('fs'); const html=fs.readFileSync('/tmp/pr37-qa-dom.html','utf8'); @@ -45,23 +45,3 @@ jobs: console.log('PR37_VALIDATION_REPORT',JSON.stringify(report)); if(report.failed||report.fatal||report.passed!==9) process.exit(1); NODE - - - name: PR37 browser validation (temporary) - shell: bash - run: | - set -euo pipefail - cp scripts/pr37-qa.html _site/pr37-qa.html - python3 -m http.server 8765 --directory _site >/tmp/pr37-http.log 2>&1 & - SERVER_PID=$! - trap 'kill $SERVER_PID || true' EXIT - sleep 2 - google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --virtual-time-budget=18000 --dump-dom http://127.0.0.1:8765/pr37-qa.html > /tmp/pr37-dom.html 2> /tmp/pr37-chrome.log - python3 - <<'PY' - import re,base64,json,html - text=open('/tmp/pr37-dom.html').read() - m=re.search(r'data-qa-report="([^"]+)"',text) - if not m: raise SystemExit("Browser test did not finish; tail of output: "+text[-1000:]) - report=json.loads(base64.b64decode(html.unescape(m.group(1)))) - print("PR37_QA_REPORT "+json.dumps(report)) - if report.get("failed",1): raise SystemExit("Interactive regression test failure") - PY From 282854ab03cc8996c5d11933a0955f8dd7d0c7dd Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:10:57 +0400 Subject: [PATCH 16/22] ci: restore baseline after exploratory browser tests --- .github/workflows/ci.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08bb63b..c798ad8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,23 +25,3 @@ jobs: - name: Build site with Eleventy run: npm run build - - - name: PR37 interactive browser and image tests (temporary) - shell: bash - run: | - set -euo pipefail - cp scripts/pr37-qa.html _site/pr37-qa.html - python3 -m http.server 8765 --directory _site >/tmp/pr37-server.log 2>&1 & - SERVER_PID=$! - trap 'kill $SERVER_PID || true' EXIT - sleep 2 - google-chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --disable-background-networking --virtual-time-budget=30000 --dump-dom http://127.0.0.1:8765/pr37-qa.html >/tmp/pr37-qa-dom.html 2>/tmp/pr37-chrome.log - node - <<'NODE' - const fs=require('fs'); - const html=fs.readFileSync('/tmp/pr37-qa-dom.html','utf8'); - const m=html.match(/data-qa-report="([^"]+)"/); - if(!m){console.log('CHROMIUM_DOM_TAIL',html.slice(-1000));process.exit(1)} - const report=JSON.parse(Buffer.from(m[1],'base64').toString()); - console.log('PR37_VALIDATION_REPORT',JSON.stringify(report)); - if(report.failed||report.fatal||report.passed!==9) process.exit(1); - NODE From a0e6243c6981069ec91045a527145c245fa99c8f Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:11:00 +0400 Subject: [PATCH 17/22] test: remove temporary PR37 browser harness --- scripts/pr37-qa.html | 145 ------------------------------------------- 1 file changed, 145 deletions(-) delete mode 100644 scripts/pr37-qa.html diff --git a/scripts/pr37-qa.html b/scripts/pr37-qa.html deleted file mode 100644 index aed4c74..0000000 --- a/scripts/pr37-qa.html +++ /dev/null @@ -1,145 +0,0 @@ - - -PR37 temporary browser validation -

PR37 Chromium validation

Running...
- - From f0d0034a3c8a5cc46fb8cfad55f1fc3b946cd151 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:18:01 +0400 Subject: [PATCH 18/22] test: stage temporary Playwright integration regression suite --- scripts/pr37-playwright.cjs | 184 ++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 scripts/pr37-playwright.cjs diff --git a/scripts/pr37-playwright.cjs b/scripts/pr37-playwright.cjs new file mode 100644 index 0000000..fd9cf7c --- /dev/null +++ b/scripts/pr37-playwright.cjs @@ -0,0 +1,184 @@ +'use strict'; +const { chromium } = require('playwright-core'); +const assert = require('node:assert/strict'); +const origin = process.env.PR37_BASE_URL || 'http://127.0.0.1:8765'; +const results = []; +const faults = []; + +async function test(name, run) { + try { + const detail = await run(); + results.push({name, ok:true, detail:detail || 'passed'}); + } catch (err) { + results.push({name, ok:false, detail:err.stack || String(err)}); + } +} +function capture(page, label) { + page.on('pageerror', e => faults.push(label + ': pageerror: ' + e.message)); + page.on('requestfailed', request => faults.push(label + ': request failed: ' + request.url() + ' ' + request.failure()?.errorText)); +} +async function navigate(page, route) { + const response = await page.goto(origin + route, {waitUntil:'load',timeout:20000}); + assert.equal(response?.status(), 200, route + ' HTTP status'); + await page.locator('body').waitFor(); +} +function themeColor(theme) {return theme==='dark' ? '#070d19' : '#eef2f8';} +async function checkTheme(page, theme) { + assert.equal(await page.locator('html').getAttribute('data-theme'),theme); + assert.equal(await page.locator('meta[name="theme-color"]').getAttribute('content'),themeColor(theme)); + assert.equal(await page.locator('.theme-toggle').getAttribute('aria-label'), theme==='dark' ? 'Switch to light theme' : 'Switch to dark theme'); +} +(async () => { + const browser = await chromium.launch({executablePath:'/usr/bin/google-chrome',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); + try { + const desktop = await browser.newContext({viewport:{width:1365,height:900},colorScheme:'dark',reducedMotion:'reduce'}); + const page = await desktop.newPage(); capture(page,'desktop'); + await test('Homepage and initial system dark theme',async()=>{ + await navigate(page,'/'); + assert.equal(await page.locator('html').getAttribute('class'),'js'); + await checkTheme(page,'dark'); + assert.equal(await page.locator('script[src="/assets/js/theme-init.js"]').count(),1); + return 'dark initialized via external JS'; + }); + await test('CSP blocks inline JavaScript (violation event)',async()=>{ + const out=await page.evaluate(async()=>{ + const meta=document.querySelector('meta[http-equiv="Content-Security-Policy"]'); + const policy=meta?.content || ''; + let fired=false; + const promise=new Promise(resolve=>{ + const listener=e=>{if(e.violatedDirective.startsWith('script-src')) {fired=true;resolve(e.violatedDirective);}}; + document.addEventListener('securitypolicyviolation',listener,{once:true}); + setTimeout(()=>resolve(null),2000); + }); + const script=document.createElement('script'); + script.textContent='window.__pr37InjectedScriptExecuted = true;'; + document.body.appendChild(script); + const directive=await promise; + return {policy, directive, executed:!!window.__pr37InjectedScriptExecuted}; + }); + assert.match(out.policy,/script-src 'self'/); + assert.doesNotMatch(out.policy,/script-src[^;]*'unsafe-inline'/); + assert.equal(out.executed,false); + assert.match(out.directive||'',/^script-src/); + return 'inline script blocked; violation '+out.directive; + }); + await test('Theme button changes color, aria-label, and localStorage',async()=>{ + await page.locator('.theme-toggle').click(); + await page.waitForFunction(()=>document.documentElement.dataset.theme==='light'); + await checkTheme(page,'light'); + assert.equal(await page.evaluate(()=>localStorage.getItem('theme')),'light'); + return 'dark → light persisted'; + }); + await test('Theme preference survives a page reload',async()=>{ + await page.reload({waitUntil:'load'}); + await checkTheme(page,'light'); + return 'light persisted across reload'; + }); + await test('Theme can switch back and survive another reload',async()=>{ + await page.locator('.theme-toggle').click(); + await page.waitForFunction(()=>document.documentElement.dataset.theme==='dark'); + await page.reload({waitUntil:'load'}); + await checkTheme(page,'dark'); + assert.equal(await page.evaluate(()=>localStorage.getItem('theme')),'dark'); + return 'light → dark, persisted'; + }); + await test('System light theme is respected with no saved preference',async()=>{ + const ctx=await browser.newContext({viewport:{width:1100,height:800},colorScheme:'light',reducedMotion:'reduce'}); + try{ + const p=await ctx.newPage();capture(p,'system-light'); + await navigate(p,'/'); + await checkTheme(p,'light'); + assert.equal(await p.evaluate(()=>localStorage.getItem('theme')),null); + return 'light system preference detected'; + }finally{await ctx.close();} + }); + await test('Initializer executes before DOMContentLoaded',async()=>{ + const ctx=await browser.newContext({colorScheme:'dark',reducedMotion:'reduce'}); + try{ + const p=await ctx.newPage();capture(p,'head-timing'); + await p.addInitScript(()=>{ + document.addEventListener('DOMContentLoaded',()=>{ + window.__pr37AtDCL={js:document.documentElement.classList.contains('js'),theme:document.documentElement.dataset.theme}; + },{once:true}); + }); + await navigate(p,'/'); + assert.deepEqual(await p.evaluate(()=>window.__pr37AtDCL),{js:true,theme:'dark'}); + return 'initialized by DOMContentLoaded (not direct paint measurement)'; + }finally{await ctx.close();} + }); + await desktop.close(); + + const mobile=await browser.newContext({viewport:{width:390,height:844},colorScheme:'dark',reducedMotion:'no-preference',isMobile:true,hasTouch:true}); + const m=await mobile.newPage();capture(m,'mobile'); + await test('Mobile navigation opens and Escape closes',async()=>{ + await navigate(m,'/'); + const toggle=m.locator('.nav-toggle'); + const links=m.locator('#nav-links'); + await toggle.click(); + assert.equal(await toggle.getAttribute('aria-expanded'),'true'); + assert.match(await links.getAttribute('class')||'',/is-open/); + await m.keyboard.press('Escape'); + assert.equal(await toggle.getAttribute('aria-expanded'),'false'); + assert.doesNotMatch(await links.getAttribute('class')||'',/is-open/); + return 'menu opened and closed'; + }); + await test('Hero canvas initializes and is visible under motion enabled',async()=>{ + const canvas=m.locator('canvas[data-canvas="scope"]'); + assert.equal(await canvas.count(),1); + await canvas.scrollIntoViewIfNeeded(); + await m.waitForFunction(()=>{ + const c=document.querySelector('canvas[data-canvas="scope"]'); + return c && c.width>0 && c.height>0 && c.classList.contains('is-armed') && c.getContext('2d'); + },null,{timeout:6000}); + const v=await canvas.evaluate(c=>({width:c.width,height:c.height,live:c.classList.contains('is-live')})); + assert.ok(v.width>0&&v.height>0); + return JSON.stringify(v); + }); + await test('Project navigation follows a real link',async()=>{ + await m.locator('a[href="/projects/"]').first().click(); + await m.waitForURL('**/projects/'); + assert.equal(await m.locator('main').count(),1); + return 'navigated to /projects/'; + }); + await mobile.close(); + + await test('Built AVIF/WebP images decode across project pages',async()=>{ + const ctx=await browser.newContext({viewport:{width:1280,height:960},reducedMotion:'reduce'}); + try{ + const p=await ctx.newPage();capture(p,'images'); + const routes=['/','/projects/','/projects/speed-reduction-gearbox/','/projects/pipe-inspection-robot/','/projects/soft-robotic-glove/']; + let decoded=0,sourceFormats=new Set(),imageRoutes=0; + for(const route of routes){ + await navigate(p,route); + const count=await p.locator('img[src]').count(); + if(count===0)continue; + imageRoutes++; + const found=await p.evaluate(async()=>{ + const imgs=[...document.querySelectorAll('img[src]')]; + const sources=[...document.querySelectorAll('picture source')].map(x=>x.type); + for(const img of imgs){ + img.loading='eager'; + await img.decode(); + if(!img.naturalWidth || !img.naturalHeight)throw Error('Image missing dimensions: '+img.currentSrc); + } + return {images:imgs.length,sources}; + }); + decoded+=found.images; + for(const s of found.sources)sourceFormats.add(s); + } + assert.ok(imageRoutes>=3,'Too few representative pages with images'); + assert.ok(decoded>=8,'Too few decoded images: '+decoded); + assert.ok(sourceFormats.has('image/avif'),'AVIF responsive source missing'); + assert.ok(sourceFormats.has('image/webp'),'WebP responsive source missing'); + return JSON.stringify({routes:routes.length,imageRoutes,decoded,formats:[...sourceFormats]}); + }finally{await ctx.close();} + }); + await test('Unexpected script/page/network errors',async()=>{ + // The CSP probe deliberately emits a script-src console error, not a pageerror. + assert.deepEqual(faults,[]); + return 'no pageerrors or failed requests'; + }); + }finally{await browser.close();} + console.log('PR37_PLAYWRIGHT_REPORT '+JSON.stringify({passed:results.filter(x=>x.ok).length,failed:results.filter(x=>!x.ok).length,results})); + if(results.some(x=>!x.ok))process.exitCode=1; +})().catch(e=>{console.error('PR37_PLAYWRIGHT_FATAL',e.stack||String(e));process.exitCode=1;}); From 5378852aab84ce142c8f377af9aa1579181f90d9 Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:18:11 +0400 Subject: [PATCH 19/22] ci: temporarily execute Playwright browser checks on PR37 --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c798ad8..227083e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,14 @@ jobs: - name: Build site with Eleventy run: npm run build + + - name: PR37 Playwright regression suite (temporary) + shell: bash + run: | + set -euo pipefail + npm install --no-save --package-lock=false --ignore-scripts playwright-core@1.58.2 + python3 -m http.server 8765 --directory _site >/tmp/pr37-server.log 2>&1 & + SERVER_PID=$! + trap 'kill $SERVER_PID || true' EXIT + sleep 2 + node scripts/pr37-playwright.cjs From 4d4043944602253dd49cabf0e58fe184be2d155f Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:20:17 +0400 Subject: [PATCH 20/22] test: open mobile menu before clicking project navigation link --- scripts/pr37-playwright.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/pr37-playwright.cjs b/scripts/pr37-playwright.cjs index fd9cf7c..a2c77ae 100644 --- a/scripts/pr37-playwright.cjs +++ b/scripts/pr37-playwright.cjs @@ -135,7 +135,8 @@ async function checkTheme(page, theme) { return JSON.stringify(v); }); await test('Project navigation follows a real link',async()=>{ - await m.locator('a[href="/projects/"]').first().click(); + await m.locator('.nav-toggle').click(); + await m.locator('#nav-links a[href="/projects/"]').click(); await m.waitForURL('**/projects/'); assert.equal(await m.locator('main').count(),1); return 'navigated to /projects/'; From 0d24b37afb2b5de0cb1ac99cb17d700149ea365d Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:21:57 +0400 Subject: [PATCH 21/22] ci: restore standard workflow after passing Playwright tests --- .github/workflows/ci.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 227083e..c798ad8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,14 +25,3 @@ jobs: - name: Build site with Eleventy run: npm run build - - - name: PR37 Playwright regression suite (temporary) - shell: bash - run: | - set -euo pipefail - npm install --no-save --package-lock=false --ignore-scripts playwright-core@1.58.2 - python3 -m http.server 8765 --directory _site >/tmp/pr37-server.log 2>&1 & - SERVER_PID=$! - trap 'kill $SERVER_PID || true' EXIT - sleep 2 - node scripts/pr37-playwright.cjs From 09512b665dfbb2a9376abc75c52bbe20c1347bda Mon Sep 17 00:00:00 2001 From: Rashid M <75667958+AstroMyth101@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:21:59 +0400 Subject: [PATCH 22/22] test: remove temporary Playwright regression harness after validation --- scripts/pr37-playwright.cjs | 185 ------------------------------------ 1 file changed, 185 deletions(-) delete mode 100644 scripts/pr37-playwright.cjs diff --git a/scripts/pr37-playwright.cjs b/scripts/pr37-playwright.cjs deleted file mode 100644 index a2c77ae..0000000 --- a/scripts/pr37-playwright.cjs +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; -const { chromium } = require('playwright-core'); -const assert = require('node:assert/strict'); -const origin = process.env.PR37_BASE_URL || 'http://127.0.0.1:8765'; -const results = []; -const faults = []; - -async function test(name, run) { - try { - const detail = await run(); - results.push({name, ok:true, detail:detail || 'passed'}); - } catch (err) { - results.push({name, ok:false, detail:err.stack || String(err)}); - } -} -function capture(page, label) { - page.on('pageerror', e => faults.push(label + ': pageerror: ' + e.message)); - page.on('requestfailed', request => faults.push(label + ': request failed: ' + request.url() + ' ' + request.failure()?.errorText)); -} -async function navigate(page, route) { - const response = await page.goto(origin + route, {waitUntil:'load',timeout:20000}); - assert.equal(response?.status(), 200, route + ' HTTP status'); - await page.locator('body').waitFor(); -} -function themeColor(theme) {return theme==='dark' ? '#070d19' : '#eef2f8';} -async function checkTheme(page, theme) { - assert.equal(await page.locator('html').getAttribute('data-theme'),theme); - assert.equal(await page.locator('meta[name="theme-color"]').getAttribute('content'),themeColor(theme)); - assert.equal(await page.locator('.theme-toggle').getAttribute('aria-label'), theme==='dark' ? 'Switch to light theme' : 'Switch to dark theme'); -} -(async () => { - const browser = await chromium.launch({executablePath:'/usr/bin/google-chrome',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); - try { - const desktop = await browser.newContext({viewport:{width:1365,height:900},colorScheme:'dark',reducedMotion:'reduce'}); - const page = await desktop.newPage(); capture(page,'desktop'); - await test('Homepage and initial system dark theme',async()=>{ - await navigate(page,'/'); - assert.equal(await page.locator('html').getAttribute('class'),'js'); - await checkTheme(page,'dark'); - assert.equal(await page.locator('script[src="/assets/js/theme-init.js"]').count(),1); - return 'dark initialized via external JS'; - }); - await test('CSP blocks inline JavaScript (violation event)',async()=>{ - const out=await page.evaluate(async()=>{ - const meta=document.querySelector('meta[http-equiv="Content-Security-Policy"]'); - const policy=meta?.content || ''; - let fired=false; - const promise=new Promise(resolve=>{ - const listener=e=>{if(e.violatedDirective.startsWith('script-src')) {fired=true;resolve(e.violatedDirective);}}; - document.addEventListener('securitypolicyviolation',listener,{once:true}); - setTimeout(()=>resolve(null),2000); - }); - const script=document.createElement('script'); - script.textContent='window.__pr37InjectedScriptExecuted = true;'; - document.body.appendChild(script); - const directive=await promise; - return {policy, directive, executed:!!window.__pr37InjectedScriptExecuted}; - }); - assert.match(out.policy,/script-src 'self'/); - assert.doesNotMatch(out.policy,/script-src[^;]*'unsafe-inline'/); - assert.equal(out.executed,false); - assert.match(out.directive||'',/^script-src/); - return 'inline script blocked; violation '+out.directive; - }); - await test('Theme button changes color, aria-label, and localStorage',async()=>{ - await page.locator('.theme-toggle').click(); - await page.waitForFunction(()=>document.documentElement.dataset.theme==='light'); - await checkTheme(page,'light'); - assert.equal(await page.evaluate(()=>localStorage.getItem('theme')),'light'); - return 'dark → light persisted'; - }); - await test('Theme preference survives a page reload',async()=>{ - await page.reload({waitUntil:'load'}); - await checkTheme(page,'light'); - return 'light persisted across reload'; - }); - await test('Theme can switch back and survive another reload',async()=>{ - await page.locator('.theme-toggle').click(); - await page.waitForFunction(()=>document.documentElement.dataset.theme==='dark'); - await page.reload({waitUntil:'load'}); - await checkTheme(page,'dark'); - assert.equal(await page.evaluate(()=>localStorage.getItem('theme')),'dark'); - return 'light → dark, persisted'; - }); - await test('System light theme is respected with no saved preference',async()=>{ - const ctx=await browser.newContext({viewport:{width:1100,height:800},colorScheme:'light',reducedMotion:'reduce'}); - try{ - const p=await ctx.newPage();capture(p,'system-light'); - await navigate(p,'/'); - await checkTheme(p,'light'); - assert.equal(await p.evaluate(()=>localStorage.getItem('theme')),null); - return 'light system preference detected'; - }finally{await ctx.close();} - }); - await test('Initializer executes before DOMContentLoaded',async()=>{ - const ctx=await browser.newContext({colorScheme:'dark',reducedMotion:'reduce'}); - try{ - const p=await ctx.newPage();capture(p,'head-timing'); - await p.addInitScript(()=>{ - document.addEventListener('DOMContentLoaded',()=>{ - window.__pr37AtDCL={js:document.documentElement.classList.contains('js'),theme:document.documentElement.dataset.theme}; - },{once:true}); - }); - await navigate(p,'/'); - assert.deepEqual(await p.evaluate(()=>window.__pr37AtDCL),{js:true,theme:'dark'}); - return 'initialized by DOMContentLoaded (not direct paint measurement)'; - }finally{await ctx.close();} - }); - await desktop.close(); - - const mobile=await browser.newContext({viewport:{width:390,height:844},colorScheme:'dark',reducedMotion:'no-preference',isMobile:true,hasTouch:true}); - const m=await mobile.newPage();capture(m,'mobile'); - await test('Mobile navigation opens and Escape closes',async()=>{ - await navigate(m,'/'); - const toggle=m.locator('.nav-toggle'); - const links=m.locator('#nav-links'); - await toggle.click(); - assert.equal(await toggle.getAttribute('aria-expanded'),'true'); - assert.match(await links.getAttribute('class')||'',/is-open/); - await m.keyboard.press('Escape'); - assert.equal(await toggle.getAttribute('aria-expanded'),'false'); - assert.doesNotMatch(await links.getAttribute('class')||'',/is-open/); - return 'menu opened and closed'; - }); - await test('Hero canvas initializes and is visible under motion enabled',async()=>{ - const canvas=m.locator('canvas[data-canvas="scope"]'); - assert.equal(await canvas.count(),1); - await canvas.scrollIntoViewIfNeeded(); - await m.waitForFunction(()=>{ - const c=document.querySelector('canvas[data-canvas="scope"]'); - return c && c.width>0 && c.height>0 && c.classList.contains('is-armed') && c.getContext('2d'); - },null,{timeout:6000}); - const v=await canvas.evaluate(c=>({width:c.width,height:c.height,live:c.classList.contains('is-live')})); - assert.ok(v.width>0&&v.height>0); - return JSON.stringify(v); - }); - await test('Project navigation follows a real link',async()=>{ - await m.locator('.nav-toggle').click(); - await m.locator('#nav-links a[href="/projects/"]').click(); - await m.waitForURL('**/projects/'); - assert.equal(await m.locator('main').count(),1); - return 'navigated to /projects/'; - }); - await mobile.close(); - - await test('Built AVIF/WebP images decode across project pages',async()=>{ - const ctx=await browser.newContext({viewport:{width:1280,height:960},reducedMotion:'reduce'}); - try{ - const p=await ctx.newPage();capture(p,'images'); - const routes=['/','/projects/','/projects/speed-reduction-gearbox/','/projects/pipe-inspection-robot/','/projects/soft-robotic-glove/']; - let decoded=0,sourceFormats=new Set(),imageRoutes=0; - for(const route of routes){ - await navigate(p,route); - const count=await p.locator('img[src]').count(); - if(count===0)continue; - imageRoutes++; - const found=await p.evaluate(async()=>{ - const imgs=[...document.querySelectorAll('img[src]')]; - const sources=[...document.querySelectorAll('picture source')].map(x=>x.type); - for(const img of imgs){ - img.loading='eager'; - await img.decode(); - if(!img.naturalWidth || !img.naturalHeight)throw Error('Image missing dimensions: '+img.currentSrc); - } - return {images:imgs.length,sources}; - }); - decoded+=found.images; - for(const s of found.sources)sourceFormats.add(s); - } - assert.ok(imageRoutes>=3,'Too few representative pages with images'); - assert.ok(decoded>=8,'Too few decoded images: '+decoded); - assert.ok(sourceFormats.has('image/avif'),'AVIF responsive source missing'); - assert.ok(sourceFormats.has('image/webp'),'WebP responsive source missing'); - return JSON.stringify({routes:routes.length,imageRoutes,decoded,formats:[...sourceFormats]}); - }finally{await ctx.close();} - }); - await test('Unexpected script/page/network errors',async()=>{ - // The CSP probe deliberately emits a script-src console error, not a pageerror. - assert.deepEqual(faults,[]); - return 'no pageerrors or failed requests'; - }); - }finally{await browser.close();} - console.log('PR37_PLAYWRIGHT_REPORT '+JSON.stringify({passed:results.filter(x=>x.ok).length,failed:results.filter(x=>!x.ok).length,results})); - if(results.some(x=>!x.ok))process.exitCode=1; -})().catch(e=>{console.error('PR37_PLAYWRIGHT_FATAL',e.stack||String(e));process.exitCode=1;});