diff --git a/.dockerignore b/.dockerignore index 8822421f..7210f97b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -81,6 +81,10 @@ docker-compose*.yml .github/ .pre-commit-config.yaml .codecov.yml +.goreleaser.yaml + +# GoReleaser artifacts +dist/ # Scripts scripts/ diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..28354ac9 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,52 @@ +name: Go Benchmark + +on: + push: + branches: + - main + - development + paths: + - 'backend/**' + pull_request: + branches: + - main + - development + paths: + - 'backend/**' + workflow_dispatch: + +permissions: + contents: write + deployments: write + +jobs: + benchmark: + name: Performance Regression Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.4' + cache-dependency-path: backend/go.sum + + - name: Run Benchmark + working-directory: backend + run: go test -bench=. -benchmem ./... | tee output.txt + + - name: Store Benchmark Result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: Go Benchmark + tool: 'go' + output-file-path: backend/output.txt + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + # Show alert with commit comment on detection of performance regression + alert-threshold: '150%' + comment-on-alert: true + fail-on-alert: false + # Enable Job Summary for PRs + summary-always: true diff --git a/.github/workflows/docker-lint.yml b/.github/workflows/docker-lint.yml new file mode 100644 index 00000000..be3b0c10 --- /dev/null +++ b/.github/workflows/docker-lint.yml @@ -0,0 +1,23 @@ +name: Docker Lint + +on: + push: + branches: [ main, development, 'feature/**' ] + paths: + - 'Dockerfile' + pull_request: + branches: [ main, development ] + paths: + - 'Dockerfile' + +jobs: + hadolint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + failure-threshold: warning diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index faebe353..ea51d1fe 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,8 +6,7 @@ on: - main - development - feature/beta-release - tags: - - 'v*.*.*' + # Note: Tags are handled by release-goreleaser.yml to avoid duplicate builds pull_request: branches: - main @@ -102,9 +101,6 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} @@ -212,28 +208,27 @@ jobs: - name: Pull Docker image run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - name: Run container + - name: Create Docker Network + run: docker network create cpmp-test-net + + - name: Run Upstream Service (whoami) + run: | + docker run -d \ + --name whoami \ + --network cpmp-test-net \ + traefik/whoami + + - name: Run CPMP Container run: | docker run -d \ --name test-container \ + --network cpmp-test-net \ -p 8080:8080 \ + -p 80:80 \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - name: Test health endpoint (retries) - run: | - set +e - for i in $(seq 1 30); do - code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000") - if [ "$code" = "200" ]; then - echo "β Health check passed on attempt $i" - exit 0 - fi - echo "Attempt $i/30: health not ready (code=$code); waiting..." - sleep 2 - done - echo "β Health check failed after retries" - docker logs test-container || true - exit 1 + - name: Run Integration Test + run: ./scripts/integration-test.sh - name: Check container logs if: always() @@ -241,7 +236,10 @@ jobs: - name: Stop container if: always() - run: docker stop test-container && docker rm test-container + run: | + docker stop test-container whoami || true + docker rm test-container whoami || true + docker network rm cpmp-test-net || true - name: Create test summary if: always() @@ -249,4 +247,4 @@ jobs: echo "## π§ͺ Docker Image Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "- **Health Check**: ${{ job.status == 'success' && 'β Passed' || 'β Failed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Integration Test**: ${{ job.status == 'success' && 'β Passed' || 'β Failed' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 0da907f3..b9dc11df 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -22,8 +22,10 @@ jobs: - name: Run Go tests id: go-tests working-directory: backend + env: + CGO_ENABLED: 1 run: | - go test -v -coverprofile=coverage.out ./... 2>&1 | tee test-output.txt + go test -race -v -coverprofile=coverage.out ./... 2>&1 | tee test-output.txt exit ${PIPESTATUS[0]} - name: Go Test Summary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 097aae9e..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*.*.*' - -permissions: - contents: write - packages: write - -jobs: - build-frontend: - name: Build Frontend - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - with: - node-version: '24.11.1' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install Dependencies - working-directory: frontend - run: npm ci - - - name: Build - working-directory: frontend - run: npm run build - - - name: Archive Frontend - working-directory: frontend - run: tar -czf ../frontend-dist.tar.gz dist/ - - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 - with: - name: frontend-dist - path: frontend-dist.tar.gz - - build-backend: - name: Build Backend - runs-on: ubuntu-latest - strategy: - matrix: - goos: [linux] - goarch: [amd64, arm64] - steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 - with: - go-version: '1.25.4' - - - name: Build - working-directory: backend - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: 1 - run: | - # Install dependencies for CGO (sqlite) - if [ "${{ matrix.goarch }}" = "arm64" ]; then - sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu - export CC=aarch64-linux-gnu-gcc - fi - - go build -ldflags "-s -w -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${{ github.ref_name }}" -o ../cpmp-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/api - - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 - with: - name: backend-${{ matrix.goos }}-${{ matrix.goarch }} - path: cpmp-${{ matrix.goos }}-${{ matrix.goarch }} - - build-caddy: - name: Build Caddy - runs-on: ubuntu-latest - strategy: - matrix: - goos: [linux] - goarch: [amd64, arm64] - steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 - with: - go-version: '1.25.4' - - - name: Install xcaddy - run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest - - - name: Build Caddy - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - run: | - xcaddy build v2.9.1 \ - --replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \ - --replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \ - --output caddy-${{ matrix.goos }}-${{ matrix.goarch }} - - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 - with: - name: caddy-${{ matrix.goos }}-${{ matrix.goarch }} - path: caddy-${{ matrix.goos }}-${{ matrix.goarch }} - - create-release: - name: Create Release - needs: [build-frontend, build-backend, build-caddy] - runs-on: ubuntu-latest - steps: - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - path: artifacts - - - name: Display structure of downloaded files - run: ls -R artifacts - - - name: Create GitHub Release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 - with: - files: | - artifacts/frontend-dist/frontend-dist.tar.gz - artifacts/backend-linux-amd64/cpmp-linux-amd64 - artifacts/backend-linux-arm64/cpmp-linux-arm64 - artifacts/caddy-linux-amd64/caddy-linux-amd64 - artifacts/caddy-linux-arm64/caddy-linux-arm64 - generate_release_notes: true - prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} - token: ${{ secrets.CPMP_TOKEN }} - - build-and-publish: - needs: create-release - uses: ./.github/workflows/docker-publish.yml # Reusable workflow present; path validated - secrets: inherit diff --git a/.gitignore b/.gitignore index 931a8623..bcf26f92 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,9 @@ backend/data/caddy/ # Docker docker-compose.override.yml +# GoReleaser +dist/ + # Testing coverage/ coverage.out diff --git a/Dockerfile b/Dockerfile index 4bca8994..ec1aec2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,12 +43,15 @@ WORKDIR /app/backend # Install build dependencies # xx-apk installs packages for the TARGET architecture ARG TARGETPLATFORM +# hadolint ignore=DL3018 RUN apk add --no-cache clang lld +# hadolint ignore=DL3018,DL3059 RUN xx-apk add --no-cache gcc musl-dev sqlite-dev # Install Delve (cross-compile for target) # Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling. # We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage. +# hadolint ignore=DL3059,DL4006 RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest && \ DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \ if [ -n "$DLV_PATH" ] && [ "$DLV_PATH" != "/go/bin/dlv" ]; then \ @@ -86,7 +89,9 @@ ARG TARGETOS ARG TARGETARCH ARG CADDY_VERSION +# hadolint ignore=DL3018 RUN apk add --no-cache git +# hadolint ignore=DL3062 RUN --mount=type=cache,target=/go/pkg/mod \ go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest @@ -105,6 +110,7 @@ FROM ${CADDY_IMAGE} WORKDIR /app # Install runtime dependencies for CPM+ (no bash needed) +# hadolint ignore=DL3018 RUN apk --no-cache add ca-certificates sqlite-libs tzdata curl \ && apk --no-cache upgrade diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 21bc7d40..27de4874 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -13,23 +13,61 @@ linters: - ineffassign - staticcheck - unused - - revive + - errcheck settings: gocritic: enabled-tags: - diagnostic - - experimental - - opinionated - performance - - style disabled-checks: - whyNoLint - wrapperFunc + - hugeParam + - rangeValCopy + - ifElseChain + - appendCombine + - appendAssign + - commentedOutCode + - sprintfQuotedString govet: enable: - shadow - revive: - rules: - - name: exported - severity: warning + errcheck: + exclude-functions: + # Ignore deferred close errors - these are intentional + - (io.Closer).Close + - (*os.File).Close + - (net/http.ResponseWriter).Write + - (*encoding/json.Encoder).Encode + - (*encoding/json.Decoder).Decode + # Test utilities + - os.Setenv + - os.Unsetenv + - os.RemoveAll + - os.MkdirAll + - os.WriteFile + - os.Remove + - (*gorm.io/gorm.DB).AutoMigrate + + exclusions: + generated: lax + presets: + - comments + rules: + # Exclude some linters from running on tests + - path: _test\.go + linters: + - errcheck + - gosec + - govet + - ineffassign + - staticcheck + # Exclude gosec file permission warnings - 0644/0755 are intentional for config/data dirs + - linters: + - gosec + text: "G301:|G304:|G306:|G104:|G110:|G305:|G602:" + # Exclude shadow warnings in specific patterns + - linters: + - govet + text: "shadows declaration" diff --git a/backend/internal/api/handlers/certificate_handler.go b/backend/internal/api/handlers/certificate_handler.go index ac285b30..861ced0a 100644 --- a/backend/internal/api/handlers/certificate_handler.go +++ b/backend/internal/api/handlers/certificate_handler.go @@ -65,14 +65,14 @@ func (h *CertificateHandler) Upload(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open cert file"}) return } - defer certSrc.Close() + defer func() { _ = certSrc.Close() }() keySrc, err := keyFile.Open() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open key file"}) return } - defer keySrc.Close() + defer func() { _ = keySrc.Close() }() // Read to string // Limit size to avoid DoS (e.g. 1MB) diff --git a/backend/internal/api/handlers/import_handler.go b/backend/internal/api/handlers/import_handler.go index e48fe90d..4503e36a 100644 --- a/backend/internal/api/handlers/import_handler.go +++ b/backend/internal/api/handlers/import_handler.go @@ -518,7 +518,7 @@ func (h *ImportHandler) Commit(c *gin.Context) { } if action == "rename" { - host.DomainNames = host.DomainNames + "-imported" + host.DomainNames += "-imported" } // Handle overwrite: preserve existing ID, UUID, and certificate diff --git a/backend/internal/api/handlers/logs_handler.go b/backend/internal/api/handlers/logs_handler.go index 1f30e19d..fcf933f8 100644 --- a/backend/internal/api/handlers/logs_handler.go +++ b/backend/internal/api/handlers/logs_handler.go @@ -88,18 +88,18 @@ func (h *LogsHandler) Download(c *gin.Context) { srcFile, err := os.Open(path) if err != nil { - tmpFile.Close() + _ = tmpFile.Close() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open log file"}) return } - defer srcFile.Close() + defer func() { _ = srcFile.Close() }() if _, err := io.Copy(tmpFile, srcFile); err != nil { - tmpFile.Close() + _ = tmpFile.Close() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to copy log file"}) return } - tmpFile.Close() + _ = tmpFile.Close() c.Header("Content-Disposition", "attachment; filename="+filename) c.File(tmpFile.Name()) diff --git a/backend/internal/api/handlers/notification_provider_handler.go b/backend/internal/api/handlers/notification_provider_handler.go index 73b1a8d2..62190ff6 100644 --- a/backend/internal/api/handlers/notification_provider_handler.go +++ b/backend/internal/api/handlers/notification_provider_handler.go @@ -74,7 +74,7 @@ func (h *NotificationProviderHandler) Test(c *gin.Context) { if err := h.service.TestProvider(provider); err != nil { // Create internal notification for the failure - h.service.Create(models.NotificationTypeError, "Test Failed", fmt.Sprintf("Provider %s test failed: %v", provider.Name, err)) + _, _ = h.service.Create(models.NotificationTypeError, "Test Failed", fmt.Sprintf("Provider %s test failed: %v", provider.Name, err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } diff --git a/backend/internal/api/handlers/remote_server_handler.go b/backend/internal/api/handlers/remote_server_handler.go index 10e9a4b9..12fde59b 100644 --- a/backend/internal/api/handlers/remote_server_handler.go +++ b/backend/internal/api/handlers/remote_server_handler.go @@ -179,12 +179,12 @@ func (h *RemoteServerHandler) TestConnection(c *gin.Context) { server.Reachable = false now := time.Now().UTC() server.LastChecked = &now - h.service.Update(server) + _ = h.service.Update(server) c.JSON(http.StatusOK, result) return } - defer conn.Close() + defer func() { _ = conn.Close() }() // Connection successful result["reachable"] = true @@ -194,7 +194,7 @@ func (h *RemoteServerHandler) TestConnection(c *gin.Context) { server.Reachable = true now := time.Now().UTC() server.LastChecked = &now - h.service.Update(server) + _ = h.service.Update(server) c.JSON(http.StatusOK, result) } @@ -227,7 +227,7 @@ func (h *RemoteServerHandler) TestConnectionCustom(c *gin.Context) { c.JSON(http.StatusOK, result) return } - defer conn.Close() + defer func() { _ = conn.Close() }() // Connection successful result["reachable"] = true diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 2c077f98..d8527e68 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -165,7 +165,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { ticker := time.NewTicker(1 * time.Minute) for range ticker.C { - uptimeService.SyncMonitors() + _ = uptimeService.SyncMonitors() uptimeService.CheckAll() } }() diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go index c1035c37..2c77d314 100644 --- a/backend/internal/caddy/manager.go +++ b/backend/internal/caddy/manager.go @@ -81,7 +81,7 @@ func (m *Manager) ApplyConfig(ctx context.Context) error { // Apply to Caddy if err := m.client.Load(ctx, config); err != nil { // Remove the failed snapshot so rollback uses the previous one - os.Remove(snapshotPath) + _ = os.Remove(snapshotPath) // Rollback on failure if rollbackErr := m.rollback(ctx); rollbackErr != nil { diff --git a/backend/internal/services/access_list_service.go b/backend/internal/services/access_list_service.go index d0f64733..7cbb8709 100644 --- a/backend/internal/services/access_list_service.go +++ b/backend/internal/services/access_list_service.go @@ -299,28 +299,68 @@ func (s *AccessListService) isPrivateIP(ip net.IP) bool { func (s *AccessListService) GetTemplates() []map[string]interface{} { return []map[string]interface{}{ { + "id": "local-network", "name": "Local Network Only", - "description": "Allow only RFC1918 private network IPs", + "description": "Allow only RFC1918 private network IPs (home/office networks)", "type": "whitelist", "local_network_only": true, + "category": "security", }, { + "id": "us-only", "name": "US Only", "description": "Allow only United States IPs", "type": "geo_whitelist", "country_codes": "US", + "category": "security", }, { + "id": "eu-only", "name": "EU Only", "description": "Allow only European Union IPs", "type": "geo_whitelist", "country_codes": "AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IE,IT,LV,LT,LU,MT,NL,PL,PT,RO,SK,SI,ES,SE", + "category": "security", }, { - "name": "Block China & Russia", - "description": "Block IPs from China and Russia", + "id": "high-risk-countries", + "name": "Block High-Risk Countries", + "description": "Block OFAC sanctioned countries and known attack sources", "type": "geo_blacklist", - "country_codes": "CN,RU", + "country_codes": "RU,CN,KP,IR,BY,SY,VE,CU,SD", + "category": "security", + }, + { + "id": "expanded-threat-countries", + "name": "Block Expanded Threat List", + "description": "Block high-risk countries plus additional bot/spam sources", + "type": "geo_blacklist", + "country_codes": "RU,CN,KP,IR,BY,SY,VE,CU,SD,PK,BD,NG,UA,VN,ID", + "category": "security", + }, + { + "id": "known-botnets", + "name": "Block Known Botnet IPs", + "description": "Block IPs known to be part of active botnets and malware networks", + "type": "blacklist", + "ip_rules": `[{"cidr":"5.8.10.0/24","description":"Spamhaus DROP"},{"cidr":"5.188.206.0/24","description":"Spamhaus DROP"},{"cidr":"23.94.0.0/15","description":"Bulletproof hosting"},{"cidr":"45.14.224.0/22","description":"Malware hosting"},{"cidr":"91.200.12.0/22","description":"Spamhaus DROP"},{"cidr":"185.234.216.0/22","description":"Botnet infrastructure"}]`, + "category": "security", + }, + { + "id": "cloud-scanners", + "name": "Block Cloud Scanners", + "description": "Block IP ranges used by Shodan, Censys, and other scanners", + "type": "blacklist", + "ip_rules": `[{"cidr":"71.6.135.0/24","description":"Shodan"},{"cidr":"71.6.167.0/24","description":"Shodan"},{"cidr":"162.142.125.0/24","description":"Censys"},{"cidr":"167.248.133.0/24","description":"Censys"},{"cidr":"198.108.66.0/24","description":"Shodan"},{"cidr":"198.20.69.0/24","description":"Shodan"}]`, + "category": "advanced", + }, + { + "id": "tor-exit-nodes", + "name": "Block Tor Exit Nodes", + "description": "Block known Tor network exit nodes", + "type": "blacklist", + "ip_rules": `[{"cidr":"185.220.100.0/22","description":"Tor exits"},{"cidr":"185.100.84.0/22","description":"Tor exits"},{"cidr":"176.10.99.0/24","description":"Tor exits"},{"cidr":"176.10.104.0/22","description":"Tor exits"}]`, + "category": "advanced", }, } } diff --git a/backend/internal/services/backup_service.go b/backend/internal/services/backup_service.go index 4048958b..04b6b95e 100644 --- a/backend/internal/services/backup_service.go +++ b/backend/internal/services/backup_service.go @@ -101,10 +101,9 @@ func (s *BackupService) CreateBackup() (string, error) { if err != nil { return "", err } - defer outFile.Close() + defer func() { _ = outFile.Close() }() w := zip.NewWriter(outFile) - defer w.Close() // Files/Dirs to backup // 1. Database @@ -125,6 +124,11 @@ func (s *BackupService) CreateBackup() (string, error) { fmt.Printf("Warning: could not backup caddy dir: %v\n", err) } + // Close zip writer and check for errors (important for zip integrity) + if err := w.Close(); err != nil { + return "", fmt.Errorf("failed to finalize backup: %w", err) + } + return filename, nil } @@ -216,7 +220,7 @@ func (s *BackupService) unzip(src, dest string) error { if err != nil { return err } - defer r.Close() + defer func() { _ = r.Close() }() for _, f := range r.File { fpath := filepath.Join(dest, f.Name) @@ -227,11 +231,11 @@ func (s *BackupService) unzip(src, dest string) error { } if f.FileInfo().IsDir() { - os.MkdirAll(fpath, os.ModePerm) + _ = os.MkdirAll(fpath, os.ModePerm) continue } - if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { return err } diff --git a/backend/internal/services/docker_service.go b/backend/internal/services/docker_service.go index 5a27cdb5..88de8fff 100644 --- a/backend/internal/services/docker_service.go +++ b/backend/internal/services/docker_service.go @@ -49,7 +49,7 @@ func (s *DockerService) ListContainers(ctx context.Context, host string) ([]Dock if err != nil { return nil, fmt.Errorf("failed to create remote client: %w", err) } - defer cli.Close() + defer func() { _ = cli.Close() }() } containers, err := cli.ContainerList(ctx, container.ListOptions{All: false}) diff --git a/backend/internal/services/log_service.go b/backend/internal/services/log_service.go index 16366d3a..df947580 100644 --- a/backend/internal/services/log_service.go +++ b/backend/internal/services/log_service.go @@ -88,10 +88,10 @@ func (s *LogService) QueryLogs(filename string, filter models.LogFilter) ([]mode if err != nil { return nil, 0, err } - defer file.Close() + defer func() { _ = file.Close() }() var logs []models.CaddyAccessLog - var totalMatches int64 = 0 + var totalMatches int64 // Read file line by line // TODO: For large files, reading from end or indexing would be better diff --git a/backend/internal/services/proxyhost_service.go b/backend/internal/services/proxyhost_service.go index 76443728..65994d55 100644 --- a/backend/internal/services/proxyhost_service.go +++ b/backend/internal/services/proxyhost_service.go @@ -103,7 +103,7 @@ func (s *ProxyHostService) TestConnection(host string, port int) error { if err != nil { return fmt.Errorf("connection failed: %w", err) } - defer conn.Close() + defer func() { _ = conn.Close() }() return nil } diff --git a/backend/internal/services/uptime_service.go b/backend/internal/services/uptime_service.go index 20527f59..5a462133 100644 --- a/backend/internal/services/uptime_service.go +++ b/backend/internal/services/uptime_service.go @@ -188,7 +188,7 @@ func (s *UptimeService) checkMonitor(monitor models.UptimeMonitor) { case "tcp": conn, err := net.DialTimeout("tcp", monitor.URL, 10*time.Second) if err == nil { - conn.Close() + _ = conn.Close() success = true msg = "Connection successful" } else { @@ -238,7 +238,7 @@ func (s *UptimeService) checkMonitor(monitor models.UptimeMonitor) { Latency: latency, Message: msg, } - s.DB.Create(&heartbeat) + _ = s.DB.Create(&heartbeat).Error // Update Monitor Status oldStatus := monitor.Status @@ -285,7 +285,7 @@ func (s *UptimeService) checkMonitor(monitor models.UptimeMonitor) { sb.WriteString(fmt.Sprintf("Reason: %s\n", msg)) - s.NotificationService.Create( + _, _ = s.NotificationService.Create( nType, title, sb.String(), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aedec5a3..9926d971 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -38,6 +38,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "jsdom": "^27.2.0", + "knip": "^5.70.2", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", @@ -148,7 +149,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -504,7 +504,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -546,11 +545,44 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" } }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1261,6 +1293,326 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.14.0.tgz", + "integrity": "sha512-jB47iZ/thvhE+USCLv+XY3IknBbkKr/p7OBsQDTHode/GPw+OHRlit3NQ1bjt1Mj8V2CS7iHdSDYobZ1/0gagQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.14.0.tgz", + "integrity": "sha512-XFJ9t7d/Cz+dWLyqtTy3Xrekz+qqN4hmOU2iOUgr7u71OQsPUHIIeS9/wKanEK0l413gPwapIkyc5x9ltlOtyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.14.0.tgz", + "integrity": "sha512-gwehBS9smA1mzK8frDsmUCHz+6baJVwkKF6qViHhoqA3kRKvIZ3k6WNP4JmF19JhOiGxRcoPa8gZRfzNgXwP2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.14.0.tgz", + "integrity": "sha512-5wwJvfuoahKiAqqAsMLOI28rqdh3P2K7HkjIWUXNMWAZq6ErX0L5rwJzu6T32+Zxw3k18C7R9IS4wDq/3Ar+6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.14.0.tgz", + "integrity": "sha512-MWTt+LOQNcQ6fa+Uu5VikkihLi1PSIrQqqp0QD44k2AORasNWl0jRGBTcMSBIgNe82qEQWYvlGzvOEEOBp01Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.14.0.tgz", + "integrity": "sha512-b6/IBqYrS3o0XiLVBsnex/wK8pTTK+hbGfAMOHVU6p7DBpwPPLgC/tav4IXoOIUCssTFz7aWh/xtUok0swn8VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.14.0.tgz", + "integrity": "sha512-o2Qh5+y5YoqVK6YfzkalHdpmQ5bkbGGxuLg1pZLQ1Ift0x+Vix7DaFEpdCl5Z9xvYXogd/TwOlL0TPl4+MTFLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.14.0.tgz", + "integrity": "sha512-lk8mCSg0Tg4sEG73RiPjb7keGcEPwqQnBHX3Z+BR2SWe+qNHpoHcyFMNafzSvEC18vlxC04AUSoa6kJl/C5zig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.14.0.tgz", + "integrity": "sha512-KykeIVhCM7pn93ABa0fNe8vk4XvnbfZMELne2s6P9tdJH9KMBsCFBi7a2BmSdUtTqWCAJokAcm46lpczU52Xaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.14.0.tgz", + "integrity": "sha512-QqPPWAcZU/jHAuam4f3zV8OdEkYRPD2XR0peVet3hoMMgsihR3Lhe7J/bLclmod297FG0+OgBYQVMh2nTN6oWA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.14.0.tgz", + "integrity": "sha512-DunWA+wafeG3hj1NADUD3c+DRvmyVNqF5LSHVUWA2bzswqmuEZXl3VYBSzxfD0j+UnRTFYLxf27AMptoMsepYg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.14.0.tgz", + "integrity": "sha512-4SRvwKTTk2k67EQr9Ny4NGf/BhlwggCI1CXwBbA9IV4oP38DH8b+NAPxDY0ySGRsWbPkG92FYOqM4AWzG4GSgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.14.0.tgz", + "integrity": "sha512-hZKvkbsurj4JOom//R1Ab2MlC4cGeVm5zzMt4IsS3XySQeYjyMJ5TDZ3J5rQ8bVj3xi4FpJU2yFZ72GApsHQ6A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.14.0.tgz", + "integrity": "sha512-hABxQXFXJurivw+0amFdeEcK67cF1BGBIN1+sSHzq3TRv4RoG8n5q2JE04Le2n2Kpt6xg4Y5+lcv+rb2mCJLgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.14.0.tgz", + "integrity": "sha512-Ln73wUB5migZRvC7obAAdqVwvFvk7AUs2JLt4g9QHr8FnqivlsjpUC9Nf2ssrybdjyQzEMjttUxPZz6aKPSAHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.14.0.tgz", + "integrity": "sha512-z+NbELmCOKNtWOqEB5qDfHXOSWB3kGQIIehq6nHtZwHLzdVO2oBq6De/ayhY3ygriC1XhgaIzzniY7jgrNl4Kw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.14.0.tgz", + "integrity": "sha512-Ft0+qd7HSO61qCTLJ4LCdBGZkpKyDj1rG0OVSZL1DxWQoh97m7vEHd7zAvUtw8EcWjOMBQuX4mfRap/x2MOCpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.14.0.tgz", + "integrity": "sha512-o54jYNSfGdPxHSvXEhZg8FOV3K99mJ1f7hb1alRFb+Yec1GQXNrJXxZPIxNMYeFT13kwAWB7zuQ0HZLnDHFxfw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.14.0.tgz", + "integrity": "sha512-j97icaORyM6A7GjgmUzfn7V+KGzVvctRA+eAlJb0c2OQNaETFxl6BXZdnGBDb+6oA0Y4Sr/wnekd1kQ0aVyKGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -2013,11 +2365,23 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2088,13 +2452,23 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2105,7 +2479,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2146,7 +2519,6 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -2501,7 +2873,6 @@ "integrity": "sha512-fvDz8o7SQpFLoSBo6Cudv+fE85/fPCkwTnLAN85M+Jv7k59w2mSIjT9Q5px7XwGrmYqqKBEYxh/09IBGd1E7AQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.14", "fflate": "^0.8.2", @@ -2537,7 +2908,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2723,6 +3093,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", @@ -2742,7 +3125,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2931,8 +3313,7 @@ "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "peer": true + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/data-urls": { "version": "6.0.0", @@ -3015,7 +3396,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -3177,7 +3559,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3432,6 +3813,36 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3444,6 +3855,26 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -3462,6 +3893,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3531,6 +3975,22 @@ "node": ">= 6" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -3848,6 +4308,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3942,7 +4412,6 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", "dev": true, - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -4028,6 +4497,61 @@ "json-buffer": "3.0.1" } }, + "node_modules/knip": { + "version": "5.70.2", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.70.2.tgz", + "integrity": "sha512-LI7DbeVnk7h9+FAet5KzzHNdDwJyqDa2+cn4uQfZYTfpuVjEqtGmYD9r5b9JEuOs4eVkf/7sskNhWXxELm3C/Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "@nodelib/fs.walk": "^1.2.3", + "fast-glob": "^3.3.3", + "formatly": "^0.3.0", + "jiti": "^2.6.0", + "js-yaml": "^4.1.1", + "minimist": "^1.2.8", + "oxc-resolver": "^11.13.2", + "picocolors": "^1.1.1", + "picomatch": "^4.0.1", + "smol-toml": "^1.5.2", + "strip-json-comments": "5.0.3", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": ">=18.18.0" + }, + "peerDependencies": { + "@types/node": ">=18", + "typescript": ">=5.0.4 <7" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4334,6 +4858,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4387,6 +4912,43 @@ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4431,6 +4993,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -4513,6 +5085,37 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-resolver": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.14.0.tgz", + "integrity": "sha512-i4wNrqhOd+4YdHJfHglHtFiqqSxXuzFA+RUqmmWN1aMD3r1HqUSrIhw17tSO4jwKfhLs9uw1wzFPmvMsWacStg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.14.0", + "@oxc-resolver/binding-android-arm64": "11.14.0", + "@oxc-resolver/binding-darwin-arm64": "11.14.0", + "@oxc-resolver/binding-darwin-x64": "11.14.0", + "@oxc-resolver/binding-freebsd-x64": "11.14.0", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.14.0", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.14.0", + "@oxc-resolver/binding-linux-arm64-gnu": "11.14.0", + "@oxc-resolver/binding-linux-arm64-musl": "11.14.0", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.14.0", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.14.0", + "@oxc-resolver/binding-linux-riscv64-musl": "11.14.0", + "@oxc-resolver/binding-linux-s390x-gnu": "11.14.0", + "@oxc-resolver/binding-linux-x64-gnu": "11.14.0", + "@oxc-resolver/binding-linux-x64-musl": "11.14.0", + "@oxc-resolver/binding-wasm32-wasi": "11.14.0", + "@oxc-resolver/binding-win32-arm64-msvc": "11.14.0", + "@oxc-resolver/binding-win32-ia32-msvc": "11.14.0", + "@oxc-resolver/binding-win32-x64-msvc": "11.14.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4603,7 +5206,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4631,7 +5233,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4661,6 +5262,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -4675,6 +5277,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -4684,6 +5287,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -4705,12 +5309,32 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4720,7 +5344,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4765,7 +5388,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/react-refresh": { "version": "0.18.0", @@ -4846,6 +5470,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.53.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", @@ -4887,6 +5522,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4970,6 +5629,19 @@ "node": ">=18" } }, + "node_modules/smol-toml": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.5.2.tgz", + "integrity": "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5134,6 +5806,19 @@ "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -5179,6 +5864,14 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5197,7 +5890,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5230,6 +5922,14 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -5275,7 +5975,6 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5369,7 +6068,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5383,7 +6081,6 @@ "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.14", "@vitest/mocker": "4.0.14", @@ -5468,6 +6165,16 @@ "node": ">=18" } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/webidl-conversions": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", @@ -5611,7 +6318,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index 724532c1..7b5a406f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "jsdom": "^27.2.0", + "knip": "^5.70.2", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", diff --git a/frontend/src/api/__tests__/docker.test.ts b/frontend/src/api/__tests__/docker.test.ts new file mode 100644 index 00000000..0a435e6c --- /dev/null +++ b/frontend/src/api/__tests__/docker.test.ts @@ -0,0 +1,96 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { dockerApi } from '../docker'; +import client from '../client'; + +vi.mock('../client', () => ({ + default: { + get: vi.fn(), + }, +})); + +describe('dockerApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('listContainers', () => { + const mockContainers = [ + { + id: 'abc123', + names: ['/container1'], + image: 'nginx:latest', + state: 'running', + status: 'Up 2 hours', + network: 'bridge', + ip: '172.17.0.2', + ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }], + }, + { + id: 'def456', + names: ['/container2'], + image: 'redis:alpine', + state: 'running', + status: 'Up 1 hour', + network: 'bridge', + ip: '172.17.0.3', + ports: [], + }, + ]; + + it('fetches containers without parameters', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); + + const result = await dockerApi.listContainers(); + + expect(client.get).toHaveBeenCalledWith('/docker/containers', { params: {} }); + expect(result).toEqual(mockContainers); + }); + + it('fetches containers with host parameter', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); + + const result = await dockerApi.listContainers('192.168.1.100'); + + expect(client.get).toHaveBeenCalledWith('/docker/containers', { + params: { host: '192.168.1.100' }, + }); + expect(result).toEqual(mockContainers); + }); + + it('fetches containers with serverId parameter', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); + + const result = await dockerApi.listContainers(undefined, 'server-uuid-123'); + + expect(client.get).toHaveBeenCalledWith('/docker/containers', { + params: { server_id: 'server-uuid-123' }, + }); + expect(result).toEqual(mockContainers); + }); + + it('fetches containers with both host and serverId parameters', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); + + const result = await dockerApi.listContainers('192.168.1.100', 'server-uuid-123'); + + expect(client.get).toHaveBeenCalledWith('/docker/containers', { + params: { host: '192.168.1.100', server_id: 'server-uuid-123' }, + }); + expect(result).toEqual(mockContainers); + }); + + it('returns empty array when no containers', async () => { + vi.mocked(client.get).mockResolvedValue({ data: [] }); + + const result = await dockerApi.listContainers(); + + expect(result).toEqual([]); + }); + + it('handles API error', async () => { + vi.mocked(client.get).mockRejectedValue(new Error('Network error')); + + await expect(dockerApi.listContainers()).rejects.toThrow('Network error'); + }); + }); +}); diff --git a/frontend/src/api/__tests__/remoteServers.test.ts b/frontend/src/api/__tests__/remoteServers.test.ts new file mode 100644 index 00000000..84f5cd1c --- /dev/null +++ b/frontend/src/api/__tests__/remoteServers.test.ts @@ -0,0 +1,146 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { + getRemoteServers, + getRemoteServer, + createRemoteServer, + updateRemoteServer, + deleteRemoteServer, + testRemoteServerConnection, + testCustomRemoteServerConnection, +} from '../remoteServers'; +import client from '../client'; + +vi.mock('../client', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }, +})); + +describe('remoteServers API', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const mockServer = { + uuid: 'server-123', + name: 'Test Server', + provider: 'docker', + host: '192.168.1.100', + port: 2375, + username: 'admin', + enabled: true, + reachable: true, + last_check: '2024-01-01T12:00:00Z', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T12:00:00Z', + }; + + describe('getRemoteServers', () => { + it('fetches all servers', async () => { + vi.mocked(client.get).mockResolvedValue({ data: [mockServer] }); + + const result = await getRemoteServers(); + + expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: {} }); + expect(result).toEqual([mockServer]); + }); + + it('fetches enabled servers only', async () => { + vi.mocked(client.get).mockResolvedValue({ data: [mockServer] }); + + const result = await getRemoteServers(true); + + expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: { enabled: true } }); + expect(result).toEqual([mockServer]); + }); + }); + + describe('getRemoteServer', () => { + it('fetches a single server by UUID', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockServer }); + + const result = await getRemoteServer('server-123'); + + expect(client.get).toHaveBeenCalledWith('/remote-servers/server-123'); + expect(result).toEqual(mockServer); + }); + }); + + describe('createRemoteServer', () => { + it('creates a new server', async () => { + const newServer = { + name: 'New Server', + provider: 'docker', + host: '10.0.0.1', + port: 2375, + }; + vi.mocked(client.post).mockResolvedValue({ data: { ...mockServer, ...newServer } }); + + const result = await createRemoteServer(newServer); + + expect(client.post).toHaveBeenCalledWith('/remote-servers', newServer); + expect(result.name).toBe('New Server'); + }); + }); + + describe('updateRemoteServer', () => { + it('updates an existing server', async () => { + const updates = { name: 'Updated Server', enabled: false }; + vi.mocked(client.put).mockResolvedValue({ data: { ...mockServer, ...updates } }); + + const result = await updateRemoteServer('server-123', updates); + + expect(client.put).toHaveBeenCalledWith('/remote-servers/server-123', updates); + expect(result.name).toBe('Updated Server'); + expect(result.enabled).toBe(false); + }); + }); + + describe('deleteRemoteServer', () => { + it('deletes a server', async () => { + vi.mocked(client.delete).mockResolvedValue({}); + + await deleteRemoteServer('server-123'); + + expect(client.delete).toHaveBeenCalledWith('/remote-servers/server-123'); + }); + }); + + describe('testRemoteServerConnection', () => { + it('tests connection to an existing server', async () => { + vi.mocked(client.post).mockResolvedValue({ data: { address: '192.168.1.100:2375' } }); + + const result = await testRemoteServerConnection('server-123'); + + expect(client.post).toHaveBeenCalledWith('/remote-servers/server-123/test'); + expect(result.address).toBe('192.168.1.100:2375'); + }); + }); + + describe('testCustomRemoteServerConnection', () => { + it('tests connection to a custom host and port', async () => { + vi.mocked(client.post).mockResolvedValue({ + data: { address: '10.0.0.1:2375', reachable: true }, + }); + + const result = await testCustomRemoteServerConnection('10.0.0.1', 2375); + + expect(client.post).toHaveBeenCalledWith('/remote-servers/test', { host: '10.0.0.1', port: 2375 }); + expect(result.reachable).toBe(true); + }); + + it('handles unreachable server', async () => { + vi.mocked(client.post).mockResolvedValue({ + data: { address: '10.0.0.1:2375', reachable: false, error: 'Connection refused' }, + }); + + const result = await testCustomRemoteServerConnection('10.0.0.1', 2375); + + expect(result.reachable).toBe(false); + expect(result.error).toBe('Connection refused'); + }); + }); +}); diff --git a/frontend/src/components/AccessListForm.tsx b/frontend/src/components/AccessListForm.tsx index 1edd0804..0b371276 100644 --- a/frontend/src/components/AccessListForm.tsx +++ b/frontend/src/components/AccessListForm.tsx @@ -268,7 +268,7 @@ export function AccessListForm({ initialData, onSubmit, onCancel, isLoading }: A
Quick-start templates based on threat intelligence feeds and best practices. Hover over (i) for data sources.
- + {/* Security Category */}Checking healthβ¦
} - {isError &&Unable to reach backend
} - {data && ( -