# Review package: bf7271a..0f9aab0 ## Commits 0f9aab0 PDF: script di generazione Chromium (Turso+R2) per Action e seed ## Files changed .gitignore | 3 + app/package-lock.json | 1062 +++++++++++++++++++++++++++++++++++++------ app/package.json | 5 +- app/scripts/pdf/data.ts | 195 ++++++++ app/scripts/pdf/generate.ts | 226 +++++++++ app/scripts/pdf/template.ts | 878 +++++++++++++++++++++++++++++++++++ 6 files changed, 2239 insertions(+), 130 deletions(-) ## Diff diff --git a/.gitignore b/.gitignore index 353d0cc..ab1f313 100644 --- a/.gitignore +++ b/.gitignore @@ -47,10 +47,13 @@ Thumbs.db .idea/ .vscode/ *.swp *.swo *~ # ===== Log / temp ===== *.log .tmp/ tmp/ + +# ===== PDF generati localmente (non vanno in repo) ===== +pdf-output/ diff --git a/app/package-lock.json b/app/package-lock.json index d81ca5a..3426263 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -14,24 +14,26 @@ "drizzle-orm": "^0.36.0", "jose": "^5.9.6" }, "devDependencies": { "@sveltejs/adapter-vercel": "^5.5.0", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/vite": "^4.0.0-beta.5", "@types/bcryptjs": "^2.4.6", "drizzle-kit": "^0.28.0", + "puppeteer": "^25.2.1", "svelte": "^5.1.0", "svelte-check": "^4.0.0", "tailwindcss": "^4.0.0-beta.5", "tslib": "^2.8.0", + "tsx": "^4.22.4", "typescript": "^5.6.0", "vite": "^6.0.0", "vitest": "^4.1.9" } }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", @@ -2067,20 +2069,45 @@ "node": ">=14" } }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz", + "integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" @@ -3949,20 +3976,95 @@ "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, @@ -4075,20 +4177,28 @@ "node": ">=8" } }, "node_modules/devalue": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.0.tgz", "integrity": "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==", "dev": true, "license": "MIT" }, + "node_modules/devtools-protocol": { + "version": "0.0.1638949", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", + "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/drizzle-kit": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.28.1.tgz", "integrity": "sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==", "dev": true, "license": "MIT", "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", @@ -4737,20 +4847,30 @@ "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.6.tgz", @@ -4907,20 +5027,43 @@ "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, "funding": { @@ -5340,20 +5483,33 @@ "win32" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5403,20 +5559,37 @@ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, "engines": { "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, @@ -5640,20 +5813,60 @@ "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/promise-limit": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", "license": "ISC" }, + "node_modules/puppeteer": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.1.tgz", + "integrity": "sha512-2D5RMkQH9FRhDU57a1/jV9xWoxqZvUjaZOYjAAPdRCEY8A01V5sxzyGOMs8XiKU9fPF91SOSwNYpHRu5SD958g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.2.1", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.2.1.tgz", + "integrity": "sha512-MwEZ4FFGJ1ZLOmu/04eISxoEMKtCnHyJBRFfgpwPPSYNG6gT6Xw1laNziFSV7uwDcx3jK+ATYIo9SfOd8Uhc3w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" }, "funding": { @@ -6137,166 +6350,677 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "~0.28.0" }, "bin": { - "vite": "bin/vite.js" + "tsx": "dist/cli.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "node": ">=18.0.0" }, "optionalDependencies": { "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } } }, - "node_modules/vitefu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" + "optional": true, + "os": [ + "aix" ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } + "engines": { + "node": ">=18" } }, - "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { "vitest": "vitest.mjs" }, "engines": { "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", @@ -6353,20 +7077,27 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -6503,49 +7234,122 @@ "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { "optional": true }, "utf-8-validate": { "optional": true } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true, "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/app/package.json b/app/package.json index 521fe5f..bf38aab 100644 --- a/app/package.json +++ b/app/package.json @@ -4,35 +4,38 @@ "private": true, "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", - "test": "vitest run" + "test": "vitest run", + "pdf:gen": "tsx scripts/pdf/generate.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@libsql/client": "^0.14.0", "bcryptjs": "^2.4.3", "drizzle-orm": "^0.36.0", "jose": "^5.9.6" }, "devDependencies": { "@sveltejs/adapter-vercel": "^5.5.0", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/vite": "^4.0.0-beta.5", "@types/bcryptjs": "^2.4.6", "drizzle-kit": "^0.28.0", + "puppeteer": "^25.2.1", "svelte": "^5.1.0", "svelte-check": "^4.0.0", "tailwindcss": "^4.0.0-beta.5", "tslib": "^2.8.0", + "tsx": "^4.22.4", "typescript": "^5.6.0", "vite": "^6.0.0", "vitest": "^4.1.9" } } diff --git a/app/scripts/pdf/data.ts b/app/scripts/pdf/data.ts new file mode 100644 index 0000000..fe23afa --- /dev/null +++ b/app/scripts/pdf/data.ts @@ -0,0 +1,195 @@ +// Carica tutti i dati DVR di un'agenzia da Turso (@libsql/client, async). +// Porting di pdf-prototype/load.js con: +// - query tradotte in async/await +// - resolveDvrAgenziaId per la gestione gemelle (dvr_condiviso_da_id) +// - campo dataUri?: string su ogni allegato (precaricato da generate.ts) + +import type { Client } from '@libsql/client'; + +export type AllegatoRow = { + id: number; + agenzia_id: number; + tipo: string; + storage_path: string; + excluded: number; + dataUri?: string; // base64 data URI precaricata da generate.ts (R2 o filesystem locale) +}; + +// Ritorna l'agenzia_id cui appartiene il DVR (se gemella, usa quella "master"; altrimenti sé stessa) +async function resolveDvrAgenziaId(client: Client, agenziaId: number): Promise { + const r = await client.execute({ + sql: 'SELECT dvr_condiviso_da_id AS shared FROM agenzie WHERE id = ?', + args: [agenziaId] + }); + const shared = r.rows[0]?.shared as number | null; + return shared ?? agenziaId; +} + +function rows(result: Awaited>): Record[] { + return result.rows as unknown as Record[]; +} + +export async function loadDvrData(client: Client, agenziaId: number) { + const dvrAgenziaId = await resolveDvrAgenziaId(client, agenziaId); + + // Anagrafica sempre dalla propria sede (non dal DVR condiviso) + const agenziaResult = await client.execute({ + sql: ` + SELECT a.*, rg.nome AS regione_geografica, ra.codice AS regione_alleanza, + zs.codice AS zona_sismica, zs.descrizione AS zona_sismica_descr, + rf.codice AS rischio_frane, rf.descrizione AS rischio_frane_descr, + ral.codice AS rischio_alluvioni, ral.descrizione AS rischio_alluvioni_descr, + imp.nome AS impresa_sopralluogo + FROM agenzie a + LEFT JOIN lookup_regioni_geografiche rg ON rg.id = a.regione_geografica_id + LEFT JOIN lookup_regioni_alleanza ra ON ra.id = a.regione_alleanza_id + LEFT JOIN lookup_zone_sismiche zs ON zs.id = a.zona_sismica_id + LEFT JOIN lookup_gradi_rischio_idro rf ON rf.id = a.rischio_frane_id + LEFT JOIN lookup_gradi_rischio_idro ral ON ral.id = a.rischio_alluvioni_id + LEFT JOIN lookup_imprese_sopralluogo imp ON imp.id = a.impresa_sopralluogo_id + WHERE a.id = ? + `, + args: [agenziaId] + }); + const agenziaRows = rows(agenziaResult); + if (!agenziaRows.length) throw new Error(`Agenzia ${agenziaId} non trovata`); + const agenzia = agenziaRows[0]; + + // DVR dalla sede master (dvrAgenziaId, può essere diversa per gemelle) + const dvrResult = await client.execute({ + sql: 'SELECT * FROM dvr WHERE agenzia_id = ?', + args: [dvrAgenziaId] + }); + const dvrRows = rows(dvrResult); + if (!dvrRows.length) throw new Error(`DVR mancante per agenzia ${dvrAgenziaId}`); + const dvr = dvrRows[0]; + + const settingsResult = await client.execute({ + sql: 'SELECT * FROM app_settings WHERE id = 1', + args: [] + }); + const settings = rows(settingsResult)[0] ?? {}; + + const pericoli = rows(await client.execute({ + sql: ` + SELECT p.id, p.punto_id, p.col_c_override, p.col_d, p.col_e, p.col_f_override, + pt.rif, pt.capitolo, pt.livello, pt.requisito, pt.ordine, + pt.codice_attivita, + lc.testo AS col_c_testo, lf.testo AS col_f_testo + FROM dvr_pericoli p + JOIN lookup_pericoli_punti pt ON pt.id = p.punto_id + LEFT JOIN lookup_pericoli_combo lc ON lc.id = p.col_c_lookup_id + LEFT JOIN lookup_pericoli_combo lf ON lf.id = p.col_f_lookup_id + WHERE p.dvr_id = ? + ORDER BY pt.ordine + `, + args: [dvr.id as number] + })); + + // Righe-cornice (capitolo/sotto-capitolo) per i titoli sezione pericoli + const puntiAlberatura = rows(await client.execute({ + sql: 'SELECT id, rif, capitolo, livello, requisito, ordine FROM lookup_pericoli_punti ORDER BY ordine', + args: [] + })); + + const valutazioni = rows(await client.execute({ + sql: ` + SELECT v.*, e.nome AS elemento_nome, e.capitolo_num, e.capitolo_nome, e.ordine AS elem_ordine, + COALESCE(lm.testo_pulito, lm.testo) AS misure_prev_testo + FROM dvr_valutazione v + JOIN lookup_valutazione_elementi e ON e.id = v.elemento_id + LEFT JOIN lookup_valutazione_misure lm ON lm.id = v.misure_prevenzione_lookup_id + WHERE v.dvr_id = ? + ORDER BY e.ordine + `, + args: [dvr.id as number] + })); + + const valutazioniMisure = rows(await client.execute({ + sql: ` + SELECT vm.*, + COALESCE(lm.testo_pulito, lm.testo) AS testo_pulito, + lm.p_score, lm.g_score, lm.r_score, + lm.titolo_gruppo, lm.codice_attivita + FROM dvr_valutazione_misure vm + LEFT JOIN lookup_valutazione_misure lm ON lm.id = vm.lookup_id + WHERE vm.valutazione_id IN (SELECT id FROM dvr_valutazione WHERE dvr_id = ?) + ORDER BY vm.valutazione_id, vm.ordine + `, + args: [dvr.id as number] + })); + + const incendioDoc = rows(await client.execute({ + sql: ` + SELECT did.id, did.documento_id, did.stato_override, did.note, + d.nome AS documento_nome, d.parent_id, d.ordine, + s.nome AS stato_nome + FROM dvr_incendio_doc did + JOIN lookup_documenti_incendio d ON d.id = did.documento_id + LEFT JOIN lookup_stati_documento_incendio s ON s.id = did.stato_lookup_id + WHERE did.dvr_id = ? + ORDER BY d.ordine + `, + args: [dvr.id as number] + })); + + const attivita = rows(await client.execute({ + sql: ` + SELECT am.*, f.nome_generico AS famiglia_nome, f.codice AS famiglia_codice, + CASE am.source_type + WHEN 'pericolo' THEN (SELECT pt.requisito FROM dvr_pericoli p JOIN lookup_pericoli_punti pt ON pt.id=p.punto_id WHERE p.id=am.source_id) + WHEN 'valutazione' THEN ( + SELECT COALESCE(lm.titolo_gruppo, e.nome) + FROM dvr_valutazione_misure vm + LEFT JOIN lookup_valutazione_misure lm ON lm.id = vm.lookup_id + JOIN dvr_valutazione v ON v.id = vm.valutazione_id + JOIN lookup_valutazione_elementi e ON e.id = v.elemento_id + WHERE vm.id = am.source_id + ) + WHEN 'piano_storico' THEN ( + SELECT COALESCE(la.attivita, pm.attivita_override) + FROM dvr_piano_miglioramento pm + LEFT JOIN lookup_azioni_piano_miglioramento la ON la.id = pm.attivita_lookup_id + WHERE pm.id = am.source_id + ) + END AS source_label, + va.attivita AS variante_scelta_testo, + (SELECT lm.p_score FROM dvr_valutazione_misure vm + LEFT JOIN lookup_valutazione_misure lm ON lm.id = vm.lookup_id + WHERE vm.id = am.source_id AND am.source_type = 'valutazione') AS src_p, + (SELECT lm.g_score FROM dvr_valutazione_misure vm + LEFT JOIN lookup_valutazione_misure lm ON lm.id = vm.lookup_id + WHERE vm.id = am.source_id AND am.source_type = 'valutazione') AS src_g, + (SELECT p.col_d FROM dvr_pericoli p WHERE p.id = am.source_id AND am.source_type = 'pericolo') AS src_col_d, + (SELECT p.col_e FROM dvr_pericoli p WHERE p.id = am.source_id AND am.source_type = 'pericolo') AS src_col_e + FROM dvr_attivita_miglioramento am + JOIN lookup_attivita_famiglie f ON f.id = am.famiglia_id + LEFT JOIN lookup_azioni_piano_miglioramento va ON va.id = am.variante_scelta_id + WHERE am.dvr_id = ? AND am.stato = 'aperta' + `, + args: [dvr.id as number] + })); + + // Allegati sempre dalla propria sede (non dal DVR condiviso — ogni sede ha i suoi allegati) + const allegatiRaw = rows(await client.execute({ + sql: ` + SELECT * FROM agenzie_allegati WHERE agenzia_id = ? AND excluded = 0 + ORDER BY + CASE tipo + WHEN 'foto_immobile' THEN 1 + WHEN 'planimetria_emergenza' THEN 2 + WHEN 'planimetria_layout' THEN 3 + WHEN 'luogo_sicuro_esterno' THEN 4 + ELSE 5 + END, id + `, + args: [agenziaId] + })); + // Inizializza dataUri a undefined — generate.ts la precarica prima del render + const allegati: AllegatoRow[] = allegatiRaw.map((a) => ({ ...a, dataUri: undefined } as AllegatoRow)); + + return { + agenzia, dvr, settings, pericoli, puntiAlberatura, + valutazioni, valutazioniMisure, incendioDoc, attivita, allegati + }; +} diff --git a/app/scripts/pdf/generate.ts b/app/scripts/pdf/generate.ts new file mode 100644 index 0000000..39867ee --- /dev/null +++ b/app/scripts/pdf/generate.ts @@ -0,0 +1,226 @@ +// Script di generazione PDF DVR. +// Gira via `npx tsx scripts/pdf/generate.ts ` fuori da SvelteKit +// (in locale e su GitHub Actions). Non importato da nessuna route SvelteKit. +// +// Modalità immagini (dual-mode): +// - Se R2_ACCOUNT_ID/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_BUCKET sono configurati +// → scarica gli allegati da R2 e carica il PDF su R2. +// - Altrimenti (sviluppo locale senza credenziali R2) +// → legge gli allegati da filesystem locale (storage/) +// → scrive il PDF in locale invece di caricarlo su R2 +// → aggiorna comunque dvr_pdf nel DB locale (stato='ready'). + +import { createClient, type Client } from '@libsql/client'; +import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; +import { readFileSync, existsSync, writeFileSync } from 'node:fs'; +import { extname, resolve } from 'node:path'; +import puppeteer from 'puppeteer'; +import { renderHtml } from './template.js'; +import { loadFingerprintInput, hashFingerprintInput } from '../../src/lib/server/pdf/fingerprint.js'; +import { loadDvrData } from './data.js'; + +// ── Repo root (usato per costruire i path locali degli allegati e del logo) +const REPO_ROOT = resolve(import.meta.dirname, '../../..'); + +// ── Env vars ──────────────────────────────────────────────── +const DB_URL = process.env.DATABASE_URL; +const DB_TOKEN = process.env.DATABASE_AUTH_TOKEN; + +if (!DB_URL) { + console.error('DATABASE_URL non impostato'); + process.exit(1); +} + +// ── R2 (opzionale — se assente, modalità locale) +const R2_CONFIGURED = + !!process.env.R2_ACCOUNT_ID && + !!process.env.R2_ACCESS_KEY_ID && + !!process.env.R2_SECRET_ACCESS_KEY && + !!process.env.R2_BUCKET; + +const r2 = R2_CONFIGURED + ? new S3Client({ + region: 'auto', + endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: process.env.R2_ACCESS_KEY_ID!, + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY! + } + }) + : null; +const BUCKET = process.env.R2_BUCKET; + +// ── Helper: scarica da R2 e restituisce data URI base64 ───── +async function r2GetBase64(key: string): Promise { + if (!r2 || !BUCKET) return null; + try { + const o = await r2.send(new GetObjectCommand({ Bucket: BUCKET, Key: key })); + const buf = Buffer.from(await (o.Body as any).transformToByteArray()); + const ext = extname(key).slice(1).toLowerCase(); + const mime = ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' : 'image/jpeg'; + return `data:${mime};base64,${buf.toString('base64')}`; + } catch (e) { + console.warn(`[r2] Impossibile scaricare ${key}: ${e}`); + return null; + } +} + +// ── Helper: legge un file locale e restituisce data URI base64 ── +function localFileToDataUri(storagePath: string): string | null { + const absPath = resolve(REPO_ROOT, storagePath); + if (!existsSync(absPath)) { + console.warn(`[local] File non trovato: ${absPath}`); + return null; + } + const ext = extname(absPath).slice(1).toLowerCase(); + const mime = ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' : 'image/jpeg'; + return `data:${mime};base64,${readFileSync(absPath).toString('base64')}`; +} + +// ── Helper: carica logo Alleanza ──────────────────────────── +function loadLogoUri(): string { + const logoPath = resolve(REPO_ROOT, 'app/static/logo-alleanza.png'); + if (!existsSync(logoPath)) return ''; + return `data:image/png;base64,${readFileSync(logoPath).toString('base64')}`; +} + +// ── Generazione di un singolo DVR ────────────────────────── +async function generateOne(client: Client, agenziaId: number, logoUri: string): Promise { + const data = await loadDvrData(client, agenziaId); + + // Precarica immagini allegati (R2 se configurato, filesystem locale altrimenti) + for (const allegato of data.allegati) { + if (R2_CONFIGURED) { + // storage_path = "storage/agenzie//filename" → R2 key = "agenzie//filename" + const key = allegato.storage_path.replace(/^storage\//, ''); + allegato.dataUri = (await r2GetBase64(key)) ?? undefined; + } else { + allegato.dataUri = localFileToDataUri(allegato.storage_path) ?? undefined; + } + } + + // Aggiungi logo al payload dati + const renderData = { ...data, logoUri }; + const html = renderHtml(renderData); + + const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + try { + const page = await browser.newPage(); + await page.emulateMediaType('print'); + await page.setContent(html, { waitUntil: 'load' }); + const pdf = await page.pdf({ format: 'A4', printBackground: true, preferCSSPageSize: true }); + + const codice = data.agenzia.codice_fisico as string; + const r2Key = `pdf/${codice}_DVR.pdf`; + + if (R2_CONFIGURED && r2 && BUCKET) { + // Carica su R2 + await r2.send(new PutObjectCommand({ + Bucket: BUCKET, + Key: r2Key, + Body: pdf, + ContentType: 'application/pdf' + })); + console.log(`OK ${agenziaId} → R2:${r2Key}`); + } else { + // Modalità locale: scrivi il PDF in scratchpad (fuori dal repo) + const outPath = resolve(REPO_ROOT, `pdf-output/${codice}_DVR.pdf`); + // Crea la dir se non esiste + const { mkdirSync } = await import('node:fs'); + mkdirSync(resolve(REPO_ROOT, 'pdf-output'), { recursive: true }); + writeFileSync(outPath, pdf); + console.log(`OK ${agenziaId} → locale:${outPath} (${pdf.length} byte)`); + } + + // Calcola e salva fingerprint (data_hash) — indipendente dalla modalità + const fpInput = await loadFingerprintInput(client, agenziaId); + const hash = hashFingerprintInput(fpInput); + const now = new Date().toISOString(); + + await client.execute({ + sql: ` + INSERT INTO dvr_pdf(agenzia_id, r2_key, data_hash, stato, generato_il, error_msg, updated_at) + VALUES (?,?,?,'ready',?,NULL,?) + ON CONFLICT(agenzia_id) DO UPDATE SET + r2_key = excluded.r2_key, + data_hash = excluded.data_hash, + stato = 'ready', + generato_il = excluded.generato_il, + error_msg = NULL, + updated_at = excluded.updated_at + `, + args: [agenziaId, r2Key, hash, now, now] + }); + } finally { + await browser.close(); + } +} + +// ── Gestione errore singola agenzia ──────────────────────── +async function markError(client: Client, agenziaId: number, err: unknown): Promise { + const now = new Date().toISOString(); + await client.execute({ + sql: ` + INSERT INTO dvr_pdf(agenzia_id, stato, error_msg, updated_at) + VALUES (?, 'error', ?, ?) + ON CONFLICT(agenzia_id) DO UPDATE SET + stato = 'error', + error_msg = excluded.error_msg, + updated_at = excluded.updated_at + `, + args: [agenziaId, String(err), now] + }); +} + +// ── Main ──────────────────────────────────────────────────── +async function main() { + const arg = process.argv[2]; + if (!arg) { + console.error('Uso: npx tsx scripts/pdf/generate.ts '); + process.exit(1); + } + + const client = createClient({ url: DB_URL!, authToken: DB_TOKEN }); + const logoUri = loadLogoUri(); + + if (!R2_CONFIGURED) { + console.log('[generate] R2 non configurato → modalità locale (PDF in pdf-output/, dvr_pdf aggiornato nel DB locale)'); + } + + if (arg === 'all') { + const result = await client.execute({ + sql: 'SELECT id FROM agenzie WHERE deleted_at IS NULL ORDER BY id', + args: [] + }); + let n = 0; + const total = result.rows.length; + for (const row of result.rows) { + const id = row.id as number; + try { + await generateOne(client, id, logoUri); + } catch (e) { + await markError(client, id, e); + console.error(`ERR ${id}: ${e}`); + } + console.log(`-- ${++n}/${total}`); + } + } else { + const id = Number(arg); + if (Number.isNaN(id)) { + console.error(`Argomento non valido: "${arg}". Passa un numero o "all".`); + process.exit(1); + } + try { + await generateOne(client, id, logoUri); + } catch (e) { + await markError(client, id, e); + console.error(`ERR ${id}: ${e}`); + process.exit(1); + } + } +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/app/scripts/pdf/template.ts b/app/scripts/pdf/template.ts new file mode 100644 index 0000000..7677c3e --- /dev/null +++ b/app/scripts/pdf/template.ts @@ -0,0 +1,878 @@ +// Porting di pdf-prototype/template.js → TypeScript per lo script di generazione. +// Unica modifica funzionale: le immagini vengono ricevute già come dataUri +// (precaricate da generate.ts da R2 o da filesystem locale), non lette da disco +// qui dentro. Layout, CSS, sezioni e matrice sono identici al prototipo approvato. + +// ── helpers ─────────────────────────────────────────────── +const esc = (s: unknown) => (s == null ? '' : String(s) + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, ''')); +const nl2br = (s: unknown) => esc(s).replace(/\n/g, '
'); + +const fmtDate = (iso: unknown) => { + if (!iso) return '—'; + const d = new Date(iso as string); + if (Number.isNaN(d.getTime())) return String(iso); + return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' }); +}; + +// Stato U/P → label+colore +const STATO_UP: Record = { + S: { label: 'Conforme', color: '#16a34a', textOn: '#fff' }, + X: { label: 'Da verif. (Alleanza)', color: '#f97316', textOn: '#fff' }, + X_COND: { label: 'Da verif. (Condominio)', color: '#facc15', textOn: '#1f1f1f' }, + N: { label: 'Non conforme', color: '#dc2626', textOn: '#fff' }, + '-': { label: 'N/A', color: '#e2e8f0', textOn: '#475569' } +}; +const renderBadgeUP = (v: string) => { + const s = STATO_UP[v]; + if (!s) return '—'; + return `${esc(v === 'X_COND' ? 'X*' : v)}`; +}; + +// ── MATRICE DEL RISCHIO (da mail cliente 2026-05-25) ── +// MATRIX[P][D] → R (0..4). P=0..4, D=1..4. +const MATRIX: Record> = { + 4: { 1: 2, 2: 3, 3: 4, 4: 4 }, + 3: { 1: 2, 2: 2, 3: 3, 4: 4 }, + 2: { 1: 1, 2: 2, 3: 3, 4: 4 }, + 1: { 1: 1, 2: 1, 3: 2, 4: 3 }, + 0: { 1: 0, 2: 0, 3: 0, 4: 0 } +}; +const rDaMatrice = (p: unknown, d: unknown): number | null => { + if (p == null || d == null) return null; + const row = MATRIX[Number(p)]; if (!row) return null; + const v = row[Number(d)]; return v == null ? null : v; +}; + +// Descrizioni livelli (dalla mail cliente, identiche fra varianti di colore) +const LIVELLO_LABELS = [ + { r: 0, key: 'NON_APPLICABILE', label: 'Non Applicabile', desc: '' }, + { r: 1, key: 'TRASCURABILE', label: 'Trascurabile', desc: 'Possono non essere necessarie ulteriori azioni di riduzione del rischio. I potenziali pericoli sono pienamente sotto controllo.' }, + { r: 2, key: 'LIEVE', label: 'Lieve', desc: 'Le azioni attuabili di riduzione vanno realizzate nel medio/lungo termine; nel frattempo verificare che i pericoli potenziali non subiscano peggioramenti.' }, + { r: 3, key: 'ALTO', label: 'Alto', desc: 'Le azioni attuabili di riduzione vanno realizzate nel breve/medio termine. Al termine degli interventi verificare l’efficacia in termini di abbattimento del rischio.' }, + { r: 4, key: 'MOLTO_ALTO', label: 'Molto alto', desc: 'Urgono interventi immediati di messa in sicurezza, di riduzione della probabilità e/o della gravità. Al termine, verificare tempestivamente l’efficacia in termini di abbattimento del rischio.' } +]; + +// Palette "loud" — colori puri richiesti dal cliente via mail +const PALETTE_LOUD: Record = { + 0: { color: '#F2F2F2', text: '#475569' }, + 1: { color: '#00FF00', text: '#0a3d0a' }, + 2: { color: '#FFFF00', text: '#3a2f00' }, + 3: { color: '#FFC000', text: '#3a1d00' }, + 4: { color: '#FF0000', text: '#ffffff' } +}; +// Palette "soft" — coerente con i badge U/P della §2 Pericoli (S/X_COND/X/N) +const PALETTE_SOFT: Record = { + 0: { color: '#e2e8f0', text: '#475569' }, + 1: { color: '#16a34a', text: '#ffffff' }, + 2: { color: '#facc15', text: '#1f1f1f' }, + 3: { color: '#f97316', text: '#ffffff' }, + 4: { color: '#dc2626', text: '#ffffff' } +}; + +const STYLE_MODE = (process.env.STYLE || 'loud').toLowerCase(); +const PALETTE = STYLE_MODE === 'soft' ? PALETTE_SOFT : PALETTE_LOUD; +const LIVELLI = LIVELLO_LABELS.map((l) => ({ ...l, ...PALETTE[l.r] })); +const livelloByR = (r: number | null) => (r == null ? null : LIVELLI.find((l) => l.r === r)); + +const rBadgeStyle = (r: number | null) => { + const l = livelloByR(r); + return l ? `background:${l.color};color:${l.text}` : 'background:#e2e8f0;color:#475569'; +}; +const rLabel = (r: number | null) => { + const l = livelloByR(r); + return l ? l.label : ''; +}; +const renderLivelloPill = (r: number | null) => { + const l = livelloByR(r); + if (!l) return ''; + return `${esc(l.label)}`; +}; + +// Pill "P=x · D=y · R=z" mostrata accanto al livello per spiegare il calcolo +const pdrPill = (p: unknown, d: unknown, r: unknown) => { + if (p == null && d == null) return ''; + return `P=${p ?? '—'} · D=${d ?? '—'} · R=${r ?? '—'}`; +}; +const upPill = (col_d: unknown, col_e: unknown, r: unknown) => { + const fmt = (v: unknown) => v === 'X_COND' ? 'X*' : (v || '—'); + return `U=${esc(fmt(col_d))} · P=${esc(fmt(col_e))} · R=${r ?? '—'}`; +}; +const breakdownPillForAttivita = (a: any, r: number | null) => { + if (a.source_type === 'valutazione') return pdrPill(a.src_p, a.src_g, r); + if (a.source_type === 'pericolo') return upPill(a.src_col_d, a.src_col_e, r); + return ''; +}; + +// Pericolo U/P (S/X/X_COND/N) → R con la nuova scala +const rDaUP = (col_d: unknown, col_e: unknown) => { + const score = (v: unknown) => v === 'N' ? 4 : (v === 'X' || v === 'X_COND') ? 3 : 0; + return Math.max(score(col_d), score(col_e)); +}; +// Piano storico (giorni rimanenti rispetto a oggi) → R +const rDaScadenza = (scadenzaIso: unknown) => { + if (!scadenzaIso) return 3; // fallback + const giorni = Math.round((new Date(scadenzaIso as string).getTime() - Date.now()) / 86400000); + if (giorni > 365) return 2; // LIEVE + if (giorni >= 180) return 3; // ALTO + return 4; // MOLTO_ALTO +}; + +// R "effettivo" di un'attività dalla nuova matrice/regole (ignora il vecchio r_score salvato) +const rDaAttivita = (a: any): number | null => { + if (a.source_type === 'valutazione') return rDaMatrice(a.src_p, a.src_g); + if (a.source_type === 'pericolo') return rDaUP(a.src_col_d, a.src_col_e); + if (a.source_type === 'piano_storico') return rDaScadenza(a.data_scadenza); + return null; +}; + +// Logo Alleanza: caricato da static/ come dataUri al momento della build del template +// Qui viene passato come parte dei dati (logoUri) per non dipendere dal filesystem. +// Se non disponibile mostra solo il testo del brand. + +// ── sezioni ─────────────────────────────────────────────── + +function renderCopertina(d: any) { + const logo = d.logoUri || ''; + return ` +
+
+ ${logo ? `` : ''} +
Alleanza Assicurazioni S.p.A.
+
+ +
+
D.Lgs. 9 aprile 2008, n. 81
+

