diff --git a/config-appview.example.yaml b/config-appview.example.yaml
index 6ab814e..cd29815 100644
--- a/config-appview.example.yaml
+++ b/config-appview.example.yaml
@@ -45,7 +45,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: ""
+ theme: "seamark"
# 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.
@@ -74,6 +74,7 @@ jetstream:
relay_endpoints:
- https://relay1.us-east.bsky.network
- https://relay1.us-west.bsky.network
+ - https://zlay.waow.tech
# JWT authentication settings.
auth:
# RSA private key for signing registry JWTs issued to Docker clients.
diff --git a/lexicons/io/atcr/sailor/profile.json b/lexicons/io/atcr/sailor/profile.json
index 63877da..d13a09d 100644
--- a/lexicons/io/atcr/sailor/profile.json
+++ b/lexicons/io/atcr/sailor/profile.json
@@ -15,6 +15,10 @@
"format": "uri",
"description": "Default hold endpoint for blob storage. If null, user has opted out of defaults."
},
+ "autoRemoveUntagged": {
+ "type": "boolean",
+ "description": "Automatically delete manifest records that become untagged after a tag overwrite. Layers are cleaned up by hold garbage collection."
+ },
"createdAt": {
"type": "string",
"format": "datetime",
diff --git a/package-lock.json b/package-lock.json
index e6bb743..a334db9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,21 +11,21 @@
"actor-typeahead": "^0.1.2",
"htmx-ext-json-enc": "^2.0.3",
"htmx.org": "^2.0.8",
- "lucide": "^0.575.0"
+ "lucide": "^0.577.0"
},
"devDependencies": {
- "@tailwindcss/cli": "^4.2.0",
+ "@tailwindcss/cli": "^4.2.1",
"@tailwindcss/typography": "^0.5.19",
"daisyui": "^5.5.19",
- "esbuild": "^0.27.3",
+ "esbuild": "^0.27.4",
"glob": "^13.0.6",
"tailwindcss": "^4.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
- "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
+ "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
"cpu": [
"ppc64"
],
@@ -40,9 +40,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
- "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
+ "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
"cpu": [
"arm"
],
@@ -57,9 +57,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
- "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
+ "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
"cpu": [
"arm64"
],
@@ -74,9 +74,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
- "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
+ "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
"cpu": [
"x64"
],
@@ -91,9 +91,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
- "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
+ "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
"cpu": [
"arm64"
],
@@ -108,9 +108,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
- "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
+ "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
"cpu": [
"x64"
],
@@ -125,9 +125,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
- "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
"cpu": [
"arm64"
],
@@ -142,9 +142,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
- "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
+ "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
"cpu": [
"x64"
],
@@ -159,9 +159,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
- "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
+ "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
"cpu": [
"arm"
],
@@ -176,9 +176,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
- "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
+ "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
"cpu": [
"arm64"
],
@@ -193,9 +193,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
- "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
+ "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
"cpu": [
"ia32"
],
@@ -210,9 +210,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
- "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
+ "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
"cpu": [
"loong64"
],
@@ -227,9 +227,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
- "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
+ "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
"cpu": [
"mips64el"
],
@@ -244,9 +244,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
- "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
+ "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
"cpu": [
"ppc64"
],
@@ -261,9 +261,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
- "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
+ "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
"cpu": [
"riscv64"
],
@@ -278,9 +278,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
- "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
+ "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
"cpu": [
"s390x"
],
@@ -295,9 +295,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
- "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
+ "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
"cpu": [
"x64"
],
@@ -312,9 +312,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
"cpu": [
"arm64"
],
@@ -329,9 +329,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
- "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
"cpu": [
"x64"
],
@@ -346,9 +346,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
"cpu": [
"arm64"
],
@@ -363,9 +363,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
- "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
"cpu": [
"x64"
],
@@ -380,9 +380,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
- "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
+ "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
"cpu": [
"arm64"
],
@@ -397,9 +397,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
- "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
+ "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
"cpu": [
"x64"
],
@@ -414,9 +414,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
- "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
+ "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
"cpu": [
"arm64"
],
@@ -431,9 +431,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
- "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
+ "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
"cpu": [
"ia32"
],
@@ -448,9 +448,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
- "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
+ "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
"cpu": [
"x64"
],
@@ -824,28 +824,28 @@
}
},
"node_modules/@tailwindcss/cli": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.0.tgz",
- "integrity": "sha512-C62SWDp+6Rj5DHJDlMyAqESpmljiQ35H4SncAcVn3Gm0rEPrKFDIdAheT74s9zAbrsa2D/L+jJaPgCO1fyZG6g==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz",
+ "integrity": "sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@parcel/watcher": "^2.5.1",
- "@tailwindcss/node": "4.2.0",
- "@tailwindcss/oxide": "4.2.0",
+ "@tailwindcss/node": "4.2.1",
+ "@tailwindcss/oxide": "4.2.1",
"enhanced-resolve": "^5.19.0",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
- "tailwindcss": "4.2.0"
+ "tailwindcss": "4.2.1"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
- "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
+ "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -855,37 +855,37 @@
"lightningcss": "1.31.1",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.2.0"
+ "tailwindcss": "4.2.1"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz",
- "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
+ "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.2.0",
- "@tailwindcss/oxide-darwin-arm64": "4.2.0",
- "@tailwindcss/oxide-darwin-x64": "4.2.0",
- "@tailwindcss/oxide-freebsd-x64": "4.2.0",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0",
- "@tailwindcss/oxide-linux-arm64-musl": "4.2.0",
- "@tailwindcss/oxide-linux-x64-gnu": "4.2.0",
- "@tailwindcss/oxide-linux-x64-musl": "4.2.0",
- "@tailwindcss/oxide-wasm32-wasi": "4.2.0",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0",
- "@tailwindcss/oxide-win32-x64-msvc": "4.2.0"
+ "@tailwindcss/oxide-android-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-x64": "4.2.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz",
- "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
+ "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
"cpu": [
"arm64"
],
@@ -900,9 +900,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz",
- "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
+ "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
"cpu": [
"arm64"
],
@@ -917,9 +917,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz",
- "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
+ "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
"cpu": [
"x64"
],
@@ -934,9 +934,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz",
- "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
+ "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
"cpu": [
"x64"
],
@@ -951,9 +951,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz",
- "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
+ "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
"cpu": [
"arm"
],
@@ -968,9 +968,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz",
- "integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
+ "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
"cpu": [
"arm64"
],
@@ -985,9 +985,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz",
- "integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
+ "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
"cpu": [
"arm64"
],
@@ -1002,9 +1002,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz",
- "integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
+ "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
"cpu": [
"x64"
],
@@ -1019,9 +1019,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz",
- "integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
+ "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
"cpu": [
"x64"
],
@@ -1036,9 +1036,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz",
- "integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
+ "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -1066,9 +1066,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz",
- "integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
+ "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
"cpu": [
"arm64"
],
@@ -1083,9 +1083,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz",
- "integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
+ "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
"cpu": [
"x64"
],
@@ -1129,9 +1129,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
- "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1175,9 +1175,9 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.19.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
- "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
+ "version": "5.20.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
+ "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1189,9 +1189,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
- "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
+ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1202,32 +1202,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.3",
- "@esbuild/android-arm": "0.27.3",
- "@esbuild/android-arm64": "0.27.3",
- "@esbuild/android-x64": "0.27.3",
- "@esbuild/darwin-arm64": "0.27.3",
- "@esbuild/darwin-x64": "0.27.3",
- "@esbuild/freebsd-arm64": "0.27.3",
- "@esbuild/freebsd-x64": "0.27.3",
- "@esbuild/linux-arm": "0.27.3",
- "@esbuild/linux-arm64": "0.27.3",
- "@esbuild/linux-ia32": "0.27.3",
- "@esbuild/linux-loong64": "0.27.3",
- "@esbuild/linux-mips64el": "0.27.3",
- "@esbuild/linux-ppc64": "0.27.3",
- "@esbuild/linux-riscv64": "0.27.3",
- "@esbuild/linux-s390x": "0.27.3",
- "@esbuild/linux-x64": "0.27.3",
- "@esbuild/netbsd-arm64": "0.27.3",
- "@esbuild/netbsd-x64": "0.27.3",
- "@esbuild/openbsd-arm64": "0.27.3",
- "@esbuild/openbsd-x64": "0.27.3",
- "@esbuild/openharmony-arm64": "0.27.3",
- "@esbuild/sunos-x64": "0.27.3",
- "@esbuild/win32-arm64": "0.27.3",
- "@esbuild/win32-ia32": "0.27.3",
- "@esbuild/win32-x64": "0.27.3"
+ "@esbuild/aix-ppc64": "0.27.4",
+ "@esbuild/android-arm": "0.27.4",
+ "@esbuild/android-arm64": "0.27.4",
+ "@esbuild/android-x64": "0.27.4",
+ "@esbuild/darwin-arm64": "0.27.4",
+ "@esbuild/darwin-x64": "0.27.4",
+ "@esbuild/freebsd-arm64": "0.27.4",
+ "@esbuild/freebsd-x64": "0.27.4",
+ "@esbuild/linux-arm": "0.27.4",
+ "@esbuild/linux-arm64": "0.27.4",
+ "@esbuild/linux-ia32": "0.27.4",
+ "@esbuild/linux-loong64": "0.27.4",
+ "@esbuild/linux-mips64el": "0.27.4",
+ "@esbuild/linux-ppc64": "0.27.4",
+ "@esbuild/linux-riscv64": "0.27.4",
+ "@esbuild/linux-s390x": "0.27.4",
+ "@esbuild/linux-x64": "0.27.4",
+ "@esbuild/netbsd-arm64": "0.27.4",
+ "@esbuild/netbsd-x64": "0.27.4",
+ "@esbuild/openbsd-arm64": "0.27.4",
+ "@esbuild/openbsd-x64": "0.27.4",
+ "@esbuild/openharmony-arm64": "0.27.4",
+ "@esbuild/sunos-x64": "0.27.4",
+ "@esbuild/win32-arm64": "0.27.4",
+ "@esbuild/win32-ia32": "0.27.4",
+ "@esbuild/win32-x64": "0.27.4"
}
},
"node_modules/glob": {
@@ -1564,9 +1564,9 @@
}
},
"node_modules/lru-cache": {
- "version": "11.2.6",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
- "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
@@ -1574,9 +1574,9 @@
}
},
"node_modules/lucide": {
- "version": "0.575.0",
- "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.575.0.tgz",
- "integrity": "sha512-+xwqZpvrqPioU8bSH49zH2xARfnKyZgIjdnfbex0CrURB3q4wNFhinYN1Z9Q3lE16Q/6N9iEXnStvyS3c70RKw==",
+ "version": "0.577.0",
+ "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.577.0.tgz",
+ "integrity": "sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==",
"license": "ISC"
},
"node_modules/magic-string": {
@@ -1590,9 +1590,9 @@
}
},
"node_modules/minimatch": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
- "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -1694,9 +1694,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz",
- "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
+ "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
"dev": true,
"license": "MIT"
},
diff --git a/package.json b/package.json
index ff031aa..db07c4d 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,10 @@
"watch": "npm run css:watch & npm run js:watch"
},
"devDependencies": {
- "@tailwindcss/cli": "^4.2.0",
+ "@tailwindcss/cli": "^4.2.1",
"@tailwindcss/typography": "^0.5.19",
"daisyui": "^5.5.19",
- "esbuild": "^0.27.3",
+ "esbuild": "^0.27.4",
"glob": "^13.0.6",
"tailwindcss": "^4.2"
},
@@ -27,6 +27,6 @@
"actor-typeahead": "^0.1.2",
"htmx-ext-json-enc": "^2.0.3",
"htmx.org": "^2.0.8",
- "lucide": "^0.575.0"
+ "lucide": "^0.577.0"
}
}
diff --git a/pkg/appview/db/queries.go b/pkg/appview/db/queries.go
index 69b7291..a5e759c 100644
--- a/pkg/appview/db/queries.go
+++ b/pkg/appview/db/queries.go
@@ -1252,6 +1252,25 @@ func UpdateFirehoseCursor(db DBTX, cursor int64) error {
return err
}
+// IsManifestReferenced checks if a manifest digest is referenced as a child of
+// any manifest list for the given user. Used to protect manifest list children
+// from auto-removal (they are untagged but still needed by their parent list).
+func IsManifestReferenced(db DBTX, did, digest string) (bool, error) {
+ var count int
+ err := db.QueryRow(`
+ SELECT COUNT(*) FROM manifest_references mr
+ JOIN manifests m ON mr.manifest_id = m.id
+ WHERE mr.digest = ? AND m.did = ?
+ LIMIT 1
+ `, digest, did).Scan(&count)
+
+ if err != nil {
+ return false, err
+ }
+
+ return count > 0, nil
+}
+
// IsManifestTagged checks if a manifest has any tags
func IsManifestTagged(db DBTX, did, repository, digest string) (bool, error) {
var count int
@@ -1295,6 +1314,49 @@ func GetManifestTags(db DBTX, did, repository, digest string) ([]string, error)
return tags, nil
}
+// GetUntaggedTopLevelManifestDigests returns digests of top-level manifests that have no tags.
+// Uses the same filtering logic as GetTopLevelManifests (manifest lists + orphaned single-arch).
+func GetUntaggedTopLevelManifestDigests(db DBTX, did, repository string) ([]string, error) {
+ rows, err := db.Query(`
+ WITH manifest_list_children AS (
+ SELECT DISTINCT mr.digest
+ FROM manifest_references mr
+ JOIN manifests m ON mr.manifest_id = m.id
+ WHERE m.did = ? AND m.repository = ?
+ )
+ SELECT m.digest
+ FROM manifests m
+ LEFT JOIN tags t ON m.digest = t.digest AND m.did = t.did AND m.repository = t.repository
+ WHERE m.did = ? AND m.repository = ?
+ AND (
+ m.media_type LIKE '%index%' OR m.media_type LIKE '%manifest.list%'
+ OR
+ m.digest NOT IN (SELECT digest FROM manifest_list_children WHERE digest IS NOT NULL)
+ )
+ GROUP BY m.id
+ HAVING COUNT(t.tag) = 0
+ `, did, repository, did, repository)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var digests []string
+ for rows.Next() {
+ var digest string
+ if err := rows.Scan(&digest); err != nil {
+ return nil, err
+ }
+ digests = append(digests, digest)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+
+ return digests, nil
+}
+
// GetAttestationDetails returns attestation manifests and their layers for a manifest list.
// Joins manifest_references (is_attestation=true) → manifests → layers.
func GetAttestationDetails(db DBTX, did, repository, manifestListDigest string) ([]AttestationDetail, error) {
@@ -1795,6 +1857,12 @@ func (h *HoldDIDDB) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (i
return UpdateManifestHoldDID(h.db, did, oldHoldDID, newHoldDID)
}
+// IsManifestReferenced checks if a digest is a child of any manifest list for the user.
+// Implements storage.ManifestReferenceChecker.
+func (h *HoldDIDDB) IsManifestReferenced(did, digest string) (bool, error) {
+ return IsManifestReferenced(h.db, did, digest)
+}
+
// RepoCardSortOrder specifies how repo cards should be sorted
type RepoCardSortOrder string
diff --git a/pkg/appview/db/queries_test.go b/pkg/appview/db/queries_test.go
index ee0faf4..9abb392 100644
--- a/pkg/appview/db/queries_test.go
+++ b/pkg/appview/db/queries_test.go
@@ -1307,3 +1307,70 @@ func TestDeleteUserData(t *testing.T) {
t.Errorf("Deleting non-existent user should not error, got: %v", err)
}
}
+
+func TestIsManifestReferenced(t *testing.T) {
+ db, err := InitDB(":memory:", LibsqlConfig{})
+ if err != nil {
+ t.Fatalf("Failed to init database: %v", err)
+ }
+ defer db.Close()
+
+ // Insert test user
+ if err := UpsertUser(db, &User{
+ DID: "did:plc:test123",
+ Handle: "testuser.bsky.social",
+ PDSEndpoint: "https://test.pds.example.com",
+ LastSeen: time.Now(),
+ }); err != nil {
+ t.Fatalf("Failed to insert user: %v", err)
+ }
+
+ // Insert a manifest list
+ _, err = db.Exec(`
+ INSERT INTO manifests (did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `, "did:plc:test123", "myapp", "sha256:indexabc", "did:web:hold.example.com", 2,
+ "application/vnd.oci.image.index.v1+json", time.Now())
+ if err != nil {
+ t.Fatalf("Failed to insert manifest list: %v", err)
+ }
+
+ var manifestID int64
+ db.QueryRow(`SELECT id FROM manifests WHERE digest = ?`, "sha256:indexabc").Scan(&manifestID)
+
+ // Insert a child manifest reference
+ _, err = db.Exec(`
+ INSERT INTO manifest_references (manifest_id, digest, media_type, size, platform_architecture, platform_os, reference_index)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `, manifestID, "sha256:childdef", "application/vnd.oci.image.manifest.v1+json", 1000, "amd64", "linux", 0)
+ if err != nil {
+ t.Fatalf("Failed to insert manifest reference: %v", err)
+ }
+
+ // Test 1: child digest should be referenced
+ referenced, err := IsManifestReferenced(db, "did:plc:test123", "sha256:childdef")
+ if err != nil {
+ t.Fatalf("IsManifestReferenced error: %v", err)
+ }
+ if !referenced {
+ t.Error("Expected sha256:childdef to be referenced as a manifest list child")
+ }
+
+ // Test 2: unrelated digest should NOT be referenced
+ referenced, err = IsManifestReferenced(db, "did:plc:test123", "sha256:unrelated")
+ if err != nil {
+ t.Fatalf("IsManifestReferenced error: %v", err)
+ }
+ if referenced {
+ t.Error("Expected sha256:unrelated to NOT be referenced")
+ }
+
+ // Test 3: same digest but different user should NOT be referenced
+ referenced, err = IsManifestReferenced(db, "did:plc:otheruser", "sha256:childdef")
+ if err != nil {
+ t.Fatalf("IsManifestReferenced error: %v", err)
+ }
+ if referenced {
+ t.Error("Expected sha256:childdef to NOT be referenced for different user")
+ }
+}
diff --git a/pkg/appview/handlers/images.go b/pkg/appview/handlers/images.go
index c31f2e0..49b639b 100644
--- a/pkg/appview/handlers/images.go
+++ b/pkg/appview/handlers/images.go
@@ -179,6 +179,74 @@ func (h *DeleteManifestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
w.WriteHeader(http.StatusOK)
}
+// deleteUntaggedRequest is the JSON body for delete untagged manifests requests
+type deleteUntaggedRequest struct {
+ Repo string `json:"repo"`
+}
+
+// DeleteUntaggedManifestsHandler handles bulk-deleting all untagged manifests in a repository
+type DeleteUntaggedManifestsHandler struct {
+ BaseUIHandler
+}
+
+func (h *DeleteUntaggedManifestsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ user := middleware.GetUser(r)
+ if user == nil {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ var req deleteUntaggedRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ digests, err := db.GetUntaggedTopLevelManifestDigests(h.DB, user.DID, req.Repo)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to query untagged manifests: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ if len(digests) == 0 {
+ render.JSON(w, r, map[string]int{"deleted": 0})
+ return
+ }
+
+ pdsClient := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher)
+
+ deleted := 0
+ for _, digest := range digests {
+ rkey := strings.TrimPrefix(digest, "sha256:")
+
+ if err := pdsClient.DeleteRecord(r.Context(), atproto.ManifestCollection, rkey); err != nil {
+ if handleOAuthError(r.Context(), h.Refresher, user.DID, err) {
+ http.Error(w, "Authentication failed, please log in again", http.StatusUnauthorized)
+ return
+ }
+ render.Status(r, http.StatusInternalServerError)
+ render.JSON(w, r, map[string]any{
+ "error": fmt.Sprintf("Failed to delete manifest %s from PDS: %v", digest, err),
+ "deleted": deleted,
+ })
+ return
+ }
+
+ if err := db.DeleteManifest(h.DB, user.DID, req.Repo, digest); err != nil {
+ render.Status(r, http.StatusInternalServerError)
+ render.JSON(w, r, map[string]any{
+ "error": fmt.Sprintf("Failed to delete manifest %s from cache: %v", digest, err),
+ "deleted": deleted,
+ })
+ return
+ }
+
+ deleted++
+ }
+
+ render.JSON(w, r, map[string]int{"deleted": deleted})
+}
+
// UploadAvatarHandler handles uploading/updating a repository avatar
type UploadAvatarHandler struct {
BaseUIHandler
diff --git a/pkg/appview/handlers/settings.go b/pkg/appview/handlers/settings.go
index 7830400..4e536da 100644
--- a/pkg/appview/handlers/settings.go
+++ b/pkg/appview/handlers/settings.go
@@ -132,10 +132,11 @@ func (h *SettingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
PageData
Meta *PageMeta
Profile struct {
- Handle string
- DID string
- PDSEndpoint string
- DefaultHold string
+ Handle string
+ DID string
+ PDSEndpoint string
+ DefaultHold string
+ AutoRemoveUntagged bool
}
ActiveHold *HoldDisplay
OtherHolds []HoldDisplay
@@ -156,6 +157,7 @@ func (h *SettingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
data.Profile.DID = user.DID
data.Profile.PDSEndpoint = user.PDSEndpoint
data.Profile.DefaultHold = profile.DefaultHold
+ data.Profile.AutoRemoveUntagged = profile.AutoRemoveUntagged
if err := h.Templates.ExecuteTemplate(w, "settings", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -381,6 +383,40 @@ func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
}
}
+// UpdateAutoRemoveUntaggedHandler handles toggling the auto-remove-untagged setting
+type UpdateAutoRemoveUntaggedHandler struct {
+ BaseUIHandler
+}
+
+func (h *UpdateAutoRemoveUntaggedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ user := middleware.GetUser(r)
+ if user == nil {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ // Create ATProto client with session provider
+ client := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher)
+
+ // Fetch existing profile
+ profile, err := storage.GetProfile(r.Context(), client)
+ if err != nil || profile == nil {
+ http.Error(w, "Failed to fetch profile", http.StatusInternalServerError)
+ return
+ }
+
+ // Toggle the setting (checkbox sends value when checked, absent when unchecked)
+ profile.AutoRemoveUntagged = !profile.AutoRemoveUntagged
+ profile.UpdatedAt = time.Now()
+
+ if err := storage.UpdateProfile(r.Context(), client, profile); err != nil {
+ http.Error(w, "Failed to update profile: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
// refreshCaptainRecord fetches a hold's captain record via XRPC and caches it locally.
// This ensures badge tiers and other captain metadata are available immediately
// without waiting for Jetstream or the next backfill cycle.
diff --git a/pkg/appview/jetstream/processor.go b/pkg/appview/jetstream/processor.go
index 3023f55..6f8049b 100644
--- a/pkg/appview/jetstream/processor.go
+++ b/pkg/appview/jetstream/processor.go
@@ -772,6 +772,13 @@ func (p *Processor) ProcessStats(ctx context.Context, holdDID string, recordData
return fmt.Errorf("failed to unmarshal stats record: %w", err)
}
+ // Ensure the owner user exists in the users table (repository_stats has FK to users.did)
+ if !isDelete && statsRecord.OwnerDID != "" {
+ if err := p.EnsureUserExists(ctx, statsRecord.OwnerDID); err != nil {
+ return fmt.Errorf("failed to ensure stats owner user exists: %w", err)
+ }
+ }
+
if isDelete {
// Delete from in-memory cache
p.statsCache.Delete(holdDID, statsRecord.OwnerDID, statsRecord.Repository)
diff --git a/pkg/appview/middleware/registry.go b/pkg/appview/middleware/registry.go
index e02c089..de25b37 100644
--- a/pkg/appview/middleware/registry.go
+++ b/pkg/appview/middleware/registry.go
@@ -170,10 +170,11 @@ func (vc *validationCache) getOrFetch(ctx context.Context, cacheKey string, fetc
// These are set by main.go during startup and copied into NamespaceResolver instances.
// After initialization, request handling uses the NamespaceResolver's instance fields.
var (
- globalRefresher *oauth.Refresher
- globalDatabase storage.HoldDIDLookup
- globalAuthorizer auth.HoldAuthorizer
- globalWebhookDispatcher storage.PushWebhookDispatcher
+ globalRefresher *oauth.Refresher
+ globalDatabase storage.HoldDIDLookup
+ globalAuthorizer auth.HoldAuthorizer
+ globalWebhookDispatcher storage.PushWebhookDispatcher
+ globalManifestRefChecker storage.ManifestReferenceChecker
)
// SetGlobalRefresher sets the OAuth refresher instance during initialization
@@ -188,6 +189,11 @@ func SetGlobalDatabase(database storage.HoldDIDLookup) {
globalDatabase = database
}
+// SetGlobalManifestRefChecker sets the manifest reference checker during initialization
+func SetGlobalManifestRefChecker(checker storage.ManifestReferenceChecker) {
+ globalManifestRefChecker = checker
+}
+
// SetGlobalAuthorizer sets the authorizer instance during initialization
// Must be called before the registry starts serving requests
func SetGlobalAuthorizer(authorizer auth.HoldAuthorizer) {
@@ -216,15 +222,16 @@ func init() {
// NamespaceResolver wraps a namespace and resolves names
type NamespaceResolver struct {
distribution.Namespace
- defaultHoldDID string // Default hold DID (e.g., "did:web:hold01.atcr.io")
- baseURL string // Base URL for error messages (e.g., "https://atcr.io")
- testMode bool // If true, fallback to default hold when user's hold is unreachable
- refresher *oauth.Refresher // OAuth session manager (copied from global on init)
- database storage.HoldDIDLookup // Database for hold DID lookups (copied from global on init)
- authorizer auth.HoldAuthorizer // Hold authorization (copied from global on init)
- webhookDispatcher storage.PushWebhookDispatcher // Push webhook dispatcher (copied from global on init)
- validationCache *validationCache // Request-level service token cache
- readmeFetcher *readme.Fetcher // README fetcher for repo pages
+ defaultHoldDID string // Default hold DID (e.g., "did:web:hold01.atcr.io")
+ baseURL string // Base URL for error messages (e.g., "https://atcr.io")
+ testMode bool // If true, fallback to default hold when user's hold is unreachable
+ refresher *oauth.Refresher // OAuth session manager (copied from global on init)
+ database storage.HoldDIDLookup // Database for hold DID lookups (copied from global on init)
+ authorizer auth.HoldAuthorizer // Hold authorization (copied from global on init)
+ webhookDispatcher storage.PushWebhookDispatcher // Push webhook dispatcher (copied from global on init)
+ manifestRefChecker storage.ManifestReferenceChecker // Manifest reference checker (copied from global on init)
+ validationCache *validationCache // Request-level service token cache
+ readmeFetcher *readme.Fetcher // README fetcher for repo pages
}
// initATProtoResolver initializes the name resolution middleware
@@ -251,16 +258,17 @@ func initATProtoResolver(ctx context.Context, ns distribution.Namespace, _ drive
// Copy shared services from globals into the instance
// This avoids accessing globals during request handling
return &NamespaceResolver{
- Namespace: ns,
- defaultHoldDID: defaultHoldDID,
- baseURL: baseURL,
- testMode: testMode,
- refresher: globalRefresher,
- database: globalDatabase,
- authorizer: globalAuthorizer,
- webhookDispatcher: globalWebhookDispatcher,
- validationCache: newValidationCache(),
- readmeFetcher: readme.NewFetcher(),
+ Namespace: ns,
+ defaultHoldDID: defaultHoldDID,
+ baseURL: baseURL,
+ testMode: testMode,
+ refresher: globalRefresher,
+ database: globalDatabase,
+ authorizer: globalAuthorizer,
+ webhookDispatcher: globalWebhookDispatcher,
+ manifestRefChecker: globalManifestRefChecker,
+ validationCache: newValidationCache(),
+ readmeFetcher: readme.NewFetcher(),
}, nil
}
@@ -297,7 +305,8 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name
slog.Debug("Resolved identity", "component", "registry/middleware", "did", did, "pds", pdsEndpoint, "handle", handle)
// Query for hold DID - either user's hold or default hold service
- holdDID := nr.findHoldDID(ctx, did, pdsEndpoint)
+ // Also returns the sailor profile so we can read preferences (e.g. AutoRemoveUntagged)
+ holdDID, sailorProfile := nr.findHoldDIDAndProfile(ctx, did, pdsEndpoint)
if holdDID == "" {
// This is a fatal configuration error - registry cannot function without a hold service
return nil, fmt.Errorf("no hold DID configured: ensure default_hold_did is set in middleware config")
@@ -476,22 +485,24 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name
// 3. The refresher already caches sessions efficiently (in-memory + DB)
// 4. Caching the repository with a stale ATProtoClient causes refresh token errors
registryCtx := &storage.RegistryContext{
- DID: did,
- Handle: handle,
- HoldDID: holdDID,
- HoldURL: holdURL,
- PDSEndpoint: pdsEndpoint,
- Repository: repositoryName,
- ServiceToken: serviceToken, // Cached service token from puller's PDS
- ATProtoClient: atprotoClient,
- AuthMethod: authMethod, // Auth method from JWT token
- PullerDID: pullerDID, // Authenticated user making the request
- PullerPDSEndpoint: pullerPDSEndpoint, // Puller's PDS for service token refresh
- Database: nr.database,
- Authorizer: nr.authorizer,
- Refresher: nr.refresher,
- ReadmeFetcher: nr.readmeFetcher,
- WebhookDispatcher: nr.webhookDispatcher,
+ DID: did,
+ Handle: handle,
+ HoldDID: holdDID,
+ HoldURL: holdURL,
+ PDSEndpoint: pdsEndpoint,
+ Repository: repositoryName,
+ ServiceToken: serviceToken, // Cached service token from puller's PDS
+ ATProtoClient: atprotoClient,
+ AuthMethod: authMethod, // Auth method from JWT token
+ PullerDID: pullerDID, // Authenticated user making the request
+ PullerPDSEndpoint: pullerPDSEndpoint, // Puller's PDS for service token refresh
+ AutoRemoveUntagged: sailorProfile != nil && sailorProfile.AutoRemoveUntagged,
+ Database: nr.database,
+ Authorizer: nr.authorizer,
+ Refresher: nr.refresher,
+ ReadmeFetcher: nr.readmeFetcher,
+ WebhookDispatcher: nr.webhookDispatcher,
+ ManifestRefChecker: nr.manifestRefChecker,
}
return storage.NewRoutingRepository(repo, registryCtx), nil
@@ -512,12 +523,14 @@ func (nr *NamespaceResolver) BlobStatter() distribution.BlobStatter {
return nr.Namespace.BlobStatter()
}
-// findHoldDID determines which hold DID to use for blob storage
+// findHoldDIDAndProfile determines which hold DID to use for blob storage and
+// returns the user's sailor profile (if available) for reading preferences like
+// AutoRemoveUntagged without an extra PDS call.
// Priority order:
// 1. User's sailor profile defaultHold (if set)
// 2. AppView's default hold DID
// Returns a hold DID (e.g., "did:web:hold01.atcr.io"), or empty string if none configured
-func (nr *NamespaceResolver) findHoldDID(ctx context.Context, did, pdsEndpoint string) string {
+func (nr *NamespaceResolver) findHoldDIDAndProfile(ctx context.Context, did, pdsEndpoint string) (string, *atproto.SailorProfileRecord) {
// Create ATProto client (without auth - reading public records)
client := atproto.NewClient(pdsEndpoint, did, "")
@@ -533,16 +546,16 @@ func (nr *NamespaceResolver) findHoldDID(ctx context.Context, did, pdsEndpoint s
// In test mode, verify it's reachable before using it
if nr.testMode {
if nr.isHoldReachable(ctx, profile.DefaultHold) {
- return profile.DefaultHold
+ return profile.DefaultHold, profile
}
slog.Debug("User's defaultHold unreachable, falling back to default", "component", "registry/middleware/testmode", "default_hold", profile.DefaultHold)
- return nr.defaultHoldDID
+ return nr.defaultHoldDID, profile
}
- return profile.DefaultHold
+ return profile.DefaultHold, profile
}
// No profile defaultHold - use AppView default
- return nr.defaultHoldDID
+ return nr.defaultHoldDID, profile
}
// resolveSuccessor checks if a hold has declared a successor and returns it.
diff --git a/pkg/appview/middleware/registry_test.go b/pkg/appview/middleware/registry_test.go
index f2e6235..80d9bf9 100644
--- a/pkg/appview/middleware/registry_test.go
+++ b/pkg/appview/middleware/registry_test.go
@@ -160,7 +160,7 @@ func TestFindHoldDID_DefaultFallback(t *testing.T) {
}
ctx := context.Background()
- holdDID := resolver.findHoldDID(ctx, "did:plc:test123", mockPDS.URL)
+ holdDID, _ := resolver.findHoldDIDAndProfile(ctx, "did:plc:test123", mockPDS.URL)
assert.Equal(t, "did:web:default.atcr.io", holdDID, "should fall back to default hold DID")
}
@@ -188,7 +188,7 @@ func TestFindHoldDID_SailorProfile(t *testing.T) {
}
ctx := context.Background()
- holdDID := resolver.findHoldDID(ctx, "did:plc:test123", mockPDS.URL)
+ holdDID, _ := resolver.findHoldDIDAndProfile(ctx, "did:plc:test123", mockPDS.URL)
assert.Equal(t, "did:web:user.hold.io", holdDID, "should use sailor profile's defaultHold")
}
@@ -215,7 +215,7 @@ func TestFindHoldDID_Priority(t *testing.T) {
}
ctx := context.Background()
- holdDID := resolver.findHoldDID(ctx, "did:plc:test123", mockPDS.URL)
+ holdDID, _ := resolver.findHoldDIDAndProfile(ctx, "did:plc:test123", mockPDS.URL)
// Profile should take priority over hold records and default
assert.Equal(t, "did:web:profile.hold.io", holdDID, "should prioritize sailor profile over hold records")
@@ -244,7 +244,7 @@ func TestFindHoldDID_TestModeFallback(t *testing.T) {
}
ctx := context.Background()
- holdDID := resolver.findHoldDID(ctx, "did:plc:test123", mockPDS.URL)
+ holdDID, _ := resolver.findHoldDIDAndProfile(ctx, "did:plc:test123", mockPDS.URL)
// In test mode with unreachable hold, should fall back to default
assert.Equal(t, "did:web:default.atcr.io", holdDID, "should fall back to default in test mode when hold unreachable")
diff --git a/pkg/appview/public/js/bundle.min.js b/pkg/appview/public/js/bundle.min.js
index 739403f..7f74411 100644
--- a/pkg/appview/public/js/bundle.min.js
+++ b/pkg/appview/public/js/bundle.min.js
@@ -1,4 +1,4 @@
-var ae=(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 i=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return i&&(i==="*"||i.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 i=parseHTML(e);takeChildrenFor(r,i.body),r.title=i.title}else if(n==="body"){r=new DocumentFragment;let i=parseHTML(t);takeChildrenFor(r,i.body),r.title=i.title}else{let i=parseHTML(''+t+"");r=i.querySelector("template").content,r.title=i.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(i=>i.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 s=0,a=0;for(let l=0;l"&&s--}a0;){let s=normalizeSelector(r.shift()),a;s.indexOf("closest ")===0?a=closest(asElement(e),normalizeSelector(s.slice(8))):s.indexOf("find ")===0?a=find(asParentNode(e),normalizeSelector(s.slice(5))):s==="next"||s==="nextElementSibling"?a=asElement(e).nextElementSibling:s.indexOf("next ")===0?a=scanForwardQuery(e,normalizeSelector(s.slice(5)),!!n):s==="previous"||s==="previousElementSibling"?a=asElement(e).previousElementSibling:s.indexOf("previous ")===0?a=scanBackwardsQuery(e,normalizeSelector(s.slice(9)),!!n):s==="document"?a=document:s==="window"?a=window:s==="body"?a=document.body:s==="root"?a=getRootNode(e,!!n):s==="host"?a=e.getRootNode().host:i.push(s),a&&o.push(a)}if(i.length>0){let s=i.join(","),a=asParentNode(getRootNode(e,!!n));o.push(...toArray(a.querySelectorAll(s)))}return o}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o=0;o--){let i=r[o];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}};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 i=processEventArgs(e,t,n,r);i.target.addEventListener(i.event,i.listener,i.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 i=asElement(getClosestMatch(e,function(s){return s!==e&&hasAttribute(asElement(s),t)}));i&&r.push(...findAttributeTargets(i,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?(i=e.substring(0,e.indexOf(":")),o=e.substring(e.indexOf(":")+1)):i=e),t.removeAttribute("hx-swap-oob"),t.removeAttribute("data-hx-swap-oob");let s=querySelectorAllExt(r,o,!1);return s.length?(forEach(s,function(a){let l,c=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(i,a)||(l=asParentNode(c));let h={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",h)&&(a=h.target,h.shouldSwap&&(handlePreservedElements(l),swapWithStyle(i,a,a,l,n),restorePreservedElements()),forEach(n.elts,function(u){triggerEvent(u,"htmx:oobAfterSwap",h)}))}),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 i=o.replace("'","\\'"),s=r.tagName.replace(":","\\:"),a=asParentNode(e),l=a&&a.querySelector(s+"[id='"+i+"']");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,i=null,s=function(){maybeCall(r.beforeSwapCallback),e=resolveTarget(e);let c=r.contextElement?getRootNode(r.contextElement,!1):getDocument(),h=document.activeElement,u={};u={elt:h,start:h?h.selectionStart:null,end:h?h.selectionEnd:null};let f=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else{let d=makeFragment(t);if(f.title=r.title||d.title,r.historyRequest&&(d=d.querySelector("[hx-history-elt],[data-hx-history-elt]")||d),r.selectOOB){let y=r.selectOOB.split(",");for(let m=0;m0?getWindow().setTimeout(v,n.settleDelay):v()},a=htmx.config.globalViewTransitions;n.hasOwnProperty("transition")&&(a=n.transition);let l=r.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",r.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let c=new Promise(function(u,f){o=u,i=f}),h=s;s=function(){document.startViewTransition(function(){return h(),c})}}try{n?.swapDelay&&n.swapDelay>0?getWindow().setTimeout(s,n.swapDelay):s()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",r.eventInfo),maybeCall(i),c}}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let i in o)if(o.hasOwnProperty(i)){let s=o[i];isRawObject(s)?n=s.target!==void 0?s.target:n:s={value:s},triggerEvent(n,i,s)}}else{let o=r.split(",");for(let i=0;i0;){let s=t[0];if(s==="]"){if(r--,r===0){i===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 s==="["&&r++;isPossibleRelativeReference(s,i,n)?o+="(("+n+"."+s+") ? ("+n+"."+s+") : (window."+s+"))":o=o+s,i=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 i=maybeGenerateConditional(e,o,"event");i&&(c.eventFilter=i),r.push(c)}else{let c={trigger:l};var i=maybeGenerateConditional(e,o,"event");for(i&&(c.eventFilter=i),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let u=o.shift();if(u==="changed")c.changed=!0;else if(u==="once")c.once=!0;else if(u==="consume")c.consume=!0;else if(u==="delay"&&o[0]===":")o.shift(),c.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(u==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var s=consumeCSSSelector(o);else{var s=consumeUntil(o,WHITESPACE_OR_COMMA);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();let v=consumeCSSSelector(o);v.length>0&&(s+=" "+v)}}c.from=s}else u==="target"&&o[0]===":"?(o.shift(),c.target=consumeCSSSelector(o)):u==="throttle"&&o[0]===":"?(o.shift(),c.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):u==="queue"&&o[0]===":"?(o.shift(),c.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):u==="root"&&o[0]===":"?(o.shift(),c[u]=consumeCSSSelector(o)):u==="threshold"&&o[0]===":"?(o.shift(),c[u]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}r.push(c)}o.length===a&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r)}return n.length>0?n:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0}function processPolling(e,t,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n))},n.pollInterval)}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else{let i=getRawAttribute(e,"method");r=i?i.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),r==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}n.forEach(function(i){addEventListener(e,function(s,a){let l=asElement(s);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(r,o,l,a)},t,i,!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 i=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:i}),!0}return!1}function addEventListener(e,t,n,r,o){let i=getInternalData(e),s;r.from?s=querySelectorAllExt(e,r.from):s=[e],r.changed&&("lastValue"in i||(i.lastValue=new WeakMap),s.forEach(function(a){i.lastValue.has(r)||i.lastValue.set(r,new WeakMap),i.lastValue.get(r).set(a,a.value)})),forEach(s,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 h=getInternalData(c);if(h.triggerSpec=r,h.handledFor==null&&(h.handledFor=[]),h.handledFor.indexOf(e)<0){if(h.handledFor.push(e),r.consume&&c.stopPropagation(),r.target&&c.target&&!matches(asElement(c.target),r.target))return;if(r.once){if(i.triggeredOnce)return;i.triggeredOnce=!0}if(r.changed){let u=c.target,f=u.value,v=i.lastValue.get(r);if(v.has(u)&&v.get(u)===f)return;v.set(u,f)}if(i.delayed&&clearTimeout(i.delayed),i.throttle)return;r.throttle>0?i.throttle||(triggerEvent(e,"htmx:trigger"),t(e,c),i.throttle=getWindow().setTimeout(function(){i.throttle=null},r.throttle)):r.delay>0?i.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 i=getAttributeValue(e,"hx-"+o);r=!0,t.path=i,t.verb=o,n.forEach(function(s){addTriggerHandler(e,s,t,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(o,i,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(s){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", "+i).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,i=function(s){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,s))})};e.addEventListener(t,i),r.onHandlers.push({event:t,listener:i})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;thtmx.config.historyCacheSize;)i.shift();for(;i.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:i}),i.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 i=getRawAttribute(r,"name");addValueToFormData(i,getValueFromInput(r),t),o&&validateElement(r,n)}r instanceof HTMLFormElement&&(forEach(r.elements,function(i){e.indexOf(i)>=0?removeValueFromFormData(i.name,getValueFromInput(i),t):e.push(i),o&&validateElement(i,n)}),new FormData(r).forEach(function(i,s){i instanceof File&&i.name===""||addValueToFormData(s,i,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,i=[],s=getInternalData(e);s.lastButtonClicked&&!bodyContains(s.lastButtonClicked)&&(s.lastButtonClicked=null);let a=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(s.lastButtonClicked&&(a=a&&s.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,i,getRelatedForm(e),a),processInputValue(n,r,i,e,a),s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let c=s.lastButtonClicked||e,h=getRawAttribute(c,"name");addValueToFormData(h,c.value,o)}let l=findAttributeTargets(e,"hx-include");return forEach(l,function(c){processInputValue(n,r,i,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(h){processInputValue(n,r,i,h,a)})}),overrideFormData(r,o),{errors:i,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(i){r.append(o,i)})}),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 s=splitOnWhitespace(n);if(s.length>0)for(let a=0;a0?o.join(":"):null;r.scroll=h,r.scrollTarget=i}else if(l.indexOf("show:")===0){var o=l.slice(5).split(":");let u=o.pop();var i=o.length>0?o.join(":"):null;r.show=u,r.showTarget=i}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 s=t.showTarget;t.showTarget==="window"&&(s="body"),o=asElement(querySelectorExt(n,s))}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 i=getAttributeValue(e,t);if(i){let s=i.trim(),a=n;if(s==="unset")return null;s.indexOf("javascript:")===0?(s=s.slice(11),a=!0):s.indexOf("js:")===0&&(s=s.slice(3),a=!0),s.indexOf("{")!==0&&(s="{"+s+"}");let l;a?l=maybeEval(e,function(){return o?Function("event","return ("+s+")").call(e,o):Function("return ("+s+")").call(e)},{}):l=parseJSON(s);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),i=(location.protocol!=="about:"?location.origin:window.origin)===r.origin;return htmx.config.selfRequestsOnly&&!i?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:r,sameHost:i},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(i){r.push(i),e.append(t,i)}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(i){e.append(t,i)})}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,i){return r[o]=i,e.delete(t),r.forEach(function(s){e.append(t,s)}),!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,i){let s=null,a=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var l=new Promise(function(p,w){s=p,a=w});n==null&&(n=getDocument().body);let c=o.handler||handleAjaxResponse,h=o.select||null;if(!bodyContains(n))return maybeCall(s),l;let u=o.targetOverride||asElement(getTarget(n));if(u==null||u==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getClosestAttributeValue(n,"hx-target")}),maybeCall(a),l;let f=getInternalData(n),v=f.lastButtonClicked;if(v){let p=getRawAttribute(v,"formaction");p!=null&&(t=p);let w=getRawAttribute(v,"formmethod");if(w!=null)if(VERBS.includes(w.toLowerCase()))e=w;else return maybeCall(s),l}let d=getClosestAttributeValue(n,"hx-confirm");if(i===void 0&&triggerEvent(n,"htmx:confirm",{target:u,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(T){return issueAjaxRequest(e,t,n,r,o,!!T)},question:d})===!1)return maybeCall(s),l;let y=n,m=getClosestAttributeValue(n,"hx-sync"),x=null,b=!1;if(m){let p=m.split(":"),w=p[0].trim();if(w==="this"?y=findThisElement(n,"hx-sync"):y=asElement(querySelectorExt(n,w)),m=(p[1]||"drop").trim(),f=getInternalData(y),m==="drop"&&f.xhr&&f.abortable!==!0)return maybeCall(s),l;if(m==="abort"){if(f.xhr)return maybeCall(s),l;b=!0}else m==="replace"?triggerEvent(y,"htmx:abort"):m.indexOf("queue")===0&&(x=(m.split(" ")[1]||"last").trim())}if(f.xhr)if(f.abortable)triggerEvent(y,"htmx:abort");else{if(x==null){if(r){let p=getInternalData(r);p&&p.triggerSpec&&p.triggerSpec.queue&&(x=p.triggerSpec.queue)}x==null&&(x="last")}return f.queuedRequests==null&&(f.queuedRequests=[]),x==="first"&&f.queuedRequests.length===0?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):x==="all"?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):x==="last"&&(f.queuedRequests=[],f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)})),maybeCall(s),l}let E=new XMLHttpRequest;f.xhr=E,f.abortable=b;let g=function(){f.xhr=null,f.abortable=!1,f.queuedRequests!=null&&f.queuedRequests.length>0&&f.queuedRequests.shift()()},X=getClosestAttributeValue(n,"hx-prompt");if(X){var k=prompt(X);if(k===null||!triggerEvent(n,"htmx:prompt",{prompt:k,target:u}))return maybeCall(s),g(),l}if(d&&!i&&!confirm(d))return maybeCall(s),g(),l;let H=getHeaders(n,u,k);e!=="get"&&!usesFormData(n)&&(H["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(H=mergeObjects(H,o.headers));let $=getInputValues(n,e),R=$.errors,z=$.formData;o.values&&overrideFormData(z,formDataFromObject(o.values));let oe=formDataFromObject(getExpressionVars(n,r)),P=overrideFormData(z,oe),D=filterValues(P,n);htmx.config.getCacheBusterParam&&e==="get"&&D.set("org.htmx.cache-buster",getRawAttribute(u,"id")||"true"),(t==null||t==="")&&(t=location.href);let N=getValuesForElement(n,"hx-request"),J=getInternalData(n).boosted,I=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,S={boosted:J,useUrlParams:I,formData:D,parameters:formDataProxy(D),unfilteredFormData:P,unfilteredParameters:formDataProxy(P),headers:H,elt:n,target:u,verb:e,errors:R,withCredentials:o.credentials||N.credentials||htmx.config.withCredentials,timeout:o.timeout||N.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",S))return maybeCall(s),g(),l;if(t=S.path,e=S.verb,H=S.headers,D=formDataFromObject(S.parameters),R=S.errors,I=S.useUrlParams,R&&R.length>0)return triggerEvent(n,"htmx:validation:halted",S),maybeCall(s),g(),l;let Y=t.split("#"),ie=Y[0],M=Y[1],A=t;if(I&&(A=ie,!D.keys().next().done&&(A.indexOf("?")<0?A+="?":A+="&",A+=urlEncode(D),M&&(A+="#"+M))),!verifyPath(n,A,S))return triggerErrorEvent(n,"htmx:invalidPath",S),maybeCall(a),g(),l;if(E.open(e.toUpperCase(),A,!0),E.overrideMimeType("text/html"),E.withCredentials=S.withCredentials,E.timeout=S.timeout,!N.noHeaders){for(let p in H)if(H.hasOwnProperty(p)){let w=H[p];safelySetHeaderValue(E,p,w)}}let C={xhr:E,target:u,requestConfig:S,etc:o,boosted:J,select:h,pathInfo:{requestPath:t,finalRequestPath:A,responsePath:null,anchor:M}};if(E.onload=function(){try{let p=hierarchyForElt(n);if(C.pathInfo.responsePath=getPathFromResponse(E),c(n,C),C.keepIndicators!==!0&&removeRequestIndicators(L,O),triggerEvent(n,"htmx:afterRequest",C),triggerEvent(n,"htmx:afterOnLoad",C),!bodyContains(n)){let w=null;for(;p.length>0&&w==null;){let T=p.shift();bodyContains(T)&&(w=T)}w&&(triggerEvent(w,"htmx:afterRequest",C),triggerEvent(w,"htmx:afterOnLoad",C))}maybeCall(s)}catch(p){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:p},C)),p}finally{g()}},E.onerror=function(){removeRequestIndicators(L,O),triggerErrorEvent(n,"htmx:afterRequest",C),triggerErrorEvent(n,"htmx:sendError",C),maybeCall(a),g()},E.onabort=function(){removeRequestIndicators(L,O),triggerErrorEvent(n,"htmx:afterRequest",C),triggerErrorEvent(n,"htmx:sendAbort",C),maybeCall(a),g()},E.ontimeout=function(){removeRequestIndicators(L,O),triggerErrorEvent(n,"htmx:afterRequest",C),triggerErrorEvent(n,"htmx:timeout",C),maybeCall(a),g()},!triggerEvent(n,"htmx:beforeRequest",C))return maybeCall(s),g(),l;var L=addRequestIndicatorClasses(n),O=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(p){forEach([E,E.upload],function(w){w.addEventListener(p,function(T){triggerEvent(n,"htmx:xhr:"+p,{lengthComputable:T.lengthComputable,loaded:T.loaded,total:T.total})})})}),triggerEvent(n,"htmx:beforeSend",C);let se=I?null:encodeParamsForBody(E,n,D);return E.send(se),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 i=t.pathInfo.finalRequestPath,s=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,h=null,u=null;return a?(h="push",u=a):l?(h="replace",u=l):c&&(h="push",u=s||i),u?u==="false"?{}:(u==="true"&&(u=s||i),t.pathInfo.anchor&&u.indexOf("#")===-1&&(u=u+"#"+t.pathInfo.anchor),{type:h,path:u}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t.${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,i=getInternalData(o);i&&i.xhr&&i.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})(),F=ae;(function(){let e;F.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(s,a){Object.hasOwn(o,a)?(Array.isArray(o[a])||(o[a]=[o[a]]),o[a].push(s)):o[a]=s});let i=e.getExpressionVars(r);return Object.keys(o).forEach(function(s){o[s]=Object.hasOwn(i,s)?i[s]:o[s]}),JSON.stringify(o)}})})();var G=document.createElement("template");G.innerHTML=`
+var ae=(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 i=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return i&&(i==="*"||i.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 i=parseHTML(e);takeChildrenFor(r,i.body),r.title=i.title}else if(n==="body"){r=new DocumentFragment;let i=parseHTML(t);takeChildrenFor(r,i.body),r.title=i.title}else{let i=parseHTML(''+t+"");r=i.querySelector("template").content,r.title=i.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(i=>i.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 s=0,a=0;for(let l=0;l"&&s--}a0;){let s=normalizeSelector(r.shift()),a;s.indexOf("closest ")===0?a=closest(asElement(e),normalizeSelector(s.slice(8))):s.indexOf("find ")===0?a=find(asParentNode(e),normalizeSelector(s.slice(5))):s==="next"||s==="nextElementSibling"?a=asElement(e).nextElementSibling:s.indexOf("next ")===0?a=scanForwardQuery(e,normalizeSelector(s.slice(5)),!!n):s==="previous"||s==="previousElementSibling"?a=asElement(e).previousElementSibling:s.indexOf("previous ")===0?a=scanBackwardsQuery(e,normalizeSelector(s.slice(9)),!!n):s==="document"?a=document:s==="window"?a=window:s==="body"?a=document.body:s==="root"?a=getRootNode(e,!!n):s==="host"?a=e.getRootNode().host:i.push(s),a&&o.push(a)}if(i.length>0){let s=i.join(","),a=asParentNode(getRootNode(e,!!n));o.push(...toArray(a.querySelectorAll(s)))}return o}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o=0;o--){let i=r[o];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}};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 i=processEventArgs(e,t,n,r);i.target.addEventListener(i.event,i.listener,i.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 i=asElement(getClosestMatch(e,function(s){return s!==e&&hasAttribute(asElement(s),t)}));i&&r.push(...findAttributeTargets(i,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?(i=e.substring(0,e.indexOf(":")),o=e.substring(e.indexOf(":")+1)):i=e),t.removeAttribute("hx-swap-oob"),t.removeAttribute("data-hx-swap-oob");let s=querySelectorAllExt(r,o,!1);return s.length?(forEach(s,function(a){let l,c=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(i,a)||(l=asParentNode(c));let h={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",h)&&(a=h.target,h.shouldSwap&&(handlePreservedElements(l),swapWithStyle(i,a,a,l,n),restorePreservedElements()),forEach(n.elts,function(u){triggerEvent(u,"htmx:oobAfterSwap",h)}))}),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 i=o.replace("'","\\'"),s=r.tagName.replace(":","\\:"),a=asParentNode(e),l=a&&a.querySelector(s+"[id='"+i+"']");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,i=null,s=function(){maybeCall(r.beforeSwapCallback),e=resolveTarget(e);let c=r.contextElement?getRootNode(r.contextElement,!1):getDocument(),h=document.activeElement,u={};u={elt:h,start:h?h.selectionStart:null,end:h?h.selectionEnd:null};let f=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else{let d=makeFragment(t);if(f.title=r.title||d.title,r.historyRequest&&(d=d.querySelector("[hx-history-elt],[data-hx-history-elt]")||d),r.selectOOB){let y=r.selectOOB.split(",");for(let m=0;m0?getWindow().setTimeout(v,n.settleDelay):v()},a=htmx.config.globalViewTransitions;n.hasOwnProperty("transition")&&(a=n.transition);let l=r.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",r.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let c=new Promise(function(u,f){o=u,i=f}),h=s;s=function(){document.startViewTransition(function(){return h(),c})}}try{n?.swapDelay&&n.swapDelay>0?getWindow().setTimeout(s,n.swapDelay):s()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",r.eventInfo),maybeCall(i),c}}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let i in o)if(o.hasOwnProperty(i)){let s=o[i];isRawObject(s)?n=s.target!==void 0?s.target:n:s={value:s},triggerEvent(n,i,s)}}else{let o=r.split(",");for(let i=0;i0;){let s=t[0];if(s==="]"){if(r--,r===0){i===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 s==="["&&r++;isPossibleRelativeReference(s,i,n)?o+="(("+n+"."+s+") ? ("+n+"."+s+") : (window."+s+"))":o=o+s,i=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 i=maybeGenerateConditional(e,o,"event");i&&(c.eventFilter=i),r.push(c)}else{let c={trigger:l};var i=maybeGenerateConditional(e,o,"event");for(i&&(c.eventFilter=i),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let u=o.shift();if(u==="changed")c.changed=!0;else if(u==="once")c.once=!0;else if(u==="consume")c.consume=!0;else if(u==="delay"&&o[0]===":")o.shift(),c.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(u==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var s=consumeCSSSelector(o);else{var s=consumeUntil(o,WHITESPACE_OR_COMMA);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();let v=consumeCSSSelector(o);v.length>0&&(s+=" "+v)}}c.from=s}else u==="target"&&o[0]===":"?(o.shift(),c.target=consumeCSSSelector(o)):u==="throttle"&&o[0]===":"?(o.shift(),c.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):u==="queue"&&o[0]===":"?(o.shift(),c.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):u==="root"&&o[0]===":"?(o.shift(),c[u]=consumeCSSSelector(o)):u==="threshold"&&o[0]===":"?(o.shift(),c[u]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}r.push(c)}o.length===a&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r)}return n.length>0?n:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0}function processPolling(e,t,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n))},n.pollInterval)}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else{let i=getRawAttribute(e,"method");r=i?i.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),r==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}n.forEach(function(i){addEventListener(e,function(s,a){let l=asElement(s);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(r,o,l,a)},t,i,!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 i=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:i}),!0}return!1}function addEventListener(e,t,n,r,o){let i=getInternalData(e),s;r.from?s=querySelectorAllExt(e,r.from):s=[e],r.changed&&("lastValue"in i||(i.lastValue=new WeakMap),s.forEach(function(a){i.lastValue.has(r)||i.lastValue.set(r,new WeakMap),i.lastValue.get(r).set(a,a.value)})),forEach(s,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 h=getInternalData(c);if(h.triggerSpec=r,h.handledFor==null&&(h.handledFor=[]),h.handledFor.indexOf(e)<0){if(h.handledFor.push(e),r.consume&&c.stopPropagation(),r.target&&c.target&&!matches(asElement(c.target),r.target))return;if(r.once){if(i.triggeredOnce)return;i.triggeredOnce=!0}if(r.changed){let u=c.target,f=u.value,v=i.lastValue.get(r);if(v.has(u)&&v.get(u)===f)return;v.set(u,f)}if(i.delayed&&clearTimeout(i.delayed),i.throttle)return;r.throttle>0?i.throttle||(triggerEvent(e,"htmx:trigger"),t(e,c),i.throttle=getWindow().setTimeout(function(){i.throttle=null},r.throttle)):r.delay>0?i.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 i=getAttributeValue(e,"hx-"+o);r=!0,t.path=i,t.verb=o,n.forEach(function(s){addTriggerHandler(e,s,t,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(o,i,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(s){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", "+i).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,i=function(s){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,s))})};e.addEventListener(t,i),r.onHandlers.push({event:t,listener:i})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;thtmx.config.historyCacheSize;)i.shift();for(;i.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:i}),i.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 i=getRawAttribute(r,"name");addValueToFormData(i,getValueFromInput(r),t),o&&validateElement(r,n)}r instanceof HTMLFormElement&&(forEach(r.elements,function(i){e.indexOf(i)>=0?removeValueFromFormData(i.name,getValueFromInput(i),t):e.push(i),o&&validateElement(i,n)}),new FormData(r).forEach(function(i,s){i instanceof File&&i.name===""||addValueToFormData(s,i,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,i=[],s=getInternalData(e);s.lastButtonClicked&&!bodyContains(s.lastButtonClicked)&&(s.lastButtonClicked=null);let a=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(s.lastButtonClicked&&(a=a&&s.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,i,getRelatedForm(e),a),processInputValue(n,r,i,e,a),s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let c=s.lastButtonClicked||e,h=getRawAttribute(c,"name");addValueToFormData(h,c.value,o)}let l=findAttributeTargets(e,"hx-include");return forEach(l,function(c){processInputValue(n,r,i,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(h){processInputValue(n,r,i,h,a)})}),overrideFormData(r,o),{errors:i,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(i){r.append(o,i)})}),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 s=splitOnWhitespace(n);if(s.length>0)for(let a=0;a0?o.join(":"):null;r.scroll=h,r.scrollTarget=i}else if(l.indexOf("show:")===0){var o=l.slice(5).split(":");let u=o.pop();var i=o.length>0?o.join(":"):null;r.show=u,r.showTarget=i}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 s=t.showTarget;t.showTarget==="window"&&(s="body"),o=asElement(querySelectorExt(n,s))}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 i=getAttributeValue(e,t);if(i){let s=i.trim(),a=n;if(s==="unset")return null;s.indexOf("javascript:")===0?(s=s.slice(11),a=!0):s.indexOf("js:")===0&&(s=s.slice(3),a=!0),s.indexOf("{")!==0&&(s="{"+s+"}");let l;a?l=maybeEval(e,function(){return o?Function("event","return ("+s+")").call(e,o):Function("return ("+s+")").call(e)},{}):l=parseJSON(s);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),i=(location.protocol!=="about:"?location.origin:window.origin)===r.origin;return htmx.config.selfRequestsOnly&&!i?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:r,sameHost:i},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(i){r.push(i),e.append(t,i)}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(i){e.append(t,i)})}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,i){return r[o]=i,e.delete(t),r.forEach(function(s){e.append(t,s)}),!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,i){let s=null,a=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var l=new Promise(function(p,w){s=p,a=w});n==null&&(n=getDocument().body);let c=o.handler||handleAjaxResponse,h=o.select||null;if(!bodyContains(n))return maybeCall(s),l;let u=o.targetOverride||asElement(getTarget(n));if(u==null||u==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getClosestAttributeValue(n,"hx-target")}),maybeCall(a),l;let f=getInternalData(n),v=f.lastButtonClicked;if(v){let p=getRawAttribute(v,"formaction");p!=null&&(t=p);let w=getRawAttribute(v,"formmethod");if(w!=null)if(VERBS.includes(w.toLowerCase()))e=w;else return maybeCall(s),l}let d=getClosestAttributeValue(n,"hx-confirm");if(i===void 0&&triggerEvent(n,"htmx:confirm",{target:u,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(T){return issueAjaxRequest(e,t,n,r,o,!!T)},question:d})===!1)return maybeCall(s),l;let y=n,m=getClosestAttributeValue(n,"hx-sync"),x=null,b=!1;if(m){let p=m.split(":"),w=p[0].trim();if(w==="this"?y=findThisElement(n,"hx-sync"):y=asElement(querySelectorExt(n,w)),m=(p[1]||"drop").trim(),f=getInternalData(y),m==="drop"&&f.xhr&&f.abortable!==!0)return maybeCall(s),l;if(m==="abort"){if(f.xhr)return maybeCall(s),l;b=!0}else m==="replace"?triggerEvent(y,"htmx:abort"):m.indexOf("queue")===0&&(x=(m.split(" ")[1]||"last").trim())}if(f.xhr)if(f.abortable)triggerEvent(y,"htmx:abort");else{if(x==null){if(r){let p=getInternalData(r);p&&p.triggerSpec&&p.triggerSpec.queue&&(x=p.triggerSpec.queue)}x==null&&(x="last")}return f.queuedRequests==null&&(f.queuedRequests=[]),x==="first"&&f.queuedRequests.length===0?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):x==="all"?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):x==="last"&&(f.queuedRequests=[],f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)})),maybeCall(s),l}let E=new XMLHttpRequest;f.xhr=E,f.abortable=b;let g=function(){f.xhr=null,f.abortable=!1,f.queuedRequests!=null&&f.queuedRequests.length>0&&f.queuedRequests.shift()()},X=getClosestAttributeValue(n,"hx-prompt");if(X){var k=prompt(X);if(k===null||!triggerEvent(n,"htmx:prompt",{prompt:k,target:u}))return maybeCall(s),g(),l}if(d&&!i&&!confirm(d))return maybeCall(s),g(),l;let H=getHeaders(n,u,k);e!=="get"&&!usesFormData(n)&&(H["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(H=mergeObjects(H,o.headers));let $=getInputValues(n,e),R=$.errors,J=$.formData;o.values&&overrideFormData(J,formDataFromObject(o.values));let oe=formDataFromObject(getExpressionVars(n,r)),P=overrideFormData(J,oe),D=filterValues(P,n);htmx.config.getCacheBusterParam&&e==="get"&&D.set("org.htmx.cache-buster",getRawAttribute(u,"id")||"true"),(t==null||t==="")&&(t=location.href);let N=getValuesForElement(n,"hx-request"),z=getInternalData(n).boosted,L=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,S={boosted:z,useUrlParams:L,formData:D,parameters:formDataProxy(D),unfilteredFormData:P,unfilteredParameters:formDataProxy(P),headers:H,elt:n,target:u,verb:e,errors:R,withCredentials:o.credentials||N.credentials||htmx.config.withCredentials,timeout:o.timeout||N.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",S))return maybeCall(s),g(),l;if(t=S.path,e=S.verb,H=S.headers,D=formDataFromObject(S.parameters),R=S.errors,L=S.useUrlParams,R&&R.length>0)return triggerEvent(n,"htmx:validation:halted",S),maybeCall(s),g(),l;let Y=t.split("#"),ie=Y[0],M=Y[1],A=t;if(L&&(A=ie,!D.keys().next().done&&(A.indexOf("?")<0?A+="?":A+="&",A+=urlEncode(D),M&&(A+="#"+M))),!verifyPath(n,A,S))return triggerErrorEvent(n,"htmx:invalidPath",S),maybeCall(a),g(),l;if(E.open(e.toUpperCase(),A,!0),E.overrideMimeType("text/html"),E.withCredentials=S.withCredentials,E.timeout=S.timeout,!N.noHeaders){for(let p in H)if(H.hasOwnProperty(p)){let w=H[p];safelySetHeaderValue(E,p,w)}}let C={xhr:E,target:u,requestConfig:S,etc:o,boosted:z,select:h,pathInfo:{requestPath:t,finalRequestPath:A,responsePath:null,anchor:M}};if(E.onload=function(){try{let p=hierarchyForElt(n);if(C.pathInfo.responsePath=getPathFromResponse(E),c(n,C),C.keepIndicators!==!0&&removeRequestIndicators(O,F),triggerEvent(n,"htmx:afterRequest",C),triggerEvent(n,"htmx:afterOnLoad",C),!bodyContains(n)){let w=null;for(;p.length>0&&w==null;){let T=p.shift();bodyContains(T)&&(w=T)}w&&(triggerEvent(w,"htmx:afterRequest",C),triggerEvent(w,"htmx:afterOnLoad",C))}maybeCall(s)}catch(p){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:p},C)),p}finally{g()}},E.onerror=function(){removeRequestIndicators(O,F),triggerErrorEvent(n,"htmx:afterRequest",C),triggerErrorEvent(n,"htmx:sendError",C),maybeCall(a),g()},E.onabort=function(){removeRequestIndicators(O,F),triggerErrorEvent(n,"htmx:afterRequest",C),triggerErrorEvent(n,"htmx:sendAbort",C),maybeCall(a),g()},E.ontimeout=function(){removeRequestIndicators(O,F),triggerErrorEvent(n,"htmx:afterRequest",C),triggerErrorEvent(n,"htmx:timeout",C),maybeCall(a),g()},!triggerEvent(n,"htmx:beforeRequest",C))return maybeCall(s),g(),l;var O=addRequestIndicatorClasses(n),F=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(p){forEach([E,E.upload],function(w){w.addEventListener(p,function(T){triggerEvent(n,"htmx:xhr:"+p,{lengthComputable:T.lengthComputable,loaded:T.loaded,total:T.total})})})}),triggerEvent(n,"htmx:beforeSend",C);let se=L?null:encodeParamsForBody(E,n,D);return E.send(se),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 i=t.pathInfo.finalRequestPath,s=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,h=null,u=null;return a?(h="push",u=a):l?(h="replace",u=l):c&&(h="push",u=s||i),u?u==="false"?{}:(u==="true"&&(u=s||i),t.pathInfo.anchor&&u.indexOf("#")===-1&&(u=u+"#"+t.pathInfo.anchor),{type:h,path:u}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t.${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,i=getInternalData(o);i&&i.xhr&&i.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})(),q=ae;(function(){let e;q.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(s,a){Object.hasOwn(o,a)?(Array.isArray(o[a])||(o[a]=[o[a]]),o[a].push(s)):o[a]=s});let i=e.getExpressionVars(r);return Object.keys(o).forEach(function(s){o[s]=Object.hasOwn(i,s)?i[s]:o[s]}),JSON.stringify(o)}})})();var G=document.createElement("template");G.innerHTML=`
@@ -92,4 +92,4 @@ var ae=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:n
-`;function K(e){return e.cloneNode(!0)}var B=class extends HTMLElement{static tag="actor-typeahead";static define(t=this.tag){this.tag=t;let n=customElements.getName(this);if(n&&n!==t)return console.warn(`${this.name} already defined as <${n}>!`);let r=customElements.get(t);if(r&&r!==this)return console.warn(`<${t}> already defined as ${r.name}!`);customElements.define(t,this)}static{let t=new URL(import.meta.url).searchParams.get("tag")||this.tag;t!=="none"&&this.define(t)}#n=this.attachShadow({mode:"closed"});#r=[];#e=-1;#o=!1;constructor(){super(),this.#n.append(K(G).content),this.#t(),this.addEventListener("input",this),this.addEventListener("focusout",this),this.addEventListener("keydown",this),this.#n.addEventListener("pointerdown",this),this.#n.addEventListener("pointerup",this),this.#n.addEventListener("click",this)}get#i(){let t=Number.parseInt(this.getAttribute("rows")??"");return Number.isNaN(t)?5:t}handleEvent(t){switch(t.type){case"input":this.#a(t);break;case"keydown":this.#s(t);break;case"focusout":this.#l(t);break;case"pointerdown":this.#c(t);break;case"pointerup":this.#u(t);break}}#s(t){switch(t.key){case"ArrowDown":t.preventDefault(),this.#e=Math.min(this.#e+1,this.#i-1),this.#t();break;case"PageDown":t.preventDefault(),this.#e=this.#i-1,this.#t();break;case"ArrowUp":t.preventDefault(),this.#e=Math.max(this.#e-1,0),this.#t();break;case"PageUp":t.preventDefault(),this.#e=0,this.#t();break;case"Escape":t.preventDefault(),this.#r=[],this.#e=-1,this.#t();break;case"Enter":t.preventDefault(),this.#n.querySelectorAll("button")[this.#e]?.dispatchEvent(new PointerEvent("pointerup",{bubbles:!0}));break}}async#a(t){let n=t.target?.value;if(!n){this.#r=[],this.#t();return}let r=this.getAttribute("host")??"https://public.api.bsky.app",o=new URL("xrpc/app.bsky.actor.searchActorsTypeahead",r);o.searchParams.set("q",n),o.searchParams.set("limit",`${this.#i}`);let s=await(await fetch(o)).json();this.#r=s.actors,this.#e=-1,this.#t()}async#l(t){this.#o||(this.#r=[],this.#e=-1,this.#t())}#t(){let t=document.createDocumentFragment(),n=-1;for(let r of this.#r){let o=K(Q).content,i=o.querySelector("button");i&&(i.dataset.handle=r.handle,++n===this.#e&&(i.dataset.active="true"));let s=o.querySelector("img");s&&r.avatar&&(s.src=r.avatar);let a=o.querySelector(".handle");a&&(a.textContent=r.handle),t.append(o)}this.#n.querySelector(".menu")?.replaceChildren(...t.children)}#c(t){this.#o=!0}#u(t){this.#o=!1,this.querySelector("input")?.focus();let n=t.target?.closest("button"),r=this.querySelector("input");!r||!n||(r.value=n.dataset.handle||"",this.#r=[],this.#t())}};function ee(){return localStorage.getItem("theme")||"system"}function le(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function _(){let e=ee(),n=le(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),ce(e)}function te(e){localStorage.setItem("theme",e),_(),ue()}function ce(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,o=n.querySelector(".theme-check");o&&(o.style.visibility=r?"visible":"hidden")})}function ue(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{ee()==="system"&&_()});function fe(){let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(e.classList.toggle("expanded"),e.classList.contains("expanded")&&t.focus())}function V(){let e=document.querySelector(".nav-search-wrapper");e&&e.classList.remove("expanded")}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")&&V(),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)&&V()}))});function ne(e,t){!t&&typeof event<"u"&&(t=event.target.closest("button")),navigator.clipboard.writeText(e).then(()=>{if(!t)return;let n=t.innerHTML;t.innerHTML=' Copied!',setTimeout(()=>{t.innerHTML=n},2e3)}).catch(n=>{console.error("Failed to copy:",n)})}document.addEventListener("DOMContentLoaded",()=>{document.addEventListener("click",e=>{let t=e.target.closest("button[data-cmd]");if(t){ne(t.getAttribute("data-cmd"),t);return}if(e.target.closest("a, button, input, .cmd"))return;let n=e.target.closest("[data-href]");n&&(window.location=n.getAttribute("data-href"))})});function de(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 i=Math.floor(t/o);if(i>=1)return i===1?`1 ${r} ago`:`${i} ${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=de(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{j(),_(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{te(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);setInterval(j,6e4);function he(){let e=document.getElementById("show-offline-toggle"),t=document.querySelector(".manifests-list");!e||!t||(localStorage.setItem("showOfflineManifests",e.checked),e.checked?t.classList.add("show-offline"):t.classList.remove("show-offline"))}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("show-offline-toggle");if(!e)return;let t=localStorage.getItem("showOfflineManifests")==="true";e.checked=t;let n=document.querySelector(".manifests-list");n&&(t?n.classList.add("show-offline"):n.classList.remove("show-offline"))});async function me(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();ge(e,t,n,o.tags)}else if(r.ok)re(n);else{let o=await r.text();alert(`Failed to delete manifest: ${o}`)}}catch(r){console.error("Error deleting manifest:",r),alert(`Error deleting manifest: ${r.message}`)}}function ge(e,t,n,r){let o=document.getElementById("manifest-delete-modal"),i=document.getElementById("manifest-delete-tags"),s=document.getElementById("confirm-manifest-delete-btn");i.innerHTML="",r.forEach(a=>{let l=document.createElement("li");l.textContent=a,i.appendChild(l)}),s.onclick=()=>pe(e,t,n),o.style.display="flex"}function W(){let e=document.getElementById("manifest-delete-modal");e.style.display="none"}async function pe(e,t,n){let r=document.getElementById("confirm-manifest-delete-btn"),o=r.textContent;try{r.disabled=!0,r.textContent="Deleting...";let i=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!0})});if(i.ok)W(),re(n),location.reload();else{let s=await i.text();alert(`Failed to delete manifest: ${s}`),r.disabled=!1,r.textContent=o}}catch(i){console.error("Error deleting manifest:",i),alert(`Error deleting manifest: ${i.message}`),r.disabled=!1,r.textContent=o}}function re(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&&W()})});async function Ee(e,t){let n=document.getElementById("vuln-detail-modal"),r=document.getElementById("vuln-modal-body");if(!(!n||!r)){r.innerHTML='
',n.showModal();try{let o=await fetch(`/api/vuln-details?digest=${encodeURIComponent(e)}&holdEndpoint=${encodeURIComponent(t)}`);r.innerHTML=await o.text()}catch{r.innerHTML='Failed to load vulnerability details
'}}}var U=class{constructor(t){this.input=t,this.typeahead=t.closest("actor-typeahead"),this.dropdown=null,this.currentFocus=-1,this.typeaheadClosed=!1,this.init()}init(){this.createDropdown(),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.hideDropdown()})}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="recent-accounts-dropdown",this.dropdown.style.display="none",this.typeahead?this.typeahead.insertAdjacentElement("afterend",this.dropdown):this.input.insertAdjacentElement("afterend",this.dropdown)}handleFocus(){this.input.value.trim().length<1&&this.showRecentAccounts()}handleInput(){this.input.value.trim().length>=1&&this.hideDropdown(),this.typeaheadClosed=!1}showRecentAccounts(){let t=this.getRecentAccounts();if(t.length===0){this.hideDropdown();return}this.dropdown.innerHTML="",this.currentFocus=-1;let n=document.createElement("div");n.className="recent-accounts-header",n.textContent="Recent accounts",this.dropdown.appendChild(n),t.forEach((r,o)=>{let i=document.createElement("div");i.className="recent-accounts-item",i.dataset.index=o,i.dataset.handle=r,i.textContent=r,i.addEventListener("click",()=>this.selectItem(r)),this.dropdown.appendChild(i)}),this.dropdown.style.display="block"}selectItem(t){this.input.value=t,this.hideDropdown(),this.input.focus()}hideDropdown(){this.dropdown.style.display="none",this.currentFocus=-1}handleKeydown(t){if(t.key==="Enter"&&this.input.value.trim().length>=2&&(this.typeaheadClosed=!0),t.key==="Tab"&&this.input.value.trim().length>=2&&!this.typeaheadClosed){t.preventDefault();let o=t.shiftKey?"ArrowUp":"ArrowDown",i=new KeyboardEvent("keydown",{key:o,bubbles:!0,cancelable:!0});this.typeahead.dispatchEvent(i);return}if(this.dropdown.style.display==="none")return;let n=this.dropdown.querySelectorAll(".recent-accounts-item");t.key==="ArrowDown"?(t.preventDefault(),this.currentFocus++,this.currentFocus>=n.length&&(this.currentFocus=0),this.updateFocus(n)):t.key==="ArrowUp"?(t.preventDefault(),this.currentFocus--,this.currentFocus<0&&(this.currentFocus=n.length-1),this.updateFocus(n)):t.key==="Enter"&&this.currentFocus>-1&&n[this.currentFocus]?(t.preventDefault(),this.selectItem(n[this.currentFocus].dataset.handle)):t.key==="Escape"&&this.hideDropdown()}updateFocus(t){t.forEach((n,r)=>{n.classList.toggle("focused",r===this.currentFocus)})}getRecentAccounts(){try{let t=localStorage.getItem("atcr_recent_handles");return t?JSON.parse(t):[]}catch{return[]}}saveRecentAccount(t){if(t)try{let n=this.getRecentAccounts();n=n.filter(r=>r!==t),n.unshift(t),n=n.slice(0,5),localStorage.setItem("atcr_recent_handles",JSON.stringify(n))}catch(n){console.error("Failed to save recent account:",n)}}};document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("login-form"),t=document.getElementById("handle");e&&t&&new U(t)});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){try{let n="atcr_recent_handles",r=JSON.parse(localStorage.getItem(n)||"[]");r=r.filter(o=>o!==t),r.unshift(t),r=r.slice(0,5),localStorage.setItem(n,JSON.stringify(r))}catch(n){console.error("Failed to save recent account:",n)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});function Z(){let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),n=document.getElementById("carousel-next");if(!e)return;let r=Array.from(e.querySelectorAll(".carousel-item"));if(r.length===0)return;let o=null,i=5e3,s=0,a=0,l=0;function c(){let b=r[0];if(!b)return;let E=getComputedStyle(e),g=parseFloat(E.gap)||24;s=b.offsetWidth+g,a=e.offsetWidth,l=e.scrollWidth}requestAnimationFrame(()=>{requestAnimationFrame(()=>{c(),m()})});let h;window.addEventListener("resize",()=>{clearTimeout(h),h=setTimeout(c,150)});function u(){return s||c(),s}function f(){return(!a||!s)&&c(),Math.round(a/s)||1}function v(){return(!l||!a)&&c(),l-a}function d(){let b=u(),E=v(),g=e.scrollLeft;g>=E-10?e.scrollTo({left:0,behavior:"smooth"}):e.scrollTo({left:g+b,behavior:"smooth"})}function y(){let b=u(),E=v(),g=e.scrollLeft;g<=10?e.scrollTo({left:E,behavior:"smooth"}):e.scrollTo({left:g-b,behavior:"smooth"})}t&&t.addEventListener("click",()=>{x(),y(),m()}),n&&n.addEventListener("click",()=>{x(),d(),m()});function m(){o||r.length<=f()||(o=setInterval(d,i))}function x(){o&&(clearInterval(o),o=null)}e.addEventListener("mouseenter",x),e.addEventListener("mouseleave",m)}document.addEventListener("DOMContentLoaded",()=>{"requestIdleCallback"in window?requestIdleCallback(Z,{timeout:2e3}):setTimeout(Z,100)});function q(e,t){let n=document.getElementById("toast-container");n||(n=document.createElement("div"),n.id="toast-container",n.className="toast toast-end toast-bottom z-50",document.body.appendChild(n));let r=t==="error"?"alert-error":"alert-success",o=document.createElement("div");o.className=`alert ${r} shadow-lg transition-opacity duration-300`,o.innerHTML=`${e}`,n.appendChild(o),setTimeout(()=>{o.style.opacity="0",setTimeout(()=>o.remove(),300)},3e3)}async function ye(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"')?q("Test webhook delivered successfully!","success"):q("Test delivery failed \u2014 check the webhook URL","error")}catch{q("Failed to reach server","error")}}window.setTheme=te;window.toggleSearch=fe;window.closeSearch=V;window.copyToClipboard=ne;window.toggleOfflineManifests=he;window.deleteManifest=me;window.closeManifestDeleteModal=W;window.openVulnDetails=Ee;window.showToast=q;window.testWebhook=ye;window.htmx=F;
+`;function K(e){return e.cloneNode(!0)}var B=class extends HTMLElement{static tag="actor-typeahead";static define(t=this.tag){this.tag=t;let n=customElements.getName(this);if(n&&n!==t)return console.warn(`${this.name} already defined as <${n}>!`);let r=customElements.get(t);if(r&&r!==this)return console.warn(`<${t}> already defined as ${r.name}!`);customElements.define(t,this)}static{let t=new URL(import.meta.url).searchParams.get("tag")||this.tag;t!=="none"&&this.define(t)}#n=this.attachShadow({mode:"closed"});#r=[];#e=-1;#o=!1;constructor(){super(),this.#n.append(K(G).content),this.#t(),this.addEventListener("input",this),this.addEventListener("focusout",this),this.addEventListener("keydown",this),this.#n.addEventListener("pointerdown",this),this.#n.addEventListener("pointerup",this),this.#n.addEventListener("click",this)}get#i(){let t=Number.parseInt(this.getAttribute("rows")??"");return Number.isNaN(t)?5:t}handleEvent(t){switch(t.type){case"input":this.#a(t);break;case"keydown":this.#s(t);break;case"focusout":this.#l(t);break;case"pointerdown":this.#c(t);break;case"pointerup":this.#u(t);break}}#s(t){switch(t.key){case"ArrowDown":t.preventDefault(),this.#e=Math.min(this.#e+1,this.#i-1),this.#t();break;case"PageDown":t.preventDefault(),this.#e=this.#i-1,this.#t();break;case"ArrowUp":t.preventDefault(),this.#e=Math.max(this.#e-1,0),this.#t();break;case"PageUp":t.preventDefault(),this.#e=0,this.#t();break;case"Escape":t.preventDefault(),this.#r=[],this.#e=-1,this.#t();break;case"Enter":t.preventDefault(),this.#n.querySelectorAll("button")[this.#e]?.dispatchEvent(new PointerEvent("pointerup",{bubbles:!0}));break}}async#a(t){let n=t.target?.value;if(!n){this.#r=[],this.#t();return}let r=this.getAttribute("host")??"https://public.api.bsky.app",o=new URL("xrpc/app.bsky.actor.searchActorsTypeahead",r);o.searchParams.set("q",n),o.searchParams.set("limit",`${this.#i}`);let s=await(await fetch(o)).json();this.#r=s.actors,this.#e=-1,this.#t()}async#l(t){this.#o||(this.#r=[],this.#e=-1,this.#t())}#t(){let t=document.createDocumentFragment(),n=-1;for(let r of this.#r){let o=K(Q).content,i=o.querySelector("button");i&&(i.dataset.handle=r.handle,++n===this.#e&&(i.dataset.active="true"));let s=o.querySelector("img");s&&r.avatar&&(s.src=r.avatar);let a=o.querySelector(".handle");a&&(a.textContent=r.handle),t.append(o)}this.#n.querySelector(".menu")?.replaceChildren(...t.children)}#c(t){this.#o=!0}#u(t){this.#o=!1,this.querySelector("input")?.focus();let n=t.target?.closest("button"),r=this.querySelector("input");!r||!n||(r.value=n.dataset.handle||"",this.#r=[],this.#t())}};function ee(){return localStorage.getItem("theme")||"system"}function le(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function _(){let e=ee(),n=le(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),ce(e)}function te(e){localStorage.setItem("theme",e),_(),ue()}function ce(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,o=n.querySelector(".theme-check");o&&(o.style.visibility=r?"visible":"hidden")})}function ue(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{ee()==="system"&&_()});function fe(){let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(e.classList.toggle("expanded"),e.classList.contains("expanded")&&t.focus())}function V(){let e=document.querySelector(".nav-search-wrapper");e&&e.classList.remove("expanded")}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")&&V(),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)&&V()}))});function ne(e,t){!t&&typeof event<"u"&&(t=event.target.closest("button")),navigator.clipboard.writeText(e).then(()=>{if(!t)return;let n=t.innerHTML;t.innerHTML=' Copied!',setTimeout(()=>{t.innerHTML=n},2e3)}).catch(n=>{console.error("Failed to copy:",n)})}document.addEventListener("DOMContentLoaded",()=>{document.addEventListener("click",e=>{let t=e.target.closest("button[data-cmd]");if(t){ne(t.getAttribute("data-cmd"),t);return}if(e.target.closest("a, button, input, .cmd"))return;let n=e.target.closest("[data-href]");n&&(window.location=n.getAttribute("data-href"))})});function de(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 i=Math.floor(t/o);if(i>=1)return i===1?`1 ${r} ago`:`${i} ${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=de(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{j(),_(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{te(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);setInterval(j,6e4);function he(){let e=document.getElementById("show-offline-toggle"),t=document.querySelector(".manifests-list");!e||!t||(localStorage.setItem("showOfflineManifests",e.checked),e.checked?t.classList.add("show-offline"):t.classList.remove("show-offline"))}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("show-offline-toggle");if(!e)return;let t=localStorage.getItem("showOfflineManifests")==="true";e.checked=t;let n=document.querySelector(".manifests-list");n&&(t?n.classList.add("show-offline"):n.classList.remove("show-offline"))});async function me(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();ge(e,t,n,o.tags)}else if(r.ok)re(n);else{let o=await r.text();alert(`Failed to delete manifest: ${o}`)}}catch(r){console.error("Error deleting manifest:",r),alert(`Error deleting manifest: ${r.message}`)}}function ge(e,t,n,r){let o=document.getElementById("manifest-delete-modal"),i=document.getElementById("manifest-delete-tags"),s=document.getElementById("confirm-manifest-delete-btn");i.innerHTML="",r.forEach(a=>{let l=document.createElement("li");l.textContent=a,i.appendChild(l)}),s.onclick=()=>pe(e,t,n),o.style.display="flex"}function W(){let e=document.getElementById("manifest-delete-modal");e.style.display="none"}async function pe(e,t,n){let r=document.getElementById("confirm-manifest-delete-btn"),o=r.textContent;try{r.disabled=!0,r.textContent="Deleting...";let i=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!0})});if(i.ok)W(),re(n),location.reload();else{let s=await i.text();alert(`Failed to delete manifest: ${s}`),r.disabled=!1,r.textContent=o}}catch(i){console.error("Error deleting manifest:",i),alert(`Error deleting manifest: ${i.message}`),r.disabled=!1,r.textContent=o}}async function Ee(e){let t=document.getElementById("confirm-untagged-delete-btn"),n=t.textContent;try{t.disabled=!0,t.textContent="Deleting...";let r=await fetch("/api/manifests/untagged",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e})}),o=await r.json();r.ok?(document.getElementById("untagged-delete-modal").close(),I(`Deleted ${o.deleted} untagged manifest(s)`,"success"),o.deleted>0&&location.reload(),t.disabled=!1,t.textContent=n):(alert(`Failed to delete untagged manifests: ${o.error||"Unknown error"}`),t.disabled=!1,t.textContent=n)}catch(r){console.error("Error deleting untagged manifests:",r),alert(`Error: ${r.message}`),t.disabled=!1,t.textContent=n}}function re(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&&W()})});async function ye(e,t){let n=document.getElementById("vuln-detail-modal"),r=document.getElementById("vuln-modal-body");if(!(!n||!r)){r.innerHTML='
',n.showModal();try{let o=await fetch(`/api/vuln-details?digest=${encodeURIComponent(e)}&holdEndpoint=${encodeURIComponent(t)}`);r.innerHTML=await o.text()}catch{r.innerHTML='Failed to load vulnerability details
'}}}var U=class{constructor(t){this.input=t,this.typeahead=t.closest("actor-typeahead"),this.dropdown=null,this.currentFocus=-1,this.typeaheadClosed=!1,this.init()}init(){this.createDropdown(),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.hideDropdown()})}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="recent-accounts-dropdown",this.dropdown.style.display="none",this.typeahead?this.typeahead.insertAdjacentElement("afterend",this.dropdown):this.input.insertAdjacentElement("afterend",this.dropdown)}handleFocus(){this.input.value.trim().length<1&&this.showRecentAccounts()}handleInput(){this.input.value.trim().length>=1&&this.hideDropdown(),this.typeaheadClosed=!1}showRecentAccounts(){let t=this.getRecentAccounts();if(t.length===0){this.hideDropdown();return}this.dropdown.innerHTML="",this.currentFocus=-1;let n=document.createElement("div");n.className="recent-accounts-header",n.textContent="Recent accounts",this.dropdown.appendChild(n),t.forEach((r,o)=>{let i=document.createElement("div");i.className="recent-accounts-item",i.dataset.index=o,i.dataset.handle=r,i.textContent=r,i.addEventListener("click",()=>this.selectItem(r)),this.dropdown.appendChild(i)}),this.dropdown.style.display="block"}selectItem(t){this.input.value=t,this.hideDropdown(),this.input.focus()}hideDropdown(){this.dropdown.style.display="none",this.currentFocus=-1}handleKeydown(t){if(t.key==="Enter"&&this.input.value.trim().length>=2&&(this.typeaheadClosed=!0),t.key==="Tab"&&this.input.value.trim().length>=2&&!this.typeaheadClosed){t.preventDefault();let o=t.shiftKey?"ArrowUp":"ArrowDown",i=new KeyboardEvent("keydown",{key:o,bubbles:!0,cancelable:!0});this.typeahead.dispatchEvent(i);return}if(this.dropdown.style.display==="none")return;let n=this.dropdown.querySelectorAll(".recent-accounts-item");t.key==="ArrowDown"?(t.preventDefault(),this.currentFocus++,this.currentFocus>=n.length&&(this.currentFocus=0),this.updateFocus(n)):t.key==="ArrowUp"?(t.preventDefault(),this.currentFocus--,this.currentFocus<0&&(this.currentFocus=n.length-1),this.updateFocus(n)):t.key==="Enter"&&this.currentFocus>-1&&n[this.currentFocus]?(t.preventDefault(),this.selectItem(n[this.currentFocus].dataset.handle)):t.key==="Escape"&&this.hideDropdown()}updateFocus(t){t.forEach((n,r)=>{n.classList.toggle("focused",r===this.currentFocus)})}getRecentAccounts(){try{let t=localStorage.getItem("atcr_recent_handles");return t?JSON.parse(t):[]}catch{return[]}}saveRecentAccount(t){if(t)try{let n=this.getRecentAccounts();n=n.filter(r=>r!==t),n.unshift(t),n=n.slice(0,5),localStorage.setItem("atcr_recent_handles",JSON.stringify(n))}catch(n){console.error("Failed to save recent account:",n)}}};document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("login-form"),t=document.getElementById("handle");e&&t&&new U(t)});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){try{let n="atcr_recent_handles",r=JSON.parse(localStorage.getItem(n)||"[]");r=r.filter(o=>o!==t),r.unshift(t),r=r.slice(0,5),localStorage.setItem(n,JSON.stringify(r))}catch(n){console.error("Failed to save recent account:",n)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});function Z(){let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),n=document.getElementById("carousel-next");if(!e)return;let r=Array.from(e.querySelectorAll(".carousel-item"));if(r.length===0)return;let o=null,i=5e3,s=0,a=0,l=0;function c(){let b=r[0];if(!b)return;let E=getComputedStyle(e),g=parseFloat(E.gap)||24;s=b.offsetWidth+g,a=e.offsetWidth,l=e.scrollWidth}requestAnimationFrame(()=>{requestAnimationFrame(()=>{c(),m()})});let h;window.addEventListener("resize",()=>{clearTimeout(h),h=setTimeout(c,150)});function u(){return s||c(),s}function f(){return(!a||!s)&&c(),Math.round(a/s)||1}function v(){return(!l||!a)&&c(),l-a}function d(){let b=u(),E=v(),g=e.scrollLeft;g>=E-10?e.scrollTo({left:0,behavior:"smooth"}):e.scrollTo({left:g+b,behavior:"smooth"})}function y(){let b=u(),E=v(),g=e.scrollLeft;g<=10?e.scrollTo({left:E,behavior:"smooth"}):e.scrollTo({left:g-b,behavior:"smooth"})}t&&t.addEventListener("click",()=>{x(),y(),m()}),n&&n.addEventListener("click",()=>{x(),d(),m()});function m(){o||r.length<=f()||(o=setInterval(d,i))}function x(){o&&(clearInterval(o),o=null)}e.addEventListener("mouseenter",x),e.addEventListener("mouseleave",m)}document.addEventListener("DOMContentLoaded",()=>{"requestIdleCallback"in window?requestIdleCallback(Z,{timeout:2e3}):setTimeout(Z,100)});function I(e,t){let n=document.getElementById("toast-container");n||(n=document.createElement("div"),n.id="toast-container",n.className="toast toast-end toast-bottom z-50",document.body.appendChild(n));let r=t==="error"?"alert-error":"alert-success",o=document.createElement("div");o.className=`alert ${r} shadow-lg transition-opacity duration-300`,o.innerHTML=`${e}`,n.appendChild(o),setTimeout(()=>{o.style.opacity="0",setTimeout(()=>o.remove(),300)},3e3)}async function ve(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"')?I("Test webhook delivered successfully!","success"):I("Test delivery failed \u2014 check the webhook URL","error")}catch{I("Failed to reach server","error")}}window.setTheme=te;window.toggleSearch=fe;window.closeSearch=V;window.copyToClipboard=ne;window.toggleOfflineManifests=he;window.deleteManifest=me;window.deleteUntaggedManifests=Ee;window.closeManifestDeleteModal=W;window.openVulnDetails=ye;window.showToast=I;window.testWebhook=ve;window.htmx=q;
diff --git a/pkg/appview/routes/routes.go b/pkg/appview/routes/routes.go
index 51001ea..29782b8 100644
--- a/pkg/appview/routes/routes.go
+++ b/pkg/appview/routes/routes.go
@@ -158,6 +158,7 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) {
r.Get("/settings", (&uihandlers.SettingsHandler{BaseUIHandler: base}).ServeHTTP)
r.Get("/api/storage", (&uihandlers.StorageHandler{BaseUIHandler: base}).ServeHTTP)
r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{BaseUIHandler: base}).ServeHTTP)
+ r.Post("/api/profile/auto-remove-untagged", (&uihandlers.UpdateAutoRemoveUntaggedHandler{BaseUIHandler: base}).ServeHTTP)
// Subscription management
r.Get("/settings/subscription/checkout", (&uihandlers.SubscriptionCheckoutHandler{BaseUIHandler: base}).ServeHTTP)
@@ -165,6 +166,7 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) {
r.Delete("/api/tags", (&uihandlers.DeleteTagHandler{BaseUIHandler: base}).ServeHTTP)
r.Delete("/api/manifests", (&uihandlers.DeleteManifestHandler{BaseUIHandler: base}).ServeHTTP)
+ r.Delete("/api/manifests/untagged", (&uihandlers.DeleteUntaggedManifestsHandler{BaseUIHandler: base}).ServeHTTP)
r.Post("/api/avatar", (&uihandlers.UploadAvatarHandler{BaseUIHandler: base}).ServeHTTP)
// Webhook management
diff --git a/pkg/appview/server.go b/pkg/appview/server.go
index a19df95..137b60d 100644
--- a/pkg/appview/server.go
+++ b/pkg/appview/server.go
@@ -231,9 +231,10 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
// Set global refresher for middleware
middleware.SetGlobalRefresher(s.Refresher)
- // Set global database for hold DID lookups
+ // Set global database for hold DID lookups and manifest reference checks
holdDIDDB := db.NewHoldDIDDB(s.Database)
middleware.SetGlobalDatabase(holdDIDDB)
+ middleware.SetGlobalManifestRefChecker(holdDIDDB)
// Create RemoteHoldAuthorizer for hold authorization with caching
s.HoldAuthorizer = auth.NewRemoteHoldAuthorizer(s.Database, testMode)
diff --git a/pkg/appview/src/js/app.js b/pkg/appview/src/js/app.js
index 8491bda..0406abc 100644
--- a/pkg/appview/src/js/app.js
+++ b/pkg/appview/src/js/app.js
@@ -352,6 +352,45 @@ async function confirmManifestDelete(repository, digest, sanitizedId) {
}
}
+// Delete all untagged manifests in a repository
+async function deleteUntaggedManifests(repository) {
+ const confirmBtn = document.getElementById('confirm-untagged-delete-btn');
+ const originalText = confirmBtn.textContent;
+
+ try {
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = 'Deleting...';
+
+ const response = await fetch('/api/manifests/untagged', {
+ method: 'DELETE',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ repo: repository }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ document.getElementById('untagged-delete-modal').close();
+ showToast(`Deleted ${data.deleted} untagged manifest(s)`, 'success');
+ if (data.deleted > 0) {
+ location.reload();
+ }
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = originalText;
+ } else {
+ alert(`Failed to delete untagged manifests: ${data.error || 'Unknown error'}`);
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = originalText;
+ }
+ } catch (err) {
+ console.error('Error deleting untagged manifests:', err);
+ alert(`Error: ${err.message}`);
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = originalText;
+ }
+}
+
// Remove a manifest element from the DOM
function removeManifestElement(sanitizedId) {
const element = document.getElementById(`manifest-${sanitizedId}`);
@@ -758,6 +797,7 @@ window.closeSearch = closeSearch;
window.copyToClipboard = copyToClipboard;
window.toggleOfflineManifests = toggleOfflineManifests;
window.deleteManifest = deleteManifest;
+window.deleteUntaggedManifests = deleteUntaggedManifests;
window.closeManifestDeleteModal = closeManifestDeleteModal;
window.openVulnDetails = openVulnDetails;
window.showToast = showToast;
diff --git a/pkg/appview/storage/context.go b/pkg/appview/storage/context.go
index 1c7d9fd..ea1d98e 100644
--- a/pkg/appview/storage/context.go
+++ b/pkg/appview/storage/context.go
@@ -15,6 +15,13 @@ type PushWebhookDispatcher interface {
DispatchForPush(ctx context.Context, event PushWebhookEvent)
}
+// ManifestReferenceChecker checks if a manifest digest is referenced as a child
+// of a manifest list (multi-arch image). Used to protect manifest list children
+// from auto-removal when their parent list is still tagged.
+type ManifestReferenceChecker interface {
+ IsManifestReferenced(did, digest string) (bool, error)
+}
+
// PushWebhookEvent contains the data needed to dispatch a push webhook.
type PushWebhookEvent struct {
OwnerDID string
@@ -53,10 +60,14 @@ type RegistryContext struct {
PullerDID string // Puller's DID - who is making the request (from JWT Subject)
PullerPDSEndpoint string // Puller's PDS endpoint URL
+ // Per-request user preferences
+ AutoRemoveUntagged bool // Whether to auto-delete untagged manifests on tag overwrite
+
// Shared services (same for all requests)
- Database HoldDIDLookup // Database for hold DID lookups
- Authorizer auth.HoldAuthorizer // Hold access authorization
- Refresher *oauth.Refresher // OAuth session manager
- ReadmeFetcher *readme.Fetcher // README fetcher for repo pages
- WebhookDispatcher PushWebhookDispatcher // Push webhook dispatcher (nil if not configured)
+ Database HoldDIDLookup // Database for hold DID lookups
+ Authorizer auth.HoldAuthorizer // Hold access authorization
+ Refresher *oauth.Refresher // OAuth session manager
+ ReadmeFetcher *readme.Fetcher // README fetcher for repo pages
+ WebhookDispatcher PushWebhookDispatcher // Push webhook dispatcher (nil if not configured)
+ ManifestRefChecker ManifestReferenceChecker // Checks if digest is a manifest list child (nil-safe)
}
diff --git a/pkg/appview/storage/manifest_store.go b/pkg/appview/storage/manifest_store.go
index 05394d7..73e4829 100644
--- a/pkg/appview/storage/manifest_store.go
+++ b/pkg/appview/storage/manifest_store.go
@@ -210,11 +210,25 @@ func (s *ManifestStore) Put(ctx context.Context, manifest distribution.Manifest,
// Also handle tag if specified
var tag string
+ var oldDigest string // Track previous digest for auto-remove-untagged cleanup
for _, option := range options {
if tagOpt, ok := option.(distribution.WithTagOption); ok {
tag = tagOpt.Tag
- tagRecord := atproto.NewTagRecord(s.ctx.ATProtoClient.DID(), s.ctx.Repository, tag, dgst.String())
tagRKey := atproto.RepositoryTagToRKey(s.ctx.Repository, tag)
+
+ // Before overwriting, capture the old digest so we can clean it up
+ if s.ctx.AutoRemoveUntagged {
+ if oldRec, getErr := s.ctx.ATProtoClient.GetRecord(ctx, atproto.TagCollection, tagRKey); getErr == nil {
+ var oldTag atproto.TagRecord
+ if json.Unmarshal(oldRec.Value, &oldTag) == nil {
+ if d, dErr := oldTag.GetManifestDigest(); dErr == nil {
+ oldDigest = d
+ }
+ }
+ }
+ }
+
+ tagRecord := atproto.NewTagRecord(s.ctx.ATProtoClient.DID(), s.ctx.Repository, tag, dgst.String())
_, err = s.ctx.ATProtoClient.PutRecord(ctx, atproto.TagCollection, tagRKey, tagRecord)
if err != nil {
return "", fmt.Errorf("failed to store tag in ATProto: %w", err)
@@ -222,6 +236,12 @@ func (s *ManifestStore) Put(ctx context.Context, manifest distribution.Manifest,
}
}
+ // Auto-remove untagged manifests: if the tag pointed to a different digest before,
+ // check if the old manifest is now untagged and clean it up
+ if s.ctx.AutoRemoveUntagged && oldDigest != "" && oldDigest != dgst.String() {
+ go s.cleanupUntaggedManifest(oldDigest)
+ }
+
// Notify hold about manifest push (for layer tracking, Bluesky posts, and stats)
// Do this asynchronously to avoid blocking the push
// Note: We notify even for tagless pushes (e.g., buildx platform manifests) to create layer records
@@ -697,3 +717,73 @@ func detectImageMimeType(contentType, url string) string {
// Unknown or unsupported type - reject
return ""
}
+
+// cleanupUntaggedManifest checks if a manifest digest is now untagged and deletes it.
+// Runs asynchronously after a tag overwrite. Protects manifest list children.
+func (s *ManifestStore) cleanupUntaggedManifest(oldDigest string) {
+ defer func() {
+ if r := recover(); r != nil {
+ slog.Error("Panic in cleanupUntaggedManifest", "panic", r)
+ }
+ }()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ // Re-check: list all tags for this repository and see if any still point to the old digest
+ records, err := s.ctx.ATProtoClient.ListRecords(ctx, atproto.TagCollection, 100)
+ if err != nil {
+ slog.Warn("Auto-remove: failed to list tags", "error", err)
+ return
+ }
+
+ for _, rec := range records {
+ var tagRecord atproto.TagRecord
+ if json.Unmarshal(rec.Value, &tagRecord) != nil {
+ continue
+ }
+ if tagRecord.Repository != s.ctx.Repository {
+ continue
+ }
+ d, dErr := tagRecord.GetManifestDigest()
+ if dErr == nil && d == oldDigest {
+ // Still tagged by another tag — do not delete
+ slog.Debug("Auto-remove: manifest still tagged, skipping",
+ "digest", oldDigest, "tag", tagRecord.Tag)
+ return
+ }
+ }
+
+ // Check if this digest is a child of a manifest list (multi-arch)
+ if s.ctx.ManifestRefChecker != nil {
+ referenced, err := s.ctx.ManifestRefChecker.IsManifestReferenced(s.ctx.DID, oldDigest)
+ if err != nil {
+ slog.Warn("Auto-remove: failed to check manifest references", "digest", oldDigest, "error", err)
+ return
+ }
+ if referenced {
+ slog.Debug("Auto-remove: manifest is a manifest list child, skipping",
+ "digest", oldDigest)
+ return
+ }
+ }
+
+ // Safe to delete — manifest is untagged and not referenced by any manifest list
+ dgst, err := digest.Parse(oldDigest)
+ if err != nil {
+ slog.Warn("Auto-remove: invalid digest", "digest", oldDigest, "error", err)
+ return
+ }
+
+ rkey := digestToRKey(dgst)
+ if err := s.ctx.ATProtoClient.DeleteRecord(ctx, atproto.ManifestCollection, rkey); err != nil {
+ slog.Warn("Auto-remove: failed to delete untagged manifest", "digest", oldDigest, "error", err)
+ return
+ }
+
+ slog.Info("Auto-removed untagged manifest",
+ "component", "manifest-store",
+ "digest", oldDigest,
+ "repository", s.ctx.Repository,
+ "did", s.ctx.DID)
+}
diff --git a/pkg/appview/templates/pages/repository.html b/pkg/appview/templates/pages/repository.html
index b6e0439..fb32ecc 100644
--- a/pkg/appview/templates/pages/repository.html
+++ b/pkg/appview/templates/pages/repository.html
@@ -200,10 +200,19 @@
{{ if .Manifests }}
@@ -313,6 +322,23 @@
+
+
+
diff --git a/pkg/atproto/lexicon.go b/pkg/atproto/lexicon.go
index 67b538d..9ff8059 100644
--- a/pkg/atproto/lexicon.go
+++ b/pkg/atproto/lexicon.go
@@ -335,6 +335,11 @@ type SailorProfileRecord struct {
// If null/empty, user has opted out of defaults
DefaultHold string `json:"defaultHold,omitempty"`
+ // AutoRemoveUntagged controls whether untagged manifests are automatically
+ // cleaned up. When true, manifests that lose all tags (e.g., after a tag
+ // overwrite) are deleted from PDS, and their layers are cleaned up by hold GC.
+ AutoRemoveUntagged bool `json:"autoRemoveUntagged,omitempty"`
+
// CreatedAt timestamp
CreatedAt time.Time `json:"createdAt"`
diff --git a/pkg/atproto/relays.go b/pkg/atproto/relays.go
index 6755fb4..92c9f53 100644
--- a/pkg/atproto/relays.go
+++ b/pkg/atproto/relays.go
@@ -35,6 +35,7 @@ var KnownRelays = []KnownRelay{
{Name: "Feeds Blue", URL: "https://relay.feeds.blue"},
{Name: "Waow", URL: "https://relay.waow.tech"},
{Name: "Zlay", URL: "https://zlay.waow.tech"},
+ {Name: "Bassh", URL: "https://relay.bas.sh"},
}
// RelayHTTPError indicates the relay responded with a non-200 status code.
diff --git a/pkg/hold/gc/gc.go b/pkg/hold/gc/gc.go
index af494ba..dbdd72d 100644
--- a/pkg/hold/gc/gc.go
+++ b/pkg/hold/gc/gc.go
@@ -578,13 +578,38 @@ func (gc *GarbageCollector) analyzeRecords(ctx context.Context) (*analysisResult
result.usersChecked++
gc.setProgress("manifests", fmt.Sprintf("Fetching manifests (%d/%d users)", result.usersChecked, totalUsers), gc.operationType)
- manifests, err := gc.fetchUserManifests(ctx, did)
+ // Resolve PDS endpoint (needed for both manifests and optional profile/tag fetches)
+ pdsEndpoint, err := atproto.ResolveDIDToPDS(ctx, did)
+ if err != nil {
+ gc.logger.Warn("Failed to resolve PDS for user, treating their records as referenced",
+ "did", did, "error", err)
+ continue
+ }
+
+ manifests, err := gc.fetchUserManifestsFromEndpoint(ctx, did, pdsEndpoint, gc.pds.DID())
if err != nil {
gc.logger.Warn("Failed to fetch manifests for user, treating their records as referenced",
"did", did, "error", err)
continue
}
+ // If user opted into auto-remove-untagged, filter out untagged manifests
+ // so their layers are treated as orphaned and cleaned up
+ if profile, _ := gc.fetchUserProfile(ctx, pdsEndpoint, did); profile != nil && profile.AutoRemoveUntagged {
+ taggedDigests, tagErr := gc.fetchUserTags(ctx, pdsEndpoint, did)
+ if tagErr != nil {
+ gc.logger.Warn("Failed to fetch tags for auto-remove, keeping all manifests referenced",
+ "did", did, "error", tagErr)
+ } else {
+ before := len(manifests)
+ manifests = gc.filterUntaggedManifests(manifests, taggedDigests)
+ if filtered := before - len(manifests); filtered > 0 {
+ gc.logger.Info("Filtered untagged manifests for auto-remove user",
+ "did", did, "filtered", filtered, "remaining", len(manifests))
+ }
+ }
+ }
+
fetchedUsers[did] = true
for _, m := range manifests {
result.manifestsChecked++
@@ -835,18 +860,153 @@ func (gc *GarbageCollector) discoverUserDIDs(ctx context.Context) ([]string, err
return dids, nil
}
-// fetchUserManifests fetches all io.atcr.manifest records for a user from their PDS,
-// filtered to manifests that reference this hold.
-func (gc *GarbageCollector) fetchUserManifests(ctx context.Context, userDID string) ([]*manifestInfo, error) {
- pdsEndpoint, err := atproto.ResolveDIDToPDS(ctx, userDID)
+// fetchUserProfile fetches the sailor profile from a user's PDS.
+// Returns nil without error if the profile doesn't exist.
+func (gc *GarbageCollector) fetchUserProfile(ctx context.Context, pdsEndpoint, userDID string) (*atproto.SailorProfileRecord, error) {
+ client := &http.Client{Timeout: 10 * time.Second}
+
+ reqURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=self",
+ pdsEndpoint, userDID, atproto.SailorProfileCollection)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
- return nil, fmt.Errorf("resolve PDS for %s: %w", userDID, err)
+ return nil, fmt.Errorf("create request: %w", err)
}
- return gc.fetchUserManifestsFromEndpoint(ctx, userDID, pdsEndpoint, gc.pds.DID())
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("http request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound {
+ return nil, nil // No profile
+ }
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("getRecord returned status %d", resp.StatusCode)
+ }
+
+ var envelope struct {
+ Value json.RawMessage `json:"value"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+
+ var profile atproto.SailorProfileRecord
+ if err := json.Unmarshal(envelope.Value, &profile); err != nil {
+ return nil, fmt.Errorf("unmarshal profile: %w", err)
+ }
+
+ return &profile, nil
+}
+
+// fetchUserTags fetches all tag records from a user's PDS.
+// Returns a map of digest → true for all tagged manifest digests.
+func (gc *GarbageCollector) fetchUserTags(ctx context.Context, pdsEndpoint, userDID string) (map[string]bool, error) {
+ tagged := make(map[string]bool)
+ cursor := ""
+ client := &http.Client{Timeout: 30 * time.Second}
+
+ for {
+ reqURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s&limit=100",
+ pdsEndpoint, userDID, atproto.TagCollection)
+ if cursor != "" {
+ reqURL += "&cursor=" + cursor
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("create request: %w", err)
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("http request: %w", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ resp.Body.Close()
+ return nil, fmt.Errorf("listRecords returned status %d", resp.StatusCode)
+ }
+
+ var listResult struct {
+ Records []struct {
+ Value json.RawMessage `json:"value"`
+ } `json:"records"`
+ Cursor string `json:"cursor,omitempty"`
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&listResult); err != nil {
+ resp.Body.Close()
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+ resp.Body.Close()
+
+ for _, rec := range listResult.Records {
+ var tag atproto.TagRecord
+ if err := json.Unmarshal(rec.Value, &tag); err != nil {
+ continue
+ }
+ if d, err := tag.GetManifestDigest(); err == nil {
+ tagged[d] = true
+ }
+ }
+
+ if listResult.Cursor == "" {
+ break
+ }
+ cursor = listResult.Cursor
+ }
+
+ return tagged, nil
+}
+
+// filterUntaggedManifests removes untagged manifests from the list, preserving
+// manifest list children that are referenced by tagged manifest lists.
+func (gc *GarbageCollector) filterUntaggedManifests(manifests []*manifestInfo, taggedDigests map[string]bool) []*manifestInfo {
+ // Build a set of digests that are children of tagged manifest lists
+ childDigests := make(map[string]bool)
+ for _, m := range manifests {
+ // Check if this manifest is a manifest list AND is tagged
+ mDigest := extractDigestFromManifestURI(m.URI)
+ if mDigest == "" || !taggedDigests[mDigest] {
+ continue
+ }
+ // This is a tagged manifest list — protect its children
+ for _, ref := range m.Record.Manifests {
+ childDigests[ref.Digest] = true
+ }
+ }
+
+ // Filter: keep manifests that are tagged, or are children of tagged manifest lists
+ var kept []*manifestInfo
+ for _, m := range manifests {
+ mDigest := extractDigestFromManifestURI(m.URI)
+ if mDigest == "" {
+ kept = append(kept, m)
+ continue
+ }
+ if taggedDigests[mDigest] || childDigests[mDigest] {
+ kept = append(kept, m)
+ }
+ }
+ return kept
+}
+
+// extractDigestFromManifestURI extracts the digest from a manifest AT-URI.
+// URI format: at://did:plc:xxx/io.atcr.manifest/{encoded_digest}
+// Returns the full digest (e.g., "sha256:abc123...") or empty string.
+func extractDigestFromManifestURI(uri string) string {
+ parts := parseATURI(uri)
+ if parts == nil || parts.Collection != atproto.ManifestCollection {
+ return ""
+ }
+ // The rkey is the digest without the "sha256:" prefix
+ return "sha256:" + parts.Rkey
}
// fetchUserManifestsFromEndpoint fetches manifests from a specific PDS endpoint.
-// Separated from fetchUserManifests for testability (avoids DID resolution).
func (gc *GarbageCollector) fetchUserManifestsFromEndpoint(ctx context.Context, userDID, pdsEndpoint, holdDID string) ([]*manifestInfo, error) {
var manifests []*manifestInfo
cursor := ""
diff --git a/pkg/hold/gc/gc_test.go b/pkg/hold/gc/gc_test.go
index db91a8c..6503e4e 100644
--- a/pkg/hold/gc/gc_test.go
+++ b/pkg/hold/gc/gc_test.go
@@ -451,6 +451,306 @@ func TestFetchUserManifests_PDSError(t *testing.T) {
}
}
+func TestExtractDigestFromManifestURI(t *testing.T) {
+ tests := []struct {
+ name string
+ uri string
+ expected string
+ }{
+ {
+ name: "valid manifest URI",
+ uri: "at://did:plc:user1/io.atcr.manifest/abc123def456",
+ expected: "sha256:abc123def456",
+ },
+ {
+ name: "wrong collection",
+ uri: "at://did:plc:user1/io.atcr.tag/abc123def456",
+ expected: "",
+ },
+ {
+ name: "invalid URI",
+ uri: "not-an-at-uri",
+ expected: "",
+ },
+ {
+ name: "empty URI",
+ uri: "",
+ expected: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := extractDigestFromManifestURI(tt.uri)
+ if got != tt.expected {
+ t.Errorf("extractDigestFromManifestURI(%q) = %q, want %q", tt.uri, got, tt.expected)
+ }
+ })
+ }
+}
+
+func TestFilterUntaggedManifests(t *testing.T) {
+ gc := &GarbageCollector{logger: newTestLogger()}
+
+ t.Run("keeps tagged manifests, removes untagged", func(t *testing.T) {
+ manifests := []*manifestInfo{
+ {URI: "at://did:plc:u1/io.atcr.manifest/aaa111", Record: &atproto.ManifestRecord{}},
+ {URI: "at://did:plc:u1/io.atcr.manifest/bbb222", Record: &atproto.ManifestRecord{}},
+ {URI: "at://did:plc:u1/io.atcr.manifest/ccc333", Record: &atproto.ManifestRecord{}},
+ }
+ tagged := map[string]bool{
+ "sha256:aaa111": true,
+ "sha256:ccc333": true,
+ }
+
+ result := gc.filterUntaggedManifests(manifests, tagged)
+ if len(result) != 2 {
+ t.Fatalf("expected 2 kept, got %d", len(result))
+ }
+ if result[0].URI != manifests[0].URI || result[1].URI != manifests[2].URI {
+ t.Errorf("unexpected manifests kept: %v, %v", result[0].URI, result[1].URI)
+ }
+ })
+
+ t.Run("preserves manifest list children", func(t *testing.T) {
+ // Manifest list (tagged) references child (untagged)
+ manifests := []*manifestInfo{
+ {
+ URI: "at://did:plc:u1/io.atcr.manifest/index111",
+ Record: &atproto.ManifestRecord{
+ Manifests: []atproto.ManifestReference{
+ {Digest: "sha256:child222", MediaType: "application/vnd.oci.image.manifest.v1+json"},
+ {Digest: "sha256:child333", MediaType: "application/vnd.oci.image.manifest.v1+json"},
+ },
+ },
+ },
+ {URI: "at://did:plc:u1/io.atcr.manifest/child222", Record: &atproto.ManifestRecord{}},
+ {URI: "at://did:plc:u1/io.atcr.manifest/child333", Record: &atproto.ManifestRecord{}},
+ {URI: "at://did:plc:u1/io.atcr.manifest/orphan444", Record: &atproto.ManifestRecord{}},
+ }
+ tagged := map[string]bool{
+ "sha256:index111": true, // Only the index is tagged
+ }
+
+ result := gc.filterUntaggedManifests(manifests, tagged)
+ if len(result) != 3 {
+ t.Fatalf("expected 3 kept (index + 2 children), got %d", len(result))
+ }
+ // orphan444 should be filtered out
+ for _, m := range result {
+ if extractDigestFromManifestURI(m.URI) == "sha256:orphan444" {
+ t.Error("orphan manifest should have been filtered out")
+ }
+ }
+ })
+
+ t.Run("removes everything when nothing is tagged", func(t *testing.T) {
+ manifests := []*manifestInfo{
+ {URI: "at://did:plc:u1/io.atcr.manifest/aaa111", Record: &atproto.ManifestRecord{}},
+ {URI: "at://did:plc:u1/io.atcr.manifest/bbb222", Record: &atproto.ManifestRecord{}},
+ }
+ tagged := map[string]bool{}
+
+ result := gc.filterUntaggedManifests(manifests, tagged)
+ if len(result) != 0 {
+ t.Fatalf("expected 0 kept, got %d", len(result))
+ }
+ })
+
+ t.Run("keeps everything when all are tagged", func(t *testing.T) {
+ manifests := []*manifestInfo{
+ {URI: "at://did:plc:u1/io.atcr.manifest/aaa111", Record: &atproto.ManifestRecord{}},
+ {URI: "at://did:plc:u1/io.atcr.manifest/bbb222", Record: &atproto.ManifestRecord{}},
+ }
+ tagged := map[string]bool{
+ "sha256:aaa111": true,
+ "sha256:bbb222": true,
+ }
+
+ result := gc.filterUntaggedManifests(manifests, tagged)
+ if len(result) != 2 {
+ t.Fatalf("expected 2 kept, got %d", len(result))
+ }
+ })
+}
+
+func TestFetchUserProfile(t *testing.T) {
+ gc := &GarbageCollector{logger: newTestLogger()}
+
+ t.Run("returns profile with autoRemoveUntagged", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "value": map[string]any{
+ "$type": "io.atcr.sailor.profile",
+ "defaultHold": "did:web:hold.example.com",
+ "autoRemoveUntagged": true,
+ "createdAt": "2025-01-01T00:00:00Z",
+ },
+ })
+ }))
+ defer server.Close()
+
+ profile, err := gc.fetchUserProfile(t.Context(), server.URL, "did:plc:test")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if profile == nil {
+ t.Fatal("expected non-nil profile")
+ }
+ if !profile.AutoRemoveUntagged {
+ t.Error("expected AutoRemoveUntagged to be true")
+ }
+ if profile.DefaultHold != "did:web:hold.example.com" {
+ t.Errorf("expected DefaultHold %q, got %q", "did:web:hold.example.com", profile.DefaultHold)
+ }
+ })
+
+ t.Run("returns nil for 404", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ profile, err := gc.fetchUserProfile(t.Context(), server.URL, "did:plc:test")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if profile != nil {
+ t.Error("expected nil profile for 404")
+ }
+ })
+
+ t.Run("returns error for server failure", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ defer server.Close()
+
+ _, err := gc.fetchUserProfile(t.Context(), server.URL, "did:plc:test")
+ if err == nil {
+ t.Fatal("expected error for 500 response")
+ }
+ })
+}
+
+func TestFetchUserTags(t *testing.T) {
+ gc := &GarbageCollector{logger: newTestLogger()}
+
+ t.Run("returns tagged digests", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "records": []map[string]any{
+ {
+ "uri": "at://did:plc:user1/io.atcr.tag/myapp_latest",
+ "cid": "bafyrei1",
+ "value": map[string]any{
+ "$type": "io.atcr.tag",
+ "repository": "myapp",
+ "tag": "latest",
+ "manifest": "at://did:plc:user1/io.atcr.manifest/abc123",
+ },
+ },
+ {
+ "uri": "at://did:plc:user1/io.atcr.tag/myapp_v1",
+ "cid": "bafyrei2",
+ "value": map[string]any{
+ "$type": "io.atcr.tag",
+ "repository": "myapp",
+ "tag": "v1",
+ "manifestDigest": "sha256:def456",
+ },
+ },
+ },
+ })
+ }))
+ defer server.Close()
+
+ tagged, err := gc.fetchUserTags(t.Context(), server.URL, "did:plc:user1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tagged) != 2 {
+ t.Fatalf("expected 2 tagged digests, got %d", len(tagged))
+ }
+ if !tagged["sha256:abc123"] {
+ t.Error("expected sha256:abc123 to be tagged")
+ }
+ if !tagged["sha256:def456"] {
+ t.Error("expected sha256:def456 to be tagged")
+ }
+ })
+
+ t.Run("handles pagination", func(t *testing.T) {
+ callCount := 0
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ callCount++
+ w.Header().Set("Content-Type", "application/json")
+ if callCount == 1 {
+ json.NewEncoder(w).Encode(map[string]any{
+ "records": []map[string]any{
+ {
+ "uri": "at://did:plc:user1/io.atcr.tag/myapp_latest",
+ "cid": "bafyrei1",
+ "value": map[string]any{
+ "$type": "io.atcr.tag",
+ "repository": "myapp",
+ "tag": "latest",
+ "manifestDigest": "sha256:aaa111",
+ },
+ },
+ },
+ "cursor": "page2",
+ })
+ } else {
+ json.NewEncoder(w).Encode(map[string]any{
+ "records": []map[string]any{
+ {
+ "uri": "at://did:plc:user1/io.atcr.tag/myapp_v2",
+ "cid": "bafyrei2",
+ "value": map[string]any{
+ "$type": "io.atcr.tag",
+ "repository": "myapp",
+ "tag": "v2",
+ "manifestDigest": "sha256:bbb222",
+ },
+ },
+ },
+ })
+ }
+ }))
+ defer server.Close()
+
+ tagged, err := gc.fetchUserTags(t.Context(), server.URL, "did:plc:user1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if callCount != 2 {
+ t.Errorf("expected 2 HTTP calls for pagination, got %d", callCount)
+ }
+ if len(tagged) != 2 {
+ t.Fatalf("expected 2 tagged digests, got %d", len(tagged))
+ }
+ })
+
+ t.Run("returns empty for no tags", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{"records": []any{}})
+ }))
+ defer server.Close()
+
+ tagged, err := gc.fetchUserTags(t.Context(), server.URL, "did:plc:user1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tagged) != 0 {
+ t.Errorf("expected 0 tagged digests, got %d", len(tagged))
+ }
+ })
+}
+
func newTestLogger() *slog.Logger {
return slog.Default().With("component", "gc-test")
}
diff --git a/themes/seamark/embed.go b/themes/seamark/embed.go
index 14a454f..4114cf3 100644
--- a/themes/seamark/embed.go
+++ b/themes/seamark/embed.go
@@ -13,6 +13,9 @@ import (
//go:embed public
var publicFS embed.FS
+//go:embed templates
+var templatesFS embed.FS
+
//go:embed theme.css
var themeCSS string
@@ -29,8 +32,9 @@ func branding() *appview.BrandingOverrides {
}
return &appview.BrandingOverrides{
- PublicFS: prefixFS{sub: pubSub},
- ExtraCSS: themeCSS,
+ PublicFS: prefixFS{sub: pubSub},
+ TemplatesFS: templatesFS,
+ ExtraCSS: themeCSS,
}
}
diff --git a/themes/seamark/public/apple-touch-icon.png b/themes/seamark/public/apple-touch-icon.png
index 53ec455..1b0909a 100644
Binary files a/themes/seamark/public/apple-touch-icon.png and b/themes/seamark/public/apple-touch-icon.png differ
diff --git a/themes/seamark/public/favicon-96x96.png b/themes/seamark/public/favicon-96x96.png
index 3650661..efcfb25 100644
Binary files a/themes/seamark/public/favicon-96x96.png and b/themes/seamark/public/favicon-96x96.png differ
diff --git a/themes/seamark/public/favicon.ico b/themes/seamark/public/favicon.ico
index 4c69326..1ee027e 100644
Binary files a/themes/seamark/public/favicon.ico and b/themes/seamark/public/favicon.ico differ
diff --git a/themes/seamark/public/favicon.svg b/themes/seamark/public/favicon.svg
index c4d9e17..2900bc0 100644
--- a/themes/seamark/public/favicon.svg
+++ b/themes/seamark/public/favicon.svg
@@ -1,51 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/themes/seamark/public/static/seamark_seagull.svg b/themes/seamark/public/static/seamark_seagull.svg
new file mode 100644
index 0000000..7390a4b
--- /dev/null
+++ b/themes/seamark/public/static/seamark_seagull.svg
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/themes/seamark/public/static/seamark_seagull_buoy.png b/themes/seamark/public/static/seamark_seagull_buoy.png
new file mode 100644
index 0000000..262bc86
Binary files /dev/null and b/themes/seamark/public/static/seamark_seagull_buoy.png differ
diff --git a/themes/seamark/public/web-app-manifest-192x192.png b/themes/seamark/public/web-app-manifest-192x192.png
index 6fd4f4d..2a213ba 100644
Binary files a/themes/seamark/public/web-app-manifest-192x192.png and b/themes/seamark/public/web-app-manifest-192x192.png differ
diff --git a/themes/seamark/public/web-app-manifest-512x512.png b/themes/seamark/public/web-app-manifest-512x512.png
index 25350ec..8a95be9 100644
Binary files a/themes/seamark/public/web-app-manifest-512x512.png and b/themes/seamark/public/web-app-manifest-512x512.png differ
diff --git a/themes/seamark/templates/components/hero.html b/themes/seamark/templates/components/hero.html
new file mode 100644
index 0000000..5423b49
--- /dev/null
+++ b/themes/seamark/templates/components/hero.html
@@ -0,0 +1,66 @@
+{{ define "hero" }}
+{{/*
+ Hero section component - displays landing page hero for non-authenticated users
+*/}}
+
+
+
+
your beacon at sea.
+
+
+ Push and pull Docker images on the AT Protocol.
+ Browse public registries or control your data.
+
+
+
+
docker login {{ .RegistryURL }}
+
docker push {{ .RegistryURL }}/you/app
+
same docker, decentralized
+
+
+
+
+
+
+
+
+ {{ icon "ship" "size-8" }}
+
+
Works with Docker
+
Use docker push & pull. No new tools to learn.
+
+
+
+ {{ icon "anchor" "size-8" }}
+
+
Your Data
+
Join shared holds or captain your own storage.
+
+
+
+ {{ icon "compass" "size-8" }}
+
+
Discover Images
+
Browse and star public container registries.
+
+
+
+
+
+{{ end }}
diff --git a/themes/seamark/theme.css b/themes/seamark/theme.css
index ca0cd67..e4dd52a 100644
--- a/themes/seamark/theme.css
+++ b/themes/seamark/theme.css
@@ -12,3 +12,13 @@
--color-accent: oklch(66.6% 0.121 28);
--color-accent-content: oklch(28% 0.121 28);
}
+
+@keyframes rock {
+ 0%, 100% { transform: translateY(-50%) rotate(-3deg); }
+ 50% { transform: translateY(-50%) rotate(3deg); }
+}
+
+.animate-rock {
+ animation: rock 3s ease-in-out infinite;
+ transform-origin: bottom center;
+}