From 0956c3fc377f78601dba821662f1980c6f7b640b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 22:17:22 +0000 Subject: [PATCH] Open a lab clicked in the side nav while the catalog is still loading On a slow machine, a side-nav lab link clicked just after the catalog arrived was ignored: the URL listener still held the empty list from before the catalog loaded, so the page stayed on the previous lab while the address bar showed the new one. This failed the e2e test 'lab links land on the first step not yet completed' twice on the v2.4.0 tag build. The listener now reads the catalog from a ref that is filled as soon as it loads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GWoueV3yPeP5Q297rR85vN --- dashboard/static/dashboard/dist/assets/LabsConsole.js | 4 ++-- frontend/src/pages/LabsConsole.tsx | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dashboard/static/dashboard/dist/assets/LabsConsole.js b/dashboard/static/dashboard/dist/assets/LabsConsole.js index 1fa5db1..5b48640 100644 --- a/dashboard/static/dashboard/dist/assets/LabsConsole.js +++ b/dashboard/static/dashboard/dist/assets/LabsConsole.js @@ -1,6 +1,6 @@ -import{At as e,Ci as t,Fr as n,Ht as r,Mr as i,Ot as a,Pr as o,Ur as s,a as c,c as l,ci as u,ir as d,kt as f,l as p,o as m,pn as h,s as g,sr as _,xi as ee,xr as te,yi as v}from"./internal.js";import{$ as ne,A as y,Ai as re,J as b,K as x,M as ie,Mi as ae,Q as S,X as C,Xa as oe,Y as se,Z as w,a as ce,ao as le,i as ue,j as de,ji as fe,ki as pe,l as T,n as me,o as E,q as D,qa as he,r as ge,s as _e,u as ve}from"./app.js";import{t as ye}from"./CodeSnippet.js";import{t as be}from"./tabs.js";import{t as O}from"./column-layout.js";import{t as k}from"./link.js";import{t as xe}from"./radio-group.js";import{t as Se}from"./segmented-control.js";var A=t(ee());function j(e){if(e?.progressBar)return{[d.progressBarBackgroundColor]:e.progressBar.backgroundColor,[d.progressBarBorderRadius]:e.progressBar.borderRadius,[d.progressBarHeight]:e.progressBar.height}}function M(e){if(e?.progressValue)return{[d.progressValueBackgroundColor]:e.progressValue.backgroundColor}}function Ce(e){if(e?.progressPercentage)return{color:e.progressPercentage.color,fontSize:e.progressPercentage.fontSize,fontWeight:e.progressPercentage.fontWeight}}var N={progress:`awsui_progress_11huc_15p30_13`,"result-state":`awsui_result-state_11huc_15p30_27`,"awsui-motion-fade-in":`awsui_awsui-motion-fade-in_11huc_15p30_1`,root:`awsui_root_11huc_15p30_49`,"with-result-button":`awsui_with-result-button_11huc_15p30_81`,"result-text":`awsui_result-text_11huc_15p30_85`,"result-container-error":`awsui_result-container-error_11huc_15p30_89`,"result-container-success":`awsui_result-container-success_11huc_15p30_89`,"result-button":`awsui_result-button_11huc_15p30_93`,"word-wrap":`awsui_word-wrap_11huc_15p30_99`,"label-flash":`awsui_label-flash_11huc_15p30_104`,"label-key-value":`awsui_label-key-value_11huc_15p30_108`,flash:`awsui_flash_11huc_15p30_116`,"progress-container":`awsui_progress-container_11huc_15p30_120`,"percentage-container":`awsui_percentage-container_11huc_15p30_127`,percentage:`awsui_percentage_11huc_15p30_127`,complete:`awsui_complete_11huc_15p30_173`,"progress-flash":`awsui_progress-flash_11huc_15p30_186`,"additional-info":`awsui_additional-info_11huc_15p30_199`},P=100,F=(e,t,n)=>Math.max(Math.min(e,n),t),I=({value:e,isInFlash:t,ariaLabel:n,ariaLabelledby:r,ariaDescribedby:a,style:o})=>{let s=F(Math.round(e),0,P),c=j(o),l=M(o),u=Ce(o);return A.createElement(`div`,{className:N[`progress-container`]},A.createElement(`progress`,{className:i(N.progress,s>=P&&N.complete,t&&N[`progress-flash`]),max:P,value:s,"aria-label":n,"aria-labelledby":n?void 0:r,"aria-describedby":a,style:{...c||{},...l||{}}}),A.createElement(`span`,{"aria-hidden":`true`,className:N[`percentage-container`]},A.createElement(f,{className:N.percentage,variant:`small`,color:t?`inherit`:void 0,nativeAttributes:u?{style:u}:void 0},`${s}%`)))},L=({color:e,children:t,className:n,id:r})=>A.createElement(f,{className:i(N[`word-wrap`],n),variant:`small`,display:`block`,color:e,id:r},t),R=({onClick:e,children:t})=>A.createElement(`div`,{className:N[`result-button`]},A.createElement(r,{formAction:`none`,onClick:e},t)),we=({isInFlash:t,resultText:n,resultButtonText:r,status:a,onClick:o})=>{let s=!!r;return t?A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:N[`result-text`]},n)):A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:i(s&&N[`with-result-button`])},A.createElement(e,{type:a===`success`?`success`:`error`},A.createElement(`span`,{className:N[`result-text`]},n))),s&&A.createElement(R,{onClick:o},r))},Te=5e3;function Ee({value:e=0,status:t=`in-progress`,variant:r=`standalone`,resultButtonText:o,label:c,ariaLabel:l,ariaLabelledby:d,ariaDescribedby:f,description:p,additionalInfo:m,resultText:g,onResultButtonClick:ee,style:v,...ne}){let{__internalRootRef:y}=n(`ProgressBar`,{props:{variant:r}}),re=te(ne),b=`${s(`awsui-progress-bar-`)}-label`,x=r===`flash`,ae=t===`in-progress`,S=s(`progressbar-description-`),C=s(`progressbar-additional-info-`),[oe,se]=(0,A.useState)(``),w=ie(e=>{se(`${e}%`)},Te,[]);return(0,A.useEffect)(()=>(w(e),()=>w.cancel()),[w,e]),x&&o&&u(`ProgressBar`,'The `resultButtonText` is ignored if you set `variant="flash"`, and the result button is not displayed. Use the `buttonText` property and the `onButtonClick` event listener of the flashbar item in which the progress bar component is embedded.'),A.createElement(`div`,{...re,className:i(re.className,N.root,r&&N[r]),ref:y},A.createElement(`div`,{className:x?N[`flash-container`]:void 0},A.createElement(`div`,{className:i(N[`word-wrap`],N[`label-${r}`]),id:b},c),p&&A.createElement(L,{color:x?`inherit`:void 0,id:S},p),A.createElement(`div`,null,ae?A.createElement(A.Fragment,null,A.createElement(I,{value:e,ariaLabel:l,ariaLabelledby:a(b,d),ariaDescribedby:a(p?S:void 0,m?C:void 0,f),isInFlash:x,style:v}),A.createElement(h,{hidden:!0,tagName:`span`,delay:0},c,c?`: `:null,oe)):A.createElement(h,{hidden:!1,tagName:`span`,delay:0},A.createElement(we,{resultText:g,isInFlash:x,resultButtonText:o,status:t,onClick:()=>{_(ee)}})))),m&&A.createElement(L,{className:N[`additional-info`],color:x?`inherit`:void 0,id:C},m))}o(Ee,`ProgressBar`);var z=v(),De=`floci_quiz_scores`,Oe=`floci:quiz-scores-changed`;function B(){try{let e=localStorage.getItem(De);return e?JSON.parse(e):{}}catch{return{}}}function ke(e,t,n){let r=B(),i=r[e];if(i&&i.total===n&&i.correct>=t)return;let a={...r,[e]:{correct:t,total:n}};try{localStorage.setItem(De,JSON.stringify(a))}catch{}window.dispatchEvent(new CustomEvent(Oe,{detail:a}))}function V(){let[e,t]=(0,A.useState)(B);return(0,A.useEffect)(()=>{let e=e=>t(e.detail);return window.addEventListener(Oe,e),()=>window.removeEventListener(Oe,e)},[]),e}var Ae=e=>!!(e&&e.total>0&&e.correct/e.total>=.7),H=(e,t)=>e.length===t.length&&t.every(t=>e.includes(t)),U=[``,`ONE`,`TWO`,`THREE`,`FOUR`],W=({quizId:e,questions:t,best:n})=>{let[r,i]=(0,A.useState)(()=>t.map(()=>[])),[a,o]=(0,A.useState)(!1),s=t.filter((e,t)=>H(r[t],e.answers)).length,l=t.every((e,t)=>r[t].length===e.answers.length),u=(e,t,n,r)=>{i(i=>i.map((i,a)=>a===e?n?r?[...i.filter(e=>e!==t),t]:i.filter(e=>e!==t):[t]:i))},d=()=>{o(!0),ke(e,s,t.length)},f=()=>{i(t.map(()=>[])),o(!1)},p=n?` · best ${n.correct}/${n.total}`:``;return(0,z.jsx)(de,{headerText:`Practice quiz: ${t.length} questions${p}`,variant:`footer`,children:(0,z.jsx)(`div`,{"data-testid":`quiz-${e}`,children:(0,z.jsxs)(g,{size:`m`,children:[t.map((e,t)=>{let n=e.answers.length>1,i=r[t],o=H(i,e.answers),s=e.options.map((e,t)=>({value:String(t),label:e}));return(0,z.jsx)(`div`,{"data-testid":`quiz-question-${t+1}`,children:(0,z.jsxs)(g,{size:`xxs`,children:[(0,z.jsxs)(m,{variant:`strong`,children:[t+1,`. `,e.question]}),n?(0,z.jsxs)(g,{size:`xxs`,children:[e.options.map((n,r)=>(0,z.jsx)(me,{checked:i.includes(r),disabled:a||!i.includes(r)&&i.length>=e.answers.length,onChange:({detail:e})=>u(t,r,!0,e.checked),children:n},n)),!e.question.includes(`(Choose`)&&(0,z.jsxs)(m,{variant:`small`,children:[`Choose `,U[e.answers.length]||e.answers.length,`.`]})]}):(0,z.jsx)(xe,{value:i.length?String(i[0]):null,items:s.map(e=>({...e,disabled:a})),onChange:({detail:e})=>u(t,Number(e.value),!1,!0)}),a&&(0,z.jsxs)(g,{size:`xxs`,children:[(0,z.jsx)(T,{type:o?`success`:`error`,children:o?`Correct`:`Answer: ${e.answers.map(t=>e.options[t]).join(`; `)}`}),(0,z.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:e.explanation})]})]})},e.question)}),(0,z.jsx)(g,{direction:`horizontal`,size:`xs`,alignItems:`center`,children:a?(0,z.jsxs)(z.Fragment,{children:[(0,z.jsxs)(T,{type:Ae({correct:s,total:t.length})?`success`:`warning`,children:[s,` of `,t.length,` correct`]}),(0,z.jsx)(c,{onClick:f,children:`Try again`})]}):(0,z.jsx)(c,{variant:`primary`,disabled:!l,onClick:d,children:`Check answers`})})]})})})};function je(e){return`${e.exam.split(`-`)[0]} ${e.task}`}var G=(e,t)=>`${e}:${t}`;function K(e){return[...new Set(e.tasks.flatMap(e=>e.labs.map(e=>G(e.service,e.key))))]}function Me(e,t){let n=0,r=0,i=0;for(let a of e.domains){let e=K(a);e.length>0&&(n+=a.weight*(e.filter(e=>t[e]).length/e.length)),i+=a.tasks.length,r+=a.tasks.filter(e=>e.labs.length>0).length}return{weighted:Math.round(n),tasksWithLabs:r,taskCount:i}}var q=({service:e,labKey:t,title:n,primary:r,completed:i,onOpenLab:a})=>{let o=w(e,t),s=!!i[G(e,t)];return(0,z.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[s?(0,z.jsx)(T,{type:`success`}):(0,z.jsx)(T,{type:`pending`}),o&&(0,z.jsxs)(E,{color:`grey`,children:[`#`,o.id]}),(0,z.jsx)(E,{color:`blue`,children:e.toUpperCase()}),(0,z.jsx)(c,{variant:`inline-link`,onClick:()=>a(e,t),children:r?(0,z.jsx)(`strong`,{children:n}):n})]})},J=({card:e,completed:t,onOpenLab:n})=>(0,z.jsx)(de,{headerText:`Concept: ${e.title}`,variant:`footer`,children:(0,z.jsxs)(g,{size:`xs`,children:[(0,z.jsx)(`ul`,{style:{margin:0,paddingLeft:`20px`},children:e.points.map(e=>(0,z.jsx)(`li`,{children:e},e))}),e.related_labs.length>0&&(0,z.jsxs)(m,{variant:`small`,children:[`Related labs:`,` `,e.related_labs.map((e,r)=>{let i=w(e.service,e.key);return(0,z.jsxs)(A.Fragment,{children:[r>0&&`, `,(0,z.jsx)(c,{variant:`inline-link`,onClick:()=>n(e.service,e.key),children:i?`#${i.id} ${i.title}`:`${e.service}/${e.key}`}),t[G(e.service,e.key)]?` ✓`:``]},G(e.service,e.key))})]}),e.planned&&(0,z.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Planned lab: `,e.planned]})]})}),Ne=({exam:e,concepts:t,completed:n,onOpenLab:r})=>{let i=Me(e,n),a=V(),o=e.domains.flatMap(e=>e.tasks).filter(e=>(e.quiz?.length??0)>0),s=o.filter(t=>Ae(a[`${e.code}:${t.id}`])).length;return(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsxs)(O,{columns:2,variant:`text-grid`,children:[(0,z.jsxs)(g,{size:`xxs`,children:[(0,z.jsxs)(m,{variant:`h2`,children:[e.code,` · `,e.title]}),(0,z.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Domains and task statements from the`,` `,(0,z.jsx)(k,{href:e.guide_url,external:!0,children:`official exam guide`}),` `,`(checked `,e.checked,`). Labs show the tasks they practice; concept cards cover objectives that can't be practiced in a local emulator.`]})]}),(0,z.jsxs)(g,{size:`xs`,children:[(0,z.jsx)(Ee,{value:i.weighted,label:`Exam-weighted lab progress`,description:`Completed labs, weighted by each domain's share of the exam. Hands-on labs cover ${i.tasksWithLabs} of ${i.taskCount} task statements; the rest have concept cards.`,status:i.weighted===100?`success`:`in-progress`}),o.length>0&&(0,z.jsx)(`div`,{"data-testid":`quiz-progress-${e.code}`,children:(0,z.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Practice quizzes passed: `,s,` of `,o.length,` task statements (70% or better).`]})})]})]}),e.domains.map((i,o)=>{let s=K(i),c=s.filter(e=>n[e]).length;return(0,z.jsx)(de,{variant:`container`,defaultExpanded:o===0,headerText:`Domain ${i.id}: ${i.title}`,headerDescription:`${i.weight}% of scored content · ${s.length>0?`${c} of ${s.length} labs complete`:`no hands-on labs yet`}`,headerActions:(0,z.jsxs)(E,{color:s.length>0?`blue`:`grey`,children:[i.weight,`%`]}),children:(0,z.jsx)(g,{size:`l`,children:i.tasks.map(i=>(0,z.jsx)(`div`,{"data-testid":`exam-task-${e.code}-${i.id}`,children:(0,z.jsxs)(g,{size:`xs`,children:[(0,z.jsxs)(m,{variant:`h4`,children:[i.id,` `,i.title]}),i.labs.length>0?(0,z.jsx)(g,{size:`xxs`,children:i.labs.map(e=>(0,z.jsx)(q,{service:e.service,labKey:e.key,title:e.title,primary:e.primary,completed:n,onOpenLab:r},G(e.service,e.key)))}):(0,z.jsx)(T,{type:`warning`,children:`No hands-on lab yet`}),i.concepts.map(e=>t[e]).filter(Boolean).map(e=>(0,z.jsx)(J,{card:e,completed:n,onOpenLab:r},e.key)),i.quiz&&i.quiz.length>0&&(0,z.jsx)(W,{quizId:`${e.code}:${i.id}`,questions:i.quiz,best:a[`${e.code}:${i.id}`]})]})},i.id))})},i.id)}),(0,z.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:`Bold labs practice this task statement as their main focus; the rest touch it along the way.`})]})},Pe=`floci_labs_study_track`,Y=`levels`;function Fe(){let e=new URLSearchParams(window.location.search).get(`track`);if(e)return e;try{return localStorage.getItem(Pe)||Y}catch{return Y}}function Ie(e=[]){return[...e].sort((e,t)=>Number(t.primary)-Number(e.primary)||e.exam.localeCompare(t.exam)||e.task.localeCompare(t.task,void 0,{numeric:!0}))}var Le=()=>{let[e,t]=(0,A.useState)([]),[n,r]=(0,A.useState)(null),[i,a]=(0,A.useState)(null),[o,s]=(0,A.useState)(Fe),u=e=>{s(e);try{localStorage.setItem(Pe,e)}catch{}let t=new URL(window.location.href);e===Y?t.searchParams.delete(`track`):t.searchParams.set(`track`,e),window.history.replaceState(null,``,t.pathname+t.search)},[d,f]=(0,A.useState)(ue);(0,A.useEffect)(()=>{ce(d)},[d]);let h=(e,t,n)=>{f(r=>({...r,[`${e}:${t}`]:n}))},[_,ee]=(0,A.useState)(null),[te,v]=(0,A.useState)(!0),[ie,me]=(0,A.useState)(null),[O,k]=(0,A.useState)(!1),[xe,j]=(0,A.useState)(null),[M,Ce]=(0,A.useState)(``),[N,P]=(0,A.useState)(`all`),[F,I]=(0,A.useState)(null),[L,R]=(0,A.useState)(!1),[we,Te]=(0,A.useState)(()=>{try{let e=localStorage.getItem(`floci_labs_active_sdk`);if(e===`cli`||e===`boto3`||e===`terraform`)return e}catch{}return`cli`}),De=e=>{Te(e);try{localStorage.setItem(`floci_labs_active_sdk`,e)}catch{}},[Oe,B]=(0,A.useState)({}),[ke,V]=(0,A.useState)({}),[Ae,H]=(0,A.useState)({}),U=(0,A.useRef)(!1),W=(0,A.useRef)(0),G=(0,A.useRef)({}),[K,Me]=(0,A.useState)(null),q=async()=>{v(!0),me(null);try{let e=((await fe()).services||[]).flatMap(e=>e.labs);t(e),a(null),pe().then(r).catch(e=>a(e?.message||`Could not load the exam study tracks.`));let n=new URLSearchParams(window.location.search),i=n.get(`lab`),o=n.get(`service`);if(i){let t=e.find(e=>e.key===i&&(!o||e.service===o));t&&J(t,!1)}ae().then(e=>{f(t=>{let n={...t};return(e.labs||[]).forEach(e=>{let t=e.key||e.lab;t&&(n[`${e.service}:${t}`]=!!e.complete)}),n})})}catch(e){console.error(e),me(e?.message||`Could not load the lab catalog.`)}finally{v(!1)}};(0,A.useEffect)(()=>{q()},[]),(0,A.useEffect)(()=>{let t=()=>{let t=new URLSearchParams(window.location.search),n=t.get(`lab`),r=t.get(`service`);if(n&&e.length>0){let t=e.find(e=>e.key===n&&(!r||e.service===r));t&&J(t,!1)}else!n&&_&&Re(!1)};return window.addEventListener(`popstate`,t),window.addEventListener(`floci_navigate`,t),()=>{window.removeEventListener(`popstate`,t),window.removeEventListener(`floci_navigate`,t)}},[e,_]);let J=async(e,t=!0)=>{U.current=!0;let n=++W.current;if(R(!1),I(null),j(null),ee(e),B({}),V({}),H({}),k(!0),t){let t=new URL(window.location.href);t.searchParams.set(`lab`,e.key),t.searchParams.set(`service`,e.service),t.hash=se,window.history.pushState(null,``,t.pathname+t.search+t.hash),window.dispatchEvent(new Event(`floci_url_changed`))}try{let t=await re(e.service,e.key);if(n!==W.current)return;if(t.error)throw Error(t.error);let r={},i={};if(Object.entries(t.steps||{}).forEach(([e,t])=>{r[e]=!!t.verified,t.verification&&(i[e]={status:t.verified?`Verified`:`Unverified`,body:t.verification.message||JSON.stringify(t.verification,null,2)})}),V(r),B(i),window.location.hash===`#next-step`){let t=e.steps||[],n=t.findIndex(e=>!(r[e.key]||e.status?.verified));Me(n>0?ne(t[n].key):C)}h(e.service,e.key,!!t.complete)}catch(e){n===W.current&&j(e.message||`Failed to load lab status`)}finally{n===W.current&&k(!1)}},Le=_?`${_.service}:${_.key}`:``;(0,A.useLayoutEffect)(()=>{if(!Le)return;let e=(window.location.hash.startsWith(`#step-`)?document.getElementById(window.location.hash.slice(1)):null)||document.getElementById(`lab-top`);e?e.scrollIntoView({block:`start`}):window.scrollTo({top:0})},[Le]),(0,A.useLayoutEffect)(()=>{if(!K||(Me(null),window.location.hash!==`#next-step`))return;let e=document.getElementById(K);e&&e.scrollIntoView({block:`start`}),K!==`lab-top`&&window.history.replaceState(window.history.state,``,`${window.location.pathname}${window.location.search}#${K}`)},[K]);let Re=(e=!0)=>{if(++W.current,U.current=!0,R(!1),ee(null),B({}),V({}),H({}),e){let e=new URL(window.location.href);e.searchParams.delete(`lab`),e.searchParams.delete(`service`),e.hash=``,window.history.pushState(null,``,e.pathname+e.search),window.dispatchEvent(new Event(`floci_url_changed`))}},ze=e=>{let t=G.current[e];if(t){let e=t.getBoundingClientRect(),n=window.pageYOffset+e.top-14;window.scrollTo({top:Math.max(0,n),behavior:`smooth`})}},Be=async e=>{if(!_)return!1;let t=W.current;j(null),I(e.key),H(t=>({...t,[e.key]:!0}));try{let n=await le(_.service,_.key,e.key);if(t!==W.current)return!1;let r=!!n.verified,i=n.stdout||JSON.stringify(n.verification||n.json||n,null,2);return B(t=>({...t,[e.key]:{status:r?`Verified`:`Not verified`,body:i}})),V(t=>{let n={...t,[e.key]:r};return _.steps?.every(t=>t.key===e.key?r:n[t.key])&&h(_.service,_.key,!0),n}),r}catch(n){return t===W.current&&B(t=>({...t,[e.key]:{status:`Failed`,body:n.message||`Execution error`}})),!1}finally{t===W.current&&I(null)}},Ve=async()=>{if(!_||L)return;R(!0),U.current=!1;let e=W.current,t=_.steps||[];for(let n=0;nsetTimeout(e,500)),U.current||e!==W.current)break;if(!await Be(r)){e===W.current&&j(`Run all stopped because this step failed or could not be verified. Review its result and retry.`);break}await new Promise(e=>setTimeout(e,1400))}}e===W.current&&R(!1)},He=async()=>{if(!_)return;let e=++W.current;j(null),U.current=!0,R(!1),k(!0);try{if(await oe(_.service,_.key),e!==W.current)return;V({}),B({}),h(_.service,_.key,!1)}catch(t){e===W.current&&j(t.message||`Failed to reset lab`)}finally{e===W.current&&k(!1)}},[Ue,We]=(0,A.useState)(!1),[Ge,Ke]=(0,A.useState)(!1),[qe,X]=(0,A.useState)(null),Je=async()=>{Ke(!0),X(null);try{let e=await he();f({}),X({type:`success`,text:`Successfully reset all ${e.reset_lab_count||D} labs in the system.`}),We(!1),await q()}catch(e){X({type:`error`,text:e.message||`Failed to reset all labs`})}finally{Ke(!1)}},Ye=[...e].sort((e,t)=>{let n=w(e.service,e.key),r=w(t.service,t.key);return(n?n.id:999)-(r?r.id:999)}).filter(e=>{let t=w(e.service,e.key),n=N===`all`||t?.levelId===N,r=!M||e.title.toLowerCase().includes(M.toLowerCase())||e.service.toLowerCase().includes(M.toLowerCase())||e.description.toLowerCase().includes(M.toLowerCase())||(e.exams||[]).some(e=>je(e).toLowerCase().includes(M.toLowerCase()))||String(t?.id).includes(M);return n&&r}),Xe=n?.exams?.find(e=>e.code===o),Ze=o!==Y&&(!!Xe||!n&&!i||!!i),Qe=Object.fromEntries((n?.concepts||[]).map(e=>[e.key,e])),$e=(t,n)=>{let r=e.find(e=>e.service===t&&e.key===n);r&&J(r)},et=_?w(_.service,_.key):void 0,tt=_?S(_.service,_.key):void 0,Z=et?et.id:-1,Q=Z>1?x[Z-2]:void 0,$=Z>0&&Z`/labs?service=${encodeURIComponent(e.service)}&lab=${encodeURIComponent(e.key)}#${se}`,rt=t=>n=>{let r=e.find(e=>e.service===t.service&&e.key===t.key);r&&(n.preventDefault(),J(r))},it=_?.steps?.filter(e=>ke[e.key]||e.status?.verified).length||0,at=_?.steps?.length||1,ot=Math.round(it/at*100),st=it===at&&at>0||!!d[`${_?.service}:${_?.key}`];return _?(0,z.jsx)(`div`,{id:C,style:{scrollMarginTop:`16px`},children:(0,z.jsxs)(g,{size:`l`,children:[xe&&(0,z.jsx)(y,{type:`error`,children:xe}),(0,z.jsx)(p,{header:(0,z.jsx)(l,{variant:`h1`,description:_.description,actions:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,z.jsx)(c,{onClick:()=>Re(),children:`← Back to Labs Directory`}),(0,z.jsx)(c,{onClick:He,loading:O,disabled:L||F!==null,children:`Reset lab`}),(0,z.jsx)(c,{variant:`primary`,iconName:`caret-right-filled`,onClick:Ve,loading:L,disabled:st||O||F!==null,children:st?`✓ Lab Fully Completed`:L?`Running steps...`:`▶ Run all remaining steps`})]}),children:(0,z.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,flexWrap:`wrap`},children:[et&&(0,z.jsxs)(E,{color:`grey`,children:[`Lesson #`,et.id,` of `,D]}),tt&&(0,z.jsx)(E,{color:tt.color,children:tt.badge}),(0,z.jsx)(E,{color:`blue`,children:_.service.toUpperCase()}),Ie(_.exams).map(e=>(0,z.jsx)(`span`,{title:`${e.exam} task ${e.task}${e.primary?` (main focus)`:``}`,children:(0,z.jsx)(E,{color:e.primary?`green`:`grey`,children:je(e)})},`${e.exam}-${e.task}`)),(0,z.jsx)(`span`,{style:{fontSize:`18px`,fontWeight:600},children:_.title}),st&&(0,z.jsx)(T,{type:`success`,children:`Completed`})]})}),children:(0,z.jsx)(Ee,{value:ot,label:`Lab Step Completion`,description:`${it} of ${at} steps complete (${ot}%)`,status:ot===100?`success`:`in-progress`})}),(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsx)(`style`,{children:` +import{At as e,Ci as t,Fr as n,Ht as r,Mr as i,Ot as a,Pr as o,Ur as s,a as c,c as l,ci as u,ir as d,kt as f,l as p,o as m,pn as h,s as g,sr as _,xi as v,xr as ee,yi as y}from"./internal.js";import{$ as te,A as b,Ai as ne,J as x,K as S,M as re,Mi as ie,Q as ae,X as oe,Xa as se,Y as ce,Z as C,a as le,ao as ue,i as de,j as w,ji as fe,ki as pe,l as T,n as me,o as E,q as D,qa as he,r as ge,s as _e,u as ve}from"./app.js";import{t as O}from"./CodeSnippet.js";import{t as ye}from"./tabs.js";import{t as be}from"./column-layout.js";import{t as k}from"./link.js";import{t as xe}from"./radio-group.js";import{t as Se}from"./segmented-control.js";var A=t(v());function j(e){if(e?.progressBar)return{[d.progressBarBackgroundColor]:e.progressBar.backgroundColor,[d.progressBarBorderRadius]:e.progressBar.borderRadius,[d.progressBarHeight]:e.progressBar.height}}function M(e){if(e?.progressValue)return{[d.progressValueBackgroundColor]:e.progressValue.backgroundColor}}function Ce(e){if(e?.progressPercentage)return{color:e.progressPercentage.color,fontSize:e.progressPercentage.fontSize,fontWeight:e.progressPercentage.fontWeight}}var N={progress:`awsui_progress_11huc_15p30_13`,"result-state":`awsui_result-state_11huc_15p30_27`,"awsui-motion-fade-in":`awsui_awsui-motion-fade-in_11huc_15p30_1`,root:`awsui_root_11huc_15p30_49`,"with-result-button":`awsui_with-result-button_11huc_15p30_81`,"result-text":`awsui_result-text_11huc_15p30_85`,"result-container-error":`awsui_result-container-error_11huc_15p30_89`,"result-container-success":`awsui_result-container-success_11huc_15p30_89`,"result-button":`awsui_result-button_11huc_15p30_93`,"word-wrap":`awsui_word-wrap_11huc_15p30_99`,"label-flash":`awsui_label-flash_11huc_15p30_104`,"label-key-value":`awsui_label-key-value_11huc_15p30_108`,flash:`awsui_flash_11huc_15p30_116`,"progress-container":`awsui_progress-container_11huc_15p30_120`,"percentage-container":`awsui_percentage-container_11huc_15p30_127`,percentage:`awsui_percentage_11huc_15p30_127`,complete:`awsui_complete_11huc_15p30_173`,"progress-flash":`awsui_progress-flash_11huc_15p30_186`,"additional-info":`awsui_additional-info_11huc_15p30_199`},we=100,P=(e,t,n)=>Math.max(Math.min(e,n),t),F=({value:e,isInFlash:t,ariaLabel:n,ariaLabelledby:r,ariaDescribedby:a,style:o})=>{let s=P(Math.round(e),0,we),c=j(o),l=M(o),u=Ce(o);return A.createElement(`div`,{className:N[`progress-container`]},A.createElement(`progress`,{className:i(N.progress,s>=we&&N.complete,t&&N[`progress-flash`]),max:we,value:s,"aria-label":n,"aria-labelledby":n?void 0:r,"aria-describedby":a,style:{...c||{},...l||{}}}),A.createElement(`span`,{"aria-hidden":`true`,className:N[`percentage-container`]},A.createElement(f,{className:N.percentage,variant:`small`,color:t?`inherit`:void 0,nativeAttributes:u?{style:u}:void 0},`${s}%`)))},I=({color:e,children:t,className:n,id:r})=>A.createElement(f,{className:i(N[`word-wrap`],n),variant:`small`,display:`block`,color:e,id:r},t),L=({onClick:e,children:t})=>A.createElement(`div`,{className:N[`result-button`]},A.createElement(r,{formAction:`none`,onClick:e},t)),Te=({isInFlash:t,resultText:n,resultButtonText:r,status:a,onClick:o})=>{let s=!!r;return t?A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:N[`result-text`]},n)):A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:i(s&&N[`with-result-button`])},A.createElement(e,{type:a===`success`?`success`:`error`},A.createElement(`span`,{className:N[`result-text`]},n))),s&&A.createElement(L,{onClick:o},r))},Ee=5e3;function De({value:e=0,status:t=`in-progress`,variant:r=`standalone`,resultButtonText:o,label:c,ariaLabel:l,ariaLabelledby:d,ariaDescribedby:f,description:p,additionalInfo:m,resultText:g,onResultButtonClick:v,style:y,...te}){let{__internalRootRef:b}=n(`ProgressBar`,{props:{variant:r}}),ne=ee(te),x=`${s(`awsui-progress-bar-`)}-label`,S=r===`flash`,ie=t===`in-progress`,ae=s(`progressbar-description-`),oe=s(`progressbar-additional-info-`),[se,ce]=(0,A.useState)(``),C=re(e=>{ce(`${e}%`)},Ee,[]);return(0,A.useEffect)(()=>(C(e),()=>C.cancel()),[C,e]),S&&o&&u(`ProgressBar`,'The `resultButtonText` is ignored if you set `variant="flash"`, and the result button is not displayed. Use the `buttonText` property and the `onButtonClick` event listener of the flashbar item in which the progress bar component is embedded.'),A.createElement(`div`,{...ne,className:i(ne.className,N.root,r&&N[r]),ref:b},A.createElement(`div`,{className:S?N[`flash-container`]:void 0},A.createElement(`div`,{className:i(N[`word-wrap`],N[`label-${r}`]),id:x},c),p&&A.createElement(I,{color:S?`inherit`:void 0,id:ae},p),A.createElement(`div`,null,ie?A.createElement(A.Fragment,null,A.createElement(F,{value:e,ariaLabel:l,ariaLabelledby:a(x,d),ariaDescribedby:a(p?ae:void 0,m?oe:void 0,f),isInFlash:S,style:y}),A.createElement(h,{hidden:!0,tagName:`span`,delay:0},c,c?`: `:null,se)):A.createElement(h,{hidden:!1,tagName:`span`,delay:0},A.createElement(Te,{resultText:g,isInFlash:S,resultButtonText:o,status:t,onClick:()=>{_(v)}})))),m&&A.createElement(I,{className:N[`additional-info`],color:S?`inherit`:void 0,id:oe},m))}o(De,`ProgressBar`);var R=y(),Oe=`floci_quiz_scores`,z=`floci:quiz-scores-changed`;function B(){try{let e=localStorage.getItem(Oe);return e?JSON.parse(e):{}}catch{return{}}}function ke(e,t,n){let r=B(),i=r[e];if(i&&i.total===n&&i.correct>=t)return;let a={...r,[e]:{correct:t,total:n}};try{localStorage.setItem(Oe,JSON.stringify(a))}catch{}window.dispatchEvent(new CustomEvent(z,{detail:a}))}function V(){let[e,t]=(0,A.useState)(B);return(0,A.useEffect)(()=>{let e=e=>t(e.detail);return window.addEventListener(z,e),()=>window.removeEventListener(z,e)},[]),e}var Ae=e=>!!(e&&e.total>0&&e.correct/e.total>=.7),H=(e,t)=>e.length===t.length&&t.every(t=>e.includes(t)),U=[``,`ONE`,`TWO`,`THREE`,`FOUR`],W=({quizId:e,questions:t,best:n})=>{let[r,i]=(0,A.useState)(()=>t.map(()=>[])),[a,o]=(0,A.useState)(!1),s=t.filter((e,t)=>H(r[t],e.answers)).length,l=t.every((e,t)=>r[t].length===e.answers.length),u=(e,t,n,r)=>{i(i=>i.map((i,a)=>a===e?n?r?[...i.filter(e=>e!==t),t]:i.filter(e=>e!==t):[t]:i))},d=()=>{o(!0),ke(e,s,t.length)},f=()=>{i(t.map(()=>[])),o(!1)},p=n?` · best ${n.correct}/${n.total}`:``;return(0,R.jsx)(w,{headerText:`Practice quiz: ${t.length} questions${p}`,variant:`footer`,children:(0,R.jsx)(`div`,{"data-testid":`quiz-${e}`,children:(0,R.jsxs)(g,{size:`m`,children:[t.map((e,t)=>{let n=e.answers.length>1,i=r[t],o=H(i,e.answers),s=e.options.map((e,t)=>({value:String(t),label:e}));return(0,R.jsx)(`div`,{"data-testid":`quiz-question-${t+1}`,children:(0,R.jsxs)(g,{size:`xxs`,children:[(0,R.jsxs)(m,{variant:`strong`,children:[t+1,`. `,e.question]}),n?(0,R.jsxs)(g,{size:`xxs`,children:[e.options.map((n,r)=>(0,R.jsx)(me,{checked:i.includes(r),disabled:a||!i.includes(r)&&i.length>=e.answers.length,onChange:({detail:e})=>u(t,r,!0,e.checked),children:n},n)),!e.question.includes(`(Choose`)&&(0,R.jsxs)(m,{variant:`small`,children:[`Choose `,U[e.answers.length]||e.answers.length,`.`]})]}):(0,R.jsx)(xe,{value:i.length?String(i[0]):null,items:s.map(e=>({...e,disabled:a})),onChange:({detail:e})=>u(t,Number(e.value),!1,!0)}),a&&(0,R.jsxs)(g,{size:`xxs`,children:[(0,R.jsx)(T,{type:o?`success`:`error`,children:o?`Correct`:`Answer: ${e.answers.map(t=>e.options[t]).join(`; `)}`}),(0,R.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:e.explanation})]})]})},e.question)}),(0,R.jsx)(g,{direction:`horizontal`,size:`xs`,alignItems:`center`,children:a?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsxs)(T,{type:Ae({correct:s,total:t.length})?`success`:`warning`,children:[s,` of `,t.length,` correct`]}),(0,R.jsx)(c,{onClick:f,children:`Try again`})]}):(0,R.jsx)(c,{variant:`primary`,disabled:!l,onClick:d,children:`Check answers`})})]})})})};function je(e){return`${e.exam.split(`-`)[0]} ${e.task}`}var G=(e,t)=>`${e}:${t}`;function K(e){return[...new Set(e.tasks.flatMap(e=>e.labs.map(e=>G(e.service,e.key))))]}function Me(e,t){let n=0,r=0,i=0;for(let a of e.domains){let e=K(a);e.length>0&&(n+=a.weight*(e.filter(e=>t[e]).length/e.length)),i+=a.tasks.length,r+=a.tasks.filter(e=>e.labs.length>0).length}return{weighted:Math.round(n),tasksWithLabs:r,taskCount:i}}var Ne=({service:e,labKey:t,title:n,primary:r,completed:i,onOpenLab:a})=>{let o=C(e,t),s=!!i[G(e,t)];return(0,R.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[s?(0,R.jsx)(T,{type:`success`}):(0,R.jsx)(T,{type:`pending`}),o&&(0,R.jsxs)(E,{color:`grey`,children:[`#`,o.id]}),(0,R.jsx)(E,{color:`blue`,children:e.toUpperCase()}),(0,R.jsx)(c,{variant:`inline-link`,onClick:()=>a(e,t),children:r?(0,R.jsx)(`strong`,{children:n}):n})]})},q=({card:e,completed:t,onOpenLab:n})=>(0,R.jsx)(w,{headerText:`Concept: ${e.title}`,variant:`footer`,children:(0,R.jsxs)(g,{size:`xs`,children:[(0,R.jsx)(`ul`,{style:{margin:0,paddingLeft:`20px`},children:e.points.map(e=>(0,R.jsx)(`li`,{children:e},e))}),e.related_labs.length>0&&(0,R.jsxs)(m,{variant:`small`,children:[`Related labs:`,` `,e.related_labs.map((e,r)=>{let i=C(e.service,e.key);return(0,R.jsxs)(A.Fragment,{children:[r>0&&`, `,(0,R.jsx)(c,{variant:`inline-link`,onClick:()=>n(e.service,e.key),children:i?`#${i.id} ${i.title}`:`${e.service}/${e.key}`}),t[G(e.service,e.key)]?` ✓`:``]},G(e.service,e.key))})]}),e.planned&&(0,R.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Planned lab: `,e.planned]})]})}),Pe=({exam:e,concepts:t,completed:n,onOpenLab:r})=>{let i=Me(e,n),a=V(),o=e.domains.flatMap(e=>e.tasks).filter(e=>(e.quiz?.length??0)>0),s=o.filter(t=>Ae(a[`${e.code}:${t.id}`])).length;return(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsxs)(be,{columns:2,variant:`text-grid`,children:[(0,R.jsxs)(g,{size:`xxs`,children:[(0,R.jsxs)(m,{variant:`h2`,children:[e.code,` · `,e.title]}),(0,R.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Domains and task statements from the`,` `,(0,R.jsx)(k,{href:e.guide_url,external:!0,children:`official exam guide`}),` `,`(checked `,e.checked,`). Labs show the tasks they practice; concept cards cover objectives that can't be practiced in a local emulator.`]})]}),(0,R.jsxs)(g,{size:`xs`,children:[(0,R.jsx)(De,{value:i.weighted,label:`Exam-weighted lab progress`,description:`Completed labs, weighted by each domain's share of the exam. Hands-on labs cover ${i.tasksWithLabs} of ${i.taskCount} task statements; the rest have concept cards.`,status:i.weighted===100?`success`:`in-progress`}),o.length>0&&(0,R.jsx)(`div`,{"data-testid":`quiz-progress-${e.code}`,children:(0,R.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Practice quizzes passed: `,s,` of `,o.length,` task statements (70% or better).`]})})]})]}),e.domains.map((i,o)=>{let s=K(i),c=s.filter(e=>n[e]).length;return(0,R.jsx)(w,{variant:`container`,defaultExpanded:o===0,headerText:`Domain ${i.id}: ${i.title}`,headerDescription:`${i.weight}% of scored content · ${s.length>0?`${c} of ${s.length} labs complete`:`no hands-on labs yet`}`,headerActions:(0,R.jsxs)(E,{color:s.length>0?`blue`:`grey`,children:[i.weight,`%`]}),children:(0,R.jsx)(g,{size:`l`,children:i.tasks.map(i=>(0,R.jsx)(`div`,{"data-testid":`exam-task-${e.code}-${i.id}`,children:(0,R.jsxs)(g,{size:`xs`,children:[(0,R.jsxs)(m,{variant:`h4`,children:[i.id,` `,i.title]}),i.labs.length>0?(0,R.jsx)(g,{size:`xxs`,children:i.labs.map(e=>(0,R.jsx)(Ne,{service:e.service,labKey:e.key,title:e.title,primary:e.primary,completed:n,onOpenLab:r},G(e.service,e.key)))}):(0,R.jsx)(T,{type:`warning`,children:`No hands-on lab yet`}),i.concepts.map(e=>t[e]).filter(Boolean).map(e=>(0,R.jsx)(q,{card:e,completed:n,onOpenLab:r},e.key)),i.quiz&&i.quiz.length>0&&(0,R.jsx)(W,{quizId:`${e.code}:${i.id}`,questions:i.quiz,best:a[`${e.code}:${i.id}`]})]})},i.id))})},i.id)}),(0,R.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:`Bold labs practice this task statement as their main focus; the rest touch it along the way.`})]})},Fe=`floci_labs_study_track`,J=`levels`;function Ie(){let e=new URLSearchParams(window.location.search).get(`track`);if(e)return e;try{return localStorage.getItem(Fe)||J}catch{return J}}function Le(e=[]){return[...e].sort((e,t)=>Number(t.primary)-Number(e.primary)||e.exam.localeCompare(t.exam)||e.task.localeCompare(t.task,void 0,{numeric:!0}))}var Y=()=>{let[e,t]=(0,A.useState)([]),[n,r]=(0,A.useState)(null),[i,a]=(0,A.useState)(null),[o,s]=(0,A.useState)(Ie),u=e=>{s(e);try{localStorage.setItem(Fe,e)}catch{}let t=new URL(window.location.href);e===J?t.searchParams.delete(`track`):t.searchParams.set(`track`,e),window.history.replaceState(null,``,t.pathname+t.search)},[d,f]=(0,A.useState)(de);(0,A.useEffect)(()=>{le(d)},[d]);let h=(e,t,n)=>{f(r=>({...r,[`${e}:${t}`]:n}))},[_,v]=(0,A.useState)(null),[ee,y]=(0,A.useState)(!0),[re,me]=(0,A.useState)(null),[be,k]=(0,A.useState)(!1),[xe,j]=(0,A.useState)(null),[M,Ce]=(0,A.useState)(``),[N,we]=(0,A.useState)(`all`),[P,F]=(0,A.useState)(null),[I,L]=(0,A.useState)(!1),[Te,Ee]=(0,A.useState)(()=>{try{let e=localStorage.getItem(`floci_labs_active_sdk`);if(e===`cli`||e===`boto3`||e===`terraform`)return e}catch{}return`cli`}),Oe=e=>{Ee(e);try{localStorage.setItem(`floci_labs_active_sdk`,e)}catch{}},[z,B]=(0,A.useState)({}),[ke,V]=(0,A.useState)({}),[Ae,H]=(0,A.useState)({}),U=(0,A.useRef)(!1),W=(0,A.useRef)(0),G=(0,A.useRef)({}),[K,Me]=(0,A.useState)(null),Ne=(0,A.useRef)([]),q=async()=>{y(!0),me(null);try{let e=((await fe()).services||[]).flatMap(e=>e.labs);Ne.current=e,t(e),a(null),pe().then(r).catch(e=>a(e?.message||`Could not load the exam study tracks.`));let n=new URLSearchParams(window.location.search),i=n.get(`lab`),o=n.get(`service`);if(i){let t=e.find(e=>e.key===i&&(!o||e.service===o));t&&Y(t,!1)}ie().then(e=>{f(t=>{let n={...t};return(e.labs||[]).forEach(e=>{let t=e.key||e.lab;t&&(n[`${e.service}:${t}`]=!!e.complete)}),n})})}catch(e){console.error(e),me(e?.message||`Could not load the lab catalog.`)}finally{y(!1)}};(0,A.useEffect)(()=>{q()},[]),(0,A.useEffect)(()=>{let e=()=>{let e=new URLSearchParams(window.location.search),t=e.get(`lab`),n=e.get(`service`),r=Ne.current;if(t&&r.length>0){let e=r.find(e=>e.key===t&&(!n||e.service===n));e&&Y(e,!1)}else!t&&_&&ze(!1)};return window.addEventListener(`popstate`,e),window.addEventListener(`floci_navigate`,e),()=>{window.removeEventListener(`popstate`,e),window.removeEventListener(`floci_navigate`,e)}},[_]);let Y=async(e,t=!0)=>{U.current=!0;let n=++W.current;if(L(!1),F(null),j(null),v(e),B({}),V({}),H({}),k(!0),t){let t=new URL(window.location.href);t.searchParams.set(`lab`,e.key),t.searchParams.set(`service`,e.service),t.hash=ce,window.history.pushState(null,``,t.pathname+t.search+t.hash),window.dispatchEvent(new Event(`floci_url_changed`))}try{let t=await ne(e.service,e.key);if(n!==W.current)return;if(t.error)throw Error(t.error);let r={},i={};if(Object.entries(t.steps||{}).forEach(([e,t])=>{r[e]=!!t.verified,t.verification&&(i[e]={status:t.verified?`Verified`:`Unverified`,body:t.verification.message||JSON.stringify(t.verification,null,2)})}),V(r),B(i),window.location.hash===`#next-step`){let t=e.steps||[],n=t.findIndex(e=>!(r[e.key]||e.status?.verified));Me(n>0?te(t[n].key):oe)}h(e.service,e.key,!!t.complete)}catch(e){n===W.current&&j(e.message||`Failed to load lab status`)}finally{n===W.current&&k(!1)}},Re=_?`${_.service}:${_.key}`:``;(0,A.useLayoutEffect)(()=>{if(!Re)return;let e=(window.location.hash.startsWith(`#step-`)?document.getElementById(window.location.hash.slice(1)):null)||document.getElementById(`lab-top`);e?e.scrollIntoView({block:`start`}):window.scrollTo({top:0})},[Re]),(0,A.useLayoutEffect)(()=>{if(!K||(Me(null),window.location.hash!==`#next-step`))return;let e=document.getElementById(K);e&&e.scrollIntoView({block:`start`}),K!==`lab-top`&&window.history.replaceState(window.history.state,``,`${window.location.pathname}${window.location.search}#${K}`)},[K]);let ze=(e=!0)=>{if(++W.current,U.current=!0,L(!1),v(null),B({}),V({}),H({}),e){let e=new URL(window.location.href);e.searchParams.delete(`lab`),e.searchParams.delete(`service`),e.hash=``,window.history.pushState(null,``,e.pathname+e.search),window.dispatchEvent(new Event(`floci_url_changed`))}},Be=e=>{let t=G.current[e];if(t){let e=t.getBoundingClientRect(),n=window.pageYOffset+e.top-14;window.scrollTo({top:Math.max(0,n),behavior:`smooth`})}},Ve=async e=>{if(!_)return!1;let t=W.current;j(null),F(e.key),H(t=>({...t,[e.key]:!0}));try{let n=await ue(_.service,_.key,e.key);if(t!==W.current)return!1;let r=!!n.verified,i=n.stdout||JSON.stringify(n.verification||n.json||n,null,2);return B(t=>({...t,[e.key]:{status:r?`Verified`:`Not verified`,body:i}})),V(t=>{let n={...t,[e.key]:r};return _.steps?.every(t=>t.key===e.key?r:n[t.key])&&h(_.service,_.key,!0),n}),r}catch(n){return t===W.current&&B(t=>({...t,[e.key]:{status:`Failed`,body:n.message||`Execution error`}})),!1}finally{t===W.current&&F(null)}},He=async()=>{if(!_||I)return;L(!0),U.current=!1;let e=W.current,t=_.steps||[];for(let n=0;nsetTimeout(e,500)),U.current||e!==W.current)break;if(!await Ve(r)){e===W.current&&j(`Run all stopped because this step failed or could not be verified. Review its result and retry.`);break}await new Promise(e=>setTimeout(e,1400))}}e===W.current&&L(!1)},Ue=async()=>{if(!_)return;let e=++W.current;j(null),U.current=!0,L(!1),k(!0);try{if(await se(_.service,_.key),e!==W.current)return;V({}),B({}),h(_.service,_.key,!1)}catch(t){e===W.current&&j(t.message||`Failed to reset lab`)}finally{e===W.current&&k(!1)}},[We,Ge]=(0,A.useState)(!1),[Ke,qe]=(0,A.useState)(!1),[Je,Ye]=(0,A.useState)(null),Xe=async()=>{qe(!0),Ye(null);try{let e=await he();f({}),Ye({type:`success`,text:`Successfully reset all ${e.reset_lab_count||D} labs in the system.`}),Ge(!1),await q()}catch(e){Ye({type:`error`,text:e.message||`Failed to reset all labs`})}finally{qe(!1)}},Ze=[...e].sort((e,t)=>{let n=C(e.service,e.key),r=C(t.service,t.key);return(n?n.id:999)-(r?r.id:999)}).filter(e=>{let t=C(e.service,e.key),n=N===`all`||t?.levelId===N,r=!M||e.title.toLowerCase().includes(M.toLowerCase())||e.service.toLowerCase().includes(M.toLowerCase())||e.description.toLowerCase().includes(M.toLowerCase())||(e.exams||[]).some(e=>je(e).toLowerCase().includes(M.toLowerCase()))||String(t?.id).includes(M);return n&&r}),Qe=n?.exams?.find(e=>e.code===o),$e=o!==J&&(!!Qe||!n&&!i||!!i),et=Object.fromEntries((n?.concepts||[]).map(e=>[e.key,e])),tt=(t,n)=>{let r=e.find(e=>e.service===t&&e.key===n);r&&Y(r)},X=_?C(_.service,_.key):void 0,nt=_?ae(_.service,_.key):void 0,Z=X?X.id:-1,Q=Z>1?S[Z-2]:void 0,$=Z>0&&Z`/labs?service=${encodeURIComponent(e.service)}&lab=${encodeURIComponent(e.key)}#${ce}`,it=t=>n=>{let r=e.find(e=>e.service===t.service&&e.key===t.key);r&&(n.preventDefault(),Y(r))},at=_?.steps?.filter(e=>ke[e.key]||e.status?.verified).length||0,ot=_?.steps?.length||1,st=Math.round(at/ot*100),ct=at===ot&&ot>0||!!d[`${_?.service}:${_?.key}`];return _?(0,R.jsx)(`div`,{id:oe,style:{scrollMarginTop:`16px`},children:(0,R.jsxs)(g,{size:`l`,children:[xe&&(0,R.jsx)(b,{type:`error`,children:xe}),(0,R.jsx)(p,{header:(0,R.jsx)(l,{variant:`h1`,description:_.description,actions:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,R.jsx)(c,{onClick:()=>ze(),children:`← Back to Labs Directory`}),(0,R.jsx)(c,{onClick:Ue,loading:be,disabled:I||P!==null,children:`Reset lab`}),(0,R.jsx)(c,{variant:`primary`,iconName:`caret-right-filled`,onClick:He,loading:I,disabled:ct||be||P!==null,children:ct?`✓ Lab Fully Completed`:I?`Running steps...`:`▶ Run all remaining steps`})]}),children:(0,R.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,flexWrap:`wrap`},children:[X&&(0,R.jsxs)(E,{color:`grey`,children:[`Lesson #`,X.id,` of `,D]}),nt&&(0,R.jsx)(E,{color:nt.color,children:nt.badge}),(0,R.jsx)(E,{color:`blue`,children:_.service.toUpperCase()}),Le(_.exams).map(e=>(0,R.jsx)(`span`,{title:`${e.exam} task ${e.task}${e.primary?` (main focus)`:``}`,children:(0,R.jsx)(E,{color:e.primary?`green`:`grey`,children:je(e)})},`${e.exam}-${e.task}`)),(0,R.jsx)(`span`,{style:{fontSize:`18px`,fontWeight:600},children:_.title}),ct&&(0,R.jsx)(T,{type:`success`,children:`Completed`})]})}),children:(0,R.jsx)(De,{value:st,label:`Lab Step Completion`,description:`${at} of ${ot} steps complete (${st}%)`,status:st===100?`success`:`in-progress`})}),(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsx)(`style`,{children:` .lab-step-completed > div { border-color: #1d8102 !important; box-shadow: 0 0 0 1px #1d8102 !important; } - `}),_.steps?.map((e,t)=>{let n=!!(ke[e.key]||e.status?.verified),r=Oe[e.key];return(0,z.jsx)(`div`,{id:ne(e.key),ref:t=>{G.current[e.key]=t},className:n?`lab-step-completed`:`lab-step-pending`,style:{scrollMarginTop:`16px`,borderRadius:`16px`,transition:`all 0.2s ease`},children:(0,z.jsx)(p,{header:(0,z.jsxs)(l,{variant:`h3`,actions:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[n&&(0,z.jsx)(T,{type:`success`,children:`Verified`}),(0,z.jsx)(c,{variant:n?`normal`:`primary`,iconName:n?`status-positive`:`caret-right-filled`,onClick:()=>Be(e),loading:F===e.key,disabled:n||L||O||F!==null,children:n?`✓ Step Complete`:`Run step`})]}),children:[`Step `,t+1,`: `,e.title]}),children:(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsx)(be,{activeTabId:we,onChange:({detail:e})=>De(e.activeTabId),tabs:[{label:`AWS CLI`,id:`cli`,content:(0,z.jsx)(ye,{language:`cli`,code:e.command})},{label:`Python (boto3)`,id:`boto3`,content:(0,z.jsx)(ye,{language:`boto3`,code:e.snippets?.boto3||e.command})},{label:`Terraform (HCL)`,id:`terraform`,content:(0,z.jsx)(ye,{language:`terraform`,code:e.snippets?.terraform||e.command})}]}),(0,z.jsx)(de,{headerText:`Explain command & concept`,expanded:!!Ae[e.key],onChange:({detail:t})=>H(n=>({...n,[e.key]:t.expanded})),children:(0,z.jsx)(`p`,{style:{lineHeight:`1.6`,margin:0},children:e.explanation})}),r&&(0,z.jsx)(p,{header:(0,z.jsx)(l,{variant:`h3`,children:(0,z.jsxs)(T,{type:r.status===`Failed`?`error`:r.status===`Not verified`?`warning`:`success`,children:[`Execution Result: `,r.status]})}),children:(0,z.jsx)(ye,{language:`json`,code:r.body})})]})})},e.key)})]}),(0,z.jsx)(p,{children:(0,z.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,flexWrap:`wrap`,gap:`12px`},children:[Q?(0,z.jsxs)(c,{href:nt(Q),onFollow:rt(Q),children:[`← Previous: #`,Q.id,`. [`,Q.service.toUpperCase(),`] `,Q.title]}):(0,z.jsx)(`div`,{}),$&&(0,z.jsxs)(c,{variant:`primary`,href:nt($),onFollow:rt($),children:[`Next: #`,$.id,`. [`,$.service.toUpperCase(),`] `,$.title,` →`]})]})})]})}):(0,z.jsxs)(g,{size:`l`,children:[qe&&(0,z.jsx)(y,{type:qe.type,dismissible:!0,onDismiss:()=>X(null),children:qe.text}),(0,z.jsx)(p,{header:(0,z.jsxs)(l,{variant:`h1`,description:`Learn AWS hands-on, from foundations to private networking, developer workflows, governance, and cost, across ${D} lessons in ${b.length} progressive levels.`,actions:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,z.jsx)(c,{variant:`normal`,iconName:`remove`,onClick:()=>We(!0),loading:Ge,children:`Reset All Labs`}),(0,z.jsx)(c,{iconName:`refresh`,onClick:q,loading:te,children:`Refresh catalog`})]}),children:[`AWS Workflow Labs Curriculum (`,D,` Progressive Lessons)`]}),children:(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,z.jsx)(Se,{label:`Study track`,selectedId:Ze?o:Y,onChange:({detail:e})=>u(e.selectedId),options:[{id:Y,text:`Levels`},...(n?.exams||[]).map(e=>({id:e.code,text:`${e.code} ${e.short_title}`}))]})}),Ze?Xe?(0,z.jsx)(Ne,{exam:Xe,concepts:Qe,completed:d,onOpenLab:$e}):i?(0,z.jsxs)(y,{type:`error`,action:(0,z.jsx)(c,{onClick:q,children:`Retry`}),children:[`Could not load the exam study tracks: `,i]}):(0,z.jsx)(T,{type:`loading`,children:`Loading exam study track`}):(0,z.jsxs)(z.Fragment,{children:[(0,z.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,z.jsx)(Se,{selectedId:N,onChange:({detail:e})=>P(e.selectedId),options:[{id:`all`,text:`All ${D} Labs`},...b.map(e=>({id:e.id,text:`${e.title} (${e.labs.length})`}))]})}),(0,z.jsx)(_e,{filteringText:M,filteringPlaceholder:`Filter labs by number (#1-#${D}), service (S3, IAM, EC2, EKS), exam task (SAA 2.1), or keyword...`,onChange:({detail:e})=>Ce(e.filteringText)}),(0,z.jsx)(ve,{cardDefinition:{header:e=>{let t=!!d[`${e.service}:${e.key}`],n=w(e.service,e.key),r=S(e.service,e.key);return(0,z.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`8px`},children:[(0,z.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[n&&(0,z.jsxs)(E,{color:`grey`,children:[`#`,n.id]}),r&&(0,z.jsx)(E,{color:r.color,children:r.badge}),(0,z.jsx)(E,{color:`blue`,children:e.service.toUpperCase()}),Ie(e.exams).filter(e=>e.primary).map(e=>(0,z.jsx)(E,{color:`grey`,children:je(e)},`${e.exam}-${e.task}`)),t&&(0,z.jsx)(E,{color:`green`,children:`✓ COMPLETE`})]}),(0,z.jsx)(c,{variant:`inline-link`,onClick:()=>J(e),children:(0,z.jsxs)(`span`,{style:{display:`inline-flex`,alignItems:`center`,gap:`6px`,fontSize:`15px`},children:[t&&(0,z.jsx)(T,{type:`success`}),(0,z.jsx)(`strong`,{style:{color:t?`#4ec9b0`:`inherit`},children:e.title})]})})]})},sections:[{id:`description`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,z.jsx)(`div`,{style:{borderRadius:`6px`,padding:t?`8px 10px`:void 0,border:t?`1px solid #1d8102`:void 0,background:t?`rgba(29, 129, 2, 0.08)`:void 0},children:e.description})}},{id:`steps`,header:`Runnable Steps`,content:e=>{let t=e.step_count||e.steps?.length||1;return`${t} ${t===1?`step`:`steps`} (CLI / Boto3 / Terraform)`}},{id:`action`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,z.jsx)(c,{variant:t?`normal`:`primary`,iconName:t?`status-positive`:`caret-right-filled`,onClick:()=>J(e),children:t?`Review Completed Lab`:`Start Lab Lesson`})}}]},items:Ye,loading:te&&e.length===0,loadingText:`Loading lab curriculum`,cardsPerRow:[{cards:1},{minWidth:600,cards:3}],empty:ie?(0,z.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,z.jsx)(`b`,{children:`Lab catalog unavailable`}),(0,z.jsx)(`p`,{children:ie}),(0,z.jsx)(c,{onClick:q,children:`Retry`})]}):(0,z.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,z.jsx)(`b`,{children:`No matching labs found`}),(0,z.jsx)(`p`,{children:`Try searching for a different keyword or level filter.`})]})})]})]})}),(0,z.jsx)(ge,{visible:Ue,onDismiss:()=>!Ge&&We(!1),header:`Reset All ${D} Workflow Labs`,footer:(0,z.jsx)(m,{float:`right`,children:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,z.jsx)(c,{variant:`link`,onClick:()=>We(!1),disabled:Ge,children:`Cancel`}),(0,z.jsx)(c,{variant:`primary`,onClick:Je,loading:Ge,children:`Reset All Labs`})]})}),children:(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsxs)(y,{type:`warning`,children:[`This will tear down all lab-created AWS resources across all 20 services (S3 buckets, DynamoDB tables, Lambda functions, IAM roles/users, SQS queues, RDS instances, ECS clusters, EKS clusters, etc.) and clear completion progress for all `,D,` lessons.`]}),(0,z.jsx)(`p`,{children:`Are you sure you want to reset every lab in the system?`})]})})]})};export{Le as LabsConsole}; \ No newline at end of file + `}),_.steps?.map((e,t)=>{let n=!!(ke[e.key]||e.status?.verified),r=z[e.key];return(0,R.jsx)(`div`,{id:te(e.key),ref:t=>{G.current[e.key]=t},className:n?`lab-step-completed`:`lab-step-pending`,style:{scrollMarginTop:`16px`,borderRadius:`16px`,transition:`all 0.2s ease`},children:(0,R.jsx)(p,{header:(0,R.jsxs)(l,{variant:`h3`,actions:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[n&&(0,R.jsx)(T,{type:`success`,children:`Verified`}),(0,R.jsx)(c,{variant:n?`normal`:`primary`,iconName:n?`status-positive`:`caret-right-filled`,onClick:()=>Ve(e),loading:P===e.key,disabled:n||I||be||P!==null,children:n?`✓ Step Complete`:`Run step`})]}),children:[`Step `,t+1,`: `,e.title]}),children:(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsx)(ye,{activeTabId:Te,onChange:({detail:e})=>Oe(e.activeTabId),tabs:[{label:`AWS CLI`,id:`cli`,content:(0,R.jsx)(O,{language:`cli`,code:e.command})},{label:`Python (boto3)`,id:`boto3`,content:(0,R.jsx)(O,{language:`boto3`,code:e.snippets?.boto3||e.command})},{label:`Terraform (HCL)`,id:`terraform`,content:(0,R.jsx)(O,{language:`terraform`,code:e.snippets?.terraform||e.command})}]}),(0,R.jsx)(w,{headerText:`Explain command & concept`,expanded:!!Ae[e.key],onChange:({detail:t})=>H(n=>({...n,[e.key]:t.expanded})),children:(0,R.jsx)(`p`,{style:{lineHeight:`1.6`,margin:0},children:e.explanation})}),r&&(0,R.jsx)(p,{header:(0,R.jsx)(l,{variant:`h3`,children:(0,R.jsxs)(T,{type:r.status===`Failed`?`error`:r.status===`Not verified`?`warning`:`success`,children:[`Execution Result: `,r.status]})}),children:(0,R.jsx)(O,{language:`json`,code:r.body})})]})})},e.key)})]}),(0,R.jsx)(p,{children:(0,R.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,flexWrap:`wrap`,gap:`12px`},children:[Q?(0,R.jsxs)(c,{href:rt(Q),onFollow:it(Q),children:[`← Previous: #`,Q.id,`. [`,Q.service.toUpperCase(),`] `,Q.title]}):(0,R.jsx)(`div`,{}),$&&(0,R.jsxs)(c,{variant:`primary`,href:rt($),onFollow:it($),children:[`Next: #`,$.id,`. [`,$.service.toUpperCase(),`] `,$.title,` →`]})]})})]})}):(0,R.jsxs)(g,{size:`l`,children:[Je&&(0,R.jsx)(b,{type:Je.type,dismissible:!0,onDismiss:()=>Ye(null),children:Je.text}),(0,R.jsx)(p,{header:(0,R.jsxs)(l,{variant:`h1`,description:`Learn AWS hands-on, from foundations to private networking, developer workflows, governance, and cost, across ${D} lessons in ${x.length} progressive levels.`,actions:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,R.jsx)(c,{variant:`normal`,iconName:`remove`,onClick:()=>Ge(!0),loading:Ke,children:`Reset All Labs`}),(0,R.jsx)(c,{iconName:`refresh`,onClick:q,loading:ee,children:`Refresh catalog`})]}),children:[`AWS Workflow Labs Curriculum (`,D,` Progressive Lessons)`]}),children:(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,R.jsx)(Se,{label:`Study track`,selectedId:$e?o:J,onChange:({detail:e})=>u(e.selectedId),options:[{id:J,text:`Levels`},...(n?.exams||[]).map(e=>({id:e.code,text:`${e.code} ${e.short_title}`}))]})}),$e?Qe?(0,R.jsx)(Pe,{exam:Qe,concepts:et,completed:d,onOpenLab:tt}):i?(0,R.jsxs)(b,{type:`error`,action:(0,R.jsx)(c,{onClick:q,children:`Retry`}),children:[`Could not load the exam study tracks: `,i]}):(0,R.jsx)(T,{type:`loading`,children:`Loading exam study track`}):(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,R.jsx)(Se,{selectedId:N,onChange:({detail:e})=>we(e.selectedId),options:[{id:`all`,text:`All ${D} Labs`},...x.map(e=>({id:e.id,text:`${e.title} (${e.labs.length})`}))]})}),(0,R.jsx)(_e,{filteringText:M,filteringPlaceholder:`Filter labs by number (#1-#${D}), service (S3, IAM, EC2, EKS), exam task (SAA 2.1), or keyword...`,onChange:({detail:e})=>Ce(e.filteringText)}),(0,R.jsx)(ve,{cardDefinition:{header:e=>{let t=!!d[`${e.service}:${e.key}`],n=C(e.service,e.key),r=ae(e.service,e.key);return(0,R.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`8px`},children:[(0,R.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[n&&(0,R.jsxs)(E,{color:`grey`,children:[`#`,n.id]}),r&&(0,R.jsx)(E,{color:r.color,children:r.badge}),(0,R.jsx)(E,{color:`blue`,children:e.service.toUpperCase()}),Le(e.exams).filter(e=>e.primary).map(e=>(0,R.jsx)(E,{color:`grey`,children:je(e)},`${e.exam}-${e.task}`)),t&&(0,R.jsx)(E,{color:`green`,children:`✓ COMPLETE`})]}),(0,R.jsx)(c,{variant:`inline-link`,onClick:()=>Y(e),children:(0,R.jsxs)(`span`,{style:{display:`inline-flex`,alignItems:`center`,gap:`6px`,fontSize:`15px`},children:[t&&(0,R.jsx)(T,{type:`success`}),(0,R.jsx)(`strong`,{style:{color:t?`#4ec9b0`:`inherit`},children:e.title})]})})]})},sections:[{id:`description`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,R.jsx)(`div`,{style:{borderRadius:`6px`,padding:t?`8px 10px`:void 0,border:t?`1px solid #1d8102`:void 0,background:t?`rgba(29, 129, 2, 0.08)`:void 0},children:e.description})}},{id:`steps`,header:`Runnable Steps`,content:e=>{let t=e.step_count||e.steps?.length||1;return`${t} ${t===1?`step`:`steps`} (CLI / Boto3 / Terraform)`}},{id:`action`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,R.jsx)(c,{variant:t?`normal`:`primary`,iconName:t?`status-positive`:`caret-right-filled`,onClick:()=>Y(e),children:t?`Review Completed Lab`:`Start Lab Lesson`})}}]},items:Ze,loading:ee&&e.length===0,loadingText:`Loading lab curriculum`,cardsPerRow:[{cards:1},{minWidth:600,cards:3}],empty:re?(0,R.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,R.jsx)(`b`,{children:`Lab catalog unavailable`}),(0,R.jsx)(`p`,{children:re}),(0,R.jsx)(c,{onClick:q,children:`Retry`})]}):(0,R.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,R.jsx)(`b`,{children:`No matching labs found`}),(0,R.jsx)(`p`,{children:`Try searching for a different keyword or level filter.`})]})})]})]})}),(0,R.jsx)(ge,{visible:We,onDismiss:()=>!Ke&&Ge(!1),header:`Reset All ${D} Workflow Labs`,footer:(0,R.jsx)(m,{float:`right`,children:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,R.jsx)(c,{variant:`link`,onClick:()=>Ge(!1),disabled:Ke,children:`Cancel`}),(0,R.jsx)(c,{variant:`primary`,onClick:Xe,loading:Ke,children:`Reset All Labs`})]})}),children:(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsxs)(b,{type:`warning`,children:[`This will tear down all lab-created AWS resources across all 20 services (S3 buckets, DynamoDB tables, Lambda functions, IAM roles/users, SQS queues, RDS instances, ECS clusters, EKS clusters, etc.) and clear completion progress for all `,D,` lessons.`]}),(0,R.jsx)(`p`,{children:`Are you sure you want to reset every lab in the system?`})]})})]})};export{Y as LabsConsole}; \ No newline at end of file diff --git a/frontend/src/pages/LabsConsole.tsx b/frontend/src/pages/LabsConsole.tsx index a7f55af..554e464 100644 --- a/frontend/src/pages/LabsConsole.tsx +++ b/frontend/src/pages/LabsConsole.tsx @@ -111,6 +111,9 @@ export const LabsConsole: React.FC = () => { const labRequestId = useRef(0); const stepRefs = useRef>({}); const [scrollTarget, setScrollTarget] = useState(null); + // The URL listener reads the catalog from here, so a lab link clicked just as the + // catalog arrives (before the listener re-subscribes with the new list) still opens. + const allLabsRef = useRef([]); const loadCatalog = async () => { setLoading(true); @@ -118,6 +121,7 @@ export const LabsConsole: React.FC = () => { try { const data = await fetchLabsCatalog(); const flattened = (data.services || []).flatMap((s) => s.labs); + allLabsRef.current = flattened; setAllLabsList(flattened); setExamError(null); @@ -168,8 +172,9 @@ export const LabsConsole: React.FC = () => { const urlLabKey = params.get('lab'); const urlServiceKey = params.get('service'); - if (urlLabKey && allLabsList.length > 0) { - const found = allLabsList.find((l) => l.key === urlLabKey && (!urlServiceKey || l.service === urlServiceKey)); + const labs = allLabsRef.current; + if (urlLabKey && labs.length > 0) { + const found = labs.find((l) => l.key === urlLabKey && (!urlServiceKey || l.service === urlServiceKey)); if (found) { openLab(found, false); } @@ -184,7 +189,7 @@ export const LabsConsole: React.FC = () => { window.removeEventListener('popstate', handleUrlSync); window.removeEventListener('floci_navigate', handleUrlSync); }; - }, [allLabsList, activeLab]); + }, [activeLab]); const openLab = async (lab: LabDefinition, updateUrl = true) => { cancelRunAllRef.current = true;