Documento di Valutazione
dei Rischi

+
DVR di sede — Rev ${String(d.dvr.rev_corrente).padStart(2, '0')}
+
+ +
+
${esc(d.agenzia.nome_agenzia)}
+
${esc(d.agenzia.nome_sede)} — ${esc(d.agenzia.tipologia)}
+
${esc(d.agenzia.indirizzo)}, ${esc(d.agenzia.cap || '')} ${esc(d.agenzia.citta)} (${esc(d.agenzia.provincia)})
+
Codice fisico ${esc(d.agenzia.codice_fisico)}${d.agenzia.codice_immobile ? ` · Codice immobile ${esc(d.agenzia.codice_immobile)}` : ''}
+
+ +
+
+
Datore di Lavoro
+
${esc(d.settings.datore_lavoro_nome || '—')}
+
${esc(d.settings.datore_lavoro_qualifica || '')}
+
+
+
+
Responsabile S.P.P.
+
${esc(d.settings.rspp_nome || '—')}
+
${esc(d.settings.rspp_qualifica || '')}
+
+
+
+ + +
`; +} + +function renderInquadramento(d: any) { + // dataUri già precaricata in generate.ts; fallback a placeholder se assente + const fotoImmobile = d.allegati.find((a: any) => a.tipo === 'foto_immobile'); + const fotoUri = fotoImmobile?.dataUri || ''; + return ` +
+

