From 9e6756d02ff90284ee2c0934e359d8a5541d0f35 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Sun, 15 Mar 2026 01:19:19 -0400 Subject: [PATCH] Implement M5: hardening, input validation, and Vite+React+TS dashboard Backend hardening: - Fix 6 nginx.go non-constant format string build errors - Add validation.go with hostname, PEM, and enum validators - Apply input validation to all POST/PUT handlers (certificates, agents, CSR, policies, teams, owners, targets, issuers) - Fix unchecked JSON decode in TriggerDeployment handler Frontend (Vite + React + TypeScript): - Migrate from single-file SPA to proper build pipeline - 7 pages: Dashboard, Certificates (list+detail), Agents, Jobs, Notifications, Policies, Audit Trail - TanStack Query for server state with auto-refetch intervals - Certificate detail with version history and renewal trigger - Job cancellation, status/type filtering, expiry countdowns - Reusable components: DataTable, StatusBadge, ErrorState, PageHeader - Dark theme with Tailwind CSS, sidebar nav via React Router Server integration: - Go server serves web/dist/ (Vite output) with SPA fallback - Falls back to web/index.html for legacy mode - .gitignore updated for web/node_modules/ and web/dist/ Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 +- cmd/server/main.go | 20 +- internal/api/handler/agents.go | 19 +- .../api/handler/certificate_handler_test.go | 6 + internal/api/handler/certificates.go | 48 +- internal/api/handler/issuers.go | 14 + internal/api/handler/owners.go | 10 + internal/api/handler/policies.go | 28 + internal/api/handler/targets.go | 14 + internal/api/handler/teams.go | 10 + internal/api/handler/validation.go | 135 ++ internal/connector/target/nginx/nginx.go | 12 +- web/index.html | 1870 +------------- web/index.html.legacy | 1868 ++++++++++++++ web/package-lock.json | 2147 +++++++++++++++++ web/package.json | 27 + web/postcss.config.cjs | 6 + web/src/api/client.ts | 90 + web/src/api/types.ts | 133 + web/src/api/utils.ts | 37 + web/src/components/DataTable.tsx | 69 + web/src/components/ErrorState.tsx | 21 + web/src/components/Layout.tsx | 60 + web/src/components/PageHeader.tsx | 17 + web/src/components/StatusBadge.tsx | 29 + web/src/index.css | 30 + web/src/main.tsx | 45 + web/src/pages/AgentsPage.tsx | 64 + web/src/pages/AuditPage.tsx | 86 + web/src/pages/CertificateDetailPage.tsx | 168 ++ web/src/pages/CertificatesPage.tsx | 103 + web/src/pages/DashboardPage.tsx | 144 ++ web/src/pages/JobsPage.tsx | 105 + web/src/pages/NotificationsPage.tsx | 48 + web/src/pages/PoliciesPage.tsx | 64 + web/src/vite-env.d.ts | 1 + web/tailwind.config.cjs | 12 + web/tsconfig.json | 21 + web/vite.config.ts | 17 + 39 files changed, 5725 insertions(+), 1878 deletions(-) create mode 100644 internal/api/handler/validation.go create mode 100644 web/index.html.legacy create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/postcss.config.cjs create mode 100644 web/src/api/client.ts create mode 100644 web/src/api/types.ts create mode 100644 web/src/api/utils.ts create mode 100644 web/src/components/DataTable.tsx create mode 100644 web/src/components/ErrorState.tsx create mode 100644 web/src/components/Layout.tsx create mode 100644 web/src/components/PageHeader.tsx create mode 100644 web/src/components/StatusBadge.tsx create mode 100644 web/src/index.css create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/AgentsPage.tsx create mode 100644 web/src/pages/AuditPage.tsx create mode 100644 web/src/pages/CertificateDetailPage.tsx create mode 100644 web/src/pages/CertificatesPage.tsx create mode 100644 web/src/pages/DashboardPage.tsx create mode 100644 web/src/pages/JobsPage.tsx create mode 100644 web/src/pages/NotificationsPage.tsx create mode 100644 web/src/pages/PoliciesPage.tsx create mode 100644 web/src/vite-env.d.ts create mode 100644 web/tailwind.config.cjs create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.gitignore b/.gitignore index ad2645f..5f7253d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,10 @@ *.so.* *.dylib bin/ -dist/ + +# Frontend +web/node_modules/ +web/dist/ # Test binary, built with `go test -c` *.test diff --git a/cmd/server/main.go b/cmd/server/main.go index 51d4c61..08655df 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -174,10 +174,15 @@ func main() { middleware.Recovery, ) - // Wrap with dashboard static file serving if web/ directory exists + // Wrap with dashboard static file serving + // Vite builds to web/dist/; fall back to web/ for legacy single-file SPA var finalHandler http.Handler - webDir := "./web" - if _, err := os.Stat(webDir); err == nil { + webDir := "./web/dist" + if _, err := os.Stat(webDir + "/index.html"); err != nil { + webDir = "./web" + } + if _, err := os.Stat(webDir + "/index.html"); err == nil { + fileServer := http.FileServer(http.Dir(webDir)) finalHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path // API and health routes go to the API handler @@ -186,10 +191,15 @@ func main() { apiHandler.ServeHTTP(w, r) return } - // Serve the dashboard SPA index.html for everything else + // Try to serve static files (JS, CSS, assets) + if len(path) > 8 && path[:8] == "/assets/" { + fileServer.ServeHTTP(w, r) + return + } + // SPA fallback: serve index.html for all other routes http.ServeFile(w, r, webDir+"/index.html") }) - logger.Info("dashboard available at /") + logger.Info("dashboard available at /", "web_dir", webDir) } else { finalHandler = apiHandler logger.Info("dashboard directory not found, serving API only") diff --git a/internal/api/handler/agents.go b/internal/api/handler/agents.go index 00c8170..94d0ce7 100644 --- a/internal/api/handler/agents.go +++ b/internal/api/handler/agents.go @@ -117,6 +117,20 @@ func (h AgentHandler) RegisterAgent(w http.ResponseWriter, r *http.Request) { return } + // Validate required fields + if err := ValidateRequired("name", agent.Name); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateStringLength("name", agent.Name, 128); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateRequired("hostname", agent.Hostname); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + created, err := h.svc.RegisterAgent(agent) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to register agent", requestID) @@ -186,8 +200,9 @@ func (h AgentHandler) AgentCSRSubmit(w http.ResponseWriter, r *http.Request) { return } - if req.CSRPEM == "" { - ErrorWithRequestID(w, http.StatusBadRequest, "CSR PEM is required", requestID) + // Validate CSR PEM + if err := ValidateCSRPEM(req.CSRPEM); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) return } diff --git a/internal/api/handler/certificate_handler_test.go b/internal/api/handler/certificate_handler_test.go index 84a8fb9..dfc5860 100644 --- a/internal/api/handler/certificate_handler_test.go +++ b/internal/api/handler/certificate_handler_test.go @@ -305,6 +305,9 @@ func TestCreateCertificate_Success(t *testing.T) { certBody := domain.ManagedCertificate{ Name: "Production Cert", CommonName: "example.com", + OwnerID: "o-alice", + TeamID: "t-platform", + IssuerID: "iss-local", } body, _ := json.Marshal(certBody) @@ -359,6 +362,9 @@ func TestCreateCertificate_ServiceError(t *testing.T) { certBody := domain.ManagedCertificate{ Name: "Production Cert", CommonName: "example.com", + OwnerID: "o-alice", + TeamID: "t-platform", + IssuerID: "iss-local", } body, _ := json.Marshal(certBody) diff --git a/internal/api/handler/certificates.go b/internal/api/handler/certificates.go index a26d503..545e961 100644 --- a/internal/api/handler/certificates.go +++ b/internal/api/handler/certificates.go @@ -120,6 +120,28 @@ func (h CertificateHandler) CreateCertificate(w http.ResponseWriter, r *http.Req return } + // Validate required fields + if err := ValidateRequired("common_name", cert.CommonName); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateCommonName(cert.CommonName); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateRequired("owner_id", cert.OwnerID); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateRequired("team_id", cert.TeamID); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateRequired("issuer_id", cert.IssuerID); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + created, err := h.svc.CreateCertificate(cert) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create certificate", requestID) @@ -153,6 +175,26 @@ func (h CertificateHandler) UpdateCertificate(w http.ResponseWriter, r *http.Req return } + // Validate required fields (if provided) + if cert.CommonName != "" { + if err := ValidateCommonName(cert.CommonName); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + } + if cert.OwnerID != "" { + if err := ValidateStringLength("owner_id", cert.OwnerID, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + } + if cert.TeamID != "" { + if err := ValidateStringLength("team_id", cert.TeamID, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + } + updated, err := h.svc.UpdateCertificate(id, cert) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to update certificate", requestID) @@ -290,7 +332,11 @@ func (h CertificateHandler) TriggerDeployment(w http.ResponseWriter, r *http.Req TargetID string `json:"target_id,omitempty"` } if r.Header.Get("Content-Type") == "application/json" { - json.NewDecoder(r.Body).Decode(&req) + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + // Log but don't fail - targetID is optional + ErrorWithRequestID(w, http.StatusBadRequest, "Invalid request body", requestID) + return + } } if err := h.svc.TriggerDeployment(certID, req.TargetID); err != nil { diff --git a/internal/api/handler/issuers.go b/internal/api/handler/issuers.go index 2ff72e5..7be8eb0 100644 --- a/internal/api/handler/issuers.go +++ b/internal/api/handler/issuers.go @@ -111,6 +111,20 @@ func (h IssuerHandler) CreateIssuer(w http.ResponseWriter, r *http.Request) { return } + // Validate required fields + if err := ValidateRequired("name", issuer.Name); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateStringLength("name", issuer.Name, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if issuer.Type == "" { + ErrorWithRequestID(w, http.StatusBadRequest, "type is required", requestID) + return + } + created, err := h.svc.CreateIssuer(issuer) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create issuer", requestID) diff --git a/internal/api/handler/owners.go b/internal/api/handler/owners.go index aaa7abf..7e9b348 100644 --- a/internal/api/handler/owners.go +++ b/internal/api/handler/owners.go @@ -112,6 +112,16 @@ func (h OwnerHandler) CreateOwner(w http.ResponseWriter, r *http.Request) { return } + // Validate required fields + if err := ValidateRequired("name", owner.Name); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateStringLength("name", owner.Name, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + created, err := h.svc.CreateOwner(owner) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create owner", requestID) diff --git a/internal/api/handler/policies.go b/internal/api/handler/policies.go index f58877c..9edb026 100644 --- a/internal/api/handler/policies.go +++ b/internal/api/handler/policies.go @@ -113,6 +113,20 @@ func (h PolicyHandler) CreatePolicy(w http.ResponseWriter, r *http.Request) { return } + // Validate required fields + if err := ValidateRequired("name", policy.Name); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if policy.Type == "" { + ErrorWithRequestID(w, http.StatusBadRequest, "type is required", requestID) + return + } + if err := ValidatePolicyType(policy.Type); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + created, err := h.svc.CreatePolicy(policy) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create policy", requestID) @@ -146,6 +160,20 @@ func (h PolicyHandler) UpdatePolicy(w http.ResponseWriter, r *http.Request) { return } + // Validate fields if provided + if policy.Name != "" { + if err := ValidateStringLength("name", policy.Name, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + } + if policy.Type != "" { + if err := ValidatePolicyType(policy.Type); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + } + updated, err := h.svc.UpdatePolicy(id, policy) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to update policy", requestID) diff --git a/internal/api/handler/targets.go b/internal/api/handler/targets.go index b7f1623..1eda98e 100644 --- a/internal/api/handler/targets.go +++ b/internal/api/handler/targets.go @@ -110,6 +110,20 @@ func (h TargetHandler) CreateTarget(w http.ResponseWriter, r *http.Request) { return } + // Validate required fields + if err := ValidateRequired("name", target.Name); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateStringLength("name", target.Name, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if target.Type == "" { + ErrorWithRequestID(w, http.StatusBadRequest, "type is required", requestID) + return + } + created, err := h.svc.CreateTarget(target) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create target", requestID) diff --git a/internal/api/handler/teams.go b/internal/api/handler/teams.go index 47b9a46..4eed9d7 100644 --- a/internal/api/handler/teams.go +++ b/internal/api/handler/teams.go @@ -112,6 +112,16 @@ func (h TeamHandler) CreateTeam(w http.ResponseWriter, r *http.Request) { return } + // Validate required fields + if err := ValidateRequired("name", team.Name); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + if err := ValidateStringLength("name", team.Name, 255); err != nil { + ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID) + return + } + created, err := h.svc.CreateTeam(team) if err != nil { ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create team", requestID) diff --git a/internal/api/handler/validation.go b/internal/api/handler/validation.go new file mode 100644 index 0000000..5c1364f --- /dev/null +++ b/internal/api/handler/validation.go @@ -0,0 +1,135 @@ +package handler + +import ( + "fmt" + "net" + "strings" +) + +// ValidationError represents a validation error with field-level details. +type ValidationError struct { + Field string + Message string +} + +// ValidateCommonName validates a certificate common name. +func ValidateCommonName(cn string) error { + if cn == "" { + return ValidationError{Field: "common_name", Message: "common_name is required"} + } + if len(cn) > 253 { + return ValidationError{Field: "common_name", Message: "common_name must be 253 characters or fewer"} + } + // Basic hostname validation: allow alphanumeric, dots, hyphens + if err := isValidHostname(cn); err != nil { + return ValidationError{Field: "common_name", Message: fmt.Sprintf("invalid hostname format: %v", err)} + } + return nil +} + +// ValidateRequired checks if a string field is present and non-empty. +func ValidateRequired(field, value string) error { + if value == "" { + return ValidationError{Field: field, Message: fmt.Sprintf("%s is required", field)} + } + return nil +} + +// ValidateStringLength checks if a string is within acceptable length bounds. +func ValidateStringLength(field, value string, maxLen int) error { + if len(value) > maxLen { + return ValidationError{Field: field, Message: fmt.Sprintf("%s must be %d characters or fewer", field, maxLen)} + } + return nil +} + +// ValidateCSRPEM validates a certificate signing request PEM block. +func ValidateCSRPEM(csrPEM string) error { + if csrPEM == "" { + return ValidationError{Field: "csr_pem", Message: "csr_pem is required"} + } + if !strings.HasPrefix(strings.TrimSpace(csrPEM), "-----BEGIN CERTIFICATE REQUEST-----") { + return ValidationError{Field: "csr_pem", Message: "csr_pem must be a valid PEM-encoded certificate request"} + } + return nil +} + +// ValidatePolicyType checks if a policy rule type is valid. +func ValidatePolicyType(policyType interface{}) error { + validTypes := map[string]bool{ + "AllowedIssuers": true, + "AllowedDomains": true, + "RequiredMetadata": true, + "AllowedEnvironments": true, + "RenewalLeadTime": true, + } + typeStr := fmt.Sprintf("%v", policyType) + if !validTypes[typeStr] { + return ValidationError{Field: "type", Message: "type must be one of: AllowedIssuers, AllowedDomains, RequiredMetadata, AllowedEnvironments, RenewalLeadTime"} + } + return nil +} + +// ValidatePolicySeverity checks if a severity level is valid. +func ValidatePolicySeverity(severity interface{}) error { + validSeverities := map[string]bool{ + "Warning": true, + "Error": true, + "Critical": true, + } + sevStr := fmt.Sprintf("%v", severity) + if !validSeverities[sevStr] { + return ValidationError{Field: "severity", Message: "severity must be one of: Warning, Error, Critical"} + } + return nil +} + +// isValidHostname performs basic validation on a hostname. +func isValidHostname(hostname string) error { + // Use net.SplitHostPort-compatible check + // Hostname can be an IP or domain name + if ip := net.ParseIP(hostname); ip != nil { + return nil // Valid IP address + } + + // For domain names, check basic format + if len(hostname) == 0 || len(hostname) > 253 { + return fmt.Errorf("hostname length invalid") + } + + // Check for invalid characters (very basic) + for _, char := range hostname { + if !isValidHostnameChar(char) { + return fmt.Errorf("hostname contains invalid character: %c", char) + } + } + + // Labels must not start or end with hyphen + labels := strings.Split(hostname, ".") + for _, label := range labels { + if len(label) == 0 { + return fmt.Errorf("hostname has empty label") + } + if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") { + return fmt.Errorf("hostname labels cannot start or end with hyphen") + } + } + + return nil +} + +// isValidHostnameChar checks if a character is valid in a hostname. +func isValidHostnameChar(r rune) bool { + return (r >= 'a' && r <= 'z') || + (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || + r == '.' || + r == '-' || + r == '_' || // Underscores are sometimes allowed + r == '*' // Wildcard support +} + +// Error method makes ValidationError satisfy the error interface. +func (e ValidationError) Error() string { + return e.Message +} diff --git a/internal/connector/target/nginx/nginx.go b/internal/connector/target/nginx/nginx.go index 7f7799d..3feab99 100644 --- a/internal/connector/target/nginx/nginx.go +++ b/internal/connector/target/nginx/nginx.go @@ -102,7 +102,7 @@ func (c *Connector) DeployCertificate(ctx context.Context, request target.Deploy TargetAddress: c.config.CertPath, Message: errMsg, DeployedAt: time.Now(), - }, fmt.Errorf(errMsg) + }, fmt.Errorf("%s", errMsg) } // Write chain with same permissions @@ -114,7 +114,7 @@ func (c *Connector) DeployCertificate(ctx context.Context, request target.Deploy TargetAddress: c.config.ChainPath, Message: errMsg, DeployedAt: time.Now(), - }, fmt.Errorf(errMsg) + }, fmt.Errorf("%s", errMsg) } // Validate NGINX configuration before reload @@ -128,7 +128,7 @@ func (c *Connector) DeployCertificate(ctx context.Context, request target.Deploy TargetAddress: c.config.CertPath, Message: errMsg, DeployedAt: time.Now(), - }, fmt.Errorf(errMsg) + }, fmt.Errorf("%s", errMsg) } // Reload NGINX @@ -142,7 +142,7 @@ func (c *Connector) DeployCertificate(ctx context.Context, request target.Deploy TargetAddress: c.config.CertPath, Message: errMsg, DeployedAt: time.Now(), - }, fmt.Errorf(errMsg) + }, fmt.Errorf("%s", errMsg) } deploymentDuration := time.Since(startTime) @@ -188,7 +188,7 @@ func (c *Connector) ValidateDeployment(ctx context.Context, request target.Valid TargetAddress: c.config.CertPath, Message: errMsg, ValidatedAt: time.Now(), - }, fmt.Errorf(errMsg) + }, fmt.Errorf("%s", errMsg) } // Verify certificate file exists and is readable @@ -201,7 +201,7 @@ func (c *Connector) ValidateDeployment(ctx context.Context, request target.Valid TargetAddress: c.config.CertPath, Message: errMsg, ValidatedAt: time.Now(), - }, fmt.Errorf(errMsg) + }, fmt.Errorf("%s", errMsg) } validationDuration := time.Since(startTime) diff --git a/web/index.html b/web/index.html index 7effd51..7a97972 100644 --- a/web/index.html +++ b/web/index.html @@ -1,1868 +1,12 @@ - + - - - certctl - Certificate Control Plane - - - - - + + + certctl - Certificate Control Plane - -
- - + +
+ diff --git a/web/index.html.legacy b/web/index.html.legacy new file mode 100644 index 0000000..7effd51 --- /dev/null +++ b/web/index.html.legacy @@ -0,0 +1,1868 @@ + + + + + + certctl - Certificate Control Plane + + + + + + + +
+ + + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..d159b51 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,2147 @@ +{ + "name": "web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.3" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.27", + "postcss": "^8.5.8", + "tailwindcss": "^3.4.19", + "typescript": "^5.9.3", + "vite": "^8.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/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/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "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-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "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/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001779", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz", + "integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "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/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/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "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/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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": "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/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "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/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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/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.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..fad9d86 --- /dev/null +++ b/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "certctl-dashboard", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.3" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.27", + "postcss": "^8.5.8", + "tailwindcss": "^3.4.19", + "typescript": "^5.9.3", + "vite": "^8.0.0" + } +} diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/web/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..4cb218a --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,90 @@ +import type { Certificate, CertificateVersion, Agent, Job, Notification, AuditEvent, PolicyRule, Issuer, Target, PaginatedResponse } from './types'; + +const BASE = '/api/v1'; + +async function fetchJSON(url: string, init?: RequestInit): Promise { + const res = await fetch(url, { + headers: { 'Content-Type': 'application/json', ...init?.headers }, + ...init, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(body.message || body.error || `HTTP ${res.status}`); + } + return res.json(); +} + +// Certificates +export const getCertificates = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/certificates?${qs}`); +}; + +export const getCertificate = (id: string) => + fetchJSON(`${BASE}/certificates/${id}`); + +export const getCertificateVersions = (id: string) => + fetchJSON>(`${BASE}/certificates/${id}/versions`); + +export const createCertificate = (data: Partial) => + fetchJSON(`${BASE}/certificates`, { method: 'POST', body: JSON.stringify(data) }); + +export const triggerRenewal = (id: string) => + fetchJSON<{ message: string }>(`${BASE}/certificates/${id}/renew`, { method: 'POST' }); + +export const triggerDeployment = (id: string, targetId: string) => + fetchJSON<{ message: string }>(`${BASE}/certificates/${id}/deploy`, { + method: 'POST', + body: JSON.stringify({ target_id: targetId }), + }); + +// Agents +export const getAgents = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/agents?${qs}`); +}; + +export const getAgent = (id: string) => + fetchJSON(`${BASE}/agents/${id}`); + +// Jobs +export const getJobs = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/jobs?${qs}`); +}; + +export const cancelJob = (id: string) => + fetchJSON<{ message: string }>(`${BASE}/jobs/${id}/cancel`, { method: 'POST' }); + +// Notifications +export const getNotifications = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/notifications?${qs}`); +}; + +// Audit +export const getAuditEvents = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/audit?${qs}`); +}; + +// Policies +export const getPolicies = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/policies?${qs}`); +}; + +// Issuers +export const getIssuers = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/issuers?${qs}`); +}; + +// Targets +export const getTargets = (params: Record = {}) => { + const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); + return fetchJSON>(`${BASE}/targets?${qs}`); +}; + +// Health +export const getHealth = () => fetchJSON<{ status: string }>('/health'); diff --git a/web/src/api/types.ts b/web/src/api/types.ts new file mode 100644 index 0000000..d542adc --- /dev/null +++ b/web/src/api/types.ts @@ -0,0 +1,133 @@ +export interface Certificate { + id: string; + name: string; + common_name: string; + sans: string[]; + status: string; + environment: string; + issuer_id: string; + owner_id: string; + team_id: string; + renewal_policy_id: string; + serial_number: string; + fingerprint: string; + key_algorithm: string; + key_size: number; + issued_at: string; + expires_at: string; + tags: Record; + created_at: string; + updated_at: string; +} + +export interface CertificateVersion { + id: string; + certificate_id: string; + version: number; + serial_number: string; + fingerprint: string; + cert_pem: string; + chain_pem: string; + csr_pem: string; + not_before: string; + not_after: string; + created_at: string; +} + +export interface Agent { + id: string; + name: string; + hostname: string; + ip_address: string; + status: string; + version: string; + last_heartbeat: string; + capabilities: string[]; + tags: Record; + created_at: string; + updated_at: string; +} + +export interface Job { + id: string; + certificate_id: string; + type: string; + status: string; + attempts: number; + max_attempts: number; + error_message: string; + scheduled_at: string; + started_at: string; + completed_at: string; + created_at: string; +} + +export interface Notification { + id: string; + type: string; + channel: string; + recipient: string; + subject: string; + message: string; + status: string; + certificate_id: string; + created_at: string; +} + +export interface AuditEvent { + id: string; + actor: string; + actor_type: string; + action: string; + resource_type: string; + resource_id: string; + details: Record; + timestamp: string; +} + +export interface PolicyRule { + id: string; + name: string; + type: string; + severity: string; + config: Record; + enabled: boolean; + created_at: string; + updated_at: string; +} + +export interface PolicyViolation { + id: string; + rule_id: string; + certificate_id: string; + severity: string; + message: string; + created_at: string; +} + +export interface Issuer { + id: string; + name: string; + type: string; + config: Record; + status: string; + created_at: string; +} + +export interface Target { + id: string; + name: string; + type: string; + hostname: string; + agent_id: string; + config: Record; + status: string; + created_at: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + per_page: number; +} diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts new file mode 100644 index 0000000..a7f3846 --- /dev/null +++ b/web/src/api/utils.ts @@ -0,0 +1,37 @@ +export function formatDate(iso: string): string { + if (!iso) return '—'; + return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); +} + +export function formatDateTime(iso: string): string { + if (!iso) return '—'; + return new Date(iso).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); +} + +export function timeAgo(iso: string): string { + if (!iso) return '—'; + const now = Date.now(); + const then = new Date(iso).getTime(); + const diff = now - then; + const mins = Math.floor(diff / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return formatDate(iso); +} + +export function daysUntil(iso: string): number { + if (!iso) return 0; + return Math.ceil((new Date(iso).getTime() - Date.now()) / 86400000); +} + +export function expiryColor(days: number): string { + if (days <= 0) return 'text-red-400'; + if (days <= 7) return 'text-red-400'; + if (days <= 14) return 'text-amber-400'; + if (days <= 30) return 'text-amber-300'; + return 'text-emerald-400'; +} diff --git a/web/src/components/DataTable.tsx b/web/src/components/DataTable.tsx new file mode 100644 index 0000000..5518bd7 --- /dev/null +++ b/web/src/components/DataTable.tsx @@ -0,0 +1,69 @@ +interface Column { + key: string; + label: string; + render: (item: T) => React.ReactNode; + className?: string; +} + +interface DataTableProps { + columns: Column[]; + data: T[]; + onRowClick?: (item: T) => void; + emptyMessage?: string; + isLoading?: boolean; +} + +export default function DataTable({ columns, data, onRowClick, emptyMessage, isLoading }: DataTableProps) { + if (isLoading) { + return ( +
+ + + + + Loading... +
+ ); + } + + if (!data.length) { + return ( +
+ {emptyMessage || 'No data found'} +
+ ); + } + + return ( +
+ + + + {columns.map(col => ( + + ))} + + + + {data.map((item, i) => ( + onRowClick?.(item)} + className={`border-b border-slate-700/50 transition-colors hover:bg-blue-500/5 ${onRowClick ? 'cursor-pointer' : ''}`} + > + {columns.map(col => ( + + ))} + + ))} + +
+ {col.label} +
+ {col.render(item)} +
+
+ ); +} + +export type { Column }; diff --git a/web/src/components/ErrorState.tsx b/web/src/components/ErrorState.tsx new file mode 100644 index 0000000..18b40e9 --- /dev/null +++ b/web/src/components/ErrorState.tsx @@ -0,0 +1,21 @@ +interface ErrorStateProps { + error: Error; + onRetry?: () => void; +} + +export default function ErrorState({ error, onRetry }: ErrorStateProps) { + return ( +
+ + + +

Failed to load data

+

{error.message}

+ {onRetry && ( + + )} +
+ ); +} diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx new file mode 100644 index 0000000..fc4c913 --- /dev/null +++ b/web/src/components/Layout.tsx @@ -0,0 +1,60 @@ +import { NavLink, Outlet } from 'react-router-dom'; + +const nav = [ + { to: '/', label: 'Dashboard', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4' }, + { to: '/certificates', label: 'Certificates', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' }, + { to: '/agents', label: 'Agents', icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2' }, + { to: '/jobs', label: 'Jobs', icon: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' }, + { to: '/notifications', label: 'Notifications', icon: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9' }, + { to: '/policies', label: 'Policies', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4' }, + { to: '/audit', label: 'Audit Trail', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' }, +]; + +function Icon({ d }: { d: string }) { + return ( + + + + ); +} + +export default function Layout() { + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ +
+
+ ); +} diff --git a/web/src/components/PageHeader.tsx b/web/src/components/PageHeader.tsx new file mode 100644 index 0000000..5f80a9c --- /dev/null +++ b/web/src/components/PageHeader.tsx @@ -0,0 +1,17 @@ +interface PageHeaderProps { + title: string; + subtitle?: string; + action?: React.ReactNode; +} + +export default function PageHeader({ title, subtitle, action }: PageHeaderProps) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {action} +
+ ); +} diff --git a/web/src/components/StatusBadge.tsx b/web/src/components/StatusBadge.tsx new file mode 100644 index 0000000..f42b168 --- /dev/null +++ b/web/src/components/StatusBadge.tsx @@ -0,0 +1,29 @@ +const statusStyles: Record = { + Active: 'badge-success', + Expiring: 'badge-warning', + Expired: 'badge-danger', + RenewalInProgress: 'badge-info', + PendingIssuance: 'badge-info', + Archived: 'badge-neutral', + Revoked: 'badge-danger', + // Job statuses + Pending: 'badge-info', + Running: 'badge-warning', + Completed: 'badge-success', + Failed: 'badge-danger', + Cancelled: 'badge-neutral', + // Agent statuses + Online: 'badge-success', + Offline: 'badge-danger', + Stale: 'badge-warning', + // Notification statuses + sent: 'badge-success', + pending: 'badge-warning', + failed: 'badge-danger', + read: 'badge-neutral', +}; + +export default function StatusBadge({ status }: { status: string }) { + const cls = statusStyles[status] || 'badge-neutral'; + return {status}; +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..dd1c20c --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,30 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-slate-900 text-slate-100 antialiased; + } +} + +@layer components { + .badge { + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; + } + .badge-success { @apply bg-emerald-500/10 text-emerald-400 border border-emerald-500/20; } + .badge-warning { @apply bg-amber-500/10 text-amber-400 border border-amber-500/20; } + .badge-danger { @apply bg-red-500/10 text-red-400 border border-red-500/20; } + .badge-info { @apply bg-blue-500/10 text-blue-400 border border-blue-500/20; } + .badge-neutral { @apply bg-slate-500/10 text-slate-400 border border-slate-500/20; } + + .card { + @apply bg-slate-800 border border-slate-700 rounded-lg; + } + + .btn { + @apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors; + } + .btn-primary { @apply bg-blue-600 hover:bg-blue-500 text-white; } + .btn-ghost { @apply hover:bg-slate-700 text-slate-300; } +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..d4d12cd --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,45 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import Layout from './components/Layout'; +import DashboardPage from './pages/DashboardPage'; +import CertificatesPage from './pages/CertificatesPage'; +import CertificateDetailPage from './pages/CertificateDetailPage'; +import AgentsPage from './pages/AgentsPage'; +import JobsPage from './pages/JobsPage'; +import NotificationsPage from './pages/NotificationsPage'; +import PoliciesPage from './pages/PoliciesPage'; +import AuditPage from './pages/AuditPage'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + retry: 1, + refetchOnWindowFocus: true, + }, + }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + +); diff --git a/web/src/pages/AgentsPage.tsx b/web/src/pages/AgentsPage.tsx new file mode 100644 index 0000000..712ffa6 --- /dev/null +++ b/web/src/pages/AgentsPage.tsx @@ -0,0 +1,64 @@ +import { useQuery } from '@tanstack/react-query'; +import { getAgents } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import DataTable from '../components/DataTable'; +import type { Column } from '../components/DataTable'; +import StatusBadge from '../components/StatusBadge'; +import ErrorState from '../components/ErrorState'; +import { timeAgo } from '../api/utils'; +import type { Agent } from '../api/types'; + +function heartbeatStatus(lastHeartbeat: string): string { + if (!lastHeartbeat) return 'Offline'; + const ago = Date.now() - new Date(lastHeartbeat).getTime(); + if (ago < 5 * 60 * 1000) return 'Online'; + if (ago < 15 * 60 * 1000) return 'Stale'; + return 'Offline'; +} + +export default function AgentsPage() { + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['agents'], + queryFn: () => getAgents(), + refetchInterval: 15000, + }); + + const columns: Column[] = [ + { + key: 'name', + label: 'Agent', + render: (a) => ( +
+
{a.name}
+
{a.id}
+
+ ), + }, + { + key: 'status', + label: 'Health', + render: (a) => , + }, + { key: 'hostname', label: 'Hostname', render: (a) => {a.hostname || '—'} }, + { key: 'ip', label: 'IP Address', render: (a) => {a.ip_address || '—'} }, + { key: 'version', label: 'Version', render: (a) => {a.version || '—'} }, + { + key: 'heartbeat', + label: 'Last Heartbeat', + render: (a) => {timeAgo(a.last_heartbeat)}, + }, + ]; + + return ( + <> + +
+ {error ? ( + refetch()} /> + ) : ( + + )} +
+ + ); +} diff --git a/web/src/pages/AuditPage.tsx b/web/src/pages/AuditPage.tsx new file mode 100644 index 0000000..abd8da3 --- /dev/null +++ b/web/src/pages/AuditPage.tsx @@ -0,0 +1,86 @@ +import { useQuery } from '@tanstack/react-query'; +import { getAuditEvents } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import DataTable from '../components/DataTable'; +import type { Column } from '../components/DataTable'; +import ErrorState from '../components/ErrorState'; +import { formatDateTime } from '../api/utils'; +import type { AuditEvent } from '../api/types'; + +const actionColors: Record = { + certificate_created: 'text-emerald-400', + renewal_triggered: 'text-blue-400', + renewal_job_created: 'text-blue-400', + renewal_completed: 'text-emerald-400', + deployment_completed: 'text-emerald-400', + deployment_failed: 'text-red-400', + expiration_alert_sent: 'text-amber-400', + agent_registered: 'text-blue-400', + policy_violated: 'text-red-400', +}; + +export default function AuditPage() { + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['audit'], + queryFn: () => getAuditEvents(), + refetchInterval: 30000, + }); + + const columns: Column[] = [ + { + key: 'action', + label: 'Action', + render: (e) => ( + + {e.action.replace(/_/g, ' ')} + + ), + }, + { + key: 'actor', + label: 'Actor', + render: (e) => ( +
+
{e.actor}
+
{e.actor_type}
+
+ ), + }, + { + key: 'resource', + label: 'Resource', + render: (e) => ( +
+
{e.resource_type}
+
{e.resource_id}
+
+ ), + }, + { + key: 'details', + label: 'Details', + render: (e) => { + if (!e.details || Object.keys(e.details).length === 0) return ; + return ( + + {JSON.stringify(e.details).slice(0, 60)} + + ); + }, + }, + { key: 'time', label: 'Time', render: (e) => {formatDateTime(e.timestamp)} }, + ]; + + return ( + <> + +
+ {error ? ( + refetch()} /> + ) : ( + + )} +
+ + ); +} diff --git a/web/src/pages/CertificateDetailPage.tsx b/web/src/pages/CertificateDetailPage.tsx new file mode 100644 index 0000000..0bcd8f6 --- /dev/null +++ b/web/src/pages/CertificateDetailPage.tsx @@ -0,0 +1,168 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getCertificate, getCertificateVersions, triggerRenewal } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import StatusBadge from '../components/StatusBadge'; +import ErrorState from '../components/ErrorState'; +import { formatDate, formatDateTime, daysUntil, expiryColor } from '../api/utils'; + +function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ); +} + +export default function CertificateDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const { data: cert, isLoading, error, refetch } = useQuery({ + queryKey: ['certificate', id], + queryFn: () => getCertificate(id!), + enabled: !!id, + }); + + const { data: versions } = useQuery({ + queryKey: ['certificate-versions', id], + queryFn: () => getCertificateVersions(id!), + enabled: !!id, + }); + + const renewMutation = useMutation({ + mutationFn: () => triggerRenewal(id!), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['certificate', id] }); + queryClient.invalidateQueries({ queryKey: ['certificates'] }); + }, + }); + + if (isLoading) { + return ( + <> + +
Loading...
+ + ); + } + + if (error || !cert) { + return ( + <> + + refetch()} /> + + ); + } + + const days = daysUntil(cert.expires_at); + + return ( + <> + + + + + } + /> +
+ {renewMutation.isSuccess && ( +
+ Renewal triggered successfully. A renewal job has been created. +
+ )} + {renewMutation.isError && ( +
+ Failed to trigger renewal: {(renewMutation.error as Error).message} +
+ )} + +
+ {/* Certificate Info */} +
+

