diff --git a/coordinator/internal/agent/releasewheel.go b/coordinator/internal/agent/releasewheel.go index a8f0f45..29cac5f 100644 --- a/coordinator/internal/agent/releasewheel.go +++ b/coordinator/internal/agent/releasewheel.go @@ -38,6 +38,9 @@ func NormalizePEP440(version string) string { // with a generous timeout: wheels can be several MB. func DownloadWheel(ctx context.Context, url, dir string) (string, error) { target := filepath.Join(dir, wheelNameFromURL(url)) + if err := os.MkdirAll(dir, 0o700); err != nil { + return "", fmt.Errorf("download wheel: %w", err) + } ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) diff --git a/coordinator/internal/agent/setupui/server.go b/coordinator/internal/agent/setupui/server.go index 57e74c5..6b3a15d 100644 --- a/coordinator/internal/agent/setupui/server.go +++ b/coordinator/internal/agent/setupui/server.go @@ -479,6 +479,12 @@ type installRuntimeResponse struct { // step. The venv python path is returned for the wizard to bake into the // task runner. func (s *Server) handleInstallRuntime(w http.ResponseWriter, r *http.Request) { + // The wizard may run the install before any config was saved, so the + // worker directory (venv, wheel) may not exist yet. + if err := os.MkdirAll(s.dir, 0o700); err != nil { + writeJSON(w, http.StatusConflict, map[string]string{"error": "could not create the worker directory"}) + return + } var req installRuntimeRequest if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"}) diff --git a/coordinator/internal/agent/setupui/template.html b/coordinator/internal/agent/setupui/template.html index 758786b..43c8c73 100644 --- a/coordinator/internal/agent/setupui/template.html +++ b/coordinator/internal/agent/setupui/template.html @@ -249,8 +249,8 @@ $('b4').onclick=async()=>{ showStatus(); }; -const checkRow=(name,ok,detail,ms)=> - '
'+(ok===null?'':ok?'':'')+'
'+name+''+(detail||'')+'
'+(ms?''+ms+' ms':'')+'
'; +const checkRow=(name,ok,detail,ms,action)=> + '
'+(ok===null?'':ok?'':'')+'
'+name+''+(detail||'')+'
'+(ms?''+ms+' ms':'')+(action?action:'')+'
'; async function runChecks(){ const box=$('checks'); box.innerHTML=checkRow('Coordinator reachable','',null,null)+checkRow('Python 3','',null,null)+checkRow('scimesh package','',null,null); @@ -260,8 +260,21 @@ async function runChecks(){ const items=[r.data.coordinator,r.data.python,r.data.scimesh]; for(const item of items){ if(item&&!item.ok)checksOk=false; - box.insertAdjacentHTML('beforeend',checkRow(item?item.name:'?',item?item.ok:null,item?item.detail:'',item?item.latency_ms:null)); + let action=''; + if(item&&item.name==='scimesh'&&!item.ok){ + action='
'; + } + box.insertAdjacentHTML('beforeend',checkRow(item?item.name:'?',item?item.ok:null,item?item.detail:'',item?item.latency_ms:null,action)); } + const btn=$('install-scimesh'); + if(btn)btn.onclick=async()=>{ + const src=$('in-pkg')?$('in-pkg').value.trim():''; + btn.disabled=true;btn.textContent='Installing…'; + const ir=await postJSON('/api/runtime/install',{scimesh_package:src}); + if(ir.status!==200){err('Could not install scimesh: '+(ir.data.error||'unknown error'));btn.disabled=false;btn.textContent='Install';return} + state.venvPython=ir.data.python; + runChecks(); + }; $('b3').disabled=!checksOk; }