1 · Inquadramento della sede

+ +
+
+

Dati anagrafici

+ + + ${d.agenzia.codice_immobile ? `` : ''} + + + + + + + + + +
Codice fisico${esc(d.agenzia.codice_fisico)}
Codice immobile${esc(d.agenzia.codice_immobile)}
Tipologia${esc(d.agenzia.tipologia)}
Nome agenzia${esc(d.agenzia.nome_agenzia)}
Nome sede${esc(d.agenzia.nome_sede)}
Indirizzo${esc(d.agenzia.indirizzo)}
CAP · Città (Prov)${esc(d.agenzia.cap || '—')} · ${esc(d.agenzia.citta)} (${esc(d.agenzia.provincia)})
Regione${esc(d.agenzia.regione_geografica || '—')} ${d.agenzia.regione_alleanza ? `· Alleanza ${esc(d.agenzia.regione_alleanza)}` : ''}
Area${esc(d.agenzia.area_macro || '—')}
Piano${esc(d.agenzia.piano || '—')}
Superficie${d.agenzia.mq ? esc(d.agenzia.mq) + ' m²' : '—'}
+
+ +
+ ${fotoUri ? `Foto immobile` : `
Foto immobile non disponibile
`} +
+
+ + ${d.dvr.descrizione_immobile ? ` +
+

Descrizione dell'immobile

+

${nl2br(d.dvr.descrizione_immobile)}

+
` : ''} + +