Certificate Details

+ } /> + + + + {cert.fingerprint.slice(0, 24)}... : '—' + } /> + + +
+ + {/* Lifecycle */} +
+

Lifecycle

+ + + {formatDate(cert.expires_at)} ({days <= 0 ? 'expired' : `${days} days`}) + + } /> + + + + + + + +
+
+ + {/* Tags */} + {cert.tags && Object.keys(cert.tags).length > 0 && ( +
+

Tags

+
+ {Object.entries(cert.tags).map(([k, v]) => ( + {k}: {v} + ))} +
+
+ )} + + {/* Version History */} +
+

+ Version History {versions?.data?.length ? `(${versions.data.length})` : ''} +

+ {!versions?.data?.length ? ( +

No versions yet

+ ) : ( +
+ {versions.data.map((v) => ( +
+
+
Version {v.version}
+
{v.serial_number}
+
+
+
{formatDate(v.not_before)} — {formatDate(v.not_after)}
+
{formatDateTime(v.created_at)}
+
+
+ ))} +
+ )} +
+
+ + ); +} diff --git a/web/src/pages/CertificatesPage.tsx b/web/src/pages/CertificatesPage.tsx new file mode 100644 index 0000000..4e12f43 --- /dev/null +++ b/web/src/pages/CertificatesPage.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { getCertificates } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import DataTable from '../components/DataTable'; +import type { Column } from '../components/DataTable'; +import StatusBadge from '../components/StatusBadge'; +import ErrorState from '../components/ErrorState'; +import { formatDate, daysUntil, expiryColor } from '../api/utils'; +import type { Certificate } from '../api/types'; + +export default function CertificatesPage() { + const navigate = useNavigate(); + const [statusFilter, setStatusFilter] = useState(''); + const [envFilter, setEnvFilter] = useState(''); + + const params: Record = {}; + if (statusFilter) params.status = statusFilter; + if (envFilter) params.environment = envFilter; + + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['certificates', params], + queryFn: () => getCertificates(params), + refetchInterval: 30000, + }); + + const columns: Column[] = [ + { + key: 'name', + label: 'Certificate', + render: (c) => ( +
+
{c.common_name}
+
{c.id}
+
+ ), + }, + { key: 'status', label: 'Status', render: (c) => }, + { + key: 'expires', + label: 'Expires', + render: (c) => { + const days = daysUntil(c.expires_at); + return ( +
+
{formatDate(c.expires_at)}
+
{days <= 0 ? 'Expired' : `${days} days`}
+
+ ); + }, + }, + { key: 'env', label: 'Environment', render: (c) => {c.environment || '—'} }, + { key: 'issuer', label: 'Issuer', render: (c) => {c.issuer_id} }, + { key: 'owner', label: 'Owner', render: (c) => {c.owner_id} }, + ]; + + return ( + <> + +
+ + +
+
+ {error ? ( + refetch()} /> + ) : ( + navigate(`/certificates/${c.id}`)} + emptyMessage="No certificates found" + /> + )} +
+ + ); +} diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..a29881a --- /dev/null +++ b/web/src/pages/DashboardPage.tsx @@ -0,0 +1,144 @@ +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { getCertificates, getAgents, getJobs, getNotifications, getHealth } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import StatusBadge from '../components/StatusBadge'; +import { daysUntil, expiryColor, formatDate } from '../api/utils'; + +function StatCard({ label, value, icon, color }: { label: string; value: string | number; icon: string; color: string }) { + const colorMap: Record = { + success: 'bg-emerald-500/10 text-emerald-400', + warning: 'bg-amber-500/10 text-amber-400', + danger: 'bg-red-500/10 text-red-400', + info: 'bg-blue-500/10 text-blue-400', + }; + return ( +
+
+ + + +
+
+

{label}

+

{value}

+
+
+ ); +} + +export default function DashboardPage() { + const navigate = useNavigate(); + + const { data: health } = useQuery({ queryKey: ['health'], queryFn: getHealth, refetchInterval: 30000 }); + const { data: certs } = useQuery({ queryKey: ['certificates', {}], queryFn: () => getCertificates(), refetchInterval: 30000 }); + const { data: agents } = useQuery({ queryKey: ['agents'], queryFn: () => getAgents(), refetchInterval: 15000 }); + const { data: jobs } = useQuery({ queryKey: ['jobs', {}], queryFn: () => getJobs(), refetchInterval: 10000 }); + const { data: notifs } = useQuery({ queryKey: ['notifications'], queryFn: () => getNotifications() }); + + const totalCerts = certs?.total || 0; + const expiringSoon = certs?.data?.filter(c => { + const d = daysUntil(c.expires_at); + return d > 0 && d <= 30; + }).length || 0; + const expired = certs?.data?.filter(c => c.status === 'Expired' || daysUntil(c.expires_at) <= 0).length || 0; + const activeAgents = agents?.data?.filter(a => a.status === 'Online').length || agents?.total || 0; + const pendingJobs = jobs?.data?.filter(j => j.status === 'Pending' || j.status === 'Running').length || 0; + + return ( + <> + +
+ {/* Stats */} +
+ + 0 ? 'warning' : 'success'} + icon="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> + 0 ? 'danger' : 'success'} + icon="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> + +
+ +
+ {/* Expiring Certificates */} +
+
+

Certificates Expiring Soon

+ +
+ {!certs?.data?.length ? ( +

No certificates

+ ) : ( +
+ {certs.data + .filter(c => c.status !== 'Archived') + .sort((a, b) => new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()) + .slice(0, 5) + .map(c => { + const days = daysUntil(c.expires_at); + return ( +
navigate(`/certificates/${c.id}`)} + className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-slate-700/50 cursor-pointer transition-colors" + > +
+
{c.common_name}
+
{c.environment || 'no env'}
+
+
+
+ {days <= 0 ? 'Expired' : `${days} days`} +
+
{formatDate(c.expires_at)}
+
+
+ ); + })} +
+ )} +
+ + {/* Recent Jobs */} +
+
+

