Merged
Keep Admin maintenance target and intent synchronized #147
urandonAdd on Oct 10
Keep Admin maintenance target and intent synchronized #147
Merged
urandonAdd on Oct 10
Total: 7 files changed+347-31
@@ -123,6 +123,18 @@ The observation UI is organized around product state rather than CLI command gro
123 123 
124Local deep links use URL search parameters: `view`, opaque `cache`, public repository id `repo`, canonical or legacy `registration`, repository `tab`, diagnostic state, public `job`, job state/type/cache/repository/failure filters, and Search Lab `q`, `kind`, `provenance`, and `limit`. A legacy registration deep link is replaced with its canonical public id and an in-page redirect explanation. No cache path, credential, provider endpoint, raw source body, or session material is included. Browser reload and back/forward navigation preserve this state. The UI explicitly renders loading, empty, partial/degraded, stale, recovered, interrupted, waiting, and API-version-mismatch states; status meaning is always present as text rather than color alone.124Local deep links use URL search parameters: `view`, opaque `cache`, public repository id `repo`, canonical or legacy `registration`, repository `tab`, diagnostic state, public `job`, job state/type/cache/repository/failure filters, and Search Lab `q`, `kind`, `provenance`, and `limit`. A legacy registration deep link is replaced with its canonical public id and an in-page redirect explanation. No cache path, credential, provider endpoint, raw source body, or session material is included. Browser reload and back/forward navigation preserve this state. The UI explicitly renders loading, empty, partial/degraded, stale, recovered, interrupted, waiting, and API-version-mismatch states; status meaning is always present as text rather than color alone.
125 125 
126+Maintenance selection and submitted intent share one opaque cache/repository
127+identity across Caches navigation, registration links, reload and history. The
128+target remains URL-addressable before enrollment too. A same-target snapshot
129+refresh preserves unsaved policy edits; changing target clears its old plan,
130+receipt and target-bound confirmation. A replaced or unavailable registration ID
131+also invalidates its pending confirmation, even within the same cache/repository.
132+Late planning or control responses cannot
133+replace the current target or edited policy's state. An explicit unavailable
134+repository, cache or registration remains unavailable until the operator selects
135+a current target; it never silently falls back to another repository. Immutable
136+conflict-resolution plans retain their durable key for lost-response replay.
137+ 
126## Themes138## Themes
127 139 
128The visible selector has exactly three choices: **Light**, **Dark**, and **System**. **System is the default** when no preference is saved or an invalid value is found. It follows `prefers-color-scheme`; explicit Light and Dark choices are stored only in browser-local storage. A small external head script applies the choice before the application starts, avoiding a first-paint theme flash while keeping the CSP free of inline script allowances.140The visible selector has exactly three choices: **Light**, **Dark**, and **System**. **System is the default** when no preference is saved or an invalid value is found. It follows `prefers-color-scheme`; explicit Light and Dark choices are stored only in browser-local storage. A small external head script applies the choice before the application starts, avoiding a first-paint theme flash while keeping the CSP free of inline script allowances.
Rinternal/adminui/assets/_app/immutable/entry/app.D3MXzpdv.js→internal/adminui/assets/_app/immutable/entry/app.C3DQiGbd.js+2-2
@@ -1,2 +1,2 @@
1-const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BpU76GJV.js","../chunks/DLaOuNhp.js","../chunks/Vw_9FM2e.js","../chunks/CF2b_dDT.js","../chunks/BojSzMqI.js","../nodes/1.J3pyGi2p.js","../chunks/B030zhyR.js","../chunks/CGe5bBV9.js","../chunks/CDSuRcmi.js","../nodes/2.DyqNvmtI.js","../chunks/D5ZZnG0e.js","../assets/2.45OGHqsa.css"])))=>i.map(i=>d[i]);1+const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BpU76GJV.js","../chunks/DLaOuNhp.js","../chunks/Vw_9FM2e.js","../chunks/CF2b_dDT.js","../chunks/BojSzMqI.js","../nodes/1.J3pyGi2p.js","../chunks/B030zhyR.js","../chunks/CGe5bBV9.js","../chunks/CDSuRcmi.js","../nodes/2.DDMJuMk3.js","../chunks/D5ZZnG0e.js","../assets/2.45OGHqsa.css"])))=>i.map(i=>d[i]);
2-import{s as p,L as G,g as f,f as W,d as Y,m as z,p as F,u as H,a as J,t as K,b as k,c as Q,e as X,h as x,i as Z,r as $,j as ee,k as L}from"../chunks/Vw_9FM2e.js";import{h as te,m as re,u as se,s as ne}from"../chunks/B030zhyR.js";import{a as b,c as j,f as V,t as ae}from"../chunks/DLaOuNhp.js";import{o as oe}from"../chunks/CDSuRcmi.js";import{p as S,i as A,c as C,b as T}from"../chunks/D5ZZnG0e.js";const ce="modulepreload",ie=function(a,e){return new URL(a,e).href},I={},O=function(e,n,i){let l=Promise.resolve();if(n&&n.length>0){let R=function(c){return Promise.all(c.map(m=>Promise.resolve(m).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const t=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),w=s?.nonce||s?.getAttribute("nonce");l=R(n.map(c=>{if(c=ie(c,i),c in I)return;I[c]=!0;const m=c.endsWith(".css"),d=m?'[rel="stylesheet"]':"";if(i)for(let h=t.length-1;h>=0;h--){const o=t[h];if(o.href===c&&(!m||o.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${c}"]${d}`))return;const u=document.createElement("link");if(u.rel=m?"stylesheet":ce,m||(u.as="script"),u.crossOrigin="",u.href=c,w&&u.setAttribute("nonce",w),document.head.appendChild(u),m)return new Promise((h,o)=>{u.addEventListener("load",h),u.addEventListener("error",()=>o(new Error(`Unable to preload CSS for ${c}`)))})}))}function r(t){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=t,window.dispatchEvent(s),!s.defaultPrevented)throw t}return l.then(t=>{for(const s of t||[])s.status==="rejected"&&r(s.reason);return e().catch(r)})},Pe={};function le(a){return class extends ue{constructor(e){super({component:a,...e})}}}class ue{#t;#e;constructor(e){var n=new Map,i=(r,t)=>{var s=z(t,!1,!1);return n.set(r,s),s};const l=new Proxy({...e.props||{},$$events:{}},{get(r,t){return f(n.get(t)??i(t,Reflect.get(r,t)))},has(r,t){return t===G?!0:(f(n.get(t)??i(t,Reflect.get(r,t))),Reflect.has(r,t))},set(r,t,s){return p(n.get(t)??i(t,s),s),Reflect.set(r,t,s)}});this.#e=(e.hydrate?te:re)(e.component,{target:e.target,anchor:e.anchor,props:l,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError}),(!e?.props?.$$host||e.sync===!1)&&W(),this.#t=l.$$events;for(const r of Object.keys(this.#e))r==="$set"||r==="$destroy"||r==="$on"||Y(this,r,{get(){return this.#e[r]},set(t){this.#e[r]=t},enumerable:!0});this.#e.$set=r=>{Object.assign(l,r)},this.#e.$destroy=()=>{se(this.#e)}}$set(e){this.#e.$set(e)}$on(e,n){this.#t[e]=this.#t[e]||[];const i=(...l)=>n.call(this,...l);return this.#t[e].push(i),()=>{this.#t[e]=this.#t[e].filter(l=>l!==i)}}$destroy(){this.#e.$destroy()}}var me=V('<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>'),fe=V("<!> <!>",1);function de(a,e){F(e,!0);let n=S(e,"components",23,()=>[]),i=S(e,"data_0",3,null),l=S(e,"data_1",3,null);H(()=>e.stores.page.set(e.page)),J(()=>{e.stores,e.page,e.constructors,n(),e.form,i(),l(),e.stores.page.notify()});let r=x(!1),t=x(!1),s=x(null);oe(()=>{const o=e.stores.page.subscribe(()=>{f(r)&&(p(t,!0),K().then(()=>{p(s,document.title||"untitled page",!0)}))});return p(r,!0),o});const w=L(()=>e.constructors[1]);var R=fe(),c=k(R);{var m=o=>{const _=L(()=>e.constructors[0]);var v=j(),E=k(v);C(E,()=>f(_),(g,y)=>{T(y(g,{get data(){return i()},get form(){return e.form},get params(){return e.page.params},children:(P,_e)=>{var D=j(),N=k(D);C(N,()=>f(w),(q,U)=>{T(U(q,{get data(){return l()},get form(){return e.form},get params(){return e.page.params}}),B=>n()[1]=B,()=>n()?.[1])}),b(P,D)},$$slots:{default:!0}}),P=>n()[0]=P,()=>n()?.[0])}),b(o,v)},d=o=>{const _=L(()=>e.constructors[0]);var v=j(),E=k(v);C(E,()=>f(_),(g,y)=>{T(y(g,{get data(){return i()},get form(){return e.form},get params(){return e.page.params}}),P=>n()[0]=P,()=>n()?.[0])}),b(o,v)};A(c,o=>{e.constructors[1]?o(m):o(d,-1)})}var u=Q(c,2);{var h=o=>{var _=me(),v=Z(_);{var E=g=>{var y=ae();ee(()=>ne(y,f(s))),b(g,y)};A(v,g=>{f(t)&&g(E)})}$(_),b(o,_)};A(u,o=>{f(r)&&o(h)})}b(a,R),X()}const we=le(de),Re=[()=>O(()=>import("../nodes/0.BpU76GJV.js"),__vite__mapDeps([0,1,2,3,4]),import.meta.url),()=>O(()=>import("../nodes/1.J3pyGi2p.js"),__vite__mapDeps([5,1,2,6,7,8]),import.meta.url),()=>O(()=>import("../nodes/2.DyqNvmtI.js"),__vite__mapDeps([9,1,2,8,6,10,4,3,11]),import.meta.url)],ke=[],pe={"/":[2]},M={handleError:(({error:a})=>{console.error(a)}),reroute:(()=>{}),transport:{}},he=Object.fromEntries(Object.entries(M.transport).map(([a,e])=>[a,e.decode])),Oe=Object.fromEntries(Object.entries(M.transport).map(([a,e])=>[a,e.encode])),xe=!1,Le=(a,e)=>he[a](e),je=()=>O(()=>import("../chunks/wbPk3Yxo.js"),[],import.meta.url).then(a=>a.default);export{Le as decode,he as decoders,pe as dictionary,Oe as encoders,je as get_error_template,xe as hash,M as hooks,Pe as matchers,Re as nodes,we as root,ke as server_loads};2+import{s as p,L as G,g as f,f as W,d as Y,m as z,p as F,u as H,a as J,t as K,b as k,c as Q,e as X,h as x,i as Z,r as $,j as ee,k as L}from"../chunks/Vw_9FM2e.js";import{h as te,m as re,u as se,s as ne}from"../chunks/B030zhyR.js";import{a as b,c as j,f as V,t as ae}from"../chunks/DLaOuNhp.js";import{o as oe}from"../chunks/CDSuRcmi.js";import{p as S,i as A,c as C,b as T}from"../chunks/D5ZZnG0e.js";const ce="modulepreload",ie=function(a,e){return new URL(a,e).href},I={},O=function(e,n,i){let l=Promise.resolve();if(n&&n.length>0){let R=function(c){return Promise.all(c.map(m=>Promise.resolve(m).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const t=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),w=s?.nonce||s?.getAttribute("nonce");l=R(n.map(c=>{if(c=ie(c,i),c in I)return;I[c]=!0;const m=c.endsWith(".css"),d=m?'[rel="stylesheet"]':"";if(i)for(let h=t.length-1;h>=0;h--){const o=t[h];if(o.href===c&&(!m||o.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${c}"]${d}`))return;const u=document.createElement("link");if(u.rel=m?"stylesheet":ce,m||(u.as="script"),u.crossOrigin="",u.href=c,w&&u.setAttribute("nonce",w),document.head.appendChild(u),m)return new Promise((h,o)=>{u.addEventListener("load",h),u.addEventListener("error",()=>o(new Error(`Unable to preload CSS for ${c}`)))})}))}function r(t){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=t,window.dispatchEvent(s),!s.defaultPrevented)throw t}return l.then(t=>{for(const s of t||[])s.status==="rejected"&&r(s.reason);return e().catch(r)})},Pe={};function le(a){return class extends ue{constructor(e){super({component:a,...e})}}}class ue{#t;#e;constructor(e){var n=new Map,i=(r,t)=>{var s=z(t,!1,!1);return n.set(r,s),s};const l=new Proxy({...e.props||{},$$events:{}},{get(r,t){return f(n.get(t)??i(t,Reflect.get(r,t)))},has(r,t){return t===G?!0:(f(n.get(t)??i(t,Reflect.get(r,t))),Reflect.has(r,t))},set(r,t,s){return p(n.get(t)??i(t,s),s),Reflect.set(r,t,s)}});this.#e=(e.hydrate?te:re)(e.component,{target:e.target,anchor:e.anchor,props:l,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError}),(!e?.props?.$$host||e.sync===!1)&&W(),this.#t=l.$$events;for(const r of Object.keys(this.#e))r==="$set"||r==="$destroy"||r==="$on"||Y(this,r,{get(){return this.#e[r]},set(t){this.#e[r]=t},enumerable:!0});this.#e.$set=r=>{Object.assign(l,r)},this.#e.$destroy=()=>{se(this.#e)}}$set(e){this.#e.$set(e)}$on(e,n){this.#t[e]=this.#t[e]||[];const i=(...l)=>n.call(this,...l);return this.#t[e].push(i),()=>{this.#t[e]=this.#t[e].filter(l=>l!==i)}}$destroy(){this.#e.$destroy()}}var me=V('<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>'),fe=V("<!> <!>",1);function de(a,e){F(e,!0);let n=S(e,"components",23,()=>[]),i=S(e,"data_0",3,null),l=S(e,"data_1",3,null);H(()=>e.stores.page.set(e.page)),J(()=>{e.stores,e.page,e.constructors,n(),e.form,i(),l(),e.stores.page.notify()});let r=x(!1),t=x(!1),s=x(null);oe(()=>{const o=e.stores.page.subscribe(()=>{f(r)&&(p(t,!0),K().then(()=>{p(s,document.title||"untitled page",!0)}))});return p(r,!0),o});const w=L(()=>e.constructors[1]);var R=fe(),c=k(R);{var m=o=>{const _=L(()=>e.constructors[0]);var v=j(),E=k(v);C(E,()=>f(_),(g,y)=>{T(y(g,{get data(){return i()},get form(){return e.form},get params(){return e.page.params},children:(P,_e)=>{var D=j(),N=k(D);C(N,()=>f(w),(q,U)=>{T(U(q,{get data(){return l()},get form(){return e.form},get params(){return e.page.params}}),B=>n()[1]=B,()=>n()?.[1])}),b(P,D)},$$slots:{default:!0}}),P=>n()[0]=P,()=>n()?.[0])}),b(o,v)},d=o=>{const _=L(()=>e.constructors[0]);var v=j(),E=k(v);C(E,()=>f(_),(g,y)=>{T(y(g,{get data(){return i()},get form(){return e.form},get params(){return e.page.params}}),P=>n()[0]=P,()=>n()?.[0])}),b(o,v)};A(c,o=>{e.constructors[1]?o(m):o(d,-1)})}var u=Q(c,2);{var h=o=>{var _=me(),v=Z(_);{var E=g=>{var y=ae();ee(()=>ne(y,f(s))),b(g,y)};A(v,g=>{f(t)&&g(E)})}$(_),b(o,_)};A(u,o=>{f(r)&&o(h)})}b(a,R),X()}const we=le(de),Re=[()=>O(()=>import("../nodes/0.BpU76GJV.js"),__vite__mapDeps([0,1,2,3,4]),import.meta.url),()=>O(()=>import("../nodes/1.J3pyGi2p.js"),__vite__mapDeps([5,1,2,6,7,8]),import.meta.url),()=>O(()=>import("../nodes/2.DDMJuMk3.js"),__vite__mapDeps([9,1,2,8,6,10,4,3,11]),import.meta.url)],ke=[],pe={"/":[2]},M={handleError:(({error:a})=>{console.error(a)}),reroute:(()=>{}),transport:{}},he=Object.fromEntries(Object.entries(M.transport).map(([a,e])=>[a,e.decode])),Oe=Object.fromEntries(Object.entries(M.transport).map(([a,e])=>[a,e.encode])),xe=!1,Le=(a,e)=>he[a](e),je=()=>O(()=>import("../chunks/wbPk3Yxo.js"),[],import.meta.url).then(a=>a.default);export{Le as decode,he as decoders,pe as dictionary,Oe as encoders,je as get_error_template,xe as hash,M as hooks,Pe as matchers,Re as nodes,we as root,ke as server_loads};
@@ -6,12 +6,12 @@
6 <meta name="color-scheme" content="light dark" />6 <meta name="color-scheme" content="light dark" />
7 <link rel="icon" href="data:," />7 <link rel="icon" href="data:," />
8 <script src="/theme-init.js"></script>8 <script src="/theme-init.js"></script>
9- <meta http-equiv="content-security-policy" content="default-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-f3suqcTmkaUfUdFkMsfh5shbxt5AN3HOKJvadNz0mdM='; style-src 'self'; base-uri 'none'; form-action 'self'">9+ <meta http-equiv="content-security-policy" content="default-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-W+gs+xBH14sxetxNgf9ilPYnxUVukbFm4buq3Ya+aGA='; style-src 'self'; base-uri 'none'; form-action 'self'">
10 <link href="/_app/immutable/entry/start.zyF_fiPG.js" rel="modulepreload">10 <link href="/_app/immutable/entry/start.zyF_fiPG.js" rel="modulepreload">
11 <link href="/_app/immutable/chunks/Vw_9FM2e.js" rel="modulepreload">11 <link href="/_app/immutable/chunks/Vw_9FM2e.js" rel="modulepreload">
12 <link href="/_app/immutable/chunks/CGe5bBV9.js" rel="modulepreload">12 <link href="/_app/immutable/chunks/CGe5bBV9.js" rel="modulepreload">
13 <link href="/_app/immutable/chunks/CDSuRcmi.js" rel="modulepreload">13 <link href="/_app/immutable/chunks/CDSuRcmi.js" rel="modulepreload">
14- <link href="/_app/immutable/entry/app.D3MXzpdv.js" rel="modulepreload">14+ <link href="/_app/immutable/entry/app.C3DQiGbd.js" rel="modulepreload">
15 <link href="/_app/immutable/chunks/B030zhyR.js" rel="modulepreload">15 <link href="/_app/immutable/chunks/B030zhyR.js" rel="modulepreload">
16 <link href="/_app/immutable/chunks/DLaOuNhp.js" rel="modulepreload">16 <link href="/_app/immutable/chunks/DLaOuNhp.js" rel="modulepreload">
17 <link href="/_app/immutable/chunks/D5ZZnG0e.js" rel="modulepreload">17 <link href="/_app/immutable/chunks/D5ZZnG0e.js" rel="modulepreload">
@@ -32,7 +32,7 @@
32 32 
33 Promise.all([33 Promise.all([
34 import("/_app/immutable/entry/start.zyF_fiPG.js"),34 import("/_app/immutable/entry/start.zyF_fiPG.js"),
35- import("/_app/immutable/entry/app.D3MXzpdv.js")35+ import("/_app/immutable/entry/app.C3DQiGbd.js")
36 ]).then(([kit, app]) => {36 ]).then(([kit, app]) => {
37 kit.start(app, element);37 kit.start(app, element);
38 });38 });
@@ -66,6 +66,7 @@
66 let stale = false;66 let stale = false;
67 let maintenanceTargetKey = '';67 let maintenanceTargetKey = '';
68 let maintenanceIntent: MaintenanceIntent = { cache_ref: '', repo_id: '', sync_mode: 'head-and-backfill', collections: ['issues', 'wiki'], rag_mode: 'off' };68 let maintenanceIntent: MaintenanceIntent = { cache_ref: '', repo_id: '', sync_mode: 'head-and-backfill', collections: ['issues', 'wiki'], rag_mode: 'off' };
69+ let maintenanceIntentGeneration = 0;
69 let maintenancePlan: MaintenancePlan | undefined;70 let maintenancePlan: MaintenancePlan | undefined;
70 let maintenanceReceipt: ControlReceipt | undefined;71 let maintenanceReceipt: ControlReceipt | undefined;
71 let maintenanceError = '';72 let maintenanceError = '';
@@ -78,6 +79,7 @@
78 let controlRunning = false;79 let controlRunning = false;
79 let pendingControl: 'maintenance_apply' | 'binding_apply' | 'rag_repair_apply' | 'conflict_resolution_apply' | 'disable' | 'reconcile' | 'repository_docs_index' | '' = '';80 let pendingControl: 'maintenance_apply' | 'binding_apply' | 'rag_repair_apply' | 'conflict_resolution_apply' | 'disable' | 'reconcile' | 'repository_docs_index' | '' = '';
80 let pendingControlKey = '';81 let pendingControlKey = '';
82+ let pendingControlRegistrationID = '';
81 let controlDialog: HTMLDialogElement | undefined;83 let controlDialog: HTMLDialogElement | undefined;
82 let controlConfirmButton: HTMLButtonElement | undefined;84 let controlConfirmButton: HTMLButtonElement | undefined;
83 let controlTriggerButton: HTMLButtonElement | undefined;85 let controlTriggerButton: HTMLButtonElement | undefined;
@@ -146,6 +148,7 @@
146 $: if (!repositoryDocsSources.some((source) => source.source_registration_id === repositoryDocsSourceID)) repositoryDocsSourceID = repositoryDocsSources[0]?.source_registration_id || '';148 $: if (!repositoryDocsSources.some((source) => source.source_registration_id === repositoryDocsSourceID)) repositoryDocsSourceID = repositoryDocsSources[0]?.source_registration_id || '';
147 $: selectedRepositoryDocsSource = repositoryDocsSources.find((source) => source.source_registration_id === repositoryDocsSourceID);149 $: selectedRepositoryDocsSource = repositoryDocsSources.find((source) => source.source_registration_id === repositoryDocsSourceID);
148 $: repoTargets = snapshot.caches.flatMap((cache) => cache.repositories.map((repo) => ({ key: `${cache.cache_ref}\u0000${repo.repo_id}`, cache, repo })));150 $: repoTargets = snapshot.caches.flatMap((cache) => cache.repositories.map((repo) => ({ key: `${cache.cache_ref}\u0000${repo.repo_id}`, cache, repo })));
151+ $: maintenanceTargetAvailable = repoTargets.some((target) => target.key === maintenanceTargetKey) && (!requestedRegistrationID || snapshot.maintenance.some((item) => item.registration_id === requestedRegistrationID || item.legacy_registration_ids?.includes(requestedRegistrationID)));
149 152 
150 function normalizeSnapshot(value: ObservationSnapshot): ObservationSnapshot {153 function normalizeSnapshot(value: ObservationSnapshot): ObservationSnapshot {
151 value.attention ||= []; value.caches ||= []; value.jobs ||= []; value.maintenance ||= []; value.diagnostics ||= []; value.capabilities ||= [];154 value.attention ||= []; value.caches ||= []; value.jobs ||= []; value.maintenance ||= []; value.diagnostics ||= []; value.capabilities ||= [];
@@ -227,7 +230,7 @@
227 if (!response.ok) throw new Error(response.status === 401 ? 'Admin session required. Run admin open again.' : 'Observation is unavailable.');230 if (!response.ok) throw new Error(response.status === 401 ? 'Admin session required. Run admin open again.' : 'Observation is unavailable.');
228 const nextSnapshot = normalizeSnapshot(await response.json());231 const nextSnapshot = normalizeSnapshot(await response.json());
229 snapshot = nextSnapshot;232 snapshot = nextSnapshot;
230- if (selectedCacheRef && selectedRepoID) maintenanceTargetKey = `${selectedCacheRef}\u0000${selectedRepoID}`;233+ if (selectedCacheRef && selectedRepoID) synchronizeMaintenanceTarget(`${selectedCacheRef}\u0000${selectedRepoID}`);
231 ensureControlSelections();234 ensureControlSelections();
232 canonicalizeRegistrationDeepLink();235 canonicalizeRegistrationDeepLink();
233 } catch (value) { error = value instanceof Error ? value.message : 'Observation is unavailable.'; }236 } catch (value) { error = value instanceof Error ? value.message : 'Observation is unavailable.'; }
@@ -289,12 +292,15 @@
289 292 
290 function selectView(view: AdminView): void {293 function selectView(view: AdminView): void {
291 active = view;294 active = view;
292- if (view !== 'Caches') { selectedCacheRef = ''; selectedRepoID = ''; }295+ if (view === 'Maintenance') {
296+ selectedCacheRef = maintenanceIntent.cache_ref; selectedRepoID = maintenanceIntent.repo_id;
297+ requestedRegistrationID = snapshot.maintenance.find((item) => item.cache_ref === selectedCacheRef && item.repo_id === selectedRepoID)?.registration_id || '';
298+ } else if (view !== 'Caches') { selectedCacheRef = ''; selectedRepoID = ''; }
293 if (view !== 'Jobs') selectedJobID = '';299 if (view !== 'Jobs') selectedJobID = '';
294 if (view !== 'Maintenance') { requestedRegistrationID = ''; registrationRedirectNotice = ''; }300 if (view !== 'Maintenance') { requestedRegistrationID = ''; registrationRedirectNotice = ''; }
295 updateLocation();301 updateLocation();
296 }302 }
297- function openRepository(cache: CacheObservation, repo: Repository): void { active = 'Caches'; selectedCacheRef = cache.cache_ref; selectedRepoID = repo.repo_id; maintenanceTargetKey = `${cache.cache_ref}\u0000${repo.repo_id}`; repoTab = 'coverage'; searchComparison = undefined; searchError = ''; providerSmoke = undefined; repairPlan = undefined; repairReceipt = undefined; repositoryDocsResult = undefined; repositoryDocsPlan = undefined; repositoryDocsError = ''; updateLocation(); }303+ function openRepository(cache: CacheObservation, repo: Repository): void { active = 'Caches'; selectedCacheRef = cache.cache_ref; selectedRepoID = repo.repo_id; synchronizeMaintenanceTarget(`${cache.cache_ref}\u0000${repo.repo_id}`); repoTab = 'coverage'; searchComparison = undefined; searchError = ''; providerSmoke = undefined; repairPlan = undefined; repairReceipt = undefined; repositoryDocsResult = undefined; repositoryDocsPlan = undefined; repositoryDocsError = ''; updateLocation(); }
298 function closeRepository(): void { selectedRepoID = ''; repoTab = 'coverage'; updateLocation(); }304 function closeRepository(): void { selectedRepoID = ''; repoTab = 'coverage'; updateLocation(); }
299 function selectRepositoryTab(value: RepositoryTab): void { repoTab = value; updateLocation(); }305 function selectRepositoryTab(value: RepositoryTab): void { repoTab = value; updateLocation(); }
300 function selectDiagnosticFilter(value: 'current' | 'recovered' | 'all'): void { diagnosticsFilter = value; updateLocation(); }306 function selectDiagnosticFilter(value: 'current' | 'recovered' | 'all'): void { diagnosticsFilter = value; updateLocation(); }
@@ -422,10 +428,20 @@
422 428 
423 function ensureControlSelections(): void {429 function ensureControlSelections(): void {
424 const targets = snapshot.caches.flatMap((cache) => cache.repositories.map((repo) => ({ key: `${cache.cache_ref}\u0000${repo.repo_id}`, cache, repo })));430 const targets = snapshot.caches.flatMap((cache) => cache.repositories.map((repo) => ({ key: `${cache.cache_ref}\u0000${repo.repo_id}`, cache, repo })));
425- if (!targets.some((target) => target.key === maintenanceTargetKey)) {431+ let target = targets.find((target) => target.key === maintenanceTargetKey);
432+ if (!target && !maintenanceTargetKey && !requestedRegistrationID) {
426 const registration = snapshot.maintenance[0];433 const registration = snapshot.maintenance[0];
427- const target = registration ? targets.find((item) => item.cache.cache_ref === registration.cache_ref && item.repo.repo_id === registration.repo_id) : targets[0];434+ target = registration ? targets.find((item) => item.cache.cache_ref === registration.cache_ref && item.repo.repo_id === registration.repo_id) : targets[0];
428- if (target) loadMaintenanceTarget(target.key, false);435+ }
436+ const registrationAvailable = !requestedRegistrationID || snapshot.maintenance.some((item) => item.registration_id === requestedRegistrationID || item.legacy_registration_ids?.includes(requestedRegistrationID));
437+ const currentRegistration = snapshot.maintenance.find((item) => `${item.cache_ref}\u0000${item.repo_id}` === maintenanceTargetKey);
438+ const confirmationReplaced = isTargetBoundControl(pendingControl) && pendingControlRegistrationID !== (currentRegistration?.registration_id || '');
439+ if (target) synchronizeMaintenanceTarget(target.key);
440+ if (!target || !registrationAvailable || confirmationReplaced) {
441+ // An explicit target can disappear during refresh. Keep its identity,
442+ // but discard reviewed work instead of selecting a different repository.
443+ cancelTargetConfirmation();
444+ invalidateMaintenancePlan();
429 }445 }
430 if (!bindingIntent.cache_ref && snapshot.caches[0]) bindingIntent = { ...bindingIntent, cache_ref: snapshot.caches[0].cache_ref };446 if (!bindingIntent.cache_ref && snapshot.caches[0]) bindingIntent = { ...bindingIntent, cache_ref: snapshot.caches[0].cache_ref };
431 }447 }
@@ -439,15 +455,27 @@
439 const nextTargetKey = `${registration.cache_ref}\u0000${registration.repo_id}`;455 const nextTargetKey = `${registration.cache_ref}\u0000${registration.repo_id}`;
440 requestedRegistrationID = registration.registration_id;456 requestedRegistrationID = registration.registration_id;
441 registrationRedirectNotice = previous === registration.registration_id ? '' : `Redirected legacy registration ${previous} to canonical ${registration.registration_id}.`;457 registrationRedirectNotice = previous === registration.registration_id ? '' : `Redirected legacy registration ${previous} to canonical ${registration.registration_id}.`;
442- if (maintenanceTargetKey !== nextTargetKey) loadMaintenanceTarget(nextTargetKey, false);458+ synchronizeMaintenanceTarget(nextTargetKey);
443 if (previous !== requestedRegistrationID) updateLocation(true);459 if (previous !== requestedRegistrationID) updateLocation(true);
444 }460 }
445 461 
462+ function maintenanceIntentMatches(key: string): boolean {
463+ return `${maintenanceIntent.cache_ref}\u0000${maintenanceIntent.repo_id}` === key;
464+ }
465+ 
466+ function synchronizeMaintenanceTarget(key: string): void {
467+ // Selection and submitted identity move together. A same-target refresh
468+ // must not overwrite the operator's unsaved policy edits or reviewed plan.
469+ if (maintenanceTargetKey !== key || !maintenanceIntentMatches(key)) loadMaintenanceTarget(key, false);
470+ }
471+ 
446 function loadMaintenanceTarget(key: string, updateDeepLink = true): void {472 function loadMaintenanceTarget(key: string, updateDeepLink = true): void {
473+ cancelTargetConfirmation();
447 maintenanceTargetKey = key;474 maintenanceTargetKey = key;
448 const [cacheRef, repoID] = key.split('\u0000');475 const [cacheRef, repoID] = key.split('\u0000');
449 const registration = snapshot.maintenance.find((item) => item.cache_ref === cacheRef && item.repo_id === repoID);476 const registration = snapshot.maintenance.find((item) => item.cache_ref === cacheRef && item.repo_id === repoID);
450 const repo = snapshot.caches.find((item) => item.cache_ref === cacheRef)?.repositories.find((item) => item.repo_id === repoID);477 const repo = snapshot.caches.find((item) => item.cache_ref === cacheRef)?.repositories.find((item) => item.repo_id === repoID);
478+ if (active === 'Maintenance') { selectedCacheRef = cacheRef; selectedRepoID = repoID; }
451 maintenanceIntent = {479 maintenanceIntent = {
452 cache_ref: cacheRef, repo_id: repoID,480 cache_ref: cacheRef, repo_id: repoID,
453 sync_mode: registration?.policy.sync_enabled === false ? 'off' : registration?.policy.sync_mode || 'head-and-backfill',481 sync_mode: registration?.policy.sync_enabled === false ? 'off' : registration?.policy.sync_mode || 'head-and-backfill',
@@ -456,10 +484,10 @@
456 head_interval_seconds: registration?.policy.head_interval_seconds || 0, rag_interval_seconds: registration?.policy.rag_interval_seconds || 0,484 head_interval_seconds: registration?.policy.head_interval_seconds || 0, rag_interval_seconds: registration?.policy.rag_interval_seconds || 0,
457 head_max_pages: registration?.policy.head_max_pages || 0, tail_slice_pages: registration?.policy.tail_slice_pages || 0, per_page: registration?.policy.per_page || 0485 head_max_pages: registration?.policy.head_max_pages || 0, tail_slice_pages: registration?.policy.tail_slice_pages || 0, per_page: registration?.policy.per_page || 0
458 };486 };
459- maintenancePlan = undefined; maintenanceReceipt = undefined; maintenanceError = ''; maintenanceFailure = undefined;487+ invalidateMaintenancePlan();
460 selectedConflictCandidateRef = ''; conflictResolutionPlan = undefined; conflictResolutionReceipt = undefined; conflictResolutionError = '';488 selectedConflictCandidateRef = ''; conflictResolutionPlan = undefined; conflictResolutionReceipt = undefined; conflictResolutionError = '';
461- if (updateDeepLink && registration) {489+ if (updateDeepLink) {
462- requestedRegistrationID = registration.registration_id;490+ requestedRegistrationID = registration?.registration_id || '';
463 registrationRedirectNotice = '';491 registrationRedirectNotice = '';
464 updateLocation();492 updateLocation();
465 }493 }
@@ -473,9 +501,21 @@
473 }501 }
474 502 
475 function invalidateMaintenancePlan(): void {503 function invalidateMaintenancePlan(): void {
504+ maintenanceIntentGeneration++;
476 maintenancePlan = undefined; maintenanceReceipt = undefined; maintenanceError = ''; maintenanceFailure = undefined;505 maintenancePlan = undefined; maintenanceReceipt = undefined; maintenanceError = ''; maintenanceFailure = undefined;
477 }506 }
478 507 
508+ function isTargetBoundControl(kind: typeof pendingControl): boolean {
509+ return kind === 'maintenance_apply' || kind === 'disable' || kind === 'reconcile' || kind === 'repository_docs_index' || kind === 'rag_repair_apply';
510+ }
511+ 
512+ function cancelTargetConfirmation(): void {
513+ // Conflict resolution already uses its immutable reviewed registration and
514+ // must retain a lost-response retry key even when SSE replaces that row.
515+ if (!isTargetBoundControl(pendingControl)) return;
516+ controlDialog?.close(); pendingControl = ''; pendingControlKey = ''; pendingControlRegistrationID = '';
517+ }
518+ 
479 function invalidateBindingPlan(): void {519 function invalidateBindingPlan(): void {
480 bindingPlan = undefined; bindingReceipt = undefined; bindingError = '';520 bindingPlan = undefined; bindingReceipt = undefined; bindingError = '';
481 }521 }
@@ -499,9 +539,18 @@
499 539 
500 async function renderMaintenancePlan(): Promise<void> {540 async function renderMaintenancePlan(): Promise<void> {
501 if (!csrfToken || !maintenanceControlsEnabled) return;541 if (!csrfToken || !maintenanceControlsEnabled) return;
542+ if (!maintenanceIntentMatches(maintenanceTargetKey) || !maintenanceTargetAvailable) {
543+ maintenanceError = 'Select a current repository in the managed cache.';
544+ return;
545+ }
546+ const generation = maintenanceIntentGeneration;
502 controlRunning = true; maintenanceError = ''; maintenanceFailure = undefined; maintenanceReceipt = undefined;547 controlRunning = true; maintenanceError = ''; maintenanceFailure = undefined; maintenanceReceipt = undefined;
503- try { maintenancePlan = await controlPost<MaintenancePlan>('/api/admin/v1/maintenance/plan', maintenanceIntent); }548+ try {
549+ const plan = await controlPost<MaintenancePlan>('/api/admin/v1/maintenance/plan', maintenanceIntent);
550+ if (generation === maintenanceIntentGeneration) maintenancePlan = plan;
551+ }
504 catch (value) {552 catch (value) {
553+ if (generation !== maintenanceIntentGeneration) return;
505 maintenancePlan = undefined;554 maintenancePlan = undefined;
506 maintenanceFailure = value instanceof Error && 'failure' in value ? (value as Error & { failure: ControlFailure }).failure : undefined;555 maintenanceFailure = value instanceof Error && 'failure' in value ? (value as Error & { failure: ControlFailure }).failure : undefined;
507 maintenanceError = maintenanceFailure?.message || (value instanceof Error ? value.message : 'Maintenance planning failed.');556 maintenanceError = maintenanceFailure?.message || (value instanceof Error ? value.message : 'Maintenance planning failed.');
@@ -576,26 +625,37 @@
576 }625 }
577 626 
578 async function confirmControl(kind: typeof pendingControl, trigger: HTMLButtonElement): Promise<void> {627 async function confirmControl(kind: typeof pendingControl, trigger: HTMLButtonElement): Promise<void> {
628+ if (isTargetBoundControl(kind) && !maintenanceTargetAvailable) return;
579 controlTriggerButton = trigger; pendingControl = kind; pendingControlKey = `admin-${kind}-${crypto.randomUUID()}`;629 controlTriggerButton = trigger; pendingControl = kind; pendingControlKey = `admin-${kind}-${crypto.randomUUID()}`;
630+ pendingControlRegistrationID = isTargetBoundControl(kind) ? selectedMaintenance?.registration_id || '' : '';
580 maintenanceError = ''; maintenanceFailure = undefined; bindingError = ''; await tick(); controlDialog?.showModal(); controlConfirmButton?.focus();631 maintenanceError = ''; maintenanceFailure = undefined; bindingError = ''; await tick(); controlDialog?.showModal(); controlConfirmButton?.focus();
581 }632 }
582 633 
583 async function cancelControlConfirmation(): Promise<void> {634 async function cancelControlConfirmation(): Promise<void> {
584- controlDialog?.close(); pendingControl = ''; pendingControlKey = ''; await tick(); controlTriggerButton?.focus();635+ controlDialog?.close(); pendingControl = ''; pendingControlKey = ''; pendingControlRegistrationID = ''; await tick(); controlTriggerButton?.focus();
585 }636 }
586 637 
587 async function executeControl(): Promise<void> {638 async function executeControl(): Promise<void> {
588 if (!pendingControl || !csrfToken) return;639 if (!pendingControl || !csrfToken) return;
640+ if (isTargetBoundControl(pendingControl) && (!maintenanceTargetAvailable || pendingControlRegistrationID !== (selectedMaintenance?.registration_id || ''))) {
641+ cancelTargetConfirmation(); invalidateMaintenancePlan(); return;
642+ }
643+ const kind = pendingControl;
644+ const generation = maintenanceIntentGeneration;
589 controlRunning = true;645 controlRunning = true;
590 if (pendingControl === 'conflict_resolution_apply') conflictResolutionError = '';646 if (pendingControl === 'conflict_resolution_apply') conflictResolutionError = '';
591 let retainedConflictReceipt: ControlReceipt | undefined;647 let retainedConflictReceipt: ControlReceipt | undefined;
592 try {648 try {
593 if (pendingControl === 'maintenance_apply' && maintenancePlan) {649 if (pendingControl === 'maintenance_apply' && maintenancePlan) {
594- maintenanceReceipt = await controlPost<ControlReceipt>('/api/admin/v1/maintenance/apply', { ...maintenanceIntent, plan_id: maintenancePlan.plan_id, idempotency_key: pendingControlKey });650+ const receipt = await controlPost<ControlReceipt>('/api/admin/v1/maintenance/apply', { ...maintenanceIntent, plan_id: maintenancePlan.plan_id, idempotency_key: pendingControlKey });
651+ if (generation !== maintenanceIntentGeneration) return;
652+ maintenanceReceipt = receipt;
595 } else if (pendingControl === 'binding_apply' && bindingPlan) {653 } else if (pendingControl === 'binding_apply' && bindingPlan) {
596 bindingReceipt = await controlPost<ControlReceipt>('/api/admin/v1/bindings/apply', { ...bindingIntent, plan_id: bindingPlan.plan_id, idempotency_key: pendingControlKey });654 bindingReceipt = await controlPost<ControlReceipt>('/api/admin/v1/bindings/apply', { ...bindingIntent, plan_id: bindingPlan.plan_id, idempotency_key: pendingControlKey });
597 } else if (pendingControl === 'rag_repair_apply' && repairPlan && selectedCache && selectedRepo) {655 } else if (pendingControl === 'rag_repair_apply' && repairPlan && selectedCache && selectedRepo) {
598- repairReceipt = await controlPost<ControlReceipt>('/api/admin/v1/rag/repair/apply', { cache_ref: selectedCache.cache_ref, repo_id: selectedRepo.repo_id, profile: repairProfile, max_chunks: repairMaxChunks, plan_id: repairPlan.plan_id, idempotency_key: pendingControlKey });656+ const receipt = await controlPost<ControlReceipt>('/api/admin/v1/rag/repair/apply', { cache_ref: selectedCache.cache_ref, repo_id: selectedRepo.repo_id, profile: repairProfile, max_chunks: repairMaxChunks, plan_id: repairPlan.plan_id, idempotency_key: pendingControlKey });
657+ if (generation !== maintenanceIntentGeneration) return;
658+ repairReceipt = receipt;
599 } else if (pendingControl === 'conflict_resolution_apply' && conflictResolutionPlan) {659 } else if (pendingControl === 'conflict_resolution_apply' && conflictResolutionPlan) {
600 // The reviewed registration is part of the durable intent. A clone660 // The reviewed registration is part of the durable intent. A clone
601 // resolution can replace the selected row through SSE before a lost661 // resolution can replace the selected row through SSE before a lost
@@ -603,14 +663,19 @@
603 conflictResolutionReceipt = await controlPost<ControlReceipt>(`/api/admin/v1/maintenance/${encodeURIComponent(conflictResolutionPlan.registration_id)}/conflict-resolution/apply`, { candidate_ref: conflictResolutionPlan.selected.candidate_ref, expected_generation: conflictResolutionPlan.expected_generation, plan_id: conflictResolutionPlan.plan_id, idempotency_key: pendingControlKey });663 conflictResolutionReceipt = await controlPost<ControlReceipt>(`/api/admin/v1/maintenance/${encodeURIComponent(conflictResolutionPlan.registration_id)}/conflict-resolution/apply`, { candidate_ref: conflictResolutionPlan.selected.candidate_ref, expected_generation: conflictResolutionPlan.expected_generation, plan_id: conflictResolutionPlan.plan_id, idempotency_key: pendingControlKey });
604 retainedConflictReceipt = conflictResolutionReceipt;664 retainedConflictReceipt = conflictResolutionReceipt;
605 } else if ((pendingControl === 'disable' || pendingControl === 'reconcile') && selectedMaintenance) {665 } else if ((pendingControl === 'disable' || pendingControl === 'reconcile') && selectedMaintenance) {
606- maintenanceReceipt = await controlPost<ControlReceipt>(`/api/admin/v1/maintenance/${encodeURIComponent(selectedMaintenance.registration_id)}/${pendingControl}`, { idempotency_key: pendingControlKey });666+ const receipt = await controlPost<ControlReceipt>(`/api/admin/v1/maintenance/${encodeURIComponent(selectedMaintenance.registration_id)}/${pendingControl}`, { idempotency_key: pendingControlKey });
667+ if (generation !== maintenanceIntentGeneration) return;
668+ maintenanceReceipt = receipt;
607 } else if (pendingControl === 'repository_docs_index' && selectedMaintenance) {669 } else if (pendingControl === 'repository_docs_index' && selectedMaintenance) {
608- maintenanceReceipt = await controlPost<ControlReceipt>(`/api/admin/v1/repository-docs/${encodeURIComponent(selectedMaintenance.registration_id)}/index`, { idempotency_key: pendingControlKey, ...repositoryDocsSelector() });670+ const receipt = await controlPost<ControlReceipt>(`/api/admin/v1/repository-docs/${encodeURIComponent(selectedMaintenance.registration_id)}/index`, { idempotency_key: pendingControlKey, ...repositoryDocsSelector() });
671+ if (generation !== maintenanceIntentGeneration) return;
672+ maintenanceReceipt = receipt;
609 }673 }
610 controlDialog?.close(); pendingControl = ''; pendingControlKey = ''; await refresh();674 controlDialog?.close(); pendingControl = ''; pendingControlKey = ''; await refresh();
611 if (retainedConflictReceipt) conflictResolutionReceipt = retainedConflictReceipt;675 if (retainedConflictReceipt) conflictResolutionReceipt = retainedConflictReceipt;
612 await tick(); controlTriggerButton?.focus();676 await tick(); controlTriggerButton?.focus();
613 } catch (value) {677 } catch (value) {
678+ if (isTargetBoundControl(kind) && generation !== maintenanceIntentGeneration) return;
614 const message = value instanceof Error ? value.message : 'The confirmed control failed.';679 const message = value instanceof Error ? value.message : 'The confirmed control failed.';
615 const failure = value instanceof Error && 'failure' in value ? (value as Error & { failure: ControlFailure }).failure : undefined;680 const failure = value instanceof Error && 'failure' in value ? (value as Error & { failure: ControlFailure }).failure : undefined;
616 if (pendingControl !== 'binding_apply' && pendingControl !== 'rag_repair_apply' && pendingControl !== 'conflict_resolution_apply') maintenanceFailure = failure;681 if (pendingControl !== 'binding_apply' && pendingControl !== 'rag_repair_apply' && pendingControl !== 'conflict_resolution_apply') maintenanceFailure = failure;
@@ -631,7 +696,12 @@
631 } finally { controlRunning = false; }696 } finally { controlRunning = false; }
632 }697 }
633 function selectTheme(value: Theme): void { theme = value; applyTheme(value); }698 function selectTheme(value: Theme): void { theme = value; applyTheme(value); }
634- function onPopState(): void { hydrateLocation(); }699+ function onPopState(): void {
700+ hydrateLocation();
701+ if (selectedCacheRef && selectedRepoID) synchronizeMaintenanceTarget(`${selectedCacheRef}\u0000${selectedRepoID}`);
702+ ensureControlSelections();
703+ canonicalizeRegistrationDeepLink();
704+ }
635 705 
636 onMount(async () => {706 onMount(async () => {
637 theme = normalizeTheme(localStorage.getItem(themeStorageKey)); applyTheme(theme); hydrateLocation(); window.addEventListener('popstate', onPopState);707 theme = normalizeTheme(localStorage.getItem(themeStorageKey)); applyTheme(theme); hydrateLocation(); window.addEventListener('popstate', onPopState);
@@ -911,7 +981,8 @@
911 <div class="control-heading"><div><span class="large-icon"><SlidersHorizontal size={22} /></span><div><p class="section-kicker">PLAN → CONFIRM → APPLY</p><h2 id="policy-editor-title">Maintenance policy</h2><p>Change a managed repository policy, review the effect ledger, then confirm the exact plan id.</p></div></div><StatusChip value={maintenancePlan?.status || selectedMaintenance?.state || 'not_planned'} label={maintenancePlan ? humanize(maintenancePlan.status) : selectedMaintenance ? humanize(selectedMaintenance.state) : 'Not planned'} /></div>981 <div class="control-heading"><div><span class="large-icon"><SlidersHorizontal size={22} /></span><div><p class="section-kicker">PLAN → CONFIRM → APPLY</p><h2 id="policy-editor-title">Maintenance policy</h2><p>Change a managed repository policy, review the effect ledger, then confirm the exact plan id.</p></div></div><StatusChip value={maintenancePlan?.status || selectedMaintenance?.state || 'not_planned'} label={maintenancePlan ? humanize(maintenancePlan.status) : selectedMaintenance ? humanize(selectedMaintenance.state) : 'Not planned'} /></div>
912 {#if repoTargets.length === 0}<div class="empty-state"><Database size={23} /><h3>No bound repositories</h3><p>Add a repository binding below before planning maintenance.</p></div>{:else}982 {#if repoTargets.length === 0}<div class="empty-state"><Database size={23} /><h3>No bound repositories</h3><p>Add a repository binding below before planning maintenance.</p></div>{:else}
913 <form class="control-form" oninput={invalidateMaintenancePlan} onsubmit={(event) => { event.preventDefault(); void renderMaintenancePlan(); }}>983 <form class="control-form" oninput={invalidateMaintenancePlan} onsubmit={(event) => { event.preventDefault(); void renderMaintenancePlan(); }}>
914- <label class="span-two"><span>Managed target</span><select value={maintenanceTargetKey} onchange={(event) => loadMaintenanceTarget(event.currentTarget.value)}>{#each repoTargets as target}<option value={target.key}>{target.repo.repo_id} · {target.cache.cache_ref}</option>{/each}</select><small>Opaque cache identity only; no filesystem path crosses the browser boundary.</small></label>984+ <label class="span-two"><span>Managed target</span><select value={maintenanceTargetKey} onchange={(event) => loadMaintenanceTarget(event.currentTarget.value)}>{#if !maintenanceTargetAvailable}<option value={maintenanceTargetKey} disabled>{maintenanceIntent.repo_id || requestedRegistrationID || 'No target selected'} · unavailable</option>{/if}{#each repoTargets as target}<option value={target.key}>{target.repo.repo_id} · {target.cache.cache_ref}</option>{/each}</select><small>Opaque cache identity only; no filesystem path crosses the browser boundary.</small></label>
985+ {#if !maintenanceTargetAvailable}<div class="span-two" role="alert">Selected maintenance target is unavailable. Select a current repository in the managed cache.</div>{/if}
915 <label><span>Sync mode</span><select bind:value={maintenanceIntent.sync_mode}><option value="off">Off</option><option value="head">Head only</option><option value="head-and-backfill">Head + bounded backfill</option></select></label>986 <label><span>Sync mode</span><select bind:value={maintenanceIntent.sync_mode}><option value="off">Off</option><option value="head">Head only</option><option value="head-and-backfill">Head + bounded backfill</option></select></label>
916 <label><span>RAG mode</span><select bind:value={maintenanceIntent.rag_mode}><option value="off">Off</option><option value="maintain">Maintain index</option></select></label>987 <label><span>RAG mode</span><select bind:value={maintenanceIntent.rag_mode}><option value="off">Off</option><option value="maintain">Maintain index</option></select></label>
917 <fieldset class="span-two collection-field"><legend>Collections</legend>{#each ['issues', 'issue-comments', 'wiki', 'pulls', 'pr-comments'] as collection}<label><input type="checkbox" checked={maintenanceIntent.collections.includes(collection)} onchange={(event) => toggleCollection(collection, event.currentTarget.checked)} />{humanize(collection)}</label>{/each}</fieldset>988 <fieldset class="span-two collection-field"><legend>Collections</legend>{#each ['issues', 'issue-comments', 'wiki', 'pulls', 'pr-comments'] as collection}<label><input type="checkbox" checked={maintenanceIntent.collections.includes(collection)} onchange={(event) => toggleCollection(collection, event.currentTarget.checked)} />{humanize(collection)}</label>{/each}</fieldset>
@@ -921,14 +992,14 @@
921 <label><span>Head pages</span><input type="number" min="0" max="1000" bind:value={maintenanceIntent.head_max_pages} /></label>992 <label><span>Head pages</span><input type="number" min="0" max="1000" bind:value={maintenanceIntent.head_max_pages} /></label>
922 <label><span>Tail slice</span><input type="number" min="0" max="1000" bind:value={maintenanceIntent.tail_slice_pages} /></label>993 <label><span>Tail slice</span><input type="number" min="0" max="1000" bind:value={maintenanceIntent.tail_slice_pages} /></label>
923 <label><span>Per page</span><input type="number" min="0" max="100" bind:value={maintenanceIntent.per_page} /></label>994 <label><span>Per page</span><input type="number" min="0" max="100" bind:value={maintenanceIntent.per_page} /></label>
924- <div class="form-actions span-two"><span>{maintenanceControlsEnabled ? 'Capability available in this daemon.' : 'Capability registry does not expose maintenance controls.'}</span><button class="primary-action" type="submit" disabled={!csrfToken || !maintenanceControlsEnabled || !!selectedMaintenance?.identity_conflict || controlRunning}><FileCheck2 size={16} />{controlRunning ? 'Planning…' : 'Render plan'}</button></div>995+ <div class="form-actions span-two"><span>{maintenanceControlsEnabled ? 'Capability available in this daemon.' : 'Capability registry does not expose maintenance controls.'}</span><button class="primary-action" type="submit" disabled={!csrfToken || !maintenanceControlsEnabled || !maintenanceTargetAvailable || !!selectedMaintenance?.identity_conflict || controlRunning}><FileCheck2 size={16} />{controlRunning ? 'Planning…' : 'Render plan'}</button></div>
925 </form>996 </form>
926 {/if}997 {/if}
927 998 
928 {#if maintenanceError}<div class="action-result error" role="alert"><AlertTriangle size={17} /><div><strong>Maintenance control failed{maintenanceFailure?.field ? ` · ${humanize(maintenanceFailure.field)}` : ''}</strong><span>{maintenanceError}</span>{#if maintenanceFailure?.remediation}<span>{maintenanceFailure.remediation}</span>{/if}{#if maintenanceFailure?.blockers?.length}<ul class="blocker-list">{#each maintenanceFailure.blockers as blocker}<li>{blocker}</li>{/each}</ul>{/if}{#if maintenanceFailure?.cli_handoff}<code>{maintenanceFailure.cli_handoff}</code>{/if}</div></div>{/if}999 {#if maintenanceError}<div class="action-result error" role="alert"><AlertTriangle size={17} /><div><strong>Maintenance control failed{maintenanceFailure?.field ? ` · ${humanize(maintenanceFailure.field)}` : ''}</strong><span>{maintenanceError}</span>{#if maintenanceFailure?.remediation}<span>{maintenanceFailure.remediation}</span>{/if}{#if maintenanceFailure?.blockers?.length}<ul class="blocker-list">{#each maintenanceFailure.blockers as blocker}<li>{blocker}</li>{/each}</ul>{/if}{#if maintenanceFailure?.cli_handoff}<code>{maintenanceFailure.cli_handoff}</code>{/if}</div></div>{/if}
929 {#if maintenancePlan}<div class="plan-panel"><div class="plan-summary"><div><p class="section-kicker">REVIEWED INTENT</p><h3>{maintenancePlan.repo_id}</h3><code>{maintenancePlan.plan_id}</code></div><StatusChip value={maintenancePlan.status} /></div>{#if maintenancePlan.blockers?.length}<ul class="blocker-list">{#each maintenancePlan.blockers as blocker}<li><AlertTriangle size={15} />{blocker}</li>{/each}</ul>{/if}<div class="effect-ledger">{#each maintenancePlan.actions as effect}<article><span class="effect-icon"><Zap size={15} /></span><div><strong>{effect.summary}</strong><small>{humanize(effect.class)}{effect.data_boundary ? ` · ${humanize(effect.data_boundary)}` : ''}</small>{#if effect.handoff}<code>{effect.handoff}</code>{/if}</div><StatusChip value={effect.status} /></article>{/each}</div><div class="plan-footer"><div><strong>Next safe action</strong><span>{maintenancePlan.next_action || 'Confirm this exact plan.'}</span></div><button class="primary-action" disabled={maintenancePlan.status === 'blocked' || controlRunning} onclick={(event) => void confirmControl('maintenance_apply', event.currentTarget)}><Power size={16} />Confirm & apply</button></div></div>{/if}1000 {#if maintenancePlan}<div class="plan-panel"><div class="plan-summary"><div><p class="section-kicker">REVIEWED INTENT</p><h3>{maintenancePlan.repo_id}</h3><code>{maintenancePlan.plan_id}</code></div><StatusChip value={maintenancePlan.status} /></div>{#if maintenancePlan.blockers?.length}<ul class="blocker-list">{#each maintenancePlan.blockers as blocker}<li><AlertTriangle size={15} />{blocker}</li>{/each}</ul>{/if}<div class="effect-ledger">{#each maintenancePlan.actions as effect}<article><span class="effect-icon"><Zap size={15} /></span><div><strong>{effect.summary}</strong><small>{humanize(effect.class)}{effect.data_boundary ? ` · ${humanize(effect.data_boundary)}` : ''}</small>{#if effect.handoff}<code>{effect.handoff}</code>{/if}</div><StatusChip value={effect.status} /></article>{/each}</div><div class="plan-footer"><div><strong>Next safe action</strong><span>{maintenancePlan.next_action || 'Confirm this exact plan.'}</span></div><button class="primary-action" disabled={maintenancePlan.status === 'blocked' || controlRunning} onclick={(event) => void confirmControl('maintenance_apply', event.currentTarget)}><Power size={16} />Confirm & apply</button></div></div>{/if}
930 {#if maintenanceReceipt}<div class="action-result" role="status"><CheckCircle2 size={17} /><div><strong>{humanize(maintenanceReceipt.outcome || maintenanceReceipt.status || 'applied')}</strong><span>{maintenanceReceipt.receipt_id ? `Receipt ${maintenanceReceipt.receipt_id}` : maintenanceReceipt.audit_receipt ? `Audit ${maintenanceReceipt.audit_receipt}` : 'The confirmed plan was accepted.'}{maintenanceReceipt.replayed ? ' · replayed safely' : ''}{maintenanceReceipt.jobs_started?.length ? ` · jobs ${maintenanceReceipt.jobs_started.join(', ')}` : ''}</span></div></div>{/if}1001 {#if maintenanceReceipt}<div class="action-result" role="status"><CheckCircle2 size={17} /><div><strong>{humanize(maintenanceReceipt.outcome || maintenanceReceipt.status || 'applied')}</strong><span>{maintenanceReceipt.receipt_id ? `Receipt ${maintenanceReceipt.receipt_id}` : maintenanceReceipt.audit_receipt ? `Audit ${maintenanceReceipt.audit_receipt}` : 'The confirmed plan was accepted.'}{maintenanceReceipt.replayed ? ' · replayed safely' : ''}{maintenanceReceipt.jobs_started?.length ? ` · jobs ${maintenanceReceipt.jobs_started.join(', ')}` : ''}</span></div></div>{/if}
931- {#if selectedMaintenance}<div class="registration-actions"><div><strong>Registration {selectedMaintenance.registration_id}</strong><span>Generation {selectedMaintenance.generation} · {selectedMaintenance.enabled ? 'enabled' : 'disabled'} · {registrationControlsEnabled ? 'reconcile is coalesced with active work.' : 'registration controls are unavailable in this daemon.'}</span></div><button disabled={!csrfToken || !registrationControlsEnabled || !!selectedMaintenance.identity_conflict || controlRunning} onclick={(event) => void confirmControl('reconcile', event.currentTarget)}><RotateCcw size={15} />Reconcile now</button><button class="danger-action" disabled={!csrfToken || !registrationControlsEnabled || !selectedMaintenance.enabled || !!selectedMaintenance.identity_conflict || controlRunning} onclick={(event) => void confirmControl('disable', event.currentTarget)}><Power size={15} />Disable</button></div>{/if}1002+ {#if selectedMaintenance}<div class="registration-actions"><div><strong>Registration {selectedMaintenance.registration_id}</strong><span>Generation {selectedMaintenance.generation} · {selectedMaintenance.enabled ? 'enabled' : 'disabled'} · {registrationControlsEnabled ? 'reconcile is coalesced with active work.' : 'registration controls are unavailable in this daemon.'}</span></div><button disabled={!csrfToken || !registrationControlsEnabled || !maintenanceTargetAvailable || !!selectedMaintenance.identity_conflict || controlRunning} onclick={(event) => void confirmControl('reconcile', event.currentTarget)}><RotateCcw size={15} />Reconcile now</button><button class="danger-action" disabled={!csrfToken || !registrationControlsEnabled || !maintenanceTargetAvailable || !selectedMaintenance.enabled || !!selectedMaintenance.identity_conflict || controlRunning} onclick={(event) => void confirmControl('disable', event.currentTarget)}><Power size={15} />Disable</button></div>{/if}
932 </section>1003 </section>
933 1004 
934 <section class="control-workbench" aria-labelledby="binding-editor-title">1005 <section class="control-workbench" aria-labelledby="binding-editor-title">
@@ -909,6 +909,239 @@ test('active retry backoff is explicit and blocks whole and collection retry', a
909 await expect(page.getByRole('button', { name: 'Retry Issues' })).toHaveCount(0);909 await expect(page.getByRole('button', { name: 'Retry Issues' })).toHaveCount(0);
910});910});
911 911 
912+function maintenanceNavigationSnapshot(): any {
913+ const state: any = structuredClone(snapshot);
914+ const secondRepo = structuredClone(state.caches[0].repositories[0]);
915+ Object.assign(secondRepo, { repo_id: 'example/other', display_name: 'Other repository' });
916+ state.caches[0].repositories.push(secondRepo);
917+ const secondRegistration = structuredClone(state.maintenance[0]);
918+ Object.assign(secondRegistration, { registration_id: 'reg-other', repo_id: 'example/other' });
919+ secondRegistration.policy.profile = 'other-profile';
920+ state.maintenance.push(secondRegistration);
921+ const otherCache = structuredClone(snapshot.caches[0]);
922+ otherCache.cache_ref = 'cache-other';
923+ otherCache.repositories[0].display_name = 'Other cache repository';
924+ state.caches.push(otherCache);
925+ const otherCacheRegistration = structuredClone(snapshot.maintenance[0]);
926+ Object.assign(otherCacheRegistration, { registration_id: 'reg-other-cache', cache_ref: 'cache-other' });
927+ otherCacheRegistration.policy.profile = 'other-cache-profile';
928+ state.maintenance.push(otherCacheRegistration);
929+ return state;
930+}
931+ 
932+for (const entry of ['repository-list', 'repository-deep-link', 'maintenance-deep-link', 'same-repository-other-cache', 'unenrolled-repository']) {
933+ test(`maintenance target identity matches the submitted intent after ${entry} navigation`, async ({ page }) => {
934+ const state = maintenanceNavigationSnapshot();
935+ const expected = entry === 'same-repository-other-cache'
936+ ? { cache_ref: 'cache-other', repo_id: 'example/repo', profile: 'other-cache-profile' }
937+ : { cache_ref: 'cache-111111112222', repo_id: 'example/other', profile: entry === 'unenrolled-repository' ? '' : 'other-profile' };
938+ if (entry === 'unenrolled-repository') state.maintenance = state.maintenance.filter((item: any) => item.repo_id !== 'example/other');
939+ await mockAdmin(page, state);
940+ let plannedBody: Record<string, unknown> | undefined;
941+ await page.route('**/api/admin/v1/maintenance/plan', async (route) => {
942+ plannedBody = route.request().postDataJSON();
943+ await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ api_version: '1', result: {
944+ schema_version: 'gitcode-mcp.maintenance-plan.v1', plan_id: 'maintenance-plan-selected-target', status: 'blocked',
945+ repo_id: expected.repo_id, cache: { cache_ref: expected.cache_ref }, actions: [], blockers: ['Fixture inspection only.']
946+ } }) });
947+ });
948+ if (entry === 'repository-list') {
949+ await page.goto('/?view=Caches');
950+ await page.getByRole('button', { name: /Other repository/ }).click();
951+ } else if (entry === 'maintenance-deep-link') {
952+ await page.goto('/?view=Maintenance&registration=reg-other');
953+ } else {
954+ await page.goto(`/?view=Caches&cache=${expected.cache_ref}&repo=${encodeURIComponent(expected.repo_id)}`);
955+ }
956+ await page.getByRole('button', { name: 'Maintenance', exact: true }).click();
957+ await expect(page.getByLabel('Managed target')).toHaveValue(`${expected.cache_ref}\u0000${expected.repo_id}`);
958+ await page.reload();
959+ await expect(page.getByLabel('Managed target')).toHaveValue(`${expected.cache_ref}\u0000${expected.repo_id}`);
960+ await page.getByRole('button', { name: 'Render plan', exact: true }).click();
961+ await expect.poll(() => plannedBody).toMatchObject(expected);
962+ });
963+}
964+ 
965+test('maintenance target history restores identity and invalidates the previous plan', async ({ page }) => {
966+ await mockAdmin(page, maintenanceNavigationSnapshot());
967+ let plannedBody: Record<string, unknown> | undefined;
968+ await page.route('**/api/admin/v1/maintenance/plan', async (route) => {
969+ plannedBody = route.request().postDataJSON();
970+ await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ api_version: '1', result: {
971+ plan_id: 'maintenance-plan-history', status: 'blocked', actions: [], blockers: [], repo_id: plannedBody!.repo_id
972+ } }) });
973+ });
974+ await page.goto('/?view=Maintenance&registration=reg-1');
975+ const target = page.getByLabel('Managed target');
976+ await expect(target).toHaveValue('cache-111111112222\u0000example/repo');
977+ await target.selectOption('cache-111111112222\u0000example/other');
978+ await page.goBack();
979+ await expect(target).toHaveValue('cache-111111112222\u0000example/repo');
980+ await expect(page.getByLabel('RAG profile')).toHaveValue('easy-rag');
981+ await page.goForward();
982+ await expect(target).toHaveValue('cache-111111112222\u0000example/other');
983+ await page.getByRole('button', { name: 'Render plan', exact: true }).click();
984+ await expect.poll(() => plannedBody).toMatchObject({ cache_ref: 'cache-111111112222', repo_id: 'example/other', profile: 'other-profile' });
985+ await expect(page.getByText('maintenance-plan-history')).toBeVisible();
986+ await page.goBack();
987+ await expect(target).toHaveValue('cache-111111112222\u0000example/repo');
988+ await expect(page.getByText('maintenance-plan-history')).toHaveCount(0);
989+});
990+ 
991+for (const action of ['Disable', 'Reconcile now']) {
992+ test(`maintenance target history cancels a pending ${action} confirmation`, async ({ page }) => {
993+ await mockAdmin(page, maintenanceNavigationSnapshot());
994+ const writes: string[] = [];
995+ await page.route('**/api/admin/v1/maintenance/*/disable', async (route) => { writes.push(route.request().url()); await route.abort(); });
996+ await page.route('**/api/admin/v1/maintenance/*/reconcile', async (route) => { writes.push(route.request().url()); await route.abort(); });
997+ await page.goto('/?view=Maintenance&registration=reg-1');
998+ await page.getByLabel('Managed target').selectOption('cache-111111112222\u0000example/other');
999+ await page.getByRole('button', { name: action, exact: true }).click();
1000+ await expect(page.getByRole('dialog')).toContainText('example/other');
1001+ await page.goBack();
1002+ await expect(page.getByLabel('Managed target')).toHaveValue('cache-111111112222\u0000example/repo');
1003+ await expect(page.getByRole('dialog')).toHaveCount(0);
1004+ await page.goForward();
1005+ await expect(page.getByRole('dialog')).toHaveCount(0);
1006+ expect(writes).toEqual([]);
1007+ });
1008+}
1009+ 
1010+for (const explicitRegistration of [true, false]) {
1011+ test(`maintenance registration replacement cancels confirmation with explicit registration ${explicitRegistration}`, async ({ page }) => {
1012+ const state = maintenanceNavigationSnapshot();
1013+ let emitSnapshotChanged!: () => void;
1014+ const snapshotChanged = new Promise<void>((resolve) => { emitSnapshotChanged = resolve; });
1015+ await mockAdmin(page, state, snapshotChanged);
1016+ const writes: string[] = [];
1017+ await page.route('**/api/admin/v1/maintenance/*/disable', async (route) => { writes.push(route.request().url()); await route.abort(); });
1018+ await page.goto(explicitRegistration ? '/?view=Maintenance&registration=reg-1' : '/?view=Maintenance');
1019+ await page.getByRole('button', { name: 'Disable', exact: true }).click();
1020+ await expect(page.getByRole('dialog')).toContainText('reg-1');
1021+ state.maintenance[0].registration_id = 'reg-replacement';
1022+ const refreshed = page.waitForResponse('**/api/admin/v1/snapshot');
1023+ emitSnapshotChanged();
1024+ await refreshed;
1025+ await expect(page.getByRole('dialog')).toHaveCount(0);
1026+ expect(writes).toEqual([]);
1027+ if (explicitRegistration) {
1028+ await expect(page.getByText('Selected maintenance target is unavailable.')).toBeVisible();
1029+ await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeDisabled();
1030+ await expect(page.getByRole('button', { name: 'Reconcile now', exact: true })).toBeDisabled();
1031+ await page.getByLabel('Managed target').selectOption('cache-111111112222\u0000example/other');
1032+ await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeEnabled();
1033+ }
1034+ });
1035+}
1036+ 
1037+for (const outcome of ['success', 'failure']) {
1038+ test(`maintenance ignores a late control ${outcome} after history changes target`, async ({ page }) => {
1039+ await mockAdmin(page, maintenanceNavigationSnapshot());
1040+ let release!: () => void;
1041+ const gate = new Promise<void>((resolve) => { release = resolve; });
1042+ const requests: string[] = [];
1043+ await page.route('**/api/admin/v1/maintenance/*/reconcile', async (route) => {
1044+ requests.push(route.request().url());
1045+ await gate;
1046+ await route.fulfill({ status: outcome === 'success' ? 200 : 422, contentType: 'application/json', body: JSON.stringify(outcome === 'success'
1047+ ? { api_version: '1', result: { outcome: 'reconciled', receipt_id: 'obsolete-control-receipt' } }
1048+ : { error: { code: 'stale_registration', message: 'Obsolete control failure.' } }) });
1049+ });
1050+ await page.goto('/?view=Maintenance&registration=reg-1');
1051+ await page.getByLabel('Managed target').selectOption('cache-111111112222\u0000example/other');
1052+ await page.getByRole('button', { name: 'Reconcile now', exact: true }).click();
1053+ const submitted = page.waitForRequest('**/api/admin/v1/maintenance/*/reconcile');
1054+ await page.getByRole('button', { name: 'Confirm action', exact: true }).click();
1055+ await submitted;
1056+ await page.goBack();
1057+ await expect(page.getByLabel('Managed target')).toHaveValue('cache-111111112222\u0000example/repo');
1058+ const response = page.waitForResponse('**/api/admin/v1/maintenance/*/reconcile');
1059+ release();
1060+ await response;
1061+ await expect(page.getByRole('button', { name: 'Render plan', exact: true })).toBeEnabled();
1062+ await expect(page.getByRole('dialog')).toHaveCount(0);
1063+ await expect(page.getByText(/obsolete-control-receipt|Obsolete control failure/)).toHaveCount(0);
1064+ expect(requests).toEqual(['http://127.0.0.1:4173/api/admin/v1/maintenance/reg-other/reconcile']);
1065+ });
1066+}
1067+ 
1068+for (const missing of ['repository', 'cache', 'registration', 'removed-repository']) {
1069+ test(`maintenance preserves a missing explicit ${missing} instead of retargeting`, async ({ page }) => {
1070+ const state = maintenanceNavigationSnapshot();
1071+ await mockAdmin(page, state);
1072+ const requests: unknown[] = [];
1073+ await page.route('**/api/admin/v1/maintenance/plan', async (route) => { requests.push(route.request().postDataJSON()); await route.abort(); });
1074+ const key = missing === 'cache' ? 'cache-missing\u0000example/repo' : missing === 'registration' ? '' : `cache-111111112222\u0000example/${missing === 'removed-repository' ? 'other' : 'missing'}`;
1075+ await page.goto(missing === 'registration' ? '/?view=Maintenance&registration=reg-missing' : `/?view=Maintenance&cache=${key.split('\u0000')[0]}&repo=${encodeURIComponent(key.split('\u0000')[1])}`);
1076+ if (missing === 'removed-repository') {
1077+ await expect(page.getByLabel('Managed target')).toHaveValue(key);
1078+ state.caches[0].repositories = state.caches[0].repositories.filter((repo: any) => repo.repo_id !== 'example/other');
1079+ state.maintenance = state.maintenance.filter((item: any) => item.registration_id !== 'reg-other');
1080+ const refreshed = page.waitForResponse('**/api/admin/v1/snapshot');
1081+ await page.getByRole('button', { name: 'Refresh snapshot', exact: true }).first().click();
1082+ await refreshed;
1083+ }
1084+ await expect(page.getByLabel('Managed target')).toHaveValue(key);
1085+ await expect(page.getByText('Selected maintenance target is unavailable.')).toBeVisible();
1086+ await expect(page.getByRole('button', { name: 'Render plan', exact: true })).toBeDisabled();
1087+ expect(requests).toEqual([]);
1088+ await page.getByLabel('Managed target').selectOption('cache-111111112222\u0000example/repo');
1089+ await expect(page.getByRole('button', { name: 'Render plan', exact: true })).toBeEnabled();
1090+ await expect(page.getByText('Selected maintenance target is unavailable.')).toHaveCount(0);
1091+ });
1092+}
1093+ 
1094+test('maintenance same-target refresh preserves unsaved policy edits', async ({ page }) => {
1095+ await mockAdmin(page, maintenanceNavigationSnapshot());
1096+ let plannedBody: Record<string, unknown> | undefined;
1097+ await page.route('**/api/admin/v1/maintenance/plan', async (route) => {
1098+ plannedBody = route.request().postDataJSON();
1099+ await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ api_version: '1', result: {
1100+ plan_id: 'maintenance-plan-edited', status: 'blocked', actions: [], blockers: []
1101+ } }) });
1102+ });
1103+ await page.goto('/?view=Maintenance&registration=reg-other');
1104+ await page.getByLabel('RAG profile').fill('draft-profile');
1105+ await page.getByLabel('Head pages').fill('7');
1106+ await page.getByRole('button', { name: 'Overview', exact: true }).click();
1107+ const refreshed = page.waitForResponse('**/api/admin/v1/snapshot');
1108+ await page.getByRole('button', { name: 'Refresh snapshot', exact: true }).first().click();
1109+ await refreshed;
1110+ await page.getByRole('button', { name: 'Maintenance', exact: true }).click();
1111+ await expect(page.getByLabel('Managed target')).toHaveValue('cache-111111112222\u0000example/other');
1112+ await expect(page.getByLabel('RAG profile')).toHaveValue('draft-profile');
1113+ await expect(page.getByLabel('Head pages')).toHaveValue('7');
1114+ await page.getByRole('button', { name: 'Render plan', exact: true }).click();
1115+ await expect.poll(() => plannedBody).toMatchObject({ cache_ref: 'cache-111111112222', repo_id: 'example/other', profile: 'draft-profile', head_max_pages: 7 });
1116+});
1117+ 
1118+for (const outcome of ['success', 'failure']) {
1119+ for (const change of ['target', 'policy']) {
1120+ test(`maintenance ignores a late ${outcome} after ${change} changes`, async ({ page }) => {
1121+ await mockAdmin(page, maintenanceNavigationSnapshot());
1122+ let release!: () => void;
1123+ const gate = new Promise<void>((resolve) => { release = resolve; });
1124+ await page.route('**/api/admin/v1/maintenance/plan', async (route) => {
1125+ await gate;
1126+ await route.fulfill({ status: outcome === 'failure' ? 422 : 200, contentType: 'application/json', body: JSON.stringify(outcome === 'failure'
1127+ ? { error: { code: 'invalid_policy', message: 'Obsolete target error.' } }
1128+ : { api_version: '1', result: { plan_id: 'maintenance-plan-obsolete', status: 'blocked', actions: [], blockers: [] } }) });
1129+ });
1130+ await page.goto('/?view=Maintenance');
1131+ const submitted = page.waitForRequest('**/api/admin/v1/maintenance/plan');
1132+ await page.getByRole('button', { name: 'Render plan', exact: true }).click();
1133+ await submitted;
1134+ if (change === 'target') await page.getByLabel('Managed target').selectOption('cache-111111112222\u0000example/other');
1135+ else await page.getByLabel('RAG profile').fill('edited-while-planning');
1136+ release();
1137+ await expect(page.getByRole('button', { name: 'Render plan', exact: true })).toBeEnabled();
1138+ await expect(page.getByText('maintenance-plan-obsolete')).toHaveCount(0);
1139+ await expect(page.getByText('Obsolete target error.')).toHaveCount(0);
1140+ await expect(page.getByLabel('RAG profile')).toHaveValue(change === 'target' ? 'other-profile' : 'edited-while-planning');
1141+ });
1142+ }
1143+}
1144+ 
912test('maintenance scope validation identifies collections and omits an unavailable CLI handoff', async ({ page }) => {1145test('maintenance scope validation identifies collections and omits an unavailable CLI handoff', async ({ page }) => {
913 await mockAdmin(page);1146 await mockAdmin(page);
914 await page.route('**/api/admin/v1/maintenance/plan', async (route) => {1147 await page.route('**/api/admin/v1/maintenance/plan', async (route) => {