Rischi territoriali

+
+
+
Sismico
+
${d.agenzia.zona_sismica ? `Zona ${esc(d.agenzia.zona_sismica)}` : 'Non specificato'}
+ ${d.dvr.rischio_sismico_descr ? `

${nl2br(d.dvr.rischio_sismico_descr)}

` : ''} +
+
+
Idrogeologico
+
${d.dvr.rischio_idro_si_no === 1 ? 'Presente' : d.dvr.rischio_idro_si_no === 0 ? 'Non presente' : 'Non specificato'}
+ ${d.agenzia.rischio_frane ? `
Frane: ${esc(d.agenzia.rischio_frane)}
` : ''} + ${d.agenzia.rischio_alluvioni ? `
Alluvioni: ${esc(d.agenzia.rischio_alluvioni)}
` : ''} + ${d.dvr.rischio_idro_descr ? `

${nl2br(d.dvr.rischio_idro_descr)}

` : ''} +
+
+
Ambientale
+
${d.dvr.rischio_ambientale_si_no === 1 ? 'Presente' : d.dvr.rischio_ambientale_si_no === 0 ? 'Non presente' : 'Non specificato'}
+ ${d.dvr.rischio_ambientale_descr ? `

${nl2br(d.dvr.rischio_ambientale_descr)}

` : ''} +
+
+ + ${d.dvr.piano_emergenze_testo ? ` +
+

Piano di gestione delle emergenze

+

${nl2br(d.dvr.piano_emergenze_testo)}

+
` : ''} +
`; +} + +function renderPericoli(d: any) { + // Raggruppa per capitolo (primi due livelli del rif) + const groups = new Map(); + d.pericoli.forEach((p: any) => { + const cap = p.capitolo || (p.rif || '').split('.').slice(0, 2).join('.'); + if (!groups.has(cap)) groups.set(cap, []); + groups.get(cap)!.push(p); + }); + + // Titoli capitolo dai punti livello 1/2 + const titoliCapitolo = new Map(); + d.puntiAlberatura.forEach((pt: any) => { + if (pt.livello <= 2) titoliCapitolo.set(pt.rif, pt.requisito); + }); + + let html = `
+