Recent Jobs

+ +
+ {!jobs?.data?.length ? ( +

No jobs

+ ) : ( +
+ {jobs.data.slice(0, 5).map(j => ( +
+
+
{j.type}
+
{j.certificate_id}
+
+ +
+ ))} +
+ )} +
+
+ + {/* Pending Jobs Banner */} + {pendingJobs > 0 && ( +
+
+

{pendingJobs} pending job{pendingJobs > 1 ? 's' : ''}

+

Jobs are waiting to be processed

+
+ +
+ )} +
+ + ); +} diff --git a/web/src/pages/JobsPage.tsx b/web/src/pages/JobsPage.tsx new file mode 100644 index 0000000..2d889f7 --- /dev/null +++ b/web/src/pages/JobsPage.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getJobs, cancelJob } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import DataTable from '../components/DataTable'; +import type { Column } from '../components/DataTable'; +import StatusBadge from '../components/StatusBadge'; +import ErrorState from '../components/ErrorState'; +import { formatDateTime } from '../api/utils'; +import type { Job } from '../api/types'; + +export default function JobsPage() { + const [statusFilter, setStatusFilter] = useState(''); + const [typeFilter, setTypeFilter] = useState(''); + const queryClient = useQueryClient(); + + const params: Record = {}; + if (statusFilter) params.status = statusFilter; + if (typeFilter) params.type = typeFilter; + + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['jobs', params], + queryFn: () => getJobs(params), + refetchInterval: 10000, + }); + + const cancelMutation = useMutation({ + mutationFn: cancelJob, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }), + }); + + const columns: Column[] = [ + { + key: 'id', + label: 'Job', + render: (j) => ( +
+
{j.id}
+
{j.type}
+
+ ), + }, + { key: 'status', label: 'Status', render: (j) => }, + { key: 'cert', label: 'Certificate', render: (j) => {j.certificate_id} }, + { + key: 'attempts', + label: 'Attempts', + render: (j) => {j.attempts}/{j.max_attempts}, + }, + { key: 'scheduled', label: 'Scheduled', render: (j) => {formatDateTime(j.scheduled_at)} }, + { key: 'completed', label: 'Completed', render: (j) => {formatDateTime(j.completed_at)} }, + { + key: 'actions', + label: '', + render: (j) => ( + j.status === 'Pending' || j.status === 'Running' ? ( + + ) : null + ), + }, + ]; + + return ( + <> + +
+ + +
+
+ {error ? ( + refetch()} /> + ) : ( + + )} +
+ + ); +} diff --git a/web/src/pages/NotificationsPage.tsx b/web/src/pages/NotificationsPage.tsx new file mode 100644 index 0000000..d963bc9 --- /dev/null +++ b/web/src/pages/NotificationsPage.tsx @@ -0,0 +1,48 @@ +import { useQuery } from '@tanstack/react-query'; +import { getNotifications } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import DataTable from '../components/DataTable'; +import type { Column } from '../components/DataTable'; +import StatusBadge from '../components/StatusBadge'; +import ErrorState from '../components/ErrorState'; +import { formatDateTime } from '../api/utils'; +import type { Notification } from '../api/types'; + +export default function NotificationsPage() { + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['notifications'], + queryFn: () => getNotifications(), + refetchInterval: 30000, + }); + + const columns: Column[] = [ + { + key: 'type', + label: 'Type', + render: (n) => {n.type.replace(/([A-Z])/g, ' $1').trim()}, + }, + { key: 'status', label: 'Status', render: (n) => }, + { key: 'channel', label: 'Channel', render: (n) => {n.channel} }, + { key: 'recipient', label: 'Recipient', render: (n) => {n.recipient} }, + { + key: 'message', + label: 'Message', + render: (n) => {n.message || n.subject}, + }, + { key: 'cert', label: 'Certificate', render: (n) => {n.certificate_id || '—'} }, + { key: 'created', label: 'Sent', render: (n) => {formatDateTime(n.created_at)} }, + ]; + + return ( + <> + +
+ {error ? ( + refetch()} /> + ) : ( + + )} +
+ + ); +} diff --git a/web/src/pages/PoliciesPage.tsx b/web/src/pages/PoliciesPage.tsx new file mode 100644 index 0000000..0830caa --- /dev/null +++ b/web/src/pages/PoliciesPage.tsx @@ -0,0 +1,64 @@ +import { useQuery } from '@tanstack/react-query'; +import { getPolicies } from '../api/client'; +import PageHeader from '../components/PageHeader'; +import DataTable from '../components/DataTable'; +import type { Column } from '../components/DataTable'; +import ErrorState from '../components/ErrorState'; +import { formatDateTime } from '../api/utils'; +import type { PolicyRule } from '../api/types'; + +const severityStyles: Record = { + low: 'badge-info', + medium: 'badge-warning', + high: 'badge-danger', + critical: 'badge-danger', +}; + +export default function PoliciesPage() { + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['policies'], + queryFn: () => getPolicies(), + }); + + const columns: Column[] = [ + { + key: 'name', + label: 'Rule', + render: (p) => ( +
+
{p.name}
+
{p.id}
+
+ ), + }, + { key: 'type', label: 'Type', render: (p) => {p.type.replace(/_/g, ' ')} }, + { + key: 'severity', + label: 'Severity', + render: (p) => {p.severity}, + }, + { + key: 'enabled', + label: 'Enabled', + render: (p) => ( + + {p.enabled ? 'Yes' : 'No'} + + ), + }, + { key: 'created', label: 'Created', render: (p) => {formatDateTime(p.created_at)} }, + ]; + + return ( + <> + +
+ {error ? ( + refetch()} /> + ) : ( + + )} +
+ + ); +} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs new file mode 100644 index 0000000..7e045bd --- /dev/null +++ b/web/tailwind.config.cjs @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + darkMode: 'class', + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..75e2ff7 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..610acf5 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:8443', + '/health': 'http://localhost:8443', + } + }, + build: { + outDir: 'dist', + sourcemap: false, + } +})