Files
spoolweight/index.html
T

137 lines
6.1 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0b0f17">
<title>Spool Putaway</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: #0b0f17; color: #e5e7eb; -webkit-text-size-adjust: 100%;
}
.wrap {
max-width: 560px; margin: 0 auto;
padding: max(16px, env(safe-area-inset-top)) 16px calc(16px + env(safe-area-inset-bottom));
min-height: 100dvh; display: flex; flex-direction: column; gap: 16px;
}
#result { flex: 1; display: flex; flex-direction: column; gap: 16px; justify-content: center; }
#buttons { display: flex; flex-direction: column; gap: 14px; }
.btn {
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; font-weight: 650; padding: 28px; border-radius: 18px;
border: none; background: #2563eb; color: #fff; cursor: pointer; width: 100%;
}
.btn.secondary { background: #1f2937; color: #e5e7eb; }
.btn:active { filter: brightness(.9); }
input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.busy { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.spinner { width: 64px; height: 64px; border: 6px solid #1f2937; border-top-color: #2563eb; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.busyText { color: #9ca3af; font-size: 1.1rem; }
.timer { color: #4b5563; font-variant-numeric: tabular-nums; }
.card { background: #111827; border: 1px solid #1f2937; border-radius: 18px; padding: 18px; }
.hero { text-align: center; padding: 24px 18px; }
.hero .label { color: #9ca3af; font-size: 1rem; letter-spacing: .03em; }
.hero .loc { font-size: 6rem; font-weight: 800; line-height: 1; margin: 6px 0; }
.hero.ok .loc { color: #34d399; }
.hero.warn .loc { color: #fbbf24; font-size: 2.4rem; }
.status { font-size: 1.25rem; font-weight: 650; }
.ok { color: #34d399; } .warn { color: #fbbf24; } .err { color: #f87171; } .muted { color: #9ca3af; }
.rows { margin-top: 10px; display: flex; justify-content: space-between; align-items: baseline; font-size: 1.05rem; }
.rows .k { color: #9ca3af; }
.rows .v { text-align: right; font-variant-numeric: tabular-nums; }
.big { font-size: 1.6rem; font-weight: 750; }
.arrow { color: #6b7280; }
.note { font-size: .95rem; color: #9ca3af; }
a { color: #60a5fa; }
.errcard { border-color: #7f1d1d; background: #1b1113; }
</style>
</head>
<body>
<div class="wrap">
<div id="result"></div>
<div id="buttons">
<label class="btn">Photo<input type="file" accept="image/*" id="photo"></label>
</div>
</div>
<script>
(function () {
const result = document.getElementById('result');
const buttons = document.getElementById('buttons');
const photo = document.getElementById('photo');
let tick = null;
function upload(file) {
buttons.style.display = 'none';
result.innerHTML = '<div class="busy"><div class="spinner"></div><div class="busyText">Processing…</div><div class="timer" id="timer">0s</div></div>';
photo.value = '';
const t0 = Date.now();
const timer = document.getElementById('timer');
tick = setInterval(() => { timer.textContent = Math.round((Date.now() - t0) / 1000) + 's'; }, 250);
const fd = new FormData();
fd.append('photo', file);
fetch('/process', { method: 'POST', body: fd })
.then(r => r.json())
.then(render)
.catch(() => render({ error: 'network error — try again' }))
.finally(() => { clearInterval(tick); buttons.style.display = 'flex'; });
}
photo.addEventListener('change', e => e.target.files[0] && upload(e.target.files[0]));
const g = n => Math.round(n) + ' g';
function render(res) {
if (res.error) {
result.innerHTML = '<div class="card errcard"><div class="status err">Couldn\'t process</div><p class="note">' + escapeHtml(res.error) + '</p></div>';
return;
}
const hasLoc = res.location && res.location.length;
const nw = res.new_weight || {}, pw = res.previous_weight || {};
let html = '<div class="card hero ' + (hasLoc ? 'ok' : 'warn') + '">' +
'<div class="label">Put it away at</div>' +
'<div class="loc">' + (hasLoc ? escapeHtml(res.location) : 'no location') + '</div></div>';
let status, rows = '';
if (res.previous_weight === undefined) {
status = '<div class="status warn">spooldb unavailable</div>';
if (nw.total != null) rows += row('On the scale', g(nw.total), true);
} else if (res.updated) {
status = '<div class="status ok">Updated</div>';
rows += '<div class="rows"><span class="k">Filament</span><span class="v big">' +
g(pw.filament) + ' <span class="arrow">→</span> ' + g(nw.filament) + '</span></div>';
} else {
status = '<div class="status muted">Already up to date</div>';
rows += row('Filament remaining', g(nw.filament), true);
}
if (nw.total != null) rows += row('On the scale', g(nw.total));
html += '<div class="card">' + status + rows + '</div>';
if (typeof res.confidence === 'number' && res.confidence < 0.6) {
html += '<div class="card"><div class="status warn">Low confidence</div>' +
'<p class="note">Double-check the scale. Votes: ' + (res.vote_weights || []).join(', ') + ' g</p></div>';
}
if (res.url) html += '<p class="note" style="text-align:center"><a href="' + res.url + '" target="_blank">' + escapeHtml(res.spool_id || 'spool') + '</a></p>';
result.innerHTML = html;
}
function row(k, v, big) {
return '<div class="rows"><span class="k">' + k + '</span><span class="v ' + (big ? 'big' : '') + '">' + v + '</span></div>';
}
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
})();
</script>
</body>
</html>