2 · Identificazione dei pericoli

+

Verifica dei requisiti minimi di sicurezza secondo Allegato IV D.Lgs. 81/08. Lo stato è indicato distintamente per gli ambienti Uffici (U) e Pertinenze (P).

+
+ ${Object.entries(STATO_UP).map(([k, v]) => `${esc(k === 'X_COND' ? 'X*' : k)} ${esc(v.label)}`).join('')} +
`; + + for (const [cap, items] of groups) { + const titolo = titoliCapitolo.get(cap) || `Capitolo ${cap}`; + html += ` +

${esc(cap)} — ${esc(titolo)}

+
+ ${items.map((p: any) => { + const verifica = p.col_c_override || p.col_c_testo || ''; + const note = p.col_f_override || p.col_f_testo || ''; + return ` +
+
+ ${esc(p.rif)} + ${esc(p.requisito)} +
+
+
+
Verifica
+
${esc(verifica) || '—'}
+
+
+
Uffici
+
${renderBadgeUP(p.col_d)}
+
+
+
Pertinenze
+
${renderBadgeUP(p.col_e)}
+
+
+ ${note ? ` +
+ Note + ${esc(note)} +
` : ''} +
`; + }).join('')} +
`; + } + + html += `
`; + return html; +} + +function renderValutazione(d: any) { + // raggruppa valutazioni per capitolo + const groups = new Map(); + d.valutazioni.forEach((v: any) => { + const cap = v.capitolo_num != null ? v.capitolo_num : 0; + if (!groups.has(cap)) groups.set(cap, { titolo: v.capitolo_nome || '', items: [] }); + groups.get(cap)!.items.push(v); + }); + + // raggruppa sub-misure per valutazione_id e per titolo_gruppo + const misurePerVal = new Map(); + d.valutazioniMisure.forEach((vm: any) => { + if (!misurePerVal.has(vm.valutazione_id)) misurePerVal.set(vm.valutazione_id, []); + misurePerVal.get(vm.valutazione_id)!.push(vm); + }); + + let html = `
+

3 · Valutazione dei rischi

+ ${renderIntroValutazione()} +

Esiti per elemento

`; + + for (const [cap, group] of [...groups.entries()].sort((a, b) => Number(a[0]) - Number(b[0]))) { + html += `

${cap ? esc(cap) + '. ' : ''}${esc(group.titolo)}

`; + for (const v of group.items) { + const subs = misurePerVal.get(v.id) || []; + // raggruppa per titolo_gruppo + const subGroups = new Map(); + subs.forEach((s: any) => { + const k = s.titolo_gruppo || ''; + if (!subGroups.has(k)) subGroups.set(k, []); + subGroups.get(k)!.push(s); + }); + + const vR = rDaMatrice(v.p, v.g); + html += ` +
+
+
${esc(v.elemento_nome)}
+ ${vR != null && vR > 0 ? `
+ ${pdrPill(v.p, v.g, vR)} +
${esc(rLabel(vR))}
+
` : ''} +
+
+ ${v.figure_esposte ? `
Figure esposte: ${esc(v.figure_esposte)}
` : ''} + ${v.rischi_descr ? `
Rischi: ${esc(v.rischi_descr)}
` : ''} +
+ +
+
+
Misure di prevenzione
+
${nl2br(v.misure_prevenzione_override || v.misure_prev_testo || '—')}
+
+ + ${subs.length === 0 ? '' : ` +
+
Misure di miglioramento
+ ${[...subGroups.entries()].map(([gname, gItems]) => ` +
+ ${gname ? `
${esc(gname)}
` : ''} +
    + ${gItems.map((s: any) => { + const r = rDaMatrice(s.p_score, s.g_score); + return `
  • + ${esc(s.testo_override || s.testo_pulito || '')} + ${r != null && r > 0 ? ` + ${pdrPill(s.p_score, s.g_score, r)} + ${esc(rLabel(r))} + ` : ''} +
  • `; + }).join('')} +
+
+ `).join('')} +
`} +
+
`; + } + } + html += `
`; + return html; +} + +function renderIncendio(d: any) { + // crea albero parent/figli + const byId = new Map(d.incendioDoc.map((x: any) => [x.documento_id, { ...x, children: [] as any[] }])); + const roots: any[] = []; + byId.forEach((node: any) => { + if (node.parent_id && byId.has(node.parent_id)) (byId.get(node.parent_id) as any).children.push(node); + else roots.push(node); + }); + roots.sort((a, b) => a.ordine - b.ordine); + + const renderNode = (n: any, depth = 0): string => ` + + ${' '.repeat(depth * 4)}${esc(n.documento_nome)} + ${esc(n.stato_override || n.stato_nome || '—')} + + ${n.children.sort((a: any, b: any) => a.ordine - b.ordine).map((c: any) => renderNode(c, depth + 1)).join('')} + `; + + return `
+

4 · Rischio incendio

+ ${d.dvr.rischio_incendio_testo ? `

${nl2br(d.dvr.rischio_incendio_testo)}

` : ''} + +

Documenti di conformità — DM 37/2008 e succ.

