diff --git a/internal/config/config.go b/internal/config/config.go index 4822f78..85e3e54 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,7 +13,8 @@ import ( const ( defaultBackupDriver = "longhorn" defaultResticImage = "restic/restic:0.16.4" - defaultJobTTLSeconds = 86400 + defaultJobTTLSeconds = 259200 + defaultJobBackoff = 1 defaultListenAddr = ":8080" defaultResticSecret = "soteria-restic" defaultLonghornURL = "http://longhorn-backend.longhorn-system.svc:9500" @@ -26,53 +27,59 @@ const ( defaultUsageSecret = "soteria-backup-usage" defaultB2ScanInterval = 15 * time.Minute defaultB2ScanTimeout = 2 * time.Minute + defaultExcludedSCs = "local-path" serviceNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" ) // Config holds the runtime settings used to build and serve Soteria. type Config struct { - Namespace string - SecretNamespace string - BackupDriver string - ResticImage string - ResticRepository string - ResticSecretName string - ResticBackupArgs []string - ResticForgetArgs []string - S3Endpoint string - S3Region string - JobTTLSeconds int32 - JobNodeSelector map[string]string - JobCPURequest string - JobCPULimit string - JobMemoryRequest string - JobMemoryLimit string - WorkerServiceAccount string - ListenAddr string - LonghornURL string - LonghornBackupMode string - AuthRequired bool - AllowedGroups []string - AuthBearerTokens []string - MetricsRefreshInterval time.Duration - PolicyEvalInterval time.Duration - PolicyBackupsPerCycle int - PolicySecretName string - UsageSecretName string - BackupMaxAge time.Duration - B2Enabled bool - B2Endpoint string - B2Region string - B2Buckets []string - B2AccessKeyID string - B2SecretAccessKey string - B2SecretNamespace string - B2SecretName string - B2AccessKeyField string - B2SecretKeyField string - B2EndpointField string - B2ScanInterval time.Duration - B2ScanTimeout time.Duration + Namespace string + SecretNamespace string + BackupDriver string + ResticImage string + ResticRepository string + ResticSecretName string + ResticBackupArgs []string + ResticForgetArgs []string + ResticPruneAfterBackup bool + ResticUnlockBeforeBackup bool + S3Endpoint string + S3Region string + JobTTLSeconds int32 + JobBackoffLimit int32 + JobNodeSelector map[string]string + JobCPURequest string + JobCPULimit string + JobMemoryRequest string + JobMemoryLimit string + ExcludedStorageClasses []string + ExcludedPVCs []string + WorkerServiceAccount string + ListenAddr string + LonghornURL string + LonghornBackupMode string + AuthRequired bool + AllowedGroups []string + AuthBearerTokens []string + MetricsRefreshInterval time.Duration + PolicyEvalInterval time.Duration + PolicyBackupsPerCycle int + PolicySecretName string + UsageSecretName string + BackupMaxAge time.Duration + B2Enabled bool + B2Endpoint string + B2Region string + B2Buckets []string + B2AccessKeyID string + B2SecretAccessKey string + B2SecretNamespace string + B2SecretName string + B2AccessKeyField string + B2SecretKeyField string + B2EndpointField string + B2ScanInterval time.Duration + B2ScanTimeout time.Duration } // Load builds the runtime configuration from environment variables and cluster state. @@ -99,6 +106,8 @@ func Load() (*Config, error) { cfg.ResticSecretName = getenvDefault("SOTERIA_RESTIC_SECRET_NAME", defaultResticSecret) cfg.ResticBackupArgs = strings.Fields(getenv("SOTERIA_RESTIC_BACKUP_ARGS")) cfg.ResticForgetArgs = strings.Fields(getenv("SOTERIA_RESTIC_FORGET_ARGS")) + cfg.ResticPruneAfterBackup = getenvBool("SOTERIA_RESTIC_PRUNE_AFTER_BACKUP") + cfg.ResticUnlockBeforeBackup = getenvBool("SOTERIA_RESTIC_UNLOCK_BEFORE_BACKUP") cfg.S3Endpoint = getenv("SOTERIA_S3_ENDPOINT") cfg.S3Region = getenv("SOTERIA_S3_REGION") cfg.WorkerServiceAccount = getenv("SOTERIA_JOB_SERVICE_ACCOUNT") @@ -108,6 +117,8 @@ func Load() (*Config, error) { cfg.JobCPULimit = getenv("SOTERIA_JOB_CPU_LIMIT") cfg.JobMemoryRequest = getenv("SOTERIA_JOB_MEMORY_REQUEST") cfg.JobMemoryLimit = getenv("SOTERIA_JOB_MEMORY_LIMIT") + cfg.ExcludedStorageClasses = parseCSV(getenvDefault("SOTERIA_EXCLUDED_STORAGE_CLASSES", defaultExcludedSCs)) + cfg.ExcludedPVCs = parseCSV(getenv("SOTERIA_EXCLUDED_PVCS")) cfg.LonghornURL = getenvDefault("SOTERIA_LONGHORN_URL", defaultLonghornURL) cfg.LonghornBackupMode = getenvDefault("SOTERIA_LONGHORN_BACKUP_MODE", defaultLonghornMode) cfg.AuthRequired = getenvBool("SOTERIA_AUTH_REQUIRED") @@ -137,6 +148,11 @@ func Load() (*Config, error) { } else { cfg.JobTTLSeconds = defaultJobTTLSeconds } + if retries, ok := getenvInt("SOTERIA_JOB_BACKOFF_LIMIT"); ok { + cfg.JobBackoffLimit = int32(retries) + } else { + cfg.JobBackoffLimit = defaultJobBackoff + } if seconds, ok := getenvInt("SOTERIA_METRICS_REFRESH_SECONDS"); ok { cfg.MetricsRefreshInterval = time.Duration(seconds) * time.Second @@ -182,6 +198,12 @@ func Load() (*Config, error) { if cfg.JobNodeSelector == nil { return nil, errors.New("SOTERIA_JOB_NODE_SELECTOR is invalid; expected key=value pairs") } + if cfg.JobTTLSeconds <= 0 { + return nil, errors.New("SOTERIA_JOB_TTL_SECONDS must be greater than zero") + } + if cfg.JobBackoffLimit < 0 { + return nil, errors.New("SOTERIA_JOB_BACKOFF_LIMIT must be greater than or equal to zero") + } if err := validateQuantity("SOTERIA_JOB_CPU_REQUEST", cfg.JobCPURequest); err != nil { return nil, err } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 599b189..12cd518 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -22,13 +22,18 @@ func clearConfigEnv(t *testing.T) { "SOTERIA_RESTIC_SECRET_NAME", "SOTERIA_RESTIC_BACKUP_ARGS", "SOTERIA_RESTIC_FORGET_ARGS", + "SOTERIA_RESTIC_PRUNE_AFTER_BACKUP", + "SOTERIA_RESTIC_UNLOCK_BEFORE_BACKUP", "SOTERIA_S3_ENDPOINT", "SOTERIA_S3_REGION", "SOTERIA_JOB_SERVICE_ACCOUNT", "SOTERIA_LISTEN_ADDR", "SOTERIA_JOB_NODE_SELECTOR", + "SOTERIA_JOB_BACKOFF_LIMIT", "SOTERIA_LONGHORN_URL", "SOTERIA_LONGHORN_BACKUP_MODE", + "SOTERIA_EXCLUDED_STORAGE_CLASSES", + "SOTERIA_EXCLUDED_PVCS", "SOTERIA_AUTH_REQUIRED", "SOTERIA_ALLOWED_GROUPS", "SOTERIA_AUTH_BEARER_TOKENS", @@ -82,6 +87,12 @@ func TestLoadDefaultsForLonghorn(t *testing.T) { if cfg.JobTTLSeconds != defaultJobTTLSeconds { t.Fatalf("expected default ttl %d, got %d", defaultJobTTLSeconds, cfg.JobTTLSeconds) } + if cfg.JobBackoffLimit != defaultJobBackoff { + t.Fatalf("expected default backoff %d, got %d", defaultJobBackoff, cfg.JobBackoffLimit) + } + if len(cfg.ExcludedStorageClasses) != 1 || cfg.ExcludedStorageClasses[0] != "local-path" { + t.Fatalf("expected local-path to be excluded by default, got %#v", cfg.ExcludedStorageClasses) + } if cfg.MetricsRefreshInterval != defaultMetricsRefresh || cfg.PolicyEvalInterval != defaultPolicyEval { t.Fatalf("unexpected interval defaults: %#v", cfg) } @@ -108,6 +119,8 @@ func TestLoadSupportsResticAndB2Overrides(t *testing.T) { withEnv(t, "SOTERIA_RESTIC_SECRET_NAME", "restic-creds") withEnv(t, "SOTERIA_RESTIC_BACKUP_ARGS", "--one-file-system --exclude /tmp") withEnv(t, "SOTERIA_RESTIC_FORGET_ARGS", "--keep-last 5 --prune") + withEnv(t, "SOTERIA_RESTIC_PRUNE_AFTER_BACKUP", "true") + withEnv(t, "SOTERIA_RESTIC_UNLOCK_BEFORE_BACKUP", "true") withEnv(t, "SOTERIA_S3_ENDPOINT", "https://b2.example.invalid") withEnv(t, "SOTERIA_S3_REGION", "us-west-000") withEnv(t, "SOTERIA_JOB_SERVICE_ACCOUNT", "soteria-worker") @@ -115,6 +128,9 @@ func TestLoadSupportsResticAndB2Overrides(t *testing.T) { withEnv(t, "SOTERIA_JOB_CPU_LIMIT", "1") withEnv(t, "SOTERIA_JOB_MEMORY_REQUEST", "256Mi") withEnv(t, "SOTERIA_JOB_MEMORY_LIMIT", "2Gi") + withEnv(t, "SOTERIA_JOB_BACKOFF_LIMIT", "2") + withEnv(t, "SOTERIA_EXCLUDED_STORAGE_CLASSES", "local-path, ephemeral") + withEnv(t, "SOTERIA_EXCLUDED_PVCS", "jenkins/jenkins-dind-cache, apps/tmp-*") withEnv(t, "SOTERIA_LISTEN_ADDR", ":9090") withEnv(t, "SOTERIA_JOB_NODE_SELECTOR", "hardware=rpi5,role=worker") withEnv(t, "SOTERIA_AUTH_REQUIRED", "true") @@ -156,12 +172,21 @@ func TestLoadSupportsResticAndB2Overrides(t *testing.T) { if strings.Join(cfg.ResticForgetArgs, " ") != "--keep-last 5 --prune" { t.Fatalf("unexpected forget args: %#v", cfg.ResticForgetArgs) } + if !cfg.ResticPruneAfterBackup || !cfg.ResticUnlockBeforeBackup { + t.Fatalf("expected explicit restic safety overrides, got prune=%v unlock=%v", cfg.ResticPruneAfterBackup, cfg.ResticUnlockBeforeBackup) + } if cfg.WorkerServiceAccount != "soteria-worker" || cfg.ListenAddr != ":9090" { t.Fatalf("unexpected worker/listen config: %#v", cfg) } if cfg.JobCPURequest != "100m" || cfg.JobCPULimit != "1" || cfg.JobMemoryRequest != "256Mi" || cfg.JobMemoryLimit != "2Gi" { t.Fatalf("unexpected job resources: %#v", cfg) } + if cfg.JobBackoffLimit != 2 { + t.Fatalf("unexpected job backoff: %#v", cfg.JobBackoffLimit) + } + if strings.Join(cfg.ExcludedStorageClasses, ",") != "local-path,ephemeral" || strings.Join(cfg.ExcludedPVCs, ",") != "jenkins/jenkins-dind-cache,apps/tmp-*" { + t.Fatalf("unexpected exclusions: storage=%#v pvcs=%#v", cfg.ExcludedStorageClasses, cfg.ExcludedPVCs) + } if cfg.JobNodeSelector["hardware"] != "rpi5" || cfg.JobNodeSelector["role"] != "worker" { t.Fatalf("unexpected node selector: %#v", cfg.JobNodeSelector) } @@ -265,6 +290,20 @@ func TestLoadRejectsInvalidConfigurations(t *testing.T) { }, substr: "SOTERIA_BACKUP_MAX_AGE_HOURS must be greater than zero", }, + { + name: "invalid job ttl", + env: map[string]string{ + "SOTERIA_JOB_TTL_SECONDS": "0", + }, + substr: "SOTERIA_JOB_TTL_SECONDS must be greater than zero", + }, + { + name: "invalid job backoff", + env: map[string]string{ + "SOTERIA_JOB_BACKOFF_LIMIT": "-1", + }, + substr: "SOTERIA_JOB_BACKOFF_LIMIT must be greater than or equal to zero", + }, { name: "b2 enabled without endpoint or secret", env: map[string]string{ diff --git a/internal/k8s/job_helpers_test.go b/internal/k8s/job_helpers_test.go index b0fe509..ce3dc6f 100644 --- a/internal/k8s/job_helpers_test.go +++ b/internal/k8s/job_helpers_test.go @@ -82,7 +82,7 @@ func TestJobNameBackupCommandAndResticEnvCoverRemainingBranches(t *testing.T) { Dedupe: &dedupe, KeepLast: &keepLast, }) - if !strings.Contains(cmd, "restic unlock") || !strings.Contains(cmd, "restic cat config") || !strings.Contains(cmd, "restic rebuild-index") || !strings.Contains(cmd, "restic init") || !strings.Contains(cmd, "--tag dedupe=off") || !strings.Contains(cmd, "--keep-last 3") { + if strings.Contains(cmd, "restic unlock") || strings.Contains(cmd, "--prune") || !strings.Contains(cmd, "set -eu;") || !strings.Contains(cmd, "restic cat config") || !strings.Contains(cmd, "restic rebuild-index") || !strings.Contains(cmd, "restic init") || !strings.Contains(cmd, "--tag dedupe=off") || !strings.Contains(cmd, "--keep-last 3") { t.Fatalf("expected backup command to include bootstrap/dedupe/keep-last logic, got %q", cmd) } if !strings.Contains(cmd, "--exclude *.tmp") || !strings.Contains(cmd, "--tag nightly") || !strings.Contains(cmd, "--tag prod") { @@ -94,6 +94,17 @@ func TestJobNameBackupCommandAndResticEnvCoverRemainingBranches(t *testing.T) { t.Fatalf("expected backup command to use configured forget args, got %q", cmd) } + cfg.ResticPruneAfterBackup = true + cfg.ResticUnlockBeforeBackup = true + cfg.ResticForgetArgs = nil + cmd = backupCommand(cfg, api.BackupRequest{PVC: "data", KeepLast: &keepLast}) + if !strings.Contains(cmd, "restic unlock") || !strings.Contains(cmd, "--keep-last 3 --prune") { + t.Fatalf("expected explicit unlock/prune opts to be honored, got %q", cmd) + } + if !strings.Contains(cmd, "ensure_restic_repo") || !strings.Contains(cmd, "already initialized") { + t.Fatalf("expected robust restic bootstrap handling, got %q", cmd) + } + env := resticEnv(cfg, "restic-secret", "") values := map[string]string{} for _, item := range env { diff --git a/internal/k8s/job_manifests.go b/internal/k8s/job_manifests.go index 1f01868..be48988 100644 --- a/internal/k8s/job_manifests.go +++ b/internal/k8s/job_manifests.go @@ -80,7 +80,7 @@ func buildBackupJob(cfg *config.Config, req api.BackupRequest, jobName, secretNa Annotations: annotations, }, Spec: batchv1.JobSpec{ - BackoffLimit: int32Ptr(0), + BackoffLimit: int32Ptr(cfg.JobBackoffLimit), TTLSecondsAfterFinished: int32Ptr(cfg.JobTTLSeconds), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: labels, Annotations: annotations}, @@ -161,7 +161,7 @@ func buildRestoreJob(cfg *config.Config, req api.RestoreTestRequest, jobName, se Annotations: annotations, }, Spec: batchv1.JobSpec{ - BackoffLimit: int32Ptr(0), + BackoffLimit: int32Ptr(cfg.JobBackoffLimit), TTLSecondsAfterFinished: int32Ptr(cfg.JobTTLSeconds), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: labels, Annotations: annotations}, @@ -216,27 +216,38 @@ func backupCommand(cfg *config.Config, req api.BackupRequest) string { cmd := strings.Join(args, " ") if keepLast > 0 { - forget := strings.Join([]string{ + forgetArgs := []string{ "restic", "forget", "--host", "soteria", "--group-by", "host,tags", "--tag", fmt.Sprintf("pvc=%s", req.PVC), "--keep-last", strconv.Itoa(keepLast), - "--prune", - }, " ") + } + if cfg.ResticPruneAfterBackup { + forgetArgs = append(forgetArgs, "--prune") + } + forget := strings.Join(forgetArgs, " ") cmd = fmt.Sprintf("%s && %s", cmd, forget) } else if len(cfg.ResticForgetArgs) > 0 { forget := strings.Join(append([]string{"restic", "forget"}, cfg.ResticForgetArgs...), " ") cmd = fmt.Sprintf("%s && %s", cmd, forget) } - bootstrap := "restic unlock >/dev/null 2>&1 || true; restic cat config >/dev/null 2>&1 || restic init; restic snapshots >/dev/null 2>&1 || restic rebuild-index" - return "set -euo pipefail; " + bootstrap + "; " + cmd + bootstrapSteps := []string{resticRepoBootstrapCommand(), "ensure_restic_repo", "restic snapshots >/dev/null 2>&1 || restic rebuild-index"} + if cfg.ResticUnlockBeforeBackup { + bootstrapSteps = append([]string{"restic unlock >/dev/null 2>&1 || true"}, bootstrapSteps...) + } + bootstrap := strings.Join(bootstrapSteps, "; ") + return "set -eu; " + bootstrap + "; " + cmd +} + +func resticRepoBootstrapCommand() string { + return `ensure_restic_repo() { probe_log="$(mktemp)"; if restic cat config >"${probe_log}" 2>&1; then rm -f "${probe_log}"; return 0; fi; if restic init >"${probe_log}" 2>&1; then rm -f "${probe_log}"; return 0; fi; if grep -qi "already initialized" "${probe_log}"; then rm -f "${probe_log}"; return 0; fi; cat "${probe_log}" >&2; rm -f "${probe_log}"; return 1; }` } func restoreCommand(snapshot string) string { return fmt.Sprintf( - "set -euo pipefail; rm -rf /cache/restore && mkdir -p /cache/restore; restic restore %s --target /cache/restore; if [ -d /cache/restore/data ]; then cp -a /cache/restore/data/. /restore/; else cp -a /cache/restore/. /restore/; fi", + "set -eu; rm -rf /cache/restore && mkdir -p /cache/restore; restic restore %s --target /cache/restore; if [ -d /cache/restore/data ]; then cp -a /cache/restore/data/. /restore/; else cp -a /cache/restore/. /restore/; fi", snapshot, ) } diff --git a/internal/k8s/job_manifests_test.go b/internal/k8s/job_manifests_test.go index 957bd5f..bfc64d0 100644 --- a/internal/k8s/job_manifests_test.go +++ b/internal/k8s/job_manifests_test.go @@ -19,6 +19,7 @@ func TestBuildBackupJobAppliesSelectorsServiceAccountAndMetadata(t *testing.T) { S3Endpoint: "https://s3.us-west-001.backblazeb2.com", S3Region: "us-west-001", JobTTLSeconds: 3600, + JobBackoffLimit: 1, JobNodeSelector: map[string]string{"hardware": "rpi5"}, JobCPURequest: "100m", JobCPULimit: "1", @@ -55,7 +56,7 @@ func TestBuildBackupJobAppliesSelectorsServiceAccountAndMetadata(t *testing.T) { if container.Name != "restic" || container.Image != "restic/restic:latest" { t.Fatalf("expected restic container, got %#v", container) } - if len(container.Args) != 1 || !strings.Contains(container.Args[0], "restic unlock") || !strings.Contains(container.Args[0], "restic backup /data") || !strings.Contains(container.Args[0], "--keep-last 3") { + if len(container.Args) != 1 || strings.Contains(container.Args[0], "restic unlock") || strings.Contains(container.Args[0], "--prune") || !strings.Contains(container.Args[0], "restic backup /data") || !strings.Contains(container.Args[0], "--keep-last 3") { t.Fatalf("expected backup command payload, got %#v", container.Args) } if got := container.Resources.Requests[corev1.ResourceCPU]; got.String() != "100m" { @@ -67,8 +68,8 @@ func TestBuildBackupJobAppliesSelectorsServiceAccountAndMetadata(t *testing.T) { if len(job.Spec.Template.Spec.Volumes) != 2 || job.Spec.Template.Spec.Volumes[0].PersistentVolumeClaim == nil { t.Fatalf("expected pvc + cache volumes, got %#v", job.Spec.Template.Spec.Volumes) } - if got := *job.Spec.BackoffLimit; got != 0 { - t.Fatalf("expected zero backoff limit, got %d", got) + if got := *job.Spec.BackoffLimit; got != 1 { + t.Fatalf("expected configured backoff limit, got %d", got) } if got := *job.Spec.TTLSecondsAfterFinished; got != 3600 { t.Fatalf("expected ttl from config, got %d", got) @@ -82,6 +83,7 @@ func TestBuildRestoreJobCoversDefaultAndTargetPVCRestoreShapes(t *testing.T) { S3Endpoint: "https://s3.us-west-001.backblazeb2.com", S3Region: "us-west-001", JobTTLSeconds: 1800, + JobBackoffLimit: 1, JobNodeSelector: map[string]string{"hardware": "rpi5"}, WorkerServiceAccount: "soteria-worker", } diff --git a/internal/k8s/job_support.go b/internal/k8s/job_support.go index 318c65b..c68d7ce 100644 --- a/internal/k8s/job_support.go +++ b/internal/k8s/job_support.go @@ -109,6 +109,11 @@ func resticRepositoryForBackup(base, namespace, pvc string, dedupe bool) string return appendRepositoryPath(base, suffix) } +// ResticRepositoryForBackup returns the repository path Soteria will use for a PVC backup. +func ResticRepositoryForBackup(base, namespace, pvc string, dedupe bool) string { + return resticRepositoryForBackup(base, namespace, pvc, dedupe) +} + func sanitizeRepositorySegment(value string) string { sanitized := sanitizeName(value) if sanitized == "" { diff --git a/internal/k8s/jobs.go b/internal/k8s/jobs.go index 5db4786..f68c832 100644 --- a/internal/k8s/jobs.go +++ b/internal/k8s/jobs.go @@ -199,6 +199,7 @@ func (c *Client) CreateBackupJob(ctx context.Context, cfg *config.Config, req ap job := buildBackupJob(cfg, req, jobName, secretName, repository, dedupeEnabled, keepLast) if nodeName, err := c.resolvePVCMountedNode(ctx, req.Namespace, req.PVC); err == nil && nodeName != "" { job.Spec.Template.Spec.NodeName = nodeName + job.Spec.Template.Spec.NodeSelector = nil } created, err := c.Clientset.BatchV1().Jobs(req.Namespace).Create(ctx, job, metav1.CreateOptions{}) if err != nil { diff --git a/internal/k8s/jobs_test.go b/internal/k8s/jobs_test.go index 9ad0861..969f528 100644 --- a/internal/k8s/jobs_test.go +++ b/internal/k8s/jobs_test.go @@ -190,6 +190,88 @@ func TestResolvePVCMountedNodeIgnoresDeadPodsAndFindsMountedClaim(t *testing.T) } } +func TestListPVCMountsMarksSoteriaAndIgnoresDeadPods(t *testing.T) { + now := metav1.NewTime(time.Now().UTC()) + client := &Client{Clientset: k8sfake.NewSimpleClientset( + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "deleted", Namespace: "apps", DeletionTimestamp: &now}, + Spec: corev1.PodSpec{Volumes: []corev1.Volume{{ + Name: "data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: "data"}, + }, + }}}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "failed", Namespace: "apps"}, + Spec: corev1.PodSpec{Volumes: []corev1.Volume{{ + Name: "data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: "data"}, + }, + }}}, + Status: corev1.PodStatus{Phase: corev1.PodFailed}, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "apps"}, + Spec: corev1.PodSpec{ + NodeName: "titan-02", + Volumes: []corev1.Volume{{ + Name: "data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: "data"}, + }, + }}, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "soteria-backup-data", + Namespace: "apps", + Labels: map[string]string{ + labelAppName: "soteria", + labelComponent: "backup", + }, + }, + Spec: corev1.PodSpec{ + NodeName: "titan-02", + Volumes: []corev1.Volume{{ + Name: "data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: "data"}, + }, + }}, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + )} + + mounts, err := client.ListPVCMounts(context.Background(), "apps", "data") + if err != nil { + t.Fatalf("list pvc mounts: %v", err) + } + if len(mounts) != 2 { + t.Fatalf("expected app and soteria mounts only, got %#v", mounts) + } + if mounts[0].PodName != "app" || mounts[0].NodeName != "titan-02" || mounts[0].SoteriaBackup { + t.Fatalf("expected first mount to be the app pod, got %#v", mounts[0]) + } + if mounts[1].PodName != "soteria-backup-data" || !mounts[1].SoteriaBackup { + t.Fatalf("expected second mount to be marked as soteria, got %#v", mounts[1]) + } + + clientset := k8sfake.NewSimpleClientset() + clientset.PrependReactor("list", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("list mounts exploded") + }) + client = &Client{Clientset: clientset} + if _, err := client.ListPVCMounts(context.Background(), "apps", "data"); err == nil || !strings.Contains(err.Error(), "list mounts exploded") { + t.Fatalf("expected list pvc mounts error, got %v", err) + } +} + func TestReadBackupJobLogCoversSuccessAndListFailures(t *testing.T) { client := &Client{Clientset: k8sfake.NewSimpleClientset( &corev1.Pod{ @@ -301,6 +383,7 @@ func TestCreateBackupJobCoversValidationDryRunAndLiveCreation(t *testing.T) { ResticRepository: "s3:https://repo/root", ResticImage: "restic/restic:latest", JobTTLSeconds: 3600, + JobNodeSelector: map[string]string{"kubernetes.io/arch": "arm64"}, WorkerServiceAccount: "soteria-sa", } @@ -345,6 +428,9 @@ func TestCreateBackupJobCoversValidationDryRunAndLiveCreation(t *testing.T) { if job.Spec.Template.Spec.NodeName != "titan-02" || job.Spec.Template.Spec.ServiceAccountName != "soteria-sa" { t.Fatalf("expected node pin + service account, got %#v", job.Spec.Template.Spec) } + if len(job.Spec.Template.Spec.NodeSelector) != 0 { + t.Fatalf("expected node selector to be cleared when backup is pinned to mounted PVC node, got %#v", job.Spec.Template.Spec.NodeSelector) + } if job.Annotations[annotationDedupeEnabled] != "false" || job.Annotations[annotationKeepLast] != "3" { t.Fatalf("expected backup annotations, got %#v", job.Annotations) } diff --git a/internal/k8s/volumes.go b/internal/k8s/volumes.go index 36379c2..f08ac4a 100644 --- a/internal/k8s/volumes.go +++ b/internal/k8s/volumes.go @@ -21,6 +21,14 @@ type PVCSummary struct { AccessModes []string } +// PVCMount describes an active pod mounting a PVC. +type PVCMount struct { + PodName string + NodeName string + Phase string + SoteriaBackup bool +} + // ResolvePVCVolume loads a PVC and its bound PV so backup handlers can act on the real volume. func (c *Client) ResolvePVCVolume(ctx context.Context, namespace, pvcName string) (string, *corev1.PersistentVolumeClaim, *corev1.PersistentVolume, error) { pvc, err := c.Clientset.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{}) @@ -39,6 +47,38 @@ func (c *Client) ResolvePVCVolume(ctx context.Context, namespace, pvcName string return pvc.Spec.VolumeName, pvc, pv, nil } +// ListPVCMounts returns active pods in a namespace that reference the PVC. +func (c *Client) ListPVCMounts(ctx context.Context, namespace, pvcName string) ([]PVCMount, error) { + pods, err := c.Clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("list pods for pvc %s/%s: %w", namespace, pvcName, err) + } + + mounts := []PVCMount{} + for _, pod := range pods.Items { + if pod.DeletionTimestamp != nil { + continue + } + if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { + continue + } + for _, volume := range pod.Spec.Volumes { + claim := volume.PersistentVolumeClaim + if claim == nil || claim.ClaimName != pvcName { + continue + } + mounts = append(mounts, PVCMount{ + PodName: pod.Name, + NodeName: pod.Spec.NodeName, + Phase: string(pod.Status.Phase), + SoteriaBackup: pod.Labels[labelAppName] == "soteria" && pod.Labels[labelComponent] == "backup", + }) + break + } + } + return mounts, nil +} + // ListBoundPVCs returns all bound PVCs in a stable namespace/name order. func (c *Client) ListBoundPVCs(ctx context.Context) ([]PVCSummary, error) { list, err := c.Clientset.CoreV1().PersistentVolumeClaims("").List(ctx, metav1.ListOptions{}) diff --git a/internal/server/backup_handlers.go b/internal/server/backup_handlers.go index 4ef4b7b..24ba90d 100644 --- a/internal/server/backup_handlers.go +++ b/internal/server/backup_handlers.go @@ -3,6 +3,7 @@ package server import ( "context" "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -238,6 +239,34 @@ func (s *Server) executeBackup(ctx context.Context, req api.BackupRequest, reque } return response, "success", nil case "restic": + _, pvc, _, err := s.client.ResolvePVCVolume(ctx, req.Namespace, req.PVC) + if err != nil { + return api.BackupResponse{}, "validation_error", err + } + storageClass := "" + if pvc != nil && pvc.Spec.StorageClassName != nil { + storageClass = *pvc.Spec.StorageClassName + } + if excluded, reason := s.pvcExcluded(req.Namespace, req.PVC, storageClass); excluded { + return api.BackupResponse{}, "validation_error", errors.New(reason) + } + blocked, reason, err := s.liveExclusivePVCMounted(ctx, req.Namespace, req.PVC, pvcAccessModes(pvc)) + if err != nil { + return api.BackupResponse{}, "backend_error", err + } + if blocked { + return api.BackupResponse{}, "validation_error", errors.New(reason) + } + repository := k8s.ResticRepositoryForBackup(s.cfg.ResticRepository, req.Namespace, req.PVC, resolvedDedupe) + if !req.DryRun { + busy, err := s.resticRepositoryBusy(ctx, repository) + if err != nil { + return api.BackupResponse{}, "backend_error", err + } + if busy { + return api.BackupResponse{}, "in_progress", fmt.Errorf("restic repository already has an active Soteria backup") + } + } jobName, secretName, err := s.client.CreateBackupJob(ctx, s.cfg, req) if err != nil { return api.BackupResponse{}, "backend_error", err @@ -268,9 +297,13 @@ func (s *Server) listNamespaceBoundPVCs(ctx context.Context, namespace string) ( } filtered := make([]k8s.PVCSummary, 0, len(items)) for _, item := range items { - if item.Namespace == namespace { - filtered = append(filtered, item) + if item.Namespace != namespace { + continue } + if excluded, _ := s.pvcSummaryExcluded(item); excluded { + continue + } + filtered = append(filtered, item) } return filtered, nil } @@ -279,6 +312,8 @@ func backupStatusCode(result string) int { switch result { case "validation_error", "unsupported_driver": return http.StatusBadRequest + case "in_progress": + return http.StatusConflict case "backend_error": return http.StatusBadGateway default: diff --git a/internal/server/backup_handlers_test.go b/internal/server/backup_handlers_test.go index c334c59..5ec1886 100644 --- a/internal/server/backup_handlers_test.go +++ b/internal/server/backup_handlers_test.go @@ -354,6 +354,69 @@ func TestHandleBackupAndNamespaceBackupValidationPaths(t *testing.T) { }) } +func TestHandleBackupReturnsExecutionErrors(t *testing.T) { + srv := newBackupTestServer( + &config.Config{AuthRequired: false, BackupDriver: "restic", ResticRepository: "s3:https://repo/root"}, + &backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{ + {Namespace: "apps", Name: "data", VolumeName: "pv-data", Phase: "Bound", StorageClass: "astreae"}, + {Namespace: "ops", Name: "other", VolumeName: "pv-other", Phase: "Bound", StorageClass: "astreae"}, + }, + backupJobs: map[string][]k8s.BackupJobSummary{ + "ops/other": { + {Name: "job-other", Namespace: "ops", PVC: "other", Repository: "s3:https://repo/root", State: "Running", CreatedAt: time.Now().UTC()}, + }, + }, + }}}, + &backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}}, + ) + + req := httptest.NewRequest(http.MethodPost, "/v1/backup", strings.NewReader(`{"namespace":"apps","pvc":"data"}`)) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + srv.Handler().ServeHTTP(res, req) + + assertErrorResponseContains(t, res, http.StatusConflict, "restic repository already has an active") +} + +func TestHandleNamespaceBackupReportsPartialFailureAndFiltersExcludedPVCs(t *testing.T) { + client := &policyCycleTestKubeClient{ + inventoryTestKubeClient: &inventoryTestKubeClient{fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{ + {Namespace: "apps", Name: "alpha", VolumeName: "pv-alpha", Phase: "Bound", StorageClass: "astreae"}, + {Namespace: "apps", Name: "beta", VolumeName: "pv-beta", Phase: "Bound", StorageClass: "astreae"}, + {Namespace: "apps", Name: "cache", VolumeName: "pv-cache", Phase: "Bound", StorageClass: "local-path"}, + {Namespace: "ops", Name: "other", VolumeName: "pv-other", Phase: "Bound", StorageClass: "astreae"}, + }, + }}, + createBackupErrForPVC: map[string]error{"beta": errors.New("create beta backup exploded")}, + } + srv := newBackupTestServer( + &config.Config{AuthRequired: false, BackupDriver: "restic", ResticRepository: "s3:https://repo/root", ExcludedStorageClasses: []string{"local-path"}}, + client, + &backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}}, + ) + + req := httptest.NewRequest(http.MethodPost, "/v1/backup/namespace", strings.NewReader(`{"namespace":"apps","dedupe":false}`)) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + srv.Handler().ServeHTTP(res, req) + if res.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", res.Code, res.Body.String()) + } + + var payload api.NamespaceBackupResponse + if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { + t.Fatalf("decode payload: %v", err) + } + if payload.Total != 2 || payload.Succeeded != 1 || payload.Failed != 1 { + t.Fatalf("expected excluded cache to be filtered and beta failure reported, got %#v", payload) + } + if len(payload.Results) != 2 || payload.Results[0].PVC != "alpha" || payload.Results[1].PVC != "beta" || payload.Results[1].Status != "backend_error" { + t.Fatalf("unexpected namespace backup results: %#v", payload.Results) + } +} + func TestExecuteBackupAndStatusHelpers(t *testing.T) { t.Run("validation error", func(t *testing.T) { srv := newBackupTestServer( @@ -427,6 +490,57 @@ func TestExecuteBackupAndStatusHelpers(t *testing.T) { } }) + t.Run("restic excluded pvc", func(t *testing.T) { + srv := newBackupTestServer( + &config.Config{AuthRequired: false, BackupDriver: "restic", ExcludedStorageClasses: []string{"local-path"}}, + &backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{{Namespace: "apps", Name: "cache", VolumeName: "pv-cache", Phase: "Bound", StorageClass: "local-path"}}, + }}}, + &backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}}, + ) + _, result, err := srv.executeBackup(context.Background(), api.BackupRequest{Namespace: "apps", PVC: "cache"}, "brad") + if result != "validation_error" || err == nil || !strings.Contains(err.Error(), "storage class local-path is excluded") { + t.Fatalf("expected excluded pvc validation error, got result=%q err=%v", result, err) + } + }) + + t.Run("restic live rwo pvc", func(t *testing.T) { + srv := newBackupTestServer( + &config.Config{AuthRequired: false, BackupDriver: "restic"}, + &backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{{Namespace: "apps", Name: "data", VolumeName: "pv-data", Phase: "Bound", AccessModes: []string{"ReadWriteOnce"}}}, + pvcMounts: map[string][]k8s.PVCMount{ + "apps/data": {{PodName: "gitea-0", NodeName: "titan-06", Phase: "Running"}}, + }, + }}}, + &backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}}, + ) + _, result, err := srv.executeBackup(context.Background(), api.BackupRequest{Namespace: "apps", PVC: "data"}, "brad") + if result != "validation_error" || err == nil || !strings.Contains(err.Error(), "mounted by active pod gitea-0") { + t.Fatalf("expected live RWO validation error, got result=%q err=%v", result, err) + } + }) + + t.Run("restic active repository conflict", func(t *testing.T) { + srv := newBackupTestServer( + &config.Config{AuthRequired: false, BackupDriver: "restic", ResticRepository: "s3:https://repo/root"}, + &backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{ + {Namespace: "apps", Name: "data", VolumeName: "pv-data", Phase: "Bound", StorageClass: "astreae"}, + {Namespace: "ops", Name: "other", VolumeName: "pv-other", Phase: "Bound", StorageClass: "astreae"}, + }, + backupJobs: map[string][]k8s.BackupJobSummary{ + "ops/other": {{Name: "job-other", Namespace: "ops", PVC: "other", Repository: "s3:https://repo/root", State: "Running", CreatedAt: time.Now().UTC()}}, + }, + }}}, + &backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}}, + ) + _, result, err := srv.executeBackup(context.Background(), api.BackupRequest{Namespace: "apps", PVC: "data"}, "brad") + if result != "in_progress" || err == nil || !strings.Contains(err.Error(), "restic repository already has an active") { + t.Fatalf("expected active repository conflict, got result=%q err=%v", result, err) + } + }) + t.Run("unsupported driver", func(t *testing.T) { srv := newBackupTestServer( &config.Config{AuthRequired: false, BackupDriver: "mystery"}, @@ -443,6 +557,7 @@ func TestExecuteBackupAndStatusHelpers(t *testing.T) { testCases := map[string]int{ "validation_error": http.StatusBadRequest, "unsupported_driver": http.StatusBadRequest, + "in_progress": http.StatusConflict, "backend_error": http.StatusBadGateway, "other": http.StatusInternalServerError, } diff --git a/internal/server/backup_safety.go b/internal/server/backup_safety.go new file mode 100644 index 0000000..eecd1de --- /dev/null +++ b/internal/server/backup_safety.go @@ -0,0 +1,147 @@ +package server + +import ( + "context" + "fmt" + "path" + "sort" + "strings" + + "scm.bstein.dev/bstein/soteria/internal/k8s" + + corev1 "k8s.io/api/core/v1" +) + +func (s *Server) pvcSummaryExcluded(pvc k8s.PVCSummary) (bool, string) { + return s.pvcExcluded(pvc.Namespace, pvc.Name, pvc.StorageClass) +} + +func (s *Server) pvcExcluded(namespace, pvc, storageClass string) (bool, string) { + storageClass = strings.TrimSpace(storageClass) + for _, excluded := range s.cfg.ExcludedStorageClasses { + if storageClass != "" && strings.EqualFold(storageClass, strings.TrimSpace(excluded)) { + return true, fmt.Sprintf("storage class %s is excluded from restic backups", storageClass) + } + } + + key := strings.Trim(strings.TrimSpace(namespace)+"/"+strings.TrimSpace(pvc), "/") + for _, pattern := range s.cfg.ExcludedPVCs { + pattern = strings.TrimSpace(pattern) + if pattern == "" { + continue + } + if pattern == key { + return true, fmt.Sprintf("PVC %s is excluded from restic backups", key) + } + matched, err := path.Match(pattern, key) + if err == nil && matched { + return true, fmt.Sprintf("PVC %s matches excluded pattern %s", key, pattern) + } + } + + return false, "" +} + +func (s *Server) liveExclusivePVCMounted(ctx context.Context, namespace, pvc string, accessModes []string) (bool, string, error) { + if !hasExclusiveAccessMode(accessModes) { + return false, "", nil + } + + mounts, err := s.client.ListPVCMounts(ctx, namespace, pvc) + if err != nil { + return false, "", err + } + for _, mount := range mounts { + if mount.SoteriaBackup { + continue + } + location := strings.TrimSpace(mount.PodName) + if mount.NodeName != "" { + location += " on " + mount.NodeName + } + return true, fmt.Sprintf("RWO PVC %s/%s is mounted by active pod %s; restic backups wait for the workload to release the claim or use Longhorn snapshots", namespace, pvc, location), nil + } + return false, "", nil +} + +func hasExclusiveAccessMode(accessModes []string) bool { + for _, mode := range accessModes { + switch corev1.PersistentVolumeAccessMode(strings.TrimSpace(mode)) { + case corev1.ReadWriteOnce, corev1.ReadWriteOncePod: + return true + } + } + return false +} + +func pvcAccessModes(pvc *corev1.PersistentVolumeClaim) []string { + if pvc == nil { + return nil + } + modes := make([]string, 0, len(pvc.Spec.AccessModes)) + for _, mode := range pvc.Spec.AccessModes { + modes = append(modes, string(mode)) + } + return modes +} + +func (s *Server) activeResticRepositories(ctx context.Context, namespaces []string) (map[string]struct{}, error) { + active := map[string]struct{}{} + for _, namespace := range uniqueSortedStrings(namespaces) { + jobs, err := s.client.ListBackupJobs(ctx, namespace) + if err != nil { + return nil, err + } + for _, job := range jobs { + if !backupJobInProgress(job.State) { + continue + } + repository := strings.TrimSpace(job.Repository) + if repository == "" { + repository = strings.TrimSpace(s.cfg.ResticRepository) + } + if repository != "" { + active[repository] = struct{}{} + } + } + } + return active, nil +} + +func (s *Server) resticRepositoryBusy(ctx context.Context, repository string) (bool, error) { + repository = strings.TrimSpace(repository) + if repository == "" { + return false, nil + } + pvcs, err := s.client.ListBoundPVCs(ctx) + if err != nil { + return false, err + } + namespaces := make([]string, 0, len(pvcs)) + for _, pvc := range pvcs { + namespaces = append(namespaces, pvc.Namespace) + } + active, err := s.activeResticRepositories(ctx, namespaces) + if err != nil { + return false, err + } + _, busy := active[repository] + return busy, nil +} + +func uniqueSortedStrings(items []string) []string { + seen := map[string]struct{}{} + for _, item := range items { + item = strings.TrimSpace(item) + if item == "" { + continue + } + seen[item] = struct{}{} + } + out := make([]string, 0, len(seen)) + for item := range seen { + out = append(out, item) + } + sort.Strings(out) + return out +} diff --git a/internal/server/inventory_builder.go b/internal/server/inventory_builder.go index c2371de..6426aa8 100644 --- a/internal/server/inventory_builder.go +++ b/internal/server/inventory_builder.go @@ -31,6 +31,13 @@ func (s *Server) buildInventory(ctx context.Context) (api.InventoryResponse, err AccessModes: summary.AccessModes, Driver: s.cfg.BackupDriver, } + if excluded, reason := s.pvcSummaryExcluded(summary); excluded { + entry.Healthy = true + entry.HealthReason = "excluded" + entry.Error = reason + groups[summary.Namespace] = append(groups[summary.Namespace], entry) + continue + } s.enrichPVCInventory(ctx, &entry, resticJobsByPVC, resticLookupErrors) groups[summary.Namespace] = append(groups[summary.Namespace], entry) } diff --git a/internal/server/inventory_builder_test.go b/internal/server/inventory_builder_test.go index 4f64453..8780cad 100644 --- a/internal/server/inventory_builder_test.go +++ b/internal/server/inventory_builder_test.go @@ -3,6 +3,7 @@ package server import ( "context" "errors" + "strings" "testing" "time" @@ -116,6 +117,30 @@ func TestBuildInventoryLonghornSortsNamespacesAndCalculatesHealth(t *testing.T) } } +func TestBuildInventoryMarksExcludedPVCsHealthy(t *testing.T) { + client := &inventoryTestKubeClient{ + fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{ + {Namespace: "apps", Name: "cache", VolumeName: "vol-cache", Phase: "Bound", StorageClass: "local-path"}, + }, + }, + } + srv := newInventoryTestServer(&config.Config{ + BackupDriver: "restic", + BackupMaxAge: 24 * time.Hour, + ExcludedStorageClasses: []string{"local-path"}, + }, client, &inventoryTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}) + + inventory, err := srv.buildInventory(context.Background()) + if err != nil { + t.Fatalf("build inventory: %v", err) + } + entry := inventory.Namespaces[0].PVCs[0] + if !entry.Healthy || entry.HealthReason != "excluded" || !strings.Contains(entry.Error, "local-path") { + t.Fatalf("expected excluded pvc to be treated as healthy exclusion, got %#v", entry) + } +} + func TestEnrichPVCInventoryCoversLonghornAndResticBranches(t *testing.T) { now := time.Now().UTC() recent := now.Add(-1 * time.Hour) diff --git a/internal/server/policy_cycle_test.go b/internal/server/policy_cycle_test.go index af25b7c..291c774 100644 --- a/internal/server/policy_cycle_test.go +++ b/internal/server/policy_cycle_test.go @@ -93,7 +93,7 @@ func TestRunPolicyCycleCoversEffectivePoliciesAndResultTracking(t *testing.T) { }, backupJobs: map[string][]k8s.BackupJobSummary{ "apps/busy": { - {Name: "job-busy", Namespace: "apps", PVC: "busy", State: "Running", CreatedAt: recent}, + {Name: "job-busy", Namespace: "apps", PVC: "busy", Repository: "s3:https://repo/root/isolated/apps/busy", State: "Running", CreatedAt: recent}, }, "apps/recent": { {Name: "job-recent", Namespace: "apps", PVC: "recent", State: "Completed", CreatedAt: recent, CompletionTime: recent, KeepLast: 1}, @@ -193,3 +193,89 @@ func TestRunPolicyCycleHonorsBackupLimit(t *testing.T) { t.Fatalf("expected cycle_limit metric, got %f", got) } } + +func TestRunPolicyCycleSkipsExcludedPVCsAndBusyRepositories(t *testing.T) { + client := &policyCycleTestKubeClient{ + inventoryTestKubeClient: &inventoryTestKubeClient{ + fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{ + {Namespace: "apps", Name: "data", VolumeName: "vol-data", Phase: "Bound", StorageClass: "astreae"}, + {Namespace: "apps", Name: "cache", VolumeName: "vol-cache", Phase: "Bound", StorageClass: "local-path"}, + }, + backupJobs: map[string][]k8s.BackupJobSummary{ + "apps/other": { + {Name: "job-other", Namespace: "apps", PVC: "other", Repository: "s3:https://repo/root", State: "Running", CreatedAt: time.Now().UTC()}, + }, + }, + }, + }, + } + srv := &Server{ + cfg: &config.Config{ + BackupDriver: "restic", + BackupMaxAge: 24 * time.Hour, + ResticRepository: "s3:https://repo/root", + ExcludedStorageClasses: []string{"local-path"}, + }, + client: client, + longhorn: &fakeLonghornClient{}, + metrics: newTelemetry(), + policies: map[string]api.BackupPolicy{ + "apps__all": {ID: "apps__all", Namespace: "apps", IntervalHours: 1, Enabled: true, Dedupe: true}, + }, + } + + srv.runPolicyCycle(context.Background()) + + if len(client.backupRequests) != 0 { + t.Fatalf("expected no backup requests while shared repo is busy and cache is excluded, got %#v", client.backupRequests) + } + if got := metricCount(srv.metrics.policyBackups, map[string]string{"result": "repo_in_progress"}); got != 1 { + t.Fatalf("expected repo_in_progress metric, got %f", got) + } + if got := metricCount(srv.metrics.policyBackups, map[string]string{"result": "excluded"}); got != 1 { + t.Fatalf("expected excluded metric, got %f", got) + } +} + +func TestRunPolicyCycleSkipsLiveExclusivePVCs(t *testing.T) { + client := &policyCycleTestKubeClient{ + inventoryTestKubeClient: &inventoryTestKubeClient{ + fakeKubeClient: &fakeKubeClient{ + pvcs: []k8s.PVCSummary{ + {Namespace: "apps", Name: "data", VolumeName: "vol-data", Phase: "Bound", AccessModes: []string{"ReadWriteOnce"}}, + {Namespace: "apps", Name: "shared", VolumeName: "vol-shared", Phase: "Bound", AccessModes: []string{"ReadWriteMany"}}, + }, + pvcMounts: map[string][]k8s.PVCMount{ + "apps/data": {{PodName: "gitea-0", NodeName: "titan-06", Phase: "Running"}}, + "apps/shared": {{PodName: "web-0", NodeName: "titan-07", Phase: "Running"}}, + }, + }, + }, + } + srv := &Server{ + cfg: &config.Config{ + BackupDriver: "restic", + BackupMaxAge: 24 * time.Hour, + ResticRepository: "s3:https://repo/root", + }, + client: client, + longhorn: &fakeLonghornClient{}, + metrics: newTelemetry(), + policies: map[string]api.BackupPolicy{ + "apps__all": {ID: "apps__all", Namespace: "apps", IntervalHours: 1, Enabled: true, Dedupe: true}, + }, + } + + srv.runPolicyCycle(context.Background()) + + if len(client.backupRequests) != 1 || client.backupRequests[0].PVC != "shared" { + t.Fatalf("expected only RWX pvc backup request, got %#v", client.backupRequests) + } + if got := metricCount(srv.metrics.policyBackups, map[string]string{"result": "live_rwo_mount"}); got != 1 { + t.Fatalf("expected live_rwo_mount skip metric, got %f", got) + } + if got := metricCount(srv.metrics.policyBackups, map[string]string{"result": "success"}); got != 1 { + t.Fatalf("expected one successful policy backup, got %f", got) + } +} diff --git a/internal/server/policy_runtime.go b/internal/server/policy_runtime.go index fa97c28..98c47e5 100644 --- a/internal/server/policy_runtime.go +++ b/internal/server/policy_runtime.go @@ -10,6 +10,7 @@ import ( "time" "scm.bstein.dev/bstein/soteria/internal/api" + "scm.bstein.dev/bstein/soteria/internal/k8s" ) func (s *Server) runPolicyCycle(ctx context.Context) { @@ -35,7 +36,9 @@ func (s *Server) runPolicyCycle(ctx context.Context) { pvcMap := make(map[string]api.PVCInventory) namespaceMap := make(map[string][]api.PVCInventory) + namespaceNames := make([]string, 0, len(inventory.Namespaces)) for _, group := range inventory.Namespaces { + namespaceNames = append(namespaceNames, group.Name) for _, pvc := range group.PVCs { key := pvc.Namespace + "/" + pvc.PVC pvcMap[key] = pvc @@ -78,6 +81,17 @@ func (s *Server) runPolicyCycle(ctx context.Context) { } sort.Strings(keys) + activeRepositories := map[string]struct{}{} + if s.cfg.BackupDriver == "restic" { + var err error + activeRepositories, err = s.activeResticRepositories(runCtx, namespaceNames) + if err != nil { + log.Printf("policy cycle active backup lookup failed: %v", err) + s.metrics.RecordPolicyBackup("active_lookup_error") + return + } + } + started := 0 for _, key := range keys { effective := effectivePolicies[key] @@ -85,6 +99,20 @@ func (s *Server) runPolicyCycle(ctx context.Context) { if !ok { continue } + if excluded, _ := s.pvcExcluded(pvc.Namespace, pvc.PVC, pvc.StorageClass); excluded { + s.metrics.RecordPolicyBackup("excluded") + continue + } + blocked, _, err := s.liveExclusivePVCMounted(runCtx, pvc.Namespace, pvc.PVC, pvc.AccessModes) + if err != nil { + log.Printf("policy cycle live PVC mount lookup failed for %s/%s: %v", pvc.Namespace, pvc.PVC, err) + s.metrics.RecordPolicyBackup("active_lookup_error") + continue + } + if blocked { + s.metrics.RecordPolicyBackup("live_rwo_mount") + continue + } // Never enqueue a new policy backup while one is already active for this PVC. // This prevents runaway job storms when a backup is stuck Pending/Running. if pvc.ActiveBackups > 0 { @@ -107,6 +135,11 @@ func (s *Server) runPolicyCycle(ctx context.Context) { s.metrics.RecordPolicyBackup("cycle_limit") break } + repository := k8s.ResticRepositoryForBackup(s.cfg.ResticRepository, pvc.Namespace, pvc.PVC, effective.Dedupe) + if _, busy := activeRepositories[repository]; busy { + s.metrics.RecordPolicyBackup("repo_in_progress") + continue + } _, result, err := s.executeBackup(runCtx, api.BackupRequest{ Namespace: pvc.Namespace, @@ -122,6 +155,9 @@ func (s *Server) runPolicyCycle(ctx context.Context) { log.Printf("policy backup failed for %s/%s: %v", pvc.Namespace, pvc.PVC, err) continue } + if s.cfg.BackupDriver == "restic" { + activeRepositories[repository] = struct{}{} + } s.metrics.RecordPolicyBackup("success") } } diff --git a/internal/server/server.go b/internal/server/server.go index 3b1de4e..6fd1a1f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -24,6 +24,7 @@ type kubeClient interface { ListBackupJobsForPVC(ctx context.Context, namespace, pvc string) ([]k8s.BackupJobSummary, error) ReadBackupJobLog(ctx context.Context, namespace, jobName string) (string, error) ListBoundPVCs(ctx context.Context) ([]k8s.PVCSummary, error) + ListPVCMounts(ctx context.Context, namespace, pvcName string) ([]k8s.PVCMount, error) PersistentVolumeClaimExists(ctx context.Context, namespace, pvcName string) (bool, error) LoadSecretData(ctx context.Context, namespace, secretName, key string) ([]byte, error) SaveSecretData(ctx context.Context, namespace, secretName, key string, value []byte, labels map[string]string) error diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 2cf3aa7..8fcd0b2 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -21,6 +21,7 @@ import ( type fakeKubeClient struct { pvcs []k8s.PVCSummary + pvcMounts map[string][]k8s.PVCMount backupJobs map[string][]k8s.BackupJobSummary jobLogs map[string]string backupJobCalls int @@ -37,6 +38,23 @@ type secretErrorKubeClient struct { } func (f *fakeKubeClient) ResolvePVCVolume(_ context.Context, namespace, pvcName string) (string, *corev1.PersistentVolumeClaim, *corev1.PersistentVolume, error) { + for _, summary := range f.pvcs { + if summary.Namespace != namespace || summary.Name != pvcName { + continue + } + pvc := &corev1.PersistentVolumeClaim{} + pvc.Namespace = namespace + pvc.Name = pvcName + pvc.Spec.VolumeName = summary.VolumeName + for _, mode := range summary.AccessModes { + pvc.Spec.AccessModes = append(pvc.Spec.AccessModes, corev1.PersistentVolumeAccessMode(mode)) + } + if summary.StorageClass != "" { + storageClass := summary.StorageClass + pvc.Spec.StorageClassName = &storageClass + } + return summary.VolumeName, pvc, nil, nil + } return namespace + "-" + pvcName + "-pv", nil, nil, nil } @@ -70,6 +88,17 @@ func (f *fakeKubeClient) ListBoundPVCs(_ context.Context) ([]k8s.PVCSummary, err return f.pvcs, nil } +func (f *fakeKubeClient) ListPVCMounts(_ context.Context, namespace, pvc string) ([]k8s.PVCMount, error) { + if f.pvcMounts == nil { + return nil, nil + } + key := namespace + "/" + pvc + items := f.pvcMounts[key] + out := make([]k8s.PVCMount, len(items)) + copy(out, items) + return out, nil +} + func (f *fakeKubeClient) ListBackupJobsForPVC(_ context.Context, namespace, pvc string) ([]k8s.BackupJobSummary, error) { if f.backupJobs == nil { return nil, nil diff --git a/internal/server/ui-dist/assets/index-B48ZTq0Y.js b/internal/server/ui-dist/assets/index-B48ZTq0Y.js new file mode 100644 index 0000000..a490352 --- /dev/null +++ b/internal/server/ui-dist/assets/index-B48ZTq0Y.js @@ -0,0 +1,40 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const u of o.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function $c(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ds={exports:{}},gl={},ps={exports:{}},O={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var pr=Symbol.for("react.element"),Vc=Symbol.for("react.portal"),Ac=Symbol.for("react.fragment"),Hc=Symbol.for("react.strict_mode"),Wc=Symbol.for("react.profiler"),Qc=Symbol.for("react.provider"),Kc=Symbol.for("react.context"),Yc=Symbol.for("react.forward_ref"),Xc=Symbol.for("react.suspense"),Gc=Symbol.for("react.memo"),Zc=Symbol.for("react.lazy"),ti=Symbol.iterator;function Jc(e){return e===null||typeof e!="object"?null:(e=ti&&e[ti]||e["@@iterator"],typeof e=="function"?e:null)}var hs={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},ms=Object.assign,vs={};function _n(e,t,n){this.props=e,this.context=t,this.refs=vs,this.updater=n||hs}_n.prototype.isReactComponent={};_n.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};_n.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ys(){}ys.prototype=_n.prototype;function ru(e,t,n){this.props=e,this.context=t,this.refs=vs,this.updater=n||hs}var lu=ru.prototype=new ys;lu.constructor=ru;ms(lu,_n.prototype);lu.isPureReactComponent=!0;var ni=Array.isArray,gs=Object.prototype.hasOwnProperty,ou={current:null},ws={key:!0,ref:!0,__self:!0,__source:!0};function ks(e,t,n){var r,l={},o=null,u=null;if(t!=null)for(r in t.ref!==void 0&&(u=t.ref),t.key!==void 0&&(o=""+t.key),t)gs.call(t,r)&&!ws.hasOwnProperty(r)&&(l[r]=t[r]);var i=arguments.length-2;if(i===1)l.children=n;else if(1>>1,X=_[A];if(0>>1;Al(Kt,z))Ael(Lt,Kt)?(_[A]=Lt,_[Ae]=z,A=Ae):(_[A]=Kt,_[Ve]=z,A=Ve);else if(Ael(Lt,z))_[A]=Lt,_[Ae]=z,A=Ae;else break e}}return T}function l(_,T){var z=_.sortIndex-T.sortIndex;return z!==0?z:_.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var u=Date,i=u.now();e.unstable_now=function(){return u.now()-i}}var s=[],d=[],v=1,m=null,p=3,w=!1,k=!1,S=!1,M=typeof setTimeout=="function"?setTimeout:null,c=typeof clearTimeout=="function"?clearTimeout:null,a=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f(_){for(var T=n(d);T!==null;){if(T.callback===null)r(d);else if(T.startTime<=_)r(d),T.sortIndex=T.expirationTime,t(s,T);else break;T=n(d)}}function y(_){if(S=!1,f(_),!k)if(n(s)!==null)k=!0,it(x);else{var T=n(d);T!==null&&st(y,T.startTime-_)}}function x(_,T){k=!1,S&&(S=!1,c(j),j=-1),w=!0;var z=p;try{for(f(T),m=n(s);m!==null&&(!(m.expirationTime>T)||_&&!me());){var A=m.callback;if(typeof A=="function"){m.callback=null,p=m.priorityLevel;var X=A(m.expirationTime<=T);T=e.unstable_now(),typeof X=="function"?m.callback=X:m===n(s)&&r(s),f(T)}else r(s);m=n(s)}if(m!==null)var zt=!0;else{var Ve=n(d);Ve!==null&&st(y,Ve.startTime-T),zt=!1}return zt}finally{m=null,p=z,w=!1}}var E=!1,P=null,j=-1,V=5,R=-1;function me(){return!(e.unstable_now()-R_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):V=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return p},e.unstable_getFirstCallbackNode=function(){return n(s)},e.unstable_next=function(_){switch(p){case 1:case 2:case 3:var T=3;break;default:T=p}var z=p;p=T;try{return _()}finally{p=z}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(_,T){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var z=p;p=_;try{return T()}finally{p=z}},e.unstable_scheduleCallback=function(_,T,z){var A=e.unstable_now();switch(typeof z=="object"&&z!==null?(z=z.delay,z=typeof z=="number"&&0A?(_.sortIndex=z,t(d,_),n(s)===null&&_===n(d)&&(S?(c(j),j=-1):S=!0,st(y,z-A))):(_.sortIndex=X,t(s,_),k||w||(k=!0,it(x))),_},e.unstable_shouldYield=me,e.unstable_wrapCallback=function(_){var T=p;return function(){var z=p;p=T;try{return _.apply(this,arguments)}finally{p=z}}}})(Es);Cs.exports=Es;var cf=Cs.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ff=D,Ce=cf;function g(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),co=Object.prototype.hasOwnProperty,df=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,li={},oi={};function pf(e){return co.call(oi,e)?!0:co.call(li,e)?!1:df.test(e)?oi[e]=!0:(li[e]=!0,!1)}function hf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function mf(e,t,n,r){if(t===null||typeof t>"u"||hf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,o,u){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=u}var le={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){le[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];le[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){le[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){le[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){le[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){le[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){le[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){le[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){le[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var iu=/[\-:]([a-z])/g;function su(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(iu,su);le[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(iu,su);le[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(iu,su);le[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){le[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});le.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){le[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function au(e,t,n,r){var l=le.hasOwnProperty(t)?le[t]:null;(l!==null?l.type!==0:r||!(2i||l[u]!==o[i]){var s=` +`+l[u].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=u&&0<=i);break}}}finally{Bl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Fn(e):""}function vf(e){switch(e.tag){case 5:return Fn(e.type);case 16:return Fn("Lazy");case 13:return Fn("Suspense");case 19:return Fn("SuspenseList");case 0:case 2:case 15:return e=$l(e.type,!1),e;case 11:return e=$l(e.type.render,!1),e;case 1:return e=$l(e.type,!0),e;default:return""}}function mo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Zt:return"Fragment";case Gt:return"Portal";case fo:return"Profiler";case cu:return"StrictMode";case po:return"Suspense";case ho:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case js:return(e.displayName||"Context")+".Consumer";case Ps:return(e._context.displayName||"Context")+".Provider";case fu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case du:return t=e.displayName||null,t!==null?t:mo(e.type)||"Memo";case ft:t=e._payload,e=e._init;try{return mo(e(t))}catch{}}return null}function yf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return mo(t);case 8:return t===cu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Et(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function zs(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function gf(e){var t=zs(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(u){r=""+u,o.call(this,u)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(u){r=""+u},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Sr(e){e._valueTracker||(e._valueTracker=gf(e))}function Ls(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=zs(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Xr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function vo(e,t){var n=t.checked;return K({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function ii(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Et(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Rs(e,t){t=t.checked,t!=null&&au(e,"checked",t,!1)}function yo(e,t){Rs(e,t);var n=Et(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?go(e,t.type,n):t.hasOwnProperty("defaultValue")&&go(e,t.type,Et(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function si(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function go(e,t,n){(t!=="number"||Xr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Un=Array.isArray;function an(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=xr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Jn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Vn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},wf=["Webkit","ms","Moz","O"];Object.keys(Vn).forEach(function(e){wf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Vn[t]=Vn[e]})});function Is(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Vn.hasOwnProperty(e)&&Vn[e]?(""+t).trim():t+"px"}function Fs(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Is(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var kf=K({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function So(e,t){if(t){if(kf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(g(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(g(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(g(61))}if(t.style!=null&&typeof t.style!="object")throw Error(g(62))}}function xo(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var _o=null;function pu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Co=null,cn=null,fn=null;function fi(e){if(e=vr(e)){if(typeof Co!="function")throw Error(g(280));var t=e.stateNode;t&&(t=_l(t),Co(e.stateNode,e.type,t))}}function Us(e){cn?fn?fn.push(e):fn=[e]:cn=e}function Bs(){if(cn){var e=cn,t=fn;if(fn=cn=null,fi(e),t)for(e=0;e>>=0,e===0?32:31-(Lf(e)/Rf|0)|0}var _r=64,Cr=4194304;function Bn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function qr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,u=n&268435455;if(u!==0){var i=u&~l;i!==0?r=Bn(i):(o&=u,o!==0&&(r=Bn(o)))}else u=n&~l,u!==0?r=Bn(u):o!==0&&(r=Bn(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function hr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ue(t),e[t]=n}function If(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Hn),ki=" ",Si=!1;function oa(e,t){switch(e){case"keyup":return cd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ua(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Jt=!1;function dd(e,t){switch(e){case"compositionend":return ua(t);case"keypress":return t.which!==32?null:(Si=!0,ki);case"textInput":return e=t.data,e===ki&&Si?null:e;default:return null}}function pd(e,t){if(Jt)return e==="compositionend"||!Su&&oa(e,t)?(e=ra(),Br=gu=mt=null,Jt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Ei(n)}}function ca(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ca(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function fa(){for(var e=window,t=Xr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Xr(e.document)}return t}function xu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function xd(e){var t=fa(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ca(n.ownerDocument.documentElement,n)){if(r!==null&&xu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Ni(n,o);var u=Ni(n,r);l&&u&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(u.node,u.offset)):(t.setEnd(u.node,u.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,qt=null,zo=null,Qn=null,Lo=!1;function Pi(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Lo||qt==null||qt!==Xr(r)||(r=qt,"selectionStart"in r&&xu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Qn&&rr(Qn,r)||(Qn=r,r=tl(zo,"onSelect"),0tn||(e.current=Fo[tn],Fo[tn]=null,tn--)}function U(e,t){tn++,Fo[tn]=e.current,e.current=t}var Nt={},ae=jt(Nt),ge=jt(!1),Bt=Nt;function yn(e,t){var n=e.type.contextTypes;if(!n)return Nt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function rl(){$(ge),$(ae)}function Mi(e,t,n){if(ae.current!==Nt)throw Error(g(168));U(ae,t),U(ge,n)}function ka(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(g(108,yf(e)||"Unknown",l));return K({},n,r)}function ll(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Nt,Bt=ae.current,U(ae,e),U(ge,ge.current),!0}function Di(e,t,n){var r=e.stateNode;if(!r)throw Error(g(169));n?(e=ka(e,t,Bt),r.__reactInternalMemoizedMergedChildContext=e,$(ge),$(ae),U(ae,e)):$(ge),U(ge,n)}var Je=null,Cl=!1,eo=!1;function Sa(e){Je===null?Je=[e]:Je.push(e)}function Md(e){Cl=!0,Sa(e)}function Tt(){if(!eo&&Je!==null){eo=!0;var e=0,t=F;try{var n=Je;for(F=1;e>=u,l-=u,qe=1<<32-Ue(t)+l|n<j?(V=P,P=null):V=P.sibling;var R=p(c,P,f[j],y);if(R===null){P===null&&(P=V);break}e&&P&&R.alternate===null&&t(c,P),a=o(R,a,j),E===null?x=R:E.sibling=R,E=R,P=V}if(j===f.length)return n(c,P),H&&Rt(c,j),x;if(P===null){for(;jj?(V=P,P=null):V=P.sibling;var me=p(c,P,R.value,y);if(me===null){P===null&&(P=V);break}e&&P&&me.alternate===null&&t(c,P),a=o(me,a,j),E===null?x=me:E.sibling=me,E=me,P=V}if(R.done)return n(c,P),H&&Rt(c,j),x;if(P===null){for(;!R.done;j++,R=f.next())R=m(c,R.value,y),R!==null&&(a=o(R,a,j),E===null?x=R:E.sibling=R,E=R);return H&&Rt(c,j),x}for(P=r(c,P);!R.done;j++,R=f.next())R=w(P,c,j,R.value,y),R!==null&&(e&&R.alternate!==null&&P.delete(R.key===null?j:R.key),a=o(R,a,j),E===null?x=R:E.sibling=R,E=R);return e&&P.forEach(function(Ge){return t(c,Ge)}),H&&Rt(c,j),x}function M(c,a,f,y){if(typeof f=="object"&&f!==null&&f.type===Zt&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case kr:e:{for(var x=f.key,E=a;E!==null;){if(E.key===x){if(x=f.type,x===Zt){if(E.tag===7){n(c,E.sibling),a=l(E,f.props.children),a.return=c,c=a;break e}}else if(E.elementType===x||typeof x=="object"&&x!==null&&x.$$typeof===ft&&Ui(x)===E.type){n(c,E.sibling),a=l(E,f.props),a.ref=Mn(c,E,f),a.return=c,c=a;break e}n(c,E);break}else t(c,E);E=E.sibling}f.type===Zt?(a=Ut(f.props.children,c.mode,y,f.key),a.return=c,c=a):(y=Yr(f.type,f.key,f.props,null,c.mode,y),y.ref=Mn(c,a,f),y.return=c,c=y)}return u(c);case Gt:e:{for(E=f.key;a!==null;){if(a.key===E)if(a.tag===4&&a.stateNode.containerInfo===f.containerInfo&&a.stateNode.implementation===f.implementation){n(c,a.sibling),a=l(a,f.children||[]),a.return=c,c=a;break e}else{n(c,a);break}else t(c,a);a=a.sibling}a=so(f,c.mode,y),a.return=c,c=a}return u(c);case ft:return E=f._init,M(c,a,E(f._payload),y)}if(Un(f))return k(c,a,f,y);if(Tn(f))return S(c,a,f,y);Lr(c,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,a!==null&&a.tag===6?(n(c,a.sibling),a=l(a,f),a.return=c,c=a):(n(c,a),a=io(f,c.mode,y),a.return=c,c=a),u(c)):n(c,a)}return M}var wn=Ea(!0),Na=Ea(!1),il=jt(null),sl=null,ln=null,Nu=null;function Pu(){Nu=ln=sl=null}function ju(e){var t=il.current;$(il),e._currentValue=t}function $o(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function pn(e,t){sl=e,Nu=ln=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ye=!0),e.firstContext=null)}function Le(e){var t=e._currentValue;if(Nu!==e)if(e={context:e,memoizedValue:t,next:null},ln===null){if(sl===null)throw Error(g(308));ln=e,sl.dependencies={lanes:0,firstContext:e}}else ln=ln.next=e;return t}var Dt=null;function Tu(e){Dt===null?Dt=[e]:Dt.push(e)}function Pa(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Tu(t)):(n.next=l.next,l.next=n),t.interleaved=n,rt(e,r)}function rt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var dt=!1;function zu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ja(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function et(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function St(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,I&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,rt(e,n)}return l=r.interleaved,l===null?(t.next=t,Tu(r)):(t.next=l.next,l.next=t),r.interleaved=t,rt(e,n)}function Vr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,mu(e,n)}}function Bi(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var u={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=u:o=o.next=u,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function al(e,t,n,r){var l=e.updateQueue;dt=!1;var o=l.firstBaseUpdate,u=l.lastBaseUpdate,i=l.shared.pending;if(i!==null){l.shared.pending=null;var s=i,d=s.next;s.next=null,u===null?o=d:u.next=d,u=s;var v=e.alternate;v!==null&&(v=v.updateQueue,i=v.lastBaseUpdate,i!==u&&(i===null?v.firstBaseUpdate=d:i.next=d,v.lastBaseUpdate=s))}if(o!==null){var m=l.baseState;u=0,v=d=s=null,i=o;do{var p=i.lane,w=i.eventTime;if((r&p)===p){v!==null&&(v=v.next={eventTime:w,lane:0,tag:i.tag,payload:i.payload,callback:i.callback,next:null});e:{var k=e,S=i;switch(p=t,w=n,S.tag){case 1:if(k=S.payload,typeof k=="function"){m=k.call(w,m,p);break e}m=k;break e;case 3:k.flags=k.flags&-65537|128;case 0:if(k=S.payload,p=typeof k=="function"?k.call(w,m,p):k,p==null)break e;m=K({},m,p);break e;case 2:dt=!0}}i.callback!==null&&i.lane!==0&&(e.flags|=64,p=l.effects,p===null?l.effects=[i]:p.push(i))}else w={eventTime:w,lane:p,tag:i.tag,payload:i.payload,callback:i.callback,next:null},v===null?(d=v=w,s=m):v=v.next=w,u|=p;if(i=i.next,i===null){if(i=l.shared.pending,i===null)break;p=i,i=p.next,p.next=null,l.lastBaseUpdate=p,l.shared.pending=null}}while(!0);if(v===null&&(s=m),l.baseState=s,l.firstBaseUpdate=d,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do u|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);At|=u,e.lanes=u,e.memoizedState=m}}function $i(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=no.transition;no.transition={};try{e(!1),t()}finally{F=n,no.transition=r}}function Qa(){return Re().memoizedState}function Ud(e,t,n){var r=_t(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ka(e))Ya(t,n);else if(n=Pa(e,t,n,r),n!==null){var l=de();Be(n,e,r,l),Xa(n,t,r)}}function Bd(e,t,n){var r=_t(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ka(e))Ya(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var u=t.lastRenderedState,i=o(u,n);if(l.hasEagerState=!0,l.eagerState=i,$e(i,u)){var s=t.interleaved;s===null?(l.next=l,Tu(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Pa(e,t,l,r),n!==null&&(l=de(),Be(n,e,r,l),Xa(n,t,r))}}function Ka(e){var t=e.alternate;return e===Q||t!==null&&t===Q}function Ya(e,t){Kn=fl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Xa(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,mu(e,n)}}var dl={readContext:Le,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},$d={readContext:Le,useCallback:function(e,t){return Qe().memoizedState=[e,t===void 0?null:t],e},useContext:Le,useEffect:Ai,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Hr(4194308,4,$a.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Hr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Hr(4,2,e,t)},useMemo:function(e,t){var n=Qe();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Qe();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Ud.bind(null,Q,e),[r.memoizedState,e]},useRef:function(e){var t=Qe();return e={current:e},t.memoizedState=e},useState:Vi,useDebugValue:Uu,useDeferredValue:function(e){return Qe().memoizedState=e},useTransition:function(){var e=Vi(!1),t=e[0];return e=Fd.bind(null,e[1]),Qe().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Q,l=Qe();if(H){if(n===void 0)throw Error(g(407));n=n()}else{if(n=t(),te===null)throw Error(g(349));Vt&30||Ra(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Ai(Ma.bind(null,r,o,e),[e]),r.flags|=2048,fr(9,Oa.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Qe(),t=te.identifierPrefix;if(H){var n=be,r=qe;n=(r&~(1<<32-Ue(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=ar++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=u.createElement(n,{is:r.is}):(e=u.createElement(n),n==="select"&&(u=e,r.multiple?u.multiple=!0:r.size&&(u.size=r.size))):e=u.createElementNS(e,n),e[Ke]=t,e[ur]=r,lc(e,t,!1,!1),t.stateNode=e;e:{switch(u=xo(n,r),n){case"dialog":B("cancel",e),B("close",e),l=r;break;case"iframe":case"object":case"embed":B("load",e),l=r;break;case"video":case"audio":for(l=0;l<$n.length;l++)B($n[l],e);l=r;break;case"source":B("error",e),l=r;break;case"img":case"image":case"link":B("error",e),B("load",e),l=r;break;case"details":B("toggle",e),l=r;break;case"input":ii(e,r),l=vo(e,r),B("invalid",e);break;case"option":l=r;break;case"select":e._wrapperState={wasMultiple:!!r.multiple},l=K({},r,{value:void 0}),B("invalid",e);break;case"textarea":ai(e,r),l=wo(e,r),B("invalid",e);break;default:l=r}So(n,l),i=l;for(o in i)if(i.hasOwnProperty(o)){var s=i[o];o==="style"?Fs(e,s):o==="dangerouslySetInnerHTML"?(s=s?s.__html:void 0,s!=null&&Ds(e,s)):o==="children"?typeof s=="string"?(n!=="textarea"||s!=="")&&Jn(e,s):typeof s=="number"&&Jn(e,""+s):o!=="suppressContentEditableWarning"&&o!=="suppressHydrationWarning"&&o!=="autoFocus"&&(Zn.hasOwnProperty(o)?s!=null&&o==="onScroll"&&B("scroll",e):s!=null&&au(e,o,s,u))}switch(n){case"input":Sr(e),si(e,r,!1);break;case"textarea":Sr(e),ci(e);break;case"option":r.value!=null&&e.setAttribute("value",""+Et(r.value));break;case"select":e.multiple=!!r.multiple,o=r.value,o!=null?an(e,!!r.multiple,o,!1):r.defaultValue!=null&&an(e,!!r.multiple,r.defaultValue,!0);break;default:typeof l.onClick=="function"&&(e.onclick=nl)}switch(n){case"button":case"input":case"select":case"textarea":r=!!r.autoFocus;break e;case"img":r=!0;break e;default:r=!1}}r&&(t.flags|=4)}t.ref!==null&&(t.flags|=512,t.flags|=2097152)}return ie(t),null;case 6:if(e&&t.stateNode!=null)uc(e,t,e.memoizedProps,r);else{if(typeof r!="string"&&t.stateNode===null)throw Error(g(166));if(n=It(sr.current),It(Xe.current),zr(t)){if(r=t.stateNode,n=t.memoizedProps,r[Ke]=t,(o=r.nodeValue!==n)&&(e=_e,e!==null))switch(e.tag){case 3:Tr(r.nodeValue,n,(e.mode&1)!==0);break;case 5:e.memoizedProps.suppressHydrationWarning!==!0&&Tr(r.nodeValue,n,(e.mode&1)!==0)}o&&(t.flags|=4)}else r=(n.nodeType===9?n:n.ownerDocument).createTextNode(r),r[Ke]=t,t.stateNode=r}return ie(t),null;case 13:if($(W),r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(H&&xe!==null&&t.mode&1&&!(t.flags&128))Ca(),gn(),t.flags|=98560,o=!1;else if(o=zr(t),r!==null&&r.dehydrated!==null){if(e===null){if(!o)throw Error(g(318));if(o=t.memoizedState,o=o!==null?o.dehydrated:null,!o)throw Error(g(317));o[Ke]=t}else gn(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;ie(t),o=!1}else Fe!==null&&(nu(Fe),Fe=null),o=!0;if(!o)return t.flags&65536?t:null}return t.flags&128?(t.lanes=n,t):(r=r!==null,r!==(e!==null&&e.memoizedState!==null)&&r&&(t.child.flags|=8192,t.mode&1&&(e===null||W.current&1?b===0&&(b=3):Qu())),t.updateQueue!==null&&(t.flags|=4),ie(t),null);case 4:return kn(),Xo(e,t),e===null&&lr(t.stateNode.containerInfo),ie(t),null;case 10:return ju(t.type._context),ie(t),null;case 17:return we(t.type)&&rl(),ie(t),null;case 19:if($(W),o=t.memoizedState,o===null)return ie(t),null;if(r=(t.flags&128)!==0,u=o.rendering,u===null)if(r)Dn(o,!1);else{if(b!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(u=cl(e),u!==null){for(t.flags|=128,Dn(o,!1),r=u.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),t.subtreeFlags=0,r=n,n=t.child;n!==null;)o=n,e=r,o.flags&=14680066,u=o.alternate,u===null?(o.childLanes=0,o.lanes=e,o.child=null,o.subtreeFlags=0,o.memoizedProps=null,o.memoizedState=null,o.updateQueue=null,o.dependencies=null,o.stateNode=null):(o.childLanes=u.childLanes,o.lanes=u.lanes,o.child=u.child,o.subtreeFlags=0,o.deletions=null,o.memoizedProps=u.memoizedProps,o.memoizedState=u.memoizedState,o.updateQueue=u.updateQueue,o.type=u.type,e=u.dependencies,o.dependencies=e===null?null:{lanes:e.lanes,firstContext:e.firstContext}),n=n.sibling;return U(W,W.current&1|2),t.child}e=e.sibling}o.tail!==null&&G()>xn&&(t.flags|=128,r=!0,Dn(o,!1),t.lanes=4194304)}else{if(!r)if(e=cl(u),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Dn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!u.alternate&&!H)return ie(t),null}else 2*G()-o.renderingStartTime>xn&&n!==1073741824&&(t.flags|=128,r=!0,Dn(o,!1),t.lanes=4194304);o.isBackwards?(u.sibling=t.child,t.child=u):(n=o.last,n!==null?n.sibling=u:t.child=u,o.last=u)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=G(),t.sibling=null,n=W.current,U(W,r?n&1|2:n&1),t):(ie(t),null);case 22:case 23:return Wu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Se&1073741824&&(ie(t),t.subtreeFlags&6&&(t.flags|=8192)):ie(t),null;case 24:return null;case 25:return null}throw Error(g(156,t.tag))}function Xd(e,t){switch(Cu(t),t.tag){case 1:return we(t.type)&&rl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return kn(),$(ge),$(ae),Ou(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ru(t),null;case 13:if($(W),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(g(340));gn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return $(W),null;case 4:return kn(),null;case 10:return ju(t.type._context),null;case 22:case 23:return Wu(),null;case 24:return null;default:return null}}var Or=!1,se=!1,Gd=typeof WeakSet=="function"?WeakSet:Set,C=null;function on(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Y(e,t,r)}else n.current=null}function Go(e,t,n){try{n()}catch(r){Y(e,t,r)}}var bi=!1;function Zd(e,t){if(Ro=br,e=fa(),xu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var u=0,i=-1,s=-1,d=0,v=0,m=e,p=null;t:for(;;){for(var w;m!==n||l!==0&&m.nodeType!==3||(i=u+l),m!==o||r!==0&&m.nodeType!==3||(s=u+r),m.nodeType===3&&(u+=m.nodeValue.length),(w=m.firstChild)!==null;)p=m,m=w;for(;;){if(m===e)break t;if(p===n&&++d===l&&(i=u),p===o&&++v===r&&(s=u),(w=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=w}n=i===-1||s===-1?null:{start:i,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Oo={focusedElem:e,selectionRange:n},br=!1,C=t;C!==null;)if(t=C,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,C=e;else for(;C!==null;){t=C;try{var k=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(k!==null){var S=k.memoizedProps,M=k.memoizedState,c=t.stateNode,a=c.getSnapshotBeforeUpdate(t.elementType===t.type?S:De(t.type,S),M);c.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(g(163))}}catch(y){Y(t,t.return,y)}if(e=t.sibling,e!==null){e.return=t.return,C=e;break}C=t.return}return k=bi,bi=!1,k}function Yn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Go(t,n,o)}l=l.next}while(l!==r)}}function Pl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Zo(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function ic(e){var t=e.alternate;t!==null&&(e.alternate=null,ic(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ke],delete t[ur],delete t[Io],delete t[Rd],delete t[Od])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function sc(e){return e.tag===5||e.tag===3||e.tag===4}function es(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||sc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Jo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=nl));else if(r!==4&&(e=e.child,e!==null))for(Jo(e,t,n),e=e.sibling;e!==null;)Jo(e,t,n),e=e.sibling}function qo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(qo(e,t,n),e=e.sibling;e!==null;)qo(e,t,n),e=e.sibling}var ne=null,Ie=!1;function ct(e,t,n){for(n=n.child;n!==null;)ac(e,t,n),n=n.sibling}function ac(e,t,n){if(Ye&&typeof Ye.onCommitFiberUnmount=="function")try{Ye.onCommitFiberUnmount(wl,n)}catch{}switch(n.tag){case 5:se||on(n,t);case 6:var r=ne,l=Ie;ne=null,ct(e,t,n),ne=r,Ie=l,ne!==null&&(Ie?(e=ne,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ne.removeChild(n.stateNode));break;case 18:ne!==null&&(Ie?(e=ne,n=n.stateNode,e.nodeType===8?bl(e.parentNode,n):e.nodeType===1&&bl(e,n),tr(e)):bl(ne,n.stateNode));break;case 4:r=ne,l=Ie,ne=n.stateNode.containerInfo,Ie=!0,ct(e,t,n),ne=r,Ie=l;break;case 0:case 11:case 14:case 15:if(!se&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,u=o.destroy;o=o.tag,u!==void 0&&(o&2||o&4)&&Go(n,t,u),l=l.next}while(l!==r)}ct(e,t,n);break;case 1:if(!se&&(on(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(i){Y(n,t,i)}ct(e,t,n);break;case 21:ct(e,t,n);break;case 22:n.mode&1?(se=(r=se)||n.memoizedState!==null,ct(e,t,n),se=r):ct(e,t,n);break;default:ct(e,t,n)}}function ts(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Gd),t.forEach(function(r){var l=op.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Oe(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=u),r&=~o}if(r=l,r=G()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*qd(r/1960))-r,10e?16:e,vt===null)var r=!1;else{if(e=vt,vt=null,ml=0,I&6)throw Error(g(331));var l=I;for(I|=4,C=e.current;C!==null;){var o=C,u=o.child;if(C.flags&16){var i=o.deletions;if(i!==null){for(var s=0;sG()-Au?Ft(e,0):Vu|=n),ke(e,t)}function yc(e,t){t===0&&(e.mode&1?(t=Cr,Cr<<=1,!(Cr&130023424)&&(Cr=4194304)):t=1);var n=de();e=rt(e,t),e!==null&&(hr(e,t,n),ke(e,n))}function lp(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),yc(e,n)}function op(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(g(314))}r!==null&&r.delete(t),yc(e,n)}var gc;gc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Kd(e,t,n);ye=!!(e.flags&131072)}else ye=!1,H&&t.flags&1048576&&xa(t,ul,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Wr(e,t),e=t.pendingProps;var l=yn(t,ae.current);pn(t,n),l=Du(null,t,r,e,l,n);var o=Iu();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,ll(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,zu(t),l.updater=Nl,t.stateNode=l,l._reactInternals=t,Ao(t,r,e,n),t=Qo(null,t,r,!0,o,n)):(t.tag=0,H&&o&&_u(t),fe(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Wr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=ip(r),e=De(r,e),l){case 0:t=Wo(null,t,r,e,n);break e;case 1:t=Zi(null,t,r,e,n);break e;case 11:t=Xi(null,t,r,e,n);break e;case 14:t=Gi(null,t,r,De(r.type,e),n);break e}throw Error(g(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:De(r,l),Wo(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:De(r,l),Zi(e,t,r,l,n);case 3:e:{if(tc(t),e===null)throw Error(g(387));r=t.pendingProps,o=t.memoizedState,l=o.element,ja(e,t),al(t,r,null,n);var u=t.memoizedState;if(r=u.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:u.cache,pendingSuspenseBoundaries:u.pendingSuspenseBoundaries,transitions:u.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Sn(Error(g(423)),t),t=Ji(e,t,r,n,l);break e}else if(r!==l){l=Sn(Error(g(424)),t),t=Ji(e,t,r,n,l);break e}else for(xe=kt(t.stateNode.containerInfo.firstChild),_e=t,H=!0,Fe=null,n=Na(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(gn(),r===l){t=lt(e,t,n);break e}fe(e,t,r,n)}t=t.child}return t;case 5:return Ta(t),e===null&&Bo(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,u=l.children,Mo(r,l)?u=null:o!==null&&Mo(r,o)&&(t.flags|=32),ec(e,t),fe(e,t,u,n),t.child;case 6:return e===null&&Bo(t),null;case 13:return nc(e,t,n);case 4:return Lu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=wn(t,null,r,n):fe(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:De(r,l),Xi(e,t,r,l,n);case 7:return fe(e,t,t.pendingProps,n),t.child;case 8:return fe(e,t,t.pendingProps.children,n),t.child;case 12:return fe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,u=l.value,U(il,r._currentValue),r._currentValue=u,o!==null)if($e(o.value,u)){if(o.children===l.children&&!ge.current){t=lt(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var i=o.dependencies;if(i!==null){u=o.child;for(var s=i.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=et(-1,n&-n),s.tag=2;var d=o.updateQueue;if(d!==null){d=d.shared;var v=d.pending;v===null?s.next=s:(s.next=v.next,v.next=s),d.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),$o(o.return,n,t),i.lanes|=n;break}s=s.next}}else if(o.tag===10)u=o.type===t.type?null:o.child;else if(o.tag===18){if(u=o.return,u===null)throw Error(g(341));u.lanes|=n,i=u.alternate,i!==null&&(i.lanes|=n),$o(u,n,t),u=o.sibling}else u=o.child;if(u!==null)u.return=o;else for(u=o;u!==null;){if(u===t){u=null;break}if(o=u.sibling,o!==null){o.return=u.return,u=o;break}u=u.return}o=u}fe(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,pn(t,n),l=Le(l),r=r(l),t.flags|=1,fe(e,t,r,n),t.child;case 14:return r=t.type,l=De(r,t.pendingProps),l=De(r.type,l),Gi(e,t,r,l,n);case 15:return qa(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:De(r,l),Wr(e,t),t.tag=1,we(r)?(e=!0,ll(t)):e=!1,pn(t,n),Ga(t,r,l),Ao(t,r,l,n),Qo(null,t,r,!0,e,n);case 19:return rc(e,t,n);case 22:return ba(e,t,n)}throw Error(g(156,t.tag))};function wc(e,t){return Ks(e,t)}function up(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Te(e,t,n,r){return new up(e,t,n,r)}function Ku(e){return e=e.prototype,!(!e||!e.isReactComponent)}function ip(e){if(typeof e=="function")return Ku(e)?1:0;if(e!=null){if(e=e.$$typeof,e===fu)return 11;if(e===du)return 14}return 2}function Ct(e,t){var n=e.alternate;return n===null?(n=Te(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Yr(e,t,n,r,l,o){var u=2;if(r=e,typeof e=="function")Ku(e)&&(u=1);else if(typeof e=="string")u=5;else e:switch(e){case Zt:return Ut(n.children,l,o,t);case cu:u=8,l|=8;break;case fo:return e=Te(12,n,t,l|2),e.elementType=fo,e.lanes=o,e;case po:return e=Te(13,n,t,l),e.elementType=po,e.lanes=o,e;case ho:return e=Te(19,n,t,l),e.elementType=ho,e.lanes=o,e;case Ts:return Tl(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ps:u=10;break e;case js:u=9;break e;case fu:u=11;break e;case du:u=14;break e;case ft:u=16,r=null;break e}throw Error(g(130,e==null?e:typeof e,""))}return t=Te(u,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Ut(e,t,n,r){return e=Te(7,e,r,t),e.lanes=n,e}function Tl(e,t,n,r){return e=Te(22,e,r,t),e.elementType=Ts,e.lanes=n,e.stateNode={isHidden:!1},e}function io(e,t,n){return e=Te(6,e,null,t),e.lanes=n,e}function so(e,t,n){return t=Te(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function sp(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Al(0),this.expirationTimes=Al(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Al(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Yu(e,t,n,r,l,o,u,i,s){return e=new sp(e,t,n,i,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Te(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},zu(o),e}function ap(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(_c)}catch(e){console.error(e)}}_c(),_s.exports=Ee;var hp=_s.exports,as=hp;ao.createRoot=as.createRoot,ao.hydrateRoot=as.hydrateRoot;function mp(e){return new Promise(t=>{window.setTimeout(t,e)})}function vp(e){const t=e.trim().slice(0,512).toLowerCase();return t.startsWith("\s*([^<]+)\s*<\/title>/i);return((n=t==null?void 0:t[1])==null?void 0:n.trim())||""}function gp(e){var n;const t=e.match(/Request ID:\s*([0-9a-f-]+)/i);return((n=t==null?void 0:t[1])==null?void 0:n.trim())||""}async function Me(e,t){const n=await fetch(e,t),r=await n.text();let l={};if(r.trim()!=="")try{l=JSON.parse(r)}catch{l={error:r}}if(!n.ok){let o=typeof l=="object"&&l!==null&&"error"in l?String(l.error):`${n.status} ${n.statusText}`;if(vp(r)){const u=yp(r),i=gp(r);o=`upstream gateway error (${n.status}${u?` ${u}`:""})`,i&&(o=`${o}; request id ${i}`)}throw new Error(o)}return l}function sn(e){if(e==null||Number.isNaN(e))return"n/a";if(e<=0)return"0 B";const t=["B","KiB","MiB","GiB","TiB"];let n=e,r=0;for(;n>=1024&&rString(l).padStart(2,"0"),r=[t.getUTCFullYear(),n(t.getUTCMonth()+1),n(t.getUTCDate()),n(t.getUTCHours()),n(t.getUTCMinutes())].join("");return`restore-${e}-${r}`.toLowerCase().replace(/[^a-z0-9-]/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").slice(0,63).replace(/-+$/g,"")}function cs(){const e=new Date,t=r=>String(r).padStart(2,"0");return`restore-${[e.getUTCFullYear(),t(e.getUTCMonth()+1),t(e.getUTCDate()),t(e.getUTCHours()),t(e.getUTCMinutes())].join("")}-`}function xp({b2Usage:e,b2Error:t,b2Refreshing:n,onRefresh:r}){return h.jsxs("section",{className:"panel",children:[h.jsxs("div",{className:"panel-header",children:[h.jsx("h2",{children:"B2 Consumption"}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>void r(),disabled:n,children:n?"Refreshing...":"Refresh B2"})]}),t&&h.jsx("p",{className:"error",children:t}),!t&&!e.enabled&&h.jsx("p",{className:"subtle",children:"B2 monitoring is disabled in Soteria config."}),!t&&e.enabled&&!e.available&&h.jsx("p",{className:"error",children:e.error||"B2 usage currently unavailable."}),e.enabled&&h.jsxs("div",{className:"stack",children:[h.jsxs("p",{className:"subtle tiny",children:["Endpoint: ",e.endpoint||"n/a"," | Region: ",e.region||"n/a"]}),h.jsxs("p",{className:"subtle tiny",children:["Last scan: ",mn(e.scanned_at)," | Duration: ",e.scan_duration_ms||0,"ms"]}),h.jsxs("div",{className:"stat-grid",children:[h.jsxs("div",{className:"stat",children:[h.jsx("span",{className:"label",children:"Stored bytes"}),h.jsx("strong",{children:sn(e.total_bytes)})]}),h.jsxs("div",{className:"stat",children:[h.jsx("span",{className:"label",children:"Objects"}),h.jsx("strong",{children:e.total_objects})]}),h.jsxs("div",{className:"stat",children:[h.jsx("span",{className:"label",children:"Recent bytes (24h)"}),h.jsx("strong",{children:sn(e.recent_bytes_24h)})]}),h.jsxs("div",{className:"stat",children:[h.jsx("span",{className:"label",children:"Recent objects (24h)"}),h.jsx("strong",{children:e.recent_objects_24h})]})]}),h.jsx("p",{className:"subtle tiny",children:"Recent 24h values are object-change bandwidth proxy from bucket scans. B2 egress billing totals are not exposed by S3 object listing."}),h.jsx("div",{className:"bucket-table-wrap",children:h.jsxs("table",{children:[h.jsx("thead",{children:h.jsxs("tr",{children:[h.jsx("th",{children:"Bucket"}),h.jsx("th",{children:"Objects"}),h.jsx("th",{children:"Stored"}),h.jsx("th",{children:"Recent 24h"})]})}),h.jsx("tbody",{children:(e.buckets||[]).map(l=>h.jsxs("tr",{children:[h.jsxs("td",{children:[h.jsx("div",{children:l.name}),h.jsxs("div",{className:"subtle tiny",children:["Last object: ",mn(l.last_modified_at)]})]}),h.jsx("td",{children:l.object_count}),h.jsx("td",{children:sn(l.total_bytes)}),h.jsx("td",{children:sn(l.recent_bytes_24h)})]},l.name))})]})})]})]})}function _p({policies:e,policyError:t,namespaceOptions:n,policyNamespace:r,policyPVC:l,policyIntervalHours:o,policyEnabled:u,policyDedupe:i,policyKeepLast:s,busy:d,onPolicyNamespaceChange:v,onPolicyPVCChange:m,onPolicyIntervalHoursChange:p,onPolicyEnabledChange:w,onPolicyDedupeChange:k,onPolicyKeepLastChange:S,onSavePolicy:M,onDeletePolicy:c,onLoadPolicy:a}){return h.jsxs("section",{className:"panel scroll-panel",children:[h.jsx("h2",{children:"Backup Policies"}),h.jsx("p",{className:"subtle tiny",children:"Policy backups create new restic snapshots. `Keep last` controls version retention per PVC: 1 means only newest copy remains after each run. With dedupe on, unchanged blocks are reused in the shared repository. With dedupe off, Soteria isolates each PVC to its own repository path."}),h.jsxs("div",{className:"stack",children:[h.jsxs("label",{children:["Namespace",h.jsx("select",{value:r,onChange:f=>v(f.target.value),children:n.map(f=>h.jsx("option",{value:f,children:f},f))})]}),h.jsxs("label",{children:["PVC (optional)",h.jsx("input",{value:l,onChange:f=>m(f.target.value),placeholder:"blank means all PVCs in namespace"})]}),h.jsxs("label",{children:["Interval hours",h.jsx("input",{type:"number",min:1,value:o,onChange:f=>p(Math.max(1,Number(f.target.value||1)))})]}),h.jsxs("label",{className:"checkbox-row",children:[h.jsx("input",{type:"checkbox",checked:u,onChange:f=>w(f.target.checked)}),"Enabled"]}),h.jsxs("label",{className:"checkbox-row",children:[h.jsx("input",{type:"checkbox",checked:i,onChange:f=>k(f.target.checked)}),"Dedupe unchanged blocks"]}),h.jsxs("label",{children:["Keep last snapshots per PVC (0 = keep all)",h.jsx("input",{type:"number",min:0,max:1e3,value:s,onChange:f=>S(Math.max(0,Math.min(1e3,Number(f.target.value||0))))})]}),h.jsx("button",{type:"button",onClick:()=>void M(),disabled:d||!r,children:"Save policy"})]}),t&&h.jsx("p",{className:"error",children:t}),!t&&e.length===0&&h.jsx("p",{className:"subtle",children:"No policies yet."}),h.jsx("div",{className:"policy-list",children:e.map(f=>h.jsxs("article",{className:"policy-item",children:[h.jsxs("div",{className:"policy-head",children:[h.jsxs("strong",{children:[f.namespace,"/",f.pvc||"*"]}),h.jsx("span",{className:`chip ${f.enabled?"good":"bad"}`,children:f.enabled?"Enabled":"Disabled"})]}),h.jsxs("p",{className:"subtle tiny",children:["Every ",f.interval_hours,"h | Dedupe: ",f.dedupe===!1?"off":"on"," | Keep last: ",f.keep_last??0," ","| Updated ",mn(f.updated_at||f.created_at)]}),h.jsxs("div",{className:"actions",children:[h.jsx("button",{type:"button",className:"secondary",onClick:()=>a(f),children:"Load"}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>void c(f.id),disabled:d,children:"Delete"})]})]},f.id))})]})}function Cp({inventory:e,inventoryError:t,manualDedupe:n,manualKeepLast:r,busy:l,onManualDedupeChange:o,onManualKeepLastChange:u,onTriggerNamespaceBackup:i,onOpenNamespaceSelection:s,onTriggerBackup:d,onOpenPVCSelection:v}){return h.jsxs("section",{className:"panel scroll-panel",children:[h.jsxs("div",{className:"panel-header",children:[h.jsx("h2",{children:"PVC Inventory"}),h.jsx("span",{className:"subtle",children:e!=null&&e.generated_at?`Updated ${mn(e.generated_at)}`:"No inventory yet"})]}),h.jsxs("label",{className:"checkbox-row",children:[h.jsx("input",{type:"checkbox",checked:n,onChange:m=>o(m.target.checked)}),"Dedupe unchanged blocks (default)"]}),h.jsxs("label",{children:["Keep last snapshots per PVC (0 = keep all)",h.jsx("input",{type:"number",min:0,max:1e3,value:r,onChange:m=>u(Math.max(0,Math.min(1e3,Number(m.target.value||0))))})]}),h.jsx("p",{className:"subtle tiny",children:"This setting applies to both `Backup now` and `Backup namespace` actions."}),t&&h.jsx("p",{className:"error",children:t}),!e&&!t&&h.jsx("p",{className:"subtle",children:"Loading inventory..."}),e==null?void 0:e.namespaces.map(m=>h.jsxs("article",{className:"namespace-block",children:[h.jsxs("div",{className:"namespace-row",children:[h.jsx("h3",{children:m.name}),h.jsxs("div",{className:"actions",children:[h.jsx("button",{type:"button",className:"secondary",onClick:()=>void i(m.name),disabled:l,children:"Backup namespace"}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>s(m.name),children:"Restore namespace"})]})]}),h.jsx("div",{className:"pvc-grid",children:m.pvcs.map(p=>{const w=p.health_reason==="excluded",k=w?"warn":p.healthy?"good":p.health_reason==="in_progress"?"warn":"bad",S=w?"Excluded":p.healthy?"Healthy":wp(p.health_reason),M=Math.max(0,Math.min(100,Number(p.last_job_progress_pct||0))),c=kp(p.last_job_state),a=!!p.last_job_name||(p.active_backups||0)>0,f=sn(p.last_backup_size_bytes),y=sn(p.total_backup_size_bytes),x=p.driver==="restic"&&(p.last_backup_size_bytes===void 0||p.total_backup_size_bytes===void 0);return h.jsxs("article",{className:"pvc-card",children:[h.jsxs("div",{className:"pvc-title-row",children:[h.jsxs("div",{children:[h.jsx("h4",{children:p.pvc}),h.jsxs("p",{className:"subtle tiny",children:[p.volume||"unknown volume"," | ",p.storage_class||"no class"," | ",p.capacity||"unknown size"]})]}),h.jsx("span",{className:`chip ${k}`,children:S})]}),h.jsxs("p",{className:"subtle tiny",children:["Last backup: ",p.last_backup_at?`${mn(p.last_backup_at)} (${(p.last_backup_age_hours||0).toFixed(1)}h ago)`:"never"]}),h.jsxs("p",{className:"subtle tiny",children:["Backups: ",p.completed_backups,"/",p.backup_count," completed | Latest size: ",f," | Total stored: ",y]}),x&&h.jsx("p",{className:"subtle tiny",children:"Per-PVC storage is estimated from restic upload summaries persisted by Soteria. Older backups created before tracking may show n/a until a new backup runs."}),a&&h.jsxs("div",{className:"backup-progress",children:[h.jsxs("div",{className:"progress-header",children:[h.jsxs("p",{className:"subtle tiny",children:["Job: ",p.last_job_name||"n/a",p.last_job_started_at?` | Started ${mn(p.last_job_started_at)}`:""]}),h.jsx("span",{className:`chip ${c}`,children:p.last_job_state||"Unknown"})]}),h.jsx("div",{className:"progress-track",role:"progressbar","aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":M,children:h.jsx("div",{className:`progress-fill ${c} ${p.active_backups>0?"active":""}`,style:{width:`${M}%`}})}),h.jsxs("p",{className:"subtle tiny",children:["Progress ",M,"% | Active jobs: ",p.active_backups||0]})]}),p.error&&h.jsx("p",{className:"error tiny",children:p.error}),h.jsxs("div",{className:"actions",children:[h.jsx("button",{type:"button",onClick:()=>void d(p.namespace,p.pvc),disabled:l||w,children:"Backup now"}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>void v(p.namespace,p.pvc),children:"Restore"})]})]},`${p.namespace}/${p.pvc}`)})})]},m.name))]})}function Ep({selection:e,namespaceOptions:t,restoreNamespace:n,restorePVC:r,restoreBackupURL:l,namespaceRestoreTarget:o,namespaceRestorePrefix:u,namespaceRestoreSnapshot:i,busy:s,onRestoreNamespaceChange:d,onRestorePVCChange:v,onRestoreBackupURLChange:m,onNamespaceRestoreTargetChange:p,onNamespaceRestorePrefixChange:w,onNamespaceRestoreSnapshotChange:k,onRunPVCRestore:S,onRunNamespaceRestore:M}){return h.jsxs("section",{className:"panel",children:[h.jsx("h2",{children:"Restore Planner"}),h.jsx("p",{className:"subtle tiny",children:"Inventory restore buttons preload this panel. This is where restore dry-runs and execution happen."}),e.kind==="none"&&h.jsx("p",{className:"subtle",children:"Choose Restore on a PVC or namespace to begin."}),e.kind==="pvc"&&h.jsxs("div",{className:"stack",children:[h.jsxs("p",{className:"subtle tiny",children:[h.jsx("strong",{children:"Source:"})," ",e.namespace,"/",e.pvc," (",e.volume,")"]}),h.jsxs("label",{children:["Backup snapshot",h.jsxs("select",{value:l,onChange:c=>m(c.target.value),children:[e.backups.length===0&&h.jsx("option",{value:"",children:"No completed backups"}),e.backups.map(c=>h.jsxs("option",{value:c.url||"",children:[c.name," | ",c.created||"unknown time"," | ",c.size||"size n/a"]},c.url||c.name))]})]}),h.jsxs("label",{children:["Target namespace",h.jsx("select",{value:n,onChange:c=>d(c.target.value),children:t.map(c=>h.jsx("option",{value:c,children:c},c))})]}),h.jsxs("label",{children:["Target PVC",h.jsx("input",{value:r,onChange:c=>v(c.target.value)})]}),h.jsxs("div",{className:"actions",children:[h.jsx("button",{type:"button",onClick:()=>void S(!1),disabled:s||!l,children:"Create restore PVC"}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>void S(!0),disabled:s||!l,children:"Dry run"})]})]}),e.kind==="namespace"&&h.jsxs("div",{className:"stack",children:[h.jsxs("p",{className:"subtle tiny",children:[h.jsx("strong",{children:"Source namespace:"})," ",e.namespace]}),h.jsxs("label",{children:["Target namespace",h.jsx("select",{value:o,onChange:c=>p(c.target.value),children:t.map(c=>h.jsx("option",{value:c,children:c},c))})]}),h.jsxs("label",{children:["Target PVC prefix",h.jsx("input",{value:u,onChange:c=>w(c.target.value)})]}),h.jsxs("label",{children:["Snapshot hint (optional)",h.jsx("input",{value:i,onChange:c=>k(c.target.value),placeholder:"blank = latest completed"})]}),h.jsxs("div",{className:"actions",children:[h.jsx("button",{type:"button",onClick:()=>void M(!1),disabled:s,children:"Create restore PVCs"}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>void M(!0),disabled:s,children:"Dry run"})]})]})]})}const fs={enabled:!1,available:!1,total_objects:0,total_bytes:0,recent_objects_24h:0,recent_bytes_24h:0,buckets:[]};function Np(){const[e,t]=D.useState(null),[n,r]=D.useState(""),[l,o]=D.useState(null),[u,i]=D.useState(""),[s,d]=D.useState([]),[v,m]=D.useState(""),[p,w]=D.useState(fs),[k,S]=D.useState(""),[M,c]=D.useState(!1),[a,f]=D.useState({kind:"none"}),[y,x]=D.useState(""),[E,P]=D.useState(""),[j,V]=D.useState(""),[R,me]=D.useState(""),[Ge,ut]=D.useState(cs()),[Nn,gr]=D.useState(""),[it,st]=D.useState(""),[_,T]=D.useState(""),[z,A]=D.useState(24),[X,zt]=D.useState(!0),[Ve,Kt]=D.useState(!0),[Ae,Lt]=D.useState(0),[Ml,Cc]=D.useState(!0),[Dl,Ec]=D.useState(0),[Nc,Ju]=D.useState("No action yet."),[Pn,oe]=D.useState(!1),jn=D.useMemo(()=>{if(!l)return 0;let N=0;for(const L of l.namespaces)for(const Z of L.pvcs)N+=Z.active_backups||0;return N},[l]),qu=D.useMemo(()=>(l==null?void 0:l.namespaces.map(N=>N.name))??[],[l]),ce=N=>{if(typeof N=="string"){Ju(N);return}Ju(JSON.stringify(N,null,2))},Pc=async()=>{try{const N=await Me("/v1/whoami");t(N),r("")}catch(N){t(null),r(N instanceof Error?N.message:"failed to load auth")}},at=async()=>{const N=async()=>Me("/v1/inventory");try{const L=await N();o(L),i(""),!it&&L.namespaces.length>0&&st(L.namespaces[0].name)}catch(L){let Z=L instanceof Error?L.message:"failed to load inventory";if(Z.includes("upstream gateway error"))try{await mp(1e3);const He=await N();o(He),i(""),!it&&He.namespaces.length>0&&st(He.namespaces[0].name);return}catch(He){Z=He instanceof Error?He.message:Z}i(Z)}},Il=async()=>{try{const N=await Me("/v1/policies");d(N.policies||[]),m("")}catch(N){d([]),m(N instanceof Error?N.message:"failed to load policies")}},Yt=async(N=!1)=>{try{const Z=await Me(N?"/v1/b2?refresh=1":"/v1/b2");w(Z),S("")}catch(L){w(fs),S(L instanceof Error?L.message:"failed to load B2 usage")}},jc=async()=>{c(!0);try{await Yt(!0)}finally{c(!1)}},bu=async()=>{oe(!0);try{await Promise.all([Pc(),at(),Il(),Yt()])}finally{oe(!1)}};D.useEffect(()=>{bu()},[]),D.useEffect(()=>{if(jn<=0)return;const N=window.setInterval(()=>{at()},8e3);return()=>{window.clearInterval(N)}},[jn]);const Tc=async(N,L)=>{oe(!0);try{const Z=await Me("/v1/backup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:N,pvc:L,dry_run:!1,dedupe:Ml,keep_last:Dl})});ce(Z),await Promise.all([at(),Yt()])}catch(Z){ce({error:Z instanceof Error?Z.message:"backup request failed",namespace:N,pvc:L})}finally{oe(!1)}},zc=async N=>{oe(!0);try{const L=await Me("/v1/backup/namespace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:N,dry_run:!1,dedupe:Ml,keep_last:Dl})});ce(L),await Promise.all([at(),Yt()])}catch(L){ce({error:L instanceof Error?L.message:"namespace backup failed",namespace:N})}finally{oe(!1)}},Lc=async(N,L)=>{oe(!0);try{const Z=await Me(`/v1/backups?namespace=${encodeURIComponent(N)}&pvc=${encodeURIComponent(L)}`),He=Z.backups.filter(ei=>ei.state==="Completed"&&ei.url);f({kind:"pvc",namespace:N,pvc:L,volume:Z.volume,backups:He}),x(N),P(Sp(L)),V(He.length>0?String(He[0].url):""),ce(Z)}catch(Z){ce({error:Z instanceof Error?Z.message:"failed to load backups",namespace:N,pvc:L})}finally{oe(!1)}},Rc=N=>{f({kind:"namespace",namespace:N}),me(N),ut(cs()),gr(""),ce(`Namespace restore planner loaded for ${N}.`)},Oc=async N=>{if(a.kind==="pvc"){oe(!0);try{const L=await Me("/v1/restores",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:a.namespace,pvc:a.pvc,backup_url:j,target_namespace:y,target_pvc:E,dry_run:N})});ce(L),await Promise.all([at(),Yt()])}catch(L){ce({error:L instanceof Error?L.message:"restore failed",namespace:a.namespace,pvc:a.pvc,target_namespace:y,target_pvc:E,dry_run:N})}finally{oe(!1)}}},Mc=async N=>{if(a.kind==="namespace"){oe(!0);try{const L=await Me("/v1/restores/namespace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:a.namespace,target_namespace:R,target_prefix:Ge,snapshot:Nn,dry_run:N})});ce(L),await Promise.all([at(),Yt()])}catch(L){ce({error:L instanceof Error?L.message:"namespace restore failed",namespace:a.namespace,target_namespace:R,target_prefix:Ge,dry_run:N})}finally{oe(!1)}}},Dc=async()=>{oe(!0);try{const N=await Me("/v1/policies",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:it,pvc:_,interval_hours:z,enabled:X,dedupe:Ve,keep_last:Ae})});ce(N),await Promise.all([Il(),at()])}catch(N){ce({error:N instanceof Error?N.message:"policy save failed"})}finally{oe(!1)}},Ic=async N=>{oe(!0);try{const L=await Me(`/v1/policies/${encodeURIComponent(N)}`,{method:"DELETE"});ce(L),await Promise.all([Il(),at()])}catch(L){ce({error:L instanceof Error?L.message:"policy delete failed",policy_id:N})}finally{oe(!1)}},Fc=N=>{st(N.namespace),T(N.pvc||""),A(N.interval_hours),zt(N.enabled),Kt(N.dedupe!==!1),Lt(N.keep_last??0)},Uc=e?`${e.user||e.email||"authenticated"} | groups: ${(e.groups||[]).join(", ")||"none"}`:n||"anonymous",Bc=e!=null&&e.allowed_groups&&e.allowed_groups.length>0?`Access requires: ${e.allowed_groups.join(", ")}`:"Access requires: any authenticated user";return h.jsxs("div",{className:"app-shell",children:[h.jsxs("header",{className:"topbar",children:[h.jsxs("div",{children:[h.jsx("h1",{children:"Soteria Backup Console"}),h.jsx("p",{className:"subtle",children:"Dark-mode React UI for backup drills, policy control, and B2 consumption visibility."})]}),h.jsxs("div",{className:"toolbar",children:[h.jsx("span",{className:`chip ${e?"good":"warn"}`,children:Uc}),h.jsx("span",{className:"chip",children:Bc}),jn>0&&h.jsxs("span",{className:"chip warn",children:[jn," backup job",jn===1?"":"s"," active"]}),h.jsx("button",{type:"button",className:"secondary",onClick:()=>void bu(),disabled:Pn,children:Pn?"Refreshing...":"Refresh"})]})]}),h.jsxs("main",{className:"layout",children:[h.jsx(Cp,{inventory:l,inventoryError:u,manualDedupe:Ml,manualKeepLast:Dl,busy:Pn,onManualDedupeChange:Cc,onManualKeepLastChange:Ec,onTriggerNamespaceBackup:zc,onOpenNamespaceSelection:Rc,onTriggerBackup:Tc,onOpenPVCSelection:Lc}),h.jsxs("section",{className:"column",children:[h.jsx(xp,{b2Usage:p,b2Error:k,b2Refreshing:M,onRefresh:jc}),h.jsx(Ep,{selection:a,namespaceOptions:qu,restoreNamespace:y,restorePVC:E,restoreBackupURL:j,namespaceRestoreTarget:R,namespaceRestorePrefix:Ge,namespaceRestoreSnapshot:Nn,busy:Pn,onRestoreNamespaceChange:x,onRestorePVCChange:P,onRestoreBackupURLChange:V,onNamespaceRestoreTargetChange:me,onNamespaceRestorePrefixChange:ut,onNamespaceRestoreSnapshotChange:gr,onRunPVCRestore:Oc,onRunNamespaceRestore:Mc}),h.jsxs("section",{className:"panel action-panel",children:[h.jsx("h2",{children:"Last Action"}),h.jsx("pre",{children:Nc})]})]}),h.jsx("section",{className:"column",children:h.jsx(_p,{policies:s,policyError:v,namespaceOptions:qu,policyNamespace:it,policyPVC:_,policyIntervalHours:z,policyEnabled:X,policyDedupe:Ve,policyKeepLast:Ae,busy:Pn,onPolicyNamespaceChange:st,onPolicyPVCChange:T,onPolicyIntervalHoursChange:A,onPolicyEnabledChange:zt,onPolicyDedupeChange:Kt,onPolicyKeepLastChange:Lt,onSavePolicy:Dc,onDeletePolicy:Ic,onLoadPolicy:Fc})})]})]})}ao.createRoot(document.getElementById("root")).render(h.jsx(nf.StrictMode,{children:h.jsx(Np,{})})); diff --git a/internal/server/ui-dist/assets/index-C8vHBL9g.js b/internal/server/ui-dist/assets/index-C8vHBL9g.js deleted file mode 100644 index 4b8ebb0..0000000 --- a/internal/server/ui-dist/assets/index-C8vHBL9g.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Wc(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ms={exports:{}},wl={},vs={exports:{}},O={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var hr=Symbol.for("react.element"),Qc=Symbol.for("react.portal"),Kc=Symbol.for("react.fragment"),Yc=Symbol.for("react.strict_mode"),Xc=Symbol.for("react.profiler"),Gc=Symbol.for("react.provider"),Zc=Symbol.for("react.context"),Jc=Symbol.for("react.forward_ref"),qc=Symbol.for("react.suspense"),bc=Symbol.for("react.memo"),ef=Symbol.for("react.lazy"),lu=Symbol.iterator;function tf(e){return e===null||typeof e!="object"?null:(e=lu&&e[lu]||e["@@iterator"],typeof e=="function"?e:null)}var ys={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},gs=Object.assign,ws={};function Cn(e,t,n){this.props=e,this.context=t,this.refs=ws,this.updater=n||ys}Cn.prototype.isReactComponent={};Cn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Cn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ks(){}ks.prototype=Cn.prototype;function oi(e,t,n){this.props=e,this.context=t,this.refs=ws,this.updater=n||ys}var ii=oi.prototype=new ks;ii.constructor=oi;gs(ii,Cn.prototype);ii.isPureReactComponent=!0;var ou=Array.isArray,Ss=Object.prototype.hasOwnProperty,ui={current:null},xs={key:!0,ref:!0,__self:!0,__source:!0};function _s(e,t,n){var r,l={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)Ss.call(t,r)&&!xs.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,X=C[A];if(0>>1;Al(Yt,L))Hel(Rt,Yt)?(C[A]=Rt,C[He]=L,A=He):(C[A]=Yt,C[Ae]=L,A=Ae);else if(Hel(Rt,L))C[A]=Rt,C[He]=L,A=He;else break e}}return T}function l(C,T){var L=C.sortIndex-T.sortIndex;return L!==0?L:C.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],c=[],y=1,v=null,h=3,k=!1,x=!1,_=!1,F=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,a=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function d(C){for(var T=n(c);T!==null;){if(T.callback===null)r(c);else if(T.startTime<=C)r(c),T.sortIndex=T.expirationTime,t(s,T);else break;T=n(c)}}function g(C){if(_=!1,d(C),!x)if(n(s)!==null)x=!0,Je(E);else{var T=n(c);T!==null&&at(g,T.startTime-C)}}function E(C,T){x=!1,_&&(_=!1,f(z),z=-1),k=!0;var L=h;try{for(d(T),v=n(s);v!==null&&(!(v.expirationTime>T)||C&&!me());){var A=v.callback;if(typeof A=="function"){v.callback=null,h=v.priorityLevel;var X=A(v.expirationTime<=T);T=e.unstable_now(),typeof X=="function"?v.callback=X:v===n(s)&&r(s),d(T)}else r(s);v=n(s)}if(v!==null)var Lt=!0;else{var Ae=n(c);Ae!==null&&at(g,Ae.startTime-T),Lt=!1}return Lt}finally{v=null,h=L,k=!1}}var j=!1,P=null,z=-1,V=5,R=-1;function me(){return!(e.unstable_now()-RC||125A?(C.sortIndex=L,t(c,C),n(s)===null&&C===n(c)&&(_?(f(z),z=-1):_=!0,at(g,L-A))):(C.sortIndex=X,t(s,C),x||k||(x=!0,Je(E))),C},e.unstable_shouldYield=me,e.unstable_wrapCallback=function(C){var T=h;return function(){var L=h;h=T;try{return C.apply(this,arguments)}finally{h=L}}}})(Ps);js.exports=Ps;var hf=js.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var mf=M,Ee=hf;function w(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),po=Object.prototype.hasOwnProperty,vf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,uu={},su={};function yf(e){return po.call(su,e)?!0:po.call(uu,e)?!1:vf.test(e)?su[e]=!0:(uu[e]=!0,!1)}function gf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function wf(e,t,n,r){if(t===null||typeof t>"u"||gf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var le={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){le[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];le[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){le[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){le[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){le[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){le[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){le[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){le[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){le[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var ai=/[\-:]([a-z])/g;function ci(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ai,ci);le[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ai,ci);le[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ai,ci);le[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){le[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});le.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){le[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function fi(e,t,n,r){var l=le.hasOwnProperty(t)?le[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` -`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{Vl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Un(e):""}function kf(e){switch(e.tag){case 5:return Un(e.type);case 16:return Un("Lazy");case 13:return Un("Suspense");case 19:return Un("SuspenseList");case 0:case 2:case 15:return e=Al(e.type,!1),e;case 11:return e=Al(e.type.render,!1),e;case 1:return e=Al(e.type,!0),e;default:return""}}function yo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case en:return"Fragment";case bt:return"Portal";case ho:return"Profiler";case di:return"StrictMode";case mo:return"Suspense";case vo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ls:return(e.displayName||"Context")+".Consumer";case Ts:return(e._context.displayName||"Context")+".Provider";case pi:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case hi:return t=e.displayName||null,t!==null?t:yo(e.type)||"Memo";case dt:t=e._payload,e=e._init;try{return yo(e(t))}catch{}}return null}function Sf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return yo(t);case 8:return t===di?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Nt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Os(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function xf(e){var t=Os(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function xr(e){e._valueTracker||(e._valueTracker=xf(e))}function Ms(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Os(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Gr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function go(e,t){var n=t.checked;return K({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function cu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Nt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Ds(e,t){t=t.checked,t!=null&&fi(e,"checked",t,!1)}function wo(e,t){Ds(e,t);var n=Nt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ko(e,t.type,n):t.hasOwnProperty("defaultValue")&&ko(e,t.type,Nt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function fu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ko(e,t,n){(t!=="number"||Gr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Bn=Array.isArray;function dn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=_r.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function qn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var An={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},_f=["Webkit","ms","Moz","O"];Object.keys(An).forEach(function(e){_f.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),An[t]=An[e]})});function Bs(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||An.hasOwnProperty(e)&&An[e]?(""+t).trim():t+"px"}function $s(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Bs(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Ef=K({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function _o(e,t){if(t){if(Ef[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(w(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(w(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(w(61))}if(t.style!=null&&typeof t.style!="object")throw Error(w(62))}}function Eo(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Co=null;function mi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var No=null,pn=null,hn=null;function hu(e){if(e=yr(e)){if(typeof No!="function")throw Error(w(280));var t=e.stateNode;t&&(t=El(t),No(e.stateNode,e.type,t))}}function Vs(e){pn?hn?hn.push(e):hn=[e]:pn=e}function As(){if(pn){var e=pn,t=hn;if(hn=pn=null,hu(e),t)for(e=0;e>>=0,e===0?32:31-(Df(e)/If|0)|0}var Er=64,Cr=4194304;function $n(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function br(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=$n(u):(o&=i,o!==0&&(r=$n(o)))}else i=n&~l,i!==0?r=$n(i):o!==0&&(r=$n(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function mr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Be(t),e[t]=n}function $f(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Wn),_u=" ",Eu=!1;function sa(e,t){switch(e){case"keyup":return hd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function aa(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var tn=!1;function vd(e,t){switch(e){case"compositionend":return aa(t);case"keypress":return t.which!==32?null:(Eu=!0,_u);case"textInput":return e=t.data,e===_u&&Eu?null:e;default:return null}}function yd(e,t){if(tn)return e==="compositionend"||!_i&&sa(e,t)?(e=ia(),$r=ki=vt=null,tn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Pu(n)}}function pa(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?pa(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ha(){for(var e=window,t=Gr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Gr(e.document)}return t}function Ei(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Nd(e){var t=ha(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&pa(n.ownerDocument.documentElement,n)){if(r!==null&&Ei(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=zu(n,o);var i=zu(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,nn=null,Ro=null,Kn=null,Oo=!1;function Tu(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Oo||nn==null||nn!==Gr(r)||(r=nn,"selectionStart"in r&&Ei(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Kn&&lr(Kn,r)||(Kn=r,r=nl(Ro,"onSelect"),0on||(e.current=Bo[on],Bo[on]=null,on--)}function U(e,t){on++,Bo[on]=e.current,e.current=t}var jt={},ae=zt(jt),ge=zt(!1),$t=jt;function wn(e,t){var n=e.type.contextTypes;if(!n)return jt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function ll(){$(ge),$(ae)}function Fu(e,t,n){if(ae.current!==jt)throw Error(w(168));U(ae,t),U(ge,n)}function _a(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(w(108,Sf(e)||"Unknown",l));return K({},n,r)}function ol(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||jt,$t=ae.current,U(ae,e),U(ge,ge.current),!0}function Uu(e,t,n){var r=e.stateNode;if(!r)throw Error(w(169));n?(e=_a(e,t,$t),r.__reactInternalMemoizedMergedChildContext=e,$(ge),$(ae),U(ae,e)):$(ge),U(ge,n)}var be=null,Cl=!1,no=!1;function Ea(e){be===null?be=[e]:be.push(e)}function Ud(e){Cl=!0,Ea(e)}function Tt(){if(!no&&be!==null){no=!0;var e=0,t=I;try{var n=be;for(I=1;e>=i,l-=i,et=1<<32-Be(t)+l|n<z?(V=P,P=null):V=P.sibling;var R=h(f,P,d[z],g);if(R===null){P===null&&(P=V);break}e&&P&&R.alternate===null&&t(f,P),a=o(R,a,z),j===null?E=R:j.sibling=R,j=R,P=V}if(z===d.length)return n(f,P),H&&Ot(f,z),E;if(P===null){for(;zz?(V=P,P=null):V=P.sibling;var me=h(f,P,R.value,g);if(me===null){P===null&&(P=V);break}e&&P&&me.alternate===null&&t(f,P),a=o(me,a,z),j===null?E=me:j.sibling=me,j=me,P=V}if(R.done)return n(f,P),H&&Ot(f,z),E;if(P===null){for(;!R.done;z++,R=d.next())R=v(f,R.value,g),R!==null&&(a=o(R,a,z),j===null?E=R:j.sibling=R,j=R);return H&&Ot(f,z),E}for(P=r(f,P);!R.done;z++,R=d.next())R=k(P,f,z,R.value,g),R!==null&&(e&&R.alternate!==null&&P.delete(R.key===null?z:R.key),a=o(R,a,z),j===null?E=R:j.sibling=R,j=R);return e&&P.forEach(function(Ze){return t(f,Ze)}),H&&Ot(f,z),E}function F(f,a,d,g){if(typeof d=="object"&&d!==null&&d.type===en&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case Sr:e:{for(var E=d.key,j=a;j!==null;){if(j.key===E){if(E=d.type,E===en){if(j.tag===7){n(f,j.sibling),a=l(j,d.props.children),a.return=f,f=a;break e}}else if(j.elementType===E||typeof E=="object"&&E!==null&&E.$$typeof===dt&&Vu(E)===j.type){n(f,j.sibling),a=l(j,d.props),a.ref=Dn(f,j,d),a.return=f,f=a;break e}n(f,j);break}else t(f,j);j=j.sibling}d.type===en?(a=Bt(d.props.children,f.mode,g,d.key),a.return=f,f=a):(g=Xr(d.type,d.key,d.props,null,f.mode,g),g.ref=Dn(f,a,d),g.return=f,f=g)}return i(f);case bt:e:{for(j=d.key;a!==null;){if(a.key===j)if(a.tag===4&&a.stateNode.containerInfo===d.containerInfo&&a.stateNode.implementation===d.implementation){n(f,a.sibling),a=l(a,d.children||[]),a.return=f,f=a;break e}else{n(f,a);break}else t(f,a);a=a.sibling}a=co(d,f.mode,g),a.return=f,f=a}return i(f);case dt:return j=d._init,F(f,a,j(d._payload),g)}if(Bn(d))return x(f,a,d,g);if(Tn(d))return _(f,a,d,g);Rr(f,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,a!==null&&a.tag===6?(n(f,a.sibling),a=l(a,d),a.return=f,f=a):(n(f,a),a=ao(d,f.mode,g),a.return=f,f=a),i(f)):n(f,a)}return F}var Sn=Pa(!0),za=Pa(!1),sl=zt(null),al=null,an=null,Pi=null;function zi(){Pi=an=al=null}function Ti(e){var t=sl.current;$(sl),e._currentValue=t}function Ao(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function vn(e,t){al=e,Pi=an=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ye=!0),e.firstContext=null)}function Re(e){var t=e._currentValue;if(Pi!==e)if(e={context:e,memoizedValue:t,next:null},an===null){if(al===null)throw Error(w(308));an=e,al.dependencies={lanes:0,firstContext:e}}else an=an.next=e;return t}var It=null;function Li(e){It===null?It=[e]:It.push(e)}function Ta(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Li(t)):(n.next=l.next,l.next=n),t.interleaved=n,ot(e,r)}function ot(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var pt=!1;function Ri(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function La(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function nt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function xt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,D&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,ot(e,n)}return l=r.interleaved,l===null?(t.next=t,Li(r)):(t.next=l.next,l.next=t),r.interleaved=t,ot(e,n)}function Ar(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yi(e,n)}}function Au(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function cl(e,t,n,r){var l=e.updateQueue;pt=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,c=s.next;s.next=null,i===null?o=c:i.next=c,i=s;var y=e.alternate;y!==null&&(y=y.updateQueue,u=y.lastBaseUpdate,u!==i&&(u===null?y.firstBaseUpdate=c:u.next=c,y.lastBaseUpdate=s))}if(o!==null){var v=l.baseState;i=0,y=c=s=null,u=o;do{var h=u.lane,k=u.eventTime;if((r&h)===h){y!==null&&(y=y.next={eventTime:k,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var x=e,_=u;switch(h=t,k=n,_.tag){case 1:if(x=_.payload,typeof x=="function"){v=x.call(k,v,h);break e}v=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=_.payload,h=typeof x=="function"?x.call(k,v,h):x,h==null)break e;v=K({},v,h);break e;case 2:pt=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[u]:h.push(u))}else k={eventTime:k,lane:h,tag:u.tag,payload:u.payload,callback:u.callback,next:null},y===null?(c=y=k,s=v):y=y.next=k,i|=h;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;h=u,u=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(y===null&&(s=v),l.baseState=s,l.firstBaseUpdate=c,l.lastBaseUpdate=y,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Ht|=i,e.lanes=i,e.memoizedState=v}}function Hu(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=lo.transition;lo.transition={};try{e(!1),t()}finally{I=n,lo.transition=r}}function Xa(){return Oe().memoizedState}function Ad(e,t,n){var r=Et(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ga(e))Za(t,n);else if(n=Ta(e,t,n,r),n!==null){var l=de();$e(n,e,r,l),Ja(n,t,r)}}function Hd(e,t,n){var r=Et(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ga(e))Za(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,Ve(u,i)){var s=t.interleaved;s===null?(l.next=l,Li(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Ta(e,t,l,r),n!==null&&(l=de(),$e(n,e,r,l),Ja(n,t,r))}}function Ga(e){var t=e.alternate;return e===Q||t!==null&&t===Q}function Za(e,t){Yn=dl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ja(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yi(e,n)}}var pl={readContext:Re,useCallback:ie,useContext:ie,useEffect:ie,useImperativeHandle:ie,useInsertionEffect:ie,useLayoutEffect:ie,useMemo:ie,useReducer:ie,useRef:ie,useState:ie,useDebugValue:ie,useDeferredValue:ie,useTransition:ie,useMutableSource:ie,useSyncExternalStore:ie,useId:ie,unstable_isNewReconciler:!1},Wd={readContext:Re,useCallback:function(e,t){return Ke().memoizedState=[e,t===void 0?null:t],e},useContext:Re,useEffect:Qu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Wr(4194308,4,Ha.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Wr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Wr(4,2,e,t)},useMemo:function(e,t){var n=Ke();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ke();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Ad.bind(null,Q,e),[r.memoizedState,e]},useRef:function(e){var t=Ke();return e={current:e},t.memoizedState=e},useState:Wu,useDebugValue:$i,useDeferredValue:function(e){return Ke().memoizedState=e},useTransition:function(){var e=Wu(!1),t=e[0];return e=Vd.bind(null,e[1]),Ke().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Q,l=Ke();if(H){if(n===void 0)throw Error(w(407));n=n()}else{if(n=t(),te===null)throw Error(w(349));At&30||Da(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Qu(Fa.bind(null,r,o,e),[e]),r.flags|=2048,dr(9,Ia.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ke(),t=te.identifierPrefix;if(H){var n=tt,r=et;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=cr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Ye]=t,e[ur]=r,uc(e,t,!1,!1),t.stateNode=e;e:{switch(i=Eo(n,r),n){case"dialog":B("cancel",e),B("close",e),l=r;break;case"iframe":case"object":case"embed":B("load",e),l=r;break;case"video":case"audio":for(l=0;lEn&&(t.flags|=128,r=!0,In(o,!1),t.lanes=4194304)}else{if(!r)if(e=fl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),In(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!H)return ue(t),null}else 2*G()-o.renderingStartTime>En&&n!==1073741824&&(t.flags|=128,r=!0,In(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=G(),t.sibling=null,n=W.current,U(W,r?n&1|2:n&1),t):(ue(t),null);case 22:case 23:return Ki(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Se&1073741824&&(ue(t),t.subtreeFlags&6&&(t.flags|=8192)):ue(t),null;case 24:return null;case 25:return null}throw Error(w(156,t.tag))}function qd(e,t){switch(Ni(t),t.tag){case 1:return we(t.type)&&ll(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return xn(),$(ge),$(ae),Di(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Mi(t),null;case 13:if($(W),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(w(340));kn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return $(W),null;case 4:return xn(),null;case 10:return Ti(t.type._context),null;case 22:case 23:return Ki(),null;case 24:return null;default:return null}}var Mr=!1,se=!1,bd=typeof WeakSet=="function"?WeakSet:Set,N=null;function cn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Y(e,t,r)}else n.current=null}function Jo(e,t,n){try{n()}catch(r){Y(e,t,r)}}var ns=!1;function ep(e,t){if(Mo=el,e=ha(),Ei(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,c=0,y=0,v=e,h=null;t:for(;;){for(var k;v!==n||l!==0&&v.nodeType!==3||(u=i+l),v!==o||r!==0&&v.nodeType!==3||(s=i+r),v.nodeType===3&&(i+=v.nodeValue.length),(k=v.firstChild)!==null;)h=v,v=k;for(;;){if(v===e)break t;if(h===n&&++c===l&&(u=i),h===o&&++y===r&&(s=i),(k=v.nextSibling)!==null)break;v=h,h=v.parentNode}v=k}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Do={focusedElem:e,selectionRange:n},el=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var _=x.memoizedProps,F=x.memoizedState,f=t.stateNode,a=f.getSnapshotBeforeUpdate(t.elementType===t.type?_:Ie(t.type,_),F);f.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(w(163))}}catch(g){Y(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return x=ns,ns=!1,x}function Xn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Jo(t,n,o)}l=l.next}while(l!==r)}}function Pl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function qo(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function cc(e){var t=e.alternate;t!==null&&(e.alternate=null,cc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ye],delete t[ur],delete t[Uo],delete t[Id],delete t[Fd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function fc(e){return e.tag===5||e.tag===3||e.tag===4}function rs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||fc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function bo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=rl));else if(r!==4&&(e=e.child,e!==null))for(bo(e,t,n),e=e.sibling;e!==null;)bo(e,t,n),e=e.sibling}function ei(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(ei(e,t,n),e=e.sibling;e!==null;)ei(e,t,n),e=e.sibling}var ne=null,Fe=!1;function ft(e,t,n){for(n=n.child;n!==null;)dc(e,t,n),n=n.sibling}function dc(e,t,n){if(Xe&&typeof Xe.onCommitFiberUnmount=="function")try{Xe.onCommitFiberUnmount(kl,n)}catch{}switch(n.tag){case 5:se||cn(n,t);case 6:var r=ne,l=Fe;ne=null,ft(e,t,n),ne=r,Fe=l,ne!==null&&(Fe?(e=ne,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ne.removeChild(n.stateNode));break;case 18:ne!==null&&(Fe?(e=ne,n=n.stateNode,e.nodeType===8?to(e.parentNode,n):e.nodeType===1&&to(e,n),nr(e)):to(ne,n.stateNode));break;case 4:r=ne,l=Fe,ne=n.stateNode.containerInfo,Fe=!0,ft(e,t,n),ne=r,Fe=l;break;case 0:case 11:case 14:case 15:if(!se&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Jo(n,t,i),l=l.next}while(l!==r)}ft(e,t,n);break;case 1:if(!se&&(cn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Y(n,t,u)}ft(e,t,n);break;case 21:ft(e,t,n);break;case 22:n.mode&1?(se=(r=se)||n.memoizedState!==null,ft(e,t,n),se=r):ft(e,t,n);break;default:ft(e,t,n)}}function ls(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new bd),t.forEach(function(r){var l=ap.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Me(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=G()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*np(r/1960))-r,10e?16:e,yt===null)var r=!1;else{if(e=yt,yt=null,vl=0,D&6)throw Error(w(331));var l=D;for(D|=4,N=e.current;N!==null;){var o=N,i=o.child;if(N.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sG()-Wi?Ut(e,0):Hi|=n),ke(e,t)}function kc(e,t){t===0&&(e.mode&1?(t=Cr,Cr<<=1,!(Cr&130023424)&&(Cr=4194304)):t=1);var n=de();e=ot(e,t),e!==null&&(mr(e,t,n),ke(e,n))}function sp(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),kc(e,n)}function ap(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(w(314))}r!==null&&r.delete(t),kc(e,n)}var Sc;Sc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Zd(e,t,n);ye=!!(e.flags&131072)}else ye=!1,H&&t.flags&1048576&&Ca(t,ul,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Qr(e,t),e=t.pendingProps;var l=wn(t,ae.current);vn(t,n),l=Fi(null,t,r,e,l,n);var o=Ui();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,ol(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Ri(t),l.updater=jl,t.stateNode=l,l._reactInternals=t,Wo(t,r,e,n),t=Yo(null,t,r,!0,o,n)):(t.tag=0,H&&o&&Ci(t),fe(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Qr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=fp(r),e=Ie(r,e),l){case 0:t=Ko(null,t,r,e,n);break e;case 1:t=bu(null,t,r,e,n);break e;case 11:t=Ju(null,t,r,e,n);break e;case 14:t=qu(null,t,r,Ie(r.type,e),n);break e}throw Error(w(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ko(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),bu(e,t,r,l,n);case 3:e:{if(lc(t),e===null)throw Error(w(387));r=t.pendingProps,o=t.memoizedState,l=o.element,La(e,t),cl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=_n(Error(w(423)),t),t=es(e,t,r,n,l);break e}else if(r!==l){l=_n(Error(w(424)),t),t=es(e,t,r,n,l);break e}else for(xe=St(t.stateNode.containerInfo.firstChild),_e=t,H=!0,Ue=null,n=za(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(kn(),r===l){t=it(e,t,n);break e}fe(e,t,r,n)}t=t.child}return t;case 5:return Ra(t),e===null&&Vo(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,Io(r,l)?i=null:o!==null&&Io(r,o)&&(t.flags|=32),rc(e,t),fe(e,t,i,n),t.child;case 6:return e===null&&Vo(t),null;case 13:return oc(e,t,n);case 4:return Oi(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Sn(t,null,r,n):fe(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ju(e,t,r,l,n);case 7:return fe(e,t,t.pendingProps,n),t.child;case 8:return fe(e,t,t.pendingProps.children,n),t.child;case 12:return fe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,U(sl,r._currentValue),r._currentValue=i,o!==null)if(Ve(o.value,i)){if(o.children===l.children&&!ge.current){t=it(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=nt(-1,n&-n),s.tag=2;var c=o.updateQueue;if(c!==null){c=c.shared;var y=c.pending;y===null?s.next=s:(s.next=y.next,y.next=s),c.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),Ao(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(w(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ao(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}fe(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,vn(t,n),l=Re(l),r=r(l),t.flags|=1,fe(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),qu(e,t,r,l,n);case 15:return tc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Qr(e,t),t.tag=1,we(r)?(e=!0,ol(t)):e=!1,vn(t,n),qa(t,r,l),Wo(t,r,l,n),Yo(null,t,r,!0,e,n);case 19:return ic(e,t,n);case 22:return nc(e,t,n)}throw Error(w(156,t.tag))};function xc(e,t){return Gs(e,t)}function cp(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Te(e,t,n,r){return new cp(e,t,n,r)}function Xi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function fp(e){if(typeof e=="function")return Xi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===pi)return 11;if(e===hi)return 14}return 2}function Ct(e,t){var n=e.alternate;return n===null?(n=Te(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xr(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Xi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case en:return Bt(n.children,l,o,t);case di:i=8,l|=8;break;case ho:return e=Te(12,n,t,l|2),e.elementType=ho,e.lanes=o,e;case mo:return e=Te(13,n,t,l),e.elementType=mo,e.lanes=o,e;case vo:return e=Te(19,n,t,l),e.elementType=vo,e.lanes=o,e;case Rs:return Tl(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ts:i=10;break e;case Ls:i=9;break e;case pi:i=11;break e;case hi:i=14;break e;case dt:i=16,r=null;break e}throw Error(w(130,e==null?e:typeof e,""))}return t=Te(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Bt(e,t,n,r){return e=Te(7,e,r,t),e.lanes=n,e}function Tl(e,t,n,r){return e=Te(22,e,r,t),e.elementType=Rs,e.lanes=n,e.stateNode={isHidden:!1},e}function ao(e,t,n){return e=Te(6,e,null,t),e.lanes=n,e}function co(e,t,n){return t=Te(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function dp(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Wl(0),this.expirationTimes=Wl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Wl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Gi(e,t,n,r,l,o,i,u,s){return e=new dp(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Te(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ri(o),e}function pp(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Nc)}catch(e){console.error(e)}}Nc(),Ns.exports=Ce;var gp=Ns.exports,ds=gp;fo.createRoot=ds.createRoot,fo.hydrateRoot=ds.hydrateRoot;const ps={enabled:!1,available:!1,total_objects:0,total_bytes:0,recent_objects_24h:0,recent_bytes_24h:0,buckets:[]};function wp(e){const t=e.trim().slice(0,512).toLowerCase();return t.startsWith("\s*([^<]+)\s*<\/title>/i);return((n=t==null?void 0:t[1])==null?void 0:n.trim())||""}function Sp(e){var n;const t=e.match(/Request ID:\s*([0-9a-f-]+)/i);return((n=t==null?void 0:t[1])==null?void 0:n.trim())||""}function xp(e){return new Promise(t=>{window.setTimeout(t,e)})}async function De(e,t){const n=await fetch(e,t),r=await n.text();let l={};if(r.trim()!=="")try{l=JSON.parse(r)}catch{l={error:r}}if(!n.ok){let o=typeof l=="object"&&l!==null&&"error"in l?String(l.error):`${n.status} ${n.statusText}`;if(wp(r)){const i=kp(r),u=Sp(r);o=`upstream gateway error (${n.status}${i?` ${i}`:""})`,u&&(o=`${o}; request id ${u}`)}throw new Error(o)}return l}function Jt(e){if(e==null||Number.isNaN(e))return"n/a";if(e<=0)return"0 B";const t=["B","KiB","MiB","GiB","TiB"];let n=e,r=0;for(;n>=1024&&rString(l).padStart(2,"0"),r=[t.getUTCFullYear(),n(t.getUTCMonth()+1),n(t.getUTCDate()),n(t.getUTCHours()),n(t.getUTCMinutes())].join("");return`restore-${e}-${r}`.toLowerCase().replace(/[^a-z0-9-]/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").slice(0,63).replace(/-+$/g,"")}function hs(){const e=new Date,t=r=>String(r).padStart(2,"0");return`restore-${[e.getUTCFullYear(),t(e.getUTCMonth()+1),t(e.getUTCDate()),t(e.getUTCHours()),t(e.getUTCMinutes())].join("")}-`}function Np(){const[e,t]=M.useState(null),[n,r]=M.useState(""),[l,o]=M.useState(null),[i,u]=M.useState(""),[s,c]=M.useState([]),[y,v]=M.useState(""),[h,k]=M.useState(ps),[x,_]=M.useState(""),[F,f]=M.useState(!1),[a,d]=M.useState({kind:"none"}),[g,E]=M.useState(""),[j,P]=M.useState(""),[z,V]=M.useState(""),[R,me]=M.useState(""),[Ze,st]=M.useState(hs()),[Pn,wr]=M.useState(""),[Je,at]=M.useState(""),[C,T]=M.useState(""),[L,A]=M.useState(24),[X,Lt]=M.useState(!0),[Ae,Yt]=M.useState(!0),[He,Rt]=M.useState(0),[Dl,jc]=M.useState(!0),[Il,Pc]=M.useState(0),[zc,bi]=M.useState("No action yet."),[We,oe]=M.useState(!1),zn=M.useMemo(()=>{if(!l)return 0;let m=0;for(const S of l.namespaces)for(const Z of S.pvcs)m+=Z.active_backups||0;return m},[l]),Fl=M.useMemo(()=>l?l.namespaces.map(m=>m.name):[],[l]),ce=m=>{if(typeof m=="string"){bi(m);return}bi(JSON.stringify(m,null,2))},Tc=async()=>{try{const m=await De("/v1/whoami");t(m),r("")}catch(m){t(null),r(m instanceof Error?m.message:"failed to load auth")}},ct=async()=>{const m=async()=>De("/v1/inventory");try{const S=await m();o(S),u(""),!Je&&S.namespaces.length>0&&at(S.namespaces[0].name)}catch(S){let Z=S instanceof Error?S.message:"failed to load inventory";if(Z.includes("upstream gateway error"))try{await xp(1e3);const je=await m();o(je),u(""),!Je&&je.namespaces.length>0&&at(je.namespaces[0].name);return}catch(je){Z=je instanceof Error?je.message:Z}u(Z)}},Ul=async()=>{try{const m=await De("/v1/policies");c(m.policies||[]),v("")}catch(m){c([]),v(m instanceof Error?m.message:"failed to load policies")}},Xt=async(m=!1)=>{try{const S=await De(m?"/v1/b2?refresh=1":"/v1/b2");k(S),_("")}catch(S){k(ps),_(S instanceof Error?S.message:"failed to load B2 usage")}},Lc=async()=>{f(!0);try{await Xt(!0)}finally{f(!1)}},eu=async()=>{oe(!0);try{await Promise.all([Tc(),ct(),Ul(),Xt()])}finally{oe(!1)}};M.useEffect(()=>{eu()},[]),M.useEffect(()=>{if(zn<=0)return;const m=window.setInterval(()=>{ct()},8e3);return()=>{window.clearInterval(m)}},[zn]);const Rc=async(m,S)=>{oe(!0);try{const Z=await De("/v1/backup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:m,pvc:S,dry_run:!1,dedupe:Dl,keep_last:Il})});ce(Z),await Promise.all([ct(),Xt()])}catch(Z){ce({error:Z instanceof Error?Z.message:"backup request failed",namespace:m,pvc:S})}finally{oe(!1)}},Oc=async m=>{oe(!0);try{const S=await De("/v1/backup/namespace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:m,dry_run:!1,dedupe:Dl,keep_last:Il})});ce(S),await Promise.all([ct(),Xt()])}catch(S){ce({error:S instanceof Error?S.message:"namespace backup failed",namespace:m})}finally{oe(!1)}},Mc=async(m,S)=>{oe(!0);try{const Z=await De(`/v1/backups?namespace=${encodeURIComponent(m)}&pvc=${encodeURIComponent(S)}`),je=Z.backups.filter(Gt=>Gt.state==="Completed"&&Gt.url);d({kind:"pvc",namespace:m,pvc:S,volume:Z.volume,backups:je}),E(m),P(Cp(S)),V(je.length>0?String(je[0].url):""),ce(Z)}catch(Z){ce({error:Z instanceof Error?Z.message:"failed to load backups",namespace:m,pvc:S})}finally{oe(!1)}},Dc=m=>{d({kind:"namespace",namespace:m}),me(m),st(hs()),wr(""),ce(`Namespace restore planner loaded for ${m}.`)},tu=async m=>{if(a.kind==="pvc"){oe(!0);try{const S=await De("/v1/restores",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:a.namespace,pvc:a.pvc,backup_url:z,target_namespace:g,target_pvc:j,dry_run:m})});ce(S),await Promise.all([ct(),Xt()])}catch(S){ce({error:S instanceof Error?S.message:"restore failed",namespace:a.namespace,pvc:a.pvc,target_namespace:g,target_pvc:j,dry_run:m})}finally{oe(!1)}}},nu=async m=>{if(a.kind==="namespace"){oe(!0);try{const S=await De("/v1/restores/namespace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:a.namespace,target_namespace:R,target_prefix:Ze,snapshot:Pn,dry_run:m})});ce(S),await Promise.all([ct(),Xt()])}catch(S){ce({error:S instanceof Error?S.message:"namespace restore failed",namespace:a.namespace,target_namespace:R,target_prefix:Ze,dry_run:m})}finally{oe(!1)}}},Ic=async()=>{oe(!0);try{const m=await De("/v1/policies",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({namespace:Je,pvc:C,interval_hours:L,enabled:X,dedupe:Ae,keep_last:He})});ce(m),await Promise.all([Ul(),ct()])}catch(m){ce({error:m instanceof Error?m.message:"policy save failed"})}finally{oe(!1)}},Fc=async m=>{oe(!0);try{const S=await De(`/v1/policies/${encodeURIComponent(m)}`,{method:"DELETE"});ce(S),await Promise.all([Ul(),ct()])}catch(S){ce({error:S instanceof Error?S.message:"policy delete failed",policy_id:m})}finally{oe(!1)}},Uc=e?`${e.user||e.email||"authenticated"} | groups: ${(e.groups||[]).join(", ")||"none"}`:n||"anonymous",Bc=e!=null&&e.allowed_groups&&e.allowed_groups.length>0?`Access requires: ${e.allowed_groups.join(", ")}`:"Access requires: any authenticated user";return p.jsxs("div",{className:"app-shell",children:[p.jsxs("header",{className:"topbar",children:[p.jsxs("div",{children:[p.jsx("h1",{children:"Soteria Backup Console"}),p.jsx("p",{className:"subtle",children:"Dark-mode React UI for backup drills, policy control, and B2 consumption visibility."})]}),p.jsxs("div",{className:"toolbar",children:[p.jsx("span",{className:`chip ${e?"good":"warn"}`,children:Uc}),p.jsx("span",{className:"chip",children:Bc}),zn>0&&p.jsxs("span",{className:"chip warn",children:[zn," backup job",zn===1?"":"s"," active"]}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>void eu(),disabled:We,children:We?"Refreshing...":"Refresh"})]})]}),p.jsxs("main",{className:"layout",children:[p.jsxs("section",{className:"panel scroll-panel",children:[p.jsxs("div",{className:"panel-header",children:[p.jsx("h2",{children:"PVC Inventory"}),p.jsx("span",{className:"subtle",children:l!=null&&l.generated_at?`Updated ${qt(l.generated_at)}`:"No inventory yet"})]}),p.jsxs("label",{className:"checkbox-row",children:[p.jsx("input",{type:"checkbox",checked:Dl,onChange:m=>jc(m.target.checked)}),"Dedupe unchanged blocks (default)"]}),p.jsxs("label",{children:["Keep last snapshots per PVC (0 = keep all)",p.jsx("input",{type:"number",min:0,max:1e3,value:Il,onChange:m=>Pc(Math.max(0,Math.min(1e3,Number(m.target.value||0))))})]}),p.jsx("p",{className:"subtle tiny",children:"This setting applies to both `Backup now` and `Backup namespace` actions."}),i&&p.jsx("p",{className:"error",children:i}),!l&&!i&&p.jsx("p",{className:"subtle",children:"Loading inventory..."}),l==null?void 0:l.namespaces.map(m=>p.jsxs("article",{className:"namespace-block",children:[p.jsxs("div",{className:"namespace-row",children:[p.jsx("h3",{children:m.name}),p.jsxs("div",{className:"actions",children:[p.jsx("button",{type:"button",className:"secondary",onClick:()=>void Oc(m.name),disabled:We,children:"Backup namespace"}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>Dc(m.name),children:"Restore namespace"})]})]}),p.jsx("div",{className:"pvc-grid",children:m.pvcs.map(S=>{const Z=S.healthy?"good":S.health_reason==="in_progress"?"warn":"bad",je=S.healthy?"Healthy":_p(S.health_reason),Gt=Math.max(0,Math.min(100,Number(S.last_job_progress_pct||0))),ru=Ep(S.last_job_state),$c=!!S.last_job_name||(S.active_backups||0)>0,Vc=Jt(S.last_backup_size_bytes),Ac=Jt(S.total_backup_size_bytes),Hc=S.driver==="restic"&&(S.last_backup_size_bytes===void 0||S.total_backup_size_bytes===void 0);return p.jsxs("article",{className:"pvc-card",children:[p.jsxs("div",{className:"pvc-title-row",children:[p.jsxs("div",{children:[p.jsx("h4",{children:S.pvc}),p.jsxs("p",{className:"subtle tiny",children:[S.volume||"unknown volume"," | ",S.storage_class||"no class"," | ",S.capacity||"unknown size"]})]}),p.jsx("span",{className:`chip ${Z}`,children:je})]}),p.jsxs("p",{className:"subtle tiny",children:["Last backup: ",S.last_backup_at?`${qt(S.last_backup_at)} (${(S.last_backup_age_hours||0).toFixed(1)}h ago)`:"never"]}),p.jsxs("p",{className:"subtle tiny",children:["Backups: ",S.completed_backups,"/",S.backup_count," completed | Latest size: ",Vc," | Total stored: ",Ac]}),Hc&&p.jsx("p",{className:"subtle tiny",children:"Per-PVC storage is estimated from restic upload summaries persisted by Soteria. Older backups created before tracking may show n/a until a new backup runs."}),$c&&p.jsxs("div",{className:"backup-progress",children:[p.jsxs("div",{className:"progress-header",children:[p.jsxs("p",{className:"subtle tiny",children:["Job: ",S.last_job_name||"n/a",S.last_job_started_at?` | Started ${qt(S.last_job_started_at)}`:""]}),p.jsx("span",{className:`chip ${ru}`,children:S.last_job_state||"Unknown"})]}),p.jsx("div",{className:"progress-track",role:"progressbar","aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Gt,children:p.jsx("div",{className:`progress-fill ${ru} ${S.active_backups>0?"active":""}`,style:{width:`${Gt}%`}})}),p.jsxs("p",{className:"subtle tiny",children:["Progress ",Gt,"% | Active jobs: ",S.active_backups||0]})]}),S.error&&p.jsx("p",{className:"error tiny",children:S.error}),p.jsxs("div",{className:"actions",children:[p.jsx("button",{type:"button",onClick:()=>void Rc(S.namespace,S.pvc),disabled:We,children:"Backup now"}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>void Mc(S.namespace,S.pvc),children:"Restore"})]})]},`${S.namespace}/${S.pvc}`)})})]},m.name))]}),p.jsxs("section",{className:"column",children:[p.jsxs("section",{className:"panel",children:[p.jsxs("div",{className:"panel-header",children:[p.jsx("h2",{children:"B2 Consumption"}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>void Lc(),disabled:F,children:F?"Refreshing...":"Refresh B2"})]}),x&&p.jsx("p",{className:"error",children:x}),!x&&!h.enabled&&p.jsx("p",{className:"subtle",children:"B2 monitoring is disabled in Soteria config."}),!x&&h.enabled&&!h.available&&p.jsx("p",{className:"error",children:h.error||"B2 usage currently unavailable."}),h.enabled&&p.jsxs("div",{className:"stack",children:[p.jsxs("p",{className:"subtle tiny",children:["Endpoint: ",h.endpoint||"n/a"," | Region: ",h.region||"n/a"]}),p.jsxs("p",{className:"subtle tiny",children:["Last scan: ",qt(h.scanned_at)," | Duration: ",h.scan_duration_ms||0,"ms"]}),p.jsxs("div",{className:"stat-grid",children:[p.jsxs("div",{className:"stat",children:[p.jsx("span",{className:"label",children:"Stored bytes"}),p.jsx("strong",{children:Jt(h.total_bytes)})]}),p.jsxs("div",{className:"stat",children:[p.jsx("span",{className:"label",children:"Objects"}),p.jsx("strong",{children:h.total_objects})]}),p.jsxs("div",{className:"stat",children:[p.jsx("span",{className:"label",children:"Recent bytes (24h)"}),p.jsx("strong",{children:Jt(h.recent_bytes_24h)})]}),p.jsxs("div",{className:"stat",children:[p.jsx("span",{className:"label",children:"Recent objects (24h)"}),p.jsx("strong",{children:h.recent_objects_24h})]})]}),p.jsx("p",{className:"subtle tiny",children:"Recent 24h values are object-change bandwidth proxy from bucket scans. B2 egress billing totals are not exposed by S3 object listing."}),p.jsx("div",{className:"bucket-table-wrap",children:p.jsxs("table",{children:[p.jsx("thead",{children:p.jsxs("tr",{children:[p.jsx("th",{children:"Bucket"}),p.jsx("th",{children:"Objects"}),p.jsx("th",{children:"Stored"}),p.jsx("th",{children:"Recent 24h"})]})}),p.jsx("tbody",{children:(h.buckets||[]).map(m=>p.jsxs("tr",{children:[p.jsxs("td",{children:[p.jsx("div",{children:m.name}),p.jsxs("div",{className:"subtle tiny",children:["Last object: ",qt(m.last_modified_at)]})]}),p.jsx("td",{children:m.object_count}),p.jsx("td",{children:Jt(m.total_bytes)}),p.jsx("td",{children:Jt(m.recent_bytes_24h)})]},m.name))})]})})]})]}),p.jsxs("section",{className:"panel",children:[p.jsx("h2",{children:"Restore Planner"}),p.jsx("p",{className:"subtle tiny",children:"Inventory restore buttons preload this panel. This is where restore dry-runs and execution happen."}),a.kind==="none"&&p.jsx("p",{className:"subtle",children:"Choose Restore on a PVC or namespace to begin."}),a.kind==="pvc"&&p.jsxs("div",{className:"stack",children:[p.jsxs("p",{className:"subtle tiny",children:[p.jsx("strong",{children:"Source:"})," ",a.namespace,"/",a.pvc," (",a.volume,")"]}),p.jsxs("label",{children:["Backup snapshot",p.jsxs("select",{value:z,onChange:m=>V(m.target.value),children:[a.backups.length===0&&p.jsx("option",{value:"",children:"No completed backups"}),a.backups.map(m=>p.jsxs("option",{value:m.url||"",children:[m.name," | ",m.created||"unknown time"," | ",m.size||"size n/a"]},m.url||m.name))]})]}),p.jsxs("label",{children:["Target namespace",p.jsx("select",{value:g,onChange:m=>E(m.target.value),children:Fl.map(m=>p.jsx("option",{value:m,children:m},m))})]}),p.jsxs("label",{children:["Target PVC",p.jsx("input",{value:j,onChange:m=>P(m.target.value)})]}),p.jsxs("div",{className:"actions",children:[p.jsx("button",{type:"button",onClick:()=>void tu(!1),disabled:We||!z,children:"Create restore PVC"}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>void tu(!0),disabled:We||!z,children:"Dry run"})]})]}),a.kind==="namespace"&&p.jsxs("div",{className:"stack",children:[p.jsxs("p",{className:"subtle tiny",children:[p.jsx("strong",{children:"Source namespace:"})," ",a.namespace]}),p.jsxs("label",{children:["Target namespace",p.jsx("select",{value:R,onChange:m=>me(m.target.value),children:Fl.map(m=>p.jsx("option",{value:m,children:m},m))})]}),p.jsxs("label",{children:["Target PVC prefix",p.jsx("input",{value:Ze,onChange:m=>st(m.target.value)})]}),p.jsxs("label",{children:["Snapshot hint (optional)",p.jsx("input",{value:Pn,onChange:m=>wr(m.target.value),placeholder:"blank = latest completed"})]}),p.jsxs("div",{className:"actions",children:[p.jsx("button",{type:"button",onClick:()=>void nu(!1),disabled:We,children:"Create restore PVCs"}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>void nu(!0),disabled:We,children:"Dry run"})]})]})]}),p.jsxs("section",{className:"panel action-panel",children:[p.jsx("h2",{children:"Last Action"}),p.jsx("pre",{children:zc})]})]}),p.jsx("section",{className:"column",children:p.jsxs("section",{className:"panel scroll-panel",children:[p.jsx("h2",{children:"Backup Policies"}),p.jsx("p",{className:"subtle tiny",children:"Policy backups create new restic snapshots. `Keep last` controls version retention per PVC: 1 means only newest copy remains after each run. With dedupe on, unchanged blocks are reused in the shared repository. With dedupe off, Soteria isolates each PVC to its own repository path."}),p.jsxs("div",{className:"stack",children:[p.jsxs("label",{children:["Namespace",p.jsx("select",{value:Je,onChange:m=>at(m.target.value),children:Fl.map(m=>p.jsx("option",{value:m,children:m},m))})]}),p.jsxs("label",{children:["PVC (optional)",p.jsx("input",{value:C,onChange:m=>T(m.target.value),placeholder:"blank means all PVCs in namespace"})]}),p.jsxs("label",{children:["Interval hours",p.jsx("input",{type:"number",min:1,value:L,onChange:m=>A(Math.max(1,Number(m.target.value||1)))})]}),p.jsxs("label",{className:"checkbox-row",children:[p.jsx("input",{type:"checkbox",checked:X,onChange:m=>Lt(m.target.checked)}),"Enabled"]}),p.jsxs("label",{className:"checkbox-row",children:[p.jsx("input",{type:"checkbox",checked:Ae,onChange:m=>Yt(m.target.checked)}),"Dedupe unchanged blocks"]}),p.jsxs("label",{children:["Keep last snapshots per PVC (0 = keep all)",p.jsx("input",{type:"number",min:0,max:1e3,value:He,onChange:m=>Rt(Math.max(0,Math.min(1e3,Number(m.target.value||0))))})]}),p.jsx("button",{type:"button",onClick:()=>void Ic(),disabled:We||!Je,children:"Save policy"})]}),y&&p.jsx("p",{className:"error",children:y}),!y&&s.length===0&&p.jsx("p",{className:"subtle",children:"No policies yet."}),p.jsx("div",{className:"policy-list",children:s.map(m=>p.jsxs("article",{className:"policy-item",children:[p.jsxs("div",{className:"policy-head",children:[p.jsxs("strong",{children:[m.namespace,"/",m.pvc||"*"]}),p.jsx("span",{className:`chip ${m.enabled?"good":"bad"}`,children:m.enabled?"Enabled":"Disabled"})]}),p.jsxs("p",{className:"subtle tiny",children:["Every ",m.interval_hours,"h | Dedupe: ",m.dedupe===!1?"off":"on"," | Keep last: ",m.keep_last??0," | Updated ",qt(m.updated_at||m.created_at)]}),p.jsxs("div",{className:"actions",children:[p.jsx("button",{type:"button",className:"secondary",onClick:()=>{at(m.namespace),T(m.pvc||""),A(m.interval_hours),Lt(m.enabled),Yt(m.dedupe!==!1),Rt(m.keep_last??0)},children:"Load"}),p.jsx("button",{type:"button",className:"secondary",onClick:()=>void Fc(m.id),disabled:We,children:"Delete"})]})]},m.id))})]})})]})]})}fo.createRoot(document.getElementById("root")).render(p.jsx(uf.StrictMode,{children:p.jsx(Np,{})})); diff --git a/internal/server/ui-dist/index.html b/internal/server/ui-dist/index.html index c5ef92e..947241d 100644 --- a/internal/server/ui-dist/index.html +++ b/internal/server/ui-dist/index.html @@ -4,7 +4,7 @@ Soteria Backup Console - + diff --git a/internal/server/ui_renderer_test.go b/internal/server/ui_renderer_test.go index b706bbc..c9e658f 100644 --- a/internal/server/ui_renderer_test.go +++ b/internal/server/ui_renderer_test.go @@ -94,7 +94,7 @@ func TestUIRendererServeAssetRejectsInvalidPathsAndServesRealAssets(t *testing.T {name: "parent traversal", method: http.MethodGet, path: "/../secret.txt", ok: false}, {name: "trailing slash", method: http.MethodGet, path: "/assets/", ok: false, use: trailingSlashRenderer}, {name: "missing file", method: http.MethodGet, path: "/assets/does-not-exist.js", ok: false, use: missingPathRenderer}, - {name: "real asset", method: http.MethodGet, path: "/assets/index-C8vHBL9g.js", ok: true}, + {name: "real asset", method: http.MethodGet, path: "/assets/index-B48ZTq0Y.js", ok: true}, {name: "head asset", method: http.MethodHead, path: "/assets/index-B24a4-XK.css", ok: true}, } diff --git a/web/src/components/PVCInventoryPanel.tsx b/web/src/components/PVCInventoryPanel.tsx index ce9335e..235bac5 100644 --- a/web/src/components/PVCInventoryPanel.tsx +++ b/web/src/components/PVCInventoryPanel.tsx @@ -68,8 +68,9 @@ export function PVCInventoryPanel({
{namespace.pvcs.map((pvc) => { - const healthClass = pvc.healthy ? 'good' : (pvc.health_reason === 'in_progress' ? 'warn' : 'bad'); - const healthLabel = pvc.healthy ? 'Healthy' : formatLabel(pvc.health_reason); + const isExcluded = pvc.health_reason === 'excluded'; + const healthClass = isExcluded ? 'warn' : (pvc.healthy ? 'good' : (pvc.health_reason === 'in_progress' ? 'warn' : 'bad')); + const healthLabel = isExcluded ? 'Excluded' : (pvc.healthy ? 'Healthy' : formatLabel(pvc.health_reason)); const progressPct = Math.max(0, Math.min(100, Number(pvc.last_job_progress_pct || 0))); const progressClass = progressChipClass(pvc.last_job_state); const showProgress = Boolean(pvc.last_job_name) || (pvc.active_backups || 0) > 0; @@ -118,7 +119,7 @@ export function PVCInventoryPanel({ )} {pvc.error &&

{pvc.error}

}
-