Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Thumbs.db
!.env.example
*.pem
*.key
*.p12
*.p8
secrets.json
credentials.json
.watermark-lint.sh
Expand Down
154 changes: 154 additions & 0 deletions desktop/app/loading.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Orcabot</title>
<style>
:root { --bg:#080b12; --fg:#eef2f8; --muted:#8ea0bd; --accent:#5b8cff; --ok:#34d399; --warn:#f59e0b; --err:#ef4444; }
* { box-sizing:border-box; }
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
margin:0; height:100vh; display:flex; align-items:center; justify-content:center;
background: radial-gradient(ellipse 80% 60% at 50% 30%, #0e1730 0%, var(--bg) 70%);
color: var(--fg); -webkit-font-smoothing:antialiased; user-select:none;
}
.wrap { width: 480px; max-width: 90vw; text-align:center; padding: 0 20px; }
.orca { width:76px; height:76px; object-fit:contain; margin:0 auto 18px;
filter: drop-shadow(0 0 26px rgba(91,140,255,.35)); animation: float 4s ease-in-out infinite; }
.orca.stopped { animation:none; filter: drop-shadow(0 0 26px rgba(239,68,68,.3)); }
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-7px)} }
.title { font-size:1.5rem; font-weight:800; letter-spacing:-.02em; }
.stage { margin-top:18px; font-size:1.05rem; font-weight:600; min-height:1.3em; }
.stage.err { color:var(--err); }
.quip { margin-top:6px; font-size:.9rem; color:var(--muted); min-height:1.2em; }
.steps { list-style:none; padding:0; margin:26px auto 0; max-width:320px; text-align:left; }
.steps li { display:flex; align-items:center; gap:10px; padding:6px 2px; font-size:.92rem; color:var(--muted); }
.steps li.up { color:var(--fg); }
.dot { width:16px; height:16px; flex:none; display:inline-flex; align-items:center; justify-content:center; }
.dot .ring { width:12px; height:12px; border-radius:50%; border:2px solid rgba(142,160,189,.35); }
.dot.doing .ring { border-top-color:var(--accent); animation:spin .8s linear infinite; }
.dot.up .ring { border-color:var(--ok); background:var(--ok); }
.dot.err .ring { border-color:var(--err); background:var(--err); }
@keyframes spin { to { transform:rotate(360deg) } }
.foot { margin-top:20px; font-size:.78rem; color:var(--muted); min-height:1.3em; }
.foot .warn { color:var(--warn); }
.diag { margin-top:10px; font-size:.72rem; color:var(--muted); font-family: ui-monospace, Menlo, monospace;
background: rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08); border-radius:8px;
padding:8px 10px; user-select:text; display:none; white-space:pre-wrap; text-align:left; }
.actions { margin-top:18px; display:flex; gap:12px; align-items:center; justify-content:center; min-height:34px; }
.btn { font: inherit; font-size:.85rem; font-weight:600; padding:7px 16px; border-radius:9px; cursor:pointer;
border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.05); color:var(--fg); }
.btn:hover { background:rgba(255,255,255,.1); }
.btn.primary { border-color:transparent; background:linear-gradient(180deg,#cfe0ff,#8fb4ff); color:#06101f; }
.link { color:var(--accent); font-size:.85rem; text-decoration:none; cursor:pointer; }
.link:hover { text-decoration:underline; }
.hidden { display:none !important; }
</style>
</head>
<body>
<div class="wrap">
<img class="orca" id="orca" src="./orca.png" alt="" onerror="this.style.display='none'" />
<div class="title">Orcabot</div>
<div class="stage" id="stage">Warming up the engines…</div>
<div class="quip" id="quip"></div>
<ul class="steps" id="steps"></ul>
<div class="foot"><span id="elapsed">0s</span><span id="hint"></span></div>
<div class="diag" id="diag"></div>
<div class="actions">
<button class="btn hidden" id="retryBtn">Retry</button>
<a class="link hidden" id="openAnyway">Open anyway →</a>
<button class="btn hidden" id="quitBtn">Quit</button>
</div>
</div>
<script>
var REDIRECT = 'http://localhost:8788/dashboards?desktop=1';
var MANUAL_AFTER = 8; // seconds -> offer "open anyway"
var FAIL_AFTER = 60; // seconds with required services down -> error state
// Boot stages in start order. `required` gates the redirect; the VM boots in
// the background so the dashboard can load without it. Quips are lighthearted
// (KSP-style) but the stage label always names the real step.
var STEPS = [
{ id:'db', label:'Local database', url:'http://127.0.0.1:9001/', required:false, slow:8,
quips:['Convincing SQLite to hold still','Filing the bytes alphabetically','Spinning up the ledger'] },
{ id:'web', label:'Web interface', url:'http://127.0.0.1:8788/', required:true, slow:12,
quips:['Painting the cockpit dials','Reticulating splines','Hanging up the dashboards'] },
{ id:'cp', label:'Control plane', url:'http://127.0.0.1:8787/health', required:true, slow:12,
quips:['Assembling mission control','Powering up the flight computer','Briefing the workers'] },
{ id:'vm', label:'Sandbox VM', url:'http://127.0.0.1:8080/health', required:false, slow:20,
quips:['Igniting the boosters 🚀','Spooling up a tiny Linux','Pressurizing the cabin','Waiting on the guidance computer (this can take a minute)'] }
];

var t0 = Date.now(), quipIdx = 0, quipStep = null, doneRedirect = false, failed = false, curStartedAt = {}, cur = null;
var $ = function(id){ return document.getElementById(id); };
STEPS.forEach(function(s){
var li = document.createElement('li'); li.id = 'li-'+s.id;
li.innerHTML = '<span class="dot" id="dot-'+s.id+'"><span class="ring"></span></span><span>'+s.label+'</span>';
$('steps').appendChild(li);
});

function probe(url){ return fetch(url,{mode:'no-cors',cache:'no-store'}).then(function(){return true;}).catch(function(){return false;}); }
function elapsed(){ return Math.round((Date.now()-t0)/1000); }
function current(){
for (var i=0;i<STEPS.length;i++){ if (STEPS[i].required && !STEPS[i].up) return STEPS[i]; }
for (var j=0;j<STEPS.length;j++){ if (!STEPS[j].up) return STEPS[j]; }
return null;
}
function diagText(){
return STEPS.map(function(s){ return s.label+': '+(s.up?'up':'down'); }).join('\n')
+ '\nelapsed: '+elapsed()+'s';
}

function render(){
$('elapsed').textContent = elapsed()+'s';
STEPS.forEach(function(s){
var dot = $('dot-'+s.id), li = $('li-'+s.id);
var cls = 'dot';
if (s.up) cls += ' up';
else if (failed && s.required) cls += ' err';
else if (s === cur) cls += ' doing';
dot.className = cls;
li.className = s.up ? 'up' : '';
});
if (failed){
$('orca').classList.add('stopped');
$('stage').className = 'stage err';
$('stage').textContent = "Orcabot couldn't finish starting up";
$('quip').textContent = 'The background services didn’t come up. A previous instance may still be running, or a port is in use.';
$('hint').textContent = '';
$('diag').style.display = 'block'; $('diag').textContent = diagText();
$('retryBtn').classList.remove('hidden');
$('quitBtn').classList.remove('hidden');
$('openAnyway').classList.remove('hidden');
return;
}
cur = current();
if (!cur){ $('stage').textContent = 'Ready — casting off 🐋'; $('quip').textContent=''; return; }
$('stage').textContent = cur.label + '…';
if (quipStep !== cur.id){ quipStep = cur.id; quipIdx = 0; curStartedAt[cur.id] = curStartedAt[cur.id] || Date.now(); }
$('quip').textContent = cur.quips[quipIdx % cur.quips.length];
var waited = (Date.now() - (curStartedAt[cur.id]||t0))/1000;
$('hint').innerHTML = waited > (cur.slow||12) ? ' · <span class="warn">'+cur.label+' is taking longer than usual…</span>' : '';
if (elapsed() > MANUAL_AFTER){ $('openAnyway').classList.remove('hidden'); }
}

function tick(){
if (failed) return;
Promise.all(STEPS.map(function(s){ return s.up ? Promise.resolve(true) : probe(s.url).then(function(u){ s.up=u; return u; }); }))
.then(function(){
var ready = STEPS.every(function(s){ return !s.required || s.up; });
if (ready && !doneRedirect){ doneRedirect = true; render(); setTimeout(function(){ window.location.replace(REDIRECT); }, 500); return; }
if (!ready && elapsed() >= FAIL_AFTER){ failed = true; }
render();
});
}

$('openAnyway').addEventListener('click', function(){ window.location.replace(REDIRECT); });
$('retryBtn').addEventListener('click', function(){ window.location.reload(); });
$('quitBtn').addEventListener('click', function(){
try { window.__TAURI__.core.invoke('quit_app'); } catch(e) { try { window.close(); } catch(_){} }
});
setInterval(function(){ if(!failed){ quipIdx++; render(); } }, 2600);
render(); tick(); setInterval(tick, 800);
</script>
</body>
</html>
Loading
Loading