A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
72
fork

Configure Feed

Select the types of activity you want to include in your feed.

improvements to how scanning works, and helmchart ui

+1609 -162
+176
cmd/hold/scan_backfill.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 8 + "atcr.io/pkg/atproto" 9 + "atcr.io/pkg/hold" 10 + 11 + "github.com/spf13/cobra" 12 + ) 13 + 14 + // Media-type fragments that identify artifact types the scanner intentionally 15 + // skips. Keep this list in sync with scanner/internal/scan/worker.go's 16 + // unscannableConfigTypes — that map keys on config media types; here we look 17 + // at *layer* media types because the backfill walks the hold's layer index 18 + // (which has manifest AT-URIs we can join against scan records). 19 + // 20 + // Detection by layer media type is reliable: helm charts always have a single 21 + // layer with media type application/vnd.cncf.helm.chart.content.v1.tar+gzip; 22 + // in-toto / DSSE attestations use distinct layer types too. 23 + var unscannableLayerMediaSubstrings = []string{ 24 + "helm.chart.content", 25 + "in-toto", 26 + "dsse.envelope", 27 + } 28 + 29 + var scanBackfillConfigFile string 30 + 31 + var scanBackfillCmd = &cobra.Command{ 32 + Use: "scan-backfill", 33 + Short: "Rewrite legacy scan records to use the status field", 34 + Long: `Walks every io.atcr.hold.scan record on this hold and assigns a status 35 + ("skipped" or "failed") to records that pre-date the status field. 36 + 37 + A legacy record is one with an empty status, no SBOM blob, and zero vulnerability 38 + counts. The tool inspects each record's manifest's layers to decide: 39 + 40 + - layer media type matches helm/in-toto/DSSE → status="skipped" 41 + - everything else → status="failed" 42 + 43 + The tool is idempotent: records that already have a status are left alone. 44 + Run once per hold after upgrading.`, 45 + Args: cobra.NoArgs, 46 + RunE: func(cmd *cobra.Command, args []string) error { 47 + cfg, err := hold.LoadConfig(scanBackfillConfigFile) 48 + if err != nil { 49 + return fmt.Errorf("load config: %w", err) 50 + } 51 + 52 + ctx := context.Background() 53 + holdPDS, cleanup, err := openHoldPDS(ctx, cfg) 54 + if err != nil { 55 + return err 56 + } 57 + defer cleanup() 58 + 59 + ri := holdPDS.RecordsIndex() 60 + if ri == nil { 61 + return fmt.Errorf("records index not available") 62 + } 63 + 64 + const batchSize = 200 65 + var ( 66 + cursor string 67 + scanned int 68 + rewritten int 69 + markSkipped int 70 + markFailed int 71 + alreadyOK int 72 + ) 73 + 74 + for { 75 + records, nextCursor, err := ri.ListRecords(atproto.ScanCollection, batchSize, cursor, true) 76 + if err != nil { 77 + return fmt.Errorf("list scan records: %w", err) 78 + } 79 + 80 + for _, rec := range records { 81 + scanned++ 82 + manifestDigest := "sha256:" + rec.Rkey 83 + 84 + _, scanRecord, err := holdPDS.GetScanRecord(ctx, manifestDigest) 85 + if err != nil { 86 + fmt.Fprintf(cmd.ErrOrStderr(), " skip rkey=%s: get failed: %v\n", rec.Rkey, err) 87 + continue 88 + } 89 + 90 + // Already classified — nothing to do. 91 + if scanRecord.Status != "" { 92 + alreadyOK++ 93 + continue 94 + } 95 + 96 + // Only legacy records that signal failure (nil blob + zero 97 + // counts) are candidates. Records with real data don't need 98 + // rewriting; their absent status will be treated as "ok". 99 + if scanRecord.SbomBlob != nil || scanRecord.Total != 0 { 100 + alreadyOK++ 101 + continue 102 + } 103 + 104 + // Determine artifact type from layer media types. 105 + layers, err := holdPDS.ListLayerRecordsForManifest(ctx, scanRecord.Manifest) 106 + if err != nil { 107 + fmt.Fprintf(cmd.ErrOrStderr(), " skip rkey=%s: list layers failed: %v\n", rec.Rkey, err) 108 + continue 109 + } 110 + 111 + skipped := false 112 + for _, l := range layers { 113 + for _, frag := range unscannableLayerMediaSubstrings { 114 + if strings.Contains(l.MediaType, frag) { 115 + skipped = true 116 + break 117 + } 118 + } 119 + if skipped { 120 + break 121 + } 122 + } 123 + 124 + var rewrite *atproto.ScanRecord 125 + if skipped { 126 + rewrite = atproto.NewSkippedScanRecord( 127 + manifestDigest, 128 + scanRecord.Repository, 129 + scanRecord.UserDID, 130 + "backfilled: unscannable artifact type", 131 + scanRecord.ScannerVersion, 132 + ) 133 + markSkipped++ 134 + } else { 135 + rewrite = atproto.NewFailedScanRecord( 136 + manifestDigest, 137 + scanRecord.Repository, 138 + scanRecord.UserDID, 139 + "backfilled: legacy record (no SBOM and zero counts)", 140 + scanRecord.ScannerVersion, 141 + ) 142 + markFailed++ 143 + } 144 + // Preserve the original ScannedAt — rewriting it would either 145 + // reset the rescan timer or invalidate audit signals. 146 + if scanRecord.ScannedAt != "" { 147 + rewrite.ScannedAt = scanRecord.ScannedAt 148 + } 149 + 150 + if _, _, err := holdPDS.CreateScanRecord(ctx, rewrite); err != nil { 151 + fmt.Fprintf(cmd.ErrOrStderr(), " rewrite rkey=%s failed: %v\n", rec.Rkey, err) 152 + continue 153 + } 154 + rewritten++ 155 + } 156 + 157 + if nextCursor == "" || len(records) == 0 { 158 + break 159 + } 160 + cursor = nextCursor 161 + } 162 + 163 + fmt.Fprintf(cmd.OutOrStdout(), "Backfill complete:\n") 164 + fmt.Fprintf(cmd.OutOrStdout(), " scanned: %d\n", scanned) 165 + fmt.Fprintf(cmd.OutOrStdout(), " already-tagged: %d\n", alreadyOK) 166 + fmt.Fprintf(cmd.OutOrStdout(), " → skipped: %d\n", markSkipped) 167 + fmt.Fprintf(cmd.OutOrStdout(), " → failed: %d\n", markFailed) 168 + fmt.Fprintf(cmd.OutOrStdout(), " rewritten: %d\n", rewritten) 169 + return nil 170 + }, 171 + } 172 + 173 + func init() { 174 + scanBackfillCmd.Flags().StringVarP(&scanBackfillConfigFile, "config", "c", "", "path to YAML configuration file") 175 + rootCmd.AddCommand(scanBackfillCmd) 176 + }
+2
deploy/upcloud/configs/appview.yaml.tmpl
··· 45 45 legal: 46 46 company_name: Seamark 47 47 jurisdiction: State of Texas, United States 48 + ai: 49 + api_key: ""
+46
docs/SBOM_SCANNING.md
··· 202 202 - Record key: SBOM manifest digest 203 203 - Contains reference to subject image 204 204 205 + ## Scan Record Status 206 + 207 + Every scan attempt produces an `io.atcr.hold.scan` record. The `status` field 208 + tells the appview how to render the result: 209 + 210 + | Status | Meaning | Stale-loop behavior | 211 + |-------------|--------------------------------------------------------------------------------------------------|--------------------------------------------------| 212 + | `ok` (or empty) | Scanner produced an SBOM. Vulnerability counts populated; SBOM blob populated. | Re-scanned on the rescan interval (default 7d). | 213 + | `failed` | Scanner ran but errored (network, OOM, parse failure). No SBOM, no counts. | Re-scanned on the rescan interval — failures may be transient. | 214 + | `skipped` | Scanner intentionally bypassed the artifact (helm chart, in-toto attestation, DSSE envelope). The `reason` field explains why. | **Never re-queued.** A skipped record won't change without a code change in the scanner. | 215 + 216 + Records written before the `status` field existed have an empty status. The 217 + appview treats empty + nil-blob + zero-count as failed (legacy fallback). 218 + 219 + ### Unscannable artifact types 220 + 221 + The scanner skips artifacts whose config media type appears in 222 + `unscannableConfigTypes` (`scanner/internal/scan/worker.go`). Currently: 223 + 224 + - `application/vnd.cncf.helm.config.v1+json` — Helm charts. Rendered with a 225 + helm-aware digest page (`pkg/appview/handlers/digest.go`) that shows 226 + Chart.yaml metadata instead of layers / vulns / SBOM. 227 + - `application/vnd.in-toto+json` — in-toto attestations. 228 + - `application/vnd.dsse.envelope.v1+json` — DSSE envelopes (SLSA provenance). 229 + 230 + For these types the appview's vuln/SBOM tabs render 231 + *"Vulnerability scanning isn't applied to this artifact type."* — no retry hint. 232 + 233 + To add a new unscannable type: append the media type to 234 + `unscannableConfigTypes`. Existing records won't auto-rewrite — run 235 + `atcr-hold scan-backfill` once to convert any pre-existing failure records 236 + into skipped records (see below). 237 + 238 + ### Backfill tool 239 + 240 + `atcr-hold scan-backfill --config <path>` walks every scan record on the 241 + hold and rewrites legacy ones (empty status + nil blob + zero counts) using 242 + the manifest's layer media types as a signal: 243 + 244 + - Layer media type contains `helm.chart.content`, `in-toto`, or 245 + `dsse.envelope` → `status="skipped"`. 246 + - Otherwise → `status="failed"`. 247 + 248 + The tool is idempotent and preserves the original `scannedAt`, so it can be 249 + re-run safely. Run once per hold after upgrading. 250 + 205 251 ## Accessing SBOMs 206 252 207 253 Multiple methods for discovering and retrieving SBOM data.
+11
lexicons/io/atcr/hold/scan.json
··· 69 69 "type": "string", 70 70 "format": "datetime", 71 71 "description": "RFC3339 timestamp of when the scan completed" 72 + }, 73 + "status": { 74 + "type": "string", 75 + "knownValues": ["ok", "failed", "skipped"], 76 + "description": "Outcome of the scan attempt. 'ok' (or omitted, for back-compat) means the scanner produced an SBOM. 'failed' means the scanner ran but errored. 'skipped' means the scanner intentionally bypassed this artifact type (e.g. helm charts).", 77 + "maxLength": 32 78 + }, 79 + "reason": { 80 + "type": "string", 81 + "description": "Optional human-readable explanation for non-ok status (e.g. 'unscannable artifact type application/vnd.cncf.helm.config.v1+json').", 82 + "maxLength": 256 72 83 } 73 84 } 74 85 }
+13 -4
pkg/appview/db/queries.go
··· 30 30 OR hold_did IN (SELECT hold_did FROM hold_crew_members WHERE member_did = ?) 31 31 )` 32 32 33 + // Artifact type values stored in manifests.artifact_type and returned by 34 + // GetArtifactType. Container-image is the default for OCI/Docker images and 35 + // manifest lists; helm-chart is for OCI helm chart artifacts. 36 + const ( 37 + ArtifactTypeContainerImage = "container-image" 38 + ArtifactTypeHelmChart = "helm-chart" 39 + ArtifactTypeUnknown = "unknown" 40 + ) 41 + 33 42 // GetArtifactType determines the artifact type based on config media type 34 43 // Returns: "helm-chart", "container-image", or "unknown" 35 44 func GetArtifactType(configMediaType string) string { 36 45 switch { 37 46 case strings.Contains(configMediaType, "helm.config"): 38 - return "helm-chart" 47 + return ArtifactTypeHelmChart 39 48 case strings.Contains(configMediaType, "oci.image.config") || 40 49 strings.Contains(configMediaType, "docker.container.image"): 41 - return "container-image" 50 + return ArtifactTypeContainerImage 42 51 case configMediaType == "": 43 52 // Manifest lists don't have a config - treat as container-image 44 - return "container-image" 53 + return ArtifactTypeContainerImage 45 54 default: 46 - return "unknown" 55 + return ArtifactTypeUnknown 47 56 } 48 57 } 49 58
+74 -1
pkg/appview/handlers/digest.go
··· 1 1 package handlers 2 2 3 3 import ( 4 + "context" 4 5 "log/slog" 5 6 "net/http" 6 7 "strings" ··· 21 22 EmptyLayer bool // ENV, LABEL, etc. — no actual layer blob 22 23 } 23 24 25 + // HelmChartContent is the data the helm-aware digest content needs: parsed 26 + // Chart.yaml metadata + a single chart-tarball "layer" pulled from the DB. 27 + type HelmChartContent struct { 28 + Meta *holdclient.HelmChartMeta 29 + Tarball *LayerDetail 30 + MetaFetchFailed bool // hold reachable but config blob couldn't be parsed 31 + HoldUnreachable bool 32 + } 33 + 34 + // buildHelmContent fetches helm chart metadata + the single chart-tarball layer. 35 + // Returns a populated HelmChartContent even when the meta fetch fails so the 36 + // page can still render the artifact card. 37 + func buildHelmContent(ctx context.Context, holdURL string, digest string, dbLayers []db.Layer) *HelmChartContent { 38 + content := &HelmChartContent{} 39 + if holdURL == "" { 40 + content.HoldUnreachable = true 41 + } else { 42 + meta, err := holdclient.FetchHelmChartMeta(ctx, holdURL, digest) 43 + if err != nil { 44 + slog.Warn("Failed to fetch helm chart meta", "error", err, "digest", digest) 45 + content.MetaFetchFailed = true 46 + } else { 47 + content.Meta = meta 48 + } 49 + } 50 + if len(dbLayers) > 0 { 51 + // Helm charts are always single-layer (the chart tarball). If somehow 52 + // multiple are present, pick the one with helm chart content media 53 + // type, falling back to the first. 54 + chosen := 0 55 + for i, l := range dbLayers { 56 + if strings.Contains(l.MediaType, "helm.chart.content") { 57 + chosen = i 58 + break 59 + } 60 + } 61 + l := dbLayers[chosen] 62 + content.Tarball = &LayerDetail{ 63 + Index: l.LayerIndex + 1, 64 + Digest: l.Digest, 65 + Size: l.Size, 66 + MediaType: l.MediaType, 67 + } 68 + } 69 + return content 70 + } 71 + 24 72 // DigestDetailHandler renders the digest detail page with layers + vulnerabilities. 25 73 type DigestDetailHandler struct { 26 74 BaseUIHandler ··· 66 114 var layers []LayerDetail 67 115 var vulnData *vulnDetailsData 68 116 var sbomData *sbomDetailsData 117 + var helmContent *HelmChartContent 69 118 70 119 if manifest.IsManifestList { 71 120 // Manifest list: no layers, show platform picker 72 121 // Platforms are already populated by GetManifestDetail 122 + } else if manifest.ArtifactType == db.ArtifactTypeHelmChart { 123 + // Helm chart: skip OCI history / vuln / SBOM entirely. Fetch helm 124 + // chart metadata from the same config blob and the single tarball 125 + // layer from the DB. 126 + dbLayers, err := db.GetLayersForManifest(h.ReadOnlyDB, manifest.ID) 127 + if err != nil { 128 + slog.Warn("Failed to fetch layers", "error", err) 129 + } 130 + hold, holdErr := ResolveHold(r.Context(), h.ReadOnlyDB, manifest.HoldEndpoint) 131 + holdURL := "" 132 + if holdErr == nil { 133 + holdURL = hold.URL 134 + } 135 + helmContent = buildHelmContent(r.Context(), holdURL, digest, dbLayers) 136 + if holdErr != nil { 137 + helmContent.HoldUnreachable = true 138 + } 73 139 } else { 74 140 // Single manifest: fetch layers from DB 75 141 dbLayers, err := db.GetLayersForManifest(h.ReadOnlyDB, manifest.ID) ··· 124 190 WithCanonical("https://" + h.SiteURL + "/d/" + owner.Handle + "/" + repository + "/" + digest). 125 191 WithSiteName(h.ClientShortName) 126 192 193 + pageData := NewPageData(r, &h.BaseUIHandler) 127 194 data := struct { 128 195 PageData 129 196 Meta *PageMeta ··· 133 200 Layers []LayerDetail 134 201 VulnData *vulnDetailsData 135 202 SbomData *sbomDetailsData 203 + HelmContent *HelmChartContent 136 204 SelectedPlatform string 205 + RegistryURL string 206 + OciClient string 137 207 }{ 138 - PageData: NewPageData(r, &h.BaseUIHandler), 208 + PageData: pageData, 139 209 Meta: meta, 140 210 Owner: owner, 141 211 Repository: repository, ··· 143 213 Layers: layers, 144 214 VulnData: vulnData, 145 215 SbomData: sbomData, 216 + HelmContent: helmContent, 146 217 SelectedPlatform: selectedPlatform, 218 + RegistryURL: h.RegistryURL, 219 + OciClient: pageData.OciClient, 147 220 } 148 221 149 222 if err := h.Templates.ExecuteTemplate(w, "digest", data); err != nil {
+67 -5
pkg/appview/handlers/digest_content.go
··· 1 1 package handlers 2 2 3 3 import ( 4 + "fmt" 4 5 "log/slog" 5 6 "net/http" 6 7 "strings" ··· 8 9 9 10 "atcr.io/pkg/appview/db" 10 11 "atcr.io/pkg/appview/holdclient" 12 + "atcr.io/pkg/appview/middleware" 11 13 "atcr.io/pkg/atproto" 12 14 "github.com/go-chi/chi/v5" 13 15 ) ··· 49 51 hold, holdErr := ResolveHold(r.Context(), h.ReadOnlyDB, manifest.HoldEndpoint) 50 52 holdReachable := holdErr == nil 51 53 54 + // Helm charts have no scannable layers / vulns / SBOM. Render helm-aware 55 + // content for the default + "chart" sections, and a not-applicable 56 + // placeholder for the legacy layers / vulns / sbom sections (which 57 + // shouldn't be requested for helm but might be if a stale tab fires). 58 + if manifest.ArtifactType == db.ArtifactTypeHelmChart { 59 + holdURL := "" 60 + if holdReachable { 61 + holdURL = hold.URL 62 + } 63 + helm := buildHelmContent(r.Context(), holdURL, digest, dbLayers) 64 + if !holdReachable { 65 + helm.HoldUnreachable = true 66 + } 67 + helmData := struct { 68 + Manifest *db.ManifestWithMetadata 69 + HelmContent *HelmChartContent 70 + RegistryURL string 71 + OwnerHandle string 72 + RepoName string 73 + OciClient string 74 + IsLoggedIn bool 75 + }{ 76 + Manifest: manifest, 77 + HelmContent: helm, 78 + RegistryURL: h.RegistryURL, 79 + OwnerHandle: identifier, 80 + RepoName: repository, 81 + OciClient: "", // helm switcher ignores this field 82 + IsLoggedIn: middleware.GetUser(r) != nil, 83 + } 84 + w.Header().Set("Content-Type", "text/html") 85 + section := r.URL.Query().Get("section") 86 + switch section { 87 + case "chart": 88 + // Used by the repo page's chart tab — no install card here 89 + // because repo-tag-section already renders one at the top. 90 + if err := h.Templates.ExecuteTemplate(w, "helm-chart-info", helmData); err != nil { 91 + slog.Warn("Failed to render helm chart info", "error", err) 92 + RenderHTMXError(w, r, http.StatusInternalServerError, "Could not render helm chart", err) 93 + } 94 + case "layers", "vulns", "sbom": 95 + // Defensive fallback if a stale tab somehow fires. The repo page 96 + // hides these tabs for helm; this should be unreachable. 97 + fmt.Fprint(w, `<p class="text-base-content/70 py-8">Helm charts don't have layers, vulnerabilities, or SBOMs.</p>`) 98 + default: 99 + // Digest detail page (full helm view, with install card). 100 + if err := h.Templates.ExecuteTemplate(w, "helm-digest-content", helmData); err != nil { 101 + slog.Warn("Failed to render helm digest content", "error", err) 102 + RenderHTMXError(w, r, http.StatusInternalServerError, "Could not render helm chart", err) 103 + } 104 + } 105 + return 106 + } 107 + 52 108 // Parallelize the three hold fetches. They're independent and each 53 109 // takes a network round-trip; serial runs add up on slow links. 54 110 var ( ··· 93 149 } 94 150 95 151 // VulnReason / SbomReason let the template branch distinctly on why 96 - // data is missing instead of collapsing three causes into a generic 97 - // "not available" message. 98 - // ok — data is present 152 + // data is missing instead of collapsing causes into a generic message. 153 + // ok — data is present 99 154 // hold-unreachable — we couldn't reach the hold 100 - // not-scanned — hold is up but no scan record exists 101 - // fetch-failed — scan record fetch failed on the hold 155 + // not-scanned — hold is up but no scan record exists 156 + // not-applicable — scan record exists with status="skipped" (artifact 157 + // type isn't scanned, e.g. in-toto, DSSE — helm 158 + // charts go through a separate code path) 159 + // fetch-failed — scan record fetch failed on the hold 102 160 vulnReason := "ok" 103 161 if !holdReachable { 104 162 vulnReason = "hold-unreachable" 105 163 } else if vulnData == nil || vulnData.Error == "never-scanned" { 106 164 vulnReason = "not-scanned" 165 + } else if vulnData.Status == atproto.ScanStatusSkipped { 166 + vulnReason = "not-applicable" 107 167 } else if vulnData.Error != "" { 108 168 vulnReason = "fetch-failed" 109 169 } ··· 113 173 sbomReason = "hold-unreachable" 114 174 } else if sbomData == nil || sbomData.Error == "never-scanned" { 115 175 sbomReason = "not-scanned" 176 + } else if sbomData.Status == atproto.ScanStatusSkipped { 177 + sbomReason = "not-applicable" 116 178 } else if sbomData.Error != "" { 117 179 sbomReason = "fetch-failed" 118 180 }
+35 -18
pkg/appview/handlers/images.go
··· 192 192 func (h *DeleteUntaggedManifestsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 193 193 user := middleware.GetUser(r) 194 194 if user == nil { 195 - http.Error(w, "Unauthorized", http.StatusUnauthorized) 195 + render.Status(r, http.StatusUnauthorized) 196 + render.JSON(w, r, map[string]string{"error": "Unauthorized"}) 196 197 return 197 198 } 198 199 199 200 var req deleteUntaggedRequest 200 201 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 201 - http.Error(w, "Invalid request body", http.StatusBadRequest) 202 + render.Status(r, http.StatusBadRequest) 203 + render.JSON(w, r, map[string]string{"error": "Invalid request body"}) 202 204 return 203 205 } 204 206 205 207 digests, err := db.GetAllUntaggedManifestDigests(h.DB, user.DID, req.Repo) 206 208 if err != nil { 207 - http.Error(w, fmt.Sprintf("Failed to query untagged manifests: %v", err), http.StatusInternalServerError) 209 + render.Status(r, http.StatusInternalServerError) 210 + render.JSON(w, r, map[string]any{ 211 + "error": fmt.Sprintf("Failed to query untagged manifests: %v", err), 212 + "deleted": 0, 213 + "failed": 0, 214 + "total": 0, 215 + }) 208 216 return 209 217 } 210 218 211 219 if len(digests) == 0 { 212 - render.JSON(w, r, map[string]int{"deleted": 0}) 220 + render.JSON(w, r, map[string]any{"deleted": 0, "failed": 0, "total": 0}) 213 221 return 214 222 } 215 223 216 224 pdsClient := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher) 217 225 218 226 deleted := 0 227 + type failure struct { 228 + Digest string `json:"digest"` 229 + Error string `json:"error"` 230 + } 231 + var failures []failure 232 + 219 233 for _, digest := range digests { 220 234 rkey := strings.TrimPrefix(digest, "sha256:") 221 235 222 236 if err := pdsClient.DeleteRecord(r.Context(), atproto.ManifestCollection, rkey); err != nil { 223 237 if handleOAuthError(r.Context(), h.Refresher, user.DID, err) { 224 - http.Error(w, "Authentication failed, please log in again", http.StatusUnauthorized) 238 + render.Status(r, http.StatusUnauthorized) 239 + render.JSON(w, r, map[string]any{ 240 + "error": "Authentication failed, please log in again", 241 + "deleted": deleted, 242 + "failed": len(failures), 243 + "total": len(digests), 244 + }) 225 245 return 226 246 } 227 - render.Status(r, http.StatusInternalServerError) 228 - render.JSON(w, r, map[string]any{ 229 - "error": fmt.Sprintf("Failed to delete manifest %s from PDS: %v", digest, err), 230 - "deleted": deleted, 231 - }) 232 - return 247 + failures = append(failures, failure{Digest: digest, Error: fmt.Sprintf("PDS: %v", err)}) 248 + continue 233 249 } 234 250 235 251 if err := db.DeleteManifest(h.DB, user.DID, req.Repo, digest); err != nil { 236 - render.Status(r, http.StatusInternalServerError) 237 - render.JSON(w, r, map[string]any{ 238 - "error": fmt.Sprintf("Failed to delete manifest %s from cache: %v", digest, err), 239 - "deleted": deleted, 240 - }) 241 - return 252 + failures = append(failures, failure{Digest: digest, Error: fmt.Sprintf("cache: %v", err)}) 253 + continue 242 254 } 243 255 244 256 deleted++ 245 257 } 246 258 247 - render.JSON(w, r, map[string]int{"deleted": deleted}) 259 + render.JSON(w, r, map[string]any{ 260 + "deleted": deleted, 261 + "failed": len(failures), 262 + "total": len(digests), 263 + "failures": failures, 264 + }) 248 265 } 249 266 250 267 // UploadAvatarHandler handles uploading/updating a repository avatar
+13
pkg/appview/handlers/sbom_details.go
··· 39 39 Packages []sbomPackage 40 40 Total int 41 41 Error string 42 + Status string // scan record's status field (ok | failed | skipped); empty for legacy records 43 + Reason string // scan record's reason field (only meaningful when Status != ok) 42 44 ScannedAt string 43 45 Digest string // image digest (for download URLs) 44 46 HoldEndpoint string // hold DID (for download URLs) ··· 130 132 return sbomDetailsData{Error: "Failed to parse scan record"} 131 133 } 132 134 135 + if scanRecord.Status == atproto.ScanStatusSkipped { 136 + return sbomDetailsData{ 137 + Status: scanRecord.Status, 138 + Reason: scanRecord.Reason, 139 + ScannedAt: scanRecord.ScannedAt, 140 + } 141 + } 142 + 133 143 // Fetch the SBOM blob 134 144 if scanRecord.SbomBlob == nil || scanRecord.SbomBlob.Ref.String() == "" { 135 145 return sbomDetailsData{ 136 146 ScannedAt: scanRecord.ScannedAt, 147 + Status: scanRecord.Status, 148 + Reason: scanRecord.Reason, 137 149 Error: "No SBOM data available", 138 150 } 139 151 } ··· 203 215 return sbomDetailsData{ 204 216 Packages: packages, 205 217 Total: len(packages), 218 + Status: scanRecord.Status, 206 219 ScannedAt: scanRecord.ScannedAt, 207 220 Digest: digest, 208 221 HoldEndpoint: holdEndpoint,
+37 -13
pkg/appview/handlers/scan_result.go
··· 25 25 } 26 26 27 27 // vulnBadgeData is the template data for the vuln-badge partial. 28 - // The badge renders one of four states, in priority order: 28 + // The badge renders one of five states, in priority order: 29 29 // 1. Error — we couldn't reach the hold at all (network/5xx) 30 30 // 2. NotScanned — hold reachable, no scan record for this digest (404) 31 - // 3. ScanFailed — scan record exists but the scanner didn't produce an SBOM 32 - // 4. Found — scan succeeded; render tier counts (or "Clean" when zero) 31 + // 3. Skipped — scan record explicitly marks this artifact as not-scannable 32 + // 4. ScanFailed — scan record exists but the scanner errored 33 + // 5. Found — scan succeeded; render tier counts (or "Clean" when zero) 33 34 // 34 35 // These states must stay distinct so users can tell "hold is down" from 35 - // "this hasn't been scanned yet" from "scanner errored on this image". 36 + // "this hasn't been scanned yet" from "scanner errored on this image" from 37 + // "this artifact type is intentionally not scanned". 36 38 type vulnBadgeData struct { 37 39 Critical int64 38 40 High int64 ··· 43 45 Found bool // true if scan record exists and succeeded 44 46 Error bool // true if hold unreachable (network/5xx) 45 47 NotScanned bool // true if hold is up but no scan record (404) 46 - ScanFailed bool // true if scan record exists but scan failed (no SBOM) 48 + ScanFailed bool // true if scan record exists but scan failed 49 + Skipped bool // true if scan record marks the artifact as intentionally not scanned (helm, in-toto, etc.) 47 50 Digest string // for the detail modal link 48 51 HoldEndpoint string // for the detail modal link 49 52 } 50 53 54 + // classifyScanRecord maps a scan record's Status field to badge data flags. 55 + // An empty Status is treated as a legacy record from before the status field 56 + // existed: nil-blob + zero-counts = treat as failed (preserves the prior badge 57 + // for un-backfilled holds); otherwise treat as success. 58 + func classifyScanRecord(scanRecord *atproto.ScanRecord) (found, skipped, failed bool) { 59 + switch scanRecord.Status { 60 + case atproto.ScanStatusSkipped: 61 + return false, true, false 62 + case atproto.ScanStatusFailed: 63 + return false, false, true 64 + case atproto.ScanStatusOK: 65 + return true, false, false 66 + default: 67 + // Legacy record (status field didn't exist when this was written). 68 + if scanRecord.SbomBlob == nil && scanRecord.Total == 0 { 69 + return false, false, true 70 + } 71 + return true, false, false 72 + } 73 + } 74 + 51 75 func (h *ScanResultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 52 76 digest := r.URL.Query().Get("digest") 53 77 holdEndpoint := r.URL.Query().Get("holdEndpoint") ··· 122 146 return 123 147 } 124 148 125 - // A failed scan has nil blobs (no SBOM generated) and zero counts. 126 - // Successful scans always have an SBOM blob even with 0 vulnerabilities. 127 - scanFailed := scanRecord.SbomBlob == nil && scanRecord.Total == 0 128 - 149 + found, skipped, failed := classifyScanRecord(&scanRecord) 129 150 h.renderBadge(w, vulnBadgeData{ 130 151 Critical: scanRecord.Critical, 131 152 High: scanRecord.High, ··· 133 154 Low: scanRecord.Low, 134 155 Total: scanRecord.Total, 135 156 ScannedAt: scanRecord.ScannedAt, 136 - Found: true, 137 - ScanFailed: scanFailed, 157 + Found: found, 158 + Skipped: skipped, 159 + ScanFailed: failed, 138 160 Digest: digest, 139 161 HoldEndpoint: holdDID, 140 162 }) ··· 189 211 return vulnBadgeData{Error: true} 190 212 } 191 213 214 + found, skipped, failed := classifyScanRecord(&scanRecord) 192 215 return vulnBadgeData{ 193 216 Critical: scanRecord.Critical, 194 217 High: scanRecord.High, ··· 196 219 Low: scanRecord.Low, 197 220 Total: scanRecord.Total, 198 221 ScannedAt: scanRecord.ScannedAt, 199 - Found: true, 200 - ScanFailed: scanRecord.SbomBlob == nil && scanRecord.Total == 0, 222 + Found: found, 223 + Skipped: skipped, 224 + ScanFailed: failed, 201 225 Digest: fullDigest, 202 226 HoldEndpoint: holdDID, 203 227 }
+22 -4
pkg/appview/handlers/vuln_details.go
··· 57 57 Matches []vulnMatch 58 58 Summary vulnSummary 59 59 Error string // non-empty if something went wrong 60 + Status string // scan record's status field (ok | failed | skipped); empty for legacy records 61 + Reason string // scan record's reason field (only meaningful when Status != ok) 60 62 ScannedAt string 61 63 Digest string // image digest (for download URLs) 62 64 HoldEndpoint string // hold DID (for download URLs) ··· 317 319 Total: scanRecord.Total, 318 320 } 319 321 322 + // Skipped scan records have no blobs and a non-failure status. The caller 323 + // classifies these as "not-applicable" rather than "fetch-failed" so the 324 + // template can show "scanning isn't applied to this artifact" instead of a 325 + // retry hint. 326 + if scanRecord.Status == atproto.ScanStatusSkipped { 327 + return vulnDetailsData{ 328 + Summary: summary, 329 + ScannedAt: scanRecord.ScannedAt, 330 + Status: scanRecord.Status, 331 + Reason: scanRecord.Reason, 332 + } 333 + } 334 + 320 335 // Fetch the vulnerability report blob 321 336 if scanRecord.VulnReportBlob == nil || scanRecord.VulnReportBlob.Ref.String() == "" { 322 337 return vulnDetailsData{ 323 338 Summary: summary, 324 339 ScannedAt: scanRecord.ScannedAt, 340 + Status: scanRecord.Status, 341 + Reason: scanRecord.Reason, 325 342 Error: "No detailed vulnerability report available. Only summary counts were recorded.", 326 343 } 327 344 } ··· 335 352 336 353 blobReq, err := http.NewRequestWithContext(ctx, "GET", blobURL, nil) 337 354 if err != nil { 338 - return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Error: "Failed to build blob request"} 355 + return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Status: scanRecord.Status, Error: "Failed to build blob request"} 339 356 } 340 357 341 358 blobResp, err := http.DefaultClient.Do(blobReq) 342 359 if err != nil { 343 - return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Error: "Failed to fetch vulnerability report"} 360 + return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Status: scanRecord.Status, Error: "Failed to fetch vulnerability report"} 344 361 } 345 362 defer blobResp.Body.Close() 346 363 347 364 if blobResp.StatusCode != http.StatusOK { 348 - return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Error: "Vulnerability report not accessible"} 365 + return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Status: scanRecord.Status, Error: "Vulnerability report not accessible"} 349 366 } 350 367 351 368 var report grypeReport 352 369 if err := json.NewDecoder(blobResp.Body).Decode(&report); err != nil { 353 - return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Error: "Failed to parse vulnerability report"} 370 + return vulnDetailsData{Summary: summary, ScannedAt: scanRecord.ScannedAt, Status: scanRecord.Status, Error: "Failed to parse vulnerability report"} 354 371 } 355 372 356 373 matches := make([]vulnMatch, 0, len(report.Matches)) ··· 390 407 return vulnDetailsData{ 391 408 Matches: matches, 392 409 Summary: summary, 410 + Status: scanRecord.Status, 393 411 ScannedAt: scanRecord.ScannedAt, 394 412 Digest: digest, 395 413 HoldEndpoint: holdEndpoint,
+148
pkg/appview/holdclient/helm_config.go
··· 1 + package holdclient 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + "net/url" 9 + "strings" 10 + "time" 11 + 12 + "atcr.io/pkg/atproto" 13 + ) 14 + 15 + // HelmChartMeta is the parsed Chart.yaml-equivalent metadata extracted from a 16 + // helm chart's OCI config blob (media type 17 + // application/vnd.cncf.helm.config.v1+json). The helm config blob is Chart.yaml 18 + // rendered as JSON, so the field names mirror Chart.yaml. 19 + type HelmChartMeta struct { 20 + Name string 21 + Version string 22 + AppVersion string 23 + Type string // "application" | "library" (empty in older charts; treat as "application") 24 + Description string 25 + KubeVersion string 26 + Home string 27 + Icon string 28 + Sources []string 29 + Keywords []string 30 + Maintainers []HelmMaintainer 31 + Dependencies []HelmDependency 32 + Annotations map[string]string 33 + Deprecated bool 34 + } 35 + 36 + type HelmMaintainer struct { 37 + Name string 38 + Email string 39 + URL string 40 + } 41 + 42 + type HelmDependency struct { 43 + Name string 44 + Version string 45 + Repository string 46 + Alias string 47 + Condition string 48 + } 49 + 50 + // helmConfigJSON matches the on-the-wire shape of a helm OCI config blob. 51 + // Field names track Chart.yaml's JSON form (lowerCamelCase for some fields, 52 + // kebab-case for none — helm's CLI marshals Chart.yaml struct directly). 53 + type helmConfigJSON struct { 54 + Name string `json:"name"` 55 + Version string `json:"version"` 56 + AppVersion string `json:"appVersion"` 57 + Type string `json:"type"` 58 + Description string `json:"description"` 59 + KubeVersion string `json:"kubeVersion"` 60 + Home string `json:"home"` 61 + Icon string `json:"icon"` 62 + Sources []string `json:"sources"` 63 + Keywords []string `json:"keywords"` 64 + Maintainers []helmMaintainerJSON `json:"maintainers"` 65 + Dependencies []helmDependencyJSON `json:"dependencies"` 66 + Annotations map[string]string `json:"annotations"` 67 + Deprecated bool `json:"deprecated"` 68 + } 69 + 70 + type helmMaintainerJSON struct { 71 + Name string `json:"name"` 72 + Email string `json:"email"` 73 + URL string `json:"url"` 74 + } 75 + 76 + type helmDependencyJSON struct { 77 + Name string `json:"name"` 78 + Version string `json:"version"` 79 + Repository string `json:"repository"` 80 + Alias string `json:"alias"` 81 + Condition string `json:"condition"` 82 + } 83 + 84 + // FetchHelmChartMeta fetches a helm chart's config blob from the hold and 85 + // parses it as Chart.yaml metadata. Uses the same getImageConfig XRPC as 86 + // FetchImageConfig but applies a helm-specific schema to the JSON. 87 + func FetchHelmChartMeta(ctx context.Context, holdURL, manifestDigest string) (*HelmChartMeta, error) { 88 + reqURL := fmt.Sprintf("%s%s?digest=%s", 89 + strings.TrimSuffix(holdURL, "/"), 90 + atproto.HoldGetImageConfig, 91 + url.QueryEscape(manifestDigest), 92 + ) 93 + 94 + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 95 + defer cancel() 96 + 97 + req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 98 + if err != nil { 99 + return nil, fmt.Errorf("build request: %w", err) 100 + } 101 + 102 + resp, err := http.DefaultClient.Do(req) 103 + if err != nil { 104 + return nil, fmt.Errorf("fetch helm chart config: %w", err) 105 + } 106 + defer resp.Body.Close() 107 + 108 + if resp.StatusCode != http.StatusOK { 109 + return nil, fmt.Errorf("hold returned status %d for %s", resp.StatusCode, reqURL) 110 + } 111 + 112 + var record struct { 113 + ConfigJSON string `json:"configJson"` 114 + } 115 + if err := json.NewDecoder(resp.Body).Decode(&record); err != nil { 116 + return nil, fmt.Errorf("parse image config response: %w", err) 117 + } 118 + 119 + var raw helmConfigJSON 120 + if err := json.Unmarshal([]byte(record.ConfigJSON), &raw); err != nil { 121 + return nil, fmt.Errorf("parse helm chart config JSON: %w", err) 122 + } 123 + 124 + meta := &HelmChartMeta{ 125 + Name: raw.Name, 126 + Version: raw.Version, 127 + AppVersion: raw.AppVersion, 128 + Type: raw.Type, 129 + Description: raw.Description, 130 + KubeVersion: raw.KubeVersion, 131 + Home: raw.Home, 132 + Icon: raw.Icon, 133 + Sources: raw.Sources, 134 + Keywords: raw.Keywords, 135 + Annotations: raw.Annotations, 136 + Deprecated: raw.Deprecated, 137 + } 138 + if meta.Type == "" { 139 + meta.Type = "application" 140 + } 141 + for _, m := range raw.Maintainers { 142 + meta.Maintainers = append(meta.Maintainers, HelmMaintainer(m)) 143 + } 144 + for _, d := range raw.Dependencies { 145 + meta.Dependencies = append(meta.Dependencies, HelmDependency(d)) 146 + } 147 + return meta, nil 148 + }
+5 -5
pkg/appview/public/js/bundle.min.js
··· 1 - var Le=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){return getInputValues(e,t||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0,historyRestoreAsHxRequest:!0,reportValidityOfForms:!1},parseInterval:null,location,_:null,version:"2.0.8"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function parseInterval(e){if(e==null)return;let t=NaN;return e.slice(-2)=="ms"?t=parseFloat(e.slice(0,-2)):e.slice(-1)=="s"?t=parseFloat(e.slice(0,-1))*1e3:e.slice(-1)=="m"?t=parseFloat(e.slice(0,-1))*1e3*60:t=parseFloat(e),isNaN(t)?void 0:t}function getRawAttribute(e,t){return e instanceof Element&&e.getAttribute(t)}function hasAttribute(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function getAttributeValue(e,t){return getRawAttribute(e,t)||getRawAttribute(e,"data-"+t)}function parentElt(e){let t=e.parentElement;return!t&&e.parentNode instanceof ShadowRoot?e.parentNode:t}function getDocument(){return document}function getRootNode(e,t){return e.getRootNode?e.getRootNode({composed:t}):getDocument()}function getClosestMatch(e,t){for(;e&&!t(e);)e=parentElt(e);return e||null}function getAttributeValueWithDisinheritance(e,t,n){let r=getAttributeValue(t,n),o=getAttributeValue(t,"hx-disinherit");var s=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return s&&(s==="*"||s.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return"unset"}return r}function getClosestAttributeValue(e,t){let n=null;if(getClosestMatch(e,function(r){return!!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}),n!=="unset")return n}function matches(e,t){return e instanceof Element&&e.matches(t)}function getStartTag(e){let n=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return n?n[1].toLowerCase():""}function parseHTML(e){return"parseHTMLUnsafe"in Document?Document.parseHTMLUnsafe(e):new DOMParser().parseFromString(e,"text/html")}function takeChildrenFor(e,t){for(;t.childNodes.length>0;)e.append(t.childNodes[0])}function duplicateScript(e){let t=getDocument().createElement("script");return forEach(e.attributes,function(n){t.setAttribute(n.name,n.value)}),t.textContent=e.textContent,t.async=!1,htmx.config.inlineScriptNonce&&(t.nonce=htmx.config.inlineScriptNonce),t}function isJavaScriptScriptNode(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function normalizeScriptTags(e){Array.from(e.querySelectorAll("script")).forEach(t=>{if(isJavaScriptScriptNode(t)){let n=duplicateScript(t),r=t.parentNode;try{r.insertBefore(n,t)}catch(o){logError(o)}finally{t.remove()}}})}function makeFragment(e){let t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,""),n=getStartTag(t),r;if(n==="html"){r=new DocumentFragment;let s=parseHTML(e);takeChildrenFor(r,s.body),r.title=s.title}else if(n==="body"){r=new DocumentFragment;let s=parseHTML(t);takeChildrenFor(r,s.body),r.title=s.title}else{let s=parseHTML('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=s.querySelector("template").content,r.title=s.title;var o=r.querySelector("title");o&&o.parentNode===r&&(o.remove(),r.title=o.innerText)}return r&&(htmx.config.allowScriptTags?normalizeScriptTags(r):r.querySelectorAll("script").forEach(s=>s.remove())),r}function maybeCall(e){e&&e()}function isType(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function isFunction(e){return typeof e=="function"}function isRawObject(e){return isType(e,"Object")}function getInternalData(e){let t="htmx-internal-data",n=e[t];return n||(n=e[t]={}),n}function toArray(e){let t=[];if(e)for(let n=0;n<e.length;n++)t.push(e[n]);return t}function forEach(e,t){if(e)for(let n=0;n<e.length;n++)t(e[n])}function isScrolledIntoView(e){let t=e.getBoundingClientRect(),n=t.top,r=t.bottom;return n<window.innerHeight&&r>=0}function bodyContains(e){return e.getRootNode({composed:!0})===document}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function parseJSON(e){try{return JSON.parse(e)}catch(t){return logError(t),null}}function canAccessLocalStorage(){let e="htmx:sessionStorageTest";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch{return!1}}function normalizePath(e){let t=new URL(e,"http://x");return t&&(e=t.pathname+t.search),e!="/"&&(e=e.replace(/\/+$/,"")),e}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(e){return htmx.on("htmx:load",function(n){e(n.detail.elt)})}function logAll(){htmx.logger=function(e,t,n){console&&console.log(t,e,n)}}function logNone(){htmx.logger=null}function find(e,t){return typeof e!="string"?e.querySelector(t):find(getDocument(),e)}function findAll(e,t){return typeof e!="string"?e.querySelectorAll(t):findAll(getDocument(),e)}function getWindow(){return window}function removeElement(e,t){e=resolveTarget(e),t?getWindow().setTimeout(function(){removeElement(e),e=null},t):parentElt(e).removeChild(e)}function asElement(e){return e instanceof Element?e:null}function asHtmlElement(e){return e instanceof HTMLElement?e:null}function asString(e){return typeof e=="string"?e:null}function asParentNode(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function addClassToElement(e,t,n){e=asElement(resolveTarget(e)),e&&(n?getWindow().setTimeout(function(){addClassToElement(e,t),e=null},n):e.classList&&e.classList.add(t))}function removeClassFromElement(e,t,n){let r=asElement(resolveTarget(e));r&&(n?getWindow().setTimeout(function(){removeClassFromElement(r,t),r=null},n):r.classList&&(r.classList.remove(t),r.classList.length===0&&r.removeAttribute("class")))}function toggleClassOnElement(e,t){e=resolveTarget(e),e.classList.toggle(t)}function takeClassForElement(e,t){e=resolveTarget(e),forEach(e.parentElement.children,function(n){removeClassFromElement(n,t)}),addClassToElement(asElement(e),t)}function closest(e,t){return e=asElement(resolveTarget(e)),e?e.closest(t):null}function startsWith(e,t){return e.substring(0,t.length)===t}function endsWith(e,t){return e.substring(e.length-t.length)===t}function normalizeSelector(e){let t=e.trim();return startsWith(t,"<")&&endsWith(t,"/>")?t.substring(1,t.length-2):t}function querySelectorAllExt(e,t,n){if(t.indexOf("global ")===0)return querySelectorAllExt(e,t.slice(7),!0);e=resolveTarget(e);let r=[];{let i=0,a=0;for(let l=0;l<t.length;l++){let c=t[l];if(c===","&&i===0){r.push(t.substring(a,l)),a=l+1;continue}c==="<"?i++:c==="/"&&l<t.length-1&&t[l+1]===">"&&i--}a<t.length&&r.push(t.substring(a))}let o=[],s=[];for(;r.length>0;){let i=normalizeSelector(r.shift()),a;i.indexOf("closest ")===0?a=closest(asElement(e),normalizeSelector(i.slice(8))):i.indexOf("find ")===0?a=find(asParentNode(e),normalizeSelector(i.slice(5))):i==="next"||i==="nextElementSibling"?a=asElement(e).nextElementSibling:i.indexOf("next ")===0?a=scanForwardQuery(e,normalizeSelector(i.slice(5)),!!n):i==="previous"||i==="previousElementSibling"?a=asElement(e).previousElementSibling:i.indexOf("previous ")===0?a=scanBackwardsQuery(e,normalizeSelector(i.slice(9)),!!n):i==="document"?a=document:i==="window"?a=window:i==="body"?a=document.body:i==="root"?a=getRootNode(e,!!n):i==="host"?a=e.getRootNode().host:s.push(i),a&&o.push(a)}if(s.length>0){let i=s.join(","),a=asParentNode(getRootNode(e,!!n));o.push(...toArray(a.querySelectorAll(i)))}return o}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o<r.length;o++){let s=r[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return s}},scanBackwardsQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=r.length-1;o>=0;o--){let s=r[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return s}};function querySelectorExt(e,t){return typeof e!="string"?querySelectorAllExt(e,t)[0]:querySelectorAllExt(getDocument().body,e)[0]}function resolveTarget(e,t){return typeof e=="string"?find(asParentNode(t)||document,e):e}function processEventArgs(e,t,n,r){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t,options:n}:{target:resolveTarget(e),event:asString(t),listener:n,options:r}}function addEventListenerImpl(e,t,n,r){return ready(function(){let s=processEventArgs(e,t,n,r);s.target.addEventListener(s.event,s.listener,s.options)}),isFunction(t)?t:n}function removeEventListenerImpl(e,t,n){return ready(function(){let r=processEventArgs(e,t,n);r.target.removeEventListener(r.event,r.listener)}),isFunction(t)?t:n}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let n=getClosestAttributeValue(e,t);if(n){if(n==="this")return[findThisElement(e,t)];{let r=querySelectorAllExt(e,n);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(n)){let s=asElement(getClosestMatch(e,function(i){return i!==e&&hasAttribute(asElement(i),t)}));s&&r.push(...findAttributeTargets(s,t))}return r.length===0?(logError('The selector "'+n+'" on '+t+" returned no matches!"),[DUMMY_ELT]):r}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(n){return getAttributeValue(asElement(n),t)!=null}))}function getTarget(e){let t=getClosestAttributeValue(e,"hx-target");return t?t==="this"?findThisElement(e,"hx-target"):querySelectorExt(e,t):getInternalData(e).boosted?getDocument().body:e}function shouldSettleAttribute(e){return htmx.config.attributesToSettle.includes(e)}function cloneAttributes(e,t){forEach(Array.from(e.attributes),function(n){!t.hasAttribute(n.name)&&shouldSettleAttribute(n.name)&&e.removeAttribute(n.name)}),forEach(t.attributes,function(n){shouldSettleAttribute(n.name)&&e.setAttribute(n.name,n.value)})}function isInlineSwap(e,t){let n=getExtensions(t);for(let r=0;r<n.length;r++){let o=n[r];try{if(o.isInlineSwap(e))return!0}catch(s){logError(s)}}return e==="outerHTML"}function oobSwap(e,t,n,r){r=r||getDocument();let o="#"+CSS.escape(getRawAttribute(t,"id")),s="outerHTML";e==="true"||(e.indexOf(":")>0?(s=e.substring(0,e.indexOf(":")),o=e.substring(e.indexOf(":")+1)):s=e),t.removeAttribute("hx-swap-oob"),t.removeAttribute("data-hx-swap-oob");let i=querySelectorAllExt(r,o,!1);return i.length?(forEach(i,function(a){let l,c=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(s,a)||(l=asParentNode(c));let d={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",d)&&(a=d.target,d.shouldSwap&&(handlePreservedElements(l),swapWithStyle(s,a,a,l,n),restorePreservedElements()),forEach(n.elts,function(u){triggerEvent(u,"htmx:oobAfterSwap",d)}))}),t.parentNode.removeChild(t)):(t.parentNode.removeChild(t),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:t})),e}function restorePreservedElements(){let e=find("#--htmx-preserve-pantry--");if(e){for(let t of[...e.children]){let n=find("#"+t.id);n.parentNode.moveBefore(t,n),n.remove()}e.remove()}}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let n=getAttributeValue(t,"id"),r=getDocument().getElementById(n);if(r!=null)if(t.moveBefore){let o=find("#--htmx-preserve-pantry--");o==null&&(getDocument().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>"),o=find("#--htmx-preserve-pantry--")),o.moveBefore(r,null)}else t.parentNode.replaceChild(r,t)})}function handleAttributes(e,t,n){forEach(t.querySelectorAll("[id]"),function(r){let o=getRawAttribute(r,"id");if(o&&o.length>0){let s=o.replace("'","\\'"),i=r.tagName.replace(":","\\:"),a=asParentNode(e),l=a&&a.querySelector(i+"[id='"+s+"']");if(l&&l!==a){let c=r.cloneNode();cloneAttributes(r,l),n.tasks.push(function(){cloneAttributes(r,c)})}}})}function makeAjaxLoadTask(e){return function(){removeClassFromElement(e,htmx.config.addedClass),processNode(asElement(e)),processFocus(asParentNode(e)),triggerEvent(e,"htmx:load")}}function processFocus(e){let t="[autofocus]",n=asHtmlElement(matches(e,t)?e:e.querySelector(t));n?.focus()}function insertNodesBefore(e,t,n,r){for(handleAttributes(e,n,r);n.childNodes.length>0;){let o=n.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&r.tasks.push(makeAjaxLoadTask(o))}}function stringHash(e,t){let n=0;for(;n<e.length;)t=(t<<5)-t+e.charCodeAt(n++)|0;return t}function attributeHash(e){let t=0;for(let n=0;n<e.attributes.length;n++){let r=e.attributes[n];r.value&&(t=stringHash(r.name,t),t=stringHash(r.value,t))}return t}function deInitOnHandlers(e){let t=getInternalData(e);if(t.onHandlers){for(let n=0;n<t.onHandlers.length;n++){let r=t.onHandlers[n];removeEventListenerImpl(e,r.event,r.listener)}delete t.onHandlers}}function deInitNode(e){let t=getInternalData(e);t.timeout&&clearTimeout(t.timeout),t.listenerInfos&&forEach(t.listenerInfos,function(n){n.on&&removeEventListenerImpl(n.on,n.trigger,n.listener)}),deInitOnHandlers(e),forEach(Object.keys(t),function(n){n!=="firstInitCompleted"&&delete t[n]})}function cleanUpElement(e){triggerEvent(e,"htmx:beforeCleanupElement"),deInitNode(e),forEach(e.children,function(t){cleanUpElement(t)})}function swapOuterHTML(e,t,n){if(e.tagName==="BODY")return swapInnerHTML(e,t,n);let r,o=e.previousSibling,s=parentElt(e);if(s){for(insertNodesBefore(s,e,t,n),o==null?r=s.firstChild:r=o.nextSibling,n.elts=n.elts.filter(function(i){return i!==e});r&&r!==e;)r instanceof Element&&n.elts.push(r),r=r.nextSibling;cleanUpElement(e),e.remove()}}function swapAfterBegin(e,t,n){return insertNodesBefore(e,e.firstChild,t,n)}function swapBeforeBegin(e,t,n){return insertNodesBefore(parentElt(e),e,t,n)}function swapBeforeEnd(e,t,n){return insertNodesBefore(e,null,t,n)}function swapAfterEnd(e,t,n){return insertNodesBefore(parentElt(e),e.nextSibling,t,n)}function swapDelete(e){cleanUpElement(e);let t=parentElt(e);if(t)return t.removeChild(e)}function swapInnerHTML(e,t,n){let r=e.firstChild;if(insertNodesBefore(e,r,t,n),r){for(;r.nextSibling;)cleanUpElement(r.nextSibling),e.removeChild(r.nextSibling);cleanUpElement(r),e.removeChild(r)}}function swapWithStyle(e,t,n,r,o){switch(e){case"none":return;case"outerHTML":swapOuterHTML(n,r,o);return;case"afterbegin":swapAfterBegin(n,r,o);return;case"beforebegin":swapBeforeBegin(n,r,o);return;case"beforeend":swapBeforeEnd(n,r,o);return;case"afterend":swapAfterEnd(n,r,o);return;case"delete":swapDelete(n);return;default:var s=getExtensions(t);for(let i=0;i<s.length;i++){let a=s[i];try{let l=a.handleSwap(e,n,r,o);if(l){if(Array.isArray(l))for(let c=0;c<l.length;c++){let d=l[c];d.nodeType!==Node.TEXT_NODE&&d.nodeType!==Node.COMMENT_NODE&&o.tasks.push(makeAjaxLoadTask(d))}return}}catch(l){logError(l)}}e==="innerHTML"?swapInnerHTML(n,r,o):swapWithStyle(htmx.config.defaultSwapStyle,t,n,r,o)}}function findAndSwapOobElements(e,t,n){var r=findAll(e,"[hx-swap-oob], [data-hx-swap-oob]");return forEach(r,function(o){if(htmx.config.allowNestedOobSwaps||o.parentElement===null){let s=getAttributeValue(o,"hx-swap-oob");s!=null&&oobSwap(s,o,t,n)}else o.removeAttribute("hx-swap-oob"),o.removeAttribute("data-hx-swap-oob")}),r.length>0}function swap(e,t,n,r){r||(r={});let o=null,s=null,i=function(){maybeCall(r.beforeSwapCallback),e=resolveTarget(e);let c=r.contextElement?getRootNode(r.contextElement,!1):getDocument(),d=document.activeElement,u={};u={elt:d,start:d?d.selectionStart:null,end:d?d.selectionEnd:null};let f=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else{let h=makeFragment(t);if(f.title=r.title||h.title,r.historyRequest&&(h=h.querySelector("[hx-history-elt],[data-hx-history-elt]")||h),r.selectOOB){let g=r.selectOOB.split(",");for(let p=0;p<g.length;p++){let y=g[p].split(":",2),w=y[0].trim();w.indexOf("#")===0&&(w=w.substring(1));let b=y[1]||"true",v=h.querySelector("#"+w);v&&oobSwap(b,v,f,c)}}if(findAndSwapOobElements(h,f,c),forEach(findAll(h,"template"),function(g){g.content&&findAndSwapOobElements(g.content,f,c)&&g.remove()}),r.select){let g=getDocument().createDocumentFragment();forEach(h.querySelectorAll(r.select),function(p){g.appendChild(p)}),h=g}handlePreservedElements(h),swapWithStyle(n.swapStyle,r.contextElement,e,h,f),restorePreservedElements()}if(u.elt&&!bodyContains(u.elt)&&getRawAttribute(u.elt,"id")){let h=document.getElementById(getRawAttribute(u.elt,"id")),g={preventScroll:n.focusScroll!==void 0?!n.focusScroll:!htmx.config.defaultFocusScroll};if(h){if(u.start&&h.setSelectionRange)try{h.setSelectionRange(u.start,u.end)}catch{}h.focus(g)}}e.classList.remove(htmx.config.swappingClass),forEach(f.elts,function(h){h.classList&&h.classList.add(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSwap",r.eventInfo)}),maybeCall(r.afterSwapCallback),n.ignoreTitle||handleTitle(f.title);let m=function(){if(forEach(f.tasks,function(h){h.call()}),forEach(f.elts,function(h){h.classList&&h.classList.remove(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSettle",r.eventInfo)}),r.anchor){let h=asElement(resolveTarget("#"+r.anchor));h&&h.scrollIntoView({block:"start",behavior:"auto"})}updateScrollState(f.elts,n),maybeCall(r.afterSettleCallback),maybeCall(o)};n.settleDelay>0?getWindow().setTimeout(m,n.settleDelay):m()},a=htmx.config.globalViewTransitions;n.hasOwnProperty("transition")&&(a=n.transition);let l=r.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",r.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let c=new Promise(function(u,f){o=u,s=f}),d=i;i=function(){document.startViewTransition(function(){return d(),c})}}try{n?.swapDelay&&n.swapDelay>0?getWindow().setTimeout(i,n.swapDelay):i()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",r.eventInfo),maybeCall(s),c}}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let s in o)if(o.hasOwnProperty(s)){let i=o[s];isRawObject(i)?n=i.target!==void 0?i.target:n:i={value:i},triggerEvent(n,s,i)}}else{let o=r.split(",");for(let s=0;s<o.length;s++)triggerEvent(n,o[s].trim(),[])}}let WHITESPACE=/\s/,WHITESPACE_OR_COMMA=/[\s,]/,SYMBOL_START=/[_$a-zA-Z]/,SYMBOL_CONT=/[_$a-zA-Z0-9]/,STRINGISH_START=['"',"'","/"],NOT_WHITESPACE=/[^\s]/,COMBINED_SELECTOR_START=/[{(]/,COMBINED_SELECTOR_END=/[})]/;function tokenizeString(e){let t=[],n=0;for(;n<e.length;){if(SYMBOL_START.exec(e.charAt(n))){for(var r=n;SYMBOL_CONT.exec(e.charAt(n+1));)n++;t.push(e.substring(r,n+1))}else if(STRINGISH_START.indexOf(e.charAt(n))!==-1){let o=e.charAt(n);var r=n;for(n++;n<e.length&&e.charAt(n)!==o;)e.charAt(n)==="\\"&&n++,n++;t.push(e.substring(r,n+1))}else{let o=e.charAt(n);t.push(o)}n++}return t}function isPossibleRelativeReference(e,t,n){return SYMBOL_START.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function maybeGenerateConditional(e,t,n){if(t[0]==="["){t.shift();let r=1,o=" return (function("+n+"){ return (",s=null;for(;t.length>0;){let i=t[0];if(i==="]"){if(r--,r===0){s===null&&(o=o+"true"),t.shift(),o+=")})";try{let a=maybeEval(e,function(){return Function(o)()},function(){return!0});return a.source=o,a}catch(a){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:a,source:o}),null}}}else i==="["&&r++;isPossibleRelativeReference(i,s,n)?o+="(("+n+"."+i+") ? ("+n+"."+i+") : (window."+i+"))":o=o+i,s=t.shift()}}}function consumeUntil(e,t){let n="";for(;e.length>0&&!t.test(e[0]);)n+=e.shift();return n}function consumeCSSSelector(e){let t;return e.length>0&&COMBINED_SELECTOR_START.test(e[0])?(e.shift(),t=consumeUntil(e,COMBINED_SELECTOR_END).trim(),e.shift()):t=consumeUntil(e,WHITESPACE_OR_COMMA),t}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(e,t,n){let r=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let a=o.length,l=consumeUntil(o,/[,\[\s]/);if(l!=="")if(l==="every"){let c={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),c.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var s=maybeGenerateConditional(e,o,"event");s&&(c.eventFilter=s),r.push(c)}else{let c={trigger:l};var s=maybeGenerateConditional(e,o,"event");for(s&&(c.eventFilter=s),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let u=o.shift();if(u==="changed")c.changed=!0;else if(u==="once")c.once=!0;else if(u==="consume")c.consume=!0;else if(u==="delay"&&o[0]===":")o.shift(),c.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(u==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var i=consumeCSSSelector(o);else{var i=consumeUntil(o,WHITESPACE_OR_COMMA);if(i==="closest"||i==="find"||i==="next"||i==="previous"){o.shift();let m=consumeCSSSelector(o);m.length>0&&(i+=" "+m)}}c.from=i}else u==="target"&&o[0]===":"?(o.shift(),c.target=consumeCSSSelector(o)):u==="throttle"&&o[0]===":"?(o.shift(),c.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):u==="queue"&&o[0]===":"?(o.shift(),c.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):u==="root"&&o[0]===":"?(o.shift(),c[u]=consumeCSSSelector(o)):u==="threshold"&&o[0]===":"?(o.shift(),c[u]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}r.push(c)}o.length===a&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r)}return n.length>0?n:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0}function processPolling(e,t,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n))},n.pollInterval)}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else{let s=getRawAttribute(e,"method");r=s?s.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),r==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}n.forEach(function(s){addEventListener(e,function(i,a){let l=asElement(i);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(r,o,l,a)},t,s,!0)})}}function shouldCancel(e,t){if(e.type==="submit"&&t.tagName==="FORM")return!0;if(e.type==="click"){let n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit")return!0;let r=t.closest("a"),o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href")))return!0}return!1}function ignoreBoostedAnchorCtrlClick(e,t){return getInternalData(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function maybeFilterEvent(e,t,n){let r=e.eventFilter;if(r)try{return r.call(t,n)!==!0}catch(o){let s=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:s}),!0}return!1}function addEventListener(e,t,n,r,o){let s=getInternalData(e),i;r.from?i=querySelectorAllExt(e,r.from):i=[e],r.changed&&("lastValue"in s||(s.lastValue=new WeakMap),i.forEach(function(a){s.lastValue.has(r)||s.lastValue.set(r,new WeakMap),s.lastValue.get(r).set(a,a.value)})),forEach(i,function(a){let l=function(c){if(!bodyContains(e)){a.removeEventListener(r.trigger,l);return}if(ignoreBoostedAnchorCtrlClick(e,c)||((o||shouldCancel(c,a))&&c.preventDefault(),maybeFilterEvent(r,e,c)))return;let d=getInternalData(c);if(d.triggerSpec=r,d.handledFor==null&&(d.handledFor=[]),d.handledFor.indexOf(e)<0){if(d.handledFor.push(e),r.consume&&c.stopPropagation(),r.target&&c.target&&!matches(asElement(c.target),r.target))return;if(r.once){if(s.triggeredOnce)return;s.triggeredOnce=!0}if(r.changed){let u=c.target,f=u.value,m=s.lastValue.get(r);if(m.has(u)&&m.get(u)===f)return;m.set(u,f)}if(s.delayed&&clearTimeout(s.delayed),s.throttle)return;r.throttle>0?s.throttle||(triggerEvent(e,"htmx:trigger"),t(e,c),s.throttle=getWindow().setTimeout(function(){s.throttle=null},r.throttle)):r.delay>0?s.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,c)},r.delay):(triggerEvent(e,"htmx:trigger"),t(e,c))}};n.listenerInfos==null&&(n.listenerInfos=[]),n.listenerInfos.push({trigger:r.trigger,listener:l,on:a}),a.addEventListener(r.trigger,l)})}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0},window.addEventListener("scroll",scrollHandler),window.addEventListener("resize",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){maybeReveal(e)}))},200))}function maybeReveal(e){!hasAttribute(e,"data-hx-revealed")&&isScrolledIntoView(e)&&(e.setAttribute("data-hx-revealed","true"),getInternalData(e).initHash?triggerEvent(e,"revealed"):e.addEventListener("htmx:afterProcessNode",function(){triggerEvent(e,"revealed")},{once:!0}))}function loadImmediately(e,t,n,r){let o=function(){n.loaded||(n.loaded=!0,triggerEvent(e,"htmx:trigger"),t(e))};r>0?getWindow().setTimeout(o,r):o()}function processVerbs(e,t,n){let r=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let s=getAttributeValue(e,"hx-"+o);r=!0,t.path=s,t.verb=o,n.forEach(function(i){addTriggerHandler(e,i,t,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(o,s,c,l)})})}}),r}function addTriggerHandler(e,t,n,r){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,r,n,t),maybeReveal(asElement(e));else if(t.trigger==="intersect"){let o={};t.root&&(o.root=querySelectorExt(e,t.root)),t.threshold&&(o.threshold=parseFloat(t.threshold)),new IntersectionObserver(function(i){for(let a=0;a<i.length;a++)if(i[a].isIntersecting){triggerEvent(e,"intersect");break}},o).observe(asElement(e)),addEventListener(asElement(e),r,n,t)}else!n.firstInitCompleted&&t.trigger==="load"?maybeFilterEvent(t,e,makeEvent("load",{elt:e}))||loadImmediately(asElement(e),r,n,t.delay):t.pollInterval>0?(n.polling=!0,processPolling(asElement(e),r,t)):addEventListener(e,r,n,t)}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return!1;let n=t.attributes;for(let r=0;r<n.length;r++){let o=n[r].name;if(startsWith(o,"hx-on:")||startsWith(o,"data-hx-on:")||startsWith(o,"hx-on-")||startsWith(o,"data-hx-on-"))return!0}return!1}let HX_ON_QUERY=new XPathEvaluator().createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function processHXOnRoot(e,t){shouldProcessHxOn(e)&&t.push(asElement(e));let n=HX_ON_QUERY.evaluate(e),r=null;for(;r=n.iterateNext();)t.push(asElement(r))}function findHxOnWildcardElements(e){let t=[];if(e instanceof DocumentFragment)for(let n of e.childNodes)processHXOnRoot(n,t);else processHXOnRoot(e,t);return t}function findElementsToProcess(e){if(e.querySelectorAll){let n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]",r=[];for(let s in extensions){let i=extensions[s];if(i.getSelectors){var t=i.getSelectors();t&&r.push(t)}}return e.querySelectorAll(VERB_SELECTOR+n+", form, [type='submit'], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(s=>", "+s).join(""))}else return[]}function maybeSetLastButtonClicked(e){let t=getTargetButton(e.target),n=getRelatedFormData(e);n&&(n.lastButtonClicked=t)}function maybeUnsetLastButtonClicked(e){let t=getRelatedFormData(e);t&&(t.lastButtonClicked=null)}function getTargetButton(e){return closest(asElement(e),"button, input[type='submit']")}function getRelatedForm(e){return e.form||closest(e,"form")}function getRelatedFormData(e){let t=getTargetButton(e.target);if(!t)return;let n=getRelatedForm(t);if(n)return getInternalData(n)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(e,t,n){let r=getInternalData(e);Array.isArray(r.onHandlers)||(r.onHandlers=[]);let o,s=function(i){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,i))})};e.addEventListener(t,s),r.onHandlers.push({event:t,listener:s})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;t<e.attributes.length;t++){let n=e.attributes[t].name,r=e.attributes[t].value;if(startsWith(n,"hx-on")||startsWith(n,"data-hx-on")){let o=n.indexOf("-on")+3,s=n.slice(o,o+1);if(s==="-"||s===":"){let i=n.slice(o+1);startsWith(i,":")?i="htmx"+i:startsWith(i,"-")?i="htmx:"+i.slice(1):startsWith(i,"htmx-")&&(i="htmx:"+i.slice(5)),addHxOnEventHandler(e,i,r)}}}}function initNode(e){triggerEvent(e,"htmx:beforeProcessNode");let t=getInternalData(e),n=getTriggerSpecs(e);processVerbs(e,t,n)||(getClosestAttributeValue(e,"hx-boost")==="true"?boostElement(e,t,n):hasAttribute(e,"hx-trigger")&&n.forEach(function(o){addTriggerHandler(e,o,t,function(){})})),(e.tagName==="FORM"||getRawAttribute(e,"type")==="submit"&&hasAttribute(e,"form"))&&initButtonTracking(e),t.firstInitCompleted=!0,triggerEvent(e,"htmx:afterProcessNode")}function maybeDeInitAndHash(e){if(!(e instanceof Element))return!1;let t=getInternalData(e),n=attributeHash(e);return t.initHash!==n?(deInitNode(e),t.initHash=n,!0):!1}function processNode(e){if(e=resolveTarget(e),eltIsDisabled(e)){cleanUpElement(e);return}let t=[];maybeDeInitAndHash(e)&&t.push(e),forEach(findElementsToProcess(e),function(n){if(eltIsDisabled(n)){cleanUpElement(n);return}maybeDeInitAndHash(n)&&t.push(n)}),forEach(findHxOnWildcardElements(e),processHxOnWildcard),forEach(t,initNode)}function kebabEventName(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function makeEvent(e,t){return new CustomEvent(e,{bubbles:!0,cancelable:!0,composed:!0,detail:t})}function triggerErrorEvent(e,t,n){triggerEvent(e,t,mergeObjects({error:t},n))}function ignoreEventForLogging(e){return e==="htmx:afterProcessNode"}function withExtensions(e,t,n){forEach(getExtensions(e,[],n),function(r){try{t(r)}catch(o){logError(o)}})}function logError(e){console.error(e)}function triggerEvent(e,t,n){e=resolveTarget(e),n==null&&(n={}),n.elt=e;let r=makeEvent(t,n);htmx.logger&&!ignoreEventForLogging(t)&&htmx.logger(e,t,n),n.error&&(logError(n.error),triggerEvent(e,"htmx:error",{errorInfo:n}));let o=e.dispatchEvent(r),s=kebabEventName(t);if(o&&s!==t){let i=makeEvent(s,r.detail);o=o&&e.dispatchEvent(i)}return withExtensions(asElement(e),function(i){o=o&&i.onEvent(t,r)!==!1&&!r.defaultPrevented}),o}let currentPathForHistory;function setCurrentPathForHistory(e){currentPathForHistory=e,canAccessLocalStorage()&&sessionStorage.setItem("htmx-current-path-for-history",e)}setCurrentPathForHistory(location.pathname+location.search);function getHistoryElement(){return getDocument().querySelector("[hx-history-elt],[data-hx-history-elt]")||getDocument().body}function saveToHistoryCache(e,t){if(!canAccessLocalStorage())return;let n=cleanInnerHtmlForHistory(t),r=getDocument().title,o=window.scrollY;if(htmx.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}e=normalizePath(e);let s=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let a=0;a<s.length;a++)if(s[a].url===e){s.splice(a,1);break}let i={url:e,content:n,title:r,scroll:o};for(triggerEvent(getDocument().body,"htmx:historyItemCreated",{item:i,cache:s}),s.push(i);s.length>htmx.config.historyCacheSize;)s.shift();for(;s.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(s));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:s}),s.shift()}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let n=0;n<t.length;n++)if(t[n].url===e)return t[n];return null}function cleanInnerHtmlForHistory(e){let t=htmx.config.requestClass,n=e.cloneNode(!0);return forEach(findAll(n,"."+t),function(r){removeClassFromElement(r,t)}),forEach(findAll(n,"[data-disabled-by-htmx]"),function(r){r.removeAttribute("disabled")}),n.innerHTML}function saveCurrentPageToHistory(){let e=getHistoryElement(),t=currentPathForHistory;canAccessLocalStorage()&&(t=sessionStorage.getItem("htmx-current-path-for-history")),t=t||location.pathname+location.search,getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')||(triggerEvent(getDocument().body,"htmx:beforeHistorySave",{path:t,historyElt:e}),saveToHistoryCache(t,e)),htmx.config.historyEnabled&&history.replaceState({htmx:!0},getDocument().title,location.href)}function pushUrlIntoHistory(e){htmx.config.getCacheBusterParam&&(e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,""),(endsWith(e,"&")||endsWith(e,"?"))&&(e=e.slice(0,-1))),htmx.config.historyEnabled&&history.pushState({htmx:!0},"",e),setCurrentPathForHistory(e)}function replaceUrlInHistory(e){htmx.config.historyEnabled&&history.replaceState({htmx:!0},"",e),setCurrentPathForHistory(e)}function settleImmediately(e){forEach(e,function(t){t.call(void 0)})}function loadHistoryFromServer(e){let t=new XMLHttpRequest,n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0},r={path:e,xhr:t,historyElt:getHistoryElement(),swapSpec:n};t.open("GET",e,!0),htmx.config.historyRestoreAsHxRequest&&t.setRequestHeader("HX-Request","true"),t.setRequestHeader("HX-History-Restore-Request","true"),t.setRequestHeader("HX-Current-URL",location.href),t.onload=function(){this.status>=200&&this.status<400?(r.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",r),swap(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:!0}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:r.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",r)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",r)&&t.send()}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll},r={path:e,item:t,historyElt:getHistoryElement(),swapSpec:n};triggerEvent(getDocument().body,"htmx:historyCacheHit",r)&&(swap(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",r))}else htmx.config.refreshOnHistoryMiss?htmx.location.reload(!0):loadHistoryFromServer(e)}function addRequestIndicatorClasses(e){let t=findAttributeTargets(e,"hx-indicator");return t==null&&(t=[e]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.classList.add.call(n.classList,htmx.config.requestClass)}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.setAttribute("disabled",""),n.setAttribute("data-disabled-by-htmx","")}),t}function removeRequestIndicators(e,t){forEach(e.concat(t),function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||1)-1}),forEach(e,function(n){getInternalData(n).requestCount===0&&n.classList.remove.call(n.classList,htmx.config.requestClass)}),forEach(t,function(n){getInternalData(n).requestCount===0&&(n.removeAttribute("disabled"),n.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(e,t){for(let n=0;n<e.length;n++)if(e[n].isSameNode(t))return!0;return!1}function shouldInclude(e){let t=e;return t.name===""||t.name==null||t.disabled||closest(t,"fieldset[disabled]")||t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"?!1:t.type==="checkbox"||t.type==="radio"?t.checked:!0}function addValueToFormData(e,t,n){e!=null&&t!=null&&(Array.isArray(t)?t.forEach(function(r){n.append(e,r)}):n.append(e,t))}function removeValueFromFormData(e,t,n){if(e!=null&&t!=null){let r=n.getAll(e);Array.isArray(t)?r=r.filter(o=>t.indexOf(o)<0):r=r.filter(o=>o!==t),n.delete(e),forEach(r,o=>n.append(e,o))}}function getValueFromInput(e){return e instanceof HTMLSelectElement&&e.multiple?toArray(e.querySelectorAll("option:checked")).map(function(t){return t.value}):e instanceof HTMLInputElement&&e.files?toArray(e.files):e.value}function processInputValue(e,t,n,r,o){if(!(r==null||haveSeenNode(e,r))){if(e.push(r),shouldInclude(r)){let s=getRawAttribute(r,"name");addValueToFormData(s,getValueFromInput(r),t),o&&validateElement(r,n)}r instanceof HTMLFormElement&&(forEach(r.elements,function(s){e.indexOf(s)>=0?removeValueFromFormData(s.name,getValueFromInput(s),t):e.push(s),o&&validateElement(s,n)}),new FormData(r).forEach(function(s,i){s instanceof File&&s.name===""||addValueToFormData(i,s,t)}))}}function validateElement(e,t){let n=e;n.willValidate&&(triggerEvent(n,"htmx:validation:validate"),n.checkValidity()||(triggerEvent(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&htmx.config.reportValidityOfForms&&n.reportValidity(),t.push({elt:n,message:n.validationMessage,validity:n.validity})))}function overrideFormData(e,t){for(let n of t.keys())e.delete(n);return t.forEach(function(n,r){e.append(r,n)}),e}function getInputValues(e,t){let n=[],r=new FormData,o=new FormData,s=[],i=getInternalData(e);i.lastButtonClicked&&!bodyContains(i.lastButtonClicked)&&(i.lastButtonClicked=null);let a=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(i.lastButtonClicked&&(a=a&&i.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,s,getRelatedForm(e),a),processInputValue(n,r,s,e,a),i.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let c=i.lastButtonClicked||e,d=getRawAttribute(c,"name");addValueToFormData(d,c.value,o)}let l=findAttributeTargets(e,"hx-include");return forEach(l,function(c){processInputValue(n,r,s,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(d){processInputValue(n,r,s,d,a)})}),overrideFormData(r,o),{errors:s,formData:r,values:formDataProxy(r)}}function appendParam(e,t,n){e!==""&&(e+="&"),String(n)==="[object Object]"&&(n=JSON.stringify(n));let r=encodeURIComponent(n);return e+=encodeURIComponent(t)+"="+r,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(n,r){t=appendParam(t,r,n)}),t}function getHeaders(e,t,n){let r={"HX-Request":"true","HX-Trigger":getRawAttribute(e,"id"),"HX-Trigger-Name":getRawAttribute(e,"name"),"HX-Target":getAttributeValue(t,"id"),"HX-Current-URL":location.href};return getValuesForElement(e,"hx-headers",!1,r),n!==void 0&&(r["HX-Prompt"]=n),getInternalData(e).boosted&&(r["HX-Boosted"]="true"),r}function filterValues(e,t){let n=getClosestAttributeValue(t,"hx-params");if(n){if(n==="none")return new FormData;if(n==="*")return e;if(n.indexOf("not ")===0)return forEach(n.slice(4).split(","),function(r){r=r.trim(),e.delete(r)}),e;{let r=new FormData;return forEach(n.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(s){r.append(o,s)})}),r}}else return e}function isAnchorLink(e){return!!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let n=t||getClosestAttributeValue(e,"hx-swap"),r={swapStyle:getInternalData(e).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(e).boosted&&!isAnchorLink(e)&&(r.show="top"),n){let i=splitOnWhitespace(n);if(i.length>0)for(let a=0;a<i.length;a++){let l=i[a];if(l.indexOf("swap:")===0)r.swapDelay=parseInterval(l.slice(5));else if(l.indexOf("settle:")===0)r.settleDelay=parseInterval(l.slice(7));else if(l.indexOf("transition:")===0)r.transition=l.slice(11)==="true";else if(l.indexOf("ignoreTitle:")===0)r.ignoreTitle=l.slice(12)==="true";else if(l.indexOf("scroll:")===0){var o=l.slice(7).split(":");let d=o.pop();var s=o.length>0?o.join(":"):null;r.scroll=d,r.scrollTarget=s}else if(l.indexOf("show:")===0){var o=l.slice(5).split(":");let u=o.pop();var s=o.length>0?o.join(":"):null;r.show=u,r.showTarget=s}else if(l.indexOf("focus-scroll:")===0){let c=l.slice(13);r.focusScroll=c=="true"}else a==0?r.swapStyle=l:logError("Unknown modifier in hx-swap: "+l)}}return r}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,n){let r=null;return withExtensions(t,function(o){r==null&&(r=o.encodeParameters(e,n,t))}),r??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(n)):urlEncode(n))}function makeSettleInfo(e){return{tasks:[],elts:[e]}}function updateScrollState(e,t){let n=e[0],r=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(n,t.scrollTarget))),t.scroll==="top"&&(n||o)&&(o=o||n,o.scrollTop=0),t.scroll==="bottom"&&(r||o)&&(o=o||r,o.scrollTop=o.scrollHeight),typeof t.scroll=="number"&&getWindow().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}if(t.show){var o=null;if(t.showTarget){let i=t.showTarget;t.showTarget==="window"&&(i="body"),o=asElement(querySelectorExt(n,i))}t.show==="top"&&(n||o)&&(o=o||n,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(e,t,n,r,o){if(r==null&&(r={}),e==null)return r;let s=getAttributeValue(e,t);if(s){let i=s.trim(),a=n;if(i==="unset")return null;i.indexOf("javascript:")===0?(i=i.slice(11),a=!0):i.indexOf("js:")===0&&(i=i.slice(3),a=!0),i.indexOf("{")!==0&&(i="{"+i+"}");let l;a?l=maybeEval(e,function(){return o?Function("event","return ("+i+")").call(e,o):Function("return ("+i+")").call(e)},{}):l=parseJSON(i);for(let c in l)l.hasOwnProperty(c)&&r[c]==null&&(r[c]=l[c])}return getValuesForElement(asElement(parentElt(e)),t,n,r,o)}function maybeEval(e,t,n){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),n)}function getHXVarsForElement(e,t,n){return getValuesForElement(e,"hx-vars",!0,n,t)}function getHXValsForElement(e,t,n){return getValuesForElement(e,"hx-vals",!1,n,t)}function getExpressionVars(e,t){return mergeObjects(getHXVarsForElement(e,t),getHXValsForElement(e,t))}function safelySetHeaderValue(e,t,n){if(n!==null)try{e.setRequestHeader(t,n)}catch{e.setRequestHeader(t,encodeURIComponent(n)),e.setRequestHeader(t+"-URI-AutoEncoded","true")}}function getPathFromResponse(e){if(e.responseURL)try{let t=new URL(e.responseURL);return t.pathname+t.search}catch{triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:e.responseURL})}}function hasHeader(e,t){return t.test(e.getAllResponseHeaders())}function ajaxHelper(e,t,n){if(e=e.toLowerCase(),n){if(n instanceof Element||typeof n=="string")return issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(n)||DUMMY_ELT,returnPromise:!0});{let r=resolveTarget(n.target);return(n.target&&!r||n.source&&!r&&!resolveTarget(n.source))&&(r=DUMMY_ELT),issueAjaxRequest(e,t,resolveTarget(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:r,swapOverride:n.swap,select:n.select,returnPromise:!0,push:n.push,replace:n.replace,selectOOB:n.selectOOB})}}else return issueAjaxRequest(e,t,null,null,{returnPromise:!0})}function hierarchyForElt(e){let t=[];for(;e;)t.push(e),e=e.parentElement;return t}function verifyPath(e,t,n){let r=new URL(t,location.protocol!=="about:"?location.href:window.origin),s=(location.protocol!=="about:"?location.origin:window.origin)===r.origin;return htmx.config.selfRequestsOnly&&!s?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:r,sameHost:s},n))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let n in e)e.hasOwnProperty(n)&&(e[n]&&typeof e[n].forEach=="function"?e[n].forEach(function(r){t.append(n,r)}):typeof e[n]=="object"&&!(e[n]instanceof Blob)?t.append(n,JSON.stringify(e[n])):t.append(n,e[n]));return t}function formDataArrayProxy(e,t,n){return new Proxy(n,{get:function(r,o){return typeof o=="number"?r[o]:o==="length"?r.length:o==="push"?function(s){r.push(s),e.append(t,s)}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(s){e.append(t,s)})}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,s){return r[o]=s,e.delete(t),r.forEach(function(i){e.append(t,i)}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,n){if(typeof n=="symbol"){let o=Reflect.get(t,n);return typeof o=="function"?function(){return o.apply(e,arguments)}:o}if(n==="toJSON")return()=>Object.fromEntries(e);if(n in t&&typeof t[n]=="function")return function(){return e[n].apply(e,arguments)};let r=e.getAll(n);if(r.length!==0)return r.length===1?r[0]:formDataArrayProxy(t,n,r)},set:function(t,n,r){return typeof n!="string"?!1:(t.delete(n),r&&typeof r.forEach=="function"?r.forEach(function(o){t.append(n,o)}):typeof r=="object"&&!(r instanceof Blob)?t.append(n,JSON.stringify(r)):t.append(n,r),!0)},deleteProperty:function(t,n){return typeof n=="string"&&t.delete(n),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,n){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),n)}})}function issueAjaxRequest(e,t,n,r,o,s){let i=null,a=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var l=new Promise(function(E,x){i=E,a=x});n==null&&(n=getDocument().body);let c=o.handler||handleAjaxResponse,d=o.select||null;if(!bodyContains(n))return maybeCall(i),l;let u=o.targetOverride||asElement(getTarget(n));if(u==null||u==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getClosestAttributeValue(n,"hx-target")}),maybeCall(a),l;let f=getInternalData(n),m=f.lastButtonClicked;if(m){let E=getRawAttribute(m,"formaction");E!=null&&(t=E);let x=getRawAttribute(m,"formmethod");if(x!=null)if(VERBS.includes(x.toLowerCase()))e=x;else return maybeCall(i),l}let h=getClosestAttributeValue(n,"hx-confirm");if(s===void 0&&triggerEvent(n,"htmx:confirm",{target:u,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(L){return issueAjaxRequest(e,t,n,r,o,!!L)},question:h})===!1)return maybeCall(i),l;let g=n,p=getClosestAttributeValue(n,"hx-sync"),y=null,w=!1;if(p){let E=p.split(":"),x=E[0].trim();if(x==="this"?g=findThisElement(n,"hx-sync"):g=asElement(querySelectorExt(n,x)),p=(E[1]||"drop").trim(),f=getInternalData(g),p==="drop"&&f.xhr&&f.abortable!==!0)return maybeCall(i),l;if(p==="abort"){if(f.xhr)return maybeCall(i),l;w=!0}else p==="replace"?triggerEvent(g,"htmx:abort"):p.indexOf("queue")===0&&(y=(p.split(" ")[1]||"last").trim())}if(f.xhr)if(f.abortable)triggerEvent(g,"htmx:abort");else{if(y==null){if(r){let E=getInternalData(r);E&&E.triggerSpec&&E.triggerSpec.queue&&(y=E.triggerSpec.queue)}y==null&&(y="last")}return f.queuedRequests==null&&(f.queuedRequests=[]),y==="first"&&f.queuedRequests.length===0?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):y==="all"?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):y==="last"&&(f.queuedRequests=[],f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)})),maybeCall(i),l}let b=new XMLHttpRequest;f.xhr=b,f.abortable=w;let v=function(){f.xhr=null,f.abortable=!1,f.queuedRequests!=null&&f.queuedRequests.length>0&&f.queuedRequests.shift()()},te=getClosestAttributeValue(n,"hx-prompt");if(te){var V=prompt(te);if(V===null||!triggerEvent(n,"htmx:prompt",{prompt:V,target:u}))return maybeCall(i),v(),l}if(h&&!s&&!confirm(h))return maybeCall(i),v(),l;let H=getHeaders(n,u,V);e!=="get"&&!usesFormData(n)&&(H["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(H=mergeObjects(H,o.headers));let ne=getInputValues(n,e),N=ne.errors,re=ne.formData;o.values&&overrideFormData(re,formDataFromObject(o.values));let Se=formDataFromObject(getExpressionVars(n,r)),j=overrideFormData(re,Se),R=filterValues(j,n);htmx.config.getCacheBusterParam&&e==="get"&&R.set("org.htmx.cache-buster",getRawAttribute(u,"id")||"true"),(t==null||t==="")&&(t=location.href);let W=getValuesForElement(n,"hx-request"),oe=getInternalData(n).boosted,P=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,C={boosted:oe,useUrlParams:P,formData:R,parameters:formDataProxy(R),unfilteredFormData:j,unfilteredParameters:formDataProxy(j),headers:H,elt:n,target:u,verb:e,errors:N,withCredentials:o.credentials||W.credentials||htmx.config.withCredentials,timeout:o.timeout||W.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",C))return maybeCall(i),v(),l;if(t=C.path,e=C.verb,H=C.headers,R=formDataFromObject(C.parameters),N=C.errors,P=C.useUrlParams,N&&N.length>0)return triggerEvent(n,"htmx:validation:halted",C),maybeCall(i),v(),l;let se=t.split("#"),Ce=se[0],X=se[1],A=t;if(P&&(A=Ce,!R.keys().next().done&&(A.indexOf("?")<0?A+="?":A+="&",A+=urlEncode(R),X&&(A+="#"+X))),!verifyPath(n,A,C))return triggerErrorEvent(n,"htmx:invalidPath",C),maybeCall(a),v(),l;if(b.open(e.toUpperCase(),A,!0),b.overrideMimeType("text/html"),b.withCredentials=C.withCredentials,b.timeout=C.timeout,!W.noHeaders){for(let E in H)if(H.hasOwnProperty(E)){let x=H[E];safelySetHeaderValue(b,E,x)}}let T={xhr:b,target:u,requestConfig:C,etc:o,boosted:oe,select:d,pathInfo:{requestPath:t,finalRequestPath:A,responsePath:null,anchor:X}};if(b.onload=function(){try{let E=hierarchyForElt(n);if(T.pathInfo.responsePath=getPathFromResponse(b),c(n,T),T.keepIndicators!==!0&&removeRequestIndicators(F,B),triggerEvent(n,"htmx:afterRequest",T),triggerEvent(n,"htmx:afterOnLoad",T),!bodyContains(n)){let x=null;for(;E.length>0&&x==null;){let L=E.shift();bodyContains(L)&&(x=L)}x&&(triggerEvent(x,"htmx:afterRequest",T),triggerEvent(x,"htmx:afterOnLoad",T))}maybeCall(i)}catch(E){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:E},T)),E}finally{v()}},b.onerror=function(){removeRequestIndicators(F,B),triggerErrorEvent(n,"htmx:afterRequest",T),triggerErrorEvent(n,"htmx:sendError",T),maybeCall(a),v()},b.onabort=function(){removeRequestIndicators(F,B),triggerErrorEvent(n,"htmx:afterRequest",T),triggerErrorEvent(n,"htmx:sendAbort",T),maybeCall(a),v()},b.ontimeout=function(){removeRequestIndicators(F,B),triggerErrorEvent(n,"htmx:afterRequest",T),triggerErrorEvent(n,"htmx:timeout",T),maybeCall(a),v()},!triggerEvent(n,"htmx:beforeRequest",T))return maybeCall(i),v(),l;var F=addRequestIndicatorClasses(n),B=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(E){forEach([b,b.upload],function(x){x.addEventListener(E,function(L){triggerEvent(n,"htmx:xhr:"+E,{lengthComputable:L.lengthComputable,loaded:L.loaded,total:L.total})})})}),triggerEvent(n,"htmx:beforeSend",T);let Ae=P?null:encodeParamsForBody(b,n,R);return b.send(Ae),l}function determineHistoryUpdates(e,t){let n=t.xhr,r=null,o=null;if(hasHeader(n,/HX-Push:/i)?(r=n.getResponseHeader("HX-Push"),o="push"):hasHeader(n,/HX-Push-Url:/i)?(r=n.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(n,/HX-Replace-Url:/i)&&(r=n.getResponseHeader("HX-Replace-Url"),o="replace"),r)return r==="false"?{}:{type:o,path:r};let s=t.pathInfo.finalRequestPath,i=t.pathInfo.responsePath,a=t.etc.push||getClosestAttributeValue(e,"hx-push-url"),l=t.etc.replace||getClosestAttributeValue(e,"hx-replace-url"),c=getInternalData(e).boosted,d=null,u=null;return a?(d="push",u=a):l?(d="replace",u=l):c&&(d="push",u=i||s),u?u==="false"?{}:(u==="true"&&(u=i||s),t.pathInfo.anchor&&u.indexOf("#")===-1&&(u=u+"#"+t.pathInfo.anchor),{type:d,path:u}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t<htmx.config.responseHandling.length;t++){var n=htmx.config.responseHandling[t];if(codeMatches(n,e.status))return n}return{swap:!1}}function handleTitle(e){if(e){let t=find("title");t?t.textContent=e:window.document.title=e}}function resolveRetarget(e,t){if(t==="this")return e;let n=asElement(querySelectorExt(e,t));if(n==null)throw triggerErrorEvent(e,"htmx:targetError",{target:t}),new Error(`Invalid re-target ${t}`);return n}function handleAjaxResponse(e,t){let n=t.xhr,r=t.target,o=t.etc,s=t.select;if(!triggerEvent(e,"htmx:beforeOnLoad",t))return;if(hasHeader(n,/HX-Trigger:/i)&&handleTriggerHeader(n,"HX-Trigger",e),hasHeader(n,/HX-Location:/i)){let w=n.getResponseHeader("HX-Location");var i={};w.indexOf("{")===0&&(i=parseJSON(w),w=i.path,delete i.path),i.push=i.push||"true",ajaxHelper("get",w,i);return}let a=hasHeader(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(hasHeader(n,/HX-Redirect:/i)){t.keepIndicators=!0,htmx.location.href=n.getResponseHeader("HX-Redirect"),a&&htmx.location.reload();return}if(a){t.keepIndicators=!0,htmx.location.reload();return}let l=determineHistoryUpdates(e,t),c=resolveResponseHandling(n),d=c.swap,u=!!c.error,f=htmx.config.ignoreTitle||c.ignoreTitle,m=c.select;c.target&&(t.target=resolveRetarget(e,c.target));var h=o.swapOverride;h==null&&c.swapOverride&&(h=c.swapOverride),hasHeader(n,/HX-Retarget:/i)&&(t.target=resolveRetarget(e,n.getResponseHeader("HX-Retarget"))),hasHeader(n,/HX-Reswap:/i)&&(h=n.getResponseHeader("HX-Reswap"));var g=n.response,p=mergeObjects({shouldSwap:d,serverResponse:g,isError:u,ignoreTitle:f,selectOverride:m,swapOverride:h},t);if(!(c.event&&!triggerEvent(r,c.event,p))&&triggerEvent(r,"htmx:beforeSwap",p)){if(r=p.target,g=p.serverResponse,u=p.isError,f=p.ignoreTitle,m=p.selectOverride,h=p.swapOverride,t.target=r,t.failed=u,t.successful=!u,p.shouldSwap){n.status===286&&cancelPolling(e),withExtensions(e,function(v){g=v.transformResponse(g,n,e)}),l.type&&saveCurrentPageToHistory();var y=getSwapSpecification(e,h);y.hasOwnProperty("ignoreTitle")||(y.ignoreTitle=f),r.classList.add(htmx.config.swappingClass),s&&(m=s),hasHeader(n,/HX-Reselect:/i)&&(m=n.getResponseHeader("HX-Reselect"));let w=o.selectOOB||getClosestAttributeValue(e,"hx-select-oob"),b=getClosestAttributeValue(e,"hx-select");swap(r,g,y,{select:m==="unset"?null:m||b,selectOOB:w,eventInfo:t,anchor:t.pathInfo.anchor,contextElement:e,afterSwapCallback:function(){if(hasHeader(n,/HX-Trigger-After-Swap:/i)){let v=e;bodyContains(e)||(v=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Swap",v)}},afterSettleCallback:function(){if(hasHeader(n,/HX-Trigger-After-Settle:/i)){let v=e;bodyContains(e)||(v=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Settle",v)}},beforeSwapCallback:function(){l.type&&(triggerEvent(getDocument().body,"htmx:beforeHistoryUpdate",mergeObjects({history:l},t)),l.type==="push"?(pushUrlIntoHistory(l.path),triggerEvent(getDocument().body,"htmx:pushedIntoHistory",{path:l.path})):(replaceUrlInHistory(l.path),triggerEvent(getDocument().body,"htmx:replacedInHistory",{path:l.path})))}})}u&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+n.status+" from "+t.pathInfo.requestPath},t))}}let extensions={};function extensionBase(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return!0},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return!1},handleSwap:function(e,t,n,r){return!1},encodeParameters:function(e,t,n){return null}}}function defineExtension(e,t){t.init&&t.init(internalAPI),extensions[e]=mergeObjects(extensionBase(),t)}function removeExtension(e){delete extensions[e]}function getExtensions(e,t,n){if(t==null&&(t=[]),e==null)return t;n==null&&(n=[]);let r=getAttributeValue(e,"hx-ext");return r&&forEach(r.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){n.push(o.slice(7));return}if(n.indexOf(o)<0){let s=extensions[o];s&&t.indexOf(s)<0&&t.push(s)}}),getExtensions(asElement(parentElt(e)),t,n)}var isReady=!1;getDocument().addEventListener("DOMContentLoaded",function(){isReady=!0});function ready(e){isReady||getDocument().readyState==="complete"?e():getDocument().addEventListener("DOMContentLoaded",e)}function insertIndicatorStyles(){if(htmx.config.includeIndicatorStyles!==!1){let e=htmx.config.inlineStyleNonce?` nonce="${htmx.config.inlineStyleNonce}"`:"",t=htmx.config.indicatorClass,n=htmx.config.requestClass;getDocument().head.insertAdjacentHTML("beforeend",`<style${e}>.${t}{opacity:0;visibility: hidden} .${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}</style>`)}}function getMetaConfig(){let e=getDocument().querySelector('meta[name="htmx-config"]');return e?parseJSON(e.content):null}function mergeMetaConfig(){let e=getMetaConfig();e&&(htmx.config=mergeObjects(htmx.config,e))}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let e=getDocument().body;processNode(e);let t=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(r){let o=r.detail.elt||r.target,s=getInternalData(o);s&&s.xhr&&s.xhr.abort()});let n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(r){r.state&&r.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent})})):n&&n(r)},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null},0)}),htmx})(),O=Le;(function(){let e;O.defineExtension("json-enc",{init:function(t){e=t},onEvent:function(t,n){t==="htmx:configRequest"&&(n.detail.headers["Content-Type"]="application/json")},encodeParameters:function(t,n,r){t.overrideMimeType("text/json");let o={};n.forEach(function(i,a){Object.hasOwn(o,a)?(Array.isArray(o[a])||(o[a]=[o[a]]),o[a].push(i)):o[a]=i});let s=e.getExpressionVars(r);return Object.keys(o).forEach(function(i){o[i]=Object.hasOwn(s,i)?s[i]:o[i]}),JSON.stringify(o)}})})();var ie="https://typeahead.waow.tech",le="https://public.api.bsky.app",Ie="/xrpc/app.bsky.actor.searchActorsTypeahead",He="/xrpc/app.bsky.actor.getProfiles";var Re="atcr_recent_handles",ce="atcr_recent_profile_cache";var $=class{constructor(t){this.input=t,this.container=t.closest(".sailor-typeahead")||t.parentElement,this.dropdown=null,this.selectedCard=null,this.actors=[],this.currentItems=[],this.mode="hidden",this.focusIndex=-1,this.debounceTimer=null,this.requestSeq=0,this.primaryUnhealthyUntil=0,this.lastPrefetchPrefix="",this.lastPrefetchAt=0,this.createDropdown(),this.bindEvents(),this.input.value.trim().length===0&&this.showRecent()}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="sailor-typeahead-dropdown",this.dropdown.setAttribute("role","listbox"),this.dropdown.style.display="none",this.input.insertAdjacentElement("afterend",this.dropdown)}bindEvents(){this.input.addEventListener("focus",()=>this.handleFocus()),this.input.addEventListener("input",()=>this.handleInput()),this.input.addEventListener("keydown",t=>this.handleKeydown(t)),document.addEventListener("click",t=>{!this.input.contains(t.target)&&!this.dropdown.contains(t.target)&&this.hide()}),document.addEventListener("keydown",t=>{t.key==="Escape"&&this.selectedCard&&this.clearSelection()})}handleFocus(){this.input.value.trim().length===0&&this.showRecent()}handleInput(){let t=this.input.value.trim();if(t.length===0){this.showRecent();return}if(t.length>=2&&t.length<4){this.hide(),this.schedulePrefetch(t);return}if(t.length>=4){this.scheduleSearch(t);return}this.hide()}schedulePrefetch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runPrefetch(t),150)}scheduleSearch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runSearch(t),150)}async runPrefetch(t){let n=Date.now();if(!(t===this.lastPrefetchPrefix&&n-this.lastPrefetchAt<1e4)&&!(n<this.primaryUnhealthyUntil)){this.lastPrefetchPrefix=t,this.lastPrefetchAt=n;try{await z(ie,t,400)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}}}async runSearch(t){let n=++this.requestSeq,r=null;if(Date.now()>=this.primaryUnhealthyUntil)try{r=await z(ie,t,1500)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}if(r===null)try{r=await z(le,t,1500)}catch{r=[]}n===this.requestSeq&&(this.actors=r||[],this.focusIndex=-1,this.renderResults())}renderResults(){if(this.mode="results",this.dropdown.innerHTML="",this.currentItems=[],this.actors.length===0){this.hide();return}this.actors.forEach((t,n)=>{this.currentItems.push(t),this.dropdown.appendChild(this.buildActorRow(t,n))}),this.dropdown.style.display="block"}buildActorRow(t,n){let r=document.createElement("div");r.className="sailor-typeahead-item",r.setAttribute("role","option"),r.setAttribute("aria-selected","false"),r.dataset.index=String(n),r.dataset.handle=t.handle;let o=document.createElement("div");if(o.className="sailor-typeahead-avatar",t.avatar){let l=document.createElement("img");l.src=t.avatar,l.alt="",l.loading="lazy",o.appendChild(l)}let s=document.createElement("div");s.className="sailor-typeahead-text";let i=t.displayName&&t.displayName!==t.handle;if(i){let l=document.createElement("div");l.className="sailor-typeahead-name",l.textContent=t.displayName,s.appendChild(l)}let a=document.createElement("div");return a.className=i?"sailor-typeahead-handle":"sailor-typeahead-name",a.textContent="@"+t.handle,s.appendChild(a),r.append(o,s),r.addEventListener("mousedown",l=>{l.preventDefault(),this.select(t)}),r}showRecent(){let t=Oe();if(t.length===0){this.hide();return}this.mode="recent",this.focusIndex=-1,this.renderRecent(t),this.enrichRecent(t)}renderRecent(t){let n=_();this.dropdown.innerHTML="",this.currentItems=[];let r=document.createElement("div");r.className="sailor-typeahead-header",r.textContent="Recent accounts",this.dropdown.appendChild(r),t.forEach((o,s)=>{let i=n[o]?.profile||{handle:o};this.currentItems.push(i),this.dropdown.appendChild(this.buildActorRow(i,s))}),this.dropdown.style.display="block"}async enrichRecent(t){let n=_(),r=Date.now(),o=t.filter(a=>{let l=n[a];return!l||r-l.ts>864e5});if(o.length===0)return;let s=await De(o);if(s.length===0)return;let i=_();s.forEach(a=>{i[a.handle]={ts:r,profile:{handle:a.handle,displayName:a.displayName,avatar:a.avatar}}}),ae(i),this.mode==="recent"&&this.renderRecent(t)}hide(){this.mode="hidden",this.focusIndex=-1,this.dropdown.style.display="none"}select(t){if(typeof t=="string"&&(t={handle:t}),this.input.value=t.handle,this.hide(),this.showSelectedCard(t),t.handle){let n=_();n[t.handle]={ts:Date.now(),profile:{handle:t.handle,displayName:t.displayName,avatar:t.avatar}},ae(n)}}showSelectedCard(t){this.clearSelectedCard();let n=document.createElement("div");n.className="sailor-typeahead-selected";let r=document.createElement("div");if(r.className="sailor-typeahead-avatar",t.avatar){let l=document.createElement("img");l.src=t.avatar,l.alt="",r.appendChild(l)}let o=document.createElement("div");o.className="sailor-typeahead-text";let s=t.displayName&&t.displayName!==t.handle;if(s){let l=document.createElement("div");l.className="sailor-typeahead-name",l.textContent=t.displayName,o.appendChild(l)}let i=document.createElement("div");i.className=s?"sailor-typeahead-handle":"sailor-typeahead-name",i.textContent="@"+t.handle,o.appendChild(i);let a=document.createElement("button");a.type="button",a.className="sailor-typeahead-clear",a.setAttribute("aria-label","Change account"),a.innerHTML="&times;",a.addEventListener("click",()=>this.clearSelection()),n.append(r,o,a),this.input.style.display="none",this.input.insertAdjacentElement("beforebegin",n),this.selectedCard=n}clearSelectedCard(){this.selectedCard&&(this.selectedCard.remove(),this.selectedCard=null)}clearSelection(){this.clearSelectedCard(),this.input.style.display="",this.input.value="",this.input.focus(),this.showRecent()}handleKeydown(t){if(this.mode==="hidden")return;let n=this.dropdown.querySelectorAll(".sailor-typeahead-item");n.length!==0&&(t.key==="ArrowDown"?(t.preventDefault(),this.focusIndex=(this.focusIndex+1)%n.length,this.updateFocus(n)):t.key==="ArrowUp"?(t.preventDefault(),this.focusIndex=this.focusIndex<=0?n.length-1:this.focusIndex-1,this.updateFocus(n)):t.key==="Enter"?this.focusIndex>=0&&this.currentItems[this.focusIndex]&&(t.preventDefault(),this.select(this.currentItems[this.focusIndex])):t.key==="Escape"?this.hide():t.key==="Tab"&&this.focusIndex===-1&&n.length>0&&(t.preventDefault(),this.focusIndex=0,this.updateFocus(n)))}updateFocus(t){t.forEach((n,r)=>{let o=r===this.focusIndex;n.classList.toggle("focused",o),n.setAttribute("aria-selected",o?"true":"false"),o&&n.scrollIntoView({block:"nearest"})})}destroy(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null)}};async function z(e,t,n){let r=new URL(Ie,e);r.searchParams.set("q",t),r.searchParams.set("limit",String(8));let o=new AbortController,s=setTimeout(()=>o.abort(),n);try{let i=await fetch(r,{signal:o.signal});if(!i.ok)throw new Error("HTTP "+i.status);let a=await i.json();return Array.isArray(a.actors)?a.actors:[]}finally{clearTimeout(s)}}async function De(e){if(e.length===0)return[];let t=new URL(He,le);e.forEach(o=>t.searchParams.append("actors",o));let n=new AbortController,r=setTimeout(()=>n.abort(),3e3);try{let o=await fetch(t,{signal:n.signal});if(!o.ok)return[];let s=await o.json();return Array.isArray(s.profiles)?s.profiles:[]}catch{return[]}finally{clearTimeout(r)}}function _(){try{return JSON.parse(localStorage.getItem(ce)||"{}")}catch{return{}}}function ae(e){try{localStorage.setItem(ce,JSON.stringify(e))}catch{}}function Oe(){try{let e=localStorage.getItem(Re);return e?JSON.parse(e):[]}catch{return[]}}var I=null;function ue(){let e=document.getElementById("handle");e&&(I&&I.input===e||(I&&I.destroy(),I=new $(e)))}document.addEventListener("DOMContentLoaded",ue);document.body.addEventListener("htmx:afterSettle",ue);document.body.addEventListener("htmx:beforeSwap",()=>{I&&!document.contains(I.input)&&(I.destroy(),I=null)});function Y(e){try{return localStorage.getItem(e)}catch{return null}}function K(e,t){try{localStorage.setItem(e,t)}catch{}}function me(){return Y("theme")||"system"}function Me(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function G(){let e=me(),n=Me(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),ke(e)}function ge(e){K("theme",e),G(),qe()}function ke(e){let t={system:"sun-moon",light:"sun",dark:"moon"};document.querySelectorAll("[data-theme-icon] use").forEach(n=>{n.setAttribute("href",`/icons.svg#${t[e]||"sun-moon"}`)}),document.querySelectorAll(".theme-option").forEach(n=>{let r=n.dataset.value===e;n.setAttribute("aria-checked",r?"true":"false");let o=n.querySelector(".theme-check");o&&(o.style.visibility=r?"visible":"hidden")})}function qe(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");if(!t)return;let n=()=>e.setAttribute("aria-expanded",t.open?"true":"false");n(),t.addEventListener("toggle",n)})});window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{me()==="system"&&G()});function pe(e,t){if(!e)return;let n=e.querySelector(".nav-search-form"),r=e.querySelector('button[aria-controls="nav-search-form"]');e.classList.toggle("expanded",t),n&&(t?n.removeAttribute("inert"):n.setAttribute("inert","")),r&&r.setAttribute("aria-expanded",t?"true":"false")}function Ne(){let e=document.querySelector(".nav-search-wrapper");if(!e)return;let t=!e.classList.contains("expanded");if(pe(e,t),t){let n=document.getElementById("nav-search-input");n&&n.focus()}}function de(){let e=document.querySelector(".nav-search-wrapper");if(pe(e,!1),e){let t=e.querySelector('[aria-controls="nav-search-form"]');t&&t.focus()}}document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(document.addEventListener("keydown",n=>{if(n.key==="Escape"&&e.classList.contains("expanded")&&de(),n.key==="/"&&!e.classList.contains("expanded")){let r=n.target.tagName;if(r==="INPUT"||r==="TEXTAREA"||n.target.isContentEditable)return;n.preventDefault(),e.classList.add("expanded"),t.focus()}}),document.addEventListener("click",n=>{e.classList.contains("expanded")&&!e.contains(n.target)&&de()}))});function J(e,t){let n=()=>{if(!t||!document.contains(t))return;let r=t.innerHTML;t.innerHTML='<svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#check"></use></svg> Copied!',setTimeout(()=>{document.contains(t)&&(t.innerHTML=r)},2e3)};if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(e).then(n).catch(r=>{console.error("Clipboard API failed, falling back:",r),fe(e)?n():S("Copy failed \u2014 check browser permissions","error")});return}fe(e)?n():S("Copy failed \u2014 select the text and copy manually","error")}function fe(e){let t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.setAttribute("aria-hidden","true"),t.style.position="fixed",t.style.top="0",t.style.left="0",t.style.width="1px",t.style.height="1px",t.style.opacity="0",t.style.pointerEvents="none",document.body.appendChild(t);let n=!1;try{t.focus(),t.select(),t.setSelectionRange(0,e.length),n=document.execCommand&&document.execCommand("copy")}catch{n=!1}return document.body.removeChild(t),!!n}function Pe(e){let t=s=>{let i=(s==null?"":String(s)).trim();return/[",\n\r]/.test(i)?'"'+i.replace(/"/g,'""')+'"':i},n=s=>Array.from(s).map(i=>t(i.textContent)).join(","),r=[],o=e.querySelector("thead tr");return o&&r.push(n(o.querySelectorAll("th,td"))),e.querySelectorAll("tbody tr").forEach(s=>{r.push(n(s.querySelectorAll("td,th")))}),r.join(` 2 - `)}document.addEventListener("DOMContentLoaded",()=>{document.addEventListener("click",e=>{let t=e.target.closest("button[data-copy-csv]");if(t){let r=t.closest("[data-csv-section]"),o=r&&r.querySelector("table");o&&J(Pe(o),t);return}let n=e.target.closest("button[data-cmd]");if(n){J(n.getAttribute("data-cmd"),n);return}})});function Fe(e){let t=Math.floor((new Date-new Date(e))/1e3),n={year:31536e3,month:2592e3,week:604800,day:86400,hour:3600,minute:60,second:1};for(let[r,o]of Object.entries(n)){let s=Math.floor(t/o);if(s>=1)return s===1?`1 ${r} ago`:`${s} ${r}s ago`}return"just now"}function U(){document.querySelectorAll("time[datetime]").forEach(e=>{let t=e.getAttribute("datetime");if(t&&!e.dataset.noUpdate){let n=Fe(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{U(),G(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{ge(t.dataset.value)})})}),document.addEventListener("click",e=>{let t=e.target.closest("details.dropdown");document.querySelectorAll("details.dropdown[open]").forEach(n=>{n!==t&&n.removeAttribute("open")})})});document.addEventListener("htmx:afterSwap",U);var M=null;function ye(){M===null&&(M=setInterval(U,6e4))}function Be(){M!==null&&(clearInterval(M),M=null)}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?Be():(U(),ye())});ye();async function _e(e,t,n){try{let r=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!1})});if(r.status===409){let o=await r.json();Ue(e,t,n,o.tags)}else if(r.ok)Ee(n);else{let o=await r.text();S(`Failed to delete manifest: ${o||r.status}`,"error")}}catch(r){console.error("Error deleting manifest:",r),S(`Error deleting manifest: ${r.message}`,"error")}}function Ue(e,t,n,r){let o=document.getElementById("manifest-delete-modal"),s=document.getElementById("manifest-delete-tags"),i=document.getElementById("confirm-manifest-delete-btn");s.innerHTML="",r.forEach(a=>{let l=document.createElement("li");l.textContent=a,s.appendChild(l)}),i.onclick=()=>Ve(e,t,n),Z(o)}function Q(){k(document.getElementById("manifest-delete-modal"))}async function Ve(e,t,n){let r=document.getElementById("confirm-manifest-delete-btn"),o=r.textContent;try{r.disabled=!0,r.textContent="Deleting...";let s=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!0})});if(s.ok)Q(),Ee(n),location.reload();else{let i=await s.text();S(`Failed to delete manifest: ${i||s.status}`,"error"),r.disabled=!1,r.textContent=o}}catch(s){console.error("Error deleting manifest:",s),S(`Error deleting manifest: ${s.message}`,"error"),r.disabled=!1,r.textContent=o}}async function je(e){let t=document.getElementById("confirm-untagged-delete-btn"),n=t.textContent;try{t.disabled=!0,t.textContent="Deleting...";let r=await fetch("/api/manifests/untagged",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e})}),o=await r.json();r.ok?(k(document.getElementById("untagged-delete-modal")),S(`Deleted ${o.deleted} untagged manifest(s)`,"success"),o.deleted>0&&location.reload(),t.disabled=!1,t.textContent=n):(S(`Failed to delete untagged manifests: ${o.error||"Unknown error"}`,"error"),t.disabled=!1,t.textContent=n)}catch(r){console.error("Error deleting untagged manifests:",r),S(`Error: ${r.message}`,"error"),t.disabled=!1,t.textContent=n}}function Ee(e){let t=document.getElementById(`manifest-${e}`);t&&t.remove()}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("manifest-delete-modal");e&&e.addEventListener("click",t=>{t.target===e&&Q()})});var q=new WeakMap;function Z(e,t){if(e&&(q.set(e,t||document.activeElement),typeof e.showModal=="function")){e.open&&(e.open=!1);try{e.showModal()}catch{}}}function k(e,{remove:t=!1}={}){if(!e)return;let n=q.get(e);if(q.delete(e),typeof e.close=="function"&&e.open)try{e.close()}catch{}t&&e.remove(),ve(n)}function ve(e){e&&typeof e.focus=="function"&&document.contains(e)&&e.focus()}document.addEventListener("close",e=>{let t=e.target;if(!(t instanceof HTMLDialogElement))return;let n=q.get(t);q.delete(t),ve(n)},!0);document.body.addEventListener("htmx:afterSettle",()=>{document.querySelectorAll("dialog.modal-open:not([data-modal-promoted]), dialog[open]:not([data-modal-promoted])").forEach(t=>{t.dataset.modalPromoted="1",Z(t)})});document.addEventListener("change",e=>{let t=e.target.closest("select[data-diff-url]");if(!t)return;let n=t.dataset.diffUrl;n&&(window.location.href=n.replace("__VALUE__",encodeURIComponent(t.value)))});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("pull-cmd-container");if(!e)return;let t=e.dataset.registryUrl,n=e.dataset.ownerHandle,r=e.dataset.repoName,o=e.dataset.tag||"latest",s=e.dataset.isLoggedIn==="true";function i(l){let d=(l==="none"?"":l+" pull ")+t+"/"+n+"/"+r+":"+o,u=document.getElementById("pull-cmd-display");if(!u)return;let f=u.querySelector("code");f&&(f.textContent=d);let m=u.querySelector("[data-cmd]");m&&(m.dataset.cmd=d),s&&window.htmx?window.htmx.ajax("POST","/api/profile/oci-client",{values:{oci_client:l},swap:"none"}):s||K("oci-client",l)}if(!s){let l=Y("oci-client");if(l){let c=document.getElementById("oci-client-switcher");c&&(c.value=l,i(l))}}let a=document.getElementById("oci-client-switcher");a&&a.addEventListener("change",()=>i(a.value))});document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelectorAll(".platform-tab[data-platform]");e.length&&e.forEach(t=>{t.addEventListener("click",()=>{e.forEach(r=>{let o=r===t;r.classList.toggle("btn-primary",o),r.classList.toggle("btn-ghost",!o),r.setAttribute("aria-selected",o?"true":"false"),r.setAttribute("tabindex",o?"0":"-1")}),document.querySelectorAll(".platform-content").forEach(r=>{r.classList.add("hidden"),r.setAttribute("hidden","")});let n=document.getElementById(t.dataset.platform+"-content");n&&(n.classList.remove("hidden"),n.removeAttribute("hidden"),t.focus())}),t.addEventListener("keydown",n=>{if(n.key!=="ArrowLeft"&&n.key!=="ArrowRight")return;n.preventDefault();let r=Array.from(e),o=r.indexOf(t);(n.key==="ArrowRight"?r[(o+1)%r.length]:r[(o-1+r.length)%r.length]).click()})})});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("login-form");e&&e.addEventListener("submit",()=>{let t=e.querySelector('button[type="submit"]');!t||t.disabled||(t.disabled=!0,t.innerHTML='<span class="loading loading-spinner loading-sm align-middle"></span> Navigating&hellip;')})});document.addEventListener("DOMContentLoaded",()=>{let e=document.cookie.split("; ").find(n=>n.startsWith("atcr_login_handle="));if(!e)return;let t=decodeURIComponent(e.split("=")[1]);if(t&&typeof t=="string"&&t.length>0){try{let n="atcr_recent_handles",r=Y(n),o=[];try{o=JSON.parse(r||"[]")}catch{o=[]}Array.isArray(o)||(o=[]),o=o.filter(s=>s!==t),o.unshift(t),o=o.slice(0,5),K(n,JSON.stringify(o))}catch(n){console.error("Failed to save recent account:",n)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});function he(){let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),n=document.getElementById("carousel-next");if(!e)return;let r=e.querySelectorAll(".carousel-item");if(r.length===0||!r[0])return;let o=null,s=5e3,i=window.matchMedia("(prefers-reduced-motion: reduce)"),a=()=>i.matches?"auto":"smooth",l=0,c=0;function d(){if(!r[0])return;let y=parseFloat(getComputedStyle(e).gap)||24;l=r[0].offsetWidth+y}d(),window.addEventListener("resize",()=>{cancelAnimationFrame(c),c=requestAnimationFrame(d)}),document.body.addEventListener("htmx:afterSettle",y=>{y.target&&y.target.contains&&y.target.contains(e)&&d()});function u(){let y=e.scrollWidth-e.clientWidth;e.scrollLeft>=y-10?e.scrollTo({left:0,behavior:a()}):e.scrollBy({left:l,behavior:a()})}function f(){e.scrollLeft<=10?e.scrollTo({left:e.scrollWidth,behavior:a()}):e.scrollBy({left:-l,behavior:a()})}function m(){o||document.visibilityState!=="hidden"&&(e.scrollWidth<=e.clientWidth+10||i.matches||(o=setInterval(u,s)))}function h(){o&&(clearInterval(o),o=null)}t&&t.addEventListener("click",()=>{h(),f(),m()}),n&&n.addEventListener("click",()=>{h(),u(),m()});let g=document.getElementById("carousel-pause"),p=!1;if(g){let y=g.querySelector(".carousel-pause-icon"),w=g.querySelector(".carousel-play-icon");g.setAttribute("aria-pressed","false"),g.setAttribute("aria-label","Pause carousel auto-advance"),g.addEventListener("click",()=>{p=!p,p?(h(),g.setAttribute("aria-pressed","true"),g.setAttribute("aria-label","Resume carousel auto-advance"),y&&y.classList.add("hidden"),w&&w.classList.remove("hidden")):(g.setAttribute("aria-pressed","false"),g.setAttribute("aria-label","Pause carousel auto-advance"),y&&y.classList.remove("hidden"),w&&w.classList.add("hidden"),m())})}e.addEventListener("mouseenter",h),e.addEventListener("mouseleave",()=>{p||m()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?h():p||m()}),m()}document.addEventListener("DOMContentLoaded",()=>{"requestIdleCallback"in window?requestIdleCallback(he,{timeout:2e3}):setTimeout(he,100)});document.body.addEventListener("htmx:responseError",e=>{let t=e.detail&&e.detail.elt;if(t&&t.closest&&t.closest("[data-suppress-htmx-toast]"))return;let n=e.detail&&e.detail.xhr,r=n&&n.getResponseHeader&&n.getResponseHeader("HX-Trigger");if(r&&r.indexOf("toast")!==-1)return;let o=n?n.status:0,s=o===401?"Session expired \u2014 please sign in again":o===403?"Not authorized":o===404?"Not found":o===429?"Too many requests \u2014 please slow down":o>=500?"Server error \u2014 please try again":"Something went wrong";S(s,"error")});document.body.addEventListener("htmx:sendError",e=>{let t=e.detail&&e.detail.elt;t&&t.closest&&t.closest("[data-suppress-htmx-toast]")||S("Network error \u2014 check your connection","error")});document.body.addEventListener("toast",e=>{let t=e&&e.detail||{},n=t.message||t.msg||"";if(!n)return;let r=t.type||"info";S(n,r)});var We=4,Xe=1500;function be(){let e=document.getElementById("toast-container");return e||(e=document.createElement("div"),e.id="toast-container",e.className="toast toast-end toast-bottom z-50",e.setAttribute("aria-live","polite"),e.setAttribute("aria-atomic","false"),document.body&&document.body.appendChild(e),e)}document.addEventListener("DOMContentLoaded",be);function S(e,t){let n=be(),r=(t||"info")+"|"+e,o=Date.now(),s=n.querySelector(`[data-toast-key="${$e(r)}"]`);if(s&&o-Number(s.dataset.toastAt)<Xe){ze(s);return}let i=t==="error",a=i?"alert-error":"alert-success",l=document.createElement("div");l.className=`alert ${a} shadow-lg transition-opacity duration-300`,l.style.willChange="opacity",l.setAttribute("role",i?"alert":"status"),l.dataset.toastKey=r,l.dataset.toastAt=String(o);let c=document.createElement("span");for(c.textContent=e,l.appendChild(c),n.appendChild(l);n.children.length>We;)n.firstElementChild.remove();we(l)}function we(e){e._dismissTimer=setTimeout(()=>{e.style.opacity="0",e._removeTimer=setTimeout(()=>e.remove(),300)},3e3)}function ze(e){clearTimeout(e._dismissTimer),clearTimeout(e._removeTimer),e.style.opacity="",e.dataset.toastAt=String(Date.now()),we(e)}function $e(e){return window.CSS&&CSS.escape?CSS.escape(e):String(e).replace(/[^a-zA-Z0-9_-]/g,t=>"\\"+t)}async function Je(e){try{let t=await fetch(`/api/webhooks/${e}/test`,{method:"POST",credentials:"include"}),n=await t.text();n.includes('class="success"')||t.ok&&!n.includes('class="error"')?S("Test webhook delivered successfully!","success"):S("Test delivery failed \u2014 check the webhook URL","error")}catch{S("Failed to reach server","error")}}(function(){let t={"switch-repo-tab":s=>window.switchRepoTab&&window.switchRepoTab(s.dataset.tab),"switch-editor-tab":s=>window.switchEditorTab&&window.switchEditorTab(s.dataset.tab),"insert-md":s=>window.insertMd&&window.insertMd(s.dataset.mdType),"toggle-editor":s=>window.toggleOverviewEditor&&window.toggleOverviewEditor(s.dataset.show==="true"),"show-modal":s=>Z(document.getElementById(s.dataset.modalId),s),"close-dialog":s=>k(s.closest("dialog")),"remove-closest-dialog":s=>k(s.closest("dialog"),{remove:!0}),"close-manifest-delete-modal":()=>window.closeManifestDeleteModal&&window.closeManifestDeleteModal(),"save-overview":()=>window.saveOverview&&window.saveOverview(),"delete-manifest":s=>window.deleteManifest&&window.deleteManifest(s.dataset.repo,s.dataset.digest,s.dataset.manifestId||""),"delete-untagged":s=>window.deleteUntaggedManifests&&window.deleteUntaggedManifests(s.dataset.repo),copy:s=>window.copyToClipboard&&window.copyToClipboard(s.dataset.copy,s),"toggle-search":()=>window.toggleSearch&&window.toggleSearch(),"switch-settings-tab":s=>window.switchSettingsTab&&window.switchSettingsTab(s.dataset.tab),"test-webhook":s=>window.testWebhook&&window.testWebhook(s.dataset.webhookId),"diff-to":(s,i)=>window.diffToTag&&window.diffToTag(i,s),"modal-backdrop-close":(s,i)=>{i.target===s&&k(s,{remove:!0})}},n={"sort-tags":s=>window.sortTags&&window.sortTags(s.value),"submit-form":s=>s.form&&s.form.requestSubmit()},r={"filter-tags":s=>window.filterTags&&window.filterTags(s.value)};function o(s,i){let a=i.target.closest("[data-action]");if(!a)return;let l=s[a.dataset.action];l&&l(a,i)}document.addEventListener("click",s=>o(t,s)),document.addEventListener("change",s=>o(n,s)),document.addEventListener("input",s=>o(r,s))})();window.setTheme=ge;window.toggleSearch=Ne;window.copyToClipboard=J;window.deleteManifest=_e;window.deleteUntaggedManifests=je;window.closeManifestDeleteModal=Q;window.showToast=S;window.testWebhook=Je;function Ye(){let e=document.getElementById("md-editor");if(!e)return;let t=e.dataset.ownerDid,n=e.dataset.repository;window.toggleOverviewEditor=function(r){document.getElementById("overview-view").classList.toggle("hidden",r),document.getElementById("overview-edit").classList.toggle("hidden",!r),r&&e.focus()},window.switchEditorTab=function(r){if(document.querySelectorAll(".editor-panel").forEach(o=>o.classList.add("hidden")),document.getElementById(r==="write"?"editor-write":"editor-preview").classList.remove("hidden"),document.querySelectorAll(".editor-tab").forEach(o=>{let s=o.dataset.tab===r;o.classList.toggle("border-primary",s),o.classList.toggle("text-primary",s),o.classList.toggle("border-transparent",!s),o.classList.toggle("text-base-content/60",!s)}),r==="preview"){let o=e.value,s=document.getElementById("preview-content");if(!o.trim()){s.innerHTML='<p class="text-base-content/60">Nothing to preview</p>';return}s.innerHTML='<p class="text-base-content/60"><span class="loading loading-spinner loading-xs align-middle"></span> Rendering preview&hellip;</p>';let i=new FormData;i.append("markdown",o),fetch("/api/repo-page/preview",{method:"POST",body:i}).then(a=>{if(!a.ok)throw new Error("HTTP "+a.status);return a.text()}).then(a=>{s.innerHTML=a}).catch(()=>{s.innerHTML='<p class="text-error">Preview failed. Check your connection and try again.</p>'})}},window.insertMd=function(r){let o=e.selectionStart,s=e.selectionEnd,i=e.value.substring(o,s),a=e.value.substring(0,o),l=e.value.substring(s),c,d,u;switch(r){case"heading":c="## "+(i||"Heading"),d=o+3,u=o+c.length;break;case"bold":c="**"+(i||"bold text")+"**",d=o+2,u=o+c.length-2;break;case"italic":c="_"+(i||"italic text")+"_",d=o+1,u=o+c.length-1;break;case"link":c="["+(i||"link text")+"](url)",d=o+c.length-4,u=o+c.length-1;break;case"image":c="!["+(i||"alt text")+"](url)",d=o+c.length-4,u=o+c.length-1;break;case"ul":c="- "+(i||"list item"),d=o+2,u=o+c.length;break;case"ol":c="1. "+(i||"list item"),d=o+3,u=o+c.length;break;case"code":i&&i.indexOf(` 3 - `)!==-1?(c="```\n"+i+"\n```",d=o+4,u=o+4+i.length):(c="`"+(i||"code")+"`",d=o+1,u=o+c.length-1);break;default:return}e.value=a+c+l,e.focus(),e.selectionStart=d,e.selectionEnd=u},window.saveOverview=function(){let r=document.getElementById("save-overview-btn");r.classList.add("btn-disabled"),r.innerHTML='<span class="loading loading-spinner loading-xs"></span> Saving...';let o=new FormData;o.append("did",t),o.append("repository",n),o.append("description",e.value),fetch("/api/repo-page",{method:"POST",body:o,headers:{"HX-Request":"true"}}).then(s=>s.ok?s.text():s.text().then(i=>{throw new Error(i)})).then(s=>{document.getElementById("overview-rendered").innerHTML=s,window.toggleOverviewEditor(!1),typeof window.showToast=="function"&&window.showToast("Overview saved","success")}).catch(s=>{typeof window.showToast=="function"&&window.showToast(s.message||"Failed to save","error")}).finally(()=>{r.classList.remove("btn-disabled"),r.innerHTML="Save"})},e.addEventListener("keydown",r=>{(r.ctrlKey||r.metaKey)&&r.key==="s"&&(r.preventDefault(),window.saveOverview())})}window.sortTags=function(e){let t=document.getElementById("tags-list");if(!t)return;let n=Array.from(t.querySelectorAll(".artifact-entry"));n.sort((r,o)=>{switch(e){case"oldest":return parseInt(r.dataset.created)-parseInt(o.dataset.created);case"az":return r.dataset.tag.localeCompare(o.dataset.tag);case"za":return o.dataset.tag.localeCompare(r.dataset.tag);default:return parseInt(o.dataset.created)-parseInt(r.dataset.created)}}),n.forEach(r=>t.appendChild(r))};var D=0;window.filterTags=function(e){D&&cancelAnimationFrame(D),D=requestAnimationFrame(()=>{D=0;let t=e.toLowerCase();document.querySelectorAll("#tags-list .artifact-entry").forEach(n=>{n.style.display=!t||n.dataset.tag.toLowerCase().includes(t)?"":"none"})})};document.body.addEventListener("htmx:beforeSwap",()=>{D&&(cancelAnimationFrame(D),D=0)});function Ke(){if(!document.getElementById("tag-content"))return;let e=["overview","layers","vulns","sbom","artifacts"],t={};function n(i,a){if(t[i]==="loading"||t[i]==="loaded")return;t[i]="loading";let l=document.getElementById(i);if(!l){delete t[i];return}let c=new AbortController,d=setTimeout(()=>c.abort(),1e4);fetch(a,{signal:c.signal}).then(u=>{if(!u.ok)throw new Error("HTTP "+u.status);return u.text()}).then(u=>{t[i]="loaded",document.contains(l)&&(l.innerHTML=u,l.querySelectorAll("script").forEach(f=>{let m=document.createElement("script");m.textContent=f.textContent,f.parentNode.replaceChild(m,f)}),typeof window.htmx<"u"&&window.htmx.process(l))}).catch(u=>{if(delete t[i],!document.contains(l))return;let m=u&&u.name==="AbortError"?"This section took too long to load.":"Couldn't load this section.";l.innerHTML='<div class="py-6 text-sm text-base-content/70"><p>'+m+'</p><button type="button" class="btn btn-sm btn-ghost mt-2" data-retry-section="'+i+'">Try again</button></div>'}).finally(()=>clearTimeout(d))}document.body.addEventListener("click",i=>{let a=i.target.closest("[data-retry-section]");if(!a)return;let l=a.getAttribute("data-retry-section"),d={"artifacts-content":o,"layers-content":()=>r("layers"),"vulns-content":()=>r("vulns"),"sbom-content":()=>r("sbom")}[l];if(d){let u=d();u&&n(l,u)}});function r(i){let a=document.getElementById("tag-content");if(!a||!a.dataset)return null;let l=a.dataset.digest,c=a.dataset.owner,d=a.dataset.repo;return!l||!c||!d?null:"/api/digest-content/"+c+"/"+d+"?digest="+encodeURIComponent(l)+"&section="+i}function o(){let i=document.getElementById("tag-content");if(!i||!i.dataset)return null;let a=i.dataset.owner,l=i.dataset.repo;return!a||!l?null:"/api/repo-tags/"+a+"/"+l}window.diffToTag=function(i,a){i.preventDefault();let l=a.dataset.diffTo,c=document.getElementById("tag-content"),d=document.getElementById("tag-selector");if(!c||!d||!l)return;let u=c.dataset.digest,f=d.value;!u||l===f||(window.location.href="/diff/"+c.dataset.owner+"/"+c.dataset.repo+"?from="+encodeURIComponent(u)+"&to="+encodeURIComponent(l))},window.switchRepoTab=function(i){window._activeRepoTab=i;let a=document.getElementById("tag-content");if(!a)return;a.querySelectorAll(".repo-panel").forEach(d=>d.classList.add("hidden"));let l=document.getElementById("tab-"+i);l&&l.classList.remove("hidden"),a.querySelectorAll(".repo-tab").forEach(d=>{let u=d.dataset.tab===i;d.classList.toggle("border-primary",u),d.classList.toggle("text-primary",u),d.classList.toggle("border-transparent",!u),d.classList.toggle("text-base-content/60",!u),d.setAttribute("aria-selected",u?"true":"false"),d.setAttribute("tabindex",u?"0":"-1")});let c=new URL(window.location);if(c.hash=i,history.replaceState(null,"",c.toString()),i==="artifacts"){let d=o();d&&n("artifacts-content",d)}if(i==="layers"){let d=r("layers");d&&n("layers-content",d)}if(i==="vulns"){let d=r("vulns");d&&n("vulns-content",d)}if(i==="sbom"){let d=r("sbom");d&&n("sbom-content",d)}};function s(){t={},[["artifacts-tab-btn","artifacts-content",o],["layers-tab-btn","layers-content",()=>r("layers")],["vulns-tab-btn","vulns-content",()=>r("vulns")],["sbom-tab-btn","sbom-content",()=>r("sbom")]].forEach(([c,d,u])=>{let f=document.getElementById(c);f&&f.addEventListener("mouseenter",()=>{let m=u();m&&n(d,m)},{once:!0})});let a=document.querySelector('[role="tablist"][aria-label="Repository sections"]');a&&!a.dataset.keyboardBound&&(a.dataset.keyboardBound="1",a.addEventListener("keydown",c=>{let d=Array.from(a.querySelectorAll(".repo-tab")),u=d.indexOf(document.activeElement);if(u===-1)return;let f=-1;switch(c.key){case"ArrowRight":f=(u+1)%d.length;break;case"ArrowLeft":f=(u-1+d.length)%d.length;break;case"Home":f=0;break;case"End":f=d.length-1;break;case"Enter":case" ":c.preventDefault(),window.switchRepoTab(d[u].dataset.tab);return;default:return}c.preventDefault(),d[f].focus()}));let l=window._activeRepoTab||window.location.hash.replace("#","")||"overview";e.indexOf(l)===-1&&(l="overview"),window.switchRepoTab(l)}s(),document.addEventListener("keydown",i=>{if(i.target.tagName==="INPUT"||i.target.tagName==="TEXTAREA"||i.target.tagName==="SELECT"||i.target.isContentEditable||i.ctrlKey||i.metaKey||i.altKey)return;let l={o:"overview",l:"layers",v:"vulns",s:"sbom",a:"artifacts"}[i.key.toLowerCase()];l&&e.indexOf(l)!==-1&&window.switchRepoTab(l)}),document.body.addEventListener("htmx:afterSettle",i=>{i.detail.target&&i.detail.target.id==="tag-content"&&s()})}document.addEventListener("DOMContentLoaded",()=>{Ye(),Ke()});function Ge(){let e=Array.from(document.querySelectorAll('.menu li[data-tab] a[role="tab"]')),t=Array.from(document.querySelectorAll(".settings-tab-mobile"));if(!e.length&&!t.length)return;function n(s,i){let a=i==="vertical"?"ArrowUp":"ArrowLeft",l=i==="vertical"?"ArrowDown":"ArrowRight";s.forEach(c=>{c.addEventListener("keydown",d=>{let u=s.indexOf(d.currentTarget);if(u===-1)return;let f=null;d.key===a?f=s[(u-1+s.length)%s.length]:d.key===l?f=s[(u+1)%s.length]:d.key==="Home"?f=s[0]:d.key==="End"&&(f=s[s.length-1]),f&&(d.preventDefault(),f.focus(),f.click())})})}n(e,"vertical"),n(t,"horizontal");function r(){let s=t.find(i=>i.getAttribute("aria-selected")==="true");s&&s.scrollIntoView({inline:"center",block:"nearest"})}r();function o(s){e.forEach(i=>{let a=i.parentElement.dataset.tab===s;i.setAttribute("aria-selected",a?"true":"false"),i.setAttribute("tabindex",a?"0":"-1"),i.parentElement.classList.toggle("menu-active",a)}),t.forEach(i=>{let a=i.dataset.tab===s;i.setAttribute("aria-selected",a?"true":"false"),i.setAttribute("tabindex",a?"0":"-1"),i.classList.toggle("btn-secondary",a),i.classList.toggle("btn-ghost",!a)}),r()}[...e,...t].forEach(s=>{s.addEventListener("click",()=>o(s.dataset.tab||s.parentElement.dataset.tab))}),document.body.addEventListener("htmx:historyRestore",()=>{let s=location.pathname.match(/^\/settings\/(user|storage|billing|devices|webhooks|advanced)/);s&&o(s[1])})}function Qe(){document.addEventListener("click",function(n){let r=n.target.closest("#delete-account-btn");r&&t(r)});function e(n){let r=document.createElement("div");return r.textContent=n,r.innerHTML}function t(n){let r=n.dataset.clientShortName||"this account",s="DELETE "+(n.dataset.profileHandle||""),i=document.getElementById("delete-pds-records").checked,a=document.createElement("div");a.className="modal modal-open",a.innerHTML=` 1 + var He=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){return getInputValues(e,t||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0,historyRestoreAsHxRequest:!0,reportValidityOfForms:!1},parseInterval:null,location,_:null,version:"2.0.8"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function parseInterval(e){if(e==null)return;let t=NaN;return e.slice(-2)=="ms"?t=parseFloat(e.slice(0,-2)):e.slice(-1)=="s"?t=parseFloat(e.slice(0,-1))*1e3:e.slice(-1)=="m"?t=parseFloat(e.slice(0,-1))*1e3*60:t=parseFloat(e),isNaN(t)?void 0:t}function getRawAttribute(e,t){return e instanceof Element&&e.getAttribute(t)}function hasAttribute(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function getAttributeValue(e,t){return getRawAttribute(e,t)||getRawAttribute(e,"data-"+t)}function parentElt(e){let t=e.parentElement;return!t&&e.parentNode instanceof ShadowRoot?e.parentNode:t}function getDocument(){return document}function getRootNode(e,t){return e.getRootNode?e.getRootNode({composed:t}):getDocument()}function getClosestMatch(e,t){for(;e&&!t(e);)e=parentElt(e);return e||null}function getAttributeValueWithDisinheritance(e,t,n){let r=getAttributeValue(t,n),o=getAttributeValue(t,"hx-disinherit");var s=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return s&&(s==="*"||s.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return"unset"}return r}function getClosestAttributeValue(e,t){let n=null;if(getClosestMatch(e,function(r){return!!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}),n!=="unset")return n}function matches(e,t){return e instanceof Element&&e.matches(t)}function getStartTag(e){let n=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return n?n[1].toLowerCase():""}function parseHTML(e){return"parseHTMLUnsafe"in Document?Document.parseHTMLUnsafe(e):new DOMParser().parseFromString(e,"text/html")}function takeChildrenFor(e,t){for(;t.childNodes.length>0;)e.append(t.childNodes[0])}function duplicateScript(e){let t=getDocument().createElement("script");return forEach(e.attributes,function(n){t.setAttribute(n.name,n.value)}),t.textContent=e.textContent,t.async=!1,htmx.config.inlineScriptNonce&&(t.nonce=htmx.config.inlineScriptNonce),t}function isJavaScriptScriptNode(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function normalizeScriptTags(e){Array.from(e.querySelectorAll("script")).forEach(t=>{if(isJavaScriptScriptNode(t)){let n=duplicateScript(t),r=t.parentNode;try{r.insertBefore(n,t)}catch(o){logError(o)}finally{t.remove()}}})}function makeFragment(e){let t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,""),n=getStartTag(t),r;if(n==="html"){r=new DocumentFragment;let s=parseHTML(e);takeChildrenFor(r,s.body),r.title=s.title}else if(n==="body"){r=new DocumentFragment;let s=parseHTML(t);takeChildrenFor(r,s.body),r.title=s.title}else{let s=parseHTML('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=s.querySelector("template").content,r.title=s.title;var o=r.querySelector("title");o&&o.parentNode===r&&(o.remove(),r.title=o.innerText)}return r&&(htmx.config.allowScriptTags?normalizeScriptTags(r):r.querySelectorAll("script").forEach(s=>s.remove())),r}function maybeCall(e){e&&e()}function isType(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function isFunction(e){return typeof e=="function"}function isRawObject(e){return isType(e,"Object")}function getInternalData(e){let t="htmx-internal-data",n=e[t];return n||(n=e[t]={}),n}function toArray(e){let t=[];if(e)for(let n=0;n<e.length;n++)t.push(e[n]);return t}function forEach(e,t){if(e)for(let n=0;n<e.length;n++)t(e[n])}function isScrolledIntoView(e){let t=e.getBoundingClientRect(),n=t.top,r=t.bottom;return n<window.innerHeight&&r>=0}function bodyContains(e){return e.getRootNode({composed:!0})===document}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function parseJSON(e){try{return JSON.parse(e)}catch(t){return logError(t),null}}function canAccessLocalStorage(){let e="htmx:sessionStorageTest";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch{return!1}}function normalizePath(e){let t=new URL(e,"http://x");return t&&(e=t.pathname+t.search),e!="/"&&(e=e.replace(/\/+$/,"")),e}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(e){return htmx.on("htmx:load",function(n){e(n.detail.elt)})}function logAll(){htmx.logger=function(e,t,n){console&&console.log(t,e,n)}}function logNone(){htmx.logger=null}function find(e,t){return typeof e!="string"?e.querySelector(t):find(getDocument(),e)}function findAll(e,t){return typeof e!="string"?e.querySelectorAll(t):findAll(getDocument(),e)}function getWindow(){return window}function removeElement(e,t){e=resolveTarget(e),t?getWindow().setTimeout(function(){removeElement(e),e=null},t):parentElt(e).removeChild(e)}function asElement(e){return e instanceof Element?e:null}function asHtmlElement(e){return e instanceof HTMLElement?e:null}function asString(e){return typeof e=="string"?e:null}function asParentNode(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function addClassToElement(e,t,n){e=asElement(resolveTarget(e)),e&&(n?getWindow().setTimeout(function(){addClassToElement(e,t),e=null},n):e.classList&&e.classList.add(t))}function removeClassFromElement(e,t,n){let r=asElement(resolveTarget(e));r&&(n?getWindow().setTimeout(function(){removeClassFromElement(r,t),r=null},n):r.classList&&(r.classList.remove(t),r.classList.length===0&&r.removeAttribute("class")))}function toggleClassOnElement(e,t){e=resolveTarget(e),e.classList.toggle(t)}function takeClassForElement(e,t){e=resolveTarget(e),forEach(e.parentElement.children,function(n){removeClassFromElement(n,t)}),addClassToElement(asElement(e),t)}function closest(e,t){return e=asElement(resolveTarget(e)),e?e.closest(t):null}function startsWith(e,t){return e.substring(0,t.length)===t}function endsWith(e,t){return e.substring(e.length-t.length)===t}function normalizeSelector(e){let t=e.trim();return startsWith(t,"<")&&endsWith(t,"/>")?t.substring(1,t.length-2):t}function querySelectorAllExt(e,t,n){if(t.indexOf("global ")===0)return querySelectorAllExt(e,t.slice(7),!0);e=resolveTarget(e);let r=[];{let i=0,a=0;for(let l=0;l<t.length;l++){let c=t[l];if(c===","&&i===0){r.push(t.substring(a,l)),a=l+1;continue}c==="<"?i++:c==="/"&&l<t.length-1&&t[l+1]===">"&&i--}a<t.length&&r.push(t.substring(a))}let o=[],s=[];for(;r.length>0;){let i=normalizeSelector(r.shift()),a;i.indexOf("closest ")===0?a=closest(asElement(e),normalizeSelector(i.slice(8))):i.indexOf("find ")===0?a=find(asParentNode(e),normalizeSelector(i.slice(5))):i==="next"||i==="nextElementSibling"?a=asElement(e).nextElementSibling:i.indexOf("next ")===0?a=scanForwardQuery(e,normalizeSelector(i.slice(5)),!!n):i==="previous"||i==="previousElementSibling"?a=asElement(e).previousElementSibling:i.indexOf("previous ")===0?a=scanBackwardsQuery(e,normalizeSelector(i.slice(9)),!!n):i==="document"?a=document:i==="window"?a=window:i==="body"?a=document.body:i==="root"?a=getRootNode(e,!!n):i==="host"?a=e.getRootNode().host:s.push(i),a&&o.push(a)}if(s.length>0){let i=s.join(","),a=asParentNode(getRootNode(e,!!n));o.push(...toArray(a.querySelectorAll(i)))}return o}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o<r.length;o++){let s=r[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return s}},scanBackwardsQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=r.length-1;o>=0;o--){let s=r[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return s}};function querySelectorExt(e,t){return typeof e!="string"?querySelectorAllExt(e,t)[0]:querySelectorAllExt(getDocument().body,e)[0]}function resolveTarget(e,t){return typeof e=="string"?find(asParentNode(t)||document,e):e}function processEventArgs(e,t,n,r){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t,options:n}:{target:resolveTarget(e),event:asString(t),listener:n,options:r}}function addEventListenerImpl(e,t,n,r){return ready(function(){let s=processEventArgs(e,t,n,r);s.target.addEventListener(s.event,s.listener,s.options)}),isFunction(t)?t:n}function removeEventListenerImpl(e,t,n){return ready(function(){let r=processEventArgs(e,t,n);r.target.removeEventListener(r.event,r.listener)}),isFunction(t)?t:n}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let n=getClosestAttributeValue(e,t);if(n){if(n==="this")return[findThisElement(e,t)];{let r=querySelectorAllExt(e,n);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(n)){let s=asElement(getClosestMatch(e,function(i){return i!==e&&hasAttribute(asElement(i),t)}));s&&r.push(...findAttributeTargets(s,t))}return r.length===0?(logError('The selector "'+n+'" on '+t+" returned no matches!"),[DUMMY_ELT]):r}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(n){return getAttributeValue(asElement(n),t)!=null}))}function getTarget(e){let t=getClosestAttributeValue(e,"hx-target");return t?t==="this"?findThisElement(e,"hx-target"):querySelectorExt(e,t):getInternalData(e).boosted?getDocument().body:e}function shouldSettleAttribute(e){return htmx.config.attributesToSettle.includes(e)}function cloneAttributes(e,t){forEach(Array.from(e.attributes),function(n){!t.hasAttribute(n.name)&&shouldSettleAttribute(n.name)&&e.removeAttribute(n.name)}),forEach(t.attributes,function(n){shouldSettleAttribute(n.name)&&e.setAttribute(n.name,n.value)})}function isInlineSwap(e,t){let n=getExtensions(t);for(let r=0;r<n.length;r++){let o=n[r];try{if(o.isInlineSwap(e))return!0}catch(s){logError(s)}}return e==="outerHTML"}function oobSwap(e,t,n,r){r=r||getDocument();let o="#"+CSS.escape(getRawAttribute(t,"id")),s="outerHTML";e==="true"||(e.indexOf(":")>0?(s=e.substring(0,e.indexOf(":")),o=e.substring(e.indexOf(":")+1)):s=e),t.removeAttribute("hx-swap-oob"),t.removeAttribute("data-hx-swap-oob");let i=querySelectorAllExt(r,o,!1);return i.length?(forEach(i,function(a){let l,c=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(s,a)||(l=asParentNode(c));let u={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",u)&&(a=u.target,u.shouldSwap&&(handlePreservedElements(l),swapWithStyle(s,a,a,l,n),restorePreservedElements()),forEach(n.elts,function(d){triggerEvent(d,"htmx:oobAfterSwap",u)}))}),t.parentNode.removeChild(t)):(t.parentNode.removeChild(t),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:t})),e}function restorePreservedElements(){let e=find("#--htmx-preserve-pantry--");if(e){for(let t of[...e.children]){let n=find("#"+t.id);n.parentNode.moveBefore(t,n),n.remove()}e.remove()}}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let n=getAttributeValue(t,"id"),r=getDocument().getElementById(n);if(r!=null)if(t.moveBefore){let o=find("#--htmx-preserve-pantry--");o==null&&(getDocument().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>"),o=find("#--htmx-preserve-pantry--")),o.moveBefore(r,null)}else t.parentNode.replaceChild(r,t)})}function handleAttributes(e,t,n){forEach(t.querySelectorAll("[id]"),function(r){let o=getRawAttribute(r,"id");if(o&&o.length>0){let s=o.replace("'","\\'"),i=r.tagName.replace(":","\\:"),a=asParentNode(e),l=a&&a.querySelector(i+"[id='"+s+"']");if(l&&l!==a){let c=r.cloneNode();cloneAttributes(r,l),n.tasks.push(function(){cloneAttributes(r,c)})}}})}function makeAjaxLoadTask(e){return function(){removeClassFromElement(e,htmx.config.addedClass),processNode(asElement(e)),processFocus(asParentNode(e)),triggerEvent(e,"htmx:load")}}function processFocus(e){let t="[autofocus]",n=asHtmlElement(matches(e,t)?e:e.querySelector(t));n?.focus()}function insertNodesBefore(e,t,n,r){for(handleAttributes(e,n,r);n.childNodes.length>0;){let o=n.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&r.tasks.push(makeAjaxLoadTask(o))}}function stringHash(e,t){let n=0;for(;n<e.length;)t=(t<<5)-t+e.charCodeAt(n++)|0;return t}function attributeHash(e){let t=0;for(let n=0;n<e.attributes.length;n++){let r=e.attributes[n];r.value&&(t=stringHash(r.name,t),t=stringHash(r.value,t))}return t}function deInitOnHandlers(e){let t=getInternalData(e);if(t.onHandlers){for(let n=0;n<t.onHandlers.length;n++){let r=t.onHandlers[n];removeEventListenerImpl(e,r.event,r.listener)}delete t.onHandlers}}function deInitNode(e){let t=getInternalData(e);t.timeout&&clearTimeout(t.timeout),t.listenerInfos&&forEach(t.listenerInfos,function(n){n.on&&removeEventListenerImpl(n.on,n.trigger,n.listener)}),deInitOnHandlers(e),forEach(Object.keys(t),function(n){n!=="firstInitCompleted"&&delete t[n]})}function cleanUpElement(e){triggerEvent(e,"htmx:beforeCleanupElement"),deInitNode(e),forEach(e.children,function(t){cleanUpElement(t)})}function swapOuterHTML(e,t,n){if(e.tagName==="BODY")return swapInnerHTML(e,t,n);let r,o=e.previousSibling,s=parentElt(e);if(s){for(insertNodesBefore(s,e,t,n),o==null?r=s.firstChild:r=o.nextSibling,n.elts=n.elts.filter(function(i){return i!==e});r&&r!==e;)r instanceof Element&&n.elts.push(r),r=r.nextSibling;cleanUpElement(e),e.remove()}}function swapAfterBegin(e,t,n){return insertNodesBefore(e,e.firstChild,t,n)}function swapBeforeBegin(e,t,n){return insertNodesBefore(parentElt(e),e,t,n)}function swapBeforeEnd(e,t,n){return insertNodesBefore(e,null,t,n)}function swapAfterEnd(e,t,n){return insertNodesBefore(parentElt(e),e.nextSibling,t,n)}function swapDelete(e){cleanUpElement(e);let t=parentElt(e);if(t)return t.removeChild(e)}function swapInnerHTML(e,t,n){let r=e.firstChild;if(insertNodesBefore(e,r,t,n),r){for(;r.nextSibling;)cleanUpElement(r.nextSibling),e.removeChild(r.nextSibling);cleanUpElement(r),e.removeChild(r)}}function swapWithStyle(e,t,n,r,o){switch(e){case"none":return;case"outerHTML":swapOuterHTML(n,r,o);return;case"afterbegin":swapAfterBegin(n,r,o);return;case"beforebegin":swapBeforeBegin(n,r,o);return;case"beforeend":swapBeforeEnd(n,r,o);return;case"afterend":swapAfterEnd(n,r,o);return;case"delete":swapDelete(n);return;default:var s=getExtensions(t);for(let i=0;i<s.length;i++){let a=s[i];try{let l=a.handleSwap(e,n,r,o);if(l){if(Array.isArray(l))for(let c=0;c<l.length;c++){let u=l[c];u.nodeType!==Node.TEXT_NODE&&u.nodeType!==Node.COMMENT_NODE&&o.tasks.push(makeAjaxLoadTask(u))}return}}catch(l){logError(l)}}e==="innerHTML"?swapInnerHTML(n,r,o):swapWithStyle(htmx.config.defaultSwapStyle,t,n,r,o)}}function findAndSwapOobElements(e,t,n){var r=findAll(e,"[hx-swap-oob], [data-hx-swap-oob]");return forEach(r,function(o){if(htmx.config.allowNestedOobSwaps||o.parentElement===null){let s=getAttributeValue(o,"hx-swap-oob");s!=null&&oobSwap(s,o,t,n)}else o.removeAttribute("hx-swap-oob"),o.removeAttribute("data-hx-swap-oob")}),r.length>0}function swap(e,t,n,r){r||(r={});let o=null,s=null,i=function(){maybeCall(r.beforeSwapCallback),e=resolveTarget(e);let c=r.contextElement?getRootNode(r.contextElement,!1):getDocument(),u=document.activeElement,d={};d={elt:u,start:u?u.selectionStart:null,end:u?u.selectionEnd:null};let f=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else{let h=makeFragment(t);if(f.title=r.title||h.title,r.historyRequest&&(h=h.querySelector("[hx-history-elt],[data-hx-history-elt]")||h),r.selectOOB){let g=r.selectOOB.split(",");for(let p=0;p<g.length;p++){let y=g[p].split(":",2),w=y[0].trim();w.indexOf("#")===0&&(w=w.substring(1));let b=y[1]||"true",v=h.querySelector("#"+w);v&&oobSwap(b,v,f,c)}}if(findAndSwapOobElements(h,f,c),forEach(findAll(h,"template"),function(g){g.content&&findAndSwapOobElements(g.content,f,c)&&g.remove()}),r.select){let g=getDocument().createDocumentFragment();forEach(h.querySelectorAll(r.select),function(p){g.appendChild(p)}),h=g}handlePreservedElements(h),swapWithStyle(n.swapStyle,r.contextElement,e,h,f),restorePreservedElements()}if(d.elt&&!bodyContains(d.elt)&&getRawAttribute(d.elt,"id")){let h=document.getElementById(getRawAttribute(d.elt,"id")),g={preventScroll:n.focusScroll!==void 0?!n.focusScroll:!htmx.config.defaultFocusScroll};if(h){if(d.start&&h.setSelectionRange)try{h.setSelectionRange(d.start,d.end)}catch{}h.focus(g)}}e.classList.remove(htmx.config.swappingClass),forEach(f.elts,function(h){h.classList&&h.classList.add(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSwap",r.eventInfo)}),maybeCall(r.afterSwapCallback),n.ignoreTitle||handleTitle(f.title);let m=function(){if(forEach(f.tasks,function(h){h.call()}),forEach(f.elts,function(h){h.classList&&h.classList.remove(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSettle",r.eventInfo)}),r.anchor){let h=asElement(resolveTarget("#"+r.anchor));h&&h.scrollIntoView({block:"start",behavior:"auto"})}updateScrollState(f.elts,n),maybeCall(r.afterSettleCallback),maybeCall(o)};n.settleDelay>0?getWindow().setTimeout(m,n.settleDelay):m()},a=htmx.config.globalViewTransitions;n.hasOwnProperty("transition")&&(a=n.transition);let l=r.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",r.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let c=new Promise(function(d,f){o=d,s=f}),u=i;i=function(){document.startViewTransition(function(){return u(),c})}}try{n?.swapDelay&&n.swapDelay>0?getWindow().setTimeout(i,n.swapDelay):i()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",r.eventInfo),maybeCall(s),c}}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let s in o)if(o.hasOwnProperty(s)){let i=o[s];isRawObject(i)?n=i.target!==void 0?i.target:n:i={value:i},triggerEvent(n,s,i)}}else{let o=r.split(",");for(let s=0;s<o.length;s++)triggerEvent(n,o[s].trim(),[])}}let WHITESPACE=/\s/,WHITESPACE_OR_COMMA=/[\s,]/,SYMBOL_START=/[_$a-zA-Z]/,SYMBOL_CONT=/[_$a-zA-Z0-9]/,STRINGISH_START=['"',"'","/"],NOT_WHITESPACE=/[^\s]/,COMBINED_SELECTOR_START=/[{(]/,COMBINED_SELECTOR_END=/[})]/;function tokenizeString(e){let t=[],n=0;for(;n<e.length;){if(SYMBOL_START.exec(e.charAt(n))){for(var r=n;SYMBOL_CONT.exec(e.charAt(n+1));)n++;t.push(e.substring(r,n+1))}else if(STRINGISH_START.indexOf(e.charAt(n))!==-1){let o=e.charAt(n);var r=n;for(n++;n<e.length&&e.charAt(n)!==o;)e.charAt(n)==="\\"&&n++,n++;t.push(e.substring(r,n+1))}else{let o=e.charAt(n);t.push(o)}n++}return t}function isPossibleRelativeReference(e,t,n){return SYMBOL_START.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function maybeGenerateConditional(e,t,n){if(t[0]==="["){t.shift();let r=1,o=" return (function("+n+"){ return (",s=null;for(;t.length>0;){let i=t[0];if(i==="]"){if(r--,r===0){s===null&&(o=o+"true"),t.shift(),o+=")})";try{let a=maybeEval(e,function(){return Function(o)()},function(){return!0});return a.source=o,a}catch(a){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:a,source:o}),null}}}else i==="["&&r++;isPossibleRelativeReference(i,s,n)?o+="(("+n+"."+i+") ? ("+n+"."+i+") : (window."+i+"))":o=o+i,s=t.shift()}}}function consumeUntil(e,t){let n="";for(;e.length>0&&!t.test(e[0]);)n+=e.shift();return n}function consumeCSSSelector(e){let t;return e.length>0&&COMBINED_SELECTOR_START.test(e[0])?(e.shift(),t=consumeUntil(e,COMBINED_SELECTOR_END).trim(),e.shift()):t=consumeUntil(e,WHITESPACE_OR_COMMA),t}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(e,t,n){let r=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let a=o.length,l=consumeUntil(o,/[,\[\s]/);if(l!=="")if(l==="every"){let c={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),c.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var s=maybeGenerateConditional(e,o,"event");s&&(c.eventFilter=s),r.push(c)}else{let c={trigger:l};var s=maybeGenerateConditional(e,o,"event");for(s&&(c.eventFilter=s),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let d=o.shift();if(d==="changed")c.changed=!0;else if(d==="once")c.once=!0;else if(d==="consume")c.consume=!0;else if(d==="delay"&&o[0]===":")o.shift(),c.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(d==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var i=consumeCSSSelector(o);else{var i=consumeUntil(o,WHITESPACE_OR_COMMA);if(i==="closest"||i==="find"||i==="next"||i==="previous"){o.shift();let m=consumeCSSSelector(o);m.length>0&&(i+=" "+m)}}c.from=i}else d==="target"&&o[0]===":"?(o.shift(),c.target=consumeCSSSelector(o)):d==="throttle"&&o[0]===":"?(o.shift(),c.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):d==="queue"&&o[0]===":"?(o.shift(),c.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):d==="root"&&o[0]===":"?(o.shift(),c[d]=consumeCSSSelector(o)):d==="threshold"&&o[0]===":"?(o.shift(),c[d]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}r.push(c)}o.length===a&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r)}return n.length>0?n:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0}function processPolling(e,t,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n))},n.pollInterval)}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else{let s=getRawAttribute(e,"method");r=s?s.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),r==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}n.forEach(function(s){addEventListener(e,function(i,a){let l=asElement(i);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(r,o,l,a)},t,s,!0)})}}function shouldCancel(e,t){if(e.type==="submit"&&t.tagName==="FORM")return!0;if(e.type==="click"){let n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit")return!0;let r=t.closest("a"),o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href")))return!0}return!1}function ignoreBoostedAnchorCtrlClick(e,t){return getInternalData(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function maybeFilterEvent(e,t,n){let r=e.eventFilter;if(r)try{return r.call(t,n)!==!0}catch(o){let s=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:s}),!0}return!1}function addEventListener(e,t,n,r,o){let s=getInternalData(e),i;r.from?i=querySelectorAllExt(e,r.from):i=[e],r.changed&&("lastValue"in s||(s.lastValue=new WeakMap),i.forEach(function(a){s.lastValue.has(r)||s.lastValue.set(r,new WeakMap),s.lastValue.get(r).set(a,a.value)})),forEach(i,function(a){let l=function(c){if(!bodyContains(e)){a.removeEventListener(r.trigger,l);return}if(ignoreBoostedAnchorCtrlClick(e,c)||((o||shouldCancel(c,a))&&c.preventDefault(),maybeFilterEvent(r,e,c)))return;let u=getInternalData(c);if(u.triggerSpec=r,u.handledFor==null&&(u.handledFor=[]),u.handledFor.indexOf(e)<0){if(u.handledFor.push(e),r.consume&&c.stopPropagation(),r.target&&c.target&&!matches(asElement(c.target),r.target))return;if(r.once){if(s.triggeredOnce)return;s.triggeredOnce=!0}if(r.changed){let d=c.target,f=d.value,m=s.lastValue.get(r);if(m.has(d)&&m.get(d)===f)return;m.set(d,f)}if(s.delayed&&clearTimeout(s.delayed),s.throttle)return;r.throttle>0?s.throttle||(triggerEvent(e,"htmx:trigger"),t(e,c),s.throttle=getWindow().setTimeout(function(){s.throttle=null},r.throttle)):r.delay>0?s.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,c)},r.delay):(triggerEvent(e,"htmx:trigger"),t(e,c))}};n.listenerInfos==null&&(n.listenerInfos=[]),n.listenerInfos.push({trigger:r.trigger,listener:l,on:a}),a.addEventListener(r.trigger,l)})}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0},window.addEventListener("scroll",scrollHandler),window.addEventListener("resize",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){maybeReveal(e)}))},200))}function maybeReveal(e){!hasAttribute(e,"data-hx-revealed")&&isScrolledIntoView(e)&&(e.setAttribute("data-hx-revealed","true"),getInternalData(e).initHash?triggerEvent(e,"revealed"):e.addEventListener("htmx:afterProcessNode",function(){triggerEvent(e,"revealed")},{once:!0}))}function loadImmediately(e,t,n,r){let o=function(){n.loaded||(n.loaded=!0,triggerEvent(e,"htmx:trigger"),t(e))};r>0?getWindow().setTimeout(o,r):o()}function processVerbs(e,t,n){let r=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let s=getAttributeValue(e,"hx-"+o);r=!0,t.path=s,t.verb=o,n.forEach(function(i){addTriggerHandler(e,i,t,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(o,s,c,l)})})}}),r}function addTriggerHandler(e,t,n,r){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,r,n,t),maybeReveal(asElement(e));else if(t.trigger==="intersect"){let o={};t.root&&(o.root=querySelectorExt(e,t.root)),t.threshold&&(o.threshold=parseFloat(t.threshold)),new IntersectionObserver(function(i){for(let a=0;a<i.length;a++)if(i[a].isIntersecting){triggerEvent(e,"intersect");break}},o).observe(asElement(e)),addEventListener(asElement(e),r,n,t)}else!n.firstInitCompleted&&t.trigger==="load"?maybeFilterEvent(t,e,makeEvent("load",{elt:e}))||loadImmediately(asElement(e),r,n,t.delay):t.pollInterval>0?(n.polling=!0,processPolling(asElement(e),r,t)):addEventListener(e,r,n,t)}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return!1;let n=t.attributes;for(let r=0;r<n.length;r++){let o=n[r].name;if(startsWith(o,"hx-on:")||startsWith(o,"data-hx-on:")||startsWith(o,"hx-on-")||startsWith(o,"data-hx-on-"))return!0}return!1}let HX_ON_QUERY=new XPathEvaluator().createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function processHXOnRoot(e,t){shouldProcessHxOn(e)&&t.push(asElement(e));let n=HX_ON_QUERY.evaluate(e),r=null;for(;r=n.iterateNext();)t.push(asElement(r))}function findHxOnWildcardElements(e){let t=[];if(e instanceof DocumentFragment)for(let n of e.childNodes)processHXOnRoot(n,t);else processHXOnRoot(e,t);return t}function findElementsToProcess(e){if(e.querySelectorAll){let n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]",r=[];for(let s in extensions){let i=extensions[s];if(i.getSelectors){var t=i.getSelectors();t&&r.push(t)}}return e.querySelectorAll(VERB_SELECTOR+n+", form, [type='submit'], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(s=>", "+s).join(""))}else return[]}function maybeSetLastButtonClicked(e){let t=getTargetButton(e.target),n=getRelatedFormData(e);n&&(n.lastButtonClicked=t)}function maybeUnsetLastButtonClicked(e){let t=getRelatedFormData(e);t&&(t.lastButtonClicked=null)}function getTargetButton(e){return closest(asElement(e),"button, input[type='submit']")}function getRelatedForm(e){return e.form||closest(e,"form")}function getRelatedFormData(e){let t=getTargetButton(e.target);if(!t)return;let n=getRelatedForm(t);if(n)return getInternalData(n)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(e,t,n){let r=getInternalData(e);Array.isArray(r.onHandlers)||(r.onHandlers=[]);let o,s=function(i){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,i))})};e.addEventListener(t,s),r.onHandlers.push({event:t,listener:s})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;t<e.attributes.length;t++){let n=e.attributes[t].name,r=e.attributes[t].value;if(startsWith(n,"hx-on")||startsWith(n,"data-hx-on")){let o=n.indexOf("-on")+3,s=n.slice(o,o+1);if(s==="-"||s===":"){let i=n.slice(o+1);startsWith(i,":")?i="htmx"+i:startsWith(i,"-")?i="htmx:"+i.slice(1):startsWith(i,"htmx-")&&(i="htmx:"+i.slice(5)),addHxOnEventHandler(e,i,r)}}}}function initNode(e){triggerEvent(e,"htmx:beforeProcessNode");let t=getInternalData(e),n=getTriggerSpecs(e);processVerbs(e,t,n)||(getClosestAttributeValue(e,"hx-boost")==="true"?boostElement(e,t,n):hasAttribute(e,"hx-trigger")&&n.forEach(function(o){addTriggerHandler(e,o,t,function(){})})),(e.tagName==="FORM"||getRawAttribute(e,"type")==="submit"&&hasAttribute(e,"form"))&&initButtonTracking(e),t.firstInitCompleted=!0,triggerEvent(e,"htmx:afterProcessNode")}function maybeDeInitAndHash(e){if(!(e instanceof Element))return!1;let t=getInternalData(e),n=attributeHash(e);return t.initHash!==n?(deInitNode(e),t.initHash=n,!0):!1}function processNode(e){if(e=resolveTarget(e),eltIsDisabled(e)){cleanUpElement(e);return}let t=[];maybeDeInitAndHash(e)&&t.push(e),forEach(findElementsToProcess(e),function(n){if(eltIsDisabled(n)){cleanUpElement(n);return}maybeDeInitAndHash(n)&&t.push(n)}),forEach(findHxOnWildcardElements(e),processHxOnWildcard),forEach(t,initNode)}function kebabEventName(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function makeEvent(e,t){return new CustomEvent(e,{bubbles:!0,cancelable:!0,composed:!0,detail:t})}function triggerErrorEvent(e,t,n){triggerEvent(e,t,mergeObjects({error:t},n))}function ignoreEventForLogging(e){return e==="htmx:afterProcessNode"}function withExtensions(e,t,n){forEach(getExtensions(e,[],n),function(r){try{t(r)}catch(o){logError(o)}})}function logError(e){console.error(e)}function triggerEvent(e,t,n){e=resolveTarget(e),n==null&&(n={}),n.elt=e;let r=makeEvent(t,n);htmx.logger&&!ignoreEventForLogging(t)&&htmx.logger(e,t,n),n.error&&(logError(n.error),triggerEvent(e,"htmx:error",{errorInfo:n}));let o=e.dispatchEvent(r),s=kebabEventName(t);if(o&&s!==t){let i=makeEvent(s,r.detail);o=o&&e.dispatchEvent(i)}return withExtensions(asElement(e),function(i){o=o&&i.onEvent(t,r)!==!1&&!r.defaultPrevented}),o}let currentPathForHistory;function setCurrentPathForHistory(e){currentPathForHistory=e,canAccessLocalStorage()&&sessionStorage.setItem("htmx-current-path-for-history",e)}setCurrentPathForHistory(location.pathname+location.search);function getHistoryElement(){return getDocument().querySelector("[hx-history-elt],[data-hx-history-elt]")||getDocument().body}function saveToHistoryCache(e,t){if(!canAccessLocalStorage())return;let n=cleanInnerHtmlForHistory(t),r=getDocument().title,o=window.scrollY;if(htmx.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}e=normalizePath(e);let s=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let a=0;a<s.length;a++)if(s[a].url===e){s.splice(a,1);break}let i={url:e,content:n,title:r,scroll:o};for(triggerEvent(getDocument().body,"htmx:historyItemCreated",{item:i,cache:s}),s.push(i);s.length>htmx.config.historyCacheSize;)s.shift();for(;s.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(s));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:s}),s.shift()}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let n=0;n<t.length;n++)if(t[n].url===e)return t[n];return null}function cleanInnerHtmlForHistory(e){let t=htmx.config.requestClass,n=e.cloneNode(!0);return forEach(findAll(n,"."+t),function(r){removeClassFromElement(r,t)}),forEach(findAll(n,"[data-disabled-by-htmx]"),function(r){r.removeAttribute("disabled")}),n.innerHTML}function saveCurrentPageToHistory(){let e=getHistoryElement(),t=currentPathForHistory;canAccessLocalStorage()&&(t=sessionStorage.getItem("htmx-current-path-for-history")),t=t||location.pathname+location.search,getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')||(triggerEvent(getDocument().body,"htmx:beforeHistorySave",{path:t,historyElt:e}),saveToHistoryCache(t,e)),htmx.config.historyEnabled&&history.replaceState({htmx:!0},getDocument().title,location.href)}function pushUrlIntoHistory(e){htmx.config.getCacheBusterParam&&(e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,""),(endsWith(e,"&")||endsWith(e,"?"))&&(e=e.slice(0,-1))),htmx.config.historyEnabled&&history.pushState({htmx:!0},"",e),setCurrentPathForHistory(e)}function replaceUrlInHistory(e){htmx.config.historyEnabled&&history.replaceState({htmx:!0},"",e),setCurrentPathForHistory(e)}function settleImmediately(e){forEach(e,function(t){t.call(void 0)})}function loadHistoryFromServer(e){let t=new XMLHttpRequest,n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0},r={path:e,xhr:t,historyElt:getHistoryElement(),swapSpec:n};t.open("GET",e,!0),htmx.config.historyRestoreAsHxRequest&&t.setRequestHeader("HX-Request","true"),t.setRequestHeader("HX-History-Restore-Request","true"),t.setRequestHeader("HX-Current-URL",location.href),t.onload=function(){this.status>=200&&this.status<400?(r.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",r),swap(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:!0}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:r.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",r)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",r)&&t.send()}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll},r={path:e,item:t,historyElt:getHistoryElement(),swapSpec:n};triggerEvent(getDocument().body,"htmx:historyCacheHit",r)&&(swap(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",r))}else htmx.config.refreshOnHistoryMiss?htmx.location.reload(!0):loadHistoryFromServer(e)}function addRequestIndicatorClasses(e){let t=findAttributeTargets(e,"hx-indicator");return t==null&&(t=[e]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.classList.add.call(n.classList,htmx.config.requestClass)}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.setAttribute("disabled",""),n.setAttribute("data-disabled-by-htmx","")}),t}function removeRequestIndicators(e,t){forEach(e.concat(t),function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||1)-1}),forEach(e,function(n){getInternalData(n).requestCount===0&&n.classList.remove.call(n.classList,htmx.config.requestClass)}),forEach(t,function(n){getInternalData(n).requestCount===0&&(n.removeAttribute("disabled"),n.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(e,t){for(let n=0;n<e.length;n++)if(e[n].isSameNode(t))return!0;return!1}function shouldInclude(e){let t=e;return t.name===""||t.name==null||t.disabled||closest(t,"fieldset[disabled]")||t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"?!1:t.type==="checkbox"||t.type==="radio"?t.checked:!0}function addValueToFormData(e,t,n){e!=null&&t!=null&&(Array.isArray(t)?t.forEach(function(r){n.append(e,r)}):n.append(e,t))}function removeValueFromFormData(e,t,n){if(e!=null&&t!=null){let r=n.getAll(e);Array.isArray(t)?r=r.filter(o=>t.indexOf(o)<0):r=r.filter(o=>o!==t),n.delete(e),forEach(r,o=>n.append(e,o))}}function getValueFromInput(e){return e instanceof HTMLSelectElement&&e.multiple?toArray(e.querySelectorAll("option:checked")).map(function(t){return t.value}):e instanceof HTMLInputElement&&e.files?toArray(e.files):e.value}function processInputValue(e,t,n,r,o){if(!(r==null||haveSeenNode(e,r))){if(e.push(r),shouldInclude(r)){let s=getRawAttribute(r,"name");addValueToFormData(s,getValueFromInput(r),t),o&&validateElement(r,n)}r instanceof HTMLFormElement&&(forEach(r.elements,function(s){e.indexOf(s)>=0?removeValueFromFormData(s.name,getValueFromInput(s),t):e.push(s),o&&validateElement(s,n)}),new FormData(r).forEach(function(s,i){s instanceof File&&s.name===""||addValueToFormData(i,s,t)}))}}function validateElement(e,t){let n=e;n.willValidate&&(triggerEvent(n,"htmx:validation:validate"),n.checkValidity()||(triggerEvent(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&htmx.config.reportValidityOfForms&&n.reportValidity(),t.push({elt:n,message:n.validationMessage,validity:n.validity})))}function overrideFormData(e,t){for(let n of t.keys())e.delete(n);return t.forEach(function(n,r){e.append(r,n)}),e}function getInputValues(e,t){let n=[],r=new FormData,o=new FormData,s=[],i=getInternalData(e);i.lastButtonClicked&&!bodyContains(i.lastButtonClicked)&&(i.lastButtonClicked=null);let a=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(i.lastButtonClicked&&(a=a&&i.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,s,getRelatedForm(e),a),processInputValue(n,r,s,e,a),i.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let c=i.lastButtonClicked||e,u=getRawAttribute(c,"name");addValueToFormData(u,c.value,o)}let l=findAttributeTargets(e,"hx-include");return forEach(l,function(c){processInputValue(n,r,s,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(u){processInputValue(n,r,s,u,a)})}),overrideFormData(r,o),{errors:s,formData:r,values:formDataProxy(r)}}function appendParam(e,t,n){e!==""&&(e+="&"),String(n)==="[object Object]"&&(n=JSON.stringify(n));let r=encodeURIComponent(n);return e+=encodeURIComponent(t)+"="+r,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(n,r){t=appendParam(t,r,n)}),t}function getHeaders(e,t,n){let r={"HX-Request":"true","HX-Trigger":getRawAttribute(e,"id"),"HX-Trigger-Name":getRawAttribute(e,"name"),"HX-Target":getAttributeValue(t,"id"),"HX-Current-URL":location.href};return getValuesForElement(e,"hx-headers",!1,r),n!==void 0&&(r["HX-Prompt"]=n),getInternalData(e).boosted&&(r["HX-Boosted"]="true"),r}function filterValues(e,t){let n=getClosestAttributeValue(t,"hx-params");if(n){if(n==="none")return new FormData;if(n==="*")return e;if(n.indexOf("not ")===0)return forEach(n.slice(4).split(","),function(r){r=r.trim(),e.delete(r)}),e;{let r=new FormData;return forEach(n.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(s){r.append(o,s)})}),r}}else return e}function isAnchorLink(e){return!!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let n=t||getClosestAttributeValue(e,"hx-swap"),r={swapStyle:getInternalData(e).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(e).boosted&&!isAnchorLink(e)&&(r.show="top"),n){let i=splitOnWhitespace(n);if(i.length>0)for(let a=0;a<i.length;a++){let l=i[a];if(l.indexOf("swap:")===0)r.swapDelay=parseInterval(l.slice(5));else if(l.indexOf("settle:")===0)r.settleDelay=parseInterval(l.slice(7));else if(l.indexOf("transition:")===0)r.transition=l.slice(11)==="true";else if(l.indexOf("ignoreTitle:")===0)r.ignoreTitle=l.slice(12)==="true";else if(l.indexOf("scroll:")===0){var o=l.slice(7).split(":");let u=o.pop();var s=o.length>0?o.join(":"):null;r.scroll=u,r.scrollTarget=s}else if(l.indexOf("show:")===0){var o=l.slice(5).split(":");let d=o.pop();var s=o.length>0?o.join(":"):null;r.show=d,r.showTarget=s}else if(l.indexOf("focus-scroll:")===0){let c=l.slice(13);r.focusScroll=c=="true"}else a==0?r.swapStyle=l:logError("Unknown modifier in hx-swap: "+l)}}return r}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,n){let r=null;return withExtensions(t,function(o){r==null&&(r=o.encodeParameters(e,n,t))}),r??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(n)):urlEncode(n))}function makeSettleInfo(e){return{tasks:[],elts:[e]}}function updateScrollState(e,t){let n=e[0],r=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(n,t.scrollTarget))),t.scroll==="top"&&(n||o)&&(o=o||n,o.scrollTop=0),t.scroll==="bottom"&&(r||o)&&(o=o||r,o.scrollTop=o.scrollHeight),typeof t.scroll=="number"&&getWindow().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}if(t.show){var o=null;if(t.showTarget){let i=t.showTarget;t.showTarget==="window"&&(i="body"),o=asElement(querySelectorExt(n,i))}t.show==="top"&&(n||o)&&(o=o||n,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(e,t,n,r,o){if(r==null&&(r={}),e==null)return r;let s=getAttributeValue(e,t);if(s){let i=s.trim(),a=n;if(i==="unset")return null;i.indexOf("javascript:")===0?(i=i.slice(11),a=!0):i.indexOf("js:")===0&&(i=i.slice(3),a=!0),i.indexOf("{")!==0&&(i="{"+i+"}");let l;a?l=maybeEval(e,function(){return o?Function("event","return ("+i+")").call(e,o):Function("return ("+i+")").call(e)},{}):l=parseJSON(i);for(let c in l)l.hasOwnProperty(c)&&r[c]==null&&(r[c]=l[c])}return getValuesForElement(asElement(parentElt(e)),t,n,r,o)}function maybeEval(e,t,n){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),n)}function getHXVarsForElement(e,t,n){return getValuesForElement(e,"hx-vars",!0,n,t)}function getHXValsForElement(e,t,n){return getValuesForElement(e,"hx-vals",!1,n,t)}function getExpressionVars(e,t){return mergeObjects(getHXVarsForElement(e,t),getHXValsForElement(e,t))}function safelySetHeaderValue(e,t,n){if(n!==null)try{e.setRequestHeader(t,n)}catch{e.setRequestHeader(t,encodeURIComponent(n)),e.setRequestHeader(t+"-URI-AutoEncoded","true")}}function getPathFromResponse(e){if(e.responseURL)try{let t=new URL(e.responseURL);return t.pathname+t.search}catch{triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:e.responseURL})}}function hasHeader(e,t){return t.test(e.getAllResponseHeaders())}function ajaxHelper(e,t,n){if(e=e.toLowerCase(),n){if(n instanceof Element||typeof n=="string")return issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(n)||DUMMY_ELT,returnPromise:!0});{let r=resolveTarget(n.target);return(n.target&&!r||n.source&&!r&&!resolveTarget(n.source))&&(r=DUMMY_ELT),issueAjaxRequest(e,t,resolveTarget(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:r,swapOverride:n.swap,select:n.select,returnPromise:!0,push:n.push,replace:n.replace,selectOOB:n.selectOOB})}}else return issueAjaxRequest(e,t,null,null,{returnPromise:!0})}function hierarchyForElt(e){let t=[];for(;e;)t.push(e),e=e.parentElement;return t}function verifyPath(e,t,n){let r=new URL(t,location.protocol!=="about:"?location.href:window.origin),s=(location.protocol!=="about:"?location.origin:window.origin)===r.origin;return htmx.config.selfRequestsOnly&&!s?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:r,sameHost:s},n))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let n in e)e.hasOwnProperty(n)&&(e[n]&&typeof e[n].forEach=="function"?e[n].forEach(function(r){t.append(n,r)}):typeof e[n]=="object"&&!(e[n]instanceof Blob)?t.append(n,JSON.stringify(e[n])):t.append(n,e[n]));return t}function formDataArrayProxy(e,t,n){return new Proxy(n,{get:function(r,o){return typeof o=="number"?r[o]:o==="length"?r.length:o==="push"?function(s){r.push(s),e.append(t,s)}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(s){e.append(t,s)})}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,s){return r[o]=s,e.delete(t),r.forEach(function(i){e.append(t,i)}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,n){if(typeof n=="symbol"){let o=Reflect.get(t,n);return typeof o=="function"?function(){return o.apply(e,arguments)}:o}if(n==="toJSON")return()=>Object.fromEntries(e);if(n in t&&typeof t[n]=="function")return function(){return e[n].apply(e,arguments)};let r=e.getAll(n);if(r.length!==0)return r.length===1?r[0]:formDataArrayProxy(t,n,r)},set:function(t,n,r){return typeof n!="string"?!1:(t.delete(n),r&&typeof r.forEach=="function"?r.forEach(function(o){t.append(n,o)}):typeof r=="object"&&!(r instanceof Blob)?t.append(n,JSON.stringify(r)):t.append(n,r),!0)},deleteProperty:function(t,n){return typeof n=="string"&&t.delete(n),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,n){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),n)}})}function issueAjaxRequest(e,t,n,r,o,s){let i=null,a=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var l=new Promise(function(E,x){i=E,a=x});n==null&&(n=getDocument().body);let c=o.handler||handleAjaxResponse,u=o.select||null;if(!bodyContains(n))return maybeCall(i),l;let d=o.targetOverride||asElement(getTarget(n));if(d==null||d==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getClosestAttributeValue(n,"hx-target")}),maybeCall(a),l;let f=getInternalData(n),m=f.lastButtonClicked;if(m){let E=getRawAttribute(m,"formaction");E!=null&&(t=E);let x=getRawAttribute(m,"formmethod");if(x!=null)if(VERBS.includes(x.toLowerCase()))e=x;else return maybeCall(i),l}let h=getClosestAttributeValue(n,"hx-confirm");if(s===void 0&&triggerEvent(n,"htmx:confirm",{target:d,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(L){return issueAjaxRequest(e,t,n,r,o,!!L)},question:h})===!1)return maybeCall(i),l;let g=n,p=getClosestAttributeValue(n,"hx-sync"),y=null,w=!1;if(p){let E=p.split(":"),x=E[0].trim();if(x==="this"?g=findThisElement(n,"hx-sync"):g=asElement(querySelectorExt(n,x)),p=(E[1]||"drop").trim(),f=getInternalData(g),p==="drop"&&f.xhr&&f.abortable!==!0)return maybeCall(i),l;if(p==="abort"){if(f.xhr)return maybeCall(i),l;w=!0}else p==="replace"?triggerEvent(g,"htmx:abort"):p.indexOf("queue")===0&&(y=(p.split(" ")[1]||"last").trim())}if(f.xhr)if(f.abortable)triggerEvent(g,"htmx:abort");else{if(y==null){if(r){let E=getInternalData(r);E&&E.triggerSpec&&E.triggerSpec.queue&&(y=E.triggerSpec.queue)}y==null&&(y="last")}return f.queuedRequests==null&&(f.queuedRequests=[]),y==="first"&&f.queuedRequests.length===0?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):y==="all"?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):y==="last"&&(f.queuedRequests=[],f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)})),maybeCall(i),l}let b=new XMLHttpRequest;f.xhr=b,f.abortable=w;let v=function(){f.xhr=null,f.abortable=!1,f.queuedRequests!=null&&f.queuedRequests.length>0&&f.queuedRequests.shift()()},te=getClosestAttributeValue(n,"hx-prompt");if(te){var W=prompt(te);if(W===null||!triggerEvent(n,"htmx:prompt",{prompt:W,target:d}))return maybeCall(i),v(),l}if(h&&!s&&!confirm(h))return maybeCall(i),v(),l;let H=getHeaders(n,d,W);e!=="get"&&!usesFormData(n)&&(H["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(H=mergeObjects(H,o.headers));let ne=getInputValues(n,e),q=ne.errors,re=ne.formData;o.values&&overrideFormData(re,formDataFromObject(o.values));let Ae=formDataFromObject(getExpressionVars(n,r)),X=overrideFormData(re,Ae),R=filterValues(X,n);htmx.config.getCacheBusterParam&&e==="get"&&R.set("org.htmx.cache-buster",getRawAttribute(d,"id")||"true"),(t==null||t==="")&&(t=location.href);let $=getValuesForElement(n,"hx-request"),oe=getInternalData(n).boosted,P=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,C={boosted:oe,useUrlParams:P,formData:R,parameters:formDataProxy(R),unfilteredFormData:X,unfilteredParameters:formDataProxy(X),headers:H,elt:n,target:d,verb:e,errors:q,withCredentials:o.credentials||$.credentials||htmx.config.withCredentials,timeout:o.timeout||$.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",C))return maybeCall(i),v(),l;if(t=C.path,e=C.verb,H=C.headers,R=formDataFromObject(C.parameters),q=C.errors,P=C.useUrlParams,q&&q.length>0)return triggerEvent(n,"htmx:validation:halted",C),maybeCall(i),v(),l;let se=t.split("#"),Le=se[0],z=se[1],A=t;if(P&&(A=Le,!R.keys().next().done&&(A.indexOf("?")<0?A+="?":A+="&",A+=urlEncode(R),z&&(A+="#"+z))),!verifyPath(n,A,C))return triggerErrorEvent(n,"htmx:invalidPath",C),maybeCall(a),v(),l;if(b.open(e.toUpperCase(),A,!0),b.overrideMimeType("text/html"),b.withCredentials=C.withCredentials,b.timeout=C.timeout,!$.noHeaders){for(let E in H)if(H.hasOwnProperty(E)){let x=H[E];safelySetHeaderValue(b,E,x)}}let S={xhr:b,target:d,requestConfig:C,etc:o,boosted:oe,select:u,pathInfo:{requestPath:t,finalRequestPath:A,responsePath:null,anchor:z}};if(b.onload=function(){try{let E=hierarchyForElt(n);if(S.pathInfo.responsePath=getPathFromResponse(b),c(n,S),S.keepIndicators!==!0&&removeRequestIndicators(F,B),triggerEvent(n,"htmx:afterRequest",S),triggerEvent(n,"htmx:afterOnLoad",S),!bodyContains(n)){let x=null;for(;E.length>0&&x==null;){let L=E.shift();bodyContains(L)&&(x=L)}x&&(triggerEvent(x,"htmx:afterRequest",S),triggerEvent(x,"htmx:afterOnLoad",S))}maybeCall(i)}catch(E){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:E},S)),E}finally{v()}},b.onerror=function(){removeRequestIndicators(F,B),triggerErrorEvent(n,"htmx:afterRequest",S),triggerErrorEvent(n,"htmx:sendError",S),maybeCall(a),v()},b.onabort=function(){removeRequestIndicators(F,B),triggerErrorEvent(n,"htmx:afterRequest",S),triggerErrorEvent(n,"htmx:sendAbort",S),maybeCall(a),v()},b.ontimeout=function(){removeRequestIndicators(F,B),triggerErrorEvent(n,"htmx:afterRequest",S),triggerErrorEvent(n,"htmx:timeout",S),maybeCall(a),v()},!triggerEvent(n,"htmx:beforeRequest",S))return maybeCall(i),v(),l;var F=addRequestIndicatorClasses(n),B=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(E){forEach([b,b.upload],function(x){x.addEventListener(E,function(L){triggerEvent(n,"htmx:xhr:"+E,{lengthComputable:L.lengthComputable,loaded:L.loaded,total:L.total})})})}),triggerEvent(n,"htmx:beforeSend",S);let Ie=P?null:encodeParamsForBody(b,n,R);return b.send(Ie),l}function determineHistoryUpdates(e,t){let n=t.xhr,r=null,o=null;if(hasHeader(n,/HX-Push:/i)?(r=n.getResponseHeader("HX-Push"),o="push"):hasHeader(n,/HX-Push-Url:/i)?(r=n.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(n,/HX-Replace-Url:/i)&&(r=n.getResponseHeader("HX-Replace-Url"),o="replace"),r)return r==="false"?{}:{type:o,path:r};let s=t.pathInfo.finalRequestPath,i=t.pathInfo.responsePath,a=t.etc.push||getClosestAttributeValue(e,"hx-push-url"),l=t.etc.replace||getClosestAttributeValue(e,"hx-replace-url"),c=getInternalData(e).boosted,u=null,d=null;return a?(u="push",d=a):l?(u="replace",d=l):c&&(u="push",d=i||s),d?d==="false"?{}:(d==="true"&&(d=i||s),t.pathInfo.anchor&&d.indexOf("#")===-1&&(d=d+"#"+t.pathInfo.anchor),{type:u,path:d}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t<htmx.config.responseHandling.length;t++){var n=htmx.config.responseHandling[t];if(codeMatches(n,e.status))return n}return{swap:!1}}function handleTitle(e){if(e){let t=find("title");t?t.textContent=e:window.document.title=e}}function resolveRetarget(e,t){if(t==="this")return e;let n=asElement(querySelectorExt(e,t));if(n==null)throw triggerErrorEvent(e,"htmx:targetError",{target:t}),new Error(`Invalid re-target ${t}`);return n}function handleAjaxResponse(e,t){let n=t.xhr,r=t.target,o=t.etc,s=t.select;if(!triggerEvent(e,"htmx:beforeOnLoad",t))return;if(hasHeader(n,/HX-Trigger:/i)&&handleTriggerHeader(n,"HX-Trigger",e),hasHeader(n,/HX-Location:/i)){let w=n.getResponseHeader("HX-Location");var i={};w.indexOf("{")===0&&(i=parseJSON(w),w=i.path,delete i.path),i.push=i.push||"true",ajaxHelper("get",w,i);return}let a=hasHeader(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(hasHeader(n,/HX-Redirect:/i)){t.keepIndicators=!0,htmx.location.href=n.getResponseHeader("HX-Redirect"),a&&htmx.location.reload();return}if(a){t.keepIndicators=!0,htmx.location.reload();return}let l=determineHistoryUpdates(e,t),c=resolveResponseHandling(n),u=c.swap,d=!!c.error,f=htmx.config.ignoreTitle||c.ignoreTitle,m=c.select;c.target&&(t.target=resolveRetarget(e,c.target));var h=o.swapOverride;h==null&&c.swapOverride&&(h=c.swapOverride),hasHeader(n,/HX-Retarget:/i)&&(t.target=resolveRetarget(e,n.getResponseHeader("HX-Retarget"))),hasHeader(n,/HX-Reswap:/i)&&(h=n.getResponseHeader("HX-Reswap"));var g=n.response,p=mergeObjects({shouldSwap:u,serverResponse:g,isError:d,ignoreTitle:f,selectOverride:m,swapOverride:h},t);if(!(c.event&&!triggerEvent(r,c.event,p))&&triggerEvent(r,"htmx:beforeSwap",p)){if(r=p.target,g=p.serverResponse,d=p.isError,f=p.ignoreTitle,m=p.selectOverride,h=p.swapOverride,t.target=r,t.failed=d,t.successful=!d,p.shouldSwap){n.status===286&&cancelPolling(e),withExtensions(e,function(v){g=v.transformResponse(g,n,e)}),l.type&&saveCurrentPageToHistory();var y=getSwapSpecification(e,h);y.hasOwnProperty("ignoreTitle")||(y.ignoreTitle=f),r.classList.add(htmx.config.swappingClass),s&&(m=s),hasHeader(n,/HX-Reselect:/i)&&(m=n.getResponseHeader("HX-Reselect"));let w=o.selectOOB||getClosestAttributeValue(e,"hx-select-oob"),b=getClosestAttributeValue(e,"hx-select");swap(r,g,y,{select:m==="unset"?null:m||b,selectOOB:w,eventInfo:t,anchor:t.pathInfo.anchor,contextElement:e,afterSwapCallback:function(){if(hasHeader(n,/HX-Trigger-After-Swap:/i)){let v=e;bodyContains(e)||(v=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Swap",v)}},afterSettleCallback:function(){if(hasHeader(n,/HX-Trigger-After-Settle:/i)){let v=e;bodyContains(e)||(v=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Settle",v)}},beforeSwapCallback:function(){l.type&&(triggerEvent(getDocument().body,"htmx:beforeHistoryUpdate",mergeObjects({history:l},t)),l.type==="push"?(pushUrlIntoHistory(l.path),triggerEvent(getDocument().body,"htmx:pushedIntoHistory",{path:l.path})):(replaceUrlInHistory(l.path),triggerEvent(getDocument().body,"htmx:replacedInHistory",{path:l.path})))}})}d&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+n.status+" from "+t.pathInfo.requestPath},t))}}let extensions={};function extensionBase(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return!0},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return!1},handleSwap:function(e,t,n,r){return!1},encodeParameters:function(e,t,n){return null}}}function defineExtension(e,t){t.init&&t.init(internalAPI),extensions[e]=mergeObjects(extensionBase(),t)}function removeExtension(e){delete extensions[e]}function getExtensions(e,t,n){if(t==null&&(t=[]),e==null)return t;n==null&&(n=[]);let r=getAttributeValue(e,"hx-ext");return r&&forEach(r.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){n.push(o.slice(7));return}if(n.indexOf(o)<0){let s=extensions[o];s&&t.indexOf(s)<0&&t.push(s)}}),getExtensions(asElement(parentElt(e)),t,n)}var isReady=!1;getDocument().addEventListener("DOMContentLoaded",function(){isReady=!0});function ready(e){isReady||getDocument().readyState==="complete"?e():getDocument().addEventListener("DOMContentLoaded",e)}function insertIndicatorStyles(){if(htmx.config.includeIndicatorStyles!==!1){let e=htmx.config.inlineStyleNonce?` nonce="${htmx.config.inlineStyleNonce}"`:"",t=htmx.config.indicatorClass,n=htmx.config.requestClass;getDocument().head.insertAdjacentHTML("beforeend",`<style${e}>.${t}{opacity:0;visibility: hidden} .${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}</style>`)}}function getMetaConfig(){let e=getDocument().querySelector('meta[name="htmx-config"]');return e?parseJSON(e.content):null}function mergeMetaConfig(){let e=getMetaConfig();e&&(htmx.config=mergeObjects(htmx.config,e))}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let e=getDocument().body;processNode(e);let t=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(r){let o=r.detail.elt||r.target,s=getInternalData(o);s&&s.xhr&&s.xhr.abort()});let n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(r){r.state&&r.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent})})):n&&n(r)},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null},0)}),htmx})(),O=He;(function(){let e;O.defineExtension("json-enc",{init:function(t){e=t},onEvent:function(t,n){t==="htmx:configRequest"&&(n.detail.headers["Content-Type"]="application/json")},encodeParameters:function(t,n,r){t.overrideMimeType("text/json");let o={};n.forEach(function(i,a){Object.hasOwn(o,a)?(Array.isArray(o[a])||(o[a]=[o[a]]),o[a].push(i)):o[a]=i});let s=e.getExpressionVars(r);return Object.keys(o).forEach(function(i){o[i]=Object.hasOwn(s,i)?s[i]:o[i]}),JSON.stringify(o)}})})();var ie="https://typeahead.waow.tech",le="https://public.api.bsky.app",Re="/xrpc/app.bsky.actor.searchActorsTypeahead",De="/xrpc/app.bsky.actor.getProfiles";var Oe="atcr_recent_handles",ce="atcr_recent_profile_cache";var Y=class{constructor(t){this.input=t,this.container=t.closest(".sailor-typeahead")||t.parentElement,this.dropdown=null,this.selectedCard=null,this.actors=[],this.currentItems=[],this.mode="hidden",this.focusIndex=-1,this.debounceTimer=null,this.requestSeq=0,this.primaryUnhealthyUntil=0,this.lastPrefetchPrefix="",this.lastPrefetchAt=0,this.createDropdown(),this.bindEvents(),this.input.value.trim().length===0&&this.showRecent()}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="sailor-typeahead-dropdown",this.dropdown.setAttribute("role","listbox"),this.dropdown.style.display="none",this.input.insertAdjacentElement("afterend",this.dropdown)}bindEvents(){this.input.addEventListener("focus",()=>this.handleFocus()),this.input.addEventListener("input",()=>this.handleInput()),this.input.addEventListener("keydown",t=>this.handleKeydown(t)),document.addEventListener("click",t=>{!this.input.contains(t.target)&&!this.dropdown.contains(t.target)&&this.hide()}),document.addEventListener("keydown",t=>{t.key==="Escape"&&this.selectedCard&&this.clearSelection()})}handleFocus(){this.input.value.trim().length===0&&this.showRecent()}handleInput(){let t=this.input.value.trim();if(t.length===0){this.showRecent();return}if(t.length>=2&&t.length<4){this.hide(),this.schedulePrefetch(t);return}if(t.length>=4){this.scheduleSearch(t);return}this.hide()}schedulePrefetch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runPrefetch(t),150)}scheduleSearch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runSearch(t),150)}async runPrefetch(t){let n=Date.now();if(!(t===this.lastPrefetchPrefix&&n-this.lastPrefetchAt<1e4)&&!(n<this.primaryUnhealthyUntil)){this.lastPrefetchPrefix=t,this.lastPrefetchAt=n;try{await J(ie,t,400)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}}}async runSearch(t){let n=++this.requestSeq,r=null;if(Date.now()>=this.primaryUnhealthyUntil)try{r=await J(ie,t,1500)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}if(r===null)try{r=await J(le,t,1500)}catch{r=[]}n===this.requestSeq&&(this.actors=r||[],this.focusIndex=-1,this.renderResults())}renderResults(){if(this.mode="results",this.dropdown.innerHTML="",this.currentItems=[],this.actors.length===0){this.hide();return}this.actors.forEach((t,n)=>{this.currentItems.push(t),this.dropdown.appendChild(this.buildActorRow(t,n))}),this.dropdown.style.display="block"}buildActorRow(t,n){let r=document.createElement("div");r.className="sailor-typeahead-item",r.setAttribute("role","option"),r.setAttribute("aria-selected","false"),r.dataset.index=String(n),r.dataset.handle=t.handle;let o=document.createElement("div");if(o.className="sailor-typeahead-avatar",t.avatar){let l=document.createElement("img");l.src=t.avatar,l.alt="",l.loading="lazy",o.appendChild(l)}let s=document.createElement("div");s.className="sailor-typeahead-text";let i=t.displayName&&t.displayName!==t.handle;if(i){let l=document.createElement("div");l.className="sailor-typeahead-name",l.textContent=t.displayName,s.appendChild(l)}let a=document.createElement("div");return a.className=i?"sailor-typeahead-handle":"sailor-typeahead-name",a.textContent="@"+t.handle,s.appendChild(a),r.append(o,s),r.addEventListener("mousedown",l=>{l.preventDefault(),this.select(t)}),r}showRecent(){let t=Ne();if(t.length===0){this.hide();return}this.mode="recent",this.focusIndex=-1,this.renderRecent(t),this.enrichRecent(t)}renderRecent(t){let n=_();this.dropdown.innerHTML="",this.currentItems=[];let r=document.createElement("div");r.className="sailor-typeahead-header",r.textContent="Recent accounts",this.dropdown.appendChild(r),t.forEach((o,s)=>{let i=n[o]?.profile||{handle:o};this.currentItems.push(i),this.dropdown.appendChild(this.buildActorRow(i,s))}),this.dropdown.style.display="block"}async enrichRecent(t){let n=_(),r=Date.now(),o=t.filter(a=>{let l=n[a];return!l||r-l.ts>864e5});if(o.length===0)return;let s=await Me(o);if(s.length===0)return;let i=_();s.forEach(a=>{i[a.handle]={ts:r,profile:{handle:a.handle,displayName:a.displayName,avatar:a.avatar}}}),ae(i),this.mode==="recent"&&this.renderRecent(t)}hide(){this.mode="hidden",this.focusIndex=-1,this.dropdown.style.display="none"}select(t){if(typeof t=="string"&&(t={handle:t}),this.input.value=t.handle,this.hide(),this.showSelectedCard(t),t.handle){let n=_();n[t.handle]={ts:Date.now(),profile:{handle:t.handle,displayName:t.displayName,avatar:t.avatar}},ae(n)}}showSelectedCard(t){this.clearSelectedCard();let n=document.createElement("div");n.className="sailor-typeahead-selected";let r=document.createElement("div");if(r.className="sailor-typeahead-avatar",t.avatar){let l=document.createElement("img");l.src=t.avatar,l.alt="",r.appendChild(l)}let o=document.createElement("div");o.className="sailor-typeahead-text";let s=t.displayName&&t.displayName!==t.handle;if(s){let l=document.createElement("div");l.className="sailor-typeahead-name",l.textContent=t.displayName,o.appendChild(l)}let i=document.createElement("div");i.className=s?"sailor-typeahead-handle":"sailor-typeahead-name",i.textContent="@"+t.handle,o.appendChild(i);let a=document.createElement("button");a.type="button",a.className="sailor-typeahead-clear",a.setAttribute("aria-label","Change account"),a.innerHTML="&times;",a.addEventListener("click",()=>this.clearSelection()),n.append(r,o,a),this.input.style.display="none",this.input.insertAdjacentElement("beforebegin",n),this.selectedCard=n}clearSelectedCard(){this.selectedCard&&(this.selectedCard.remove(),this.selectedCard=null)}clearSelection(){this.clearSelectedCard(),this.input.style.display="",this.input.value="",this.input.focus(),this.showRecent()}handleKeydown(t){if(this.mode==="hidden")return;let n=this.dropdown.querySelectorAll(".sailor-typeahead-item");n.length!==0&&(t.key==="ArrowDown"?(t.preventDefault(),this.focusIndex=(this.focusIndex+1)%n.length,this.updateFocus(n)):t.key==="ArrowUp"?(t.preventDefault(),this.focusIndex=this.focusIndex<=0?n.length-1:this.focusIndex-1,this.updateFocus(n)):t.key==="Enter"?this.focusIndex>=0&&this.currentItems[this.focusIndex]&&(t.preventDefault(),this.select(this.currentItems[this.focusIndex])):t.key==="Escape"?this.hide():t.key==="Tab"&&this.focusIndex===-1&&n.length>0&&(t.preventDefault(),this.focusIndex=0,this.updateFocus(n)))}updateFocus(t){t.forEach((n,r)=>{let o=r===this.focusIndex;n.classList.toggle("focused",o),n.setAttribute("aria-selected",o?"true":"false"),o&&n.scrollIntoView({block:"nearest"})})}destroy(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null)}};async function J(e,t,n){let r=new URL(Re,e);r.searchParams.set("q",t),r.searchParams.set("limit",String(8));let o=new AbortController,s=setTimeout(()=>o.abort(),n);try{let i=await fetch(r,{signal:o.signal});if(!i.ok)throw new Error("HTTP "+i.status);let a=await i.json();return Array.isArray(a.actors)?a.actors:[]}finally{clearTimeout(s)}}async function Me(e){if(e.length===0)return[];let t=new URL(De,le);e.forEach(o=>t.searchParams.append("actors",o));let n=new AbortController,r=setTimeout(()=>n.abort(),3e3);try{let o=await fetch(t,{signal:n.signal});if(!o.ok)return[];let s=await o.json();return Array.isArray(s.profiles)?s.profiles:[]}catch{return[]}finally{clearTimeout(r)}}function _(){try{return JSON.parse(localStorage.getItem(ce)||"{}")}catch{return{}}}function ae(e){try{localStorage.setItem(ce,JSON.stringify(e))}catch{}}function Ne(){try{let e=localStorage.getItem(Oe);return e?JSON.parse(e):[]}catch{return[]}}var I=null;function ue(){let e=document.getElementById("handle");e&&(I&&I.input===e||(I&&I.destroy(),I=new Y(e)))}document.addEventListener("DOMContentLoaded",ue);document.body.addEventListener("htmx:afterSettle",ue);document.body.addEventListener("htmx:beforeSwap",()=>{I&&!document.contains(I.input)&&(I.destroy(),I=null)});function U(e){try{return localStorage.getItem(e)}catch{return null}}function V(e,t){try{localStorage.setItem(e,t)}catch{}}function me(){return U("theme")||"system"}function ke(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function G(){let e=me(),n=ke(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),qe(e)}function ge(e){V("theme",e),G(),Pe()}function qe(e){let t={system:"sun-moon",light:"sun",dark:"moon"};document.querySelectorAll("[data-theme-icon] use").forEach(n=>{n.setAttribute("href",`/icons.svg#${t[e]||"sun-moon"}`)}),document.querySelectorAll(".theme-option").forEach(n=>{let r=n.dataset.value===e;n.setAttribute("aria-checked",r?"true":"false");let o=n.querySelector(".theme-check");o&&(o.style.visibility=r?"visible":"hidden")})}function Pe(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");if(!t)return;let n=()=>e.setAttribute("aria-expanded",t.open?"true":"false");n(),t.addEventListener("toggle",n)})});window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{me()==="system"&&G()});function pe(e,t){if(!e)return;let n=e.querySelector(".nav-search-form"),r=e.querySelector('button[aria-controls="nav-search-form"]');e.classList.toggle("expanded",t),n&&(t?n.removeAttribute("inert"):n.setAttribute("inert","")),r&&r.setAttribute("aria-expanded",t?"true":"false")}function Fe(){let e=document.querySelector(".nav-search-wrapper");if(!e)return;let t=!e.classList.contains("expanded");if(pe(e,t),t){let n=document.getElementById("nav-search-input");n&&n.focus()}}function de(){let e=document.querySelector(".nav-search-wrapper");if(pe(e,!1),e){let t=e.querySelector('[aria-controls="nav-search-form"]');t&&t.focus()}}document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(document.addEventListener("keydown",n=>{if(n.key==="Escape"&&e.classList.contains("expanded")&&de(),n.key==="/"&&!e.classList.contains("expanded")){let r=n.target.tagName;if(r==="INPUT"||r==="TEXTAREA"||n.target.isContentEditable)return;n.preventDefault(),e.classList.add("expanded"),t.focus()}}),document.addEventListener("click",n=>{e.classList.contains("expanded")&&!e.contains(n.target)&&de()}))});function K(e,t){let n=()=>{if(!t||!document.contains(t))return;let r=t.innerHTML;t.innerHTML='<svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#check"></use></svg> Copied!',setTimeout(()=>{document.contains(t)&&(t.innerHTML=r)},2e3)};if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(e).then(n).catch(r=>{console.error("Clipboard API failed, falling back:",r),fe(e)?n():T("Copy failed \u2014 check browser permissions","error")});return}fe(e)?n():T("Copy failed \u2014 select the text and copy manually","error")}function fe(e){let t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.setAttribute("aria-hidden","true"),t.style.position="fixed",t.style.top="0",t.style.left="0",t.style.width="1px",t.style.height="1px",t.style.opacity="0",t.style.pointerEvents="none",document.body.appendChild(t);let n=!1;try{t.focus(),t.select(),t.setSelectionRange(0,e.length),n=document.execCommand&&document.execCommand("copy")}catch{n=!1}return document.body.removeChild(t),!!n}function Be(e){let t=s=>{let i=(s==null?"":String(s)).trim();return/[",\n\r]/.test(i)?'"'+i.replace(/"/g,'""')+'"':i},n=s=>Array.from(s).map(i=>t(i.textContent)).join(","),r=[],o=e.querySelector("thead tr");return o&&r.push(n(o.querySelectorAll("th,td"))),e.querySelectorAll("tbody tr").forEach(s=>{r.push(n(s.querySelectorAll("td,th")))}),r.join(` 2 + `)}document.addEventListener("DOMContentLoaded",()=>{document.addEventListener("click",e=>{let t=e.target.closest("button[data-copy-csv]");if(t){let r=t.closest("[data-csv-section]"),o=r&&r.querySelector("table");o&&K(Be(o),t);return}let n=e.target.closest("button[data-cmd]");if(n){K(n.getAttribute("data-cmd"),n);return}})});function _e(e){let t=Math.floor((new Date-new Date(e))/1e3),n={year:31536e3,month:2592e3,week:604800,day:86400,hour:3600,minute:60,second:1};for(let[r,o]of Object.entries(n)){let s=Math.floor(t/o);if(s>=1)return s===1?`1 ${r} ago`:`${s} ${r}s ago`}return"just now"}function j(){document.querySelectorAll("time[datetime]").forEach(e=>{let t=e.getAttribute("datetime");if(t&&!e.dataset.noUpdate){let n=_e(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{j(),G(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{ge(t.dataset.value)})})}),document.addEventListener("click",e=>{let t=e.target.closest("details.dropdown");document.querySelectorAll("details.dropdown[open]").forEach(n=>{n!==t&&n.removeAttribute("open")})})});document.addEventListener("htmx:afterSwap",j);var M=null;function ye(){M===null&&(M=setInterval(j,6e4))}function Ue(){M!==null&&(clearInterval(M),M=null)}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?Ue():(j(),ye())});ye();async function Ve(e,t,n){try{let r=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!1})});if(r.status===409){let o=await r.json();je(e,t,n,o.tags)}else if(r.ok)Ee(n);else{let o=await r.text();T(`Failed to delete manifest: ${o||r.status}`,"error")}}catch(r){console.error("Error deleting manifest:",r),T(`Error deleting manifest: ${r.message}`,"error")}}function je(e,t,n,r){let o=document.getElementById("manifest-delete-modal"),s=document.getElementById("manifest-delete-tags"),i=document.getElementById("confirm-manifest-delete-btn");s.innerHTML="",r.forEach(a=>{let l=document.createElement("li");l.textContent=a,s.appendChild(l)}),i.onclick=()=>We(e,t,n),Z(o)}function Q(){N(document.getElementById("manifest-delete-modal"))}async function We(e,t,n){let r=document.getElementById("confirm-manifest-delete-btn"),o=r.textContent;try{r.disabled=!0,r.textContent="Deleting...";let s=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!0})});if(s.ok)Q(),Ee(n),location.reload();else{let i=await s.text();T(`Failed to delete manifest: ${i||s.status}`,"error"),r.disabled=!1,r.textContent=o}}catch(s){console.error("Error deleting manifest:",s),T(`Error deleting manifest: ${s.message}`,"error"),r.disabled=!1,r.textContent=o}}async function Xe(e){let t=document.getElementById("confirm-untagged-delete-btn"),n=t.textContent,r=()=>{t.disabled=!1,t.textContent=n};try{t.disabled=!0,t.textContent="Deleting...";let o=await fetch("/api/manifests/untagged",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e})}),s=await o.text(),i=null;try{i=s?JSON.parse(s):null}catch{}if(o.ok){let a=i&&i.deleted||0,l=i&&i.failed||0;if(N(document.getElementById("untagged-delete-modal")),l>0?T(`Deleted ${a} of ${a+l} untagged manifest(s); ${l} failed`,"error"):a>0?T(`Deleted ${a} untagged manifest(s)`,"success"):T("No untagged manifests to delete","info"),a>0){location.reload();return}r()}else{let a=i&&i.error||s||`HTTP ${o.status}`,l=i&&i.deleted?` (${i.deleted} succeeded before failure)`:"";T(`Failed to delete untagged manifests: ${a}${l}`,"error"),r()}}catch(o){console.error("Error deleting untagged manifests:",o),T(`Error: ${o.message}`,"error"),r()}}function Ee(e){let t=document.getElementById(`manifest-${e}`);t&&t.remove()}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("manifest-delete-modal");e&&e.addEventListener("click",t=>{t.target===e&&Q()})});var k=new WeakMap;function Z(e,t){if(e&&(k.set(e,t||document.activeElement),typeof e.showModal=="function")){e.open&&(e.open=!1);try{e.showModal()}catch{}}}function N(e,{remove:t=!1}={}){if(!e)return;let n=k.get(e);if(k.delete(e),typeof e.close=="function"&&e.open)try{e.close()}catch{}t&&e.remove(),ve(n)}function ve(e){e&&typeof e.focus=="function"&&document.contains(e)&&e.focus()}document.addEventListener("close",e=>{let t=e.target;if(!(t instanceof HTMLDialogElement))return;let n=k.get(t);k.delete(t),ve(n)},!0);document.body.addEventListener("htmx:afterSettle",()=>{document.querySelectorAll("dialog.modal-open:not([data-modal-promoted]), dialog[open]:not([data-modal-promoted])").forEach(t=>{t.dataset.modalPromoted="1",Z(t)})});document.addEventListener("change",e=>{let t=e.target.closest("select[data-diff-url]");if(!t)return;let n=t.dataset.diffUrl;n&&(window.location.href=n.replace("__VALUE__",encodeURIComponent(t.value)))});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("pull-cmd-container");if(!e)return;let t=e.dataset.registryUrl,n=e.dataset.ownerHandle,r=e.dataset.repoName,o=e.dataset.tag||"latest",s=e.dataset.isLoggedIn==="true";function i(l){let u=(l==="none"?"":l+" pull ")+t+"/"+n+"/"+r+":"+o,d=document.getElementById("pull-cmd-display");if(!d)return;let f=d.querySelector("code");f&&(f.textContent=u);let m=d.querySelector("[data-cmd]");m&&(m.dataset.cmd=u),s&&window.htmx?window.htmx.ajax("POST","/api/profile/oci-client",{values:{oci_client:l},swap:"none"}):s||V("oci-client",l)}if(!s){let l=U("oci-client");if(l){let c=document.getElementById("oci-client-switcher");c&&(c.value=l,i(l))}}let a=document.getElementById("oci-client-switcher");a&&a.addEventListener("change",()=>i(a.value))});function be(e){let t=document.getElementById("helm-cmd-container");if(!t)return;let n=t.dataset.registryUrl,r=t.dataset.ownerHandle,o=t.dataset.repoName,s=t.dataset.tag||"",i="oci://"+n+"/"+r+"/"+o,a=s?" --version "+s:"",l=e==="pull"?"helm pull "+i+a:"helm install "+o+" "+i+a,c=document.getElementById("helm-cmd-display");if(!c)return;let u=c.querySelector("code");u&&(u.textContent=l);let d=c.querySelector("[data-cmd]");d&&(d.dataset.cmd=l)}function we(){let e=document.getElementById("helm-cmd-switcher");if(!e)return;let t=U("helm-cmd");(t==="install"||t==="pull")&&(e.value=t),be(e.value)}document.addEventListener("DOMContentLoaded",we);document.body.addEventListener("htmx:afterSettle",we);document.addEventListener("change",e=>{!e.target||e.target.id!=="helm-cmd-switcher"||(V("helm-cmd",e.target.value),be(e.target.value))});document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelectorAll(".platform-tab[data-platform]");e.length&&e.forEach(t=>{t.addEventListener("click",()=>{e.forEach(r=>{let o=r===t;r.classList.toggle("btn-primary",o),r.classList.toggle("btn-ghost",!o),r.setAttribute("aria-selected",o?"true":"false"),r.setAttribute("tabindex",o?"0":"-1")}),document.querySelectorAll(".platform-content").forEach(r=>{r.classList.add("hidden"),r.setAttribute("hidden","")});let n=document.getElementById(t.dataset.platform+"-content");n&&(n.classList.remove("hidden"),n.removeAttribute("hidden"),t.focus())}),t.addEventListener("keydown",n=>{if(n.key!=="ArrowLeft"&&n.key!=="ArrowRight")return;n.preventDefault();let r=Array.from(e),o=r.indexOf(t);(n.key==="ArrowRight"?r[(o+1)%r.length]:r[(o-1+r.length)%r.length]).click()})})});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("login-form");e&&e.addEventListener("submit",()=>{let t=e.querySelector('button[type="submit"]');!t||t.disabled||(t.disabled=!0,t.innerHTML='<span class="loading loading-spinner loading-sm align-middle"></span> Navigating&hellip;')})});document.addEventListener("DOMContentLoaded",()=>{let e=document.cookie.split("; ").find(n=>n.startsWith("atcr_login_handle="));if(!e)return;let t=decodeURIComponent(e.split("=")[1]);if(t&&typeof t=="string"&&t.length>0){try{let n="atcr_recent_handles",r=U(n),o=[];try{o=JSON.parse(r||"[]")}catch{o=[]}Array.isArray(o)||(o=[]),o=o.filter(s=>s!==t),o.unshift(t),o=o.slice(0,5),V(n,JSON.stringify(o))}catch(n){console.error("Failed to save recent account:",n)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});function he(){let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),n=document.getElementById("carousel-next");if(!e)return;let r=e.querySelectorAll(".carousel-item");if(r.length===0||!r[0])return;let o=null,s=5e3,i=window.matchMedia("(prefers-reduced-motion: reduce)"),a=()=>i.matches?"auto":"smooth",l=0,c=0;function u(){if(!r[0])return;let y=parseFloat(getComputedStyle(e).gap)||24;l=r[0].offsetWidth+y}u(),window.addEventListener("resize",()=>{cancelAnimationFrame(c),c=requestAnimationFrame(u)}),document.body.addEventListener("htmx:afterSettle",y=>{y.target&&y.target.contains&&y.target.contains(e)&&u()});function d(){let y=e.scrollWidth-e.clientWidth;e.scrollLeft>=y-10?e.scrollTo({left:0,behavior:a()}):e.scrollBy({left:l,behavior:a()})}function f(){e.scrollLeft<=10?e.scrollTo({left:e.scrollWidth,behavior:a()}):e.scrollBy({left:-l,behavior:a()})}function m(){o||document.visibilityState!=="hidden"&&(e.scrollWidth<=e.clientWidth+10||i.matches||(o=setInterval(d,s)))}function h(){o&&(clearInterval(o),o=null)}t&&t.addEventListener("click",()=>{h(),f(),m()}),n&&n.addEventListener("click",()=>{h(),d(),m()});let g=document.getElementById("carousel-pause"),p=!1;if(g){let y=g.querySelector(".carousel-pause-icon"),w=g.querySelector(".carousel-play-icon");g.setAttribute("aria-pressed","false"),g.setAttribute("aria-label","Pause carousel auto-advance"),g.addEventListener("click",()=>{p=!p,p?(h(),g.setAttribute("aria-pressed","true"),g.setAttribute("aria-label","Resume carousel auto-advance"),y&&y.classList.add("hidden"),w&&w.classList.remove("hidden")):(g.setAttribute("aria-pressed","false"),g.setAttribute("aria-label","Pause carousel auto-advance"),y&&y.classList.remove("hidden"),w&&w.classList.add("hidden"),m())})}e.addEventListener("mouseenter",h),e.addEventListener("mouseleave",()=>{p||m()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?h():p||m()}),m()}document.addEventListener("DOMContentLoaded",()=>{"requestIdleCallback"in window?requestIdleCallback(he,{timeout:2e3}):setTimeout(he,100)});document.body.addEventListener("htmx:responseError",e=>{let t=e.detail&&e.detail.elt;if(t&&t.closest&&t.closest("[data-suppress-htmx-toast]"))return;let n=e.detail&&e.detail.xhr,r=n&&n.getResponseHeader&&n.getResponseHeader("HX-Trigger");if(r&&r.indexOf("toast")!==-1)return;let o=n?n.status:0,s=o===401?"Session expired \u2014 please sign in again":o===403?"Not authorized":o===404?"Not found":o===429?"Too many requests \u2014 please slow down":o>=500?"Server error \u2014 please try again":"Something went wrong";T(s,"error")});document.body.addEventListener("htmx:sendError",e=>{let t=e.detail&&e.detail.elt;t&&t.closest&&t.closest("[data-suppress-htmx-toast]")||T("Network error \u2014 check your connection","error")});document.body.addEventListener("toast",e=>{let t=e&&e.detail||{},n=t.message||t.msg||"";if(!n)return;let r=t.type||"info";T(n,r)});var $e=4,ze=1500;function xe(){let e=document.getElementById("toast-container");return e||(e=document.createElement("div"),e.id="toast-container",e.className="toast toast-end toast-bottom z-50",e.setAttribute("aria-live","polite"),e.setAttribute("aria-atomic","false"),document.body&&document.body.appendChild(e),e)}document.addEventListener("DOMContentLoaded",xe);function T(e,t){let n=xe(),r=(t||"info")+"|"+e,o=Date.now(),s=n.querySelector(`[data-toast-key="${Ye(r)}"]`);if(s&&o-Number(s.dataset.toastAt)<ze){Je(s);return}let i=t==="error",a=i?"alert-error":"alert-success",l=document.createElement("div");l.className=`alert ${a} shadow-lg transition-opacity duration-300`,l.style.willChange="opacity",l.setAttribute("role",i?"alert":"status"),l.dataset.toastKey=r,l.dataset.toastAt=String(o);let c=document.createElement("span");for(c.textContent=e,l.appendChild(c),n.appendChild(l);n.children.length>$e;)n.firstElementChild.remove();Te(l)}function Te(e){e._dismissTimer=setTimeout(()=>{e.style.opacity="0",e._removeTimer=setTimeout(()=>e.remove(),300)},3e3)}function Je(e){clearTimeout(e._dismissTimer),clearTimeout(e._removeTimer),e.style.opacity="",e.dataset.toastAt=String(Date.now()),Te(e)}function Ye(e){return window.CSS&&CSS.escape?CSS.escape(e):String(e).replace(/[^a-zA-Z0-9_-]/g,t=>"\\"+t)}async function Ke(e){try{let t=await fetch(`/api/webhooks/${e}/test`,{method:"POST",credentials:"include"}),n=await t.text();n.includes('class="success"')||t.ok&&!n.includes('class="error"')?T("Test webhook delivered successfully!","success"):T("Test delivery failed \u2014 check the webhook URL","error")}catch{T("Failed to reach server","error")}}(function(){let t={"switch-repo-tab":s=>window.switchRepoTab&&window.switchRepoTab(s.dataset.tab),"switch-editor-tab":s=>window.switchEditorTab&&window.switchEditorTab(s.dataset.tab),"insert-md":s=>window.insertMd&&window.insertMd(s.dataset.mdType),"toggle-editor":s=>window.toggleOverviewEditor&&window.toggleOverviewEditor(s.dataset.show==="true"),"show-modal":s=>Z(document.getElementById(s.dataset.modalId),s),"close-dialog":s=>N(s.closest("dialog")),"remove-closest-dialog":s=>N(s.closest("dialog"),{remove:!0}),"close-manifest-delete-modal":()=>window.closeManifestDeleteModal&&window.closeManifestDeleteModal(),"save-overview":()=>window.saveOverview&&window.saveOverview(),"delete-manifest":s=>window.deleteManifest&&window.deleteManifest(s.dataset.repo,s.dataset.digest,s.dataset.manifestId||""),"delete-untagged":s=>window.deleteUntaggedManifests&&window.deleteUntaggedManifests(s.dataset.repo),copy:s=>window.copyToClipboard&&window.copyToClipboard(s.dataset.copy,s),"toggle-search":()=>window.toggleSearch&&window.toggleSearch(),"switch-settings-tab":s=>window.switchSettingsTab&&window.switchSettingsTab(s.dataset.tab),"test-webhook":s=>window.testWebhook&&window.testWebhook(s.dataset.webhookId),"diff-to":(s,i)=>window.diffToTag&&window.diffToTag(i,s),"modal-backdrop-close":(s,i)=>{i.target===s&&N(s,{remove:!0})}},n={"sort-tags":s=>window.sortTags&&window.sortTags(s.value),"submit-form":s=>s.form&&s.form.requestSubmit()},r={"filter-tags":s=>window.filterTags&&window.filterTags(s.value)};function o(s,i){let a=i.target.closest("[data-action]");if(!a)return;let l=s[a.dataset.action];l&&l(a,i)}document.addEventListener("click",s=>o(t,s)),document.addEventListener("change",s=>o(n,s)),document.addEventListener("input",s=>o(r,s))})();window.setTheme=ge;window.toggleSearch=Fe;window.copyToClipboard=K;window.deleteManifest=Ve;window.deleteUntaggedManifests=Xe;window.closeManifestDeleteModal=Q;window.showToast=T;window.testWebhook=Ke;function Ge(){let e=document.getElementById("md-editor");if(!e)return;let t=e.dataset.ownerDid,n=e.dataset.repository;window.toggleOverviewEditor=function(r){document.getElementById("overview-view").classList.toggle("hidden",r),document.getElementById("overview-edit").classList.toggle("hidden",!r),r&&e.focus()},window.switchEditorTab=function(r){if(document.querySelectorAll(".editor-panel").forEach(o=>o.classList.add("hidden")),document.getElementById(r==="write"?"editor-write":"editor-preview").classList.remove("hidden"),document.querySelectorAll(".editor-tab").forEach(o=>{let s=o.dataset.tab===r;o.classList.toggle("border-primary",s),o.classList.toggle("text-primary",s),o.classList.toggle("border-transparent",!s),o.classList.toggle("text-base-content/60",!s)}),r==="preview"){let o=e.value,s=document.getElementById("preview-content");if(!o.trim()){s.innerHTML='<p class="text-base-content/60">Nothing to preview</p>';return}s.innerHTML='<p class="text-base-content/60"><span class="loading loading-spinner loading-xs align-middle"></span> Rendering preview&hellip;</p>';let i=new FormData;i.append("markdown",o),fetch("/api/repo-page/preview",{method:"POST",body:i}).then(a=>{if(!a.ok)throw new Error("HTTP "+a.status);return a.text()}).then(a=>{s.innerHTML=a}).catch(()=>{s.innerHTML='<p class="text-error">Preview failed. Check your connection and try again.</p>'})}},window.insertMd=function(r){let o=e.selectionStart,s=e.selectionEnd,i=e.value.substring(o,s),a=e.value.substring(0,o),l=e.value.substring(s),c,u,d;switch(r){case"heading":c="## "+(i||"Heading"),u=o+3,d=o+c.length;break;case"bold":c="**"+(i||"bold text")+"**",u=o+2,d=o+c.length-2;break;case"italic":c="_"+(i||"italic text")+"_",u=o+1,d=o+c.length-1;break;case"link":c="["+(i||"link text")+"](url)",u=o+c.length-4,d=o+c.length-1;break;case"image":c="!["+(i||"alt text")+"](url)",u=o+c.length-4,d=o+c.length-1;break;case"ul":c="- "+(i||"list item"),u=o+2,d=o+c.length;break;case"ol":c="1. "+(i||"list item"),u=o+3,d=o+c.length;break;case"code":i&&i.indexOf(` 3 + `)!==-1?(c="```\n"+i+"\n```",u=o+4,d=o+4+i.length):(c="`"+(i||"code")+"`",u=o+1,d=o+c.length-1);break;default:return}e.value=a+c+l,e.focus(),e.selectionStart=u,e.selectionEnd=d},window.saveOverview=function(){let r=document.getElementById("save-overview-btn");r.classList.add("btn-disabled"),r.innerHTML='<span class="loading loading-spinner loading-xs"></span> Saving...';let o=new FormData;o.append("did",t),o.append("repository",n),o.append("description",e.value),fetch("/api/repo-page",{method:"POST",body:o,headers:{"HX-Request":"true"}}).then(s=>s.ok?s.text():s.text().then(i=>{throw new Error(i)})).then(s=>{document.getElementById("overview-rendered").innerHTML=s,window.toggleOverviewEditor(!1),typeof window.showToast=="function"&&window.showToast("Overview saved","success")}).catch(s=>{typeof window.showToast=="function"&&window.showToast(s.message||"Failed to save","error")}).finally(()=>{r.classList.remove("btn-disabled"),r.innerHTML="Save"})},e.addEventListener("keydown",r=>{(r.ctrlKey||r.metaKey)&&r.key==="s"&&(r.preventDefault(),window.saveOverview())})}window.sortTags=function(e){let t=document.getElementById("tags-list");if(!t)return;let n=Array.from(t.querySelectorAll(".artifact-entry"));n.sort((r,o)=>{switch(e){case"oldest":return parseInt(r.dataset.created)-parseInt(o.dataset.created);case"az":return r.dataset.tag.localeCompare(o.dataset.tag);case"za":return o.dataset.tag.localeCompare(r.dataset.tag);default:return parseInt(o.dataset.created)-parseInt(r.dataset.created)}}),n.forEach(r=>t.appendChild(r))};var D=0;window.filterTags=function(e){D&&cancelAnimationFrame(D),D=requestAnimationFrame(()=>{D=0;let t=e.toLowerCase();document.querySelectorAll("#tags-list .artifact-entry").forEach(n=>{n.style.display=!t||n.dataset.tag.toLowerCase().includes(t)?"":"none"})})};document.body.addEventListener("htmx:beforeSwap",()=>{D&&(cancelAnimationFrame(D),D=0)});function Qe(){if(!document.getElementById("tag-content"))return;let e=["overview","layers","vulns","sbom","artifacts","chart"],t={};function n(i,a){if(t[i]==="loading"||t[i]==="loaded")return;t[i]="loading";let l=document.getElementById(i);if(!l){delete t[i];return}let c=new AbortController,u=setTimeout(()=>c.abort(),1e4);fetch(a,{signal:c.signal}).then(d=>{if(!d.ok)throw new Error("HTTP "+d.status);return d.text()}).then(d=>{t[i]="loaded",document.contains(l)&&(l.innerHTML=d,l.querySelectorAll("script").forEach(f=>{let m=document.createElement("script");m.textContent=f.textContent,f.parentNode.replaceChild(m,f)}),typeof window.htmx<"u"&&window.htmx.process(l))}).catch(d=>{if(delete t[i],!document.contains(l))return;let m=d&&d.name==="AbortError"?"This section took too long to load.":"Couldn't load this section.";l.innerHTML='<div class="py-6 text-sm text-base-content/70"><p>'+m+'</p><button type="button" class="btn btn-sm btn-ghost mt-2" data-retry-section="'+i+'">Try again</button></div>'}).finally(()=>clearTimeout(u))}document.body.addEventListener("click",i=>{let a=i.target.closest("[data-retry-section]");if(!a)return;let l=a.getAttribute("data-retry-section"),u={"artifacts-content":o,"layers-content":()=>r("layers"),"vulns-content":()=>r("vulns"),"sbom-content":()=>r("sbom")}[l];if(u){let d=u();d&&n(l,d)}});function r(i){let a=document.getElementById("tag-content");if(!a||!a.dataset)return null;let l=a.dataset.digest,c=a.dataset.owner,u=a.dataset.repo;return!l||!c||!u?null:"/api/digest-content/"+c+"/"+u+"?digest="+encodeURIComponent(l)+"&section="+i}function o(){let i=document.getElementById("tag-content");if(!i||!i.dataset)return null;let a=i.dataset.owner,l=i.dataset.repo;return!a||!l?null:"/api/repo-tags/"+a+"/"+l}window.diffToTag=function(i,a){i.preventDefault();let l=a.dataset.diffTo,c=document.getElementById("tag-content"),u=document.getElementById("tag-selector");if(!c||!u||!l)return;let d=c.dataset.digest,f=u.value;!d||l===f||(window.location.href="/diff/"+c.dataset.owner+"/"+c.dataset.repo+"?from="+encodeURIComponent(d)+"&to="+encodeURIComponent(l))},window.switchRepoTab=function(i){window._activeRepoTab=i;let a=document.getElementById("tag-content");if(!a)return;a.querySelectorAll(".repo-panel").forEach(u=>u.classList.add("hidden"));let l=document.getElementById("tab-"+i);l&&l.classList.remove("hidden"),a.querySelectorAll(".repo-tab").forEach(u=>{let d=u.dataset.tab===i;u.classList.toggle("border-primary",d),u.classList.toggle("text-primary",d),u.classList.toggle("border-transparent",!d),u.classList.toggle("text-base-content/60",!d),u.setAttribute("aria-selected",d?"true":"false"),u.setAttribute("tabindex",d?"0":"-1")});let c=new URL(window.location);if(c.hash=i,history.replaceState(null,"",c.toString()),i==="artifacts"){let u=o();u&&n("artifacts-content",u)}if(i==="layers"){let u=r("layers");u&&n("layers-content",u)}if(i==="vulns"){let u=r("vulns");u&&n("vulns-content",u)}if(i==="sbom"){let u=r("sbom");u&&n("sbom-content",u)}if(i==="chart"){let u=r("chart");u&&n("chart-content",u)}};function s(){t={},[["artifacts-tab-btn","artifacts-content",o],["layers-tab-btn","layers-content",()=>r("layers")],["vulns-tab-btn","vulns-content",()=>r("vulns")],["sbom-tab-btn","sbom-content",()=>r("sbom")],["chart-tab-btn","chart-content",()=>r("chart")]].forEach(([c,u,d])=>{let f=document.getElementById(c);f&&f.addEventListener("mouseenter",()=>{let m=d();m&&n(u,m)},{once:!0})});let a=document.querySelector('[role="tablist"][aria-label="Repository sections"]');a&&!a.dataset.keyboardBound&&(a.dataset.keyboardBound="1",a.addEventListener("keydown",c=>{let u=Array.from(a.querySelectorAll(".repo-tab")),d=u.indexOf(document.activeElement);if(d===-1)return;let f=-1;switch(c.key){case"ArrowRight":f=(d+1)%u.length;break;case"ArrowLeft":f=(d-1+u.length)%u.length;break;case"Home":f=0;break;case"End":f=u.length-1;break;case"Enter":case" ":c.preventDefault(),window.switchRepoTab(u[d].dataset.tab);return;default:return}c.preventDefault(),u[f].focus()}));let l=window._activeRepoTab||window.location.hash.replace("#","")||"overview";e.indexOf(l)===-1&&(l="overview"),window.switchRepoTab(l)}s(),document.addEventListener("keydown",i=>{if(i.target.tagName==="INPUT"||i.target.tagName==="TEXTAREA"||i.target.tagName==="SELECT"||i.target.isContentEditable||i.ctrlKey||i.metaKey||i.altKey)return;let l={o:"overview",l:"layers",v:"vulns",s:"sbom",a:"artifacts",c:"chart"}[i.key.toLowerCase()];l&&e.indexOf(l)!==-1&&window.switchRepoTab(l)}),document.body.addEventListener("htmx:afterSettle",i=>{i.detail.target&&i.detail.target.id==="tag-content"&&s()})}document.addEventListener("DOMContentLoaded",()=>{Ge(),Qe()});function Ze(){let e=Array.from(document.querySelectorAll('.menu li[data-tab] a[role="tab"]')),t=Array.from(document.querySelectorAll(".settings-tab-mobile"));if(!e.length&&!t.length)return;function n(s,i){let a=i==="vertical"?"ArrowUp":"ArrowLeft",l=i==="vertical"?"ArrowDown":"ArrowRight";s.forEach(c=>{c.addEventListener("keydown",u=>{let d=s.indexOf(u.currentTarget);if(d===-1)return;let f=null;u.key===a?f=s[(d-1+s.length)%s.length]:u.key===l?f=s[(d+1)%s.length]:u.key==="Home"?f=s[0]:u.key==="End"&&(f=s[s.length-1]),f&&(u.preventDefault(),f.focus(),f.click())})})}n(e,"vertical"),n(t,"horizontal");function r(){let s=t.find(i=>i.getAttribute("aria-selected")==="true");s&&s.scrollIntoView({inline:"center",block:"nearest"})}r();function o(s){e.forEach(i=>{let a=i.parentElement.dataset.tab===s;i.setAttribute("aria-selected",a?"true":"false"),i.setAttribute("tabindex",a?"0":"-1"),i.parentElement.classList.toggle("menu-active",a)}),t.forEach(i=>{let a=i.dataset.tab===s;i.setAttribute("aria-selected",a?"true":"false"),i.setAttribute("tabindex",a?"0":"-1"),i.classList.toggle("btn-secondary",a),i.classList.toggle("btn-ghost",!a)}),r()}[...e,...t].forEach(s=>{s.addEventListener("click",()=>o(s.dataset.tab||s.parentElement.dataset.tab))}),document.body.addEventListener("htmx:historyRestore",()=>{let s=location.pathname.match(/^\/settings\/(user|storage|billing|devices|webhooks|advanced)/);s&&o(s[1])})}function et(){document.addEventListener("click",function(n){let r=n.target.closest("#delete-account-btn");r&&t(r)});function e(n){let r=document.createElement("div");return r.textContent=n,r.innerHTML}function t(n){let r=n.dataset.clientShortName||"this account",s="DELETE "+(n.dataset.profileHandle||""),i=document.getElementById("delete-pds-records").checked,a=document.createElement("div");a.className="modal modal-open",a.innerHTML=` 4 4 <div class="modal-box bg-base-200 max-w-lg"> 5 5 <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 6 6 <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#alert-triangle"></use></svg> ··· 39 39 </div> 40 40 </div> 41 41 <div class="modal-backdrop bg-black/50" id="modal-backdrop"></div> 42 - `,document.body.appendChild(a);let l=document.getElementById("confirm-delete-input"),c=document.getElementById("confirm-delete"),d=document.getElementById("cancel-delete");setTimeout(()=>l.focus(),100),l.addEventListener("input",function(){c.disabled=this.value!==s}),l.addEventListener("keydown",function(m){m.key==="Enter"&&this.value===s&&f()}),d.addEventListener("click",()=>a.remove()),document.getElementById("modal-backdrop").addEventListener("click",()=>a.remove());function u(m){m.key==="Escape"&&(a.remove(),document.removeEventListener("keydown",u))}document.addEventListener("keydown",u),c.addEventListener("click",f);async function f(){let m=document.getElementById("delete-pds-records").checked;c.disabled=!0,c.innerHTML='<svg class="icon size-4 animate-spin" aria-hidden="true"><use href="/icons.svg#loader-2"></use></svg> Deleting...',d.disabled=!0;try{let h=await fetch("/api/account",{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({delete_pds_records:m,confirmation:s})}),g=await h.json();if(h.ok&&g.success)a.querySelector(".modal-box").innerHTML=` 42 + `,document.body.appendChild(a);let l=document.getElementById("confirm-delete-input"),c=document.getElementById("confirm-delete"),u=document.getElementById("cancel-delete");setTimeout(()=>l.focus(),100),l.addEventListener("input",function(){c.disabled=this.value!==s}),l.addEventListener("keydown",function(m){m.key==="Enter"&&this.value===s&&f()}),u.addEventListener("click",()=>a.remove()),document.getElementById("modal-backdrop").addEventListener("click",()=>a.remove());function d(m){m.key==="Escape"&&(a.remove(),document.removeEventListener("keydown",d))}document.addEventListener("keydown",d),c.addEventListener("click",f);async function f(){let m=document.getElementById("delete-pds-records").checked;c.disabled=!0,c.innerHTML='<svg class="icon size-4 animate-spin" aria-hidden="true"><use href="/icons.svg#loader-2"></use></svg> Deleting...',u.disabled=!0;try{let h=await fetch("/api/account",{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({delete_pds_records:m,confirmation:s})}),g=await h.json();if(h.ok&&g.success)a.querySelector(".modal-box").innerHTML=` 43 43 <h2 class="text-xl font-bold flex items-center gap-2 text-success"> 44 44 <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 45 45 Account Deleted ··· 73 73 <div class="modal-action"> 74 74 <button type="button" class="btn" data-dismiss-modal>Close</button> 75 75 </div> 76 - `,a.querySelector("[data-dismiss-modal]").addEventListener("click",()=>a.remove())}}}}document.addEventListener("DOMContentLoaded",()=>{Ge(),Qe()});var ee="showEmptyLayers";function Ze(e,t,n){let r=0;for(let o=t;o<t+n;o++){let s=e[o].querySelector("td[data-bytes]");if(!s)continue;let i=Number(s.dataset.bytes);Number.isFinite(i)&&(r+=i)}return r}function et(e){return e<1024?e+" B":e<1048576?(e/1024).toFixed(1)+" KB":e<1073741824?(e/1048576).toFixed(1)+" MB":(e/1073741824).toFixed(1)+" GB"}function tt(e){let t=e.querySelector("tbody");if(!t)return;let n=Array.from(t.querySelectorAll("tr")),r=0;for(;r<n.length;){if(n[r].dataset.noCommand!=="true"){r++;continue}let o=r;for(;r<n.length&&n[r].dataset.noCommand==="true";)n[r].classList.add("no-history-row","hidden"),r++;let s=r-o;if(s<=1){n[o].classList.remove("hidden");continue}let i=n[o].querySelector("td").textContent.trim(),a=n[r-1].querySelector("td").textContent.trim(),l=et(Ze(n,o,s)),c=document.createElement("tr");c.className="no-history-summary cursor-pointer hover:bg-base-300",c.innerHTML='<td colspan="2" class="text-sm py-2">Layers '+i+"-"+a+' contain no history <span class="text-xs ml-2">('+s+' layers, click to expand)</span></td><td class="text-right text-sm whitespace-nowrap">'+l+"</td>",c.addEventListener("click",()=>{c.remove();for(let d=o;d<o+s;d++)n[d].classList.remove("hidden")}),t.insertBefore(c,n[o])}}function xe(e){let t=localStorage.getItem(ee)==="true";e.querySelectorAll('tr[data-empty="true"]').forEach(n=>{n.style.display=t?"":"none"})}function Te(e){let t=e||document;(t.querySelectorAll?t.querySelectorAll(".layers-table:not([data-layers-processed])"):[]).forEach(r=>{r.setAttribute("data-layers-processed","1"),tt(r),xe(r)})}function nt(e){localStorage.setItem(ee,e),document.querySelectorAll(".show-empty-layers-cb").forEach(t=>{t.checked=e}),document.querySelectorAll(".layers-table").forEach(xe)}document.addEventListener("DOMContentLoaded",()=>{let e=localStorage.getItem(ee)==="true";document.querySelectorAll(".show-empty-layers-cb").forEach(t=>{t.checked=e}),Te()});document.addEventListener("change",e=>{e.target.matches("[data-toggle-empty-layers]")&&nt(e.target.checked)});document.body.addEventListener("htmx:afterSettle",e=>{e.target&&e.target.querySelectorAll&&Te(e.target)});window.htmx=O;O.config.methodsThatUseUrlParams=["get"]; 76 + `,a.querySelector("[data-dismiss-modal]").addEventListener("click",()=>a.remove())}}}}document.addEventListener("DOMContentLoaded",()=>{Ze(),et()});var ee="showEmptyLayers";function tt(e,t,n){let r=0;for(let o=t;o<t+n;o++){let s=e[o].querySelector("td[data-bytes]");if(!s)continue;let i=Number(s.dataset.bytes);Number.isFinite(i)&&(r+=i)}return r}function nt(e){return e<1024?e+" B":e<1048576?(e/1024).toFixed(1)+" KB":e<1073741824?(e/1048576).toFixed(1)+" MB":(e/1073741824).toFixed(1)+" GB"}function rt(e){let t=e.querySelector("tbody");if(!t)return;let n=Array.from(t.querySelectorAll("tr")),r=0;for(;r<n.length;){if(n[r].dataset.noCommand!=="true"){r++;continue}let o=r;for(;r<n.length&&n[r].dataset.noCommand==="true";)n[r].classList.add("no-history-row","hidden"),r++;let s=r-o;if(s<=1){n[o].classList.remove("hidden");continue}let i=n[o].querySelector("td").textContent.trim(),a=n[r-1].querySelector("td").textContent.trim(),l=nt(tt(n,o,s)),c=document.createElement("tr");c.className="no-history-summary cursor-pointer hover:bg-base-300",c.innerHTML='<td colspan="2" class="text-sm py-2">Layers '+i+"-"+a+' contain no history <span class="text-xs ml-2">('+s+' layers, click to expand)</span></td><td class="text-right text-sm whitespace-nowrap">'+l+"</td>",c.addEventListener("click",()=>{c.remove();for(let u=o;u<o+s;u++)n[u].classList.remove("hidden")}),t.insertBefore(c,n[o])}}function Se(e){let t=localStorage.getItem(ee)==="true";e.querySelectorAll('tr[data-empty="true"]').forEach(n=>{n.style.display=t?"":"none"})}function Ce(e){let t=e||document;(t.querySelectorAll?t.querySelectorAll(".layers-table:not([data-layers-processed])"):[]).forEach(r=>{r.setAttribute("data-layers-processed","1"),rt(r),Se(r)})}function ot(e){localStorage.setItem(ee,e),document.querySelectorAll(".show-empty-layers-cb").forEach(t=>{t.checked=e}),document.querySelectorAll(".layers-table").forEach(Se)}document.addEventListener("DOMContentLoaded",()=>{let e=localStorage.getItem(ee)==="true";document.querySelectorAll(".show-empty-layers-cb").forEach(t=>{t.checked=e}),Ce()});document.addEventListener("change",e=>{e.target.matches("[data-toggle-empty-layers]")&&ot(e.target.checked)});document.body.addEventListener("htmx:afterSettle",e=>{e.target&&e.target.querySelectorAll&&Ce(e.target)});window.htmx=O;O.config.methodsThatUseUrlParams=["get"];
+74 -10
pkg/appview/src/js/app.js
··· 438 438 const confirmBtn = document.getElementById('confirm-untagged-delete-btn'); 439 439 const originalText = confirmBtn.textContent; 440 440 441 + const restoreButton = () => { 442 + confirmBtn.disabled = false; 443 + confirmBtn.textContent = originalText; 444 + }; 445 + 441 446 try { 442 447 confirmBtn.disabled = true; 443 448 confirmBtn.textContent = 'Deleting...'; ··· 449 454 body: JSON.stringify({ repo: repository }), 450 455 }); 451 456 452 - const data = await response.json(); 457 + const raw = await response.text(); 458 + let data = null; 459 + try { 460 + data = raw ? JSON.parse(raw) : null; 461 + } catch (_) { 462 + // Non-JSON body (e.g., upstream proxy error page) — fall through. 463 + } 453 464 454 465 if (response.ok) { 466 + const deleted = (data && data.deleted) || 0; 467 + const failed = (data && data.failed) || 0; 455 468 closeDialog(document.getElementById('untagged-delete-modal')); 456 - showToast(`Deleted ${data.deleted} untagged manifest(s)`, 'success'); 457 - if (data.deleted > 0) { 469 + if (failed > 0) { 470 + showToast(`Deleted ${deleted} of ${deleted + failed} untagged manifest(s); ${failed} failed`, 'error'); 471 + } else if (deleted > 0) { 472 + showToast(`Deleted ${deleted} untagged manifest(s)`, 'success'); 473 + } else { 474 + showToast('No untagged manifests to delete', 'info'); 475 + } 476 + if (deleted > 0) { 458 477 location.reload(); 478 + return; 459 479 } 460 - confirmBtn.disabled = false; 461 - confirmBtn.textContent = originalText; 480 + restoreButton(); 462 481 } else { 463 - showToast(`Failed to delete untagged manifests: ${data.error || 'Unknown error'}`, 'error'); 464 - confirmBtn.disabled = false; 465 - confirmBtn.textContent = originalText; 482 + const errMsg = (data && data.error) || raw || `HTTP ${response.status}`; 483 + const partial = data && data.deleted ? ` (${data.deleted} succeeded before failure)` : ''; 484 + showToast(`Failed to delete untagged manifests: ${errMsg}${partial}`, 'error'); 485 + restoreButton(); 466 486 } 467 487 } catch (err) { 468 488 console.error('Error deleting untagged manifests:', err); 469 489 showToast(`Error: ${err.message}`, 'error'); 470 - confirmBtn.disabled = false; 471 - confirmBtn.textContent = originalText; 490 + restoreButton(); 472 491 } 473 492 } 474 493 ··· 611 630 612 631 const sel = document.getElementById('oci-client-switcher'); 613 632 if (sel) sel.addEventListener('change', () => updatePullCommand(sel.value)); 633 + }); 634 + 635 + // Helm install/pull switcher. Persisted via localStorage (no profile setting 636 + // — too minor to round-trip through the API). Uses event delegation + 637 + // htmx:afterSettle so it survives the repo page's HTMX tag swaps; binding 638 + // on DOMContentLoaded alone would only catch the first render. 639 + function helmCmdSwitcher_render(mode) { 640 + const container = document.getElementById('helm-cmd-container'); 641 + if (!container) return; 642 + const registryURL = container.dataset.registryUrl; 643 + const ownerHandle = container.dataset.ownerHandle; 644 + const repoName = container.dataset.repoName; 645 + const tag = container.dataset.tag || ''; 646 + const ociRef = 'oci://' + registryURL + '/' + ownerHandle + '/' + repoName; 647 + const versionFlag = tag ? ' --version ' + tag : ''; 648 + const cmd = mode === 'pull' 649 + ? 'helm pull ' + ociRef + versionFlag 650 + : 'helm install ' + repoName + ' ' + ociRef + versionFlag; 651 + const display = document.getElementById('helm-cmd-display'); 652 + if (!display) return; 653 + const code = display.querySelector('code'); 654 + if (code) code.textContent = cmd; 655 + const btn = display.querySelector('[data-cmd]'); 656 + if (btn) btn.dataset.cmd = cmd; 657 + } 658 + 659 + function helmCmdSwitcher_apply() { 660 + const sel = document.getElementById('helm-cmd-switcher'); 661 + if (!sel) return; 662 + const saved = lsGet('helm-cmd'); 663 + if (saved === 'install' || saved === 'pull') { 664 + sel.value = saved; 665 + } 666 + helmCmdSwitcher_render(sel.value); 667 + } 668 + 669 + document.addEventListener('DOMContentLoaded', helmCmdSwitcher_apply); 670 + document.body.addEventListener('htmx:afterSettle', helmCmdSwitcher_apply); 671 + 672 + // Delegated change handler: works on the initial render AND on any switcher 673 + // HTMX swaps into the DOM later (repo page tag switch). 674 + document.addEventListener('change', (e) => { 675 + if (!e.target || e.target.id !== 'helm-cmd-switcher') return; 676 + lsSet('helm-cmd', e.target.value); 677 + helmCmdSwitcher_render(e.target.value); 614 678 }); 615 679 616 680 // Install page: platform tab switcher. Each .platform-tab has data-platform
+4 -2
pkg/appview/src/js/repository.js
··· 202 202 function initTabController() { 203 203 if (!document.getElementById('tag-content')) return; 204 204 205 - const validTabs = ['overview', 'layers', 'vulns', 'sbom', 'artifacts']; 205 + const validTabs = ['overview', 'layers', 'vulns', 'sbom', 'artifacts', 'chart']; 206 206 // State per target id: 'loading' while a request is in-flight, 'loaded' 207 207 // on success. On error we clear the entry so the retry button can 208 208 // trigger a fresh fetch; without a separate 'loading' marker, a failing ··· 328 328 if (tabId === 'layers') { const u = contentUrl('layers'); if (u) lazyLoad('layers-content', u); } 329 329 if (tabId === 'vulns') { const u = contentUrl('vulns'); if (u) lazyLoad('vulns-content', u); } 330 330 if (tabId === 'sbom') { const u = contentUrl('sbom'); if (u) lazyLoad('sbom-content', u); } 331 + if (tabId === 'chart') { const u = contentUrl('chart'); if (u) lazyLoad('chart-content', u); } 331 332 }; 332 333 333 334 function initTabs() { ··· 338 339 ['layers-tab-btn', 'layers-content', () => contentUrl('layers')], 339 340 ['vulns-tab-btn', 'vulns-content', () => contentUrl('vulns')], 340 341 ['sbom-tab-btn', 'sbom-content', () => contentUrl('sbom')], 342 + ['chart-tab-btn', 'chart-content', () => contentUrl('chart')], 341 343 ]; 342 344 prefetch.forEach(([btnId, targetId, urlFn]) => { 343 345 const btn = document.getElementById(btnId); ··· 386 388 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || 387 389 e.target.tagName === 'SELECT' || e.target.isContentEditable) return; 388 390 if (e.ctrlKey || e.metaKey || e.altKey) return; 389 - const map = { o: 'overview', l: 'layers', v: 'vulns', s: 'sbom', a: 'artifacts' }; 391 + const map = { o: 'overview', l: 'layers', v: 'vulns', s: 'sbom', a: 'artifacts', c: 'chart' }; 390 392 const tab = map[e.key.toLowerCase()]; 391 393 if (tab && validTabs.indexOf(tab) !== -1) window.switchRepoTab(tab); 392 394 });
+20 -7
pkg/appview/templates/components/pull-command-switcher.html
··· 9 9 Anonymous users: saves to localStorage. 10 10 */}} 11 11 {{ if eq .ArtifactType "helm-chart" }} 12 - <div class="space-y-2"> 13 - <p class="text-sm font-medium text-base-content/70">Pull this chart</p> 14 - {{ if .Tag }} 15 - {{ template "docker-command" (print "helm pull oci://" .RegistryURL "/" .OwnerHandle "/" .RepoName " --version " .Tag) }} 16 - {{ else }} 17 - {{ template "docker-command" (print "helm pull oci://" .RegistryURL "/" .OwnerHandle "/" .RepoName) }} 18 - {{ end }} 12 + {{/* Helm chart: small install/pull toggle. JS in app.js wires the 13 + switcher and persists the choice in localStorage. Default = install. */}} 14 + {{ $versionFlag := "" }} 15 + {{ if .Tag }}{{ $versionFlag = print " --version " .Tag }}{{ end }} 16 + {{ $ociRef := print "oci://" .RegistryURL "/" .OwnerHandle "/" .RepoName }} 17 + <div class="space-y-2" id="helm-cmd-container" 18 + data-registry-url="{{ .RegistryURL }}" 19 + data-owner-handle="{{ .OwnerHandle }}" 20 + data-repo-name="{{ .RepoName }}" 21 + data-tag="{{ if .Tag }}{{ .Tag }}{{ end }}"> 22 + <label for="helm-cmd-switcher" class="text-sm font-medium text-base-content/70">Use this chart</label> 23 + <div class="flex items-center gap-2"> 24 + <select id="helm-cmd-switcher" class="select select-xs select-bordered w-auto"> 25 + <option value="install" selected>helm install</option> 26 + <option value="pull">helm pull</option> 27 + </select> 28 + <div id="helm-cmd-display" class="flex-1 min-w-0" aria-live="polite"> 29 + {{ template "docker-command" (print "helm install " .RepoName " " $ociRef $versionFlag) }} 30 + </div> 31 + </div> 19 32 </div> 20 33 {{ else }} 21 34 <div class="space-y-2" id="pull-cmd-container"
+28 -4
pkg/appview/templates/pages/digest.html
··· 23 23 <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-3"> 24 24 <div class="flex flex-wrap items-start justify-between gap-4"> 25 25 <div class="space-y-2 min-w-0"> 26 - <!-- Title: tags or truncated digest --> 26 + {{/* Helm chart with parsed metadata: lead with chart name + version so the page reads as a chart, not a digest. */}} 27 + {{ $helmMeta := "" }} 28 + {{ if and .HelmContent .HelmContent.Meta }}{{ $helmMeta = .HelmContent.Meta }}{{ end }} 29 + <!-- Title --> 27 30 <div class="flex flex-wrap items-center gap-2"> 28 - {{ if .Manifest.Tags }} 31 + {{ if $helmMeta }} 32 + <h1 class="text-xl font-bold flex items-center gap-2 min-w-0"> 33 + {{ icon "helm" "size-5 text-base-content/70 shrink-0" }} 34 + <span class="truncate" title="{{ $helmMeta.Name }}">{{ $helmMeta.Name }}</span> 35 + {{ if $helmMeta.Version }}<span class="text-base-content/70 font-normal">{{ $helmMeta.Version }}</span>{{ end }} 36 + </h1> 37 + <span class="badge badge-md badge-soft badge-helm">Helm chart</span> 38 + {{ else if .Manifest.Tags }} 29 39 <h1 class="text-xl font-bold flex flex-wrap gap-x-1 items-center min-w-0">{{ range $i, $tag := .Manifest.Tags }}{{ if lt $i 3 }}{{ if $i }}<span aria-hidden="true">,</span>{{ end }}<span class="inline-block max-w-[24ch] truncate align-baseline" title="{{ $tag }}">{{ $tag }}</span>{{ end }}{{ end }}{{ if gt (len .Manifest.Tags) 3 }}<span class="text-sm font-normal text-base-content/60" title="{{ range $i, $tag := .Manifest.Tags }}{{ if $i }}, {{ end }}{{ $tag }}{{ end }}">+{{ sub (len .Manifest.Tags) 3 }} more</span>{{ end }}</h1> 30 40 {{ else }} 31 41 <h1 class="text-xl font-bold font-mono" title="{{ .Manifest.Digest }}">{{ truncateDigest (trimPrefix "sha256:" .Manifest.Digest) 16 }}</h1> 32 42 {{ end }} 33 43 {{ if .Manifest.IsManifestList }} 34 44 <span class="badge badge-md badge-soft badge-accent">Multi-arch</span> 35 - {{ else if eq .Manifest.ArtifactType "helm-chart" }} 45 + {{ else if and (eq .Manifest.ArtifactType "helm-chart") (not $helmMeta) }} 36 46 <span class="badge badge-md badge-soft badge-helm">{{ icon "helm" "size-3" }} Helm</span> 37 47 {{ end }} 38 48 {{ if .Manifest.HasAttestations }} 39 49 <span class="badge badge-md badge-soft badge-success">{{ icon "shield-check" "size-3" }} Attested</span> 40 50 {{ end }} 41 51 </div> 52 + {{ if $helmMeta }} 53 + {{/* Subtitle: appVersion + chart type */}} 54 + <div class="text-sm text-base-content/70"> 55 + {{ if $helmMeta.AppVersion }}appVersion {{ $helmMeta.AppVersion }}{{ if $helmMeta.Type }} · {{ end }}{{ end }}{{ if $helmMeta.Type }}{{ $helmMeta.Type }} chart{{ end }} 56 + {{ if $helmMeta.Deprecated }}<span class="badge badge-sm badge-warning ml-2">Deprecated</span>{{ end }} 57 + </div> 58 + {{ if .Manifest.Tags }} 59 + <div class="flex flex-wrap gap-1 text-xs"> 60 + {{ range .Manifest.Tags }}<span class="badge badge-ghost badge-sm font-mono" title="tag">{{ . }}</span>{{ end }} 61 + </div> 62 + {{ end }} 63 + {{ end }} 42 64 <!-- Digest (small) --> 43 65 <div class="flex items-center gap-2 text-base-content/70"> 44 66 <code class="font-mono text-xs" title="{{ .Manifest.Digest }}">{{ truncateDigest (trimPrefix "sha256:" .Manifest.Digest) 16 }}</code> ··· 81 103 hx-swap="innerHTML"> 82 104 </div> 83 105 84 - <!-- Content: Layers + Vulnerabilities --> 106 + <!-- Content: Layers + Vulnerabilities (or chart info for helm) --> 85 107 <div id="digest-content" aria-live="polite" aria-busy="false"> 86 108 {{ if .Manifest.IsManifestList }} 87 109 {{ if .Manifest.Platforms }} ··· 98 120 {{ else }} 99 121 <p class="py-12 text-center text-base-content/60">No platform manifests found for this image index.</p> 100 122 {{ end }} 123 + {{ else if eq .Manifest.ArtifactType "helm-chart" }} 124 + {{ template "helm-digest-content" (dict "Manifest" .Manifest "HelmContent" .HelmContent "RegistryURL" .RegistryURL "OwnerHandle" .Owner.Handle "RepoName" .Repository "OciClient" .OciClient "IsLoggedIn" (ne .User nil)) }} 101 125 {{ else }} 102 126 {{ template "digest-content" . }} 103 127 {{ end }}
+3 -23
pkg/appview/templates/pages/repository.html
··· 120 120 </div> 121 121 {{ end }} 122 122 </div> 123 - {{ if .SelectedTag.Info.IsMultiArch }} 124 - <div id="platform-badges" class="flex flex-wrap items-center gap-1"> 125 - {{ range .SelectedTag.Info.Platforms }} 126 - <span class="badge badge-sm badge-outline font-mono">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 127 - {{ end }} 128 - </div> 129 - {{ else if gt (len .SelectedTag.Info.Platforms) 0 }} 130 - {{ $p := index .SelectedTag.Info.Platforms 0 }} 131 - {{ if $p.OS }} 132 - <div id="platform-badges"> 133 - <span class="badge badge-sm badge-outline font-mono">{{ $p.OS }}/{{ $p.Architecture }}{{ if $p.Variant }}/{{ $p.Variant }}{{ end }}</span> 134 - </div> 135 - {{ end }} 136 - {{ end }} 137 - {{ if .SelectedTag.Info.HasAttestations }} 138 - <button class="badge badge-sm badge-soft badge-success cursor-pointer hover:opacity-80" 139 - hx-get="/api/attestation-details?digest={{ .SelectedTag.Info.Digest | urlquery }}&did={{ .Owner.DID | urlquery }}&repo={{ .Repository.Name | urlquery }}" 140 - hx-target="#attestation-modal-body" 141 - hx-swap="innerHTML" 142 - data-action="show-modal" data-modal-id="attestation-detail-modal"> 143 - {{ icon "shield-check" "size-3" }} Attested 144 - </button> 145 - {{ end }} 123 + {{/* Platform / attestation badges live INSIDE repo-tag-section 124 + so they re-render with each tag swap. Putting them here 125 + would freeze them at the page's initial-load tag. */}} 146 126 </div> 147 127 {{ end }} 148 128
+92
pkg/appview/templates/partials/helm-digest-content.html
··· 1 + {{ define "helm-digest-content" }} 2 + {{/* 3 + Helm-aware digest content WITH the install/pull command. Used on the 4 + digest detail page (/d/<handle>/<repo>/<digest>) where there's no other 5 + install switcher above. The repo page's chart tab uses helm-chart-info 6 + (without the install card) since repo-tag-section already renders an 7 + install switcher at the top. 8 + 9 + Expects dict: Manifest, HelmContent, RegistryURL, OwnerHandle, RepoName, 10 + OciClient, IsLoggedIn. 11 + */}} 12 + <div class="space-y-6"> 13 + {{ if .HelmContent.HoldUnreachable }} 14 + <div class="alert alert-warning" role="status"> 15 + {{ icon "wifi-off" "size-4 shrink-0" }} 16 + <div> 17 + <p class="font-medium">We couldn't reach the hold</p> 18 + <p class="text-sm">Chart metadata is stored on the hold. It may be offline right now.</p> 19 + </div> 20 + </div> 21 + {{ end }} 22 + 23 + {{/* Install / Pull command — reuses the existing switcher component 24 + (helm branch renders an install/pull toggle). */}} 25 + {{ $tag := "" }} 26 + {{ if .Manifest.Tags }}{{ $tag = index .Manifest.Tags 0 }}{{ end }} 27 + {{ if and (not $tag) .HelmContent.Meta }}{{ $tag = .HelmContent.Meta.Version }}{{ end }} 28 + <div class="card bg-base-200 border border-base-300 p-6"> 29 + {{ template "pull-command-switcher" (dict 30 + "RegistryURL" .RegistryURL 31 + "OwnerHandle" .OwnerHandle 32 + "RepoName" .RepoName 33 + "Tag" $tag 34 + "ArtifactType" "helm-chart" 35 + "OciClient" .OciClient 36 + "IsLoggedIn" .IsLoggedIn 37 + ) }} 38 + </div> 39 + 40 + {{ template "helm-chart-info" . }} 41 + </div> 42 + {{ end }} 43 + 44 + {{ define "helm-chart-info" }} 45 + {{/* 46 + Chart metadata + artifact card + scanning note. No install switcher — 47 + callers are expected to render one elsewhere on the page if needed. 48 + Standard tab-content padding (space-y-4 min-w-0 pt-6) matches the 49 + layers/vulns/sbom partials so the chart tab lines up visually. 50 + 51 + Expects dict: HelmContent (and the rest is ignored). 52 + */}} 53 + <div class="space-y-4 min-w-0 pt-6"> 54 + <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> 55 + {{/* Metadata (left, ~2/3) */}} 56 + <div class="lg:col-span-2 space-y-4 min-w-0"> 57 + {{ if .HelmContent.Meta }} 58 + {{ template "helm-metadata" .HelmContent.Meta }} 59 + {{ else if .HelmContent.MetaFetchFailed }} 60 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6"> 61 + <p class="font-medium text-base-content">Couldn't read chart metadata</p> 62 + <p class="text-sm text-base-content/70 mt-1">The hold is reachable but didn't return a valid Chart.yaml for this digest. The chart tarball is still pullable below.</p> 63 + </div> 64 + {{ end }} 65 + </div> 66 + 67 + {{/* Artifact + scanning note (right, ~1/3) */}} 68 + <div class="space-y-4 min-w-0"> 69 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-3"> 70 + <h2 class="text-sm font-semibold uppercase tracking-wider text-base-content/70">Chart artifact</h2> 71 + {{ if .HelmContent.Tarball }} 72 + <div class="space-y-2"> 73 + <p class="font-mono text-xs break-all text-base-content/70" title="{{ .HelmContent.Tarball.MediaType }}">{{ .HelmContent.Tarball.MediaType }}</p> 74 + <p class="text-lg font-bold">{{ humanizeBytes .HelmContent.Tarball.Size }}</p> 75 + <div class="flex items-center gap-2 text-xs"> 76 + <code class="font-mono break-all" title="{{ .HelmContent.Tarball.Digest }}">{{ truncateDigest (trimPrefix "sha256:" .HelmContent.Tarball.Digest) 16 }}</code> 77 + <button class="btn btn-ghost btn-xs" data-action="copy" data-copy="{{ .HelmContent.Tarball.Digest }}" aria-label="Copy digest">{{ icon "copy" "size-3" }}</button> 78 + </div> 79 + </div> 80 + {{ else }} 81 + <p class="text-sm text-base-content/70">No chart tarball recorded for this digest.</p> 82 + {{ end }} 83 + </div> 84 + 85 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-2"> 86 + <h2 class="text-sm font-semibold uppercase tracking-wider text-base-content/70">About scanning</h2> 87 + <p class="text-sm text-base-content/70">ATCR doesn't scan helm charts for vulnerabilities. Run a chart linter such as <a class="link link-primary" href="https://github.com/stackrox/kube-linter" rel="noopener noreferrer">kube-linter</a> in your CI before publishing.</p> 88 + </div> 89 + </div> 90 + </div> 91 + </div> 92 + {{ end }}
+115
pkg/appview/templates/partials/helm-metadata.html
··· 1 + {{ define "helm-metadata" }} 2 + {{/* 3 + Renders parsed Chart.yaml metadata as a description list. Expects a 4 + HelmChartMeta value. Rows are skipped silently when fields are empty so 5 + sparse charts don't render with a wall of "—" cells. 6 + */}} 7 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 8 + {{ if .Description }} 9 + <div> 10 + <h2 class="text-sm font-semibold uppercase tracking-wider text-base-content/70 mb-2">Description</h2> 11 + <p class="text-sm leading-relaxed">{{ .Description }}</p> 12 + </div> 13 + {{ end }} 14 + 15 + <dl class="grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm"> 16 + {{ if .Type }} 17 + <dt class="font-medium text-base-content/70">Type</dt> 18 + <dd>{{ .Type }}</dd> 19 + {{ end }} 20 + {{ if .AppVersion }} 21 + <dt class="font-medium text-base-content/70">App version</dt> 22 + <dd class="font-mono text-xs">{{ .AppVersion }}</dd> 23 + {{ end }} 24 + {{ if .KubeVersion }} 25 + <dt class="font-medium text-base-content/70">Kube version</dt> 26 + <dd class="font-mono text-xs">{{ .KubeVersion }}</dd> 27 + {{ end }} 28 + {{ if .Home }} 29 + <dt class="font-medium text-base-content/70">Home</dt> 30 + <dd class="min-w-0 truncate"> 31 + {{ if or (hasPrefix .Home "http://") (hasPrefix .Home "https://") }} 32 + <a href="{{ .Home }}" class="link link-primary" rel="noopener noreferrer" title="{{ .Home }}">{{ .Home }}</a> 33 + {{ else }} 34 + {{/* Non-http schemes (oci://, etc.) get sanitized to #ZgotmplZ 35 + in href context. Render as plain text so the user can 36 + still see and copy the value. */}} 37 + <code class="font-mono text-xs break-all" title="{{ .Home }}">{{ .Home }}</code> 38 + {{ end }} 39 + </dd> 40 + {{ end }} 41 + {{ if .Sources }} 42 + <dt class="font-medium text-base-content/70">Sources</dt> 43 + <dd class="space-y-1 min-w-0"> 44 + {{ range .Sources }} 45 + <div class="truncate"> 46 + {{ if or (hasPrefix . "http://") (hasPrefix . "https://") }} 47 + <a href="{{ . }}" class="link link-primary" rel="noopener noreferrer" title="{{ . }}">{{ . }}</a> 48 + {{ else }} 49 + <code class="font-mono text-xs break-all" title="{{ . }}">{{ . }}</code> 50 + {{ end }} 51 + </div> 52 + {{ end }} 53 + </dd> 54 + {{ end }} 55 + {{ if .Keywords }} 56 + <dt class="font-medium text-base-content/70">Keywords</dt> 57 + <dd class="flex flex-wrap gap-1"> 58 + {{ range .Keywords }}<span class="badge badge-ghost badge-sm">{{ . }}</span>{{ end }} 59 + </dd> 60 + {{ end }} 61 + {{ if .Maintainers }} 62 + <dt class="font-medium text-base-content/70">Maintainers</dt> 63 + <dd class="space-y-1"> 64 + {{ range .Maintainers }} 65 + <div class="text-sm"> 66 + {{ if and .URL (or (hasPrefix .URL "http://") (hasPrefix .URL "https://")) }} 67 + <a href="{{ .URL }}" class="link link-primary" rel="noopener noreferrer">{{ if .Name }}{{ .Name }}{{ else }}{{ .URL }}{{ end }}</a> 68 + {{ else if .Name }} 69 + {{ .Name }} 70 + {{ else if .URL }} 71 + <code class="font-mono text-xs">{{ .URL }}</code> 72 + {{ end }} 73 + {{ if .Email }}<span class="text-base-content/60">&lt;{{ .Email }}&gt;</span>{{ end }} 74 + </div> 75 + {{ end }} 76 + </dd> 77 + {{ end }} 78 + </dl> 79 + 80 + {{ if .Dependencies }} 81 + <div class="pt-2 border-t border-base-300"> 82 + <h2 class="text-sm font-semibold uppercase tracking-wider text-base-content/70 mb-2">Dependencies</h2> 83 + <div class="overflow-x-auto"> 84 + <table class="table table-xs"> 85 + <thead> 86 + <tr> 87 + <th>Name</th> 88 + <th>Version</th> 89 + <th>Repository</th> 90 + </tr> 91 + </thead> 92 + <tbody> 93 + {{ range .Dependencies }} 94 + <tr> 95 + <td class="font-medium">{{ .Name }}{{ if .Alias }} <span class="text-base-content/60">({{ .Alias }})</span>{{ end }}</td> 96 + <td class="font-mono text-xs">{{ .Version }}</td> 97 + <td class="text-xs"> 98 + {{ if .Repository }} 99 + {{ if or (hasPrefix .Repository "http://") (hasPrefix .Repository "https://") }} 100 + <a href="{{ .Repository }}" class="link link-primary" rel="noopener noreferrer">{{ .Repository }}</a> 101 + {{ else }} 102 + {{/* oci://, file://, etc. — render as code so it's copyable. */}} 103 + <code class="font-mono break-all">{{ .Repository }}</code> 104 + {{ end }} 105 + {{ end }} 106 + </td> 107 + </tr> 108 + {{ end }} 109 + </tbody> 110 + </table> 111 + </div> 112 + </div> 113 + {{ end }} 114 + </div> 115 + {{ end }}
+52
pkg/appview/templates/partials/repo-tag-section.html
··· 1 1 {{ define "repo-tag-section" }} 2 2 <div id="tag-content" data-owner="{{ .Owner.Handle }}" data-repo="{{ .Repository.Name }}"{{ if .SelectedTag }} data-digest="{{ if and .SelectedTag.Info.IsMultiArch .SelectedTag.Info.Platforms }}{{ (index .SelectedTag.Info.Platforms 0).Digest }}{{ else }}{{ .SelectedTag.Info.Digest }}{{ end }}"{{ end }}> 3 3 {{ if .SelectedTag }} 4 + {{/* Tag-scoped badges (platform / attested). These live INSIDE the 5 + HTMX-swapped section so they re-render on each tag change. 6 + Putting them in pages/repository.html freezes them at page load. */}} 7 + <div class="flex flex-wrap items-center gap-2 mb-2 min-h-6"> 8 + {{ if .SelectedTag.Info.IsMultiArch }} 9 + {{ range .SelectedTag.Info.Platforms }} 10 + <span class="badge badge-sm badge-outline font-mono">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 11 + {{ end }} 12 + <span class="badge badge-sm badge-soft badge-accent">Multi-arch</span> 13 + {{ else if gt (len .SelectedTag.Info.Platforms) 0 }} 14 + {{ $p := index .SelectedTag.Info.Platforms 0 }} 15 + {{ if $p.OS }} 16 + <span class="badge badge-sm badge-outline font-mono">{{ $p.OS }}/{{ $p.Architecture }}{{ if $p.Variant }}/{{ $p.Variant }}{{ end }}</span> 17 + {{ end }} 18 + {{ end }} 19 + {{ if eq .ArtifactType "helm-chart" }} 20 + <span class="badge badge-sm badge-soft badge-helm">{{ icon "helm" "size-3" }} Helm</span> 21 + {{ end }} 22 + {{ if .SelectedTag.Info.HasAttestations }} 23 + <button class="badge badge-sm badge-soft badge-success cursor-pointer hover:opacity-80" 24 + hx-get="/api/attestation-details?digest={{ .SelectedTag.Info.Digest | urlquery }}&did={{ .Owner.DID | urlquery }}&repo={{ .Repository.Name | urlquery }}" 25 + hx-target="#attestation-modal-body" 26 + hx-swap="innerHTML" 27 + data-action="show-modal" data-modal-id="attestation-detail-modal"> 28 + {{ icon "shield-check" "size-3" }} Attested 29 + </button> 30 + {{ end }} 31 + </div> 4 32 <!-- Pull Command with Client Switcher --> 5 33 {{ template "pull-command-switcher" (dict "RegistryURL" .RegistryURL "OwnerHandle" .Owner.Handle "RepoName" .Repository.Name "Tag" .SelectedTag.Info.Tag.Tag "ArtifactType" .ArtifactType "OciClient" .OciClient "IsLoggedIn" (ne .User nil)) }} 6 34 ··· 87 115 Overview 88 116 </button> 89 117 {{ if .SelectedTag }} 118 + {{ if eq .ArtifactType "helm-chart" }} 119 + {{/* Helm charts: a single Chart tab replaces Layers/Vulns/SBOM — 120 + those concepts don't apply. */}} 121 + <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 122 + data-tab="chart" 123 + role="tab" 124 + aria-selected="false" 125 + aria-controls="tab-chart" 126 + id="chart-tab-btn" 127 + data-action="switch-repo-tab"> 128 + Chart 129 + </button> 130 + {{ else }} 90 131 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 91 132 data-tab="layers" 92 133 role="tab" ··· 114 155 data-action="switch-repo-tab"> 115 156 SBOM 116 157 </button> 158 + {{ end }} 117 159 {{ end }} 118 160 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 119 161 data-tab="artifacts" ··· 197 239 </div> 198 240 199 241 {{ if .SelectedTag }} 242 + {{ if eq .ArtifactType "helm-chart" }} 243 + <div id="tab-chart" class="repo-panel hidden" role="tabpanel" aria-labelledby="chart-tab-btn" tabindex="0"> 244 + <div id="chart-content"> 245 + <div class="flex justify-center py-12"> 246 + <span class="loading loading-spinner loading-lg"></span> 247 + </div> 248 + </div> 249 + </div> 250 + {{ else }} 200 251 <div id="tab-layers" class="repo-panel hidden" role="tabpanel" aria-labelledby="layers-tab-btn" tabindex="0"> 201 252 <div id="layers-content"> 202 253 <div class="flex justify-center py-12"> ··· 220 271 </div> 221 272 </div> 222 273 </div> 274 + {{ end }} 223 275 {{ end }} 224 276 225 277 <div id="tab-artifacts" class="repo-panel hidden" role="tabpanel" aria-labelledby="artifacts-tab-btn" tabindex="0">
+2
pkg/appview/templates/partials/sbom-section.html
··· 7 7 {{ icon "wifi-off" "size-4 shrink-0" }} 8 8 <span>We couldn't reach the hold to load the SBOM.</span> 9 9 </div> 10 + {{ else if eq .SbomReason "not-applicable" }} 11 + <p class="text-base-content/70">SBOMs aren't generated for this artifact type. ATCR only produces SBOMs for container images.</p> 10 12 {{ else if eq .SbomReason "fetch-failed" }} 11 13 <p class="text-base-content/70">SBOM data couldn't be loaded. Try refreshing in a minute.</p> 12 14 {{ else }}
+6
pkg/appview/templates/partials/vuln-badge.html
··· 6 6 </span> 7 7 {{ else if .NotScanned }} 8 8 <span class="badge badge-sm badge-ghost" title="No scan recorded yet">Not scanned</span> 9 + {{ else if .Skipped }} 10 + {{/* Artifact is intentionally not scanned (helm chart, in-toto, DSSE). 11 + Render an empty span — the artifact-type badge already tells the user 12 + this isn't a container image, so a separate "not scannable" pill is 13 + redundant noise on listings. */}} 14 + <span></span> 9 15 {{ else if .ScanFailed }} 10 16 <span class="badge badge-sm badge-warning" title="Scanner ran but produced no SBOM">{{ icon "alert-triangle" "size-3" }} Scan failed</span> 11 17 {{ else if eq .Total 0 }}
+5
pkg/appview/templates/partials/vulns-section.html
··· 10 10 <p class="text-sm">Scan data is stored on the hold. It may be offline or unreachable right now.</p> 11 11 </div> 12 12 </div> 13 + {{ else if eq .VulnReason "not-applicable" }} 14 + <div class="py-8 text-sm text-base-content/70 max-w-prose"> 15 + <p class="font-medium text-base-content">Vulnerability scanning isn't applied to this artifact type</p> 16 + <p class="mt-1">ATCR's scanner only runs on container images. This artifact was intentionally skipped.</p> 17 + </div> 13 18 {{ else if eq .VulnReason "fetch-failed" }} 14 19 <div class="py-8 text-sm text-base-content/70 max-w-prose"> 15 20 <p class="font-medium text-base-content">Scan data couldn't be loaded</p>
+2
pkg/appview/ui.go
··· 296 296 return s 297 297 }, 298 298 299 + "hasPrefix": strings.HasPrefix, 300 + 299 301 "displayHoldDID": func(holdDID string) string { 300 302 // did:web:hold01.atcr.io → hold01.atcr.io 301 303 if strings.HasPrefix(holdDID, "did:web:") {
+84 -1
pkg/atproto/cbor_gen.go
··· 2182 2182 } 2183 2183 2184 2184 cw := cbg.NewCborWriter(w) 2185 + fieldCount := 15 2185 2186 2186 - if _, err := cw.Write([]byte{173}); err != nil { 2187 + if t.Status == "" { 2188 + fieldCount-- 2189 + } 2190 + 2191 + if t.Reason == "" { 2192 + fieldCount-- 2193 + } 2194 + 2195 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2187 2196 return err 2188 2197 } 2189 2198 ··· 2294 2303 } 2295 2304 } else { 2296 2305 if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Medium-1)); err != nil { 2306 + return err 2307 + } 2308 + } 2309 + 2310 + // t.Reason (string) (string) 2311 + if t.Reason != "" { 2312 + 2313 + if len("reason") > 8192 { 2314 + return xerrors.Errorf("Value in field \"reason\" was too long") 2315 + } 2316 + 2317 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reason"))); err != nil { 2318 + return err 2319 + } 2320 + if _, err := cw.WriteString(string("reason")); err != nil { 2321 + return err 2322 + } 2323 + 2324 + if len(t.Reason) > 8192 { 2325 + return xerrors.Errorf("Value in field t.Reason was too long") 2326 + } 2327 + 2328 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Reason))); err != nil { 2329 + return err 2330 + } 2331 + if _, err := cw.WriteString(string(t.Reason)); err != nil { 2332 + return err 2333 + } 2334 + } 2335 + 2336 + // t.Status (string) (string) 2337 + if t.Status != "" { 2338 + 2339 + if len("status") > 8192 { 2340 + return xerrors.Errorf("Value in field \"status\" was too long") 2341 + } 2342 + 2343 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 2344 + return err 2345 + } 2346 + if _, err := cw.WriteString(string("status")); err != nil { 2347 + return err 2348 + } 2349 + 2350 + if len(t.Status) > 8192 { 2351 + return xerrors.Errorf("Value in field t.Status was too long") 2352 + } 2353 + 2354 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { 2355 + return err 2356 + } 2357 + if _, err := cw.WriteString(string(t.Status)); err != nil { 2297 2358 return err 2298 2359 } 2299 2360 } ··· 2624 2685 } 2625 2686 2626 2687 t.Medium = int64(extraI) 2688 + } 2689 + // t.Reason (string) (string) 2690 + case "reason": 2691 + 2692 + { 2693 + sval, err := cbg.ReadStringWithMax(cr, 8192) 2694 + if err != nil { 2695 + return err 2696 + } 2697 + 2698 + t.Reason = string(sval) 2699 + } 2700 + // t.Status (string) (string) 2701 + case "status": 2702 + 2703 + { 2704 + sval, err := cbg.ReadStringWithMax(cr, 8192) 2705 + if err != nil { 2706 + return err 2707 + } 2708 + 2709 + t.Status = string(sval) 2627 2710 } 2628 2711 // t.UserDID (string) (string) 2629 2712 case "userDid":
+45
pkg/atproto/lexicon.go
··· 848 848 Total int64 `json:"total" cborgen:"total"` // Total vulnerability count 849 849 ScannerVersion string `json:"scannerVersion" cborgen:"scannerVersion"` // Scanner version (e.g., "atcr-scanner-v1.0.0") 850 850 ScannedAt string `json:"scannedAt" cborgen:"scannedAt"` // RFC3339 timestamp of scan completion 851 + Status string `json:"status,omitempty" cborgen:"status,omitempty"` // Scan outcome: "ok" (default if empty), "failed", or "skipped" 852 + Reason string `json:"reason,omitempty" cborgen:"reason,omitempty"` // Optional reason for non-ok status (e.g., unscannable artifact type) 851 853 } 854 + 855 + // Scan record status values. An empty Status field is treated as ScanStatusOK 856 + // for back-compat with records written before the field was introduced. 857 + const ( 858 + ScanStatusOK = "ok" 859 + ScanStatusFailed = "failed" 860 + ScanStatusSkipped = "skipped" 861 + ) 852 862 853 863 // NewScanRecord creates a new scan record 854 864 // manifestDigest: the manifest digest (e.g., "sha256:abc123...") ··· 870 880 Total: int64(total), 871 881 ScannerVersion: scannerVersion, 872 882 ScannedAt: time.Now().Format(time.RFC3339), 883 + Status: ScanStatusOK, 884 + } 885 + } 886 + 887 + // NewSkippedScanRecord creates a scan record marking an artifact as intentionally 888 + // not scanned (e.g., helm charts, in-toto attestations). The stale-scan loop 889 + // leaves these records alone since the outcome won't change without a code 890 + // change in the scanner. 891 + func NewSkippedScanRecord(manifestDigest, repository, userDID, reason, scannerVersion string) *ScanRecord { 892 + return &ScanRecord{ 893 + Type: ScanCollection, 894 + Manifest: BuildManifestURI(userDID, manifestDigest), 895 + Repository: repository, 896 + UserDID: userDID, 897 + ScannerVersion: scannerVersion, 898 + ScannedAt: time.Now().Format(time.RFC3339), 899 + Status: ScanStatusSkipped, 900 + Reason: reason, 901 + } 902 + } 903 + 904 + // NewFailedScanRecord creates a scan record marking a scan attempt as failed 905 + // (e.g., scanner crash, OOM, network error during fetch). The stale-scan loop 906 + // will re-queue these records on the rescan interval — failures may be 907 + // transient. 908 + func NewFailedScanRecord(manifestDigest, repository, userDID, reason, scannerVersion string) *ScanRecord { 909 + return &ScanRecord{ 910 + Type: ScanCollection, 911 + Manifest: BuildManifestURI(userDID, manifestDigest), 912 + Repository: repository, 913 + UserDID: userDID, 914 + ScannerVersion: scannerVersion, 915 + ScannedAt: time.Now().Format(time.RFC3339), 916 + Status: ScanStatusFailed, 917 + Reason: reason, 873 918 } 874 919 } 875 920
+1
pkg/hold/admin/admin.go
··· 522 522 r.Post("/admin/api/gc/reconcile", ui.handleGCReconcile) 523 523 r.Post("/admin/api/gc/delete-records", ui.handleGCDeleteRecords) 524 524 r.Post("/admin/api/gc/delete-blobs", ui.handleGCDeleteBlobs) 525 + r.Post("/admin/api/gc/backfill-configs-preview", ui.handleGCBackfillConfigsPreview) 525 526 r.Post("/admin/api/gc/backfill-configs", ui.handleGCBackfillConfigs) 526 527 r.Get("/admin/api/gc/status", ui.handleGCStatus) 527 528
+51
pkg/hold/admin/handlers_gc.go
··· 137 137 }) 138 138 } 139 139 140 + // handleGCBackfillConfigsPreview starts a dry-run scan that counts how many 141 + // manifests are missing an image config record without writing anything. 142 + func (ui *AdminUI) handleGCBackfillConfigsPreview(w http.ResponseWriter, r *http.Request) { 143 + if ui.gc == nil { 144 + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) 145 + return 146 + } 147 + 148 + session := getSessionFromContext(r.Context()) 149 + 150 + if ui.gc.StartBackfillConfigsPreview() { 151 + slog.Info("GC backfill configs preview started via admin panel", "by", session.DID) 152 + } 153 + 154 + progress := ui.gc.GetProgress() 155 + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ 156 + Phase: progress.Phase, 157 + Message: progress.Message, 158 + OpType: progress.OperationType, 159 + }) 160 + } 161 + 140 162 // handleGCBackfillConfigs starts image config backfill in the background 141 163 func (ui *AdminUI) handleGCBackfillConfigs(w http.ResponseWriter, r *http.Request) { 142 164 if ui.gc == nil { ··· 221 243 ui.renderTemplate(w, "partials/gc_preview.html", struct { 222 244 Preview *gc.GCPreview 223 245 }{Preview: preview}) 246 + 247 + case "backfill-configs-preview": 248 + preview, _ := ui.gc.LastBackfillPreview() 249 + if preview == nil { 250 + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"No backfill preview results available"}) 251 + return 252 + } 253 + slog.Info("GC backfill preview completed via admin panel", 254 + "missing", preview.MissingCount, 255 + "present", preview.PresentCount, 256 + "usersAffected", preview.UsersAffected, 257 + "duration", preview.Duration) 258 + ui.renderTemplate(w, "partials/gc_backfill_preview.html", struct { 259 + Preview *gc.BackfillConfigsPreview 260 + }{Preview: preview}) 261 + 262 + case "backfill-configs": 263 + result, _ := ui.gc.LastResult() 264 + if result == nil { 265 + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"No results available"}) 266 + return 267 + } 268 + slog.Info("GC backfill completed via admin panel", 269 + "created", result.RecordsReconciled, 270 + "skipped", result.RecordsSkipped, 271 + "duration", result.Duration) 272 + ui.renderTemplate(w, "partials/gc_backfill_result.html", struct { 273 + Result *gc.GCResult 274 + }{Result: result}) 224 275 225 276 default: 226 277 result, _ := ui.gc.LastResult()
+90
pkg/hold/admin/templates/partials/gc_backfill_preview.html
··· 1 + {{define "partials/gc_backfill_preview.html"}} 2 + <div class="space-y-6"> 3 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> 4 + <div class="stats shadow bg-base-100"> 5 + <div class="stat"> 6 + <div class="stat-title">Missing</div> 7 + <div class="stat-value {{if .Preview.MissingCount}}text-warning{{end}}">{{.Preview.MissingCount}}</div> 8 + <div class="stat-desc">Would be created</div> 9 + </div> 10 + </div> 11 + <div class="stats shadow bg-base-100"> 12 + <div class="stat"> 13 + <div class="stat-title">Already Present</div> 14 + <div class="stat-value text-success">{{.Preview.PresentCount}}</div> 15 + <div class="stat-desc">Would be skipped</div> 16 + </div> 17 + </div> 18 + <div class="stats shadow bg-base-100"> 19 + <div class="stat"> 20 + <div class="stat-title">Manifests Checked</div> 21 + <div class="stat-value">{{.Preview.ManifestsChecked}}</div> 22 + <div class="stat-desc">Distinct from layer records</div> 23 + </div> 24 + </div> 25 + <div class="stats shadow bg-base-100"> 26 + <div class="stat"> 27 + <div class="stat-title">Users Affected</div> 28 + <div class="stat-value">{{.Preview.UsersAffected}}</div> 29 + <div class="stat-desc">{{formatDuration .Preview.Duration}}</div> 30 + </div> 31 + </div> 32 + </div> 33 + 34 + {{if .Preview.Missing}} 35 + <div class="collapse collapse-arrow bg-base-100 shadow-sm"> 36 + <input type="checkbox" aria-label="Show manifests missing image configs" /> 37 + <div class="collapse-title font-medium"> 38 + {{ icon "file-plus" "size-4" }} Missing Image Configs ({{.Preview.MissingCount}}{{if gt .Preview.MissingCount (len .Preview.Missing)}}, showing first {{len .Preview.Missing}}{{end}}) 39 + </div> 40 + <div class="collapse-content"> 41 + <div class="overflow-x-auto"> 42 + <table class="table table-sm table-fixed"> 43 + <caption class="sr-only">Manifests missing image config records</caption> 44 + <colgroup> 45 + <col style="width: 30%"> 46 + <col style="width: 30%"> 47 + <col style="width: 40%"> 48 + </colgroup> 49 + <thead> 50 + <tr> 51 + <th scope="col">Digest</th> 52 + <th scope="col">User</th> 53 + <th scope="col">Manifest</th> 54 + </tr> 55 + </thead> 56 + <tbody> 57 + {{range .Preview.Missing}} 58 + <tr> 59 + <td><code class="text-xs font-mono truncate block max-w-full" title="{{.Digest}}">{{truncate .Digest 24}}</code></td> 60 + <td><code class="text-xs font-mono truncate block max-w-full" title="{{.UserDID}}">{{truncate .UserDID 24}}</code></td> 61 + <td><code class="text-xs font-mono truncate block max-w-full" title="{{.ManifestURI}}">{{truncate .ManifestURI 50}}</code></td> 62 + </tr> 63 + {{end}} 64 + </tbody> 65 + </table> 66 + </div> 67 + </div> 68 + </div> 69 + {{end}} 70 + 71 + {{if .Preview.MissingCount}} 72 + <div class="flex flex-wrap items-center gap-3 mt-6"> 73 + <button class="btn btn-warning gap-2" 74 + hx-post="/admin/api/gc/backfill-configs" 75 + hx-target="#gc-results" 76 + hx-swap="innerHTML" 77 + hx-confirm="Create {{.Preview.MissingCount}} image config records by reading config blobs from S3?"> 78 + {{ icon "refresh-cw" "size-4" }} 79 + Backfill {{.Preview.MissingCount}} Image Configs 80 + </button> 81 + </div> 82 + <p class="text-sm text-base-content/50 mt-2">Backfill is idempotent: present records are left alone.</p> 83 + {{else}} 84 + <div class="alert alert-success"> 85 + {{ icon "check-circle" "size-5" }} 86 + <span>Every manifest already has an image config record. Nothing to backfill.</span> 87 + </div> 88 + {{end}} 89 + </div> 90 + {{end}}
+31
pkg/hold/admin/templates/partials/gc_backfill_result.html
··· 1 + {{define "partials/gc_backfill_result.html"}} 2 + <div class="space-y-6"> 3 + <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> 4 + <div class="stats shadow bg-base-100"> 5 + <div class="stat"> 6 + <div class="stat-title">Created</div> 7 + <div class="stat-value text-success">{{.Result.RecordsReconciled}}</div> 8 + <div class="stat-desc">New image config records</div> 9 + </div> 10 + </div> 11 + <div class="stats shadow bg-base-100"> 12 + <div class="stat"> 13 + <div class="stat-title">Skipped</div> 14 + <div class="stat-value">{{.Result.RecordsSkipped}}</div> 15 + <div class="stat-desc">Already had a record</div> 16 + </div> 17 + </div> 18 + <div class="stats shadow bg-base-100"> 19 + <div class="stat"> 20 + <div class="stat-title">Duration</div> 21 + <div class="stat-value text-lg">{{formatDuration .Result.Duration}}</div> 22 + <div class="stat-desc">Wall-clock time</div> 23 + </div> 24 + </div> 25 + </div> 26 + 27 + <p class="text-sm text-base-content/50"> 28 + Run Scan again to verify nothing else is missing. 29 + </p> 30 + </div> 31 + {{end}}
+2 -3
pkg/hold/admin/templates/partials/tab_storage.html
··· 42 42 Scan for Orphans 43 43 </button> 44 44 <button class="btn btn-outline gap-2" 45 - hx-post="/admin/api/gc/backfill-configs" 45 + hx-post="/admin/api/gc/backfill-configs-preview" 46 46 hx-target="#gc-results" 47 47 hx-swap="innerHTML" 48 - hx-confirm="Backfill image config records from OCI config blobs in S3?" 49 48 {{if .Running}}disabled{{end}}> 50 49 {{ icon "refresh-cw" "size-4" }} 51 - Backfill Image Configs 50 + Scan Image Configs 52 51 </button> 53 52 </div> 54 53
+143 -39
pkg/hold/gc/gc.go
··· 63 63 Duration time.Duration `json:"duration"` 64 64 } 65 65 66 + // BackfillConfigCandidate identifies one manifest that's missing its image 67 + // config record on the hold. 68 + type BackfillConfigCandidate struct { 69 + ManifestURI string `json:"manifestUri"` 70 + UserDID string `json:"userDid"` 71 + Digest string `json:"digest"` 72 + } 73 + 74 + // BackfillConfigsPreview is the dry-run output for the image-config backfill. 75 + // No PDS writes or S3 fetches happen during preview — we only check which 76 + // manifest digests already have an io.atcr.hold.image.config record. 77 + type BackfillConfigsPreview struct { 78 + Missing []BackfillConfigCandidate `json:"missing"` 79 + MissingCount int `json:"missingCount"` 80 + PresentCount int `json:"presentCount"` 81 + ManifestsChecked int `json:"manifestsChecked"` 82 + UsersAffected int `json:"usersAffected"` 83 + Duration time.Duration `json:"duration"` 84 + } 85 + 66 86 // GarbageCollector handles cleanup of orphaned blobs from storage 67 87 type GarbageCollector struct { 68 88 pds *pds.HoldPDS ··· 80 100 running bool 81 101 82 102 // Last results (for admin panel display) 83 - lastPreview *GCPreview 84 - lastPreviewAt time.Time 85 - lastResult *GCResult 86 - lastResultAt time.Time 103 + lastPreview *GCPreview 104 + lastPreviewAt time.Time 105 + lastResult *GCResult 106 + lastResultAt time.Time 107 + lastBackfillPreview *BackfillConfigsPreview 108 + lastBackfillPreviewAt time.Time 87 109 88 110 // Progress tracking for background operations 89 111 phase string // "manifests", "records", "blobs", "deleting", "complete", "error" ··· 105 127 OrphanedBlobs int64 `json:"orphaned_blobs"` 106 128 ReferencedBlobs int64 `json:"referenced_blobs"` 107 129 RecordsReconciled int64 `json:"records_reconciled"` 130 + RecordsSkipped int64 `json:"records_skipped"` 108 131 ManifestsChecked int64 `json:"manifests_checked"` 109 132 UsersChecked int64 `json:"users_checked"` 110 133 Duration time.Duration `json:"duration"` ··· 174 197 type GCProgress struct { 175 198 Phase string // "manifests", "records", "blobs", "deleting", "complete", "error" 176 199 Message string 177 - OperationType string // "preview", "run", "reconcile", "delete-records", "delete-blobs" 200 + OperationType string // "preview", "run", "reconcile", "delete-records", "delete-blobs", "backfill-configs", "backfill-configs-preview" 178 201 Running bool 179 202 Error string 180 203 } ··· 815 838 } 816 839 } 817 840 841 + // StartBackfillConfigsPreview launches a dry-run scan that classifies every 842 + // manifest URI referenced from layer records as either already having an 843 + // image config record or missing one. No PDS or S3 writes happen. 844 + func (gc *GarbageCollector) StartBackfillConfigsPreview() bool { 845 + return gc.startBackground("backfill-configs-preview", "records", "Scanning for manifests missing image config records...", func(ctx context.Context) error { 846 + _, err := gc.doBackfillConfigsPreview(ctx) 847 + return err 848 + }) 849 + } 850 + 818 851 // StartBackfillConfigs launches image config backfill in the background. 819 852 // Creates io.atcr.hold.image.config records for manifests that don't have one yet 820 853 // by fetching OCI config blobs from S3. ··· 825 858 }) 826 859 } 827 860 828 - // doBackfillConfigs creates image config records for manifests that are missing them. 829 - func (gc *GarbageCollector) doBackfillConfigs(ctx context.Context) (*GCResult, error) { 861 + // scanBackfillCandidates walks every layer record, dedupes the manifest URIs 862 + // they reference, and bucket each one as already-present or missing an image 863 + // config record. Returns missing candidates and the count of present. 864 + // 865 + // opType is the GC operationType used for progress messages so this helper 866 + // can serve both the preview and the run. 867 + func (gc *GarbageCollector) scanBackfillCandidates(ctx context.Context, opType string) (missing []BackfillConfigCandidate, presentCount int, err error) { 830 868 recordsIndex := gc.pds.RecordsIndex() 831 869 if recordsIndex == nil { 832 - return nil, fmt.Errorf("records index not available") 870 + return nil, 0, fmt.Errorf("records index not available") 833 871 } 834 872 835 - // Step 1: Collect unique manifest URIs from layer records 836 873 manifestURIs := make(map[string]bool) 837 874 cursor := "" 838 875 totalScanned := 0 839 - 840 876 for { 841 - records, nextCursor, err := recordsIndex.ListRecords(atproto.LayerCollection, 1000, cursor, true) 842 - if err != nil { 843 - return nil, fmt.Errorf("list layer records: %w", err) 877 + records, nextCursor, listErr := recordsIndex.ListRecords(atproto.LayerCollection, 1000, cursor, true) 878 + if listErr != nil { 879 + return nil, 0, fmt.Errorf("list layer records: %w", listErr) 844 880 } 845 - 846 881 for _, rec := range records { 847 882 totalScanned++ 848 - layer, err := gc.decodeLayerRecord(ctx, rec) 849 - if err != nil { 883 + layer, decodeErr := gc.decodeLayerRecord(ctx, rec) 884 + if decodeErr != nil { 850 885 continue 851 886 } 852 887 manifestURIs[layer.Manifest] = true 853 888 } 854 - 855 889 if nextCursor == "" { 856 890 break 857 891 } ··· 862 896 "manifests", len(manifestURIs), 863 897 "layersScanned", totalScanned) 864 898 865 - // Step 2: For each manifest, check if config record exists, create if not 866 - start := time.Now() 867 - result := &GCResult{} 868 - created := int64(0) 869 - skipped := int64(0) 870 899 processed := 0 871 - httpClient := &http.Client{Timeout: 30 * time.Second} 872 - 873 900 for manifestURI := range manifestURIs { 874 901 processed++ 875 902 gc.setProgress("records", 876 - fmt.Sprintf("Backfilling configs (%d/%d manifests)...", processed, len(manifestURIs)), 877 - "backfill-configs") 903 + fmt.Sprintf("Checking image configs (%d/%d manifests)...", processed, len(manifestURIs)), 904 + opType) 878 905 879 - aturi, err := syntax.ParseATURI(manifestURI) 880 - if err != nil { 881 - gc.logger.Warn("Invalid manifest URI", "uri", manifestURI, "error", err) 906 + aturi, parseErr := syntax.ParseATURI(manifestURI) 907 + if parseErr != nil { 908 + gc.logger.Warn("Invalid manifest URI", "uri", manifestURI, "error", parseErr) 882 909 continue 883 910 } 884 - 885 911 manifestDigest := "sha256:" + aturi.RecordKey().String() 886 912 887 - // Check if config record already exists 888 - if _, _, err := gc.pds.GetImageConfigRecord(ctx, manifestDigest); err == nil { 889 - skipped++ 913 + if _, _, getErr := gc.pds.GetImageConfigRecord(ctx, manifestDigest); getErr == nil { 914 + presentCount++ 890 915 continue 891 916 } 917 + missing = append(missing, BackfillConfigCandidate{ 918 + ManifestURI: manifestURI, 919 + UserDID: aturi.Authority().String(), 920 + Digest: manifestDigest, 921 + }) 922 + } 923 + return missing, presentCount, nil 924 + } 892 925 893 - userDID := aturi.Authority().String() 894 - manifestRkey := aturi.RecordKey().String() 926 + // doBackfillConfigsPreview runs scanBackfillCandidates and stores the result 927 + // for the admin UI to display. The full missing slice is kept in memory but 928 + // rendering is capped via maxPreviewItems in the template layer. 929 + func (gc *GarbageCollector) doBackfillConfigsPreview(ctx context.Context) (*BackfillConfigsPreview, error) { 930 + start := time.Now() 931 + 932 + missing, presentCount, err := gc.scanBackfillCandidates(ctx, "backfill-configs-preview") 933 + if err != nil { 934 + return nil, err 935 + } 936 + 937 + users := make(map[string]struct{}, len(missing)) 938 + for _, c := range missing { 939 + users[c.UserDID] = struct{}{} 940 + } 941 + 942 + missingCount := len(missing) 943 + display := missing 944 + if len(display) > maxPreviewItems { 945 + display = display[:maxPreviewItems] 946 + } 947 + 948 + preview := &BackfillConfigsPreview{ 949 + Missing: display, 950 + MissingCount: missingCount, 951 + PresentCount: presentCount, 952 + ManifestsChecked: missingCount + presentCount, 953 + UsersAffected: len(users), 954 + Duration: time.Since(start), 955 + } 956 + 957 + gc.mu.Lock() 958 + gc.lastBackfillPreview = preview 959 + gc.lastBackfillPreviewAt = time.Now() 960 + gc.mu.Unlock() 961 + 962 + gc.logger.Info("Image config backfill preview complete", 963 + "missing", missingCount, 964 + "present", presentCount, 965 + "usersAffected", preview.UsersAffected, 966 + "duration", preview.Duration) 967 + return preview, nil 968 + } 969 + 970 + // doBackfillConfigs creates image config records for manifests that are missing them. 971 + func (gc *GarbageCollector) doBackfillConfigs(ctx context.Context) (*GCResult, error) { 972 + start := time.Now() 973 + 974 + missing, presentCount, err := gc.scanBackfillCandidates(ctx, "backfill-configs") 975 + if err != nil { 976 + return nil, err 977 + } 978 + 979 + result := &GCResult{RecordsSkipped: int64(presentCount)} 980 + created := int64(0) 981 + httpClient := &http.Client{Timeout: 30 * time.Second} 982 + 983 + for i, candidate := range missing { 984 + gc.setProgress("records", 985 + fmt.Sprintf("Backfilling configs (%d/%d missing)...", i+1, len(missing)), 986 + "backfill-configs") 987 + 988 + userDID := candidate.UserDID 989 + manifestRkey := strings.TrimPrefix(candidate.Digest, "sha256:") 990 + manifestURI := candidate.ManifestURI 991 + manifestDigest := candidate.Digest 895 992 896 993 pdsEndpoint, err := atproto.ResolveDIDToPDS(ctx, userDID) 897 994 if err != nil { ··· 899 996 continue 900 997 } 901 998 902 - // Fetch manifest via getRecord to get config digest 903 999 reqURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s", 904 1000 pdsEndpoint, 905 1001 url.QueryEscape(userDID), ··· 938 1034 continue 939 1035 } 940 1036 941 - // Fetch config blob from S3 942 1037 configBytes, err := gc.s3.GetBytes(ctx, s3.BlobPath(manifest.Config.Digest)) 943 1038 if err != nil { 944 1039 gc.logger.Warn("Failed to fetch config blob", "digest", manifest.Config.Digest, "error", err) 945 1040 continue 946 1041 } 947 1042 948 - // Create image config record 949 1043 configRecord := atproto.NewImageConfigRecord(manifestURI, string(configBytes)) 950 1044 if _, _, err := gc.pds.CreateImageConfigRecord(ctx, configRecord, manifestDigest); err != nil { 951 1045 gc.logger.Warn("Failed to create image config record", "manifest", manifestURI, "error", err) ··· 963 1057 gc.lastResultAt = time.Now() 964 1058 gc.mu.Unlock() 965 1059 966 - gc.logger.Info("Image config backfill complete", "created", created, "skipped", skipped) 1060 + gc.logger.Info("Image config backfill complete", 1061 + "created", created, 1062 + "skipped", result.RecordsSkipped) 967 1063 return result, nil 968 1064 } 969 1065 ··· 1508 1604 gc.mu.Lock() 1509 1605 defer gc.mu.Unlock() 1510 1606 return gc.lastResult, gc.lastResultAt 1607 + } 1608 + 1609 + // LastBackfillPreview returns the most recent image-config backfill preview 1610 + // and when it was generated. 1611 + func (gc *GarbageCollector) LastBackfillPreview() (*BackfillConfigsPreview, time.Time) { 1612 + gc.mu.Lock() 1613 + defer gc.mu.Unlock() 1614 + return gc.lastBackfillPreview, gc.lastBackfillPreviewAt 1511 1615 } 1512 1616 1513 1617 // IsRunning returns whether a GC operation is currently in progress
+62 -12
pkg/hold/pds/scan_broadcaster.go
··· 79 79 80 80 // ScannerMessage is a message received from scanner over WebSocket 81 81 type ScannerMessage struct { 82 - Type string `json:"type"` // "ack", "result", "error" 82 + Type string `json:"type"` // "ack", "result", "error", "skipped" 83 83 Seq int64 `json:"seq"` // Job sequence number 84 84 SBOM string `json:"sbom,omitempty"` 85 85 VulnReport string `json:"vulnReport,omitempty"` 86 86 Summary *VulnerabilitySummary `json:"summary,omitempty"` 87 87 Error string `json:"error,omitempty"` 88 + Reason string `json:"reason,omitempty"` // Populated for "skipped" messages 88 89 } 89 90 90 91 // VulnerabilitySummary contains counts of vulnerabilities by severity ··· 447 448 sb.handleResult(sub, msg) 448 449 case "error": 449 450 sb.handleError(sub, msg) 451 + case "skipped": 452 + sb.handleSkipped(sub, msg) 450 453 default: 451 454 slog.Warn("Unknown scanner message type", 452 455 "type", msg.Type, ··· 579 582 "total", msg.Summary.Total) 580 583 } 581 584 582 - // handleError marks a job as failed and creates a scan record so the proactive 583 - // scanner treats it as "stale" rather than "never scanned" (avoids retry loops). 585 + // handleError marks a job as failed and creates a scan record so the stale 586 + // loop won't immediately retry. Failed records still get retried on the 587 + // rescan interval since failures may be transient (network, OOM, etc.). 584 588 func (sb *ScanBroadcaster) handleError(sub *ScanSubscriber, msg ScannerMessage) { 585 589 ctx := context.Background() 586 590 587 - // Get job details to create failure scan record 588 591 var manifestDigest, repository, userDID string 589 592 err := sb.db.QueryRow(` 590 593 SELECT manifest_digest, repository, user_did ··· 594 597 slog.Error("Failed to get job details for failure record", 595 598 "seq", msg.Seq, "error", err) 596 599 } else { 597 - // Create a scan record with zero counts and nil blobs — marks it as 598 - // "scanned" so the proactive scheduler won't retry until rescan interval. 599 - // Nil blobs signal failure to the appview (successful scans always have blobs). 600 - scanRecord := atproto.NewScanRecord( 600 + scanRecord := atproto.NewFailedScanRecord( 601 601 manifestDigest, repository, userDID, 602 - nil, nil, // no SBOM or vuln report — signals scan failure 603 - 0, 0, 0, 0, 0, 602 + msg.Error, 604 603 "atcr-scanner-v1.0.0", 605 604 ) 606 605 if _, _, err := sb.pds.CreateScanRecord(ctx, scanRecord); err != nil { ··· 609 608 } 610 609 } 611 610 612 - // Mark job as failed 613 611 _, err = sb.db.Exec(` 614 612 UPDATE scan_jobs SET status = 'failed', completed_at = ? 615 613 WHERE seq = ? ··· 620 618 "error", err) 621 619 } 622 620 623 - // Remove from in-flight tracking and wake dispatch loop 624 621 sb.removeInflight(manifestDigest) 625 622 sb.signalCompletion() 626 623 ··· 630 627 "error", msg.Error) 631 628 } 632 629 630 + // handleSkipped marks a job complete and creates a scan record with 631 + // status="skipped". The stale-scan loop will leave these records alone — the 632 + // outcome won't change until the scanner gains support for the artifact type. 633 + func (sb *ScanBroadcaster) handleSkipped(sub *ScanSubscriber, msg ScannerMessage) { 634 + ctx := context.Background() 635 + 636 + var manifestDigest, repository, userDID string 637 + err := sb.db.QueryRow(` 638 + SELECT manifest_digest, repository, user_did 639 + FROM scan_jobs WHERE seq = ? 640 + `, msg.Seq).Scan(&manifestDigest, &repository, &userDID) 641 + if err != nil { 642 + slog.Error("Failed to get job details for skip record", 643 + "seq", msg.Seq, "error", err) 644 + } else { 645 + scanRecord := atproto.NewSkippedScanRecord( 646 + manifestDigest, repository, userDID, 647 + msg.Reason, 648 + "atcr-scanner-v1.0.0", 649 + ) 650 + if _, _, err := sb.pds.CreateScanRecord(ctx, scanRecord); err != nil { 651 + slog.Error("Failed to store skipped scan record", 652 + "seq", msg.Seq, "error", err) 653 + } 654 + } 655 + 656 + _, err = sb.db.Exec(` 657 + UPDATE scan_jobs SET status = 'completed', completed_at = ? 658 + WHERE seq = ? 659 + `, time.Now(), msg.Seq) 660 + if err != nil { 661 + slog.Error("Failed to mark scan job as completed (skipped)", 662 + "seq", msg.Seq, 663 + "error", err) 664 + } 665 + 666 + sb.removeInflight(manifestDigest) 667 + sb.signalCompletion() 668 + 669 + slog.Info("Scan job skipped", 670 + "seq", msg.Seq, 671 + "subscriberId", sub.id, 672 + "reason", msg.Reason) 673 + } 674 + 633 675 // drainPendingJobs sends pending/timed-out jobs to a newly connected scanner. 634 676 // Collects all pending rows first, closes cursor, then assigns and dispatches 635 677 // to avoid holding a SELECT cursor open during UPDATEs (prevents SQLite BUSY). ··· 1047 1089 // Fetch the actual scan record to check staleness 1048 1090 _, scanRecord, err := sb.pds.GetScanRecord(ctx, manifestDigest) 1049 1091 if err != nil { 1092 + sb.removeInflight(manifestDigest) 1093 + continue 1094 + } 1095 + 1096 + // Permanently-skipped records (helm charts, in-toto, etc.) won't 1097 + // change outcome on retry — leave them alone. Failed records still 1098 + // get retried since failures may be transient. 1099 + if scanRecord.Status == atproto.ScanStatusSkipped { 1050 1100 sb.removeInflight(manifestDigest) 1051 1101 continue 1052 1102 }
+7
scanner/internal/client/hold.go
··· 194 194 c.sendJSON(scanner.ErrorMessage{Type: "error", Seq: seq, Error: errMsg}) 195 195 } 196 196 197 + // SendSkipped sends a skipped message for an artifact the scanner intentionally 198 + // won't process (e.g., helm charts). Distinct from SendError so the hold can 199 + // distinguish a permanent skip from a retryable failure. 200 + func (c *HoldClient) SendSkipped(seq int64, reason string) { 201 + c.sendJSON(scanner.SkippedMessage{Type: "skipped", Seq: seq, Reason: reason}) 202 + } 203 + 197 204 func (c *HoldClient) sendJSON(v any) { 198 205 c.mu.Lock() 199 206 defer c.mu.Unlock()
+30 -11
scanner/internal/scan/worker.go
··· 4 4 5 5 import ( 6 6 "context" 7 + "errors" 7 8 "fmt" 8 9 "log/slog" 9 10 "os" 10 11 "runtime" 11 - "strings" 12 12 "sync" 13 13 "time" 14 14 ··· 17 17 "atcr.io/scanner/internal/config" 18 18 "atcr.io/scanner/internal/queue" 19 19 ) 20 + 21 + // SkipError is returned by processJob when the scanner intentionally bypasses 22 + // an artifact type it can't analyze (helm charts, in-toto attestations, DSSE). 23 + // The worker dispatches these to hold via SendSkipped so the hold can mark 24 + // the scan record "skipped" instead of "failed". Skipped records are never 25 + // retried by the stale-scan loop; failures are. 26 + type SkipError struct { 27 + Reason string 28 + } 29 + 30 + func (e *SkipError) Error() string { return "skipped: " + e.Reason } 20 31 21 32 // WorkerPool manages a pool of scan workers 22 33 type WorkerPool struct { ··· 94 105 95 106 result, err := wp.processJob(ctx, job) 96 107 if err != nil { 97 - logLevel := slog.LevelError 98 - if strings.HasPrefix(err.Error(), "skipped:") { 99 - logLevel = slog.LevelInfo 108 + var skipErr *SkipError 109 + if errors.As(err, &skipErr) { 110 + slog.Info("Scan job skipped", 111 + "worker_id", id, 112 + "repository", job.Repository, 113 + "reason", skipErr.Reason) 114 + wp.client.SendSkipped(job.Seq, skipErr.Reason) 115 + } else { 116 + slog.Error("Scan job failed", 117 + "worker_id", id, 118 + "repository", job.Repository, 119 + "error", err) 120 + wp.client.SendError(job.Seq, err.Error()) 100 121 } 101 - slog.Log(ctx, logLevel, "Scan job failed", 102 - "worker_id", id, 103 - "repository", job.Repository, 104 - "error", err) 105 - wp.client.SendError(job.Seq, err.Error()) 106 122 } else { 107 123 wp.client.SendResult(job.Seq, result) 108 124 ··· 138 154 func (wp *WorkerPool) processJob(ctx context.Context, job *scanner.ScanJob) (*scanner.ScanResult, error) { 139 155 startTime := time.Now() 140 156 141 - // Skip non-container OCI artifacts (Helm charts, WASM modules, etc.) 157 + // Skip non-container OCI artifacts (Helm charts, in-toto, DSSE, etc.). 158 + // Returning *SkipError tells the worker dispatch loop to send a "skipped" 159 + // message rather than an "error" — the hold marks these records as 160 + // permanently skipped and won't retry them on the rescan interval. 142 161 if unscannableConfigTypes[job.Config.MediaType] { 143 - return nil, fmt.Errorf("skipped: unscannable artifact type %s", job.Config.MediaType) 162 + return nil, &SkipError{Reason: fmt.Sprintf("unscannable artifact type %s", job.Config.MediaType)} 144 163 } 145 164 146 165 // Ensure tmp dir exists
+11
scanner/types.go
··· 82 82 Seq int64 `json:"seq"` 83 83 Error string `json:"error"` 84 84 } 85 + 86 + // SkippedMessage is sent from scanner to hold when an artifact is intentionally 87 + // not scanned (e.g., helm charts, in-toto attestations). Distinct from 88 + // ErrorMessage so the hold can mark the scan record as "skipped" rather than 89 + // "failed" — the stale-scan loop will leave skipped records alone since the 90 + // outcome won't change without a code change in the scanner. 91 + type SkippedMessage struct { 92 + Type string `json:"type"` // "skipped" 93 + Seq int64 `json:"seq"` 94 + Reason string `json:"reason"` 95 + }