diff --git a/pkg/appview/public/js/bundle.min.js b/pkg/appview/public/js/bundle.min.js index 41774ef..7e39511 100644 --- a/pkg/appview/public/js/bundle.min.js +++ b/pkg/appview/public/js/bundle.min.js @@ -1,4 +1,4 @@ -var se=(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;nFailed to load vulnerability details
'}}}var V=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 V(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 Q(){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(Q,{timeout:2e3}):setTimeout(Q,100)});window.setTheme=ee;window.toggleSearch=ue;window.closeSearch=B;window.copyToClipboard=te;window.toggleOfflineManifests=de;window.deleteManifest=he;window.closeManifestDeleteModal=j;window.openVulnDetails=pe;window.htmx=F; +`;function Y(e){return e.cloneNode(!0)}var M=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(Y(K).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=Y(G).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 Z(){return localStorage.getItem("theme")||"system"}function ae(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function U(){let e=Z(),n=ae(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),le(e)}function ee(e){localStorage.setItem("theme",e),U(),ce()}function le(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 ce(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{Z()==="system"&&U()});function ue(){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 B(){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")&&B(),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)&&B()}))});function te(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){te(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 fe(e){let t=Math.floor((new Date-new Date(e))/1e3),n={year:31536e3,month:2592e3,week:604800,day:86400,hour:3600,minute:60,second:1};for(let[r,o]of Object.entries(n)){let i=Math.floor(t/o);if(i>=1)return i===1?`1 ${r} ago`:`${i} ${r}s ago`}return"just now"}function _(){document.querySelectorAll("time[datetime]").forEach(e=>{let t=e.getAttribute("datetime");if(t&&!e.dataset.noUpdate){let n=fe(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{_(),U(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{ee(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",_);setInterval(_,6e4);function de(){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 he(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();me(e,t,n,o.tags)}else if(r.ok)ne(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 me(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=()=>ge(e,t,n),o.style.display="flex"}function j(){let e=document.getElementById("manifest-delete-modal");e.style.display="none"}async function ge(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)j(),ne(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 ne(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&&j()})});async function pe(e,t){let n=document.getElementById("vuln-detail-modal"),r=document.getElementById("vuln-modal-body");if(!(!n||!r)){r.innerHTML='Failed to load vulnerability details
'}}}var V=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 V(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 Q(){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(Q,{timeout:2e3}):setTimeout(Q,100)});window.setTheme=ee;window.toggleSearch=ue;window.closeSearch=B;window.copyToClipboard=te;window.toggleOfflineManifests=de;window.deleteManifest=he;window.closeManifestDeleteModal=j;window.openVulnDetails=pe;window.htmx=F; diff --git a/pkg/appview/src/js/app.js b/pkg/appview/src/js/app.js index 0890191..78978cd 100644 --- a/pkg/appview/src/js/app.js +++ b/pkg/appview/src/js/app.js @@ -85,11 +85,20 @@ document.addEventListener('DOMContentLoaded', () => { if (!wrapper || !input) return; - // Close on Escape key + // Close on Escape key, open on "/" key (GitHub-style) document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && wrapper.classList.contains('expanded')) { closeSearch(); } + + if (e.key === '/' && !wrapper.classList.contains('expanded')) { + const tag = e.target.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return; + + e.preventDefault(); + wrapper.classList.add('expanded'); + input.focus(); + } }); // Close on click outside diff --git a/pkg/hold/admin/admin.go b/pkg/hold/admin/admin.go index 170d0c1..46e30ec 100644 --- a/pkg/hold/admin/admin.go +++ b/pkg/hold/admin/admin.go @@ -411,12 +411,13 @@ func (ui *AdminUI) RegisterRoutes(r chi.Router) { r.Post("/admin/relays/crawl", ui.handleRelayCrawl) r.Post("/admin/relays/crawl-all", ui.handleRelayCrawlAll) - // GC POSTs + // GC (background operations + polling status) r.Post("/admin/api/gc/preview", ui.handleGCPreview) r.Post("/admin/api/gc/run", ui.handleGCRun) r.Post("/admin/api/gc/reconcile", ui.handleGCReconcile) r.Post("/admin/api/gc/delete-records", ui.handleGCDeleteRecords) r.Post("/admin/api/gc/delete-blobs", ui.handleGCDeleteBlobs) + r.Get("/admin/api/gc/status", ui.handleGCStatus) // API endpoints (for HTMX) r.Get("/admin/api/stats", ui.handleStatsAPI) diff --git a/pkg/hold/admin/handlers_gc.go b/pkg/hold/admin/handlers_gc.go index 00dc231..769a01e 100644 --- a/pkg/hold/admin/handlers_gc.go +++ b/pkg/hold/admin/handlers_gc.go @@ -21,6 +21,13 @@ type gcTabData struct { LastResultAge string } +// gcProgressData is the data passed to the gc_progress.html partial +type gcProgressData struct { + Phase string + Message string + OpType string +} + // handleGCTab returns the storage/GC tab content (HTMX partial) func (ui *AdminUI) handleGCTab(w http.ResponseWriter, r *http.Request) { if ui.gc == nil { @@ -44,136 +51,171 @@ func (ui *AdminUI) handleGCTab(w http.ResponseWriter, r *http.Request) { ui.renderTemplate(w, "partials/tab_storage.html", data) } -// handleGCPreview runs a GC preview (analysis only, no mutations) +// handleGCPreview starts a GC preview in the background func (ui *AdminUI) handleGCPreview(w http.ResponseWriter, r *http.Request) { if ui.gc == nil { ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) return } - preview, err := ui.gc.Preview(r.Context()) - if err != nil { - slog.Error("GC preview failed", "error", err) - ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{err.Error()}) - return + session := getSessionFromContext(r.Context()) + + if ui.gc.StartPreview() { + slog.Info("GC preview started via admin panel", "by", session.DID) } - session := getSessionFromContext(r.Context()) - slog.Info("GC preview completed via admin panel", - "orphanedRecords", len(preview.OrphanedRecords), - "orphanedBlobs", len(preview.OrphanedBlobs), - "missingRecords", len(preview.MissingRecords), - "duration", preview.Duration, - "by", session.DID) - - ui.renderTemplate(w, "partials/gc_preview.html", struct { - Preview *gc.GCPreview - }{Preview: preview}) + // Whether we just started or it was already running, show progress + progress := ui.gc.GetProgress() + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ + Phase: progress.Phase, + Message: progress.Message, + OpType: progress.OperationType, + }) } -// handleGCRun executes an actual GC run +// handleGCRun starts a full GC run in the background func (ui *AdminUI) handleGCRun(w http.ResponseWriter, r *http.Request) { if ui.gc == nil { ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) return } - result, err := ui.gc.Run(r.Context()) - if err != nil { - slog.Error("GC run failed", "error", err) - ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{err.Error()}) - return + session := getSessionFromContext(r.Context()) + + if ui.gc.StartRun() { + slog.Info("GC run started via admin panel", "by", session.DID) } - session := getSessionFromContext(r.Context()) - slog.Info("GC run completed via admin panel", - "blobsDeleted", result.BlobsDeleted, - "bytesReclaimed", result.BytesReclaimed, - "recordsDeleted", result.RecordsDeleted, - "recordsReconciled", result.RecordsReconciled, - "duration", result.Duration, - "by", session.DID) - - ui.renderTemplate(w, "partials/gc_result.html", struct { - Result *gc.GCResult - }{Result: result}) + progress := ui.gc.GetProgress() + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ + Phase: progress.Phase, + Message: progress.Message, + OpType: progress.OperationType, + }) } -// handleGCReconcile creates missing layer records without deleting anything +// handleGCReconcile starts record reconciliation in the background func (ui *AdminUI) handleGCReconcile(w http.ResponseWriter, r *http.Request) { if ui.gc == nil { ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) return } - result, err := ui.gc.Reconcile(r.Context()) - if err != nil { - slog.Error("GC reconcile failed", "error", err) - ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{err.Error()}) - return + session := getSessionFromContext(r.Context()) + + if ui.gc.StartReconcile() { + slog.Info("GC reconcile started via admin panel", "by", session.DID) } - session := getSessionFromContext(r.Context()) - slog.Info("GC reconcile completed via admin panel", - "recordsReconciled", result.RecordsReconciled, - "duration", result.Duration, - "by", session.DID) - - ui.renderTemplate(w, "partials/gc_result.html", struct { - Result *gc.GCResult - }{Result: result}) + progress := ui.gc.GetProgress() + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ + Phase: progress.Phase, + Message: progress.Message, + OpType: progress.OperationType, + }) } -// handleGCDeleteRecords deletes orphaned layer records (no blob deletion) +// handleGCDeleteRecords starts orphaned record deletion in the background func (ui *AdminUI) handleGCDeleteRecords(w http.ResponseWriter, r *http.Request) { if ui.gc == nil { ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) return } - result, err := ui.gc.DeleteOrphanedRecords(r.Context()) - if err != nil { - slog.Error("GC delete records failed", "error", err) - ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{err.Error()}) - return + session := getSessionFromContext(r.Context()) + + if ui.gc.StartDeleteRecords() { + slog.Info("GC delete records started via admin panel", "by", session.DID) } - session := getSessionFromContext(r.Context()) - slog.Info("GC delete orphaned records completed via admin panel", - "recordsDeleted", result.RecordsDeleted, - "orphanedRecords", result.OrphanedRecords, - "duration", result.Duration, - "by", session.DID) - - ui.renderTemplate(w, "partials/gc_result.html", struct { - Result *gc.GCResult - }{Result: result}) + progress := ui.gc.GetProgress() + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ + Phase: progress.Phase, + Message: progress.Message, + OpType: progress.OperationType, + }) } -// handleGCDeleteBlobs walks S3 and deletes unreferenced blobs +// handleGCDeleteBlobs starts orphaned blob deletion in the background func (ui *AdminUI) handleGCDeleteBlobs(w http.ResponseWriter, r *http.Request) { if ui.gc == nil { ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) return } - result, err := ui.gc.DeleteOrphanedBlobs(r.Context()) - if err != nil { - slog.Error("GC delete blobs failed", "error", err) - ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{err.Error()}) + session := getSessionFromContext(r.Context()) + + if ui.gc.StartDeleteBlobs() { + slog.Info("GC delete blobs started via admin panel", "by", session.DID) + } + + progress := ui.gc.GetProgress() + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ + Phase: progress.Phase, + Message: progress.Message, + OpType: progress.OperationType, + }) +} + +// handleGCStatus returns current progress or final results (polled by gc_progress.html) +func (ui *AdminUI) handleGCStatus(w http.ResponseWriter, r *http.Request) { + if ui.gc == nil { + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"GC not available"}) return } - session := getSessionFromContext(r.Context()) - slog.Info("GC delete orphaned blobs completed via admin panel", - "blobsDeleted", result.BlobsDeleted, - "bytesReclaimed", result.BytesReclaimed, - "duration", result.Duration, - "by", session.DID) + progress := ui.gc.GetProgress() - ui.renderTemplate(w, "partials/gc_result.html", struct { - Result *gc.GCResult - }{Result: result}) + // Still running — return progress partial (which will poll again) + if progress.Running { + ui.renderTemplate(w, "partials/gc_progress.html", gcProgressData{ + Phase: progress.Phase, + Message: progress.Message, + OpType: progress.OperationType, + }) + return + } + + // Error — show error + if progress.Phase == "error" { + slog.Error("GC operation failed", "type", progress.OperationType, "error", progress.Error) + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{progress.Error}) + return + } + + // Complete — render final results based on operation type + switch progress.OperationType { + case "preview": + preview, _ := ui.gc.LastPreview() + if preview == nil { + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"No preview results available"}) + return + } + slog.Info("GC preview completed via admin panel", + "orphanedRecords", len(preview.OrphanedRecords), + "orphanedBlobs", len(preview.OrphanedBlobs), + "missingRecords", len(preview.MissingRecords), + "duration", preview.Duration) + ui.renderTemplate(w, "partials/gc_preview.html", struct { + Preview *gc.GCPreview + }{Preview: preview}) + + default: + result, _ := ui.gc.LastResult() + if result == nil { + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{"No results available"}) + return + } + slog.Info("GC operation completed via admin panel", + "type", progress.OperationType, + "blobsDeleted", result.BlobsDeleted, + "recordsDeleted", result.RecordsDeleted, + "recordsReconciled", result.RecordsReconciled, + "duration", result.Duration) + ui.renderTemplate(w, "partials/gc_result.html", struct { + Result *gc.GCResult + }{Result: result}) + } } // timeAgo returns a human-readable relative time string diff --git a/pkg/hold/admin/templates/partials/gc_preview.html b/pkg/hold/admin/templates/partials/gc_preview.html index aa61303..1ab7918 100644 --- a/pkg/hold/admin/templates/partials/gc_preview.html +++ b/pkg/hold/admin/templates/partials/gc_preview.html @@ -142,8 +142,7 @@ @@ -153,8 +152,7 @@ hx-post="/admin/api/gc/delete-records" hx-target="#gc-results" hx-swap="innerHTML" - hx-confirm="Delete {{len .Preview.OrphanedRecords}} orphaned layer records?" - hx-indicator="#gc-loading"> + hx-confirm="Delete {{len .Preview.OrphanedRecords}} orphaned layer records?"> {{ icon "file-x" "size-4" }} Delete {{len .Preview.OrphanedRecords}} Orphaned Records @@ -164,8 +162,7 @@ hx-post="/admin/api/gc/delete-blobs" hx-target="#gc-results" hx-swap="innerHTML" - hx-confirm="Delete {{len .Preview.OrphanedBlobs}} orphaned blobs from S3? This cannot be undone." - hx-indicator="#gc-loading"> + hx-confirm="Delete {{len .Preview.OrphanedBlobs}} orphaned blobs from S3? This cannot be undone."> {{ icon "trash-2" "size-4" }} Delete {{len .Preview.OrphanedBlobs}} Orphaned Blobs diff --git a/pkg/hold/admin/templates/partials/gc_progress.html b/pkg/hold/admin/templates/partials/gc_progress.html new file mode 100644 index 0000000..dc7e8b6 --- /dev/null +++ b/pkg/hold/admin/templates/partials/gc_progress.html @@ -0,0 +1,21 @@ +{{define "partials/gc_progress.html"}} +{{.Message}}
++ {{if eq .Phase "manifests"}}Fetching manifests from user PDS instances... + {{else if eq .Phase "records"}}Scanning layer records... + {{else if eq .Phase "blobs"}}Walking S3 storage... + {{else if eq .Phase "deleting"}}Processing changes... + {{else}}Working... + {{end}} +
+Scanning storage...
-This may take a few minutes for large holds.
+Operation in progress...
+Checking status...
+