+ + + + + + ${roots.map((r: any) => renderNode(r, 0)).join('')} + +
DocumentoStato
+
`; +} + +// Tabelle introduttive §3 — dalla mail cliente 2026-05-25 +const TAB_D = [ + { v: 1, criteri: 'Bassa o trascurabile', descrizione: 'Non si rende necessaria alcuna cura particolare e il danno alla salute è completamente reversibile in breve tempo.' }, + { v: 2, criteri: 'Lieve', descrizione: 'Lo stato di salute può comportare una breve convalescenza ma non produce alcuna lesione permanente. / Si sono verificati danni che hanno prodotto inabilità temporanea con prognosi inferiore a 3 giorni. / Sono presenti agenti biologici del gruppo 1, sostanze e/o preparati tossici per ingestione, nocivi per inalazione e/o contatto cutaneo o irritanti.' }, + { v: 3, criteri: 'Media', descrizione: 'Si rende necessaria una lunga convalescenza ma non si producono lesioni permanenti. / Si sono verificati danni che hanno prodotto inabilità temporanea con prognosi inferiore a 40 giorni. / Sono presenti sostanze e/o preparati cancerogeni, molto tossici per ingestione e/o contatto cutaneo, infiammabili, comburenti, agenti biologici del gruppo 2.' }, + { v: 4, criteri: 'Alta', descrizione: 'Sono possibili lesioni permanenti non trascurabili o il decesso. / Si sono verificati danni che hanno prodotto inabilità temporanea con prima prognosi maggiore di 40 giorni di guarigione. / Nell’ambito dell’attività è stata individuata una tipologia di incidente che può causare danni gravi a cose e/all’ambiente.' } +]; +const TAB_P = [ + { v: 0, criteri: 'Inesistente', descrizione: 'Il fattore di pericolo non è presente nell’ambito lavorativo dell’azienda e conseguentemente la probabilità è assolutamente nulla.' }, + { v: 1, criteri: 'Bassa o trascurabile', descrizione: 'È improbabile che possano determinarsi circostanze tali da determinare un danno / Il manifestarsi di un danno è da considerarsi possibile ma la frequenza di esposizione al pericolo è bassa (poche volte l’anno).' }, + { v: 2, criteri: 'Lieve', descrizione: 'È possibile che possano determinarsi circostanze tali da determinare un danno. / Il manifestarsi di un danno è da considerarsi probabile ma la frequenza di esposizione al pericolo è molto bassa (poche volte l’anno e di breve durata).' }, + { v: 3, criteri: 'Media', descrizione: 'È probabile che possano determinarsi circostanze tali da determinare un danno. / Il manifestarsi di un danno è da considerarsi atteso ma la frequenza di esposizione al pericolo è del tutto occasionale (meno di una volta i 3 anni).' }, + { v: 4, criteri: 'Alta', descrizione: 'Il manifestarsi di un danno è da considerarsi atteso e l’esposizione al pericolo non è trascurabile.' } +]; + +function renderTabellaScala(numero: number, titolo: string, indice: string, righe: { v: number; criteri: string; descrizione: string }[]) { + return ` +
+
Tabella ${numero} — Scala dell'indice "${indice}" · ${esc(titolo)}
+ + + + ${righe.map((r) => ` + + + + + `).join('')} + +
ValoreCriteriDescrizione
${r.v}${esc(r.criteri)}${esc(r.descrizione)}
+
`; +} + +function renderMatriceRischio() { + // P righe (4..0), D colonne (1..4) + const Ps = [4, 3, 2, 1, 0]; + const Ds = [1, 2, 3, 4]; + return ` +
+
Tabella 3 — Matrice dei valori del rischio R
+ + + + + + + + + ${Ds.map((d) => ``).join('')} + + + + ${Ps.map((p, i) => { + const cells = p === 0 + ? `` + : Ds.map((d) => { + const r = rDaMatrice(p, d); + return ``; + }).join(''); + const pAxis = i === 0 ? `` : ''; + return `${pAxis}${cells}`; + }).join('')} + +
D
${d}
N.A.${r}P
${p}
+
`; +} + +function renderTabellaLivelliR() { + return ` +
+
Tabella 4 — Valori del rischio R · Criteri e azioni da intraprendere
+ + + + + + + + + + ${LIVELLI.map((l) => ` + + + + + `).join('')} + +
ValoreCriteriDescrizione
${l.r}${esc(l.label)}${esc(l.desc || '')}
+
`; +} + +function renderIntroValutazione() { + return ` +

Ai sensi del D.Lgs. 81/2008 s.m.i., il datore di lavoro è tenuto ad analizzare e valutare tutti i rischi normalmente ricorrenti per i propri lavoratori in relazione alla tipologia delle lavorazioni svolte. In questa sezione vengono riportati i risultati della valutazione dei rischi presenti nell'attività in oggetto, individuati per gli ambiti Uffici e Pertinenze e altre destinazioni, considerando il lavoratore con accesso a entrambi.

+

Si riporta, per facilità di lettura, il metodo di valutazione utilizzato, già illustrato nel DVR master.

+ + ${renderTabellaScala(1, 'Entità del danno potenziale', 'D', TAB_D)} + ${renderTabellaScala(2, 'Probabilità — frequenza eventi', 'P', TAB_P)} + ${renderMatriceRischio()} + ${renderTabellaLivelliR()}`; +} + +function renderAttivita(d: any) { + // ricalcolo R con la nuova matrice e ordino per R desc, poi per scadenza asc + const attiv = d.attivita + .map((a: any) => ({ ...a, r_new: rDaAttivita(a) })) + .sort((a: any, b: any) => { + const dr = (b.r_new || 0) - (a.r_new || 0); + if (dr !== 0) return dr; + const da = a.data_scadenza ? new Date(a.data_scadenza).getTime() : Infinity; + const db = b.data_scadenza ? new Date(b.data_scadenza).getTime() : Infinity; + return da - db; + }); + + if (!attiv.length) return `
+

5 · Piano di miglioramento — Attività aperte

+ ${renderTabellaLivelliR()} +

Nessuna attività aperta.

+
`; + + return `
+

5 · Piano di miglioramento

+

Sono elencate le ${attiv.length} attività aperte, ordinate per livello di rischio decrescente. Le attività risolte sono archiviate nello storico e non vengono riprodotte in questo documento.

+ + ${renderTabellaLivelliR()} + +

Attività aperte

+ + + + + + + + + + + ${attiv.map((a: any) => ` + + + + + + + `).join('')} + +
LivelloFamigliaRiferimentoScadenza
+
+ ${renderLivelloPill(a.r_new)} + ${breakdownPillForAttivita(a, a.r_new)} +
+
${esc(a.famiglia_nome)} +
${esc(a.source_label || '—')}
+ ${a.variante_scelta_testo ? `
↳ ${esc(a.variante_scelta_testo)}
` : ''} +
${fmtDate(a.data_scadenza)}
+ +
+
Responsabile del piano di miglioramento
+
Operation di rete — Alleanza Assicurazioni
+
+
`; +} + +function renderAllegati(d: any) { + const TIPI: Record = { + foto_immobile: 'Foto dell\'immobile', + planimetria_emergenza: 'Planimetria di emergenza', + planimetria_layout: 'Planimetria di layout', + luogo_sicuro_esterno: 'Indicazione luogo sicuro esterno', + altro: 'Allegato' + }; + + if (!d.allegati.length) return ''; + return `
+

6 · Allegati

