1 line
16 KiB
JavaScript
1 line
16 KiB
JavaScript
"use strict";!function(t,e){const a=e();"object"==typeof module&&module.exports&&(module.exports=a),t?.document&&t.addEventListener("DOMContentLoaded",()=>a.mount(t))}("undefined"!=typeof window?window:null,function(){const t=["upload","inspect","recipe","destination","finalize"];function e(t){return Array.from(t||[]).map(t=>{return{file:t,relativePath:(e=t.webkitRelativePath||t.name,String(e||"").replace(/\\/g,"/").replace(/^\/+/,"").split("/").filter(t=>t&&"."!==t&&".."!==t).join("/")),size:Number(t.size)};var e}).filter(t=>t.relativePath&&Number.isSafeInteger(t.size)&&t.size>0)}function a(t,e,a){return e?(t+(a?-1:1)+e)%e:-1}function i(t){const e=Array.isArray(t?.files)?t.files:[],a=e.reduce((t,e)=>t+Number(e.size||0),0),i=e.reduce((t,e)=>t+Number(e.offset??e.bytesReceived??0),0);return{resumable:["created","uploading"].includes(t?.state),expected:a,received:i,percent:a?Math.min(100,Math.round(i/a*100)):0}}function r(t){return 0!==t.step||t.selection?.length||t.session?t.step>=2&&"classified"!==t.classification?.status?"importWizard.reviewRequired":t.step>=3&&!t.destinationId?"importWizard.error.destination":4!==t.step||t.confirmed?null:"importWizard.error.confirm":"importWizard.error.selection"}const o=t=>String(t??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[t])),s=t=>{const e=Number(t);if(!Number.isFinite(e))return"unknown";const a=["B","KiB","MiB","GiB","TiB"];let i=0,r=e;for(;r>=1024&&i<a.length-1;)r/=1024,i+=1;return`${r.toFixed(i?1:0)} ${a[i]}`};async function n(t){const e=await crypto.subtle.digest("SHA-256",await t.arrayBuffer());return Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("")}return Object.freeze({deriveResumeState:i,mount:function(d){const l=d.pork;if(d.document.getElementById("chimera-import-open"))return;const c=(t,e)=>d.i18n?.t?.(t,e)||e,p={step:0,selection:[],session:null,job:null,classification:null,order:[],patchIds:new Set,artworkIds:new Set,installNotes:"",destinations:[],destinationId:"",preview:null,uploads:[],jobs:[],busy:!1,confirmed:!1,status:"",error:""};let m=null;const u=d.document.createElement("button");u.id="chimera-import-open",u.type="button",u.className="btn btn-accent chimera-import-launcher",u.style.cssText="position:fixed;right:22px;bottom:22px;z-index:1150",u.dataset.i18n="importWizard.open",u.textContent=c("importWizard.open","Managed import"),d.document.body.appendChild(u);const f=d.document.createElement("div");f.id="import-wizard-overlay",f.className="import-wizard-overlay",f.hidden=!0,f.innerHTML='<section class="import-wizard" role="dialog" aria-modal="true" aria-labelledby="import-wizard-title" aria-describedby="import-wizard-subtitle"><header class="import-wizard-head"><div><span class="import-wizard-kicker">QUARANTINE / VERIFY / COMMIT</span><h2 id="import-wizard-title" data-i18n="importWizard.title">Managed Import</h2><p id="import-wizard-subtitle" data-i18n="importWizard.subtitle">Upload, inspect and finalize without executing package content.</p></div><button type="button" class="import-wizard-close" data-wizard-close aria-label="Close import wizard" data-i18n-aria-label="importWizard.close">x</button></header><div class="import-wizard-steps" role="list"></div><main class="import-wizard-body"></main><footer class="import-wizard-footer"><div class="import-wizard-status" role="status" aria-live="polite"></div><div class="import-wizard-footer-actions"><button type="button" class="btn" data-wizard-previous data-i18n="importWizard.previous">Previous</button><button type="button" class="btn btn-accent" data-wizard-next data-i18n="importWizard.next">Next</button></div></footer><input id="import-wizard-files" type="file" multiple hidden><input id="import-wizard-folder" type="file" webkitdirectory multiple hidden></section>',d.document.body.appendChild(f);const b=f.querySelector(".import-wizard-body"),v=f.querySelector(".import-wizard-status"),h=()=>({installNotes:p.installNotes,componentOrder:p.order,patchComponentIds:[...p.patchIds],artworkComponentIds:[...p.artworkIds]}),w=()=>p.classification?.components?.reduce((t,e)=>t+Number(e.size||0),0)||p.selection.reduce((t,e)=>t+e.size,0);function z(){const t=p.session?i(p.session):{percent:0,received:0,expected:w()};b.innerHTML=`<div class="import-wizard-drop"><p data-i18n="importWizard.choose">Choose multiple files or a complete folder. Files stay quarantined until every validation succeeds.</p><button class="import-wizard-choice" type="button" data-choose-files data-i18n="importWizard.files">Files</button><button class="import-wizard-choice" type="button" data-choose-folder data-i18n="importWizard.folder">Folder</button></div>${p.selection.length?`<div class="import-wizard-progress" aria-label="${o(c("importWizard.progress","Upload progress"))}"><span style="width:${t.percent}%"></span></div><p>${p.selection.length} files - ${s(w())}</p><button class="btn btn-accent" type="button" data-start-upload data-i18n="importWizard.startUpload">Start resumable upload</button>`:""}<p class="hint" data-i18n="importWizard.resumeHint">Reselect the same files to continue from the verified server offsets.</p><div class="import-wizard-resumes">${[...p.uploads.filter(t=>i(t).resumable).map(t=>{const e=i(t);return`<article class="import-wizard-resume"><div><strong>${o(t.files?.[0]?.relativePath||t.id)}</strong><small>${e.percent}% - ${s(e.received)} / ${s(e.expected)}</small></div><button class="btn btn-sm" type="button" data-resume-upload="${o(t.id)}" data-i18n="importWizard.resume">Resume</button></article>`}),...p.jobs.filter(t=>!["finalized","cancelled"].includes(t.state)).map(t=>`<article class="import-wizard-resume"><div><strong>${o(t.id)}</strong><small>${o(t.state)}</small></div><button class="btn btn-sm" type="button" data-resume-job="${o(t.id)}" data-i18n="importWizard.resume">Resume</button></article>`)].join("")||'<p class="import-wizard-empty" data-i18n="importWizard.noResumes">No resumable imports.</p>'}</div>`}function g(){const t=p.classification,e=t?.issues||[];b.innerHTML='<button class="btn btn-accent" type="button" data-run-inspection data-i18n="importWizard.runInspection">Run inspection</button>'+(t?`<div class="import-wizard-preview"><div><span>Status</span><strong>${o(t.status)}</strong></div><div><span>Components</span><strong>${t.components?.length||0}</strong></div></div>${e.length?`<ul class="import-wizard-issues">${e.map(t=>`<li><strong>${o(t.code)}</strong><br>${o(t.message||t.detail||"")}</li>`).join("")}</ul>`:""}`:'<p class="import-wizard-empty">No inspection evidence yet.</p>')}function y(){const t=p.classification?.components||[];b.innerHTML=`<label class="import-wizard-field"><span data-i18n="importWizard.installNotes">Install notes (never executed)</span><textarea data-install-notes maxlength="10000">${o(p.installNotes)}</textarea></label><h3 data-i18n="importWizard.componentOrder">Structured component order</h3><div class="import-wizard-components">${p.order.map((e,a)=>{const i=t.find(t=>t.id===e);return`<article class="import-wizard-component"><div><strong>${a+1}. ${o(i?.relativePath||e)}</strong><small>${o(i?.type?.value||"")} - ${o(i?.version?.value||"")}</small></div><div class="import-wizard-component-controls"><button type="button" class="btn btn-sm" data-order-up="${o(e)}" aria-label="Move up">^</button><button type="button" class="btn btn-sm" data-order-down="${o(e)}" aria-label="Move down">v</button></div></article>`}).join("")}</div><h3 data-i18n="importWizard.attach">Attach to portable manifest</h3><div class="import-wizard-components">${t.filter(t=>["patch_bundle","apr_ampr","image"].includes(t.type?.value)).map(t=>{const e="image"===t.type.value,a=e?p.artworkIds.has(t.id):p.patchIds.has(t.id);return`<label class="import-wizard-component import-wizard-attach"><span><strong>${o(t.relativePath)}</strong><small>${o(t.type.value)}</small></span><input type="checkbox" data-attach-id="${o(t.id)}" data-attach-kind="${e?"artwork":"patch"}" ${a?"checked":""}></label>`}).join("")||'<p class="hint">No patch/APR or artwork components detected.</p>'}</div>`}function $(){const t=p.destinations.find(t=>t.id===p.destinationId);b.innerHTML=`<label class="import-wizard-field"><span data-i18n="importWizard.volume">Managed destination volume</span><select data-destination><option value="">Select...</option>${p.destinations.map(t=>`<option value="${o(t.id)}" ${t.id===p.destinationId?"selected":""}>${o(t.label)}</option>`).join("")}</select></label><button class="btn" type="button" data-refresh-preview data-i18n="importWizard.preview">Refresh final preview</button><div class="import-wizard-preview"><div><span>Normalized path</span><strong>${o(p.preview?.destination||"-")}</strong></div><div><span data-i18n="importWizard.space">Required / available space</span><strong>${s(w())} / ${s(t?.availableBytes)}</strong></div></div>`}function W(){const t=p.destinations.find(t=>t.id===p.destinationId);b.innerHTML=`<div class="import-wizard-preview"><div><span>Title</span><strong>${o(p.preview?.titleName||p.classification?.title?.id?.value||"-")}</strong></div><div><span>Normalized path</span><strong>${o(p.preview?.destination||"-")}</strong></div><div><span>Volume</span><strong>${o(t?.label||"-")}</strong></div><div><span data-i18n="importWizard.space">Required / available space</span><strong>${s(w())} / ${s(t?.availableBytes)}</strong></div></div><label class="import-wizard-confirm"><input type="checkbox" data-final-confirm ${p.confirmed?"checked":""}><span data-i18n="importWizard.confirm">I reviewed the evidence, order and destination.</span></label><button class="btn btn-accent" type="button" data-finalize data-i18n="importWizard.finalizeAction">Finalize managed import</button>`}function x(){f.querySelector(".import-wizard-steps").innerHTML=t.map((t,e)=>`<span role="listitem" class="import-wizard-step ${e===p.step?"active":""} ${e<p.step?"complete":""}" data-number="${e+1}" data-i18n="importWizard.${t}">${o(c(`importWizard.${t}`,t))}</span>`).join(""),[z,g,y,$,W][p.step](),f.querySelector("[data-wizard-previous]").hidden=0===p.step,f.querySelector("[data-wizard-next]").hidden=0===p.step||4===p.step,f.querySelectorAll("button,input,select,textarea").forEach(t=>{t.matches("[data-wizard-close]")||(t.disabled=p.busy)}),v.textContent=p.error||p.status,v.classList.toggle("error",Boolean(p.error)),d.i18n?.apply?.(f),u.textContent=c("importWizard.open","Managed import")}function I(){f.hidden=!0,d.document.body.classList.remove("chimera-modal-open"),m?.focus?.()}async function k(){if(!p.selection.length)throw new Error(c("importWizard.error.selection","Select at least one non-empty file."));p.session||(p.session=await l.importUploadCreate(p.selection.map(t=>({relativePath:t.relativePath,size:t.size}))));for(const t of p.selection){const e=p.session.files.find(e=>e.relativePath===t.relativePath);if(!e||Number(e.size)!==t.size)throw new Error("Selected files do not match the resumable upload.");let a=Number(e.offset??e.bytesReceived??0);for(;a<t.size;){const i=t.file.slice(a,Math.min(a+4194304,t.size)),r=await l.importUploadChunk(p.session.id,e.id,a,i,await n(i));a=Number(r.uploadOffset??r.offset??r.bytesReceived),e.offset=a,p.status=`${t.relativePath}: ${s(a)} / ${s(t.size)}`,x()}}p.session=await l.importUploadGet(p.session.id),p.job=await l.importJobCreate(p.session.id),p.step=1,p.status="",x()}async function S(){p.classification=await l.importClassify(p.job.id),p.order=p.classification.recipeProposal?.installOrder?.map(t=>t.componentId)||[],p.patchIds=new Set(p.classification.components.filter(t=>["patch_bundle","apr_ampr"].includes(t.type?.value)).map(t=>t.id)),p.artworkIds=new Set(p.classification.components.filter(t=>"image"===t.type?.value).map(t=>t.id)),x()}async function N(){p.preview=await l.importManifestPreview(p.job.id,h()),x()}async function j(){const t=r(p);if(t)throw new Error(c(t,t));await l.importManifestCreate(p.job.id,h()),await l.importJobValidate(p.job.id);const e=await l.importJobFinalize(p.job.id,{managedRootId:p.destinationId,relativePath:p.preview.destination});p.status=c("importWizard.done","Import finalized. No package was installed or executed."),p.job=e,p.confirmed=!1,x()}async function M(t){p.busy=!0,p.error="",x();try{await t()}catch(t){p.error=t.message}finally{p.busy=!1,x()}}function E(t,e){const a=p.order.indexOf(t),i=a+e;a<0||i<0||i>=p.order.length||([p.order[a],p.order[i]]=[p.order[i],p.order[a]],x())}u.addEventListener("click",function(){!function(){if(d.document.querySelector("link[data-import-wizard-style]"))return;const t=d.document.createElement("link");t.rel="stylesheet",t.href="styles/import-wizard.css",t.dataset.importWizardStyle="true",d.document.head.appendChild(t)}(),m=d.document.activeElement,f.hidden=!1,d.document.body.classList.add("chimera-modal-open"),p.error="",async function(){if(!l?.importUploadList)return p.error=c("importWizard.unavailable","Managed import is available from the authenticated Chimera server interface."),void x();try{[p.uploads,p.jobs,p.destinations]=await Promise.all([l.importUploadList(),l.importJobList(),l.importDestinations()])}catch(t){p.error=t.message}x()}().finally(()=>f.querySelector("[data-wizard-close]")?.focus())}),f.addEventListener("click",t=>{t.target===f||t.target.closest("[data-wizard-close]")?I():t.target.closest("[data-choose-files]")?f.querySelector("#import-wizard-files").click():t.target.closest("[data-choose-folder]")?f.querySelector("#import-wizard-folder").click():t.target.closest("[data-start-upload]")?M(k):t.target.closest("[data-run-inspection]")?M(S):t.target.closest("[data-refresh-preview]")?M(N):t.target.closest("[data-finalize]")?M(j):t.target.closest("[data-order-up]")?E(t.target.closest("[data-order-up]").dataset.orderUp,-1):t.target.closest("[data-order-down]")?E(t.target.closest("[data-order-down]").dataset.orderDown,1):t.target.closest("[data-resume-upload]")?(p.session=p.uploads.find(e=>e.id===t.target.closest("[data-resume-upload]").dataset.resumeUpload),p.selection=[],p.status=c("importWizard.resumeHint","Reselect the same files to continue."),x()):t.target.closest("[data-resume-job]")?M(async()=>{p.job=await l.importJobGet(t.target.closest("[data-resume-job]").dataset.resumeJob);try{p.classification=await l.importClassificationGet(p.job.id)}catch(t){p.classification=null}p.step="classified"===p.classification?.status?2:1,p.classification&&(p.order=p.classification.recipeProposal?.installOrder?.map(t=>t.componentId)||[])}):t.target.closest("[data-wizard-previous]")?(p.step=Math.max(0,p.step-1),p.error="",x()):t.target.closest("[data-wizard-next]")&&M(async()=>{const t=r(p);if(t)throw new Error(c(t,t));1!==p.step||p.classification||await S(),2!==p.step&&3!==p.step||await N(),p.step=Math.min(4,p.step+1)})}),f.addEventListener("change",t=>{if(t.target.matches("#import-wizard-files,#import-wizard-folder")&&(p.selection=e(t.target.files),p.session=p.session&&p.selection.length?p.session:null,p.error="",x()),t.target.matches("[data-attach-id]")){const e="artwork"===t.target.dataset.attachKind?p.artworkIds:p.patchIds;t.target.checked?e.add(t.target.dataset.attachId):e.delete(t.target.dataset.attachId)}t.target.matches("[data-destination]")&&(p.destinationId=t.target.value,p.preview=null,p.confirmed=!1,x()),t.target.matches("[data-final-confirm]")&&(p.confirmed=t.target.checked)}),f.addEventListener("input",t=>{t.target.matches("[data-install-notes]")&&(p.installNotes=t.target.value)}),f.addEventListener("keydown",t=>{if("Escape"===t.key)return t.preventDefault(),void I();if("Tab"!==t.key)return;const e=[...f.querySelectorAll("button:not([disabled]):not([hidden]),input:not([disabled]):not([hidden]),select:not([disabled]),textarea:not([disabled])")].filter(t=>null!==t.offsetParent),i=e.indexOf(d.document.activeElement),r=a(i,e.length,t.shiftKey);(t.shiftKey&&i<=0||!t.shiftKey&&i===e.length-1)&&(t.preventDefault(),e[r]?.focus())}),d.addEventListener("chimera:language-changed",()=>{x()}),x()},nextFocusIndex:a,normalizeFileSelection:e,validateWizardStep:r})}); |