@needle-tools/worker-launcher 0.0.0-stage → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/index.js +3 -0
- package/package.json +32 -6
- package/public/app.css +44 -0
- package/public/app.js +124 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +10 -0
- package/public/index.html +79 -0
- package/public/needle-logo.svg +59 -0
- package/public/workspace-controls.css +159 -0
- package/public/workspace-tokens.css +113 -0
- package/scripts/prepare-ui.mjs +9 -0
- package/src/autostart.js +52 -0
- package/src/cli.js +29 -0
- package/src/credentials.js +60 -0
- package/src/docker-client.js +16 -0
- package/src/docker-readiness.js +40 -0
- package/src/fleet.js +28 -0
- package/src/manager.js +112 -0
- package/src/registry-access.js +80 -0
- package/src/registry-metadata.js +14 -0
- package/src/registry-pull.js +29 -0
- package/src/server.js +47 -0
- package/src/supervisor.js +21 -0
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Needle Worker Launcher
|
|
2
|
+
|
|
3
|
+
Run Needle Cloud workers on your computer.
|
|
4
|
+
|
|
5
|
+
Requires Node.js 22+ and Docker with Linux containers.
|
|
6
|
+
|
|
7
|
+
Install or update, then launch:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @needle-tools/worker-launcher@canary
|
|
11
|
+
needle-worker-launcher start
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Open **http://127.0.0.1:8425**. Add your worker and connect your registry account
|
|
15
|
+
in Settings, then start the worker.
|
|
16
|
+
|
|
17
|
+
Preview software. Closing the page leaves workers running; use Stop worker to
|
|
18
|
+
stop them.
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@needle-tools/worker-launcher",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@needle-tools/worker-launcher",
|
|
3
|
+
"version": "1.0.0-alpha.9",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Needle Cloud worker launcher",
|
|
6
|
+
"bin": {
|
|
7
|
+
"needle-worker-launcher": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node scripts/prepare-ui.mjs && node --watch index.js start",
|
|
11
|
+
"start": "node scripts/prepare-ui.mjs && node index.js start",
|
|
12
|
+
"prepack": "node scripts/prepare-ui.mjs",
|
|
13
|
+
"test": "node --test test/*.test.js"
|
|
14
|
+
},
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {},
|
|
18
|
+
"files": [
|
|
19
|
+
"README.md",
|
|
20
|
+
"index.js",
|
|
21
|
+
"src",
|
|
22
|
+
"public",
|
|
23
|
+
"scripts/prepare-ui.mjs"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/public/app.css
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
*{box-sizing:border-box}body{margin:0;background:var(--ui-background);color:var(--ui-text);font:var(--ui-font-size)/1.5 var(--ui-font-family)}a{color:inherit;text-underline-offset:3px}button{cursor:pointer}button:disabled{cursor:default;opacity:.5}[hidden]{display:none!important}.app-header{display:flex;align-items:center;justify-content:space-between;min-height:76px;margin:0 auto;padding:var(--ui-spacing-xl) 32px;border-bottom:1px solid var(--ui-border)}.brand{display:flex;align-items:center;gap:var(--ui-spacing-md);text-decoration:none}.brand-logo{display:block;width:110px;height:33px;object-fit:contain}.brand>span{color:var(--ui-text-secondary);margin-left:var(--ui-spacing-md);font-size:16px}main{margin:40px auto;padding:0 32px}.heading{display:flex;align-items:center;justify-content:space-between;gap:var(--ui-spacing-xl);margin-bottom:32px}h1{font-size:26px;line-height:1.2;letter-spacing:-.6px;margin:0 0 10px;font-weight:600}h2{font-size:16px;font-weight:600;margin:0 0 22px}p{margin:0;color:var(--ui-text-secondary);max-width:72ch}.layout{display:grid;grid-template-columns:340px minmax(0,1fr);gap:40px}.configuration{border-right:1px solid var(--ui-border);padding-right:32px}form{display:flex;flex-direction:column;align-items:stretch;gap:16px}label{display:flex;flex-direction:column;gap:6px;font-weight:500}input,select{width:100%;min-width:0;font:inherit;color:var(--ui-input-text);background:var(--ui-input-background);border:1px solid var(--ui-input-border);border-radius:var(--ui-border-radius);padding:9px 10px;min-height:38px}input:focus-visible,select:focus-visible{outline:2px solid var(--ui-border-focus);outline-offset:1px}form>button{align-self:flex-start}form>a{font-size:12px;margin-top:-9px}.resources{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resources label{font-size:12px}.hint{font-size:12px;color:var(--ui-text-secondary);font-weight:400}#key-state{margin-top:-10px}.section-heading{display:flex;justify-content:space-between;gap:16px;align-items:baseline}.state{font-size:12px;color:var(--ui-text-secondary)}.state[data-online=true]{color:var(--ui-success)}.state[data-online=false]{color:var(--ui-error)}.runtime{display:grid;gap:12px;margin:0 0 24px}.runtime>div{display:grid;gap:3px}dt{font-size:12px;color:var(--ui-text-secondary)}dd{margin:0;overflow-wrap:anywhere}.actions{display:flex;gap:var(--ui-spacing-md);margin-bottom:12px}.table-wrap{overflow:auto;margin-top:24px}table{border-collapse:collapse;width:100%;text-align:left}th{font-weight:500;color:var(--ui-text-secondary);font-size:12px}td,th{padding:12px 10px;border-bottom:1px solid var(--ui-border)}td:first-child,th:first-child{padding-left:0}td{overflow-wrap:anywhere}td:last-child{font-size:12px;max-width:260px}.empty{padding:38px 10px!important;text-align:center;color:var(--ui-text-secondary)}#notice{padding:12px 16px;border:1px solid var(--ui-border);border-radius:var(--ui-border-radius);margin-bottom:24px;background:var(--ui-background-elevated)}#notice[data-error=true]{border-color:var(--ui-danger-border);color:var(--ui-error)}.confirm-stop{display:flex;flex-direction:row;align-items:center;font-size:12px;font-weight:400}.confirm-stop input{width:16px;min-height:16px;height:16px;accent-color:var(--ui-error)}footer{border-top:1px solid var(--ui-border);margin-top:40px;padding-top:20px;color:var(--ui-text-secondary);font-size:12px;max-width:90ch}@media(max-width:850px){.layout{grid-template-columns:1fr;gap:32px}.configuration{border-right:0;border-bottom:1px solid var(--ui-border);padding:0 0 28px}main{margin-top:28px;padding:0 20px}.app-header{padding:16px 20px}.brand>span{font-size:13px}h1{font-size:23px}}@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|
|
2
|
+
|
|
3
|
+
.resources label{display:grid;grid-template-rows:36px auto;gap:0;align-items:start}.resources input{align-self:end}
|
|
4
|
+
|
|
5
|
+
.app-header,main{max-width:1280px}
|
|
6
|
+
|
|
7
|
+
.heading>.actions{margin:0;align-items:center}.actions{align-items:center}.actions>a:not(.button){font-size:12px;margin-left:var(--ui-spacing-md)}.empty-worker{padding:48px 0;border-top:1px solid var(--ui-border);border-bottom:1px solid var(--ui-border);display:grid;justify-items:start;gap:var(--ui-spacing-xl)}.empty-worker h2{margin:0}.empty-worker p{max-width:58ch}.worker-summary{display:flex;justify-content:space-between;gap:var(--ui-spacing-xl);align-items:baseline;margin-top:0;margin-bottom:24px;padding-top:0}.worker-summary h2{margin:0 0 var(--ui-spacing-md);overflow-wrap:anywhere}.worker-summary p{font-size:12px}.worker-details{margin-top:24px}.worker-details summary{cursor:pointer;color:var(--ui-text-secondary)}.worker-details .runtime{margin-top:16px}.back-link{display:inline-block;margin-bottom:20px;font-size:12px}.setup-form{gap:24px}.setup-form fieldset{border:0;margin:0;padding:0;min-width:0}.setup-sections{display:grid;grid-template-columns:minmax(280px,1fr) minmax(280px,1fr);gap:48px;max-width:980px}.setup-sections>section{display:flex;flex-direction:column;gap:18px}.setup-sections h2{margin:0}.setup-locked{padding:16px;border:1px solid var(--ui-border);border-radius:var(--ui-border-radius);margin-bottom:24px}.setup-form fieldset:disabled{opacity:.65}#key-state{margin-top:0}@media(max-width:750px){.setup-sections{grid-template-columns:1fr;gap:32px}.worker-summary{flex-direction:column;gap:12px}.heading{align-items:flex-start}.heading>.actions{flex-wrap:wrap;justify-content:flex-end}}
|
|
8
|
+
|
|
9
|
+
.header-actions,.header-actions nav{display:flex;align-items:center;gap:var(--ui-spacing-xl)}.header-actions nav a{text-decoration:none;color:var(--ui-text-secondary);padding:var(--ui-spacing-sm) 0}.header-actions nav a[aria-current=page]{color:var(--ui-text);box-shadow:0 2px var(--ui-primary)}.launcher-settings{max-width:620px;padding-top:var(--ui-spacing-lg);border-top:1px solid var(--ui-border)}.launcher-settings h2{margin-bottom:0}.toggle-setting{display:flex;flex-direction:row;align-items:center;gap:var(--ui-spacing-md)}.toggle-setting input{width:18px;height:18px;min-height:18px;margin:0;accent-color:var(--ui-primary)}@media(max-width:600px){.app-header{flex-wrap:wrap;gap:var(--ui-spacing-lg)}.header-actions{margin-left:auto}.brand>span{font-size:12px;margin-left:0}}
|
|
10
|
+
|
|
11
|
+
.settings-columns{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:48px;align-items:start}.settings-columns>*{min-width:0}.registry-settings{display:flex;flex-direction:column;gap:var(--ui-spacing-xl)}.registry-images{min-width:0;padding-top:var(--ui-spacing-lg);border-top:1px solid var(--ui-border)}#registry-versions{list-style:none;padding:0;max-height:360px;overflow:auto}#registry-versions li{display:flex;justify-content:space-between;align-items:center;gap:var(--ui-spacing-md);padding:var(--ui-spacing-sm) 0;border-bottom:1px solid var(--ui-border)}#registry-versions code{overflow-wrap:anywhere;min-width:0}#registry-image-state[data-error=true]{color:var(--ui-error)}.registry-settings p{text-wrap:balance}@media(max-width:750px){.settings-columns{grid-template-columns:1fr;gap:32px}}
|
|
12
|
+
|
|
13
|
+
.initial-loading{display:flex;justify-content:center;padding:var(--ui-spacing-lg);margin-bottom:var(--ui-spacing-lg)}.loading-spinner{display:block;width:22px;height:22px;border:2px solid var(--ui-border);border-top-color:var(--ui-text-secondary);border-radius:50%;animation:launcher-spin .8s linear infinite}@keyframes launcher-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.loading-spinner{animation:none}}
|
|
14
|
+
|
|
15
|
+
body{min-height:100dvh;display:flex;flex-direction:column}main{width:100%;flex:1;display:flex;flex-direction:column;margin-bottom:0}main>footer{position:fixed;inset:auto 0 0;z-index:90;margin:0;padding:12px max(32px,calc((100vw - 1216px)/2));max-width:none;display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:16px;background:var(--ui-background)}main>section:not([hidden]){margin-bottom:40px}footer p{font:inherit;color:inherit;max-width:none}#docker-state{color:inherit}
|
|
16
|
+
|
|
17
|
+
.app-header{position:sticky;top:0;z-index:var(--z-header,100);width:100%;flex-shrink:0;gap:var(--ui-spacing-xl);background:var(--ui-background)}.brand{flex-shrink:0;white-space:nowrap}.header-actions{margin-left:auto;flex-shrink:0}@media(max-width:750px){.app-header{flex-wrap:wrap}.header-actions{gap:var(--ui-spacing-lg)}}
|
|
18
|
+
|
|
19
|
+
#supervision-state{display:inline-flex;align-items:center;gap:var(--ui-spacing-sm)}#supervision-spinner{width:14px;height:14px;flex:none}#status-refresh{margin:0}
|
|
20
|
+
|
|
21
|
+
h1,h2,p,.confirm-stop{text-wrap:balance}#overview-endpoint{overflow-wrap:anywhere;font-size:var(--ui-font-size-sm)}
|
|
22
|
+
#start,#stop{display:inline-flex;align-items:center;gap:var(--ui-spacing-sm)}#start-spinner,#stop-spinner{width:14px;height:14px;flex:none}
|
|
23
|
+
.launcher-toast{position:fixed;inset:auto 24px 24px auto;z-index:1000;max-width:min(32rem,calc(100vw - 32px));display:flex;align-items:center;gap:var(--ui-spacing-md);padding:var(--ui-spacing-md) var(--ui-spacing-lg);border:1px solid var(--ui-border);border-radius:var(--ui-border-radius);background:var(--ui-background-elevated);box-shadow:0 6px 24px #0002}
|
|
24
|
+
#notice.launcher-toast{margin:0}#notice-close{flex:none;border:0;background:transparent;box-shadow:none;padding:0 4px;min-height:0;font-size:20px;color:inherit}
|
|
25
|
+
|
|
26
|
+
.overview-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--ui-spacing-lg);margin-bottom:24px}.overview-toolbar #status-refresh{margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
27
|
+
|
|
28
|
+
.worker-card{padding:24px 0}td.empty{border-bottom:0!important}.launcher-dialog{width:min(440px,calc(100vw - 32px));padding:24px;border:1px solid var(--ui-border);border-radius:var(--ui-border-radius);background:var(--ui-background-elevated);color:var(--ui-text);box-shadow:0 12px 40px #0003}.launcher-dialog::backdrop{background:#0005}.launcher-dialog .actions{justify-content:flex-end;margin-top:24px}
|
|
29
|
+
|
|
30
|
+
footer{border-top:0}#worker-cards [data-field="supervision-state"]{display:inline-flex;align-items:center;gap:var(--ui-spacing-sm)}#worker-cards [data-field="supervision-spinner"],#worker-cards [data-field="start-spinner"],#worker-cards [data-field="stop-spinner"]{width:14px;height:14px;flex:none}#worker-cards [data-field="stop-controls"]{display:flex;align-items:center;margin-left:auto}#worker-cards [data-field="start"],#worker-cards [data-field="stop"]{display:inline-flex;align-items:center;gap:var(--ui-spacing-sm)}
|
|
31
|
+
|
|
32
|
+
.worker-header-actions{display:flex;align-items:center;gap:var(--ui-spacing-md)}.worker-header-actions>button,.worker-header-actions>.button{display:inline-flex;align-items:center;justify-content:center;gap:var(--ui-spacing-sm);white-space:nowrap;text-decoration:none}.worker-header-actions svg{width:16px;height:16px;flex:none}@media(max-width:600px){.header-actions{flex-wrap:wrap;justify-content:flex-end}.worker-header-actions{flex-wrap:wrap}}
|
|
33
|
+
|
|
34
|
+
.worker-card tbody tr:last-child>td{border-bottom:0}
|
|
35
|
+
|
|
36
|
+
.worker-card>.actions{flex-wrap:wrap}
|
|
37
|
+
|
|
38
|
+
#worker-cards [data-field="supervision-state"]{min-height:20px;line-height:20px}#worker-cards [data-field="supervision-spinner"][hidden]{display:block!important;visibility:hidden;animation:none}
|
|
39
|
+
|
|
40
|
+
main{padding-bottom:72px}#docker-state{margin-left:auto;text-align:right;white-space:nowrap}.launcher-toast{bottom:72px}@media(max-width:850px){main>footer{padding-inline:20px}}@media(max-width:480px){main>footer{font-size:10px;gap:8px}#status-refresh{text-wrap:balance}}
|
|
41
|
+
|
|
42
|
+
#registry-connect{display:inline-flex;align-items:center;gap:var(--ui-spacing-sm)}#registry-save-spinner{width:14px;height:14px;flex:none}#registry-versions{padding:0 14px 0 0;scrollbar-width:thin;scrollbar-color:var(--ui-border) transparent;scrollbar-gutter:stable}#registry-versions::-webkit-scrollbar{width:6px}#registry-versions::-webkit-scrollbar-track{background:transparent}#registry-versions::-webkit-scrollbar-thumb{background:var(--ui-border);border-radius:6px}main>footer{background:color-mix(in srgb,var(--ui-background) 80%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
|
|
43
|
+
|
|
44
|
+
.registry-image-info{display:grid;gap:4px;min-width:0}.registry-image-info>code{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:48ch;font-size:11px;color:var(--ui-text-secondary)}.registry-image-info strong{font-size:13px;font-weight:500}.registry-image-info details{font-size:12px}.registry-image-info summary{cursor:pointer;color:var(--ui-text-secondary)}.registry-image-info details p{margin-top:8px;overflow-wrap:anywhere}#registry-versions li{align-items:flex-start;padding-block:12px}#registry-versions li>button{flex:none}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const form=document.querySelector('#settings');
|
|
2
|
+
const $=id=>document.getElementById(id);
|
|
3
|
+
const route=location.pathname,setupPage=route==='/setup',settingsPage=route==='/settings';
|
|
4
|
+
let initialized=false,working=false,state,polling=false,pendingAction=null,noticeTimer;
|
|
5
|
+
function notice(message,error=false,source='action'){clearTimeout(noticeTimer);$('notice-message').textContent=message;$('notice').dataset.error=String(error);$('notice').dataset.source=source;$('notice').hidden=false;if(!error&&source==='action')noticeTimer=setTimeout(()=>$('notice').hidden=true,6000);}
|
|
6
|
+
$('notice-close').addEventListener('click',()=>{clearTimeout(noticeTimer);$('notice').hidden=true;});
|
|
7
|
+
async function api(path,body){const options=body===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)};let response=await fetch('/api/'+path,options);if(response.status===401){const renewed=await fetch('/api/session',{headers:{'X-Launcher-Session':'refresh'}});if(renewed.ok)response=await fetch('/api/'+path,options);}const data=await response.json();if(!response.ok)throw new Error(data.error||'Launcher request failed.');return data;}
|
|
8
|
+
const selectedId=new URLSearchParams(location.search).get('worker');
|
|
9
|
+
$('worker-header-actions').hidden=setupPage||settingsPage;
|
|
10
|
+
const cards=new Map();let profiles=[],stopTarget=null;
|
|
11
|
+
const field=(card,id)=>card.querySelector(`[data-field="${id}"]`);
|
|
12
|
+
function buttons(){
|
|
13
|
+
const selected=profiles.find(profile=>profile.profileId===selectedId);
|
|
14
|
+
$('save').disabled=working||!!selected?.busy||!!selected?.enabled||!!selected?.containers.length;$('refresh').disabled=working;$('remove-worker').hidden=!selected;$('remove-worker').disabled=working||!!selected?.busy||!!selected?.enabled||!!selected?.containers.length;
|
|
15
|
+
for(const profile of profiles){const card=cards.get(profile.profileId);if(!card)continue;const blocked=working||profile.busy;
|
|
16
|
+
field(card,'start').disabled=!!blocked||!profile.configured||(profile.enabled&&profile.supervision?.phase!=='error');
|
|
17
|
+
field(card,'stop').disabled=!!blocked;
|
|
18
|
+
const starting=pendingAction?.path==='start'&&pendingAction.profileId===profile.profileId,stopping=pendingAction?.path==='stop'&&pendingAction.profileId===profile.profileId;
|
|
19
|
+
field(card,'start-spinner').hidden=!starting;field(card,'stop-spinner').hidden=!stopping;
|
|
20
|
+
field(card,'start-label').textContent=starting?'Starting...':profile.enabled?'Retry now':'Start worker';field(card,'stop-label').textContent=stopping?'Stopping...':'Stop and remove containers';
|
|
21
|
+
field(card,'start').setAttribute('aria-busy',String(starting));field(card,'stop').setAttribute('aria-busy',String(stopping));
|
|
22
|
+
if(stopping){field(card,'supervision-label').textContent='Stopping containers';field(card,'supervision-spinner').hidden=false;}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function cloudUrl(environment){return environment==='production'?'https://cloud.needle.tools/admin/workers':'https://cloud-staging.needle.tools/admin/workers';}
|
|
26
|
+
function renderDockerFooter(value){$('docker-state').hidden=false;$('docker-state').textContent=value.docker?.available?`Docker ${value.docker.version} / ${value.docker.architecture}`:'Docker unavailable';}
|
|
27
|
+
function renderCard(profile){
|
|
28
|
+
let card=cards.get(profile.profileId);if(!card){card=$('worker-template').content.firstElementChild.cloneNode(true);card.dataset.profileId=profile.profileId;
|
|
29
|
+
if(!cards.size)for(const element of card.querySelectorAll('[data-field]'))element.id=element.dataset.field;
|
|
30
|
+
cards.set(profile.profileId,card);$('worker-cards').append(card);
|
|
31
|
+
field(card,'start').addEventListener('click',()=>action('start',{profileId:profile.profileId},'Worker enabled.'));
|
|
32
|
+
field(card,'stop').addEventListener('click',()=>{stopTarget=profile.profileId;$('stop-dialog').showModal();});
|
|
33
|
+
}
|
|
34
|
+
const name=profile.workerName||profile.settings.workerId||'Worker';field(card,'worker-name').textContent=name;card.setAttribute('aria-label',name);
|
|
35
|
+
const total=profile.settings.count>1?` / ${profile.settings.count*profile.settings.cpus} CPUs and ${profile.settings.count*profile.settings.memory} GB total`:'';
|
|
36
|
+
field(card,'worker-summary').textContent=`${profile.settings.environment==='production'?'Production':'Staging'} / ${profile.containers.filter(container=>container.state==='running').length} of ${profile.settings.count} instances running / ${profile.settings.cpus} CPUs and ${profile.settings.memory} GB per container${total}`;
|
|
37
|
+
field(card,'overview-endpoint').textContent=profile.endpoint||'';field(card,'edit-configuration').href='/setup?worker='+encodeURIComponent(profile.profileId);field(card,'cloud-worker').href=cloudUrl(profile.settings.environment);
|
|
38
|
+
const phase=profile.supervision?.phase||(profile.enabled?'running':'paused');
|
|
39
|
+
field(card,'supervision-label').textContent=({running:'Containers running',paused:'Stopped',checking:'Preparing Docker',recovering:'Preparing worker containers',error:'Needs attention'})[phase]||phase;
|
|
40
|
+
field(card,'supervision-spinner').hidden=!['checking','recovering'].includes(phase);field(card,'supervision-state').setAttribute('aria-busy',String(['checking','recovering'].includes(phase)));
|
|
41
|
+
if(profile.supervision?.error)notice(profile.supervision.error,true,'background');else if(profile.credentialError)notice(profile.credentialError,true,'background');
|
|
42
|
+
const rows=field(card,'container-rows');rows.replaceChildren();if(!profile.containers.length){const row=document.createElement('tr'),cell=document.createElement('td');cell.colSpan=3;cell.className='empty';cell.textContent=profile.enabled?'Waiting for worker containers...':'This worker is stopped.';row.append(cell);rows.append(row);}
|
|
43
|
+
for(const container of profile.containers){const row=document.createElement('tr');for(const text of [container.name,container.status,container.image]){const cell=document.createElement('td');cell.textContent=text;row.append(cell);}rows.append(row);}
|
|
44
|
+
field(card,'stop-controls').hidden=!profile.containers.length&&!profile.enabled;
|
|
45
|
+
}
|
|
46
|
+
function render(value){
|
|
47
|
+
state=value;profiles=Array.isArray(value.profiles)?value.profiles:value.settings?.workerId?[{...value,profileId:'legacy'}]:[];
|
|
48
|
+
const selected=profiles.find(profile=>profile.profileId===selectedId),setup=selected||{settings:{environment:'staging',workerId:'',count:1,cpus:2,memory:4},containers:[],imageTag:'',keyProtection:value.keyProtection};
|
|
49
|
+
$('initial-loading').hidden=true;
|
|
50
|
+
if(!initialized){for(const [key,item] of Object.entries(setup.settings))if(form.elements.namedItem(key))form.elements.namedItem(key).value=item;const version=form.elements.imageTag;if(setup.imageTag&&!Array.from(version.options).some(option=>option.value===setup.imageTag)){const option=document.createElement('option');option.value=setup.imageTag;option.textContent=setup.imageTag;version.append(option);}version.value=setup.imageTag||'';initialized=true;}
|
|
51
|
+
$('cloud-admin').href=cloudUrl(form.elements.environment.value);renderDockerFooter(selected||profiles[0]||value);
|
|
52
|
+
$('endpoint').textContent=setup.endpoint||(setup.settings.environment==='production'?'https://cloud.needle.tools/api':'https://cloud-staging.needle.tools/api');$('image').textContent=setup.image||'Environment channel';$('protection').textContent=setup.keyProtection||'OS credential store';
|
|
53
|
+
$('key-state').textContent=setup.credentialError||(setup.configured?'Key is set. Leave blank to keep it.':'Enter the one-time key from Cloud.');
|
|
54
|
+
$('empty-worker').hidden=profiles.length>0;$('configured-worker').hidden=!profiles.length;$('setup-heading').textContent=selected?'Edit worker configuration':'Add worker';
|
|
55
|
+
$('setup-locked').hidden=!setup.enabled&&!setup.containers.length;$('setup-fields').disabled=!!setup.enabled||setup.containers.length>0;
|
|
56
|
+
for(const profile of profiles)renderCard(profile);for(const [id,card] of cards)if(!profiles.some(profile=>profile.profileId===id)){card.remove();cards.delete(id);}
|
|
57
|
+
const updated=new Date();$('status-updated').dateTime=updated.toISOString();$('status-updated').textContent=updated.toLocaleTimeString();buttons();
|
|
58
|
+
}
|
|
59
|
+
async function refresh(){if(polling||working)return;polling=true;try{render(await api('status'));}catch(error){notice(error.message,true);}finally{polling=false;}}
|
|
60
|
+
async function action(path,body,message){working=true;pendingAction={path,profileId:body?.profileId};buttons();try{await api(path,body);if(path==='settings'||path==='remove'){form.elements.workerKey.value='';location.assign('/');return;}notice(message);}catch(error){notice(error.message,true);}finally{working=false;pendingAction=null;await refresh();buttons();}}
|
|
61
|
+
form.elements.environment.addEventListener('change',()=>{form.elements.workerKey.value='';$('cloud-admin').href=cloudUrl(form.elements.environment.value);});
|
|
62
|
+
form.addEventListener('submit',event=>{event.preventDefault();const data=Object.fromEntries(new FormData(form));for(const key of ['count','cpus','memory'])data[key]=Number(data[key]);if(selectedId)data.profileId=selectedId;action('settings',data,'Worker configuration saved.');});
|
|
63
|
+
document.addEventListener('keydown',event=>{if(setupPage&&(event.ctrlKey||event.metaKey)&&event.key.toLowerCase()==='s'){event.preventDefault();if(!event.repeat&&!$('save').disabled&&!document.querySelector('dialog[open]'))form.requestSubmit();}});
|
|
64
|
+
$('remove-worker').addEventListener('click',()=>$('remove-dialog').showModal());
|
|
65
|
+
$('remove-cancel').addEventListener('click',()=>$('remove-dialog').close());
|
|
66
|
+
$('remove-confirm').addEventListener('click',()=>{$('remove-dialog').close();if(selectedId)void action('remove',{profileId:selectedId,confirmed:true},'Configuration removed.');});
|
|
67
|
+
$('stop-cancel').addEventListener('click',()=>$('stop-dialog').close());
|
|
68
|
+
$('stop-confirm').addEventListener('click',()=>{const profileId=stopTarget;$('stop-dialog').close();if(profileId)void action('stop',{confirmed:true,profileId},'Worker stopped. Automatic recovery is paused.');});
|
|
69
|
+
$('stop-dialog').addEventListener('close',()=>{stopTarget=null;});
|
|
70
|
+
$('refresh').addEventListener('click',refresh);
|
|
71
|
+
$('workers-view').hidden=settingsPage||setupPage;$('setup-view').hidden=!setupPage;$('settings-view').hidden=!settingsPage;
|
|
72
|
+
$(settingsPage?'settings-nav':'workers-nav').setAttribute('aria-current','page');
|
|
73
|
+
if(settingsPage){
|
|
74
|
+
void api('status').then(renderDockerFooter).catch(()=>{});
|
|
75
|
+
$('initial-loading').hidden=true;
|
|
76
|
+
const checkbox=$('autostart-enabled'),save=$('autostart-save'),status=$('autostart-state');
|
|
77
|
+
async function loadAutostart(){try{const value=await api('autostart');checkbox.checked=value.enabled;checkbox.disabled=!value.supported;save.disabled=!value.supported;status.textContent=value.supported?(value.enabled?'Enabled for your next sign-in.':'Not enabled.') : value.reason;}catch(error){status.textContent=error.message;}}
|
|
78
|
+
$('autostart-form').addEventListener('submit',async event=>{
|
|
79
|
+
event.preventDefault();save.disabled=true;checkbox.disabled=true;
|
|
80
|
+
notice('Updating startup setting...',false,'progress');
|
|
81
|
+
try{const value=await api('autostart',{enabled:checkbox.checked});notice(value.enabled?'The launcher will start in the background when you next sign in.':'The launcher will no longer start when you sign in.');}
|
|
82
|
+
catch(error){notice(error.message,true);}
|
|
83
|
+
finally{await loadAutostart();}
|
|
84
|
+
});
|
|
85
|
+
await loadAutostart();
|
|
86
|
+
}else{await refresh();setInterval(refresh,5000);}
|
|
87
|
+
|
|
88
|
+
let registryWorking=false;
|
|
89
|
+
function registryMessage(message,error=false){const element=$('registry-image-state');element.textContent=message;element.dataset.error=String(error);}
|
|
90
|
+
async function registryStatus(){
|
|
91
|
+
const value=await api('registry');
|
|
92
|
+
if(value.configured||!$('registry-user').value)$('registry-user').value=value.userId||'';$('registry-key').placeholder=value.configured?'******** (saved key)':'';
|
|
93
|
+
$('registry-state').textContent=value.configured?`Saved for ${value.userId} (${value.protection}). Refresh versions to check current access.`:'Not connected. Enter your registry user ID and API key.';
|
|
94
|
+
$('registry-forget').disabled=registryWorking||!value.configured;$('registry-refresh').disabled=registryWorking||!value.configured;
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
async function loadWorkerVersions(){
|
|
98
|
+
const result=await api('registry/list',{});
|
|
99
|
+
const select=form.elements.imageTag,current=select.value;select.replaceChildren();
|
|
100
|
+
for(const [value,label] of [['','Environment channel (recommended)'],...result.tags.map(tag=>[tag,tag])]){const option=document.createElement('option');option.value=value;option.textContent=label;select.append(option);}
|
|
101
|
+
if(current&&!result.tags.includes(current)){const option=document.createElement('option');option.value=current;option.textContent=current+' (saved)';select.append(option);}select.value=current;
|
|
102
|
+
$('worker-version-state').textContent=`${result.tags.length} available versions. Downloading does not update running workers.`;
|
|
103
|
+
const list=$('registry-versions');list.replaceChildren();
|
|
104
|
+
const ordered=[...result.tags].sort((a,b)=>{const rank=tag=>tag==='staging'||tag==='production'?0:/^v?\d+\.\d+/.test(tag)?1:tag.startsWith('digest-')?3:2;return rank(a)-rank(b)||b.localeCompare(a,undefined,{numeric:true});});
|
|
105
|
+
for(const tag of ordered){const row=document.createElement('li'),info=document.createElement('div'),label=document.createElement('strong'),raw=document.createElement('code'),download=document.createElement('button'),details=document.createElement('details'),summary=document.createElement('summary'),meta=document.createElement('p');
|
|
106
|
+
label.textContent=tag==='staging'?'Staging channel':tag==='production'?'Production channel':/^v?\d+\.\d+/.test(tag)?'Version '+tag:tag.startsWith('sha-')?'Build '+tag.slice(4,16):tag.startsWith('digest-')?'Image '+tag.slice(7,19):tag;
|
|
107
|
+
raw.textContent=tag;raw.title=tag;summary.textContent='Build details';meta.className='hint';details.append(summary,meta);let loaded=false;
|
|
108
|
+
details.addEventListener('toggle',async()=>{if(!details.open||loaded)return;loaded=true;meta.textContent='Loading metadata...';try{const value=await api('registry/details',{tag});const parts=[value.title,value.version?'Version '+value.version:'',value.platform,value.created?'Built '+value.created:'',value.revision?'Revision '+value.revision:''].filter(Boolean);meta.textContent=parts.length?parts.join(' ? '):'This image does not publish version or build metadata.';}catch(error){loaded=false;meta.textContent=error.message;}});
|
|
109
|
+
download.type='button';download.textContent='Download image';download.title='Download this image to Docker?s local cache. Does not start, replace or update worker containers.';download.dataset.tag=tag;download.addEventListener('click',()=>registryAction(async()=>{await api('registry/install',{tag});registryMessage(`Downloaded ${result.repository}:${tag}. Choose it in worker setup to use it.`);}));info.className='registry-image-info';info.append(label,raw,details);row.append(info,download);list.append(row);}
|
|
110
|
+
registryMessage(result.tags.length?`${result.tags.length} worker versions available.`:'No worker versions are published for this repository.');
|
|
111
|
+
}
|
|
112
|
+
async function registryAction(operation,saving=false){
|
|
113
|
+
if(registryWorking)return;registryWorking=true;if(saving){$('registry-save-spinner').hidden=false;$('registry-save-label').textContent='Saving credentials...';$('registry-connect').setAttribute('aria-busy','true');$('registry-user').disabled=true;$('registry-key').disabled=true;}
|
|
114
|
+
const controls=[$('registry-connect'),$('registry-forget'),$('registry-refresh'),$('worker-versions-refresh'),...document.querySelectorAll('#registry-versions button')];controls.forEach(control=>control.disabled=true);
|
|
115
|
+
try{await operation();}catch(error){registryMessage(error.message,true);if(setupPage)$('worker-version-state').textContent=error.message;}
|
|
116
|
+
finally{registryWorking=false;if(saving){$('registry-save-spinner').hidden=true;$('registry-save-label').textContent='Connect registry';$('registry-connect').removeAttribute('aria-busy');$('registry-user').disabled=false;$('registry-key').disabled=false;}controls.forEach(control=>control.disabled=false);if(settingsPage)try{await registryStatus();}catch(error){registryMessage(error.message,true);}}
|
|
117
|
+
}
|
|
118
|
+
$('worker-versions-refresh').addEventListener('click',()=>registryAction(loadWorkerVersions));
|
|
119
|
+
$('registry-form').addEventListener('submit',event=>{event.preventDefault();const userId=$('registry-user').value.trim(),apiKey=$('registry-key').value;registryMessage('Checking access and saving protected credentials...');void registryAction(async()=>{await api('registry/connect',{userId,apiKey});$('registry-key').value='';await registryStatus();await loadWorkerVersions();},true);});
|
|
120
|
+
$('registry-refresh').addEventListener('click',()=>registryAction(loadWorkerVersions));
|
|
121
|
+
$('registry-forget').addEventListener('click',()=>$('forget-dialog').showModal());
|
|
122
|
+
$('forget-cancel').addEventListener('click',()=>$('forget-dialog').close());
|
|
123
|
+
$('forget-confirm').addEventListener('click',()=>{$('forget-dialog').close();void registryAction(async()=>{await api('registry/forget',{});$('registry-key').value='';$('registry-user').value='';$('registry-versions').replaceChildren();registryMessage('Registry credentials removed. Existing local images and running containers are unchanged.');});});
|
|
124
|
+
if(settingsPage){try{await registryStatus();}catch(error){registryMessage(error.message,true);}}
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title">
|
|
2
|
+
<title id="title">Needle Worker Launcher</title>
|
|
3
|
+
<!-- Brand icon: 64-unit grid, 6-unit inset, filled faceted worker cube with an upward launch arrow. Facet joins are intentional. -->
|
|
4
|
+
<g data-join="true">
|
|
5
|
+
<path fill="#7fbf45" d="M6 28 30 15 54 28 30 41Z"/>
|
|
6
|
+
<path fill="#238c69" d="M6 28 30 41V59L6 46Z"/>
|
|
7
|
+
<path fill="#166653" d="M30 41 54 28V46L30 59Z"/>
|
|
8
|
+
</g>
|
|
9
|
+
<path fill="#f0df22" d="m45 5 13 15h-8v15H40V20h-8Z"/>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Needle Worker Launcher</title><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="alternate icon" href="/favicon.ico"><link rel="stylesheet" href="/workspace-tokens.css"><link rel="stylesheet" href="/workspace-controls.css"><link rel="stylesheet" href="/app.css"><script type="module" src="/app.js"></script></head>
|
|
4
|
+
<body class="workspace-page workspace-controls">
|
|
5
|
+
<header class="app-header"><a class="brand" href="/" aria-label="Needle Worker Launcher"><img class="brand-logo" src="/needle-logo.svg" width="110" height="33" alt="Needle"><span>Worker Launcher</span></a><div class="header-actions"><div id="worker-header-actions" class="worker-header-actions"><button id="refresh" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.3 6.7"/><path d="M20 4v7h-7"/></svg><span>Refresh</span></button><a class="button" id="setup-link" href="/setup"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span>Add worker</span></a></div><nav aria-label="Launcher"><a href="/" id="workers-nav">Workers</a><a href="/settings" id="settings-nav">Settings</a></nav></div></header>
|
|
6
|
+
<main>
|
|
7
|
+
<div id="notice" class="launcher-toast" role="status" aria-live="polite" hidden><span id="notice-message"></span><button id="notice-close" type="button" aria-label="Dismiss message">×</button></div>
|
|
8
|
+
<section id="workers-view" aria-labelledby="workers-heading">
|
|
9
|
+
<h1 id="workers-heading" class="sr-only">Workers</h1>
|
|
10
|
+
|
|
11
|
+
<div id="initial-loading" class="initial-loading" role="status" aria-label="Loading worker status"><span class="loading-spinner" aria-hidden="true"></span></div>
|
|
12
|
+
<div id="empty-worker" class="empty-worker" hidden><h2>Add your first worker</h2><p>Connect a worker created in Cloud admin. Choose its environment, credentials and the number of containers during setup.</p><a class="button" href="/setup">Set up worker</a></div>
|
|
13
|
+
<section id="configured-worker" aria-label="Worker configurations" hidden><div id="worker-cards"></div></section>
|
|
14
|
+
<template id="worker-template"><article class="worker-card">
|
|
15
|
+
<div class="worker-summary"><div><h2 data-field="worker-name">Worker</h2><p data-field="worker-summary"></p><p class="hint"><span data-field="overview-endpoint"></span></p></div><span data-field="supervision-state" class="state" role="status" aria-live="polite"><span data-field="supervision-spinner" class="loading-spinner" aria-hidden="true" hidden></span><span data-field="supervision-label"></span></span></div>
|
|
16
|
+
<div class="actions"><button data-field="start" class="ui-button-primary ui-button-save" type="button" disabled><span data-field="start-spinner" class="loading-spinner" aria-hidden="true" hidden></span><span data-field="start-label">Start worker</span></button><a class="button" data-field="edit-configuration" href="/setup">Edit configuration</a><a data-field="cloud-worker" href="https://cloud-staging.needle.tools/admin/workers" target="_blank" rel="noreferrer">Manage permissions in Cloud</a><div data-field="stop-controls" hidden><button type="button" class="ui-button-danger ui-button-danger-subtle" data-field="stop" disabled><span data-field="stop-spinner" class="loading-spinner" aria-hidden="true" hidden></span><span data-field="stop-label">Stop and remove containers</span></button></div></div>
|
|
17
|
+
<div class="table-wrap"><table><thead><tr><th>Container</th><th>Status</th><th>Image</th></tr></thead><tbody data-field="container-rows"><tr><td colspan="3">Loading containers...</td></tr></tbody></table></div>
|
|
18
|
+
|
|
19
|
+
</article></template>
|
|
20
|
+
|
|
21
|
+
</section>
|
|
22
|
+
<section id="setup-view" hidden aria-labelledby="setup-heading">
|
|
23
|
+
<a class="back-link" href="/">Back to workers</a>
|
|
24
|
+
<div class="heading"><div><h1 id="setup-heading">Set up worker</h1><p>Connect this computer to a named worker in Cloud admin.</p></div></div>
|
|
25
|
+
<p id="setup-locked" class="setup-locked" hidden>Stop and remove this worker's containers before changing its setup. <a href="/">Manage running workers</a></p>
|
|
26
|
+
<form id="settings" class="setup-form">
|
|
27
|
+
<fieldset id="setup-fields" disabled><div class="setup-sections">
|
|
28
|
+
<section aria-labelledby="connection-heading"><h2 id="connection-heading">Cloud connection</h2>
|
|
29
|
+
<label>Environment<select name="environment"><option value="staging">Staging</option><option value="production">Production</option></select></label>
|
|
30
|
+
<p class="hint">Create a named worker in <a id="cloud-admin" href="https://cloud-staging.needle.tools/admin/workers" target="_blank" rel="noreferrer">Cloud admin</a>, then paste its ID and one-time key below.</p>
|
|
31
|
+
<label>Worker ID<input name="workerId" autocomplete="off" spellcheck="false" placeholder="worker-..." required></label>
|
|
32
|
+
<label>Worker key<input name="workerKey" type="password" autocomplete="new-password" placeholder="Paste the one-time key from Cloud" spellcheck="false"></label>
|
|
33
|
+
<p class="hint" id="key-state">The key is protected by your operating system and restored after sign-in.</p>
|
|
34
|
+
</section>
|
|
35
|
+
<section aria-labelledby="resources-heading"><h2 id="resources-heading">Resources</h2>
|
|
36
|
+
<label>Worker version<select name="imageTag"><option value="">Environment channel (recommended)</option></select></label>
|
|
37
|
+
<div class="actions"><button id="worker-versions-refresh" type="button">Load available versions</button><a href="/settings">Registry settings</a></div>
|
|
38
|
+
<p id="worker-version-state" class="hint">The environment channel is used unless you choose a specific version. Stop and remove containers before changing versions.</p>
|
|
39
|
+
<div class="resources"><label>Worker instances<input name="count" type="number" min="1" max="32" value="1" required></label><label>CPUs per container<input name="cpus" type="number" min="1" max="128" value="2" required></label><label>Memory per container (GB)<input name="memory" type="number" min="1" max="512" value="4" required></label></div>
|
|
40
|
+
<p class="hint">Each instance is a separate container and can process one job at a time. All instances share this worker identity and configuration. CPU and memory limits apply to each instance. CPU workers support URL imports, optimization and HDRI compression. Team access and priority are controlled in Cloud.</p>
|
|
41
|
+
<p class="hint">After saving, choose Start worker on the overview. The launcher then keeps this many containers running, including after it restarts.</p>
|
|
42
|
+
</section>
|
|
43
|
+
</div></fieldset>
|
|
44
|
+
<div class="actions"><button type="submit" id="save" disabled>Save worker</button><button type="button" id="remove-worker" class="ui-button-danger" hidden>Remove configuration</button><a href="/">Cancel</a></div>
|
|
45
|
+
</form>
|
|
46
|
+
<details class="worker-details"><summary>Connection details</summary><dl class="runtime"><div><dt>API endpoint</dt><dd id="endpoint">-</dd></div><div><dt>Worker image</dt><dd id="image">-</dd></div><div><dt>Credential protection</dt><dd id="protection">-</dd></div></dl></details>
|
|
47
|
+
</section>
|
|
48
|
+
<section id="settings-view" hidden aria-labelledby="settings-heading">
|
|
49
|
+
<div class="heading"><div><h1 id="settings-heading">Launcher settings</h1><p>Settings for this computer and your user account.</p></div></div>
|
|
50
|
+
<div class="settings-columns">
|
|
51
|
+
<form id="autostart-form" class="launcher-settings"><h2>Startup</h2>
|
|
52
|
+
<label class="toggle-setting"><input id="autostart-enabled" type="checkbox" disabled><span>Start launcher when I sign in</span></label>
|
|
53
|
+
<p>Run the launcher in the background after signing in. Previously enabled workers recover automatically, including starting Docker Desktop if needed.</p>
|
|
54
|
+
<p id="autostart-state" class="hint">Checking startup registration...</p>
|
|
55
|
+
<p class="hint">Missing Docker installations or Linux service permissions are reported on the Workers page. The launcher does not install Docker or request administrator privileges.</p>
|
|
56
|
+
<button type="submit" id="autostart-save" disabled>Save startup setting</button>
|
|
57
|
+
</form>
|
|
58
|
+
<section class="registry-settings" aria-labelledby="registry-heading">
|
|
59
|
+
<form id="registry-form" class="launcher-settings"><h2 id="registry-heading">Container registry</h2>
|
|
60
|
+
<p>Connect to container.needle.tools to list and download Needle worker images. Use your Logto user ID and a Zot API key from the registry's Account > API Keys menu.</p>
|
|
61
|
+
<label for="registry-user">Registry user ID</label><input id="registry-user" autocomplete="username" spellcheck="false" required maxlength="256">
|
|
62
|
+
<label for="registry-key">Zot API key</label><input id="registry-key" type="password" autocomplete="new-password" spellcheck="false" required maxlength="4096">
|
|
63
|
+
<p id="registry-state" class="hint" role="status">Checking registry connection...</p>
|
|
64
|
+
<div class="actions"><button id="registry-connect" type="submit"><span id="registry-save-spinner" class="loading-spinner" aria-hidden="true" hidden></span><span id="registry-save-label">Connect registry</span></button><button id="registry-forget" class="ui-button-danger" type="button" disabled>Forget credentials</button></div>
|
|
65
|
+
<p class="hint">The key is protected by your OS credential store, never saved in settings or sent to worker containers. Read access to needle-cloud/worker is required. Existing Docker logins are unchanged.</p>
|
|
66
|
+
</form>
|
|
67
|
+
<div class="registry-images"><div class="section-heading"><h2>Available worker images</h2><button id="registry-refresh" type="button" disabled>Refresh versions</button></div>
|
|
68
|
+
<p id="registry-image-state" class="hint" role="status">Connect your registry account to browse versions.</p><ul id="registry-versions"></ul>
|
|
69
|
+
<p class="hint">Download caches an image locally. It does not replace running workers. Select a version in worker setup after stopping and removing existing containers. No remote images are deleted.</p>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
<footer><p id="status-refresh" class="hint">Last updated <time id="status-updated">not yet</time>. Refreshes every 5 seconds.</p><span id="docker-state" hidden></span></footer>
|
|
75
|
+
<dialog id="forget-dialog" aria-labelledby="forget-title" class="launcher-dialog workspace-controls"><h2 id="forget-title">Forget registry credentials?</h2><p>This removes the saved registry key from this computer. Downloaded images and running workers are kept.</p><div class="actions"><button id="forget-cancel" type="button">Cancel</button><button id="forget-confirm" class="ui-button-danger" type="button">Forget credentials</button></div></dialog>
|
|
76
|
+
<dialog id="remove-dialog" aria-labelledby="remove-title" class="launcher-dialog workspace-controls"><h2 id="remove-title">Remove worker configuration?</h2><p>This removes this launcher configuration and its saved worker key. The worker account in Cloud is kept.</p><div class="actions"><button id="remove-cancel" type="button">Cancel</button><button id="remove-confirm" class="ui-button-danger" type="button">Remove configuration</button></div></dialog>
|
|
77
|
+
<dialog id="stop-dialog" aria-labelledby="stop-dialog-title" class="launcher-dialog workspace-controls"><h2 id="stop-dialog-title">Stop worker?</h2><p>This stops and removes its containers and pauses automatic recovery. Active jobs on these containers will be interrupted.</p><div class="actions"><button id="stop-cancel" type="button">Cancel</button><button id="stop-confirm" class="ui-button-danger" type="button">Stop worker</button></div></dialog>
|
|
78
|
+
</main>
|
|
79
|
+
</body></html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
+
<svg width="100%" height="100%" viewBox="0 0 509 154" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
4
|
+
<g transform="matrix(1.00797,0,0,1.00797,-452.406,-63.7089)">
|
|
5
|
+
<path d="M665.95,132.73L665.95,177.61L655.39,169.21C654.59,168.57 654.19,167.77 654.19,166.81L654.19,134.41C654.19,127.93 650.07,124.69 641.83,124.69C639.67,124.69 637.65,125.09 635.77,125.89C633.89,126.69 632.23,127.69 630.79,128.89C629.35,130.09 628.23,131.39 627.43,132.79C626.63,134.19 626.23,135.49 626.23,136.69L626.23,177.61L615.55,169.21C614.83,168.57 614.47,167.77 614.47,166.81L614.47,113.05L625.39,121.57C625.71,121.81 625.95,122.01 626.11,122.17C626.27,122.33 626.47,122.49 626.71,122.65C627.67,121.45 628.85,120.37 630.25,119.41C631.65,118.45 633.17,117.65 634.81,117.01C636.45,116.37 638.15,115.87 639.91,115.51C641.67,115.15 643.35,114.97 644.95,114.97C646.39,114.97 647.87,115.01 649.39,115.09C650.91,115.17 652.23,115.37 653.35,115.69C657.91,116.81 661.15,118.81 663.07,121.69C664.99,124.57 665.95,128.25 665.95,132.73Z" style="fill-rule:nonzero;"/>
|
|
6
|
+
</g>
|
|
7
|
+
<g transform="matrix(1.00797,0,0,1.00797,-452.406,-63.7089)">
|
|
8
|
+
<path d="M732.38,146.05C732.38,146.93 732.4,147.55 732.44,147.91C732.48,148.27 732.42,148.89 732.26,149.77L725.18,149.77C723.1,149.77 720.74,149.75 718.1,149.71C715.46,149.67 712.74,149.65 709.94,149.65L687.86,149.65C687.86,152.53 688.42,155.01 689.54,157.09C690.66,159.17 692.14,160.89 693.98,162.25C695.82,163.61 697.92,164.61 700.28,165.25C702.64,165.89 705.02,166.21 707.42,166.21C710.46,166.21 713.32,165.45 716,163.93C718.68,162.41 720.94,160.41 722.78,157.93C723.42,158.49 724.32,159.41 725.48,160.69C726.64,161.97 728.42,163.89 730.82,166.45C728.02,169.81 724.6,172.47 720.56,174.43C716.52,176.39 712.14,177.37 707.42,177.37C702.7,177.37 698.5,176.73 694.58,175.45C690.66,174.17 687.26,172.21 684.38,169.57C681.5,166.93 679.26,163.59 677.66,159.55C676.06,155.51 675.26,150.73 675.26,145.21C675.26,140.25 675.92,135.79 677.24,131.83C678.56,127.87 680.46,124.51 682.94,121.75C685.42,118.99 688.38,116.85 691.82,115.33C695.26,113.81 699.14,113.05 703.46,113.05C709.22,113.05 713.98,113.93 717.74,115.69C721.5,117.45 724.46,119.85 726.62,122.89C728.78,125.93 730.28,129.43 731.12,133.39C731.96,137.35 732.38,141.57 732.38,146.05ZM702.98,123.25C700.82,123.41 698.82,123.97 696.98,124.93C695.14,125.89 693.56,127.13 692.24,128.65C690.92,130.17 689.88,131.93 689.12,133.93C688.36,135.93 687.98,138.05 687.98,140.29L721.1,140.29C721.1,138.29 720.88,136.23 720.44,134.11C720,131.99 719.14,130.09 717.86,128.41C716.58,126.73 714.76,125.39 712.4,124.39C710.04,123.39 706.9,123.01 702.98,123.25Z" style="fill-rule:nonzero;"/>
|
|
9
|
+
</g>
|
|
10
|
+
<g transform="matrix(1.00797,0,0,1.00797,-452.406,-63.7089)">
|
|
11
|
+
<path d="M795.93,146.05C795.93,146.93 795.95,147.55 795.99,147.91C796.03,148.27 795.97,148.89 795.81,149.77L788.73,149.77C786.65,149.77 784.29,149.75 781.65,149.71C779.01,149.67 776.29,149.65 773.49,149.65L751.41,149.65C751.41,152.53 751.97,155.01 753.09,157.09C754.21,159.17 755.69,160.89 757.53,162.25C759.37,163.61 761.47,164.61 763.83,165.25C766.19,165.89 768.57,166.21 770.97,166.21C774.01,166.21 776.87,165.45 779.55,163.93C782.23,162.41 784.49,160.41 786.33,157.93C786.97,158.49 787.87,159.41 789.03,160.69C790.19,161.97 791.97,163.89 794.37,166.45C791.57,169.81 788.15,172.47 784.11,174.43C780.07,176.39 775.69,177.37 770.97,177.37C766.25,177.37 762.05,176.73 758.13,175.45C754.21,174.17 750.81,172.21 747.93,169.57C745.05,166.93 742.81,163.59 741.21,159.55C739.61,155.51 738.81,150.73 738.81,145.21C738.81,140.25 739.47,135.79 740.79,131.83C742.11,127.87 744.01,124.51 746.49,121.75C748.97,118.99 751.93,116.85 755.37,115.33C758.81,113.81 762.69,113.05 767.01,113.05C772.77,113.05 777.53,113.93 781.29,115.69C785.05,117.45 788.01,119.85 790.17,122.89C792.33,125.93 793.83,129.43 794.67,133.39C795.51,137.35 795.93,141.57 795.93,146.05ZM766.53,123.25C764.37,123.41 762.37,123.97 760.53,124.93C758.69,125.89 757.11,127.13 755.79,128.65C754.47,130.17 753.43,131.93 752.67,133.93C751.91,135.93 751.53,138.05 751.53,140.29L784.65,140.29C784.65,138.29 784.43,136.23 783.99,134.11C783.55,131.99 782.69,130.09 781.41,128.41C780.13,126.73 778.31,125.39 775.95,124.39C773.59,123.39 770.45,123.01 766.53,123.25Z" style="fill-rule:nonzero;"/>
|
|
12
|
+
</g>
|
|
13
|
+
<g transform="matrix(1.00797,0,0,1.00797,-452.406,-63.7089)">
|
|
14
|
+
<path d="M858.57,97.21C859.21,97.69 859.53,98.37 859.53,99.25L859.53,174.13C859.45,175.17 859.41,176.25 859.41,177.37C857.57,175.85 855.85,174.45 854.25,173.17C852.89,172.05 851.59,170.99 850.35,169.99C849.11,168.99 848.29,168.33 847.89,168.01C846.13,170.49 843.63,172.45 840.39,173.89C837.15,175.33 833.37,176.05 829.05,176.05C825.21,176.05 821.65,175.35 818.37,173.95C815.09,172.55 812.23,170.51 809.79,167.83C807.35,165.15 805.45,161.89 804.09,158.05C802.73,154.21 802.05,149.89 802.05,145.09C802.05,140.77 802.83,136.75 804.39,133.03C805.95,129.31 807.99,126.11 810.51,123.43C813.03,120.75 815.89,118.65 819.09,117.13C822.29,115.61 825.57,114.85 828.93,114.85C831.49,114.85 833.75,115.07 835.71,115.51C837.67,115.95 839.39,116.57 840.87,117.37C842.35,118.17 843.65,119.11 844.77,120.19C845.89,121.27 846.93,122.41 847.89,123.61L847.89,88.57L858.57,97.21ZM830.61,165.13C834.21,165.13 837.13,164.45 839.37,163.09C841.61,161.73 843.35,160.03 844.59,157.99C845.83,155.95 846.69,153.77 847.17,151.45C847.65,149.13 847.89,147.01 847.89,145.09L847.89,143.89C847.89,142.77 847.67,141.19 847.23,139.15C846.79,137.11 845.95,135.09 844.71,133.09C843.47,131.09 841.71,129.39 839.43,127.99C837.15,126.59 834.21,125.97 830.61,126.13C827.17,126.13 824.35,126.87 822.15,128.35C819.95,129.83 818.19,131.61 816.87,133.69C815.55,135.77 814.63,137.89 814.11,140.05C813.59,142.21 813.33,143.97 813.33,145.33C813.33,147.17 813.57,149.25 814.05,151.57C814.53,153.89 815.41,156.05 816.69,158.05C817.97,160.05 819.73,161.73 821.97,163.09C824.21,164.45 827.09,165.13 830.61,165.13Z" style="fill-rule:nonzero;"/>
|
|
15
|
+
</g>
|
|
16
|
+
<g transform="matrix(1.00797,0,0,1.00797,-452.406,-63.7089)">
|
|
17
|
+
<path d="M882.81,97.09C883.45,97.57 883.77,98.21 883.77,99.01L883.65,140.05L883.65,177.13L873.09,168.73C872.37,168.09 872.01,167.29 872.01,166.33L872.01,88.45L882.81,97.09Z" style="fill-rule:nonzero;"/>
|
|
18
|
+
</g>
|
|
19
|
+
<g transform="matrix(1.00797,0,0,1.00797,-452.406,-63.7089)">
|
|
20
|
+
<path d="M950.36,146.05C950.36,146.93 950.38,147.55 950.42,147.91C950.46,148.27 950.4,148.89 950.24,149.77L943.16,149.77C941.08,149.77 938.72,149.75 936.08,149.71C933.44,149.67 930.72,149.65 927.92,149.65L905.84,149.65C905.84,152.53 906.4,155.01 907.52,157.09C908.64,159.17 910.12,160.89 911.96,162.25C913.8,163.61 915.9,164.61 918.26,165.25C920.62,165.89 923,166.21 925.4,166.21C928.44,166.21 931.3,165.45 933.98,163.93C936.66,162.41 938.92,160.41 940.76,157.93C941.4,158.49 942.3,159.41 943.46,160.69C944.62,161.97 946.4,163.89 948.8,166.45C946,169.81 942.58,172.47 938.54,174.43C934.5,176.39 930.12,177.37 925.4,177.37C920.68,177.37 916.48,176.73 912.56,175.45C908.64,174.17 905.24,172.21 902.36,169.57C899.48,166.93 897.24,163.59 895.64,159.55C894.04,155.51 893.24,150.73 893.24,145.21C893.24,140.25 893.9,135.79 895.22,131.83C896.54,127.87 898.44,124.51 900.92,121.75C903.4,118.99 906.36,116.85 909.8,115.33C913.24,113.81 917.12,113.05 921.44,113.05C927.2,113.05 931.96,113.93 935.72,115.69C939.48,117.45 942.44,119.85 944.6,122.89C946.76,125.93 948.26,129.43 949.1,133.39C949.94,137.35 950.36,141.57 950.36,146.05ZM920.96,123.25C918.8,123.41 916.8,123.97 914.96,124.93C913.12,125.89 911.54,127.13 910.22,128.65C908.9,130.17 907.86,131.93 907.1,133.93C906.34,135.93 905.96,138.05 905.96,140.29L939.08,140.29C939.08,138.29 938.86,136.23 938.42,134.11C937.98,131.99 937.12,130.09 935.84,128.41C934.56,126.73 932.74,125.39 930.38,124.39C928.02,123.39 924.88,123.01 920.96,123.25Z" style="fill-rule:nonzero;"/>
|
|
21
|
+
</g>
|
|
22
|
+
<g transform="matrix(0.806379,0,0,0.806379,200.028,1.4214)">
|
|
23
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
24
|
+
<path d="M79.32,36.98L79.32,187.74L95,174.54L101.59,18.23L79.32,36.98Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
|
25
|
+
</g>
|
|
26
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
27
|
+
<path d="M79.32,36.98L57.05,18.23L63.64,174.54L79.32,187.74L79.32,36.98Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
|
|
28
|
+
</g>
|
|
29
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
30
|
+
<path d="M25.19,104.83L33.82,153.87L46.32,138.92L43.86,82.5L25.19,104.83Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
|
31
|
+
</g>
|
|
32
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
33
|
+
<path d="M25.19,104.83L0,90.24L16.97,144.1L33.82,153.87L25.19,104.83Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
|
|
34
|
+
</g>
|
|
35
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
36
|
+
<path d="M43.86,82.5L18.69,67.98L0,90.24L25.18,104.83L43.86,82.5Z" style="fill:rgb(153,204,51);fill-rule:nonzero;"/>
|
|
37
|
+
</g>
|
|
38
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
39
|
+
<path d="M134.82,78.69L124.85,135.19L140.43,126.15L160,64.1L134.82,78.69Z" style="fill:url(#_Linear5);fill-rule:nonzero;"/>
|
|
40
|
+
</g>
|
|
41
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
42
|
+
<path d="M134.82,78.69L116.14,56.36L113.28,121.36L124.85,135.19L134.82,78.69Z" style="fill:url(#_Linear6);fill-rule:nonzero;"/>
|
|
43
|
+
</g>
|
|
44
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
45
|
+
<path d="M160,64.1L141.31,41.84L116.14,56.36L134.81,78.69L160,64.1Z" style="fill:rgb(255,225,19);fill-rule:nonzero;"/>
|
|
46
|
+
</g>
|
|
47
|
+
<g transform="matrix(1,0,0,1,-245.129,-0.406369)">
|
|
48
|
+
<path d="M101.59,18.23L79.32,0L57.05,18.23L79.32,36.98L101.59,18.23Z" style="fill:rgb(243,230,0);fill-rule:nonzero;"/>
|
|
49
|
+
</g>
|
|
50
|
+
</g>
|
|
51
|
+
<defs>
|
|
52
|
+
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.84,-162.96,162.96,0.84,89.64,184.81)"><stop offset="0" style="stop-color:rgb(98,211,153);stop-opacity:1"/><stop offset="0.51" style="stop-color:rgb(172,216,66);stop-opacity:1"/><stop offset="0.9" style="stop-color:rgb(215,219,10);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(215,219,10);stop-opacity:1"/></linearGradient>
|
|
53
|
+
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.6,-162.13,162.13,-1.6,69.68,178.9)"><stop offset="0" style="stop-color:rgb(11,163,152);stop-opacity:1"/><stop offset="0.5" style="stop-color:rgb(76,163,82);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,163,10);stop-opacity:1"/></linearGradient>
|
|
54
|
+
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.9,-67.98,67.98,-1.9,36.6,152.17)"><stop offset="0" style="stop-color:rgb(54,163,130);stop-opacity:1"/><stop offset="0.19" style="stop-color:rgb(54,163,130);stop-opacity:1"/><stop offset="0.54" style="stop-color:rgb(73,164,89);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,163,11);stop-opacity:1"/></linearGradient>
|
|
55
|
+
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.18,-62.38,62.38,2.18,15.82,153.24)"><stop offset="0" style="stop-color:rgb(38,120,128);stop-opacity:1"/><stop offset="0.51" style="stop-color:rgb(69,122,92);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(113,117,22);stop-opacity:1"/></linearGradient>
|
|
56
|
+
<linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(13.85,-71.96,71.96,13.85,135.08,135.43)"><stop offset="0" style="stop-color:rgb(176,217,57);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(234,219,4);stop-opacity:1"/></linearGradient>
|
|
57
|
+
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(26.159,-64.7372,64.7372,26.159,107.418,128.145)"><stop offset="0" style="stop-color:rgb(116,175,82);stop-opacity:1"/><stop offset="0.17" style="stop-color:rgb(116,175,82);stop-opacity:1"/><stop offset="0.48" style="stop-color:rgb(153,190,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(192,196,10);stop-opacity:1"/></linearGradient>
|
|
58
|
+
</defs>
|
|
59
|
+
</svg>
|