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;});