diff --git a/.gitignore b/.gitignore index 1242aef..76b402f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ __pycache__/ *.py[cod] +node_modules/ +dist/ agent/.venv/ agent/build/ +web-client/dist/ web-server/uploads/ diff --git a/agent/scripts/build-deb.sh b/agent/scripts/build-deb.sh index 5000911..b0df65c 100644 --- a/agent/scripts/build-deb.sh +++ b/agent/scripts/build-deb.sh @@ -1,49 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -PKG_NAME="local-installer-agent" +VERSION="${VERSION:-0.1.0}" ARCH="${ARCH:-amd64}" -PUBLISH_DIR="${AGENT_PUBLISH_DIR:-../web-server/uploads/packages/agent}" - -if [ -z "${BUILD_ROOT:-}" ]; then - if [[ "$(pwd -P)" == /mnt/* ]]; then - BUILD_ROOT="/tmp/${PKG_NAME}-build" - else - BUILD_ROOT="build" - fi -fi - -next_patch_version() { - local latest="" - - if [ -d "${PUBLISH_DIR}" ]; then - latest="$( - for package_path in "${PUBLISH_DIR}/${PKG_NAME}_"*"_${ARCH}.deb"; do - [ -e "${package_path}" ] || continue - package_file="$(basename "${package_path}")" - package_version="${package_file#${PKG_NAME}_}" - package_version="${package_version%_${ARCH}.deb}" - printf '%s\n' "${package_version}" - done | sort -V | tail -n 1 - )" - fi - - if [ -z "${latest}" ]; then - latest="$( - sed -nE 's/^Version:[[:space:]]*([^[:space:]]+).*/\1/p' packaging/DEBIAN/control | - head -n 1 - )" - fi - - if [[ "${latest}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - printf '%s.%s.%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$((BASH_REMATCH[3] + 1))" - return - fi - - printf '0.1.0\n' -} - -VERSION="${VERSION:-$(next_patch_version)}" +PKG_NAME="local-installer-agent" +BUILD_ROOT="build" BUILD_DIR="${BUILD_ROOT}/${PKG_NAME}_${VERSION}_${ARCH}" rm -rf "${BUILD_ROOT}" @@ -56,11 +17,6 @@ mkdir -p "${BUILD_DIR}/DEBIAN" cp -r app "${BUILD_DIR}/opt/local-installer-agent/" cp requirements.txt "${BUILD_DIR}/opt/local-installer-agent/" -find "${BUILD_DIR}/opt/local-installer-agent/app" -type d -name "__pycache__" -prune -exec rm -rf {} + -find "${BUILD_DIR}/opt/local-installer-agent/app" -type f \( -name "*.pyc" -o -name "*.pyo" \) -delete -find "${BUILD_DIR}/opt/local-installer-agent" -type d -exec chmod 755 {} + -find "${BUILD_DIR}/opt/local-installer-agent" -type f -exec chmod 644 {} + - cp packaging/systemd/local-installer-agent.service \ "${BUILD_DIR}/etc/systemd/system/local-installer-agent.service" @@ -72,11 +28,6 @@ cp packaging/DEBIAN/postrm "${BUILD_DIR}/DEBIAN/postrm" chmod 755 "${BUILD_DIR}/DEBIAN/postinst" chmod 755 "${BUILD_DIR}/DEBIAN/prerm" chmod 755 "${BUILD_DIR}/DEBIAN/postrm" -chmod 644 "${BUILD_DIR}/DEBIAN/control" -chmod 644 "${BUILD_DIR}/etc/systemd/system/local-installer-agent.service" - -sed -i -E "s/^Version:.*/Version: ${VERSION}/" "${BUILD_DIR}/DEBIAN/control" -sed -i -E "s/^Architecture:.*/Architecture: ${ARCH}/" "${BUILD_DIR}/DEBIAN/control" cat > "${BUILD_DIR}/etc/local-installer-agent/agent.env" < PACKAGE_PROXY_TARGET. +VITE_PACKAGE_BASE_URL= +VITE_AGENT_BASE_URL=http://127.0.0.1:5010 +PACKAGE_PROXY_TARGET=http://localhost:3000 diff --git a/web-client/README.md b/web-client/README.md new file mode 100644 index 0000000..5d14935 --- /dev/null +++ b/web-client/README.md @@ -0,0 +1,41 @@ +# Robot Installer Web Client + +Web Client public cho user cài, cập nhật và gỡ app thông qua Local Installer Agent. + +## Chạy local + +```bash +npm install +npm run dev +``` + +Mặc định khi chạy dev, client gọi package server qua Vite proxy: + +```text +robot.package API: http://localhost:5173/api -> http://localhost:3000/api +Local Agent: http://127.0.0.1:5010 +``` + +Có thể đổi trong UI hoặc qua `.env`: + +```env +VITE_PACKAGE_BASE_URL= +VITE_AGENT_BASE_URL=http://127.0.0.1:5010 +PACKAGE_PROXY_TARGET=http://localhost:3000 +``` + +Khi deploy `robot.installer` thật, đặt `VITE_PACKAGE_BASE_URL=https://robot.package` để browser gọi thẳng package server. + +## Test thật + +1. Chạy `web-server` tại `http://localhost:3000`. +2. Chạy hoặc cài Local Installer Agent tại `http://127.0.0.1:5010`. +3. Khi test local, Agent nên có: + +```env +ROBOT_PACKAGE_BASE_URL=http://localhost:3000 +ALLOWED_ORIGINS=http://localhost:5173,http://localhost:4173 +ALLOWED_DOWNLOAD_HOSTS=localhost,127.0.0.1 +``` + +4. Mở Web Client, bấm `Retry`, chọn app đã `Released`, rồi bấm `Install`. diff --git a/web-client/index.html b/web-client/index.html new file mode 100644 index 0000000..c5b13fc --- /dev/null +++ b/web-client/index.html @@ -0,0 +1,19 @@ + + + + + + + Robot Installer + + + + + +
+ + + diff --git a/web-client/package-lock.json b/web-client/package-lock.json new file mode 100644 index 0000000..7b48d54 --- /dev/null +++ b/web-client/package-lock.json @@ -0,0 +1,1716 @@ +{ + "name": "robot-installer-web-client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "robot-installer-web-client", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^7.0.0" + }, + "devDependencies": {} + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/web-client/package.json b/web-client/package.json new file mode 100644 index 0000000..53500c3 --- /dev/null +++ b/web-client/package.json @@ -0,0 +1,20 @@ +{ + "name": "robot-installer-web-client", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Public web client for installing Robot applications through the Local Installer Agent.", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "vite": "^7.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "lucide-react": "^0.468.0" + }, + "devDependencies": {} +} diff --git a/web-client/src/main.jsx b/web-client/src/main.jsx new file mode 100644 index 0000000..a71c340 --- /dev/null +++ b/web-client/src/main.jsx @@ -0,0 +1,861 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { + Activity, + AlertCircle, + Box, + CheckCircle2, + Clipboard, + Cpu, + Download, + ExternalLink, + HardDrive, + Loader2, + PackageCheck, + Play, + PlugZap, + RefreshCcw, + RotateCcw, + Search, + Server, + Settings, + ShieldCheck, + TerminalSquare, + Trash2, + WifiOff, + XCircle +} from 'lucide-react'; +import { + DEFAULT_AGENT_BASE_URL, + DEFAULT_PACKAGE_BASE_URL, + fetchAgentHealth, + fetchAgentSystemInfo, + fetchApplicationDetail, + fetchApplicationManifest, + fetchInstalledApps, + fetchPackageApps, + fetchTaskComponents, + fetchTaskLogs, + fetchTaskStatus, + joinUrl, + normalizeUrl, + queueInstall, + queueRemove, + queueUpdate +} from './services/api.js'; +import './styles.css'; + +const SETTINGS_KEY = 'robot-installer-client-settings'; +const TERMINAL_TASK_STATUSES = new Set(['success', 'failed', 'cancelled']); + +function readSettings() { + try { + const parsed = JSON.parse(window.localStorage.getItem(SETTINGS_KEY) || '{}'); + return { + packageBaseUrl: normalizeUrl(parsed.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL), + agentBaseUrl: normalizeUrl(parsed.agentBaseUrl || DEFAULT_AGENT_BASE_URL) + }; + } catch { + return { + packageBaseUrl: DEFAULT_PACKAGE_BASE_URL, + agentBaseUrl: DEFAULT_AGENT_BASE_URL + }; + } +} + +function saveSettings(settings) { + window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); +} + +function statusTone(status) { + if (status === 'success' || status === 'installed' || status === 'online') return 'success'; + if (status === 'running' || status === 'queued') return 'info'; + if (status === 'failed' || status === 'offline') return 'danger'; + if (status === 'update') return 'warning'; + return 'muted'; +} + +function formatDate(value) { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat('vi-VN', { + dateStyle: 'short', + timeStyle: 'short' + }).format(date); +} + +function getErrorMessage(error) { + return error instanceof Error ? error.message : String(error || 'Có lỗi xảy ra'); +} + +function App() { + const [settings, setSettings] = useState(readSettings); + const [draftSettings, setDraftSettings] = useState(settings); + const [apps, setApps] = useState([]); + const [installedApps, setInstalledApps] = useState([]); + const [agentHealth, setAgentHealth] = useState(null); + const [systemInfo, setSystemInfo] = useState(null); + const [packageStatus, setPackageStatus] = useState({ state: 'idle', message: '' }); + const [agentStatus, setAgentStatus] = useState({ state: 'idle', message: '' }); + const [selectedAppId, setSelectedAppId] = useState(''); + const [selectedDetail, setSelectedDetail] = useState(null); + const [selectedManifest, setSelectedManifest] = useState(null); + const [detailStatus, setDetailStatus] = useState({ state: 'idle', message: '' }); + const [query, setQuery] = useState(''); + const [filter, setFilter] = useState('all'); + const [toast, setToast] = useState(null); + const [busyAction, setBusyAction] = useState(''); + const [activeTask, setActiveTask] = useState(null); + const [task, setTask] = useState(null); + const [taskLogs, setTaskLogs] = useState([]); + const [taskComponents, setTaskComponents] = useState([]); + const [taskStatus, setTaskStatus] = useState({ state: 'idle', message: '' }); + + const packageBaseUrl = settings.packageBaseUrl; + const agentBaseUrl = settings.agentBaseUrl; + const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`; + + const installedByAppId = useMemo(() => { + return new Map(installedApps.map((app) => [app.appId, app])); + }, [installedApps]); + + const mergedApps = useMemo(() => { + return apps.map((app) => { + const installed = installedByAppId.get(app.appId); + const isInstalled = Boolean(installed); + const canUpdate = Boolean(isInstalled && installed.version && installed.version !== app.version); + + return { + ...app, + installed, + localStatus: canUpdate ? 'update' : (isInstalled ? 'installed' : 'available'), + canUpdate + }; + }); + }, [apps, installedByAppId]); + + const filteredApps = useMemo(() => { + const needle = query.trim().toLowerCase(); + return mergedApps.filter((app) => { + const matchesQuery = !needle || [ + app.appId, + app.appName, + app.version, + app.status + ].join(' ').toLowerCase().includes(needle); + + if (!matchesQuery) return false; + if (filter === 'installed') return app.localStatus === 'installed' || app.localStatus === 'update'; + if (filter === 'updates') return app.localStatus === 'update'; + if (filter === 'available') return app.localStatus === 'available'; + return true; + }); + }, [filter, mergedApps, query]); + + const selectedApp = useMemo(() => { + return mergedApps.find((app) => app.appId === selectedAppId) || mergedApps[0] || null; + }, [mergedApps, selectedAppId]); + + const stats = useMemo(() => { + return { + available: apps.length, + installed: installedApps.length, + updates: mergedApps.filter((app) => app.canUpdate).length, + components: selectedManifest?.components?.length || selectedDetail?.packages?.length || 0 + }; + }, [apps.length, installedApps.length, mergedApps, selectedDetail, selectedManifest]); + + const notify = useCallback((type, message) => { + setToast({ id: Date.now(), type, message }); + }, []); + + const refreshPackage = useCallback(async () => { + setPackageStatus({ state: 'loading', message: 'Đang tải app từ package server' }); + try { + const nextApps = await fetchPackageApps(packageBaseUrl); + setApps(nextApps); + setPackageStatus({ state: 'success', message: `${nextApps.length} app released` }); + return nextApps; + } catch (error) { + setPackageStatus({ state: 'danger', message: getErrorMessage(error) }); + setApps([]); + return []; + } + }, [packageBaseUrl]); + + const refreshAgent = useCallback(async () => { + setAgentStatus({ state: 'loading', message: 'Đang kiểm tra Agent local' }); + try { + const health = await fetchAgentHealth(agentBaseUrl); + setAgentHealth(health); + setAgentStatus({ state: 'success', message: `${health.hostname || 'Agent'} online` }); + + const [info, installed] = await Promise.all([ + fetchAgentSystemInfo(agentBaseUrl).catch(() => null), + fetchInstalledApps(agentBaseUrl) + ]); + setSystemInfo(info); + setInstalledApps(installed); + return true; + } catch (error) { + setAgentHealth(null); + setSystemInfo(null); + setInstalledApps([]); + setAgentStatus({ state: 'danger', message: getErrorMessage(error) }); + return false; + } + }, [agentBaseUrl]); + + const refreshAll = useCallback(async () => { + await Promise.all([refreshPackage(), refreshAgent()]); + }, [refreshAgent, refreshPackage]); + + const loadSelectedDetail = useCallback(async (app) => { + if (!app) { + setSelectedDetail(null); + setSelectedManifest(null); + return; + } + + setDetailStatus({ state: 'loading', message: 'Đang tải manifest' }); + try { + const [detail, manifest] = await Promise.all([ + fetchApplicationDetail(packageBaseUrl, app.appId).catch(() => null), + fetchApplicationManifest(packageBaseUrl, app.appId, app.version).catch(() => null) + ]); + setSelectedDetail(detail); + setSelectedManifest(manifest); + setDetailStatus({ state: 'success', message: manifest ? 'Manifest sẵn sàng' : 'Đã tải app detail' }); + } catch (error) { + setSelectedDetail(null); + setSelectedManifest(null); + setDetailStatus({ state: 'danger', message: getErrorMessage(error) }); + } + }, [packageBaseUrl]); + + const loadTaskSnapshot = useCallback(async (taskId) => { + setTaskStatus({ state: 'loading', message: 'Đang cập nhật task' }); + try { + const [nextTask, nextLogs, nextComponents] = await Promise.all([ + fetchTaskStatus(agentBaseUrl, taskId), + fetchTaskLogs(agentBaseUrl, taskId), + fetchTaskComponents(agentBaseUrl, taskId).catch(() => []) + ]); + setTask(nextTask); + setTaskLogs(nextLogs); + setTaskComponents(nextComponents); + setTaskStatus({ state: statusTone(nextTask.status), message: nextTask.status }); + + if (TERMINAL_TASK_STATUSES.has(nextTask.status)) { + await refreshAgent(); + } + return nextTask; + } catch (error) { + setTaskStatus({ state: 'danger', message: getErrorMessage(error) }); + return null; + } + }, [agentBaseUrl, refreshAgent]); + + const startTask = useCallback((queuedTask, action, app) => { + setActiveTask({ + taskId: queuedTask.taskId, + action, + appId: app.appId, + appName: app.appName, + queuedAt: new Date().toISOString() + }); + setTask({ + taskId: queuedTask.taskId, + type: action, + appId: app.appId, + appName: app.appName, + status: queuedTask.status || 'queued', + progress: 0, + currentStep: 'queued' + }); + setTaskLogs([]); + setTaskComponents([]); + }, []); + + const runAppAction = useCallback(async (action, app) => { + if (!agentHealth) { + notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.'); + return; + } + + if (action === 'remove' && !window.confirm(`Remove ${app.appName} khỏi máy local?`)) { + return; + } + + const key = `${action}:${app.appId}`; + setBusyAction(key); + try { + let queuedTask; + if (action === 'install') { + queuedTask = await queueInstall(agentBaseUrl, app); + } else if (action === 'update') { + queuedTask = await queueUpdate(agentBaseUrl, app, app.installed); + } else { + queuedTask = await queueRemove(agentBaseUrl, app); + } + + startTask(queuedTask, action, app); + notify('success', `Đã queue task ${queuedTask.taskId}`); + } catch (error) { + notify('failure', getErrorMessage(error)); + } finally { + setBusyAction(''); + } + }, [agentBaseUrl, agentHealth, notify, startTask]); + + const applySettings = useCallback(() => { + const nextSettings = { + packageBaseUrl: normalizeUrl(draftSettings.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL), + agentBaseUrl: normalizeUrl(draftSettings.agentBaseUrl || DEFAULT_AGENT_BASE_URL) + }; + setSettings(nextSettings); + saveSettings(nextSettings); + notify('info', 'Đã cập nhật endpoint test'); + }, [draftSettings, notify]); + + const copyInstallCommand = useCallback(async () => { + try { + await navigator.clipboard.writeText(installCommand); + notify('success', 'Đã copy lệnh cài Agent'); + } catch { + notify('warning', 'Không thể copy tự động trong browser này'); + } + }, [installCommand, notify]); + + useEffect(() => { + refreshAll(); + }, [refreshAll]); + + useEffect(() => { + if (!selectedApp) { + setSelectedDetail(null); + setSelectedManifest(null); + return; + } + setSelectedAppId(selectedApp.appId); + loadSelectedDetail(selectedApp); + }, [loadSelectedDetail, selectedApp?.appId]); + + useEffect(() => { + if (!activeTask?.taskId) return undefined; + let disposed = false; + + async function poll() { + const nextTask = await loadTaskSnapshot(activeTask.taskId); + if (disposed || !nextTask) return; + if (TERMINAL_TASK_STATUSES.has(nextTask.status)) { + window.clearInterval(timer); + } + } + + poll(); + const timer = window.setInterval(poll, 1200); + + return () => { + disposed = true; + window.clearInterval(timer); + }; + }, [activeTask?.taskId, loadTaskSnapshot]); + + useEffect(() => { + if (!toast) return undefined; + const timer = window.setTimeout(() => setToast(null), 3200); + return () => window.clearTimeout(timer); + }, [toast]); + + return ( +
+ + +
+
+
+ robot.installer + {agentHealth ? 'Ready for install' : 'Waiting for Agent'} +
+
+ + + +
+
+ +
+
+
+

Application catalog

+

Released apps từ package server và trạng thái cài đặt trên máy local.

+
+
+ + + +
+
+ +
+ + + + +
+ + {!agentHealth && ( +
+
+ )} + +
+
+
+ + +
+ +
+ + + + + + + + + + + + {packageStatus.state === 'loading' && ( + + + + )} + {packageStatus.state === 'danger' && ( + + + + )} + {packageStatus.state !== 'loading' && packageStatus.state !== 'danger' && filteredApps.length === 0 && ( + + + + )} + {filteredApps.map((app) => ( + setSelectedAppId(app.appId)} + > + + + + + + + ))} + +
ApplicationReleasedLocalPackagesActions
Đang tải danh sách app...
{packageStatus.message}
Chưa có app phù hợp bộ lọc.
+ + {app.appId} + + {app.version} + + + {app.packageCount || 0} +
+ {!app.installed && ( + + )} + {app.installed && app.canUpdate && ( + + )} + {app.installed && ( + + )} +
+
+
+
+ {filteredApps.length} / {mergedApps.length} apps + {packageBaseUrl} +
+
+ + +
+
+
+ + {toast && } +
+ ); +} + +function StatusBox({ icon: Icon, title, detail, tone }) { + return ( +
+ +
+ {title} + {detail} +
+
+ ); +} + +function MetricCard({ label, value, note, tone }) { + return ( +
+ {label} +
+ {value} + {note} +
+
+ ); +} + +function LocalStatus({ app }) { + if (app.localStatus === 'update') { + return ( + + Update + {app.installed.version} + + ); + } + + if (app.localStatus === 'installed') { + return ( + + Installed + {app.installed.version} + + ); + } + + return Available; +} + +function AgentPanel({ health, systemInfo, status }) { + return ( +
+
+
+

Local Agent

+

{status.message || '127.0.0.1:5010'}

+
+ {health ?
+
+
+
Version
+
{health?.agentVersion || '-'}
+
+
+
Host
+
{health?.hostname || '-'}
+
+
+
OS
+
{systemInfo?.os || health?.os || '-'}
+
+
+
Arch
+
{systemInfo?.architecture || health?.architecture || '-'}
+
+
+
+ + +
+
+ ); +} + +function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) { + const packages = detail?.packages || []; + const components = manifest?.components || []; + + return ( +
+
+
+

{app?.appName || 'App detail'}

+

{app?.appId || status.message || packageBaseUrl}

+
+ {status.state === 'loading' ?
+ + {app ? ( + <> +
+
+
Released
+
{app.version}
+
+
+
Status
+
{app.status || 'Released'}
+
+
+
Local
+
{app.installed ? `${app.installed.version} · ${app.installed.status || 'installed'}` : 'Not installed'}
+
+
+ +
+
+
+ {(components.length ? components : packages).slice(0, 5).map((item) => ( +
+
+ {item.componentId || item.code || item.name} + {item.packageName || item.selectedVersion || item.type} +
+ {item.version || item.selectedVersion || item.type || 'pkg'} +
+ ))} + {!components.length && !packages.length && ( +
{status.message || 'Chưa có component.'}
+ )} +
+ + ) : ( +
Chọn app để xem manifest.
+ )} +
+ ); +} + +function TaskPanel({ activeTask, task, logs, components, status, onRefresh }) { + const progress = Math.max(0, Math.min(100, Number(task?.progress || 0))); + + return ( +
+
+
+

Task monitor

+

{activeTask?.taskId || 'Chưa có task'}

+
+ +
+ + {task ? ( + <> +
+
+ {task.status} + {task.appName || task.appId} + {task.currentStep || '-'} +
+ {progress}% +
+
+
+
+ + {components.length > 0 && ( +
+
+
+ {components.map((component) => ( +
+
+ {component.componentId} + {component.currentStep || component.type} +
+ {component.progress || 0}% +
+ ))} +
+ )} + +
+
+
+
+ {logs.slice(-8).map((log, index) => ( +
+ + {log.message} +
+ ))} + {logs.length === 0 && ( +
{status.message || 'Đang chờ log.'}
+ )} +
+
+ + ) : ( +
Install, update hoặc remove để bắt đầu theo dõi.
+ )} +
+ ); +} + +function Toast({ toast }) { + const tone = toast.type === 'failure' ? 'danger' : toast.type; + const Icon = tone === 'danger' ? AlertCircle : tone === 'success' ? CheckCircle2 : Activity; + return ( +
+
+ ); +} + +createRoot(document.getElementById('root')).render(); diff --git a/web-client/src/services/api.js b/web-client/src/services/api.js new file mode 100644 index 0000000..291682d --- /dev/null +++ b/web-client/src/services/api.js @@ -0,0 +1,204 @@ +export const DEFAULT_PACKAGE_BASE_URL = normalizeUrl( + import.meta.env.VITE_PACKAGE_BASE_URL || window.location.origin +); +export const DEFAULT_AGENT_BASE_URL = normalizeUrl( + import.meta.env.VITE_AGENT_BASE_URL || 'http://127.0.0.1:5010' +); + +export function normalizeUrl(value) { + const text = String(value || '').trim(); + return text.replace(/\/+$/, ''); +} + +export function joinUrl(baseUrl, path) { + const normalizedBaseUrl = normalizeUrl(baseUrl); + const normalizedPath = String(path || '').startsWith('/') ? path : `/${path || ''}`; + return `${normalizedBaseUrl}${normalizedPath}`; +} + +async function requestJson(baseUrl, path, options = {}) { + const { + timeoutMs = 8000, + body, + headers, + ...fetchOptions + } = options; + const controller = new AbortController(); + const timeout = window.setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(joinUrl(baseUrl, path), { + ...fetchOptions, + headers: { + Accept: 'application/json', + ...(body ? { 'Content-Type': 'application/json' } : {}), + ...headers + }, + body: body ? JSON.stringify(body) : undefined, + signal: controller.signal + }); + + const text = await response.text(); + let payload = null; + if (text) { + try { + payload = JSON.parse(text); + } catch { + payload = text; + } + } + + if (!response.ok) { + const detail = payload?.detail || payload?.error || payload || response.statusText; + throw new Error(`${response.status} ${detail}`); + } + + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Request timeout: ${joinUrl(baseUrl, path)}`); + } + throw error; + } finally { + window.clearTimeout(timeout); + } +} + +export async function fetchPackageApps(packageBaseUrl) { + const payload = await requestJson(packageBaseUrl, '/api/apps', { timeoutMs: 10000 }); + return Array.isArray(payload?.apps) ? payload.apps.map(normalizePackageApp) : []; +} + +export async function fetchApplicationDetail(packageBaseUrl, appId) { + return requestJson(packageBaseUrl, `/api/apps/${encodeURIComponent(appId)}`, { timeoutMs: 10000 }); +} + +export async function fetchApplicationManifest(packageBaseUrl, appId, version) { + return requestJson( + packageBaseUrl, + `/api/apps/${encodeURIComponent(appId)}/versions/${encodeURIComponent(version)}/manifest`, + { timeoutMs: 10000 } + ); +} + +export async function fetchAgentHealth(agentBaseUrl) { + return requestJson(agentBaseUrl, '/health', { timeoutMs: 2800 }); +} + +export async function fetchAgentSystemInfo(agentBaseUrl) { + return requestJson(agentBaseUrl, '/system-info', { timeoutMs: 5000 }); +} + +export async function fetchInstalledApps(agentBaseUrl) { + const payload = await requestJson(agentBaseUrl, '/apps/installed', { timeoutMs: 7000 }); + return Array.isArray(payload) ? payload.map(normalizeInstalledApp) : []; +} + +export async function queueInstall(agentBaseUrl, app) { + return requestJson(agentBaseUrl, '/apps/install', { + method: 'POST', + timeoutMs: 10000, + body: { + appId: app.appId, + appName: app.appName, + version: app.version + } + }); +} + +export async function queueUpdate(agentBaseUrl, app, installedApp) { + return requestJson(agentBaseUrl, '/apps/update', { + method: 'POST', + timeoutMs: 10000, + body: { + appId: app.appId, + appName: app.appName, + currentVersion: installedApp?.version || '', + targetVersion: app.version + } + }); +} + +export async function queueRemove(agentBaseUrl, app) { + return requestJson(agentBaseUrl, '/apps/remove', { + method: 'POST', + timeoutMs: 10000, + body: { + appId: app.appId, + purge: false + } + }); +} + +export async function fetchTaskStatus(agentBaseUrl, taskId) { + return normalizeTask(await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}`, { timeoutMs: 7000 })); +} + +export async function fetchTaskLogs(agentBaseUrl, taskId) { + const payload = await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}/logs`, { timeoutMs: 7000 }); + return Array.isArray(payload?.logs) ? payload.logs.map(normalizeLog) : []; +} + +export async function fetchTaskComponents(agentBaseUrl, taskId) { + const payload = await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}/components`, { timeoutMs: 7000 }); + return Array.isArray(payload?.components) ? payload.components.map(normalizeComponent) : []; +} + +function normalizePackageApp(app) { + return { + appId: String(app.appId || app.app_id || '').trim(), + appName: String(app.appName || app.app_name || app.name || '').trim(), + version: String(app.version || '').trim(), + status: String(app.status || 'Released').trim(), + packageCount: Number(app.packageCount || app.package_count || 0) + }; +} + +function normalizeInstalledApp(app) { + return { + appId: String(app.appId || app.app_id || '').trim(), + appName: String(app.appName || app.app_name || '').trim(), + version: String(app.installedVersion || app.version || app.package_version || '').trim(), + status: String(app.status || 'installed').trim(), + installedAt: app.installedAt || app.installed_at || '', + updatedAt: app.updatedAt || app.updated_at || '' + }; +} + +function normalizeTask(task) { + return { + taskId: task.taskId || task.task_id, + type: task.type, + appId: task.appId || task.app_id, + appName: task.appName || task.app_name, + status: task.status, + progress: Number(task.progress || 0), + currentStep: task.currentStep || task.current_step, + currentComponentId: task.currentComponentId || task.current_component_id, + errorMessage: task.errorMessage || task.error_message, + createdAt: task.createdAt || task.created_at, + startedAt: task.startedAt || task.started_at, + finishedAt: task.finishedAt || task.finished_at + }; +} + +function normalizeLog(log) { + return { + time: log.time || log.timestamp || '', + level: log.level || 'info', + message: log.message || '' + }; +} + +function normalizeComponent(component) { + return { + componentId: component.componentId || component.component_id, + type: component.type, + status: component.status, + progress: Number(component.progress || 0), + currentStep: component.currentStep || component.current_step, + errorMessage: component.errorMessage || component.error_message, + startedAt: component.startedAt || component.started_at, + finishedAt: component.finishedAt || component.finished_at + }; +} diff --git a/web-client/src/styles.css b/web-client/src/styles.css new file mode 100644 index 0000000..bfe8f67 --- /dev/null +++ b/web-client/src/styles.css @@ -0,0 +1,1132 @@ +:root { + --primary: #3755c3; + --primary-dim: #2848b7; + --primary-container: #dde1ff; + --on-primary: #f8f7ff; + --background: #f7f9fb; + --surface-lowest: #ffffff; + --surface-low: #f0f4f7; + --surface: #e8eff3; + --surface-high: #e1e9ee; + --on-surface: #2a3439; + --on-surface-variant: #566166; + --outline-variant: #a9b4b9; + --danger: #b42318; + --danger-bg: #fee4e2; + --success: #067647; + --success-bg: #dcfae6; + --warning: #b54708; + --warning-bg: #fef0c7; + --info: #175cd3; + --info-bg: #d1e9ff; + --muted-bg: #e2e8f0; + --radius: 8px; + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.18); +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; + width: 100%; +} + +body { + margin: 0; + background: var(--background); + color: var(--on-surface); + font-family: "Inter", Arial, sans-serif; + font-size: 14px; + overflow: hidden; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.56; +} + +a { + color: inherit; + text-decoration: none; +} + +h1, +h2, +h3, +p { + margin: 0; +} + +h1, +h2, +.brand-copy strong { + font-family: "Manrope", Arial, sans-serif; +} + +code { + background: #111827; + border-radius: 6px; + color: #f8fafc; + display: block; + font-family: Consolas, "Liberation Mono", monospace; + font-size: 12px; + line-height: 1.45; + margin-top: 6px; + overflow-wrap: anywhere; + padding: 8px 10px; +} + +.app-shell { + display: flex; + height: 100vh; + width: 100vw; +} + +.sidebar { + background: #f1f5f9; + border-right: 1px solid rgba(169, 180, 185, 0.25); + display: flex; + flex-direction: column; + flex-shrink: 0; + height: 100vh; + width: 256px; +} + +.brand-block { + align-items: center; + display: flex; + gap: 10px; + min-height: 64px; + padding: 14px; +} + +.brand-mark, +.status-icon { + align-items: center; + display: inline-flex; + justify-content: center; +} + +.brand-mark { + background: var(--primary); + border-radius: var(--radius); + color: var(--on-primary); + height: 38px; + width: 38px; +} + +.brand-copy { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.brand-copy strong { + color: #111827; + font-size: 14px; + font-weight: 800; +} + +.brand-copy span, +.settings-field span, +.nav-label { + color: var(--on-surface-variant); + font-size: 10px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + +.nav-section { + display: flex; + flex: 1; + flex-direction: column; + gap: 10px; + overflow: auto; + padding: 8px 12px 14px; +} + +.nav-label { + padding: 8px 2px 0; +} + +.status-box { + align-items: center; + background: rgba(255, 255, 255, 0.68); + border: 1px solid rgba(169, 180, 185, 0.35); + border-radius: var(--radius); + display: grid; + gap: 10px; + grid-template-columns: 34px minmax(0, 1fr); + min-height: 56px; + padding: 10px; +} + +.status-box strong, +.status-box span, +.settings-field span { + display: block; +} + +.status-box strong { + color: #111827; + font-size: 12px; + font-weight: 800; +} + +.status-box div > span { + color: #64748b; + font-size: 11px; + line-height: 1.35; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-icon { + background: var(--muted-bg); + border-radius: var(--radius); + color: #475569; + height: 34px; + width: 34px; +} + +.tone-success .status-icon, +.tone-success.status-box .status-icon { + background: var(--success-bg); + color: var(--success); +} + +.tone-danger .status-icon, +.tone-danger.status-box .status-icon { + background: var(--danger-bg); + color: var(--danger); +} + +.tone-warning .status-icon, +.tone-warning.status-box .status-icon { + background: var(--warning-bg); + color: var(--warning); +} + +.tone-info .status-icon, +.tone-info.status-box .status-icon { + background: var(--info-bg); + color: var(--info); +} + +.settings-field { + display: flex; + flex-direction: column; + gap: 5px; +} + +.settings-field input, +.filter-field input, +.filter-field select { + background: var(--surface-low); + border: 1px solid #d8e1e8; + border-radius: var(--radius); + color: var(--on-surface); + min-height: 34px; + outline: 0; + padding: 7px 10px; + width: 100%; +} + +.settings-field input { + font-size: 12px; +} + +.settings-field input:focus, +.filter-field input:focus, +.filter-field select:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(55, 85, 195, 0.14); +} + +.main-shell { + display: flex; + flex: 1; + flex-direction: column; + height: 100vh; + min-width: 0; +} + +.topbar { + align-items: center; + background: rgba(248, 250, 252, 0.92); + border-bottom: 1px solid rgba(169, 180, 185, 0.22); + display: flex; + flex-shrink: 0; + height: 56px; + justify-content: space-between; + padding: 0 24px; +} + +.topbar-title { + display: flex; + flex-direction: column; + gap: 2px; +} + +.topbar-title span { + color: #64748b; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; +} + +.topbar-title strong { + color: #111827; + font-size: 13px; +} + +.topbar-actions, +.page-actions, +.action-group { + align-items: center; + display: flex; + gap: 8px; +} + +.page { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + overflow: hidden; + padding: 24px; +} + +.page-header { + align-items: flex-start; + display: flex; + flex-shrink: 0; + gap: 16px; + justify-content: space-between; + margin-bottom: 16px; +} + +.page-header h1 { + color: #111827; + font-size: 24px; + font-weight: 800; + line-height: 1.2; +} + +.page-header p, +.panel-header p { + color: var(--on-surface-variant); + font-size: 13px; + line-height: 1.5; + margin-top: 4px; +} + +.btn, +.icon-button { + align-items: center; + border: 1px solid transparent; + border-radius: var(--radius); + display: inline-flex; + font-size: 12px; + font-weight: 800; + gap: 6px; + justify-content: center; + transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease, transform 0.16s ease; + white-space: nowrap; +} + +.btn { + min-height: 36px; + padding: 8px 12px; +} + +.btn.compact { + min-height: 32px; + padding: 6px 10px; +} + +.btn.full { + width: 100%; +} + +.btn:active, +.icon-button:active { + transform: scale(0.98); +} + +.btn-primary { + background: var(--primary); + color: var(--on-primary); +} + +.btn-primary:hover { + background: var(--primary-dim); +} + +.btn-secondary { + background: #ffffff; + border-color: #cbd5e1; + color: #334155; +} + +.btn-secondary:hover { + background: #f1f5f9; +} + +.btn-warning { + background: #f59e0b; + color: #111827; +} + +.btn-warning:hover { + background: #d97706; + color: #ffffff; +} + +.icon-button { + background: #ffffff; + border-color: #dbe3ea; + color: #475569; + height: 34px; + padding: 0; + width: 34px; +} + +.icon-button:hover, +.icon-button.subtle:hover { + background: #eef2ff; + color: var(--primary); +} + +.icon-button.subtle { + background: transparent; + border-color: transparent; + color: #64748b; +} + +.icon-button.danger { + background: transparent; + border-color: transparent; + color: #ef4444; +} + +.icon-button.danger:hover { + background: var(--danger-bg); + color: var(--danger); +} + +.dashboard-stats { + display: grid; + flex-shrink: 0; + gap: 14px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-bottom: 14px; +} + +.metric-card, +.panel, +.table-panel, +.offline-banner { + background: var(--surface-lowest); + border: 1px solid rgba(169, 180, 185, 0.4); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); +} + +.metric-card { + display: flex; + flex-direction: column; + min-height: 88px; + padding: 14px; +} + +.metric-card > span, +.filter-field span, +.detail-list dt, +.component-list-title { + color: var(--on-surface-variant); + font-size: 10px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + +.metric-card div { + align-items: baseline; + display: flex; + justify-content: space-between; + margin-top: auto; +} + +.metric-card strong { + color: #111827; + font-family: "Manrope", Arial, sans-serif; + font-size: 27px; + font-weight: 800; +} + +.metric-card small { + color: var(--on-surface-variant); + font-size: 11px; + font-weight: 700; +} + +.metric-card.tone-warning { + border-color: rgba(181, 71, 8, 0.3); +} + +.metric-card.tone-success { + border-color: rgba(6, 118, 71, 0.28); +} + +.offline-banner { + align-items: center; + display: grid; + flex-shrink: 0; + gap: 12px; + grid-template-columns: auto minmax(0, 1fr) auto; + margin-bottom: 14px; + padding: 12px 14px; +} + +.offline-banner > svg { + color: var(--warning); +} + +.offline-banner strong { + color: #111827; + display: block; + font-size: 13px; +} + +.workbench-grid { + display: grid; + flex: 1; + gap: 16px; + grid-template-columns: minmax(0, 1fr) minmax(340px, 420px); + min-height: 0; +} + +.table-panel, +.panel { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.side-stack { + display: flex; + flex-direction: column; + gap: 14px; + min-height: 0; + overflow: auto; +} + +.page-filters { + align-items: center; + display: flex; + flex-shrink: 0; + gap: 12px; + margin-bottom: 14px; +} + +.page-filters.inline { + margin: 0; + padding: 14px 16px; +} + +.filter-field { + display: flex; + flex-direction: column; + gap: 5px; +} + +.filter-field.wide { + flex: 1; +} + +.input-with-icon { + align-items: center; + display: flex; + position: relative; +} + +.input-with-icon svg { + color: #64748b; + left: 10px; + position: absolute; +} + +.input-with-icon input { + padding-left: 32px; +} + +.table-wrap { + flex: 1; + min-height: 0; + overflow: auto; +} + +table { + border-collapse: collapse; + min-width: 860px; + text-align: left; + width: 100%; +} + +thead { + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + position: sticky; + top: 0; + z-index: 5; +} + +th { + color: #64748b; + font-size: 10px; + font-weight: 800; + letter-spacing: 0; + padding: 10px 16px; + text-transform: uppercase; + white-space: nowrap; +} + +td { + color: #475569; + font-size: 13px; + padding: 12px 16px; + vertical-align: middle; +} + +tbody tr { + border-bottom: 1px solid #f1f5f9; + transition: background 0.16s ease; +} + +tbody tr:hover, +tbody tr.selected-row { + background: #f8fafc; +} + +.table-title { + color: #172033; + display: block; + font-weight: 800; + line-height: 1.35; +} + +.table-title.as-button { + background: transparent; + border: 0; + padding: 0; + text-align: left; +} + +.table-title:hover { + color: var(--primary); +} + +.table-subtitle { + color: #64748b; + display: block; + font-size: 11px; + line-height: 1.45; + margin-top: 2px; +} + +.table-empty { + color: #64748b; + font-size: 13px; + font-weight: 700; + padding: 22px 16px; + text-align: center; +} + +.compact-empty { + padding: 16px; +} + +.danger-text { + color: var(--danger); +} + +.action-col { + position: sticky; + right: 0; + text-align: right; + white-space: nowrap; +} + +td.action-col { + background: #ffffff; + box-shadow: -10px 0 12px -14px rgba(15, 23, 42, 0.45); +} + +tbody tr:hover td.action-col, +tbody tr.selected-row td.action-col { + background: #f8fafc; +} + +.badge { + border-radius: 6px; + display: inline-flex; + font-size: 11px; + font-weight: 800; + line-height: 1; + padding: 6px 8px; + white-space: nowrap; +} + +.badge-primary, +.badge-info { + background: var(--info-bg); + color: var(--info); +} + +.badge-success { + background: var(--success-bg); + color: var(--success); +} + +.badge-warning { + background: var(--warning-bg); + color: var(--warning); +} + +.badge-danger { + background: var(--danger-bg); + color: var(--danger); +} + +.badge-muted { + background: var(--muted-bg); + color: #475569; +} + +.status-inline { + align-items: center; + display: inline-flex; + gap: 7px; +} + +.status-inline small { + color: #64748b; + font-size: 11px; + font-weight: 700; +} + +.page-pager { + align-items: center; + background: #f8fafc; + border-top: 1px solid #e2e8f0; + color: #64748b; + display: flex; + flex-shrink: 0; + font-size: 12px; + justify-content: space-between; + padding: 10px 14px; +} + +.panel-header { + align-items: flex-start; + border-bottom: 1px solid #eef2f7; + display: flex; + flex-shrink: 0; + gap: 14px; + justify-content: space-between; + padding: 15px 16px; +} + +.panel-header h2 { + color: #111827; + font-size: 15px; + font-weight: 800; +} + +.panel-state { + color: #64748b; + flex-shrink: 0; +} + +.panel-state.success { + color: var(--success); +} + +.panel-state.danger { + color: var(--danger); +} + +.detail-list { + display: flex; + flex-direction: column; + gap: 0; + margin: 0; + padding: 4px 16px 12px; +} + +.detail-list div { + border-bottom: 1px solid #eef2f7; + display: grid; + gap: 10px; + grid-template-columns: 92px minmax(0, 1fr); + padding: 10px 0; +} + +.detail-list div:last-child { + border-bottom: 0; +} + +.detail-list dd { + color: #172033; + margin: 0; + min-width: 0; + overflow-wrap: anywhere; +} + +.detail-list dt { + margin: 0; +} + +.agent-metrics { + border-top: 1px solid #eef2f7; + display: grid; + gap: 8px; + grid-template-columns: 1fr 1fr; + padding: 12px 16px 14px; +} + +.agent-metrics span { + align-items: center; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 7px; + color: #64748b; + display: inline-flex; + font-size: 11px; + font-weight: 700; + gap: 6px; + min-width: 0; + overflow: hidden; + padding: 8px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.component-list { + border-top: 1px solid #eef2f7; + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 16px 14px; +} + +.component-list-title { + align-items: center; + display: inline-flex; + gap: 6px; +} + +.component-item { + align-items: center; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: var(--radius); + display: grid; + gap: 8px; + grid-template-columns: minmax(0, 1fr) auto; + padding: 9px 10px; +} + +.component-item strong, +.component-item span { + display: block; +} + +.component-item strong { + color: #172033; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.component-item div > span { + color: #64748b; + font-size: 11px; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-summary { + align-items: center; + display: flex; + gap: 12px; + justify-content: space-between; + padding: 14px 16px 8px; +} + +.task-summary > div { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.task-summary strong { + color: #111827; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-summary small { + color: #64748b; + font-size: 12px; +} + +.task-summary > span { + color: #111827; + font-family: "Manrope", Arial, sans-serif; + font-size: 24px; + font-weight: 800; +} + +.progress-track { + background: #e2e8f0; + border-radius: 999px; + height: 8px; + margin: 0 16px 12px; + overflow: hidden; +} + +.progress-track div { + background: var(--primary); + border-radius: inherit; + height: 100%; + transition: width 0.18s ease; +} + +.task-components { + padding-top: 10px; +} + +.logs-box { + border-top: 1px solid #eef2f7; + display: flex; + flex-direction: column; + min-height: 0; + padding: 12px 16px 14px; +} + +.log-lines { + background: #111827; + border-radius: var(--radius); + color: #f8fafc; + display: flex; + flex-direction: column; + gap: 0; + margin-top: 8px; + max-height: 240px; + overflow: auto; + padding: 8px; +} + +.log-line { + display: grid; + gap: 8px; + grid-template-columns: 118px minmax(0, 1fr); + padding: 4px 2px; +} + +.log-line time { + color: #94a3b8; + font-size: 11px; +} + +.log-line span { + font-family: Consolas, "Liberation Mono", monospace; + font-size: 11px; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.log-line.level-error span { + color: #fecaca; +} + +.toast { + align-items: center; + background: #111827; + border-radius: var(--radius); + bottom: 20px; + box-shadow: var(--shadow-lg); + color: #ffffff; + display: inline-flex; + font-size: 13px; + font-weight: 700; + gap: 8px; + max-width: min(420px, calc(100vw - 32px)); + padding: 12px 14px; + position: fixed; + right: 20px; + z-index: 120; +} + +.toast.tone-success { + background: var(--success); +} + +.toast.tone-danger { + background: var(--danger); +} + +.toast.tone-warning { + background: var(--warning); +} + +.spin { + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 1180px) { + .workbench-grid { + grid-template-columns: 1fr; + overflow: auto; + } + + .side-stack { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + overflow: visible; + } + + .task-panel { + grid-column: 1 / -1; + } +} + +@media (max-width: 980px) { + body { + overflow: auto; + } + + .app-shell { + display: block; + height: auto; + min-height: 100vh; + } + + .sidebar { + border-bottom: 1px solid rgba(169, 180, 185, 0.25); + border-right: 0; + height: auto; + width: 100%; + } + + .nav-section { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .nav-label { + grid-column: 1 / -1; + } + + .main-shell { + height: auto; + min-height: 0; + } + + .page { + overflow: visible; + } + + .dashboard-stats, + .side-stack { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 680px) { + .topbar, + .page-header, + .page-filters, + .offline-banner { + align-items: stretch; + flex-direction: column; + } + + .topbar { + height: auto; + gap: 12px; + padding: 12px 16px; + } + + .topbar-actions, + .page-actions { + flex-wrap: wrap; + } + + .topbar-actions .btn, + .page-actions .btn, + .page-actions a { + flex: 1; + } + + .page { + padding: 16px; + } + + .dashboard-stats, + .side-stack, + .nav-section { + grid-template-columns: 1fr; + } + + .offline-banner { + display: flex; + } + + table { + min-width: 760px; + } + + .page-pager { + align-items: flex-start; + flex-direction: column; + gap: 6px; + } + + .log-line { + grid-template-columns: 1fr; + } +} diff --git a/web-client/vite.config.js b/web-client/vite.config.js new file mode 100644 index 0000000..941065b --- /dev/null +++ b/web-client/vite.config.js @@ -0,0 +1,36 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +const packageProxyTarget = process.env.PACKAGE_PROXY_TARGET + || process.env.VITE_PACKAGE_BASE_URL + || 'http://localhost:3000'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + strictPort: false, + proxy: { + '/api': { + target: packageProxyTarget, + changeOrigin: true + }, + '/install-agent.sh': { + target: packageProxyTarget, + changeOrigin: true + }, + '/uploads': { + target: packageProxyTarget, + changeOrigin: true + }, + '/packages': { + target: packageProxyTarget, + changeOrigin: true + } + } + }, + preview: { + port: 4173, + strictPort: false + } +}); diff --git a/web-server/server.js b/web-server/server.js index 487e02d..34868f4 100644 --- a/web-server/server.js +++ b/web-server/server.js @@ -18,6 +18,11 @@ const agentPackageDir = path.resolve(process.env.AGENT_PACKAGE_DIR || path.join( const authCookieName = 'robot_installer_session'; const sessionMaxAgeMs = Number(process.env.SESSION_MAX_AGE_MS || 1000 * 60 * 60 * 8); const authSecret = process.env.AUTH_SECRET || process.env.SESSION_SECRET || 'robot-installer-dev-secret'; +const publicApiCorsOrigins = getCsvEnv(process.env.WEB_CLIENT_ORIGINS || process.env.PUBLIC_API_CORS_ORIGINS, [ + 'https://robot.installer', + 'http://localhost:5173', + 'http://localhost:4173' +]); const agentVersionCollator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' @@ -99,6 +104,7 @@ app.set('views', path.join(__dirname, 'views')); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); app.use('/vendor/notiflix', express.static(path.join(__dirname, 'node_modules/notiflix/dist'))); +app.use(applyPublicApiCors); app.get('/packages/agent/latest.deb', asyncRoute(async (req, res) => { const arch = normalizeAgentArch(req.query.arch); const latestPackage = await findLatestAgentPackage(arch); @@ -142,6 +148,44 @@ function helpers() { }; } +function getCsvEnv(value, fallback) { + if (!value) return fallback; + return String(value) + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} + +function isPublicApiCorsPath(pathname) { + return pathname === '/api/apps' + || pathname.startsWith('/api/apps/') + || pathname === '/install-agent.sh'; +} + +function applyPublicApiCors(req, res, next) { + if (!isPublicApiCorsPath(req.path)) { + next(); + return; + } + + const origin = req.headers.origin; + const allowAnyOrigin = publicApiCorsOrigins.includes('*'); + + if (origin && (allowAnyOrigin || publicApiCorsOrigins.includes(origin))) { + res.setHeader('Access-Control-Allow-Origin', allowAnyOrigin ? '*' : origin); + res.setHeader('Vary', 'Origin'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Accept, Content-Type'); + } + + if (req.method === 'OPTIONS') { + res.sendStatus(204); + return; + } + + next(); +} + function getVisibleNavItems(user) { return navItems.filter((item) => !item.adminOnly || (user && user.role === 'Admin')); }