diff --git a/config-appview.example.yaml b/config-appview.example.yaml
index d162586..57537e1 100644
--- a/config-appview.example.yaml
+++ b/config-appview.example.yaml
@@ -29,8 +29,6 @@ server:
default_hold_did: ""
# Allows HTTP (not HTTPS) for DID resolution and uses transition:generic OAuth scope.
test_mode: false
- # Path to P-256 private key for OAuth client authentication. Auto-generated on first run.
- oauth_key_path: /var/lib/atcr/oauth/client.key
# Display name shown on OAuth authorization screens.
client_name: AT Container Registry
# Short name used in page titles and browser tabs.
@@ -45,7 +43,7 @@ ui:
# SQLite/libSQL database for OAuth sessions, stars, pull counts, and device approvals.
database_path: /var/lib/atcr/ui.db
# Visual theme name (e.g. "seamark"). Empty uses default atcr.io branding.
- theme: "seamark"
+ theme: ""
# libSQL sync URL (libsql://...). Works with Turso cloud or self-hosted libsql-server. Leave empty for local-only SQLite.
libsql_sync_url: ""
# Auth token for libSQL sync. Required if libsql_sync_url is set.
diff --git a/deploy/upcloud/configs/appview.yaml.tmpl b/deploy/upcloud/configs/appview.yaml.tmpl
index 2e4a63f..56af52f 100644
--- a/deploy/upcloud/configs/appview.yaml.tmpl
+++ b/deploy/upcloud/configs/appview.yaml.tmpl
@@ -11,7 +11,6 @@ server:
addr: :5000
base_url: "https://seamark.dev"
default_hold_did: "{{.HoldDid}}"
- oauth_key_path: "{{.BasePath}}/oauth/client.key"
client_name: Seamark
test_mode: false
client_short_name: Seamark
@@ -40,7 +39,6 @@ jetstream:
- https://relay1.us-east.bsky.network
- https://relay1.us-west.bsky.network
auth:
- key_path: "{{.BasePath}}/auth/private-key.pem"
cert_path: "{{.BasePath}}/auth/private-key.crt"
legal:
company_name: Seamark
diff --git a/pkg/appview/config.go b/pkg/appview/config.go
index 3b5a91d..b8cf97f 100644
--- a/pkg/appview/config.go
+++ b/pkg/appview/config.go
@@ -52,9 +52,6 @@ type ServerConfig struct {
// Allows HTTP (not HTTPS) for DID resolution.
TestMode bool `yaml:"test_mode" comment:"Allows HTTP (not HTTPS) for DID resolution and uses transition:generic OAuth scope."`
- // Path to P-256 private key for OAuth client authentication.
- OAuthKeyPath string `yaml:"oauth_key_path" comment:"Path to P-256 private key for OAuth client authentication. Auto-generated on first run."`
-
// Display name shown on OAuth authorization screens.
ClientName string `yaml:"client_name" comment:"Display name shown on OAuth authorization screens."`
@@ -116,11 +113,8 @@ type JetstreamConfig struct {
// AuthConfig defines authentication settings
type AuthConfig struct {
- // RSA private key for signing registry JWTs.
- KeyPath string `yaml:"key_path" comment:"RSA private key for signing registry JWTs issued to Docker clients."`
-
// X.509 certificate matching the JWT signing key.
- CertPath string `yaml:"cert_path" comment:"X.509 certificate matching the JWT signing key."`
+ CertPath string `yaml:"cert_path" comment:"X.509 certificate matching the JWT signing key (auto-generated on each boot from the JWT key in the database)."`
// TokenExpiration is the JWT expiration duration (5 minutes, not configurable)
TokenExpiration time.Duration `yaml:"-"`
@@ -169,7 +163,6 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("server.test_mode", false)
v.SetDefault("server.client_name", "AT Container Registry")
v.SetDefault("server.client_short_name", "ATCR")
- v.SetDefault("server.oauth_key_path", "/var/lib/atcr/oauth/client.key")
v.SetDefault("server.registry_domains", []string{})
v.SetDefault("server.managed_holds", []string{})
@@ -200,7 +193,6 @@ func setDefaults(v *viper.Viper) {
})
// Auth defaults
- v.SetDefault("auth.key_path", "/var/lib/atcr/auth/private-key.pem")
v.SetDefault("auth.cert_path", "/var/lib/atcr/auth/private-key.crt")
// Log shipper defaults
diff --git a/pkg/appview/crypto_keys.go b/pkg/appview/crypto_keys.go
index c7ec79c..52519c1 100644
--- a/pkg/appview/crypto_keys.go
+++ b/pkg/appview/crypto_keys.go
@@ -18,10 +18,8 @@ import (
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
-// loadOAuthKey loads the OAuth P-256 key with priority: DB → file → generate.
-// Keys loaded from file or newly generated are stored in the DB.
-func loadOAuthKey(database *sql.DB, keyPath string) (*atcrypto.PrivateKeyP256, error) {
- // Try database first
+// loadOAuthKey loads the OAuth P-256 key from the DB, generating one if absent.
+func loadOAuthKey(database *sql.DB) (*atcrypto.PrivateKeyP256, error) {
data, err := db.GetCryptoKey(database, "oauth_p256")
if err != nil {
return nil, fmt.Errorf("failed to query crypto_keys: %w", err)
@@ -35,23 +33,6 @@ func loadOAuthKey(database *sql.DB, keyPath string) (*atcrypto.PrivateKeyP256, e
return key, nil
}
- // Try file fallback
- if keyPath != "" {
- if fileData, err := os.ReadFile(keyPath); err == nil {
- key, err := atcrypto.ParsePrivateBytesP256(fileData)
- if err != nil {
- return nil, fmt.Errorf("failed to parse OAuth key from file %s: %w", keyPath, err)
- }
- // Migrate to database
- if err := db.PutCryptoKey(database, "oauth_p256", fileData); err != nil {
- return nil, fmt.Errorf("failed to store OAuth key in database: %w", err)
- }
- slog.Info("Migrated OAuth P-256 key from file to database", "path", keyPath)
- return key, nil
- }
- }
-
- // Generate new key
p256Key, err := atcrypto.GeneratePrivateKeyP256()
if err != nil {
return nil, fmt.Errorf("failed to generate OAuth P-256 key: %w", err)
@@ -66,16 +47,15 @@ func loadOAuthKey(database *sql.DB, keyPath string) (*atcrypto.PrivateKeyP256, e
return p256Key, nil
}
-// loadJWTKeyAndCert loads the JWT RSA key from DB (with file fallback) and generates
-// a self-signed certificate. The cert is always regenerated and written to certPath
-// on disk because the distribution library reads it via os.Open().
-func loadJWTKeyAndCert(database *sql.DB, keyPath, certPath string) (*rsa.PrivateKey, []byte, error) {
- rsaKey, err := loadRSAKey(database, keyPath)
+// loadJWTKeyAndCert loads the JWT RSA key from the DB and generates a self-signed
+// certificate. The cert is always regenerated and written to certPath on disk
+// because the distribution library reads it via os.Open().
+func loadJWTKeyAndCert(database *sql.DB, certPath string) (*rsa.PrivateKey, []byte, error) {
+ rsaKey, err := loadRSAKey(database)
if err != nil {
return nil, nil, err
}
- // Generate cert and write to disk for distribution library
certDER, err := generateAndWriteCert(rsaKey, certPath)
if err != nil {
return nil, nil, err
@@ -84,9 +64,8 @@ func loadJWTKeyAndCert(database *sql.DB, keyPath, certPath string) (*rsa.Private
return rsaKey, certDER, nil
}
-// loadRSAKey loads the RSA private key with priority: DB → file → generate.
-func loadRSAKey(database *sql.DB, keyPath string) (*rsa.PrivateKey, error) {
- // Try database first
+// loadRSAKey loads the RSA private key from the DB, generating one if absent.
+func loadRSAKey(database *sql.DB) (*rsa.PrivateKey, error) {
data, err := db.GetCryptoKey(database, "jwt_rsa")
if err != nil {
return nil, fmt.Errorf("failed to query crypto_keys: %w", err)
@@ -100,23 +79,6 @@ func loadRSAKey(database *sql.DB, keyPath string) (*rsa.PrivateKey, error) {
return key, nil
}
- // Try file fallback
- if keyPath != "" {
- if fileData, err := os.ReadFile(keyPath); err == nil {
- key, err := parseRSAKeyPEM(fileData)
- if err != nil {
- return nil, fmt.Errorf("failed to parse RSA key from file %s: %w", keyPath, err)
- }
- // Migrate to database
- if err := db.PutCryptoKey(database, "jwt_rsa", fileData); err != nil {
- return nil, fmt.Errorf("failed to store RSA key in database: %w", err)
- }
- slog.Info("Migrated JWT RSA key from file to database", "path", keyPath)
- return key, nil
- }
- }
-
- // Generate new key
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate RSA key: %w", err)
diff --git a/pkg/appview/jetstream/processor_test.go b/pkg/appview/jetstream/processor_test.go
index 31ab479..1196c47 100644
--- a/pkg/appview/jetstream/processor_test.go
+++ b/pkg/appview/jetstream/processor_test.go
@@ -489,43 +489,6 @@ func TestProcessStar(t *testing.T) {
}
}
-func TestProcessStar_OldFormat(t *testing.T) {
- database := setupTestDB(t)
- defer database.Close()
-
- // Insert test users
- for _, did := range []string{"did:plc:starrer456", "did:plc:owner456"} {
- _, err := database.Exec(
- `INSERT INTO users (did, handle, pds_endpoint, last_seen) VALUES (?, ?, ?, ?)`,
- did, did+".test", "https://pds.example.com", time.Now())
- if err != nil {
- t.Fatalf("Failed to insert test user %s: %v", did, err)
- }
- }
-
- p := NewProcessor(database, false, nil)
- ctx := context.Background()
-
- // Old format JSON (object subject with did + repository)
- oldFormatJSON := `{"$type":"io.atcr.sailor.star","subject":{"did":"did:plc:owner456","repository":"legacy-app"},"createdAt":"2025-06-01T00:00:00Z"}`
-
- err := p.ProcessStar(ctx, "did:plc:starrer456", []byte(oldFormatJSON))
- if err != nil {
- t.Fatalf("ProcessStar (old format) failed: %v", err)
- }
-
- // Verify star was inserted with correct owner/repo
- var count int
- err = database.QueryRow("SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = ? AND repository = ?",
- "did:plc:starrer456", "did:plc:owner456", "legacy-app").Scan(&count)
- if err != nil {
- t.Fatalf("Failed to query stars: %v", err)
- }
- if count != 1 {
- t.Errorf("Expected 1 star from old format, got %d", count)
- }
-}
-
func TestProcessManifest_Duplicate(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
diff --git a/pkg/appview/public/js/bundle.min.js b/pkg/appview/public/js/bundle.min.js
index a28d4b3..77bf22f 100644
--- a/pkg/appview/public/js/bundle.min.js
+++ b/pkg/appview/public/js/bundle.min.js
@@ -1,5 +1,5 @@
-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(/
]*)?>[\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(''+t+" ");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=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"&&i--}a0;){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=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;r0?(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","
"),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(;n0}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;p0?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;s0;){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;a0?(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", "+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;thtmx.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=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;nt.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;a0?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.${t}{opacity:0;visibility: hidden} .${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`)}}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)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="×",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=' 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(`
-`)}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=' Navigating…')})});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)$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='Nothing to preview
';return}s.innerHTML=' Rendering preview…
';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='Preview failed. Check your connection and try again.
'})}},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="",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(`
+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(/]*)?>[\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(''+t+" ");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=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"&&i--}a0;){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=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;r0?(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","
"),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(;n0}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;p0?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;s0;){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;a0?(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", "+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;thtmx.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=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;nt.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;a0?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()()},ne=getClosestAttributeValue(n,"hx-prompt");if(ne){var W=prompt(ne);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 re=getInputValues(n,e),q=re.errors,oe=re.formData;o.values&&overrideFormData(oe,formDataFromObject(o.values));let Ae=formDataFromObject(getExpressionVars(n,r)),X=overrideFormData(oe,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"),se=getInternalData(n).boosted,P=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,C={boosted:se,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 ie=t.split("#"),Le=ie[0],z=ie[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:se,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.${t}{opacity:0;visibility: hidden} .${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`)}}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 ae="https://typeahead.waow.tech",ce="https://public.api.bsky.app",Re="/xrpc/app.bsky.actor.searchActorsTypeahead",De="/xrpc/app.bsky.actor.getProfiles";var Oe="atcr_recent_handles",ue="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)try{r=await J(ae,t,1500)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}if(r===null)try{r=await J(ce,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}}}),le(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}},le(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="×",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,ce);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(ue)||"{}")}catch{return{}}}function le(e){try{localStorage.setItem(ue,JSON.stringify(e))}catch{}}function Ne(){try{let e=localStorage.getItem(Oe);return e?JSON.parse(e):[]}catch{return[]}}var I=null;function de(){let e=document.getElementById("handle");e&&(I&&I.input===e||(I&&I.destroy(),I=new Y(e)))}document.addEventListener("DOMContentLoaded",de);document.body.addEventListener("htmx:afterSettle",de);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 ge(){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=ge(),n=ke(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),qe(e)}function pe(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",()=>{ge()==="system"&&G()});function Q(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(Q(e,t),t){let n=document.getElementById("nav-search-input");n&&n.focus()}}function fe(){let e=document.querySelector(".nav-search-wrapper");if(Q(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")&&fe(),n.key==="/"&&!e.classList.contains("expanded")){let r=n.target.tagName;if(r==="INPUT"||r==="TEXTAREA"||n.target.isContentEditable)return;n.preventDefault(),Q(e,!0),t.focus()}}),document.addEventListener("click",n=>{e.classList.contains("expanded")&&!e.contains(n.target)&&fe()}))});function K(e,t){let n=()=>{if(!t||!document.contains(t))return;let r=t.innerHTML;t.innerHTML=' 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),he(e)?n():T("Copy failed \u2014 check browser permissions","error")});return}he(e)?n():T("Copy failed \u2014 select the text and copy manually","error")}function he(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(`
+`)}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",()=>{pe(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),ee(o)}function Z(){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)Z(),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&&Z()})});var k=new WeakMap;function ee(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",ee(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=' Navigating…')})});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 me(){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(me,{timeout:2e3}):setTimeout(me,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)$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=>ee(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=pe;window.toggleSearch=Fe;window.copyToClipboard=K;window.deleteManifest=Ve;window.deleteUntaggedManifests=Xe;window.closeManifestDeleteModal=Z;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='Nothing to preview
';return}s.innerHTML=' Rendering preview…
';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='Preview failed. Check your connection and try again.
'})}},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="",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(`
`)!==-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=' 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=''}).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)+"§ion="+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=`
@@ -73,4 +73,4 @@ var He=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:n
Close
- `,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;oLayers '+i+"-"+a+' contain no history ('+s+' layers, click to expand) '+l+" ",c.addEventListener("click",()=>{c.remove();for(let u=o;u{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"];
+ `,a.querySelector("[data-dismiss-modal]").addEventListener("click",()=>a.remove())}}}}document.addEventListener("DOMContentLoaded",()=>{Ze(),et()});var te="showEmptyLayers";function tt(e,t,n){let r=0;for(let o=t;oLayers '+i+"-"+a+' contain no history ('+s+' layers, click to expand) '+l+" ",c.addEventListener("click",()=>{c.remove();for(let u=o;u{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(te,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(te)==="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"];
diff --git a/pkg/appview/server.go b/pkg/appview/server.go
index c6619d0..42de6a1 100644
--- a/pkg/appview/server.go
+++ b/pkg/appview/server.go
@@ -202,8 +202,7 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
atproto.SetTestMode(true)
}
- // Load crypto keys from database (with file fallback and migration)
- oauthKey, err := loadOAuthKey(s.Database, cfg.Server.OAuthKeyPath)
+ oauthKey, err := loadOAuthKey(s.Database)
if err != nil {
return nil, fmt.Errorf("failed to load OAuth key: %w", err)
}
@@ -455,17 +454,6 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
}(client, s.Refresher, holdDID, s.HoldAuthorizer)
}
- // Migrate old-format star records to AT URI format in background
- go func(client *atproto.Client, did string) {
- ctx := context.Background()
- migrated, err := atproto.MigrateStarRecords(ctx, client)
- if err != nil {
- slog.Warn("Star record migration failed", "component", "appview/callback", "did", did, "error", err, "migrated", migrated)
- } else if migrated > 0 {
- slog.Info("Migrated star records to AT URI format", "component", "appview/callback", "did", did, "count", migrated)
- }
- }(client, did)
-
// Drain manifests from old hold to successor in background
go func(client *atproto.Client, did string) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
@@ -485,7 +473,7 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
// Create token issuer
if cfg.Distribution.Auth["token"] != nil {
- rsaKey, certDER, err := loadJWTKeyAndCert(s.Database, cfg.Auth.KeyPath, cfg.Auth.CertPath)
+ rsaKey, certDER, err := loadJWTKeyAndCert(s.Database, cfg.Auth.CertPath)
if err != nil {
return nil, fmt.Errorf("failed to load JWT key material: %w", err)
}
diff --git a/pkg/appview/src/js/app.js b/pkg/appview/src/js/app.js
index b37ec3a..6eecd1e 100644
--- a/pkg/appview/src/js/app.js
+++ b/pkg/appview/src/js/app.js
@@ -138,7 +138,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return;
e.preventDefault();
- wrapper.classList.add('expanded');
+ setSearchExpanded(wrapper, true);
input.focus();
}
});
diff --git a/pkg/appview/storage/crew.go b/pkg/appview/storage/crew.go
index 8d77200..7368fa2 100644
--- a/pkg/appview/storage/crew.go
+++ b/pkg/appview/storage/crew.go
@@ -29,6 +29,18 @@ func EnsureCrewMembership(ctx context.Context, client *atproto.Client, refresher
return
}
+ // Short-circuit if we have a recent cached approval. The hold's requestCrew
+ // would just return success for an established crew member, so the round-trip
+ // is pure waste. Cache lookup uses the same key (holdDID, userDID) that
+ // CheckWriteAccess will hit later in the request.
+ if authorizer != nil {
+ if cached, err := authorizer.IsCachedCrewMember(ctx, holdDID, client.DID()); err == nil && cached {
+ slog.Debug("crew membership cached, skipping requestCrew",
+ "holdDID", holdDID, "userDID", client.DID())
+ return
+ }
+ }
+
// Resolve hold DID to HTTP endpoint
holdEndpoint, err := atproto.ResolveHoldURL(ctx, holdDID)
if err != nil {
@@ -61,8 +73,17 @@ func EnsureCrewMembership(ctx context.Context, client *atproto.Client, refresher
slog.Info("successfully registered as crew member", "holdDID", holdDID, "userDID", client.DID())
- // Clear any cached denial to ensure immediate access
if authorizer != nil {
+ // Warm the approval cache so subsequent CheckWriteAccess calls within this
+ // request (e.g. each layer in a multi-layer push) skip the XRPC getRecord.
+ if err := authorizer.RecordCrewApproval(ctx, holdDID, client.DID()); err != nil {
+ slog.Warn("failed to record crew approval after crew registration",
+ "holdDID", holdDID,
+ "userDID", client.DID(),
+ "error", err)
+ }
+
+ // Clear any cached denial to ensure immediate access
if err := authorizer.ClearCrewDenial(ctx, holdDID, client.DID()); err != nil {
slog.Warn("failed to clear denial cache after crew registration",
"holdDID", holdDID,
diff --git a/pkg/appview/storage/crew_test.go b/pkg/appview/storage/crew_test.go
index 58a29b5..86cd41c 100644
--- a/pkg/appview/storage/crew_test.go
+++ b/pkg/appview/storage/crew_test.go
@@ -2,7 +2,11 @@ package storage
import (
"context"
+ "sync/atomic"
"testing"
+
+ "atcr.io/pkg/atproto"
+ "atcr.io/pkg/auth"
)
func TestEnsureCrewMembership_EmptyHoldDID(t *testing.T) {
@@ -11,4 +15,83 @@ func TestEnsureCrewMembership_EmptyHoldDID(t *testing.T) {
// If we get here without panic, test passes
}
-// TODO: Add comprehensive tests with HTTP client mocking
+// fakeAuthorizer records cache calls for use in EnsureCrewMembership tests.
+type fakeAuthorizer struct {
+ cachedReturn bool
+ isCachedCalls atomic.Int32
+ recordApprovalCalls atomic.Int32
+ clearDenialCalls atomic.Int32
+}
+
+func (f *fakeAuthorizer) GetCaptainRecord(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error) {
+ return nil, nil
+}
+func (f *fakeAuthorizer) CheckReadAccess(ctx context.Context, holdDID, userDID string) (bool, error) {
+ return false, nil
+}
+func (f *fakeAuthorizer) CheckWriteAccess(ctx context.Context, holdDID, userDID string) (bool, error) {
+ return false, nil
+}
+func (f *fakeAuthorizer) IsCrewMember(ctx context.Context, holdDID, userDID string) (bool, error) {
+ return false, nil
+}
+func (f *fakeAuthorizer) ClearCrewDenial(ctx context.Context, holdDID, userDID string) error {
+ f.clearDenialCalls.Add(1)
+ return nil
+}
+func (f *fakeAuthorizer) IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error) {
+ f.isCachedCalls.Add(1)
+ return f.cachedReturn, nil
+}
+func (f *fakeAuthorizer) RecordCrewApproval(ctx context.Context, holdDID, userDID string) error {
+ f.recordApprovalCalls.Add(1)
+ return nil
+}
+
+var _ auth.HoldAuthorizer = (*fakeAuthorizer)(nil)
+
+// TestEnsureCrewMembership_SkipsRequestCrewWhenCached verifies the cache short-circuit:
+// when IsCachedCrewMember returns true, the function returns before attempting the
+// requestCrew POST (and before fetching a service token, which would otherwise fail
+// with a nil refresher).
+func TestEnsureCrewMembership_SkipsRequestCrewWhenCached(t *testing.T) {
+ authz := &fakeAuthorizer{cachedReturn: true}
+ client := atproto.NewClient("https://pds.example", "did:plc:user123", "")
+ holdDID := "did:web:hold01.atcr.io"
+
+ // Pass nil refresher: if the cache check did NOT short-circuit, the function
+ // would log "skipping crew registration" and still return without panic — but
+ // it also would NOT have called IsCachedCrewMember, which is what we assert.
+ EnsureCrewMembership(context.Background(), client, nil, holdDID, authz)
+
+ if authz.isCachedCalls.Load() != 1 {
+ t.Errorf("Expected IsCachedCrewMember to be called once, got %d", authz.isCachedCalls.Load())
+ }
+ if authz.recordApprovalCalls.Load() != 0 {
+ t.Errorf("Expected RecordCrewApproval not to be called on cache hit, got %d", authz.recordApprovalCalls.Load())
+ }
+ if authz.clearDenialCalls.Load() != 0 {
+ t.Errorf("Expected ClearCrewDenial not to be called on cache hit, got %d", authz.clearDenialCalls.Load())
+ }
+}
+
+// TestEnsureCrewMembership_NoRefresherFallsThroughCacheCheck verifies that a cache
+// miss with a nil refresher takes the existing app-password skip path, and does not
+// call RecordCrewApproval (since requestCrew never ran).
+func TestEnsureCrewMembership_NoRefresherFallsThroughCacheCheck(t *testing.T) {
+ authz := &fakeAuthorizer{cachedReturn: false}
+ client := atproto.NewClient("https://pds.example", "did:plc:user123", "")
+ holdDID := "did:web:hold01.atcr.io"
+
+ EnsureCrewMembership(context.Background(), client, nil, holdDID, authz)
+
+ if authz.isCachedCalls.Load() != 1 {
+ t.Errorf("Expected IsCachedCrewMember to be called once, got %d", authz.isCachedCalls.Load())
+ }
+ if authz.recordApprovalCalls.Load() != 0 {
+ t.Errorf("Expected RecordCrewApproval not to be called when requestCrew did not run, got %d", authz.recordApprovalCalls.Load())
+ }
+}
+
+// TODO: Add comprehensive tests with HTTP client mocking for the requestCrew success
+// path (requires a working oauth.Refresher to obtain a service token).
diff --git a/pkg/appview/storage/profile.go b/pkg/appview/storage/profile.go
index e1ee09e..72965f1 100644
--- a/pkg/appview/storage/profile.go
+++ b/pkg/appview/storage/profile.go
@@ -19,40 +19,62 @@ const ProfileRKey = "self"
// Used to prevent duplicate migration goroutines
var migrationLocks sync.Map
-// EnsureProfile checks if a user's profile exists and creates it if needed
-// This should be called during authentication (OAuth exchange or token service)
-// If defaultHoldDID is provided, creates profile with that default (or empty if not provided)
-// Expected format: "did:web:hold01.atcr.io"
-// Normalizes URLs to DIDs for consistency (for backward compatibility)
+// EnsureProfile checks if a user's profile exists and creates it if needed.
+// If the profile already exists, missing fields are reconciled against the
+// AppView's defaults so downstream logic (e.g. successor migration) has a
+// concrete defaultHold to anchor on instead of relying on the empty fallback.
+// Currently only defaultHold is reconciled; other zero-valued fields have
+// their own runtime defaults and shouldn't be written unprompted.
+// This should be called during authentication (OAuth exchange or token service).
+// Expected format for defaultHoldDID: "did:web:hold01.atcr.io"
func EnsureProfile(ctx context.Context, client *atproto.Client, defaultHoldDID string) error {
- // Check if profile already exists
- profile, err := client.GetRecord(ctx, atproto.SailorProfileCollection, ProfileRKey)
- if err == nil && profile != nil {
- // Profile exists, nothing to do
- return nil
- }
-
- // Normalize to DID if it's a URL (or pass through if already a DID)
- // This ensures we store DIDs consistently in new profiles
+ // Resolve the AppView default to a DID up-front; used both for create and reconcile paths.
normalizedDID := ""
if defaultHoldDID != "" {
resolved, err := atproto.ResolveHoldDID(ctx, defaultHoldDID)
if err != nil {
- slog.Warn("Failed to resolve hold DID for new profile", "component", "profile", "defaultHold", defaultHoldDID, "error", err)
+ slog.Warn("Failed to resolve hold DID", "component", "profile", "defaultHold", defaultHoldDID, "error", err)
} else {
normalizedDID = resolved
}
}
- // Profile doesn't exist - create it
- newProfile := atproto.NewSailorProfileRecord(normalizedDID)
-
- _, err = client.PutRecord(ctx, atproto.SailorProfileCollection, ProfileRKey, newProfile)
- if err != nil {
- return fmt.Errorf("failed to create sailor profile: %w", err)
+ record, err := client.GetRecord(ctx, atproto.SailorProfileCollection, ProfileRKey)
+ if err != nil && !errors.Is(err, atproto.ErrRecordNotFound) {
+ // Preserve previous best-effort behavior: log and treat as missing.
+ slog.Warn("Failed to fetch existing profile", "component", "profile", "did", client.DID(), "error", err)
+ record = nil
}
- slog.Debug("Created sailor profile", "component", "profile", "default_hold", normalizedDID)
+ if record == nil {
+ newProfile := atproto.NewSailorProfileRecord(normalizedDID)
+ if _, err := client.PutRecord(ctx, atproto.SailorProfileCollection, ProfileRKey, newProfile); err != nil {
+ return fmt.Errorf("failed to create sailor profile: %w", err)
+ }
+ slog.Debug("Created sailor profile", "component", "profile", "default_hold", normalizedDID)
+ return nil
+ }
+
+ var profile atproto.SailorProfileRecord
+ if err := json.Unmarshal(record.Value, &profile); err != nil {
+ return fmt.Errorf("failed to parse existing profile: %w", err)
+ }
+
+ changed := false
+ if profile.DefaultHold == "" && normalizedDID != "" {
+ profile.DefaultHold = normalizedDID
+ changed = true
+ slog.Info("Reconciling empty defaultHold to AppView default", "component", "profile", "did", client.DID(), "default_hold", normalizedDID)
+ }
+
+ if !changed {
+ return nil
+ }
+
+ profile.UpdatedAt = time.Now()
+ if _, err := client.PutRecord(ctx, atproto.SailorProfileCollection, ProfileRKey, &profile); err != nil {
+ return fmt.Errorf("failed to reconcile sailor profile: %w", err)
+ }
return nil
}
diff --git a/pkg/appview/storage/profile_test.go b/pkg/appview/storage/profile_test.go
index 44d3236..5899e23 100644
--- a/pkg/appview/storage/profile_test.go
+++ b/pkg/appview/storage/profile_test.go
@@ -203,6 +203,99 @@ func TestEnsureProfile_Exists(t *testing.T) {
}
}
+// TestEnsureProfile_ReconcilesEmptyDefaultHold tests that an existing profile
+// with empty defaultHold gets backfilled with the AppView default. This anchors
+// users (typically app-password-only ones who never log in interactively) to a
+// concrete hold so successor migration / drain has a candidate to work with.
+func TestEnsureProfile_ReconcilesEmptyDefaultHold(t *testing.T) {
+ var sentProfile map[string]any
+ var mu sync.Mutex
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // GetRecord: profile exists, defaultHold is empty
+ if r.Method == "GET" {
+ response := `{
+ "uri": "at://did:plc:test123/io.atcr.sailor.profile/self",
+ "cid": "bafytest",
+ "value": {
+ "$type": "io.atcr.sailor.profile",
+ "createdAt": "2025-01-01T00:00:00Z",
+ "updatedAt": "2025-01-01T00:00:00Z"
+ }
+ }`
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(response))
+ return
+ }
+
+ // PutRecord: reconciliation should write
+ if r.Method == "POST" && strings.Contains(r.URL.Path, "putRecord") {
+ var body map[string]any
+ json.NewDecoder(r.Body).Decode(&body)
+ mu.Lock()
+ sentProfile = body
+ mu.Unlock()
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{"uri":"at://did:plc:test123/io.atcr.sailor.profile/self","cid":"bafytest"}`))
+ return
+ }
+ }))
+ defer server.Close()
+
+ client := atproto.NewClient(server.URL, "did:plc:test123", "test-token")
+ if err := EnsureProfile(context.Background(), client, "did:web:hold01.atcr.io"); err != nil {
+ t.Fatalf("EnsureProfile() error = %v", err)
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+ if sentProfile == nil {
+ t.Fatal("expected reconciliation PutRecord call")
+ }
+ recordData := sentProfile["record"].(map[string]any)
+ if got := recordData["defaultHold"]; got != "did:web:hold01.atcr.io" {
+ t.Errorf("defaultHold after reconcile = %v, want did:web:hold01.atcr.io", got)
+ }
+}
+
+// TestEnsureProfile_NoReconcileWhenSet asserts we don't churn existing profiles
+// that already have a defaultHold (regardless of whether it matches the AppView
+// default — that's the user's explicit choice).
+func TestEnsureProfile_NoReconcileWhenSet(t *testing.T) {
+ putRecordCalled := false
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "GET" {
+ response := `{
+ "uri": "at://did:plc:test123/io.atcr.sailor.profile/self",
+ "cid": "bafytest",
+ "value": {
+ "$type": "io.atcr.sailor.profile",
+ "defaultHold": "did:plc:userpicked",
+ "createdAt": "2025-01-01T00:00:00Z",
+ "updatedAt": "2025-01-01T00:00:00Z"
+ }
+ }`
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(response))
+ return
+ }
+ if r.Method == "POST" && strings.Contains(r.URL.Path, "putRecord") {
+ putRecordCalled = true
+ t.Error("PutRecord should not be called when defaultHold is already set")
+ }
+ }))
+ defer server.Close()
+
+ client := atproto.NewClient(server.URL, "did:plc:test123", "test-token")
+ if err := EnsureProfile(context.Background(), client, "did:web:hold01.atcr.io"); err != nil {
+ t.Fatalf("EnsureProfile() error = %v", err)
+ }
+ if putRecordCalled {
+ t.Error("unexpected PutRecord call")
+ }
+}
+
// TestGetProfile tests retrieving a user's profile
func TestGetProfile(t *testing.T) {
tests := []struct {
diff --git a/pkg/appview/templates/components/nav-brand.html b/pkg/appview/templates/components/nav-brand.html
index ac409bf..f602cbc 100644
--- a/pkg/appview/templates/components/nav-brand.html
+++ b/pkg/appview/templates/components/nav-brand.html
@@ -1,11 +1,6 @@
{{ define "nav-brand" }}
-
-
-
-
-
-
+
{{ .ClientName }}
{{ end }}
diff --git a/pkg/atproto/lexicon.go b/pkg/atproto/lexicon.go
index 54e7170..76ddd2b 100644
--- a/pkg/atproto/lexicon.go
+++ b/pkg/atproto/lexicon.go
@@ -3,7 +3,6 @@ package atproto
//go:generate go run generate.go
import (
- "context"
"crypto/sha256"
"encoding/base32"
"encoding/base64"
@@ -434,45 +433,8 @@ type StarRecord struct {
CreatedAt time.Time `json:"createdAt"`
}
-// UnmarshalJSON handles both old format (object subject) and new format (AT URI string subject)
-func (s *StarRecord) UnmarshalJSON(data []byte) error {
- // Use a raw type to inspect the subject field
- type starRecordRaw struct {
- Type string `json:"$type"`
- Subject json.RawMessage `json:"subject"`
- CreatedAt time.Time `json:"createdAt"`
- }
- var raw starRecordRaw
- if err := json.Unmarshal(data, &raw); err != nil {
- return fmt.Errorf("failed to unmarshal star record: %w", err)
- }
-
- s.Type = raw.Type
- s.CreatedAt = raw.CreatedAt
-
- // Try new format: subject is a string AT URI
- var subjectStr string
- if err := json.Unmarshal(raw.Subject, &subjectStr); err == nil && strings.HasPrefix(subjectStr, "at://") {
- s.Subject = subjectStr
- return nil
- }
-
- // Fall back to old format: subject is an object with did + repository
- var oldSubject struct {
- DID string `json:"did"`
- Repository string `json:"repository"`
- }
- if err := json.Unmarshal(raw.Subject, &oldSubject); err != nil {
- return fmt.Errorf("failed to unmarshal star subject (neither AT URI string nor {did, repository} object): %w", err)
- }
-
- s.Subject = BuildRepoPageURI(oldSubject.DID, oldSubject.Repository)
- return nil
-}
-
// GetSubjectDIDAndRepository extracts the owner DID and repository name
-// from the star record's subject AT URI. UnmarshalJSON normalizes old format
-// to AT URI, so this always works with ParseRepoPageURI.
+// from the star record's subject AT URI.
func (s *StarRecord) GetSubjectDIDAndRepository() (ownerDID, repository string, err error) {
return ParseRepoPageURI(s.Subject)
}
@@ -532,62 +494,6 @@ func ParseStarRecordKey(rkey string) (ownerDID, repository string, err error) {
return parts[0], parts[1], nil
}
-// MigrateStarRecords lists the user's star records and rewrites any old-format
-// records (object subject with did/repository) to the new AT URI format.
-// Returns the number of records migrated.
-func MigrateStarRecords(ctx context.Context, client *Client) (int, error) {
- migrated := 0
- cursor := ""
-
- for {
- records, nextCursor, err := client.ListRecordsWithCursor(ctx, StarCollection, 100, cursor)
- if err != nil {
- return migrated, fmt.Errorf("failed to list star records: %w", err)
- }
-
- for _, rec := range records {
- // Try to unmarshal as new format (string subject) — skip if already migrated
- var subjectStr string
- if err := json.Unmarshal(rec.Value, &struct {
- Subject *string `json:"subject"`
- }{Subject: &subjectStr}); err == nil && strings.HasPrefix(subjectStr, "at://") {
- continue
- }
-
- // Old format — unmarshal via StarRecord (which normalizes to AT URI)
- var starRecord StarRecord
- if err := json.Unmarshal(rec.Value, &starRecord); err != nil {
- continue
- }
-
- // Extract rkey from the record URI (at://did/collection/rkey)
- uriParts := strings.Split(rec.URI, "/")
- if len(uriParts) < 2 {
- continue
- }
- rkey := uriParts[len(uriParts)-1]
-
- // Rewrite with new format at the same rkey
- newRecord := &StarRecord{
- Type: StarCollection,
- Subject: starRecord.Subject, // Already normalized to AT URI by UnmarshalJSON
- CreatedAt: starRecord.CreatedAt,
- }
- if _, err := client.PutRecord(ctx, StarCollection, rkey, newRecord); err != nil {
- return migrated, fmt.Errorf("failed to migrate star record %s: %w", rec.URI, err)
- }
- migrated++
- }
-
- if nextCursor == "" {
- break
- }
- cursor = nextCursor
- }
-
- return migrated, nil
-}
-
// IsDID checks if a string is a DID (starts with "did:")
func IsDID(s string) bool {
return len(s) > 4 && s[:4] == "did:"
diff --git a/pkg/atproto/lexicon_test.go b/pkg/atproto/lexicon_test.go
index 5a1f4d4..f27a8c6 100644
--- a/pkg/atproto/lexicon_test.go
+++ b/pkg/atproto/lexicon_test.go
@@ -812,52 +812,27 @@ func TestBlobReference_JSONSerialization(t *testing.T) {
}
}
-func TestStarRecord_DualFormatDeserialization(t *testing.T) {
- tests := []struct {
- name string
- jsonData string
- wantSubject string
- wantOwnerDID string
- wantRepo string
- }{
- {
- name: "new format - AT URI string",
- jsonData: `{"$type":"io.atcr.sailor.star","subject":"at://did:plc:alice123/io.atcr.repo.page/myapp","createdAt":"2025-01-01T00:00:00Z"}`,
- wantSubject: "at://did:plc:alice123/io.atcr.repo.page/myapp",
- wantOwnerDID: "did:plc:alice123",
- wantRepo: "myapp",
- },
- {
- name: "old format - object subject",
- jsonData: `{"$type":"io.atcr.sailor.star","subject":{"did":"did:plc:alice123","repository":"myapp"},"createdAt":"2025-01-01T00:00:00Z"}`,
- wantSubject: "at://did:plc:alice123/io.atcr.repo.page/myapp",
- wantOwnerDID: "did:plc:alice123",
- wantRepo: "myapp",
- },
+func TestStarRecord_Deserialization(t *testing.T) {
+ const jsonData = `{"$type":"io.atcr.sailor.star","subject":"at://did:plc:alice123/io.atcr.repo.page/myapp","createdAt":"2025-01-01T00:00:00Z"}`
+
+ var record StarRecord
+ if err := json.Unmarshal([]byte(jsonData), &record); err != nil {
+ t.Fatalf("Unmarshal error: %v", err)
}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- var record StarRecord
- if err := json.Unmarshal([]byte(tt.jsonData), &record); err != nil {
- t.Fatalf("Unmarshal error: %v", err)
- }
+ if record.Subject != "at://did:plc:alice123/io.atcr.repo.page/myapp" {
+ t.Errorf("Subject = %v", record.Subject)
+ }
- if record.Subject != tt.wantSubject {
- t.Errorf("Subject = %v, want %v", record.Subject, tt.wantSubject)
- }
-
- ownerDID, repo, err := record.GetSubjectDIDAndRepository()
- if err != nil {
- t.Fatalf("GetSubjectDIDAndRepository error: %v", err)
- }
- if ownerDID != tt.wantOwnerDID {
- t.Errorf("ownerDID = %v, want %v", ownerDID, tt.wantOwnerDID)
- }
- if repo != tt.wantRepo {
- t.Errorf("repository = %v, want %v", repo, tt.wantRepo)
- }
- })
+ ownerDID, repo, err := record.GetSubjectDIDAndRepository()
+ if err != nil {
+ t.Fatalf("GetSubjectDIDAndRepository error: %v", err)
+ }
+ if ownerDID != "did:plc:alice123" {
+ t.Errorf("ownerDID = %v", ownerDID)
+ }
+ if repo != "myapp" {
+ t.Errorf("repository = %v", repo)
}
}
diff --git a/pkg/auth/hold_authorizer.go b/pkg/auth/hold_authorizer.go
index b06008a..1333144 100644
--- a/pkg/auth/hold_authorizer.go
+++ b/pkg/auth/hold_authorizer.go
@@ -30,6 +30,16 @@ type HoldAuthorizer interface {
// Called when user successfully becomes a crew member to ensure immediate access
// Returns nil if no denial cache exists or invalidation succeeds
ClearCrewDenial(ctx context.Context, holdDID, userDID string) error
+
+ // IsCachedCrewMember returns true only if there is a non-expired approval
+ // in the cache. It MUST NOT make any network calls. Cache miss returns (false, nil).
+ IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)
+
+ // RecordCrewApproval writes an approval to the cache with the implementation's
+ // standard TTL. Used to warm the cache after an out-of-band confirmation of crew
+ // membership (e.g. a successful requestCrew POST). No-op for implementations
+ // without a cache.
+ RecordCrewApproval(ctx context.Context, holdDID, userDID string) error
}
// CheckReadAccessWithCaptain implements the standard read authorization logic
diff --git a/pkg/auth/hold_remote.go b/pkg/auth/hold_remote.go
index 93190ac..6fbdfe4 100644
--- a/pkg/auth/hold_remote.go
+++ b/pkg/auth/hold_remote.go
@@ -401,6 +401,24 @@ func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID,
return false, nil
}
+// IsCachedCrewMember returns true if there is a non-expired approval row.
+// Never makes network calls. Cache miss or no DB returns (false, nil).
+func (a *RemoteHoldAuthorizer) IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error) {
+ if a.db == nil {
+ return false, nil
+ }
+ return a.getCachedApproval(holdDID, userDID)
+}
+
+// RecordCrewApproval writes an approval to the cache with the standard 15-min TTL.
+// No-op if there is no DB.
+func (a *RemoteHoldAuthorizer) RecordCrewApproval(ctx context.Context, holdDID, userDID string) error {
+ if a.db == nil {
+ return nil
+ }
+ return a.cacheApproval(holdDID, userDID, 15*time.Minute)
+}
+
// CheckReadAccess implements read authorization using shared logic
func (a *RemoteHoldAuthorizer) CheckReadAccess(ctx context.Context, holdDID, userDID string) (bool, error) {
captain, err := a.GetCaptainRecord(ctx, holdDID)
diff --git a/pkg/auth/hold_remote_test.go b/pkg/auth/hold_remote_test.go
index 230a915..8691d9e 100644
--- a/pkg/auth/hold_remote_test.go
+++ b/pkg/auth/hold_remote_test.go
@@ -446,3 +446,129 @@ func TestClearAllDenials_OnStartup(t *testing.T) {
t.Error("Expected all denials to be cleared after ClearAllDenials")
}
}
+
+func TestIsCachedCrewMember_Hit(t *testing.T) {
+ testDB := setupTestDB(t)
+ remote := NewRemoteHoldAuthorizer(testDB, false).(*RemoteHoldAuthorizer)
+ defer close(remote.stopCleanup)
+
+ holdDID := "did:web:hold01.atcr.io"
+ userDID := "did:plc:user123"
+
+ if err := remote.cacheApproval(holdDID, userDID, 15*time.Minute); err != nil {
+ t.Fatalf("cacheApproval failed: %v", err)
+ }
+
+ cached, err := remote.IsCachedCrewMember(context.Background(), holdDID, userDID)
+ if err != nil {
+ t.Fatalf("IsCachedCrewMember returned error: %v", err)
+ }
+ if !cached {
+ t.Error("Expected cache hit, got miss")
+ }
+}
+
+func TestIsCachedCrewMember_Miss(t *testing.T) {
+ testDB := setupTestDB(t)
+ remote := NewRemoteHoldAuthorizer(testDB, false).(*RemoteHoldAuthorizer)
+ defer close(remote.stopCleanup)
+
+ cached, err := remote.IsCachedCrewMember(context.Background(),
+ "did:web:hold01.atcr.io", "did:plc:nobody")
+ if err != nil {
+ t.Fatalf("IsCachedCrewMember returned error: %v", err)
+ }
+ if cached {
+ t.Error("Expected cache miss, got hit")
+ }
+}
+
+func TestIsCachedCrewMember_Expired(t *testing.T) {
+ testDB := setupTestDB(t)
+ remote := NewRemoteHoldAuthorizer(testDB, false).(*RemoteHoldAuthorizer)
+ defer close(remote.stopCleanup)
+
+ holdDID := "did:web:hold01.atcr.io"
+ userDID := "did:plc:user123"
+
+ // Insert a row that already expired one minute ago.
+ now := time.Now()
+ _, err := testDB.Exec(`
+ INSERT INTO hold_crew_approvals (hold_did, user_did, approved_at, expires_at)
+ VALUES (?, ?, ?, ?)
+ `, holdDID, userDID, now.Add(-2*time.Minute), now.Add(-1*time.Minute))
+ if err != nil {
+ t.Fatalf("seed insert failed: %v", err)
+ }
+
+ cached, err := remote.IsCachedCrewMember(context.Background(), holdDID, userDID)
+ if err != nil {
+ t.Fatalf("IsCachedCrewMember returned error: %v", err)
+ }
+ if cached {
+ t.Error("Expected expired entry to be treated as miss")
+ }
+
+ // Expired entry should have been cleaned up by getCachedApproval.
+ var count int
+ if err := testDB.QueryRow(`
+ SELECT COUNT(*) FROM hold_crew_approvals WHERE hold_did = ? AND user_did = ?
+ `, holdDID, userDID).Scan(&count); err != nil {
+ t.Fatalf("count query failed: %v", err)
+ }
+ if count != 0 {
+ t.Errorf("Expected expired row to be deleted, found %d", count)
+ }
+}
+
+func TestRecordCrewApproval_WritesAndReadsBack(t *testing.T) {
+ testDB := setupTestDB(t)
+ remote := NewRemoteHoldAuthorizer(testDB, false).(*RemoteHoldAuthorizer)
+ defer close(remote.stopCleanup)
+
+ holdDID := "did:web:hold01.atcr.io"
+ userDID := "did:plc:user123"
+
+ if err := remote.RecordCrewApproval(context.Background(), holdDID, userDID); err != nil {
+ t.Fatalf("RecordCrewApproval failed: %v", err)
+ }
+
+ cached, err := remote.IsCachedCrewMember(context.Background(), holdDID, userDID)
+ if err != nil {
+ t.Fatalf("IsCachedCrewMember returned error: %v", err)
+ }
+ if !cached {
+ t.Error("Expected RecordCrewApproval to populate the cache")
+ }
+
+ // Verify TTL is roughly 15 minutes from now.
+ var expiresAt time.Time
+ if err := testDB.QueryRow(`
+ SELECT expires_at FROM hold_crew_approvals WHERE hold_did = ? AND user_did = ?
+ `, holdDID, userDID).Scan(&expiresAt); err != nil {
+ t.Fatalf("expires_at query failed: %v", err)
+ }
+ ttl := time.Until(expiresAt)
+ if ttl < 14*time.Minute || ttl > 16*time.Minute {
+ t.Errorf("Expected TTL ~15min, got %v", ttl)
+ }
+}
+
+func TestIsCachedCrewMember_NoDB(t *testing.T) {
+ remote := NewRemoteHoldAuthorizer(nil, false).(*RemoteHoldAuthorizer)
+ defer close(remote.stopCleanup)
+
+ cached, err := remote.IsCachedCrewMember(context.Background(),
+ "did:web:hold01.atcr.io", "did:plc:user123")
+ if err != nil {
+ t.Errorf("Expected nil error with nil DB, got %v", err)
+ }
+ if cached {
+ t.Error("Expected false with nil DB")
+ }
+
+ if err := remote.RecordCrewApproval(context.Background(),
+ "did:web:hold01.atcr.io", "did:plc:user123"); err != nil {
+ t.Errorf("Expected nil error from RecordCrewApproval with nil DB, got %v", err)
+ }
+}
diff --git a/pkg/auth/holdlocal/holdlocal.go b/pkg/auth/holdlocal/holdlocal.go
index 66314f1..b238ccc 100644
--- a/pkg/auth/holdlocal/holdlocal.go
+++ b/pkg/auth/holdlocal/holdlocal.go
@@ -102,3 +102,14 @@ func (a *Authorizer) CheckWriteAccess(ctx context.Context, holdDID, userDID stri
func (a *Authorizer) ClearCrewDenial(ctx context.Context, holdDID, userDID string) error {
return nil
}
+
+// IsCachedCrewMember always returns (false, nil) for the local authorizer.
+// There is no cache; callers fall through to the direct PDS lookup.
+func (a *Authorizer) IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error) {
+ return false, nil
+}
+
+// RecordCrewApproval is a no-op for the local authorizer (no cache to warm).
+func (a *Authorizer) RecordCrewApproval(ctx context.Context, holdDID, userDID string) error {
+ return nil
+}
diff --git a/pkg/hold/pds/crew.go b/pkg/hold/pds/crew.go
index caa17ce..4dd66d3 100644
--- a/pkg/hold/pds/crew.go
+++ b/pkg/hold/pds/crew.go
@@ -5,7 +5,6 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
"strings"
"time"
@@ -202,95 +201,3 @@ func (p *HoldPDS) UpdateCrewMemberTier(ctx context.Context, memberDID, tier stri
return nil
}
-
-// TODO(crew-migration): Remove this migration code after all holds have been upgraded (added 2026-01-06)
-// This migrates TID-based crew records to hash-based rkeys for O(1) lookups
-
-// MigrateCrewRecordsToHashRkeys migrates old TID-based crew records to hash-based rkeys
-// This is idempotent - records that already have hash-based rkeys are skipped
-// Returns the number of records migrated
-func (p *HoldPDS) MigrateCrewRecordsToHashRkeys(ctx context.Context) (int, error) {
- // List all crew members (includes both TID and hash-based rkeys)
- members, err := p.ListCrewMembers(ctx)
- if err != nil {
- return 0, fmt.Errorf("failed to list crew members: %w", err)
- }
-
- slog.Info("Starting crew record migration", "totalRecords", len(members))
-
- migrated := 0
- duplicatesDeleted := 0
- alreadyHashBased := 0
- seen := make(map[string]bool) // Track seen member DIDs to handle duplicates
-
- for _, m := range members {
- memberDID := m.Record.Member
- expectedRkey := atproto.CrewRecordKey(memberDID)
-
- // Skip if already using hash-based rkey
- if m.Rkey == expectedRkey {
- seen[memberDID] = true
- alreadyHashBased++
- continue
- }
-
- // This is a TID-based record that needs migration
- slog.Info("Migrating crew record to hash-based rkey",
- "memberDID", memberDID,
- "oldRkey", m.Rkey,
- "newRkey", expectedRkey)
-
- // Check if we already have a hash-based record for this DID (duplicate handling)
- if seen[memberDID] {
- // Already migrated this DID, just delete the old TID record
- slog.Info("Deleting duplicate TID-based crew record",
- "memberDID", memberDID,
- "rkey", m.Rkey)
- if err := p.RemoveCrewMember(ctx, m.Rkey); err != nil {
- slog.Warn("Failed to delete duplicate crew record",
- "rkey", m.Rkey,
- "error", err)
- } else {
- duplicatesDeleted++
- }
- continue
- }
-
- // Create new record with hash-based rkey (PutRecord handles upsert)
- newRecord := &atproto.CrewRecord{
- Type: atproto.CrewCollection,
- Member: m.Record.Member,
- Role: m.Record.Role,
- Permissions: m.Record.Permissions,
- Tier: m.Record.Tier,
- AddedAt: m.Record.AddedAt,
- }
-
- _, _, err := p.repomgr.PutRecord(ctx, p.uid, atproto.CrewCollection, expectedRkey, newRecord)
- if err != nil {
- slog.Error("Failed to create hash-based crew record",
- "memberDID", memberDID,
- "error", err)
- continue
- }
-
- // Delete the old TID-based record
- if err := p.RemoveCrewMember(ctx, m.Rkey); err != nil {
- slog.Warn("Failed to delete old TID-based crew record",
- "rkey", m.Rkey,
- "error", err)
- // Continue anyway - the new record is created
- }
-
- seen[memberDID] = true
- migrated++
- }
-
- slog.Info("Crew record migration complete",
- "migrated", migrated,
- "duplicatesDeleted", duplicatesDeleted,
- "alreadyHashBased", alreadyHashBased,
- "totalRecords", len(members))
-
- return migrated, nil
-}
diff --git a/pkg/hold/pds/hold_pds.go b/pkg/hold/pds/hold_pds.go
index 9c4d72c..1508624 100644
--- a/pkg/hold/pds/hold_pds.go
+++ b/pkg/hold/pds/hold_pds.go
@@ -353,14 +353,6 @@ func (p *HoldPDS) Bootstrap(ctx context.Context, s3svc *s3.S3Service, cfg Bootst
}
}
- // TODO(crew-migration): Remove this call after all holds have been upgraded (added 2026-01-06)
- // Migrate TID-based crew records to hash-based rkeys for O(1) lookups
- if migrated, err := p.MigrateCrewRecordsToHashRkeys(ctx); err != nil {
- slog.Warn("Crew record migration failed", "error", err)
- } else if migrated > 0 {
- slog.Info("Migrated crew records to hash-based rkeys", "count", migrated)
- }
-
// Create or sync Bluesky profile record from config
// This runs even if captain exists (for existing holds being upgraded)
// Skip if no S3 service (e.g., in tests)