diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7eafff..725a87c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: java: ['17', '21'] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: ${{ matrix.java }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..907292e --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,256 @@ +name: E2E + +on: + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + ds-single: + name: DS single node + MinIO + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Build + run: mvn -B -ntp package -DskipTests + + - name: Start MinIO and node + run: | + cp harness/local/.env.example harness/local/.env + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.ds.yml \ + up -d --build + + - name: Wait for readiness + run: timeout 180 bash -c 'until curl -sf http://127.0.0.1:9090/ready; do sleep 2; done' + + - name: Assert admin plane + run: | + curl -sf http://127.0.0.1:9090/health | grep -qx ok + curl -sf http://127.0.0.1:9090/admin/cluster | jq -e '.registered == true and .raft.isLeader == true and .raft.leader != null' + curl -sf http://127.0.0.1:9090/admin/nodes | jq -e '.nodes | length == 1' + curl -sf -o /dev/null http://127.0.0.1:9090/ + + - name: Assert stream lifecycle via admin + run: | + curl -sf -X PUT -H 'Content-Type: application/json' -d '' http://127.0.0.1:4437/e2e/orders + curl -sf -X POST -H 'Content-Type: application/json' -d '{"n":1}' http://127.0.0.1:4437/e2e/orders + curl -sf http://127.0.0.1:9090/admin/streams/e2e/orders | jq -e '.ownerLocal == true and .meta.closed == false' + + - name: DS bench + run: | + java -jar cli/target/streamstack.jar ds bench --endpoint http://127.0.0.1:4437 \ + -b 1024 -n 64 -w 8 -d 8 + + - name: Collect logs + if: always() + run: | + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.ds.yml \ + logs --no-color > ds-single-logs.txt || true + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: ds-single-logs + path: ds-single-logs.txt + + - name: Teardown + if: always() + run: | + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.ds.yml \ + down -v || true + + ds-cluster: + name: DS three-node cluster + MinIO + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Build + run: mvn -B -ntp package -DskipTests + + - name: Start MinIO and cluster + run: | + cp harness/local/.env.example harness/local/.env + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.cluster.ds.yml \ + up -d --build + + - name: Wait for readiness on all nodes + run: | + for port in 9091 9092 9093; do + timeout 180 bash -c "until curl -sf http://127.0.0.1:${port}/ready; do sleep 2; done" + done + + - name: Assert cluster state + run: | + curl -sf http://127.0.0.1:9091/admin/nodes | jq -e '.nodes | length == 3' + leaders=0 + for port in 9091 9092 9093; do + if curl -sf "http://127.0.0.1:${port}/admin/cluster" | jq -e '.raft.isLeader == true' > /dev/null; then + leaders=$((leaders + 1)) + fi + done + test "$leaders" -eq 1 + + - name: Assert ownership redirects + run: | + curl -sf -X PUT -H 'Content-Type: application/json' -d '' http://127.0.0.1:4437/e2e/redirect + curl -sf -X POST -H 'Content-Type: application/json' -d '{"n":1}' http://127.0.0.1:4437/e2e/redirect + code=$(curl -s -o /dev/null -w '%{http_code}' -m 30 http://127.0.0.1:4438/e2e/redirect) + test "$code" = "307" + curl -sfL -m 30 http://127.0.0.1:4438/e2e/redirect | grep -q '"n":1' + + - name: DS bench + run: | + java -jar cli/target/streamstack.jar ds bench --endpoint http://127.0.0.1:4437 \ + -b 1024 -n 64 -w 8 -d 8 + + - name: Leader failover + run: | + leader="" + for port in 9091 9092 9093; do + node=$(curl -sf "http://127.0.0.1:${port}/admin/cluster" | jq -r 'select(.raft.isLeader == true) | .nodeId') + if [ -n "$node" ]; then + leader="$node" + fi + done + test -n "$leader" + echo "killing leader node ${leader}" + docker kill "local-node${leader}-1" + survivors="" + for id in 1 2 3; do + if [ "$id" != "$leader" ]; then + survivors="$survivors $((9090 + id))" + fi + done + elected="" + for attempt in $(seq 1 60); do + for port in $survivors; do + if curl -sf -m 3 "http://127.0.0.1:${port}/admin/cluster" | jq -e '.raft.isLeader == true' > /dev/null 2>&1; then + elected="$port" + break 2 + fi + done + sleep 2 + done + test -n "$elected" + echo "new leader reported by admin port ${elected}" + survivor=$(echo "$survivors" | awk '{print $1}') + http_port=$((survivor - 9090 + 4436)) + timeout 120 bash -c "until curl -sf http://127.0.0.1:${survivor}/ready; do sleep 2; done" + curl -sf -X PUT -H 'Content-Type: application/json' -d '' "http://127.0.0.1:${http_port}/e2e/failover" + curl -sf -X POST -H 'Content-Type: application/json' -d '{"after":"failover"}' "http://127.0.0.1:${http_port}/e2e/failover" + curl -sf -m 30 "http://127.0.0.1:${http_port}/e2e/failover" | grep -q '"after":"failover"' + + - name: Collect logs + if: always() + run: | + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.cluster.ds.yml \ + logs --no-color > ds-cluster-logs.txt || true + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: ds-cluster-logs + path: ds-cluster-logs.txt + + - name: Teardown + if: always() + run: | + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.cluster.ds.yml \ + down -v || true + + s2-single: + name: S2 single node + MinIO + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Build + run: mvn -B -ntp package -DskipTests + + - name: Start MinIO and node + run: | + cp harness/local/.env.example harness/local/.env + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.s2.yml \ + up -d --build + + - name: Wait for readiness + run: timeout 180 bash -c 'until curl -sf http://127.0.0.1:9090/ready; do sleep 2; done' + + - name: Assert admin plane + run: | + curl -sf http://127.0.0.1:9090/health | grep -qx ok + curl -sf http://127.0.0.1:9090/admin/cluster | jq -e '.registered == true and .raft.isLeader == true' + + - name: S2 bench + run: | + java -jar cli/target/streamstack.jar s2 bench --endpoint http://127.0.0.1:4437 e2esmokebasin \ + -b 1024 -n 64 -w 8 -d 8 + + - name: Collect logs + if: always() + run: | + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.s2.yml \ + logs --no-color > s2-single-logs.txt || true + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: s2-single-logs + path: s2-single-logs.txt + + - name: Teardown + if: always() + run: | + docker compose --env-file harness/local/.env \ + -f harness/local/docker-compose.minio.yml \ + -f harness/local/docker-compose.s2.yml \ + down -v || true diff --git a/.gitignore b/.gitignore index 7e8e06b..828fd30 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ server/conformance-run.log /docs/node_modules/ /docs/.vitepress/cache/ /docs/.vitepress/dist/ + +### Dashboard ### +/dashboard/node_modules/ +/server/src/main/resources/admin/dist/ diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..d8b08a2 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + StreamStack Admin + + +
+ + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..201618a --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,2099 @@ +{ + "name": "streamstack-dashboard", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "streamstack-dashboard", + "version": "0.0.0", + "dependencies": { + "preact": "^10.25.0" + }, + "devDependencies": { + "@preact/preset-vite": "^2.9.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@preact/preset-vite": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.6.tgz", + "integrity": "sha512-ZZ5RIT2ZVXg8UxRA8VZpoT8CdpDG7RFIqOT4bELqNBp85+HKvQBbgmh3gsoW1UOQXx26TLe+ZRNKI7q281Kckw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@prefresh/vite": "^2.4.11", + "@rollup/pluginutils": "^5.0.0", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.4.3", + "magic-string": "^0.30.21", + "picocolors": "^1.1.1", + "vite-prerender-plugin": "^0.5.8", + "zimmerframe": "^1.1.4" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.3.tgz", + "integrity": "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/core": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.10.tgz", + "integrity": "sha512-7yPTFbG56sutaFu8krp3B4a200KOFUvrtlllKWRuLjsYXo9UUucHOZRcer+gtgMkFTpv6ob8TGcTwA32bSwa1w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.12.tgz", + "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "^0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.407", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", + "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "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-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/simple-code-frame": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/simple-code-frame/-/simple-code-frame-1.3.0.tgz", + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.6.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0.tgz", + "integrity": "sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "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": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-prerender-plugin": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.13.tgz", + "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "magic-string": "0.x >= 0.26.0", + "node-html-parser": "^6.1.12", + "simple-code-frame": "^1.3.0", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "vite": "5.x || 6.x || 7.x || 8.x" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..9c94218 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,19 @@ +{ + "name": "streamstack-dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "preact": "^10.25.0" + }, + "devDependencies": { + "@preact/preset-vite": "^2.9.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/dashboard/src/api.ts b/dashboard/src/api.ts new file mode 100644 index 0000000..8e6386c --- /dev/null +++ b/dashboard/src/api.ts @@ -0,0 +1,47 @@ +export interface ClusterInfo { + clusterId: string + nodeId: number + nodeEpoch: number + advertisedAddress: string + registered: boolean + raft: { + leader: string | null + isLeader: boolean + appliedIndex: number + applySuccessCount: number + applyFailCount: number + peers: string[] | null + } + streamCount: number + objectCount: number + destroyedObjectBacklog: number +} + +export interface NodeInfo { + nodeId: number + nodeEpoch: number + advertisedAddress: string | null + local: boolean +} + +export interface Readiness { + ready: boolean + started: boolean + leaderKnown: boolean + registered: boolean +} + +async function get(path: string): Promise { + const res = await fetch(path, { headers: { Accept: 'application/json' } }) + const body = (await res.json()) as T + + if (!res.ok && path !== '/ready') { + throw new Error(`GET ${path} failed: ${res.status}`) + } + + return body +} + +export const fetchCluster = () => get('/admin/cluster') +export const fetchNodes = () => get<{ nodes: NodeInfo[] }>('/admin/nodes') +export const fetchReady = () => get('/ready') diff --git a/dashboard/src/app.tsx b/dashboard/src/app.tsx new file mode 100644 index 0000000..475ae93 --- /dev/null +++ b/dashboard/src/app.tsx @@ -0,0 +1,197 @@ +import { useEffect, useState } from 'preact/hooks' +import { + fetchCluster, + fetchNodes, + fetchReady, + type ClusterInfo, + type NodeInfo, + type Readiness, +} from './api' + +const POLL_INTERVAL_MS = 2000 + +function Logo() { + return ( + + + + + ) +} + +function Pill({ state, label }: { state: 'ok' | 'warn' | 'err'; label: string }) { + return ( + + + {label} + + ) +} + +function Stat({ label, value, small }: { label: string; value: unknown; small?: boolean }) { + return ( +
+
{label}
+
{String(value ?? '—')}
+
+ ) +} + +export function App() { + const [cluster, setCluster] = useState(null) + const [nodes, setNodes] = useState([]) + const [ready, setReady] = useState(null) + const [error, setError] = useState(null) + const [updatedAt, setUpdatedAt] = useState(null) + + useEffect(() => { + let alive = true + + async function poll() { + try { + const [c, n, r] = await Promise.all([fetchCluster(), fetchNodes(), fetchReady()]) + + if (!alive) { + return + } + + setCluster(c) + setNodes(n.nodes) + setReady(r) + setError(null) + setUpdatedAt(new Date()) + } catch (e) { + if (alive) { + setError(e instanceof Error ? e.message : String(e)) + } + } + } + + poll() + const timer = setInterval(poll, POLL_INTERVAL_MS) + + return () => { + alive = false + clearInterval(timer) + } + }, []) + + const readyState = ready?.ready ? 'ok' : ready ? 'warn' : 'err' + const readyLabel = ready?.ready ? 'ready' : ready ? 'not ready' : 'unknown' + + return ( +
+
+ + StreamStack + admin + + {error ? : } +
+ +
+ + + + + + +
+ +
+

This node

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Advertised address{cluster?.advertisedAddress ?? '—'}
Node epoch{cluster?.nodeEpoch ?? '—'}
Raft role{cluster ? (cluster.raft.isLeader ? 'leader' : 'follower') : '—'}
Registered{cluster ? String(cluster.registered) : '—'}
Apply success / fail + {cluster ? `${cluster.raft.applySuccessCount} / ${cluster.raft.applyFailCount}` : '—'} +
Destroyed object backlog{cluster?.destroyedObjectBacklog ?? '—'}
+
+ +
+

Nodes

+ {nodes.length === 0 ? ( +
No registered nodes
+ ) : ( + + + + + + + + + + + {nodes.map((n) => ( + + + + + + + ))} + +
NodeAdvertised addressEpoch
{n.nodeId}{n.advertisedAddress ?? '—'}{n.nodeEpoch}{n.local ? : null}
+ )} +
+ +
+

Raft peers

+ {!cluster?.raft.peers || cluster.raft.peers.length === 0 ? ( +
Peer list only available on the raft leader
+ ) : ( + + + + + + + + {cluster.raft.peers.map((p) => ( + + + + ))} + +
Peer
{p}
+ )} +
+ +
+ {error + ? `Last error: ${error}` + : updatedAt + ? `Updated ${updatedAt.toLocaleTimeString()} · polling every ${POLL_INTERVAL_MS / 1000}s` + : 'Loading…'} +
+
+ ) +} diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx new file mode 100644 index 0000000..dff38a0 --- /dev/null +++ b/dashboard/src/main.tsx @@ -0,0 +1,5 @@ +import { render } from 'preact' +import { App } from './app' +import './styles.css' + +render(, document.getElementById('app')!) diff --git a/dashboard/src/styles.css b/dashboard/src/styles.css new file mode 100644 index 0000000..535454c --- /dev/null +++ b/dashboard/src/styles.css @@ -0,0 +1,217 @@ +:root { + --ss-font-base: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + --ss-font-mono: + "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; + --ss-bg: #0c0c0d; + --ss-bg-alt: #141416; + --ss-bg-elv: #19191c; + --ss-divider: #2b2b2f; + --ss-gutter: #202024; + --ss-text-1: #f5f5f3; + --ss-text-2: #a7a7a1; + --ss-brand: #ffffff; + --ss-brand-soft: rgb(255 255 255 / 0.14); + --ss-ok: #3fb27f; + --ss-warn: #d9a03f; + --ss-err: #d95757; + --ss-radius: 2px; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--ss-bg); + color: var(--ss-text-1); + font-family: var(--ss-font-base); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +.ss-app { + max-width: 1180px; + margin: 0 auto; + padding: 0 24px 64px; +} + +.ss-topbar { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 0; + border-bottom: 1px solid var(--ss-gutter); + margin-bottom: 24px; +} + +.ss-topbar svg { + width: 28px; + height: 28px; + flex: none; +} + +.ss-topbar .name { + font-weight: 600; + font-size: 15px; + letter-spacing: 0.01em; +} + +.ss-topbar .tag { + color: var(--ss-text-2); + font-size: 12px; + border: 1px solid var(--ss-divider); + border-radius: var(--ss-radius); + padding: 1px 8px; +} + +.ss-topbar .spacer { + flex: 1; +} + +.ss-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 12px; + margin-bottom: 24px; +} + +.ss-card { + background: var(--ss-bg-alt); + border: 1px solid var(--ss-gutter); + border-radius: var(--ss-radius); + padding: 14px 16px; +} + +.ss-card .label { + color: var(--ss-text-2); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 6px; +} + +.ss-card .value { + font-family: var(--ss-font-mono); + font-size: 20px; + font-weight: 500; +} + +.ss-card .value.small { + font-size: 14px; + word-break: break-all; +} + +.ss-section { + margin-bottom: 24px; +} + +.ss-section h2 { + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ss-text-2); + margin: 0 0 10px; +} + +table { + width: 100%; + border-collapse: collapse; + background: var(--ss-bg-alt); + border: 1px solid var(--ss-gutter); + border-radius: var(--ss-radius); + overflow: hidden; +} + +th, +td { + text-align: left; + padding: 9px 14px; + border-bottom: 1px solid var(--ss-gutter); + font-size: 13px; +} + +th { + color: var(--ss-text-2); + font-weight: 500; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; + background: var(--ss-bg-elv); +} + +tr:last-child td { + border-bottom: none; +} + +td.mono { + font-family: var(--ss-font-mono); +} + +.ss-pill { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-family: var(--ss-font-mono); + border: 1px solid var(--ss-divider); + border-radius: var(--ss-radius); + padding: 2px 10px; + color: var(--ss-text-2); +} + +.ss-pill .dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--ss-text-2); +} + +.ss-pill.ok { + color: var(--ss-ok); + border-color: rgb(63 178 127 / 0.35); +} + +.ss-pill.ok .dot { + background: var(--ss-ok); +} + +.ss-pill.err { + color: var(--ss-err); + border-color: rgb(217 87 87 / 0.35); +} + +.ss-pill.err .dot { + background: var(--ss-err); +} + +.ss-pill.warn { + color: var(--ss-warn); + border-color: rgb(217 160 63 / 0.35); +} + +.ss-pill.warn .dot { + background: var(--ss-warn); +} + +.ss-empty { + color: var(--ss-text-2); + padding: 24px; + text-align: center; + background: var(--ss-bg-alt); + border: 1px solid var(--ss-gutter); + border-radius: var(--ss-radius); +} + +.ss-footer { + color: var(--ss-text-2); + font-size: 12px; + padding-top: 16px; + border-top: 1px solid var(--ss-gutter); +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 0000000..3500bc1 --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "jsxImportSource": "preact", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/dashboard/tsconfig.tsbuildinfo b/dashboard/tsconfig.tsbuildinfo new file mode 100644 index 0000000..57f6982 --- /dev/null +++ b/dashboard/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/api.ts","./src/app.tsx","./src/main.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 0000000..87e440a --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +export default defineConfig({ + plugins: [preact()], + base: './', + build: { + outDir: '../server/src/main/resources/admin/dist', + emptyOutDir: true, + }, + server: { + proxy: { + '/admin': 'http://127.0.0.1:9090', + '/ready': 'http://127.0.0.1:9090', + '/health': 'http://127.0.0.1:9090', + }, + }, +}) diff --git a/docs/pages/deployment.md b/docs/pages/deployment.md index c64c5e9..5869725 100644 --- a/docs/pages/deployment.md +++ b/docs/pages/deployment.md @@ -35,4 +35,25 @@ java -jar frontend/ds/server/target/ds-server.jar --topo harness/local/topo.clus URI forms: `0@s3://bucket?region=us-east-1`, `-2@file:///path`, `0@mem://bucket` (for tests only). Omit `global.wal` to default WAL to the storage URI when storage is S3. -`--topo` requires `--node-id`. CLI flags override topo values and export credentials in the environment that starts the process. \ No newline at end of file +`--topo` requires `--node-id`. CLI flags override topo values and export credentials in the environment that starts the process. + +## Admin and health + +Every node serves an admin plane on a separate port, independent of the protocol facade. Default port is `9090`; set it per node with `adminPort` in the topo, `--admin-port` on the CLI, or `ADMIN_PORT` in the Docker image. Set `0` to disable. + +| Endpoint | Description | +|----------|-------------| +| `GET /health` | Liveness. `200 ok` as soon as the process is up, before raft and storage are ready | +| `GET /ready` | Readiness. `200` once raft has a leader, the node is registered, and storage started; `503` otherwise | +| `GET /admin/cluster` | Cluster and raft status: leader, applied index, stream and object counts | +| `GET /admin/nodes` | Registered nodes with advertised addresses and epochs | +| `GET /admin/streams/{name}` | Owner and metadata for a stream | +| `POST /admin/peers` | Add a raft peer: `{"peer": "host:port"}` (leader only) | +| `DELETE /admin/peers/{peer}` | Remove a raft peer (leader only) | +| `POST /admin/transfer-leader` | Transfer raft leadership: `{"peer": "host:port"}` (leader only) | + +The admin port also serves a read-only dashboard at `/` with cluster, node, and raft status. + +Point load balancer target-group health checks at `/ready`. On shutdown the node flips `/ready` to `503` first and waits `shutdownDrainSec` (topo config, default `0`) before closing, so the load balancer drains in-flight traffic. + +At startup the node verifies the storage and WAL buckets are reachable, retrying with backoff for up to ten attempts before failing; `/health` responds while it retries. \ No newline at end of file diff --git a/frontend/ds/server/src/main/java/io/streamstack/server/ds/DurableStreamsServer.java b/frontend/ds/server/src/main/java/io/streamstack/server/ds/DurableStreamsServer.java index 1aa5e41..7561f0e 100644 --- a/frontend/ds/server/src/main/java/io/streamstack/server/ds/DurableStreamsServer.java +++ b/frontend/ds/server/src/main/java/io/streamstack/server/ds/DurableStreamsServer.java @@ -79,6 +79,8 @@ public StreamStackNode node() { @Override public void close() { + node.drainBeforeShutdown(); + try { app.stop(); } catch (Exception ignored) { diff --git a/frontend/ds/server/src/test/java/io/streamstack/server/ds/MultiNodeRoutingIntegrationTest.java b/frontend/ds/server/src/test/java/io/streamstack/server/ds/MultiNodeRoutingIntegrationTest.java index 40c6c37..66659cb 100644 --- a/frontend/ds/server/src/test/java/io/streamstack/server/ds/MultiNodeRoutingIntegrationTest.java +++ b/frontend/ds/server/src/test/java/io/streamstack/server/ds/MultiNodeRoutingIntegrationTest.java @@ -120,6 +120,7 @@ private static ServerConfig nodeConfig( .nodeEpoch(nodeId) .httpHost("127.0.0.1") .httpPort(httpPort) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(peers) diff --git a/frontend/ds/server/src/test/java/io/streamstack/server/ds/S3MinioIntegrationTest.java b/frontend/ds/server/src/test/java/io/streamstack/server/ds/S3MinioIntegrationTest.java index f94cb37..1c6c781 100644 --- a/frontend/ds/server/src/test/java/io/streamstack/server/ds/S3MinioIntegrationTest.java +++ b/frontend/ds/server/src/test/java/io/streamstack/server/ds/S3MinioIntegrationTest.java @@ -45,6 +45,7 @@ void createAppendReadRestartRecovery() throws Exception { .nodeEpoch(1) .httpHost("127.0.0.1") .httpPort(httpPort) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(List.of("127.0.0.1:" + raftPort)) @@ -82,6 +83,7 @@ void createAppendReadRestartRecovery() throws Exception { .nodeEpoch(2) .httpHost("127.0.0.1") .httpPort(TestPorts.freePort()) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(List.of("127.0.0.1:" + raftPort)) diff --git a/frontend/ds/server/src/test/java/io/streamstack/server/ds/StreamStackServerIntegrationTest.java b/frontend/ds/server/src/test/java/io/streamstack/server/ds/StreamStackServerIntegrationTest.java index 3b4203b..2aa6824 100644 --- a/frontend/ds/server/src/test/java/io/streamstack/server/ds/StreamStackServerIntegrationTest.java +++ b/frontend/ds/server/src/test/java/io/streamstack/server/ds/StreamStackServerIntegrationTest.java @@ -32,6 +32,7 @@ void createAppendReadCloseEndToEnd() throws Exception { .nodeEpoch(1) .httpHost("127.0.0.1") .httpPort(httpPort) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(List.of("127.0.0.1:" + raftPort)) diff --git a/frontend/s2/server/src/main/java/io/streamstack/s2/server/S2Server.java b/frontend/s2/server/src/main/java/io/streamstack/s2/server/S2Server.java index b24f5d4..c4b59f1 100644 --- a/frontend/s2/server/src/main/java/io/streamstack/s2/server/S2Server.java +++ b/frontend/s2/server/src/main/java/io/streamstack/s2/server/S2Server.java @@ -120,6 +120,8 @@ public StreamStackNode node() { @Override public void close() { + node.drainBeforeShutdown(); + try { app.stop(); } catch (Exception ignored) { diff --git a/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ClientIntegrationTest.java b/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ClientIntegrationTest.java index e4ed813..e0e7bfb 100644 --- a/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ClientIntegrationTest.java +++ b/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ClientIntegrationTest.java @@ -45,6 +45,7 @@ void clientRoundTrip() throws Exception { .nodeEpoch(1) .httpHost("127.0.0.1") .httpPort(httpPort) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(List.of("127.0.0.1:" + raftPort)) diff --git a/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ServerIntegrationTest.java b/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ServerIntegrationTest.java index 69b6bd6..84192a6 100644 --- a/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ServerIntegrationTest.java +++ b/frontend/s2/server/src/test/java/io/streamstack/s2/server/S2ServerIntegrationTest.java @@ -46,6 +46,7 @@ void basinsStreamsAndRecordsEndToEnd() throws Exception { .nodeEpoch(1) .httpHost("127.0.0.1") .httpPort(httpPort) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(List.of("127.0.0.1:" + raftPort)) diff --git a/harness/README.md b/harness/README.md index ed7b61b..7dfdcdd 100644 --- a/harness/README.md +++ b/harness/README.md @@ -15,7 +15,7 @@ docker compose --env-file harness/local/.env \ up -d --build ``` -Swap `docker-compose.ds.yml` for `docker-compose.s2.yml` to run S2 instead. Node listens on `127.0.0.1:4437`. +Swap `docker-compose.ds.yml` for `docker-compose.s2.yml` to run S2 instead. Node listens on `127.0.0.1:4437`. Admin plane and dashboard: http://127.0.0.1:9090 (`/health`, `/ready`, `/admin/*`). [BENCH.md](BENCH.md) has DS and S2 smoke/load tests. MinIO console: http://127.0.0.1:9001 (`minioadmin` / `minioadmin`). @@ -67,7 +67,7 @@ java -jar frontend/ds/server/target/ds-server.jar \ ## Multi-node (local) -`docker-compose.cluster.ds.yml` starts three DS processes against `local/topo.cluster.yaml`. Each service is a fixed `NODE_ID` (1 / 2 / 3). Host network so they bind 4437–4439 / 8091–8093 as in the topo. +`docker-compose.cluster.ds.yml` starts three DS processes against `local/topo.cluster.yaml`. Each service is a fixed `NODE_ID` (1 / 2 / 3). Host network so they bind 4437–4439 / 8091–8093 / admin 9091–9093 as in the topo. ```bash docker compose --env-file harness/local/.env \ diff --git a/harness/aws/docker-compose.ds.yml b/harness/aws/docker-compose.ds.yml index bd22119..37afe77 100644 --- a/harness/aws/docker-compose.ds.yml +++ b/harness/aws/docker-compose.ds.yml @@ -24,6 +24,7 @@ services: ports: - "4437:4437" - "8091:8091" + - "9090:9090" restart: on-failure volumes: diff --git a/harness/aws/docker-compose.s2.yml b/harness/aws/docker-compose.s2.yml index fa6efae..4c7fb20 100644 --- a/harness/aws/docker-compose.s2.yml +++ b/harness/aws/docker-compose.s2.yml @@ -24,6 +24,7 @@ services: ports: - "4437:4437" - "8091:8091" + - "9090:9090" restart: on-failure volumes: diff --git a/harness/aws/topo.cluster.yaml b/harness/aws/topo.cluster.yaml index ba7eb15..8f8ae93 100644 --- a/harness/aws/topo.cluster.yaml +++ b/harness/aws/topo.cluster.yaml @@ -1,4 +1,4 @@ -# Three nodes on one host (ports 4437–4439 / 8091–8093). Real S3. DATA_BUCKET / WAL_BUCKET override the URIs. +# Three nodes on one host (ports 4437–4439 / 8091–8093 / admin 9091–9093). Real S3. DATA_BUCKET / WAL_BUCKET override the URIs. global: clusterName: aws storage: 0@s3://streamstack-data?region=us-east-1 @@ -11,15 +11,18 @@ nodes: - host: 127.0.0.1 nodeId: 1 httpPort: 4437 + adminPort: 9091 raftPort: 8091 dataDir: /tmp/streamstack/aws/n1 - host: 127.0.0.1 nodeId: 2 httpPort: 4438 + adminPort: 9092 raftPort: 8092 dataDir: /tmp/streamstack/aws/n2 - host: 127.0.0.1 nodeId: 3 httpPort: 4439 + adminPort: 9093 raftPort: 8093 dataDir: /tmp/streamstack/aws/n3 diff --git a/harness/docker/entrypoint.sh b/harness/docker/entrypoint.sh index 60ab46b..30497fd 100755 --- a/harness/docker/entrypoint.sh +++ b/harness/docker/entrypoint.sh @@ -11,6 +11,9 @@ fi if [ -n "${NODE_ID:-}" ]; then set -- "$@" --node-id "$NODE_ID" fi +if [ -n "${ADMIN_PORT:-}" ]; then + set -- "$@" --admin-port "$ADMIN_PORT" +fi if [ -n "${DATA_DIR:-}" ]; then set -- "$@" --data-dir "$DATA_DIR" fi diff --git a/harness/local/docker-compose.ds.yml b/harness/local/docker-compose.ds.yml index b968ba3..7539baf 100644 --- a/harness/local/docker-compose.ds.yml +++ b/harness/local/docker-compose.ds.yml @@ -19,9 +19,12 @@ services: volumes: - .:/opt/streamstack/topo:ro - node1-data:/tmp/streamstack + extra_hosts: + - "host.docker.internal:host-gateway" ports: - "4437:4437" - "8091:8091" + - "9090:9090" restart: on-failure volumes: diff --git a/harness/local/docker-compose.s2.yml b/harness/local/docker-compose.s2.yml index aac08e1..69b8281 100644 --- a/harness/local/docker-compose.s2.yml +++ b/harness/local/docker-compose.s2.yml @@ -19,9 +19,12 @@ services: volumes: - .:/opt/streamstack/topo:ro - node1-data:/tmp/streamstack + extra_hosts: + - "host.docker.internal:host-gateway" ports: - "4437:4437" - "8091:8091" + - "9090:9090" restart: on-failure volumes: diff --git a/harness/local/topo.cluster.yaml b/harness/local/topo.cluster.yaml index 9be11c2..74894ee 100644 --- a/harness/local/topo.cluster.yaml +++ b/harness/local/topo.cluster.yaml @@ -1,4 +1,4 @@ -# Three nodes on one host (ports 4437–4439 / 8091–8093). MinIO at 127.0.0.1:9000 (host network). +# Three nodes on one host (ports 4437–4439 / 8091–8093 / admin 9091–9093). MinIO at 127.0.0.1:9000 (host network). global: clusterName: cluster storage: 0@s3://streams-data?region=us-east-1&endpoint=http://127.0.0.1:9000&pathStyle=true @@ -18,15 +18,18 @@ nodes: - host: 127.0.0.1 nodeId: 1 httpPort: 4437 + adminPort: 9091 raftPort: 8091 dataDir: /tmp/streamstack/cluster/n1 - host: 127.0.0.1 nodeId: 2 httpPort: 4438 + adminPort: 9092 raftPort: 8092 dataDir: /tmp/streamstack/cluster/n2 - host: 127.0.0.1 nodeId: 3 httpPort: 4439 + adminPort: 9093 raftPort: 8093 dataDir: /tmp/streamstack/cluster/n3 diff --git a/metadata/src/main/java/io/streamstack/metadata/raft/MetadataNode.java b/metadata/src/main/java/io/streamstack/metadata/raft/MetadataNode.java index 30c4135..91bf656 100644 --- a/metadata/src/main/java/io/streamstack/metadata/raft/MetadataNode.java +++ b/metadata/src/main/java/io/streamstack/metadata/raft/MetadataNode.java @@ -293,6 +293,10 @@ public CompletableFuture addPeer(PeerId peer) { return future; } + public Status transferLeader(PeerId peer) { + return node.transferLeadershipTo(peer); + } + public CompletableFuture removePeer(PeerId peer) { CompletableFuture future = new CompletableFuture<>(); diff --git a/server/pom.xml b/server/pom.xml index e90031f..d065241 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -13,6 +13,10 @@ server streamstack-server + + false + + io.streamstack @@ -22,6 +26,10 @@ io.streamstack metadata + + io.javalin + javalin + com.fasterxml.jackson.core jackson-databind @@ -48,6 +56,48 @@ + + com.github.eirslett + frontend-maven-plugin + 1.15.1 + + ${project.basedir}/../dashboard + ${project.build.directory}/frontend + ${dashboard.skip} + + + + install-node-and-npm + + install-node-and-npm + + generate-resources + + v22.12.0 + + + + npm-install + + npm + + generate-resources + + install + + + + npm-build + + npm + + generate-resources + + run build + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/server/src/main/java/io/streamstack/server/AdminServer.java b/server/src/main/java/io/streamstack/server/AdminServer.java new file mode 100644 index 0000000..c635a7b --- /dev/null +++ b/server/src/main/java/io/streamstack/server/AdminServer.java @@ -0,0 +1,300 @@ +package io.streamstack.server; + +import com.alipay.sofa.jraft.entity.PeerId; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.javalin.Javalin; +import io.javalin.http.Context; +import io.javalin.http.staticfiles.Location; +import io.streamstack.metadata.raft.MetadataHealth; +import io.streamstack.metadata.raft.MetadataNode; +import io.streamstack.server.model.Owner; +import io.streamstack.server.model.StreamMeta; +import io.streamstack.server.model.config.ServerConfig; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class AdminServer implements AutoCloseable { + + private static final Logger LOGGER = LoggerFactory.getLogger(AdminServer.class); + + private static final long PEER_CHANGE_TIMEOUT_SEC = 30; + + private final StreamStackNode node; + private final ServerConfig config; + private final ObjectMapper mapper = new ObjectMapper(); + private final Javalin app; + + private final AtomicBoolean started = new AtomicBoolean(false); + + public AdminServer(StreamStackNode node) { + this.node = Objects.requireNonNull(node, "node"); + this.config = node.config(); + + this.app = Javalin.create(cfg -> { + cfg.showJavalinBanner = false; + cfg.useVirtualThreads = true; + + if (Objects.nonNull(AdminServer.class.getResource("/admin/dist/index.html"))) { + cfg.staticFiles.add(staticFiles -> { + staticFiles.directory = "/admin/dist"; + staticFiles.location = Location.CLASSPATH; + }); + } + }); + + app.get("/health", this::health); + app.get("/ready", this::ready); + app.get("/admin/cluster", this::cluster); + app.get("/admin/nodes", this::nodes); + app.get("/admin/streams/", this::stream); + app.post("/admin/peers", this::addPeer); + app.delete("/admin/peers/{peer}", this::removePeer); + app.post("/admin/transfer-leader", this::transferLeader); + + app.exception(Exception.class, (e, ctx) -> { + LOGGER.warn("admin request failed: {} {}", ctx.method(), ctx.path(), e); + error(ctx, 500, e.getMessage()); + }); + } + + public void start() { + if (!started.compareAndSet(false, true)) { + return; + } + + app.start(config.httpHost(), config.adminPort()); + LOGGER.info("streamstack admin server started nodeId={} admin={}:{}", + config.nodeId(), config.httpHost(), config.adminPort()); + } + + private void health(Context ctx) { + ctx.status(200); + ctx.result("ok"); + } + + private void ready(Context ctx) { + MetadataNode metadata = node.metadataNode(); + boolean leaderKnown = Objects.nonNull(metadata.leaderId()); + boolean registered = metadata.isRegistered(); + boolean ready = node.isReady() && leaderKnown && registered; + Map body = new LinkedHashMap<>(); + + body.put("ready", ready); + body.put("started", node.isReady()); + body.put("leaderKnown", leaderKnown); + body.put("registered", registered); + json(ctx, ready ? 200 : 503, body); + } + + private void cluster(Context ctx) { + MetadataNode metadata = node.metadataNode(); + MetadataHealth health = metadata.health(); + Map raft = new LinkedHashMap<>(); + + raft.put("leader", health.leaderId()); + raft.put("isLeader", health.isLeader()); + raft.put("appliedIndex", health.appliedIndex()); + raft.put("applySuccessCount", health.applySuccessCount()); + raft.put("applyFailCount", health.applyFailCount()); + raft.put("peers", peers(metadata)); + + Map body = new LinkedHashMap<>(); + + body.put("clusterId", config.clusterId()); + body.put("nodeId", config.nodeId()); + body.put("nodeEpoch", metadata.nodeEpoch()); + body.put("advertisedAddress", config.httpAddress()); + body.put("registered", metadata.isRegistered()); + body.put("raft", raft); + body.put("streamCount", health.streamCount()); + body.put("objectCount", health.objectCount()); + body.put("destroyedObjectBacklog", health.destroyedBacklog()); + json(ctx, 200, body); + } + + private void nodes(Context ctx) { + MetadataNode metadata = node.metadataNode(); + List> result = metadata.stateMachine().read(() -> { + Map epochs = metadata.stateMachine().streamControlManager().nodeEpochs(); + Map addresses = metadata.stateMachine().streamControlManager().nodeAddresses(); + List> list = new ArrayList<>(); + + for (Map.Entry entry : epochs.entrySet()) { + Map item = new LinkedHashMap<>(); + + item.put("nodeId", entry.getKey()); + item.put("nodeEpoch", entry.getValue()); + item.put("advertisedAddress", addresses.get(entry.getKey())); + item.put("local", entry.getKey() == config.nodeId()); + list.add(item); + } + + return list; + }); + + json(ctx, 200, Map.of("nodes", result)); + } + + private void stream(Context ctx) throws Exception { + String name = "/" + ctx.pathParam("name"); + Owner owner = node.service().ownership().ownerOf(name); + Map body = new LinkedHashMap<>(); + + body.put("name", name); + body.put("streamId", owner.streamId().isPresent() ? owner.streamId().getAsLong() : null); + body.put("ownerLocal", owner.local()); + body.put("ownerNodeId", owner.local() ? config.nodeId() : owner.ownerNodeId()); + body.put("ownerAdvertisedAddress", + owner.local() ? config.httpAddress() : owner.ownerAdvertisedAddress()); + + if (owner.local()) { + Optional meta = node.service().lifecycle().head(name); + + if (meta.isEmpty()) { + error(ctx, 404, "stream not found: " + name); + return; + } + + body.put("meta", metaBody(meta.get())); + } + + json(ctx, 200, body); + } + + private void addPeer(Context ctx) throws Exception { + PeerId peer = requirePeer(ctx); + + if (Objects.isNull(peer) || !requireLeader(ctx)) { + return; + } + + node.metadataNode().addPeer(peer).get(PEER_CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS); + json(ctx, 200, Map.of("added", peer.toString())); + } + + private void removePeer(Context ctx) throws Exception { + PeerId peer = new PeerId(); + + if (!peer.parse(ctx.pathParam("peer"))) { + error(ctx, 400, "invalid peer, expected host:port"); + return; + } + + if (!requireLeader(ctx)) { + return; + } + + node.metadataNode().removePeer(peer).get(PEER_CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS); + json(ctx, 200, Map.of("removed", peer.toString())); + } + + private void transferLeader(Context ctx) throws Exception { + PeerId peer = requirePeer(ctx); + + if (Objects.isNull(peer) || !requireLeader(ctx)) { + return; + } + + var status = node.metadataNode().transferLeader(peer); + + if (!status.isOk()) { + error(ctx, 500, status.getErrorMsg()); + return; + } + + json(ctx, 200, Map.of("transferredTo", peer.toString())); + } + + private PeerId requirePeer(Context ctx) throws Exception { + JsonNode json = mapper.readTree(ctx.bodyInputStream()); + JsonNode value = json.get("peer"); + + if (Objects.isNull(value) || !value.isTextual()) { + error(ctx, 400, "body must be {\"peer\": \"host:port\"}"); + return null; + } + + PeerId peer = new PeerId(); + + if (!peer.parse(value.asText())) { + error(ctx, 400, "invalid peer, expected host:port"); + return null; + } + + return peer; + } + + private boolean requireLeader(Context ctx) { + MetadataNode metadata = node.metadataNode(); + + if (metadata.isLeader()) { + return true; + } + + PeerId leader = metadata.leaderId(); + Map body = new LinkedHashMap<>(); + + body.put("error", "not raft leader"); + body.put("leader", Objects.isNull(leader) ? null : leader.toString()); + json(ctx, 409, body); + return false; + } + + private static List peers(MetadataNode metadata) { + try { + return metadata.raftNode().listPeers().stream().map(PeerId::toString).toList(); + } catch (Exception e) { + return null; + } + } + + private static Map metaBody(StreamMeta meta) { + Map body = new LinkedHashMap<>(); + + body.put("streamId", meta.streamId()); + body.put("contentType", meta.contentType()); + body.put("ttlSeconds", meta.ttlSeconds()); + body.put("expiresAt", Objects.isNull(meta.expiresAt()) ? null : meta.expiresAt().toString()); + body.put("startOffset", Objects.toString(meta.startOffset(), null)); + body.put("nextOffset", Objects.toString(meta.nextOffset(), null)); + body.put("closed", meta.closed()); + return body; + } + + private void json(Context ctx, int status, Object body) { + try { + ctx.status(status); + ctx.contentType("application/json"); + ctx.result(mapper.writeValueAsBytes(body)); + } catch (Exception e) { + ctx.status(500); + ctx.result("{\"error\":\"serialization failed\"}"); + } + } + + private void error(Context ctx, int status, String message) { + json(ctx, status, Map.of("error", Objects.toString(message, "internal error"))); + } + + @Override + public void close() { + try { + app.stop(); + } catch (Exception ignored) { + } + + started.set(false); + } +} diff --git a/server/src/main/java/io/streamstack/server/StreamStackNode.java b/server/src/main/java/io/streamstack/server/StreamStackNode.java index 56ef2ad..a0aaa5d 100644 --- a/server/src/main/java/io/streamstack/server/StreamStackNode.java +++ b/server/src/main/java/io/streamstack/server/StreamStackNode.java @@ -43,6 +43,10 @@ public final class StreamStackNode implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(StreamStackNode.class); + private static final int STORAGE_READINESS_MAX_ATTEMPTS = 10; + private static final long STORAGE_READINESS_INITIAL_BACKOFF_MS = 1_000; + private static final long STORAGE_READINESS_MAX_BACKOFF_MS = 30_000; + private final ServerConfig config; private final MetadataNode metadataNode; private final ObjectStorage objectStorage; @@ -53,8 +57,10 @@ public final class StreamStackNode implements AutoCloseable { private final S3StreamService streamService; private final RaftKVClient kvClient; private final StreamService service; + private final AdminServer adminServer; private final AtomicBoolean started = new AtomicBoolean(false); + private final AtomicBoolean ready = new AtomicBoolean(false); public StreamStackNode(ServerConfig config) throws Exception { this.config = Objects.requireNonNull(config, "config"); @@ -119,6 +125,7 @@ public StreamStackNode(ServerConfig config) throws Exception { RaftOwnershipService ownership = new RaftOwnershipService(metadataNode, streamService); this.service = new StreamService(streamService, streamService, streamService, ownership); + this.adminServer = config.adminEnabled() ? new AdminServer(this) : null; } private WalBundle createWal(String walUri, BucketURI dataBucket, ObjectStorage dataStorage) { @@ -174,16 +181,56 @@ public void start() throws Exception { return; } + if (Objects.nonNull(adminServer)) { + adminServer.start(); + } + + awaitObjectStorageReady(); metadataNode.awaitLeader(30, TimeUnit.SECONDS); metadataNode.awaitRegistered(30, TimeUnit.SECONDS); storage.startup(); compactionManager.start(); + ready.set(true); LOGGER.info( "streamstack node started nodeId={} advertised={} raft={}:{} storage={} wal={}", config.nodeId(), config.httpAddress(), config.raftHost(), config.raftPort(), config.storageUri(), config.resolveWalUri()); } + private void awaitObjectStorageReady() throws InterruptedException { + long backoffMs = STORAGE_READINESS_INITIAL_BACKOFF_MS; + + for (int attempt = 1; ; attempt++) { + boolean dataReady = storageReady(objectStorage); + boolean walReady = Objects.isNull(walObjectStorage) || storageReady(walObjectStorage); + + if (dataReady && walReady) { + return; + } + + if (attempt >= STORAGE_READINESS_MAX_ATTEMPTS) { + throw new IllegalStateException(String.format( + "object storage not ready after %d attempts (data=%b wal=%b), storage=%s wal=%s", + attempt, dataReady, walReady, config.storageUri(), config.resolveWalUri())); + } + + LOGGER.warn( + "object storage not ready (attempt {}/{}, data={} wal={}), retrying in {}ms", + attempt, STORAGE_READINESS_MAX_ATTEMPTS, dataReady, walReady, backoffMs); + Thread.sleep(backoffMs); + backoffMs = Math.min(backoffMs * 2, STORAGE_READINESS_MAX_BACKOFF_MS); + } + } + + private static boolean storageReady(ObjectStorage storage) { + try { + return storage.readinessCheck(); + } catch (Exception e) { + LOGGER.warn("object storage readiness check failed: {}", e.getMessage()); + return false; + } + } + public StreamService service() { return service; } @@ -208,8 +255,35 @@ public S3StreamService streamService() { return streamService; } + public boolean isReady() { + return ready.get(); + } + + public void markNotReady() { + ready.set(false); + } + + public void drainBeforeShutdown() { + ready.set(false); + int drainSec = config.shutdownDrainSec(); + + if (drainSec <= 0) { + return; + } + + LOGGER.info("draining for {}s before shutdown nodeId={}", drainSec, config.nodeId()); + + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(drainSec)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + @Override public void close() { + ready.set(false); + try { for (var stream : new ArrayList<>(streamService.openStreamSnapshot())) { try { @@ -250,6 +324,11 @@ public void close() { } metadataNode.close(); + + if (Objects.nonNull(adminServer)) { + adminServer.close(); + } + started.set(false); } diff --git a/server/src/main/java/io/streamstack/server/model/config/ClusterConfig.java b/server/src/main/java/io/streamstack/server/model/config/ClusterConfig.java index 008f3a8..b4ffee3 100644 --- a/server/src/main/java/io/streamstack/server/model/config/ClusterConfig.java +++ b/server/src/main/java/io/streamstack/server/model/config/ClusterConfig.java @@ -203,6 +203,7 @@ public static final class Node { private String host; private int nodeId; private Integer httpPort; + private Integer adminPort; private Integer raftPort; private String dataDir; public String getHost() { @@ -229,6 +230,14 @@ public void setHttpPort(Integer httpPort) { this.httpPort = httpPort; } + public Integer getAdminPort() { + return adminPort; + } + + public void setAdminPort(Integer adminPort) { + this.adminPort = adminPort; + } + public Integer getRaftPort() { return raftPort; } @@ -257,6 +266,10 @@ public Integer httpPort() { return httpPort; } + public Integer adminPort() { + return adminPort; + } + public Integer raftPort() { return raftPort; } diff --git a/server/src/main/java/io/streamstack/server/model/config/ServerConfig.java b/server/src/main/java/io/streamstack/server/model/config/ServerConfig.java index 5a46b0b..1b9312e 100644 --- a/server/src/main/java/io/streamstack/server/model/config/ServerConfig.java +++ b/server/src/main/java/io/streamstack/server/model/config/ServerConfig.java @@ -19,6 +19,7 @@ public final class ServerConfig { private final long nodeEpoch; private final String httpHost; private final int httpPort; + private final int adminPort; private final String raftHost; private final int raftPort; private final List raftPeers; @@ -30,6 +31,7 @@ public final class ServerConfig { private final int longPollTimeoutSec; private final int sseMaxDurationSec; private final int maxChunkSize; + private final int shutdownDrainSec; private final StreamConfig streamConfig; private final Map envs; @@ -38,6 +40,7 @@ private ServerConfig(Builder builder) { this.nodeEpoch = builder.nodeEpoch; this.httpHost = builder.httpHost; this.httpPort = builder.httpPort; + this.adminPort = builder.adminPort; this.raftHost = builder.raftHost; this.raftPort = builder.raftPort; this.raftPeers = List.copyOf(builder.raftPeers); @@ -49,6 +52,7 @@ private ServerConfig(Builder builder) { this.longPollTimeoutSec = builder.longPollTimeoutSec; this.sseMaxDurationSec = builder.sseMaxDurationSec; this.maxChunkSize = builder.maxChunkSize; + this.shutdownDrainSec = builder.shutdownDrainSec; this.streamConfig = builder.streamConfig.build(); this.envs = Map.copyOf(builder.envs); } @@ -73,6 +77,14 @@ public int httpPort() { return httpPort; } + public int adminPort() { + return adminPort; + } + + public boolean adminEnabled() { + return adminPort > 0; + } + public String raftHost() { return raftHost; } @@ -141,6 +153,10 @@ public int maxChunkSize() { return maxChunkSize; } + public int shutdownDrainSec() { + return shutdownDrainSec; + } + public StreamConfig streamConfig() { return streamConfig; } @@ -190,6 +206,7 @@ public static ServerConfig fromArgs(String[] args) { case "--node-epoch" -> builder.nodeEpoch(Long.parseLong(requireValue(args, ++i, arg))); case "--http-host" -> builder.httpHost(requireValue(args, ++i, arg)); case "--http-port" -> builder.httpPort(Integer.parseInt(requireValue(args, ++i, arg))); + case "--admin-port" -> builder.adminPort(Integer.parseInt(requireValue(args, ++i, arg))); case "--raft-host" -> builder.raftHost(requireValue(args, ++i, arg)); case "--raft-port" -> builder.raftPort(Integer.parseInt(requireValue(args, ++i, arg))); case "--peer" -> builder.addPeer(requireValue(args, ++i, arg)); @@ -199,6 +216,8 @@ public static ServerConfig fromArgs(String[] args) { case "--wal" -> builder.walUri(requireValue(args, ++i, arg)); case "--cluster-id" -> builder.clusterId(requireValue(args, ++i, arg)); case "--routing" -> builder.routingMode(RoutingMode.valueOf(requireValue(args, ++i, arg))); + case "--shutdown-drain-sec" -> + builder.shutdownDrainSec(Integer.parseInt(requireValue(args, ++i, arg))); case "--wal-cache-size" -> builder.streamConfig().walCacheSize(Long.parseLong(requireValue(args, ++i, arg))); case "--block-cache-size" -> builder.streamConfig().blockCacheSize(Long.parseLong(requireValue(args, ++i, arg))); @@ -230,6 +249,7 @@ public static final class Builder { private long nodeEpoch = System.currentTimeMillis(); private String httpHost = "127.0.0.1"; private int httpPort = 4437; + private int adminPort = 9090; private String raftHost = "127.0.0.1"; private int raftPort = 8091; private final List raftPeers = new ArrayList<>(); @@ -241,6 +261,7 @@ public static final class Builder { private int longPollTimeoutSec = 25; private int sseMaxDurationSec = 55; private int maxChunkSize = 64 * 1024; + private int shutdownDrainSec = 0; private final StreamConfig.Builder streamConfig = StreamConfig.builder(); private final Map envs = new LinkedHashMap<>(); public Builder nodeId(int nodeId) { @@ -263,6 +284,11 @@ public Builder httpPort(int httpPort) { return this; } + public Builder adminPort(int adminPort) { + this.adminPort = adminPort; + return this; + } + public Builder raftHost(String raftHost) { this.raftHost = Objects.requireNonNull(raftHost); return this; @@ -332,6 +358,11 @@ public Builder maxChunkSize(int maxChunkSize) { return this; } + public Builder shutdownDrainSec(int shutdownDrainSec) { + this.shutdownDrainSec = shutdownDrainSec; + return this; + } + public StreamConfig.Builder streamConfig() { return streamConfig; } @@ -362,6 +393,10 @@ public Builder applyTopology(ClusterConfig topology, int forNodeId) { this.httpPort = node.httpPort(); } + if (Objects.nonNull(node.adminPort())) { + this.adminPort = node.adminPort(); + } + this.raftHost = node.host(); if (Objects.nonNull(node.raftPort())) { @@ -407,6 +442,7 @@ public Builder applyConfigMap(Map config) { case "longPollTimeoutSec" -> longPollTimeoutSec(toInt(value)); case "sseMaxDurationSec" -> sseMaxDurationSec(toInt(value)); case "maxChunkSize" -> maxChunkSize(toInt(value)); + case "shutdownDrainSec" -> shutdownDrainSec(toInt(value)); case "clusterId" -> clusterId(String.valueOf(value)); default -> throw new IllegalArgumentException("unknown topology config key: " + key); } diff --git a/server/src/test/java/io/streamstack/server/AdminServerTest.java b/server/src/test/java/io/streamstack/server/AdminServerTest.java new file mode 100644 index 0000000..26b4b50 --- /dev/null +++ b/server/src/test/java/io/streamstack/server/AdminServerTest.java @@ -0,0 +1,193 @@ +package io.streamstack.server; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.streamstack.server.model.CreateCommand; +import io.streamstack.server.model.config.RoutingMode; +import io.streamstack.server.model.config.ServerConfig; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AdminServerTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @TempDir + Path tempDir; + + @Test + void adminPlaneServesHealthReadinessClusterNodesAndStreams() throws Exception { + int raftPort = freePort(); + int adminPort = freePort(); + ServerConfig config = ServerConfig.builder() + .nodeId(1) + .nodeEpoch(1) + .httpHost("127.0.0.1") + .httpPort(freePort()) + .adminPort(adminPort) + .raftHost("127.0.0.1") + .raftPort(raftPort) + .raftPeers(List.of("127.0.0.1:" + raftPort)) + .dataDir(tempDir.resolve("data").toFile()) + .objectDir(tempDir.resolve("objects").toFile()) + .routingMode(RoutingMode.LOCAL_ALWAYS) + .build(); + + HttpClient client = HttpClient.newHttpClient(); + + try (StreamStackNode node = new StreamStackNode(config)) { + node.start(); + + HttpResponse health = get(client, adminPort, "/health"); + + assertEquals(200, health.statusCode()); + assertEquals("ok", health.body()); + + HttpResponse ready = get(client, adminPort, "/ready"); + JsonNode readyBody = MAPPER.readTree(ready.body()); + + assertEquals(200, ready.statusCode()); + assertTrue(readyBody.get("ready").asBoolean()); + assertTrue(readyBody.get("leaderKnown").asBoolean()); + assertTrue(readyBody.get("registered").asBoolean()); + + HttpResponse cluster = get(client, adminPort, "/admin/cluster"); + JsonNode clusterBody = MAPPER.readTree(cluster.body()); + + assertEquals(200, cluster.statusCode()); + assertEquals(1, clusterBody.get("nodeId").asInt()); + assertTrue(clusterBody.get("raft").get("isLeader").asBoolean()); + assertEquals("127.0.0.1:" + raftPort, clusterBody.get("raft").get("leader").asText()); + assertEquals(0, clusterBody.get("streamCount").asInt()); + + HttpResponse nodes = get(client, adminPort, "/admin/nodes"); + JsonNode nodesBody = MAPPER.readTree(nodes.body()); + + assertEquals(200, nodes.statusCode()); + assertEquals(1, nodesBody.get("nodes").size()); + assertEquals(1, nodesBody.get("nodes").get(0).get("nodeId").asInt()); + assertTrue(nodesBody.get("nodes").get(0).get("local").asBoolean()); + + HttpResponse missing = get(client, adminPort, "/admin/streams/streams/demo"); + + assertEquals(404, missing.statusCode()); + + node.service().lifecycle().create(new CreateCommand( + "/streams/demo", "text/plain", null, null, false, new byte[0])); + + HttpResponse stream = get(client, adminPort, "/admin/streams/streams/demo"); + JsonNode streamBody = MAPPER.readTree(stream.body()); + + assertEquals(200, stream.statusCode()); + assertEquals("/streams/demo", streamBody.get("name").asText()); + assertTrue(streamBody.get("ownerLocal").asBoolean()); + assertEquals("text/plain", streamBody.get("meta").get("contentType").asText()); + assertFalse(streamBody.get("meta").get("closed").asBoolean()); + + node.markNotReady(); + + HttpResponse notReady = get(client, adminPort, "/ready"); + + assertEquals(503, notReady.statusCode()); + assertFalse(MAPPER.readTree(notReady.body()).get("ready").asBoolean()); + } + } + + @Test + void peerChangesRequireValidBody() throws Exception { + int raftPort = freePort(); + int adminPort = freePort(); + ServerConfig config = ServerConfig.builder() + .nodeId(1) + .nodeEpoch(1) + .httpHost("127.0.0.1") + .httpPort(freePort()) + .adminPort(adminPort) + .raftHost("127.0.0.1") + .raftPort(raftPort) + .raftPeers(List.of("127.0.0.1:" + raftPort)) + .dataDir(tempDir.resolve("data").toFile()) + .objectDir(tempDir.resolve("objects").toFile()) + .routingMode(RoutingMode.LOCAL_ALWAYS) + .build(); + + HttpClient client = HttpClient.newHttpClient(); + + try (StreamStackNode node = new StreamStackNode(config)) { + node.start(); + + HttpResponse badBody = client.send( + HttpRequest.newBuilder() + .uri(URI.create("http://127.0.0.1:" + adminPort + "/admin/peers")) + .POST(HttpRequest.BodyPublishers.ofString("{}")) + .build(), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(400, badBody.statusCode()); + + HttpResponse badPeer = client.send( + HttpRequest.newBuilder() + .uri(URI.create("http://127.0.0.1:" + adminPort + "/admin/peers")) + .POST(HttpRequest.BodyPublishers.ofString("{\"peer\":\"not a peer\"}")) + .build(), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(400, badPeer.statusCode()); + } + } + + @Test + void adminDisabledWhenPortIsZero() throws Exception { + int raftPort = freePort(); + ServerConfig config = ServerConfig.builder() + .nodeId(1) + .nodeEpoch(1) + .httpHost("127.0.0.1") + .httpPort(freePort()) + .adminPort(0) + .raftHost("127.0.0.1") + .raftPort(raftPort) + .raftPeers(List.of("127.0.0.1:" + raftPort)) + .dataDir(tempDir.resolve("data").toFile()) + .objectDir(tempDir.resolve("objects").toFile()) + .routingMode(RoutingMode.LOCAL_ALWAYS) + .build(); + + assertFalse(config.adminEnabled()); + + try (StreamStackNode node = new StreamStackNode(config)) { + node.start(); + assertTrue(node.isReady()); + } + } + + private static HttpResponse get(HttpClient client, int port, String path) throws Exception { + return client.send( + HttpRequest.newBuilder() + .uri(URI.create("http://127.0.0.1:" + port + path)) + .GET() + .build(), + HttpResponse.BodyHandlers.ofString()); + } + + private static int freePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } +} diff --git a/server/src/test/java/io/streamstack/server/StreamStackNodeServiceTest.java b/server/src/test/java/io/streamstack/server/StreamStackNodeServiceTest.java index 3cc612a..0c2a03b 100644 --- a/server/src/test/java/io/streamstack/server/StreamStackNodeServiceTest.java +++ b/server/src/test/java/io/streamstack/server/StreamStackNodeServiceTest.java @@ -34,6 +34,7 @@ void createAppendReadCloseViaServices() throws Exception { .nodeEpoch(1) .httpHost("127.0.0.1") .httpPort(freePort()) + .adminPort(0) .raftHost("127.0.0.1") .raftPort(raftPort) .raftPeers(List.of("127.0.0.1:" + raftPort))