+ ${d.allegati.map((a: any) => { + // dataUri già precaricata in generate.ts + if (!a.dataUri) return ''; + return ` +
+
+
${esc(TIPI[a.tipo] || a.tipo)}
+
+ ${esc(a.tipo)} +
`; + }).join('')} +
`; +} + +// ── style ───────────────────────────────────────────────── +const STYLE = ` +:root{ + --brand:#003060; --brand-50:#e8eef4; --brand-100:#c5d5e3; --brand-300:#5d83a8; + --ink:#1b2433; --muted:#64748b; --line:#e2e8f0; --line-strong:#cbd5e1; + --bg-card:#ffffff; +} +*{box-sizing:border-box;margin:0;padding:0} +html,body{ + font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',Roboto,Arial,sans-serif; + font-size:9.5pt; line-height:1.45; color:var(--ink); -webkit-print-color-adjust:exact; print-color-adjust:exact; +} +@page{ + size:A4; margin:18mm 14mm 16mm 14mm; + @top-left{ content:""; } + @top-right{ content:""; } + @bottom-center{ content:counter(page) " / " counter(pages); font-size:8pt; color:var(--muted); } +} +@page :first{ + margin:0; @top-left{content:""} @top-right{content:""} @bottom-center{content:""} +} + +/* ── COVER ── */ +.cover{ + page:cover; height:297mm; width:210mm; padding:24mm 22mm; + background:linear-gradient(180deg, var(--brand-50) 0%, #ffffff 60%); + display:flex; flex-direction:column; gap:14mm; +} +.cover-top{display:flex; justify-content:space-between; align-items:center;} +.cover-logo{height:18mm; object-fit:contain} +.cover-eyebrow{font-size:8.5pt; color:var(--brand); letter-spacing:.06em; text-transform:uppercase; font-weight:600} +.cover-main{margin-top:6mm} +.cover-tag{display:inline-block; background:var(--brand); color:#fff; padding:3px 10px; border-radius:3px; font-size:8.5pt; letter-spacing:.05em; text-transform:uppercase; font-weight:600} +.cover-title{font-size:34pt; font-weight:800; color:var(--brand); margin-top:6mm; line-height:1.05; letter-spacing:-.01em} +.cover-subtitle{margin-top:4mm; font-size:13pt; color:var(--brand-300); font-weight:600} + +.cover-sede{ + background:var(--brand); color:#fff; padding:8mm 9mm; border-radius:2mm; + display:flex; flex-direction:column; gap:1mm; +} +.cover-sede-name{font-size:22pt; font-weight:700; letter-spacing:-.01em} +.cover-sede-sub{font-size:11pt; opacity:.9} +.cover-sede-addr{font-size:10pt; opacity:.9; margin-top:1mm} +.cover-sede-codice{font-size:9pt; opacity:.85; margin-top:2mm; padding-top:2mm; border-top:1px solid rgba(255,255,255,.2)} + +.cover-firme{display:grid; grid-template-columns:1fr 1fr; gap:8mm} +.cover-firma{border:1px solid var(--line-strong); padding:5mm 6mm; border-radius:2mm; min-height:32mm; background:#fff} +.cover-firma-label{font-size:8pt; text-transform:uppercase; letter-spacing:.06em; color:var(--brand); font-weight:600} +.cover-firma-value{font-size:13pt; font-weight:600; margin-top:2mm; color:var(--ink)} +.cover-firma-sub{font-size:9pt; color:var(--muted); margin-top:1mm} +.cover-firma-sign{margin-top:8mm; border-top:1px dashed var(--line-strong); padding-top:2mm; font-size:7.5pt; color:var(--muted); content:"Firma"} +.cover-firma-sign::before{content:"Firma"} + +.cover-footer{margin-top:auto; display:flex; justify-content:space-between; font-size:8.5pt; color:var(--muted); border-top:2px solid var(--brand); padding-top:3mm} + +/* ── PAGE / SECTIONS ── */ +.page{padding-top:2mm} +.page-break{page-break-before:always} +.sec-num{ + font-size:16pt; color:var(--brand); font-weight:800; + padding-bottom:2mm; border-bottom:2px solid var(--brand); margin-bottom:4mm; + letter-spacing:-.01em; +} +.sec-intro{color:var(--muted); margin-bottom:3mm; max-width:170mm; font-size:9pt} +.cap-title{ + font-size:11pt; color:var(--brand); font-weight:700; margin-top:4mm; margin-bottom:1.5mm; + padding-left:3mm; border-left:3px solid var(--brand); break-after:avoid; +} +.muted{color:var(--muted)} +.mt-12{margin-top:4mm}.mt-16{margin-top:5mm} + +/* ── CARDS / KV ── */ +.card{ background:var(--bg-card); border:1px solid var(--line); border-radius:2mm; padding:3mm 4mm } +.card-title{font-size:9pt; color:var(--brand); font-weight:700; text-transform:uppercase; letter-spacing:.05em; margin-bottom:1.5mm} +.kv{width:100%; border-collapse:collapse} +.kv th, .kv td{ text-align:left; vertical-align:top; padding:.9mm 0; font-size:8.6pt; border-bottom:1px dotted var(--line) } +.kv th{ width:38%; color:var(--muted); font-weight:500 } +.kv tr:last-child th, .kv tr:last-child td{ border-bottom:none } +.grid-2{display:grid; grid-template-columns:1fr 1fr; gap:3mm} +.grid-3{display:grid; grid-template-columns:1fr 1fr 1fr; gap:3mm} +.prose{font-size:9pt; line-height:1.45; color:var(--ink); white-space:normal} + +.foto-immobile{ width:100%; height:auto; max-height:60mm; object-fit:cover; border-radius:1.5mm; display:block } +.foto-placeholder{ height:50mm; display:flex; align-items:center; justify-content:center; color:var(--muted); background:var(--brand-50); border-radius:1.5mm; font-size:9pt } +.foto-caption{font-size:7.5pt; color:var(--muted); margin-top:1.5mm; text-align:center} + +.risk-card{ background:#fff; border:1px solid var(--line); border-radius:2mm; padding:3mm; text-align:center } +.risk-title{font-size:7.5pt; text-transform:uppercase; letter-spacing:.06em; color:var(--brand); font-weight:600} +.risk-value{font-size:10pt; font-weight:600; margin-top:.5mm} +.risk-sub{font-size:8pt; color:var(--muted); margin-top:.5mm} +.risk-descr{font-size:8pt; color:var(--ink); margin-top:1.5mm; text-align:left; line-height:1.4} + +/* ── BADGES ── */ +.badge-up{ + display:inline-block; min-width:5mm; padding:.5mm 1.5mm; border-radius:1mm; + font-size:8pt; font-weight:700; text-align:center; line-height:1.2; +} +.badge-up.empty{color:var(--muted); background:transparent} +.pill{ display:inline-block; padding:.6mm 2mm; border-radius:10mm; font-size:7.8pt; font-weight:600; color:#fff; letter-spacing:.02em } + +.legend{ display:flex; gap:4mm; flex-wrap:wrap; padding:2mm 3mm; background:var(--brand-50); border-radius:1.5mm; margin-bottom:3mm; font-size:8pt } +.legend-item{ display:inline-flex; align-items:center; gap:1mm; color:var(--ink) } + +/* ── TABLES ── */ +.tbl-incendio, .tbl-attivita{ width:100%; border-collapse:collapse; font-size:8.4pt; margin-top:1mm } +.tbl-incendio thead th, .tbl-attivita thead th{ + background:var(--brand); color:#fff; padding:1.6mm 2mm; text-align:left; font-weight:600; + font-size:8pt; text-transform:uppercase; letter-spacing:.04em; +} +.tbl-incendio tbody tr, .tbl-attivita tbody tr{ break-inside:avoid } +.tbl-incendio tbody td, .tbl-attivita tbody td{ padding:1.4mm 2mm; border-bottom:1px solid var(--line); vertical-align:top } +.tbl-incendio tbody tr:nth-child(even), .tbl-attivita tbody tr:nth-child(even){ background:#f8fafc } + +/* ── PERICOLI CARDS ── */ +.peri-list{ display:flex; flex-direction:column; gap:1.5mm; margin-top:1mm } +.peri-card{ + background:#fff; border:1px solid var(--line); border-radius:1.2mm; + padding:1.8mm 2.5mm; break-inside:avoid; +} +.peri-card:nth-child(even){ background:#f5f8fc } +.peri-head{ + display:flex; gap:2.5mm; align-items:baseline; + padding-bottom:1mm; border-bottom:1px dotted var(--line); margin-bottom:1.2mm; +} +.peri-rif{ + font-family:'SF Mono',Menlo,Consolas,monospace; font-size:7.5pt; color:var(--brand); + font-weight:700; padding:.4mm 1.5mm; background:var(--brand-50); border-radius:.8mm; + flex-shrink:0; +} +.peri-req{ font-size:9pt; font-weight:500; color:var(--ink); line-height:1.35 } +.peri-fields{ display:grid; grid-template-columns:1fr 28mm 28mm; gap:3mm; align-items:start } +.peri-field-label{ font-size:6.8pt; color:var(--muted); text-transform:uppercase; letter-spacing:.06em; font-weight:600; margin-bottom:.4mm } +.peri-field-value{ font-size:8.5pt; line-height:1.35; color:var(--ink) } +.peri-field-verifica .peri-field-value{ color:var(--ink) } +.peri-field-up{ text-align:center } +.peri-field-up .peri-field-label{ text-align:center } +.peri-note{ + margin-top:1.2mm; padding-top:1mm; border-top:1px dotted var(--line); + display:flex; gap:2mm; align-items:baseline; +} +.peri-note-label{ + font-size:6.8pt; color:var(--brand); font-weight:700; + text-transform:uppercase; letter-spacing:.06em; flex-shrink:0; +} +.peri-note-value{ font-size:8.4pt; color:var(--ink); line-height:1.4 } +.col-note{ color:var(--muted) } + +.col-doc{ width:90mm } +.col-stato{ width:30mm } +.tbl-incendio-2col .col-doc{ width:auto } +.tbl-incendio-2col .col-stato{ width:50mm } +.tbl-incendio tr.depth-1 .col-doc{ color:var(--muted); font-size:8pt } + +.col-livello{ width:26mm; text-align:center } +.col-famiglia{ width:42mm; font-weight:500 } +.col-descr{ } +.col-data{ width:24mm; white-space:nowrap; font-variant-numeric:tabular-nums } + +/* ── VALUTAZIONE CARDS ── */ +.val-card{ + background:#fff; border:1px solid var(--line); border-radius:1.5mm; padding:3mm 4mm; margin-bottom:3mm; + break-inside:avoid; +} +.val-card-head{ display:flex; justify-content:space-between; align-items:baseline; gap:4mm; border-bottom:1px solid var(--line); padding-bottom:1.5mm; margin-bottom:1.5mm } +.val-card-title{ font-size:10pt; font-weight:700; color:var(--brand) } +.val-card-r{ font-size:8.5pt; padding:.6mm 2mm; border-radius:1mm; font-weight:600; white-space:nowrap; flex-shrink:0 } +.val-card-r-group{ display:flex; gap:1.5mm; align-items:center; flex-shrink:0 } +.val-misura-r-group{ display:inline-flex; gap:1.5mm; align-items:center; flex-shrink:0 } + +.pdr-pill{ + display:inline-block; font-size:7.2pt; font-weight:600; + padding:.4mm 1.6mm; border-radius:1mm; + background:var(--brand-50); color:var(--brand); + white-space:nowrap; flex-shrink:0; + font-variant-numeric:tabular-nums; letter-spacing:.02em; +} + +.livello-stack{ display:flex; flex-direction:column; align-items:center; gap:.8mm } +.val-card-meta{ font-size:8.5pt; color:var(--ink); display:flex; flex-direction:column; gap:.5mm; margin-bottom:2mm } +.val-card-meta .lbl{ color:var(--muted); font-weight:600 } +.val-card-body{ display:grid; grid-template-columns:1fr; gap:2mm } +.val-section-title{ font-size:8pt; text-transform:uppercase; letter-spacing:.05em; color:var(--brand); font-weight:600; margin-bottom:1mm } +.val-section-text{ font-size:9pt; line-height:1.45 } +.val-subgroup{ margin-bottom:1.5mm } +.val-subgroup-name{ font-size:8.5pt; font-weight:600; color:var(--ink); margin-bottom:.8mm } +.val-misure-list{ list-style:none; padding-left:0 } +.val-misure-list li{ + font-size:8.8pt; padding:1mm 0 1mm 5mm; position:relative; border-bottom:1px dotted var(--line); + display:flex; justify-content:space-between; gap:2mm; align-items:start; +} +.val-misure-list li::before{ content:"›"; position:absolute; left:1mm; color:var(--brand); font-weight:700 } +.val-misure-list li:last-child{ border-bottom:none; padding-bottom:0 } +.val-subgroup:last-child{ margin-bottom:0 } +.val-misura-text{ flex:1 } +.val-misura-r{ font-size:7.5pt; padding:.3mm 1.5mm; border-radius:1mm; font-weight:600; white-space:nowrap } + +/* ── TABELLE METODO RISCHIO (intro §3 + duplicato §5) ── */ +.rsk-tab{ margin-top:3mm; break-inside:avoid } +.rsk-tab-title{ font-size:8.5pt; color:var(--brand); margin-bottom:1.2mm; line-height:1.35 } +.rsk-tab-num{ display:inline-block; background:var(--brand); color:#fff; padding:.2mm 1.5mm; border-radius:.6mm; font-weight:700; font-size:7.2pt; text-transform:uppercase; letter-spacing:.05em; margin-right:1.5mm } +.rsk-tab-axis-label{ text-align:center; font-size:7.5pt; color:var(--muted); margin-top:1mm; font-weight:600 } + +.tbl-scala, .tbl-livelli{ width:100%; border-collapse:collapse; font-size:8.2pt; border:1px solid var(--line); border-radius:1.5mm } +.tbl-scala thead th, .tbl-livelli thead th{ background:var(--brand-50); color:var(--brand); padding:1.2mm 2mm; text-align:left; font-weight:700; font-size:7.2pt; text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--line-strong) } +.tbl-scala tbody td, .tbl-livelli tbody td{ padding:1.4mm 2mm; border-bottom:1px solid var(--line); vertical-align:top; line-height:1.4 } +.tbl-scala tbody tr:last-child td, .tbl-livelli tbody tr:last-child td{ border-bottom:none } +.sc-v{ width:14mm; text-align:center } +.sc-badge{ display:inline-block; min-width:6mm; padding:.4mm 1.8mm; background:var(--brand); color:#fff; font-weight:700; border-radius:.8mm; font-size:8.2pt } +.sc-c{ width:34mm; font-weight:600; color:var(--ink) } +.sc-d{ font-size:8pt } +.lvl-v{ width:14mm } +.lvl-c{ width:34mm; font-weight:600 } +.lvl-d{ font-size:8pt } + +/* matrice */ +.tbl-matrice{ + border-collapse:collapse; font-size:9pt; margin:0 auto; +} +.tbl-matrice th, .tbl-matrice td{ + width:14mm; height:9mm; text-align:center; vertical-align:middle; + border:1px solid var(--line-strong); padding:0 1mm; font-weight:700; +} +.tbl-matrice .mtx-p-axis, +.tbl-matrice .mtx-d-axis{ + background:var(--brand-50); color:var(--brand); font-size:10.5pt; font-weight:800; + text-transform:uppercase; letter-spacing:.04em; +} +.tbl-matrice .mtx-p-axis{ width:8mm } +.tbl-matrice .mtx-p, +.tbl-matrice .mtx-d{ background:var(--brand-50); color:var(--brand); font-size:9.5pt; font-weight:700 } +.tbl-matrice .mtx-p{ width:9mm } +.tbl-matrice .mtx-cell{ font-size:10pt; font-weight:800; font-style:italic } +.tbl-matrice .mtx-na{ font-size:9pt; font-weight:700 } +.tbl-matrice .mtx-empty{ border:none; background:transparent } + +/* ── RESPONSABILE ── */ +.resp-box{ background:var(--brand-50); border-left:4px solid var(--brand); padding:3mm 5mm; border-radius:1.5mm } +.resp-label{ font-size:7.5pt; color:var(--brand); text-transform:uppercase; letter-spacing:.06em; font-weight:600 } +.resp-value{ font-size:10pt; font-weight:600; margin-top:.5mm } + +/* ── ALLEGATI ── */ +.allegato-block{ page-break-inside:avoid; margin-bottom:8mm } +.allegato-head{ border-bottom:2px solid var(--brand); padding-bottom:1.5mm; margin-bottom:3mm; display:flex; justify-content:space-between; align-items:baseline } +.allegato-title{ font-size:11pt; font-weight:700; color:var(--brand) } +.allegato-sub{ font-size:8pt; color:var(--muted) } +.allegato-img{ width:100%; height:auto; max-height:230mm; object-fit:contain; display:block } +`; + +export function renderHtml(data: any): string { + const d = data; + return ` +DVR ${esc(d.agenzia.nome_agenzia)} — ${esc(d.agenzia.nome_sede)} + + +${renderCopertina(d)} +${renderInquadramento(d)} +${renderPericoli(d)} +${renderValutazione(d)} +${renderIncendio(d)} +${renderAttivita(d)} +${renderAllegati(d)} +`; +}