Add the Extrudex Design System v3 bundle
Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the self-hosted JetBrains Mono fonts, and interactive mockups of every screen across three iterations. Iteration 3 is final: phone, tablet and desktop tiers, device preview, and "empty spool weight" terminology. The build scripts regenerate the screens byte-identically from source. Implementation is tracked in Maestro feature UI Refresh - V3.0 (224b72a1-8d84-4970-97ea-5dafa4d8ea3b). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9ba0d409c8
commit
d9ed1898b9
@@ -0,0 +1,87 @@
|
||||
/* ── Mockup app shell (shared by all four screens) ── */
|
||||
.shell { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: var(--space-6); max-width: 1440px; margin: 0 auto; padding: var(--space-6); min-height: 100vh; }
|
||||
.rail { position: sticky; top: var(--space-6); align-self: start; padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-5); }
|
||||
.wordmark { font-weight: var(--fw-display); font-size: var(--fs-h3); letter-spacing: var(--tracking-wordmark); display: flex; align-items: center; gap: var(--space-2); }
|
||||
.wordmark-mark { width: 30px; height: 30px; border-radius: 10px; background: var(--accent-fill); color: var(--text-on-accent); display: grid; place-items: center; font-size: 15px; box-shadow: var(--nm-raised-1); border: 1px solid var(--border-control); }
|
||||
.nav { display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.nav a { display: flex; align-items: center; gap: var(--space-3); padding: 10px 12px; border-radius: var(--radius-md); color: var(--text-primary); text-decoration: none; border: 1px solid transparent; font-weight: var(--fw-medium); }
|
||||
.nav a:hover { border-color: var(--border-control); }
|
||||
.nav a:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
|
||||
.nav a[aria-current="page"] { background: var(--surface-inset); box-shadow: var(--nm-inset-1); border-color: var(--text-accent); color: var(--text-accent); font-weight: var(--fw-bold); }
|
||||
.nav svg { width: 18px; height: 18px; flex: none; }
|
||||
.nav .nav-new { margin-left: auto; }
|
||||
.rail-foot { display: flex; flex-direction: column; gap: var(--space-3); border-top: 1px solid var(--border-hairline); padding-top: var(--space-4); }
|
||||
.seg { display: inline-flex; padding: 4px; gap: 4px; border-radius: var(--radius-pill); background: var(--surface-inset); box-shadow: var(--nm-inset-1); border: 1px solid var(--border-control); }
|
||||
.seg button { font: var(--fw-medium) var(--fs-label)/1 var(--font-body); color: var(--text-secondary); background: transparent; border: 1px solid transparent; border-radius: var(--radius-pill); padding: 7px 12px; cursor: pointer; display: inline-flex; gap: 6px; align-items: center; }
|
||||
.seg button[aria-pressed="true"] { background: var(--surface-raised); color: var(--text-primary); box-shadow: var(--nm-raised-1); border-color: var(--border-control); font-weight: var(--fw-bold); }
|
||||
.seg button[aria-pressed="true"]::before { content: "✓"; color: var(--text-accent); }
|
||||
.seg button:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: 2px; }
|
||||
.who { font-size: var(--fs-label); }
|
||||
|
||||
main { min-width: 0; display: flex; flex-direction: column; gap: var(--space-6); padding-bottom: var(--space-16); }
|
||||
.proposal { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); align-items: center; font-size: var(--fs-label); color: var(--text-secondary); padding: var(--space-3) var(--space-4); }
|
||||
.page-head { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-end; justify-content: space-between; }
|
||||
.page-head h1 { margin: 0; font-size: var(--fs-h1); line-height: var(--lh-tight); font-weight: var(--fw-display); }
|
||||
.page-head p { margin: var(--space-1) 0 0; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
|
||||
h2.section { font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); margin: 0 0 var(--space-3); font-weight: var(--fw-bold); display: flex; align-items: center; gap: var(--space-2); }
|
||||
.card { padding: var(--space-5); }
|
||||
.card h3 { margin: 0; font-size: var(--fs-body-lg); }
|
||||
.grid { display: grid; gap: var(--space-5); }
|
||||
.stack { display: flex; flex-direction: column; gap: var(--space-3); }
|
||||
.row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
|
||||
.spacer { flex: 1; }
|
||||
a { color: var(--text-accent); }
|
||||
a:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }
|
||||
small, .small { font-size: var(--fs-label); }
|
||||
.tiny { font-size: var(--fs-caption); }
|
||||
code { font-family: var(--font-mono); font-size: 0.92em; background: var(--surface-inset); padding: 1px 5px; border-radius: var(--radius-xs); border: 1px solid var(--border-hairline); }
|
||||
.kbd-note { font-size: var(--fs-caption); color: var(--text-secondary); }
|
||||
|
||||
/* Data tables: square, numbers right-aligned, inside a soft card */
|
||||
.table-wrap { overflow-x: auto; margin: 0 calc(var(--space-5) * -1); padding: 0 var(--space-5); }
|
||||
table.tbl { width: 100%; border-collapse: collapse; border-radius: var(--radius-none); font-size: 13px; }
|
||||
.tbl th { text-align: left; font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-secondary); font-weight: var(--fw-bold); padding: 10px 10px; border-bottom: 1px solid var(--border-strong); white-space: nowrap; }
|
||||
.tbl td { padding: 11px 10px; border-bottom: 1px solid var(--border-hairline); vertical-align: middle; }
|
||||
.tbl tr:last-child td { border-bottom: 0; }
|
||||
.tbl .r { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.tbl tbody tr:hover td { background: var(--accent-wash); }
|
||||
|
||||
/* Provenance marker used across screens */
|
||||
.src { display: inline-flex; gap: 4px; align-items: center; font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
|
||||
|
||||
/* Stat tiles */
|
||||
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-5); }
|
||||
.stat { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.stat-value { font-size: 30px; font-weight: var(--fw-display); line-height: var(--lh-tight); font-variant-numeric: tabular-nums; }
|
||||
.stat-label { font-size: var(--fs-label); color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-caps); font-weight: var(--fw-bold); }
|
||||
.stat-foot { font-size: var(--fs-label); color: var(--text-secondary); }
|
||||
.stat.warn .stat-value { color: var(--status-warning); }
|
||||
|
||||
/* Modal backdrop for dialogs */
|
||||
.scrim { position: fixed; inset: 0; background: rgba(10, 10, 14, 0.55); }
|
||||
:root[data-theme="light"] .scrim { background: rgba(60, 55, 80, 0.35); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1100px) { .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 860px) {
|
||||
.shell { grid-template-columns: 1fr; padding: var(--space-4); gap: var(--space-4); }
|
||||
.rail { position: static; flex-direction: row; flex-wrap: wrap; align-items: center; padding: var(--space-3) var(--space-4); gap: var(--space-3); }
|
||||
.nav { flex-direction: row; flex-wrap: wrap; gap: var(--space-1); }
|
||||
.nav a { padding: 8px 10px; }
|
||||
.nav a span.lbl { display: none; }
|
||||
.nav a[aria-current="page"] span.lbl { display: inline; }
|
||||
.nav .nav-new { display: none; }
|
||||
.rail-foot { border: 0; padding: 0; flex-direction: row; margin-left: auto; }
|
||||
.who { display: none; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.stats { grid-template-columns: 1fr 1fr; gap: var(--space-3); }
|
||||
.stat { padding: var(--space-4); }
|
||||
.stat-value { font-size: 22px; }
|
||||
.page-head h1 { font-size: var(--fs-h2); }
|
||||
}
|
||||
|
||||
a.btn { text-decoration: none; }
|
||||
.th-note { display: block; margin-top: 4px; }
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
"""Assemble self-contained mockup pages: tokens + fonts (base64) + shell + page."""
|
||||
import base64, pathlib, re
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
OUT = pathlib.Path.home() / 'Design/extrudex-v3'
|
||||
FONTS = OUT / 'fonts'
|
||||
|
||||
tokens = (OUT / 'design-system-v3.css').read_text()
|
||||
|
||||
def embed_font(match):
|
||||
name = match.group(1)
|
||||
data = base64.b64encode((FONTS / name).read_bytes()).decode()
|
||||
return f"url('data:font/woff2;base64,{data}')"
|
||||
|
||||
tokens = re.sub(r"url\('\./fonts/([^']+)'\)", embed_font, tokens)
|
||||
app_css = (HERE / 'app.css').read_text()
|
||||
|
||||
ICONS = {
|
||||
'dashboard': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></svg>',
|
||||
'filament': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><path d="M12 3v6M12 15v6"/></svg>',
|
||||
'prints': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>',
|
||||
'printers': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 17h8M9 7h6v5H9z"/></svg>',
|
||||
}
|
||||
NAV = [('dashboard', 'Dashboard', '01-dashboard.html'), ('filament', 'Filament', '02-filament.html'),
|
||||
('prints', 'Prints', None), ('printers', 'Printers', '04-printers.html')]
|
||||
|
||||
THEME_HEAD = """<script>
|
||||
(function () {
|
||||
var root = document.documentElement, key = 'extrudex-v3-theme', t = null;
|
||||
try { t = localStorage.getItem(key); } catch (e) {}
|
||||
var q = /[?&]theme=(light|dark)/.exec(location.search); if (q) t = q[1];
|
||||
if (t !== 'light' && t !== 'dark') {
|
||||
t = window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
root.setAttribute('data-theme', t);
|
||||
function set(v) {
|
||||
root.setAttribute('data-theme', v);
|
||||
document.querySelectorAll('[data-set-theme]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', String(b.getAttribute('data-set-theme') === v));
|
||||
});
|
||||
try { localStorage.setItem(key, v); } catch (e) {}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () { set(root.getAttribute('data-theme')); });
|
||||
document.addEventListener('click', function (e) {
|
||||
var b = e.target.closest && e.target.closest('[data-set-theme]');
|
||||
if (b) set(b.getAttribute('data-set-theme'));
|
||||
});
|
||||
})();
|
||||
</script>"""
|
||||
|
||||
def rail(active):
|
||||
links = []
|
||||
for key, label, href in NAV:
|
||||
cur = ' aria-current="page"' if key == active else ''
|
||||
extra = ''
|
||||
if key == 'dashboard': extra = '<span class="chip chip-accent nav-new">new</span>'
|
||||
if href is None:
|
||||
links.append(f'<a href="#" title="Prints history: not part of this proposal"{cur}>{ICONS[key]}<span class="lbl">{label}</span></a>')
|
||||
else:
|
||||
links.append(f'<a href="{href}"{cur}>{ICONS[key]}<span class="lbl">{label}</span>{extra}</a>')
|
||||
return f"""<nav class="rail nm-raised" aria-label="Main">
|
||||
<div class="wordmark"><span class="wordmark-mark" aria-hidden="true">X</span>Extrudex</div>
|
||||
<div class="nav">{''.join(links)}</div>
|
||||
<div class="rail-foot">
|
||||
<div class="seg" role="group" aria-label="Color theme">
|
||||
<button type="button" data-set-theme="light" aria-pressed="false">Light</button>
|
||||
<button type="button" data-set-theme="dark" aria-pressed="true">Dark</button>
|
||||
</div>
|
||||
<span class="who muted">Signed in · operator</span>
|
||||
</div>
|
||||
</nav>"""
|
||||
|
||||
def page(slug, title, active, description):
|
||||
src = (HERE / 'pages' / f'{slug}.html').read_text()
|
||||
css_m = re.search(r'<style>(.*?)</style>', src, re.S)
|
||||
page_css = css_m.group(1) if css_m else ''
|
||||
body = re.sub(r'<style>.*?</style>', '', src, flags=re.S).strip()
|
||||
js_m = re.search(r'<script>(.*?)</script>', body, re.S)
|
||||
page_js = ''
|
||||
if js_m:
|
||||
page_js = f'<script>{js_m.group(1)}</script>'
|
||||
body = re.sub(r'<script>.*?</script>', '', body, flags=re.S)
|
||||
html = f"""<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title}</title>
|
||||
<meta name="description" content="{description}">
|
||||
{THEME_HEAD}
|
||||
<style>
|
||||
{tokens}
|
||||
{app_css}
|
||||
{page_css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
{rail(active)}
|
||||
<main id="main">
|
||||
<div class="proposal nm-inset" role="note">
|
||||
<span class="chip chip-accent">Design System v3 proposal</span>
|
||||
<span>Mockup with sample data from the real inventory. Nothing here is wired to the API.</span>
|
||||
<span class="spacer"></span>
|
||||
<span class="row" style="gap:12px"><span class="src"><span class="chip chip-neutral">dev</span> on dev today</span>
|
||||
<span class="src"><span class="chip chip-info chip-dashed">feature</span> feature branch only</span>
|
||||
<span class="src"><span class="chip chip-warn chip-dashed">API gap</span> stored, not served</span></span>
|
||||
</div>
|
||||
{body}
|
||||
</main>
|
||||
</div>
|
||||
{page_js}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
(OUT / 'screens' / f'{slug}.html').write_text(html)
|
||||
print(slug, len(html) // 1024, 'KB')
|
||||
|
||||
page('01-dashboard', 'Extrudex Dashboard', 'dashboard', 'Stock summary and recent prints, Soft UI v3 mockup.')
|
||||
page('02-filament', 'Extrudex Filament', 'filament', 'Filament inventory list with search, add, edit and delete, Soft UI v3 mockup.')
|
||||
page('03-spool-form', 'Extrudex Spool Form', 'filament', 'Add/Edit spool form with 409 conflict and field-level 400 errors, Soft UI v3 mockup.')
|
||||
page('04-printers', 'Extrudex Printers', 'printers', 'Printers screen with stored fields, dropped MQTT state and a Future section, Soft UI v3 mockup.')
|
||||
@@ -0,0 +1,10 @@
|
||||
def lum(h):
|
||||
h=h.lstrip('#'); r,g,b=[int(h[i:i+2],16)/255 for i in (0,2,4)]
|
||||
f=lambda c: c/12.92 if c<=0.03928 else ((c+0.055)/1.055)**2.4
|
||||
return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b)
|
||||
def cr(a,b):
|
||||
la,lb=sorted([lum(a),lum(b)],reverse=True); return (la+0.05)/(lb+0.05)
|
||||
import sys
|
||||
pairs=[l.split() for l in sys.stdin if l.strip()]
|
||||
for p in pairs:
|
||||
print(f"{p[0]:>9} on {p[1]:>9} {cr(p[0],p[1]):5.2f} {' '.join(p[2:])}")
|
||||
@@ -0,0 +1,22 @@
|
||||
import pathlib; exec(open(pathlib.Path(__file__).with_name('contrast.py')).read().split('import sys')[0])
|
||||
def blend(fg,bg,a):
|
||||
f=[int(fg[i:i+2],16) for i in (1,3,5)]; b=[int(bg[i:i+2],16) for i in (1,3,5)]
|
||||
return '#'+''.join(f'{round(x*a+y*(1-a)):02x}' for x,y in zip(f,b))
|
||||
D=dict(page='#2c2c30',inset='#232327',high='#36363a',text='#f4f4f6',muted='#b2b2ba',acc='#a99af0',accbg='#a99af0',onacc='#2a2431',ctl='#85858f',ok='#a9d88b',err='#f08b93',warn='#e8c170',info='#8fc3f0')
|
||||
L=dict(page='#e4e2ea',inset='#d9d6e1',high='#f1f0f5',text='#2a2431',muted='#55505f',acc='#5b43b8',accbg='#5b43b8',onacc='#ffffff',ctl='#736e86',ok='#2c6619',err='#a8202f',warn='#7c5100',info='#1d5e94')
|
||||
rows=[]
|
||||
for name,T in (('Dark',D),('Light',L)):
|
||||
chk=[('Body text','text','page',4.5),('Body text on inset (inputs)','text','inset',4.5),('Body text on high surface (dialog)','text','high',4.5),
|
||||
('Secondary text','muted','page',4.5),('Secondary text on inset','muted','inset',4.5),('Secondary text on high','muted','high',4.5),
|
||||
('Accent text / links','acc','page',4.5),('Accent text on high','acc','high',4.5),('Text on primary button','onacc','accbg',4.5),
|
||||
('Success text','ok','page',4.5),('Error text','err','page',4.5),('Error text on high','err','high',4.5),('Warning text','warn','page',4.5),('Info text','info','page',4.5),
|
||||
('Control border vs page (1.4.11)','ctl','page',3),('Control border vs inset','ctl','inset',3),('Control border vs high','ctl','high',3),('Focus ring vs page','acc','page',3),('Focus ring vs high','acc','high',3),
|
||||
('Primary button fill vs page','accbg','page',3),('Error border vs inset','err','inset',3),('Warning border vs page','warn','page',3)]
|
||||
for label,a,b,need in chk:
|
||||
r=cr(T[a],T[b]); rows.append((name,label,T[a],T[b],r,need))
|
||||
# tinted chips: 10% wash of status over page
|
||||
for s in ('ok','err','warn','info','acc'):
|
||||
bg=blend(T[s],T['page'],0.10); r=cr(T[s],bg); rows.append((name,f'{s} chip text on 10% wash',T[s],bg,r,4.5))
|
||||
print('| Theme | Pair | Foreground | Background | Ratio | Needs | Result |\n|---|---|---|---|---|---|---|')
|
||||
for n,l,a,b,r,need in rows:
|
||||
print(f"| {n} | {l} | `{a}` | `{b}` | {r:.2f}:1 | {need}:1 | {'Pass' if r>=need else '**FAIL**'} |")
|
||||
@@ -0,0 +1,176 @@
|
||||
<style>
|
||||
.dash { display: grid; grid-template-columns: minmax(0, 2.4fr) minmax(280px, 1fr); gap: var(--space-6); align-items: start; }
|
||||
.fleet { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); }
|
||||
.fleet a { text-decoration: none; color: inherit; padding: var(--space-4); display: flex; flex-direction: column; gap: 6px; border-radius: var(--radius-md); }
|
||||
.fleet a:hover { border-color: var(--text-accent); }
|
||||
.fleet .name { font-weight: var(--fw-bold); font-size: 13px; }
|
||||
.mat { display: grid; grid-template-columns: 56px minmax(0, 1fr) 84px; gap: var(--space-3); align-items: center; font-size: 13px; }
|
||||
.mat .track { height: 12px; }
|
||||
.low li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-3); align-items: center; padding: var(--space-3) 0; border-bottom: 1px solid var(--border-hairline); }
|
||||
.low li:last-child { border-bottom: 0; }
|
||||
.low ul { list-style: none; margin: 0; padding: 0; }
|
||||
.low .nm { font-weight: var(--fw-bold); font-size: 13px; line-height: var(--lh-snug); }
|
||||
.low .sub { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
|
||||
.low .g { text-align: right; font-variant-numeric: tabular-nums; line-height: var(--lh-snug); }
|
||||
.low .g strong { color: var(--status-warning); font-size: 15px; }
|
||||
.job { font-weight: var(--fw-bold); }
|
||||
.job-sub { display: block; font-size: var(--fs-caption); color: var(--text-secondary); font-weight: var(--fw-regular); }
|
||||
.dur-ub { color: var(--status-warning); font-weight: var(--fw-bold); }
|
||||
.legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: var(--space-3) var(--space-5); font-size: var(--fs-label); color: var(--text-secondary); margin-top: var(--space-4); padding: var(--space-4); }
|
||||
.legend div { display: flex; gap: var(--space-2); align-items: baseline; }
|
||||
@media (max-width: 1280px) { .dash { grid-template-columns: minmax(0, 1fr); } }
|
||||
@media (max-width: 640px) { .fleet { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p class="muted">Friday 18 September · refreshes every 10 s · last update 09:41:07</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn" href="02-filament.html">Open inventory</a>
|
||||
<a class="btn btn-primary" href="03-spool-form.html">+ Add spool</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="stock-h">
|
||||
<h2 class="section" id="stock-h">Stock</h2>
|
||||
<div class="stats">
|
||||
<div class="stat nm-raised"><span class="stat-label">Spools</span><span class="stat-value">105</span><span class="stat-foot">18 brands · 6 materials</span></div>
|
||||
<div class="stat nm-raised"><span class="stat-label">Filament left <abbr title="Net filament, not including the empty spools">net</abbr></span><span class="stat-value">95.72 kg</span><span class="stat-foot">filament only, empty spools excluded</span></div>
|
||||
<div class="stat nm-raised"><span class="stat-label">Stock value</span><span class="stat-value">$1,721.18</span><span class="stat-foot">remaining net g × each spool’s $/kg</span></div>
|
||||
<div class="stat nm-raised warn"><span class="stat-label">Running low</span><span class="stat-value">⚠ 5</span><span class="stat-foot">under 200 g net each</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dash">
|
||||
<div class="stack" style="gap: var(--space-6)">
|
||||
<section class="card nm-raised" aria-labelledby="prints-h">
|
||||
<div class="row" style="margin-bottom: var(--space-3)">
|
||||
<h2 class="section" id="prints-h" style="margin:0">Recent prints</h2>
|
||||
<span class="spacer"></span>
|
||||
<a href="#" class="small">All prints →</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr>
|
||||
<th>Job</th><th>Status</th><th class="r">Duration</th>
|
||||
<th class="r">Filament</th><th>Weight source <span class="th-note"><span class="chip chip-warn chip-dashed" title="Stored in print.weight_source; GET /api/prints does not return it yet">API gap</span></span></th><th class="r">Cost</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="job">Gridfinity_bins_3x2</span><span class="job-sub">P1S - Prints Charming · 08:37</span></td>
|
||||
<td><span class="chip chip-accent">● Running</span></td>
|
||||
<td class="r">1h 04m <span class="job-sub">so far</span></td>
|
||||
<td class="r muted">—</td>
|
||||
<td><span class="small muted">weighed when it ends</span></td>
|
||||
<td class="r muted">pending</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="job">Benchy_x4</span><span class="job-sub">H2C - Constipation Orion · 02:15</span></td>
|
||||
<td><span class="chip chip-ok">✓ Finished</span></td>
|
||||
<td class="r"><span class="dur-ub" title="The connection dropped once during this print. That time is inside the duration and nothing watched it, so this is an upper bound, not a measurement.">≤ 3h 12m</span>
|
||||
<span class="job-sub"><span class="chip chip-warn chip-dashed">upper bound · 1 gap</span></span></td>
|
||||
<td class="r">46 g</td>
|
||||
<td><span class="chip chip-info">Sliced</span> <span class="job-sub">from this run's 3MF</span></td>
|
||||
<td class="r">$0.53</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="job">Sign_CubeCraft</span><span class="job-sub">H2C - Constipation Orion · yesterday 21:48 · 2 colors</span></td>
|
||||
<td><span class="chip chip-ok">✓ Finished</span></td>
|
||||
<td class="r">2h 05m</td>
|
||||
<td class="r">64 g <span class="job-sub">41 g + 23 g</span></td>
|
||||
<td><span class="chip chip-info">Sliced</span></td>
|
||||
<td class="r">$0.92</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="job">Cable_clips</span><span class="job-sub">A1 - Jordyns Layer Slayer · yesterday 19:30</span></td>
|
||||
<td><span class="chip chip-warn chip-dashed">? Unknown</span></td>
|
||||
<td class="r"><span class="muted" title="Nobody saw this print end. The only measurable figure is the outage, which is not how long anything printed.">no duration</span><span class="job-sub">end not seen</span></td>
|
||||
<td class="r">12 g</td>
|
||||
<td><span class="chip chip-neutral">Typed</span> <span class="job-sub">someone weighed it</span></td>
|
||||
<td class="r">$0.14</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="job">Lid_v3</span><span class="job-sub">P1S - Prints Charming · yesterday 16:02</span></td>
|
||||
<td><span class="chip chip-err">✕ Failed</span></td>
|
||||
<td class="r">47m</td>
|
||||
<td class="r">118 g <span class="job-sub">full job weight</span></td>
|
||||
<td><span class="chip chip-neutral">Typed</span></td>
|
||||
<td class="r">$1.38</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="job">Planter_small</span><span class="job-sub">A1 - Jordyns Layer Slayer · 16 Sep 10:20</span></td>
|
||||
<td><span class="chip chip-ok">✓ Finished</span></td>
|
||||
<td class="r">5h 40m</td>
|
||||
<td class="r">212 g</td>
|
||||
<td><span class="chip chip-info chip-dashed">Recalled</span> <span class="job-sub">remembered for this job</span></td>
|
||||
<td class="r">$2.49</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="job">Test_cube</span><span class="job-sub">P1S - Prints Charming · 7 Aug 14:11</span></td>
|
||||
<td><span class="chip chip-ok">✓ Finished</span></td>
|
||||
<td class="r">31m <span class="job-sub" title="This print predates gap counting (connection_gaps is null), so nobody asked whether the connection dropped.">gaps not counted</span></td>
|
||||
<td class="r">9 g</td>
|
||||
<td><span class="chip chip-warn chip-dashed">Unknown</span> <span class="job-sub">predates provenance</span></td>
|
||||
<td class="r">$0.10</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="legend nm-inset" aria-label="How to read this table">
|
||||
<div><span class="dur-ub">≤</span><span><b>Upper bound.</b> The connection dropped during the print, so the time includes a gap nobody watched.</span></div>
|
||||
<div><span class="chip chip-neutral">Typed</span><span>A measurement. It survives an identity correction.</span></div>
|
||||
<div><span class="chip chip-info">Sliced</span><span>Read from the job's 3MF over FTPS.</span></div>
|
||||
<div><span class="chip chip-info chip-dashed">Recalled</span><span>A copy of a weight remembered for the same gcode path.</span></div>
|
||||
<div><span class="chip chip-warn chip-dashed">Unknown</span><span>Written before the source was recorded. Neither path will move it.</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="stack" style="gap: var(--space-6)" aria-label="Stock detail">
|
||||
<section class="card nm-raised low" aria-labelledby="low-h">
|
||||
<h2 class="section" id="low-h">⚠ Running low <span class="muted">· under 200 g</span></h2>
|
||||
<ul>
|
||||
<li><span class="swatch"><i style="background:#1f3fbf"></i></span>
|
||||
<span><span class="nm">Klein Blue</span><br><span class="sub">Sunlu PLA + 2.0 Basic · 250 g mini</span></span>
|
||||
<span class="g"><strong>64 g</strong><br><span class="sub">gross 124 g</span></span></li>
|
||||
<li><span class="swatch"><i style="background:#b87333"></i></span>
|
||||
<span><span class="nm">Copper</span><br><span class="sub">Sunlu PLA + Silk</span></span>
|
||||
<span class="g"><strong>97 g</strong><br><span class="sub">gross 237 g</span></span></li>
|
||||
<li><span class="swatch"><i style="background:#141414"></i></span>
|
||||
<span><span class="nm">Black</span><br><span class="sub">Elegoo Rapid PETG Basic</span></span>
|
||||
<span class="g"><strong>142 g</strong><br><span class="sub">gross 296 g</span></span></li>
|
||||
<li><span class="swatch"><i style="background:#ece8df"></i></span>
|
||||
<span><span class="nm">Rock White</span><br><span class="sub">Overture PLA Marble</span></span>
|
||||
<span class="g"><strong>171 g</strong><br><span class="sub">gross 326 g</span></span></li>
|
||||
<li><span class="swatch"><i style="background:#161616"></i><i style="background:#c0232f"></i></span>
|
||||
<span><span class="nm">Black Red</span><br><span class="sub">TECSONAR PLA Matte</span></span>
|
||||
<span class="g"><strong>188 g</strong><br><span class="sub">gross <span class="chip chip-warn chip-dashed" title="No tare is known for this spool, so gross weight cannot be worked out. It is never shown as 0.">tare unknown</span></span></span></li>
|
||||
</ul>
|
||||
<p class="tiny muted" style="margin: var(--space-3) 0 0">Net grams are filament only. Gross = empty-spool tare + net, what a scale reads.</p>
|
||||
</section>
|
||||
|
||||
<section class="card nm-raised" aria-labelledby="mat-h">
|
||||
<h2 class="section" id="mat-h">Net filament by material</h2>
|
||||
<div class="stack">
|
||||
<div class="mat"><b>PLA</b><div class="track"><span style="width:100%"></span></div><span class="r num">48.12 kg</span></div>
|
||||
<div class="mat"><b>PETG</b><div class="track"><span style="width:68.8%"></span></div><span class="r num">33.10 kg</span></div>
|
||||
<div class="mat"><b>ASA</b><div class="track"><span style="width:19.7%"></span></div><span class="r num">9.50 kg</span></div>
|
||||
<div class="mat"><b>TPU</b><div class="track"><span style="width:6.2%"></span></div><span class="r num">3.00 kg</span></div>
|
||||
<div class="mat"><b>PA6</b><div class="track"><span style="width:2.1%"></span></div><span class="r num">1.00 kg</span></div>
|
||||
<div class="mat"><b>ABS</b><div class="track"><span style="width:2.1%"></span></div><span class="r num">1.00 kg</span></div>
|
||||
</div>
|
||||
<p class="tiny muted" style="margin: var(--space-3) 0 0">55 PLA · 34 PETG · 10 ASA · 3 TPU · 2 PA6 · 1 ABS spools</p>
|
||||
</section>
|
||||
|
||||
<section class="card nm-raised" aria-labelledby="fleet-h">
|
||||
<h2 class="section" id="fleet-h">Printers</h2>
|
||||
<div class="fleet" style="grid-template-columns: 1fr">
|
||||
<a class="nm-raised-sm" href="04-printers.html"><span class="name">P1S - Prints Charming</span><span><span class="chip chip-accent">● Printing</span> <span class="small muted">Gridfinity_bins_3x2</span></span></a>
|
||||
<a class="nm-raised-sm" href="04-printers.html"><span class="name">H2C - Constipation Orion</span><span><span class="chip chip-neutral">Idle</span> <span class="small muted">last: Benchy_x4</span></span></a>
|
||||
<a class="nm-raised-sm" href="04-printers.html" style="border-color: var(--status-warning)"><span class="name">A1 - Jordyns Layer Slayer</span><span><span class="chip chip-warn">⚡ Connection lost</span> <span class="small muted">last contact 6m ago</span></span></a>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,182 @@
|
||||
<style>
|
||||
.filters { padding: var(--space-4) var(--space-5); display: grid; grid-template-columns: minmax(220px, 2fr) repeat(3, minmax(130px, 1fr)); gap: var(--space-3) var(--space-4); align-items: end; }
|
||||
.filters .field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.filters label > span, .lbl-t { font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-secondary); font-weight: var(--fw-bold); }
|
||||
.filters .toggles { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
|
||||
.search { position: relative; }
|
||||
.search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-secondary); pointer-events: none; }
|
||||
.search .input { padding-left: 36px; }
|
||||
.name-main { font-weight: var(--fw-bold); }
|
||||
.name-parts { display: block; font-size: var(--fs-caption); color: var(--text-secondary); }
|
||||
.name-parts .opt { opacity: 1; font-style: italic; }
|
||||
.colorcell { display: flex; gap: var(--space-3); align-items: center; }
|
||||
.rem { min-width: 150px; }
|
||||
.rem .track { margin-top: 5px; height: 8px; }
|
||||
.wt { line-height: var(--lh-snug); }
|
||||
.wt .k { font-size: var(--fs-caption); color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
|
||||
.loc { min-width: 140px; font-size: 13px; line-height: var(--lh-snug); }
|
||||
.fn { display: block; font-size: 13px; }
|
||||
.acts { display: flex; gap: var(--space-2); justify-content: flex-end; }
|
||||
.low-row td:first-child { box-shadow: inset 3px 0 0 var(--status-warning); }
|
||||
.cards { display: none; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--space-5); }
|
||||
.view-cards .cards { display: grid; }
|
||||
.view-cards .table-card { display: none; }
|
||||
.scard { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
|
||||
.scard .head { display: flex; gap: var(--space-3); align-items: center; }
|
||||
.scard dl { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--space-3); margin: 0; font-size: var(--fs-label); }
|
||||
.scard dt { color: var(--text-secondary); }
|
||||
.scard dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.pager { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-4); }
|
||||
dialog.confirm { padding: 0; border: 0; background: transparent; color: inherit; max-width: 480px; width: calc(100% - 32px); }
|
||||
dialog.confirm::backdrop { background: rgba(10, 10, 14, 0.55); }
|
||||
dialog.confirm .nm-dialog { padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-4); }
|
||||
dialog.confirm h2 { margin: 0; font-size: var(--fs-h3); }
|
||||
.empty-note { padding: var(--space-8); text-align: center; }
|
||||
@media (max-width: 640px) { #view:not(.view-table) .table-card { display: none; } #view:not(.view-table) .cards { display: grid; } }
|
||||
@media (max-width: 900px) { .filters { grid-template-columns: 1fr 1fr; } .filters .search-f { grid-column: 1 / -1; } }
|
||||
</style>
|
||||
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>Filament</h1>
|
||||
<p class="muted"><b class="num">105</b> spools · <b>18</b> brands · <b class="num">95.72 kg</b> net · <b class="num">$1,721.18</b></p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="small muted">Labels outstanding: <b>12</b></span>
|
||||
<button class="btn" type="button">Download unlabeled labels</button>
|
||||
<button class="btn" type="button">Manage lists</button>
|
||||
<a class="btn btn-primary" href="03-spool-form.html#add">+ Add spool</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="filters nm-raised" aria-label="Search and filter">
|
||||
<label class="field search-f"><span>Search</span>
|
||||
<span class="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
|
||||
<input class="input" id="q" type="search" placeholder="Brand, color, material, location…" autocomplete="off">
|
||||
</span>
|
||||
</label>
|
||||
<label class="field"><span>Brand</span>
|
||||
<select class="select" id="brand"><option value="">All brands (105)</option><option>Sunlu</option><option>Elegoo</option><option>Overture</option><option>TECSONAR</option><option>AMOLEN</option><option>JAYO</option><option>iSANMATE</option></select></label>
|
||||
<label class="field"><span>Material</span>
|
||||
<select class="select" id="mat"><option value="">All materials (105)</option><option>PLA</option><option>PETG</option><option>ASA</option><option>TPU</option><option>PA6</option><option>ABS</option></select></label>
|
||||
<label class="field"><span>Sort</span>
|
||||
<select class="select" id="sort"><option value="asc">Least remaining</option><option value="desc">Most remaining</option><option value="brand">Brand A–Z</option><option value="value">Highest value</option></select></label>
|
||||
<div class="toggles">
|
||||
<button class="btn btn-sm" type="button" aria-pressed="false" id="t-low">Low stock only</button>
|
||||
<button class="btn btn-sm" type="button" aria-pressed="false" id="t-store">In storage only</button>
|
||||
<button class="btn btn-sm" type="button" aria-pressed="false" id="t-tare">Tare unknown only</button>
|
||||
<span class="spacer"></span>
|
||||
<div class="seg" role="group" aria-label="Inventory view">
|
||||
<button type="button" aria-pressed="true" data-view="table">Table</button>
|
||||
<button type="button" aria-pressed="false" data-view="cards">Cards</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="view">
|
||||
<section class="card nm-raised table-card" aria-label="Filament inventory">
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr>
|
||||
<th>Spool <span class="th-note tiny muted" style="text-transform:none;letter-spacing:0">color · brand speed material type finish</span></th>
|
||||
<th class="r">Remaining <abbr title="Net filament only">net</abbr></th>
|
||||
<th class="r">Empty spool (tare)</th>
|
||||
<th class="r">Gross <abbr title="What the scale reads: tare + remaining">on scale</abbr></th>
|
||||
<th class="r">Value</th><th>Location</th><th class="r">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pager"><span class="small muted" id="count"></span>
|
||||
<div class="actions"><button class="btn btn-sm" disabled type="button">← Previous</button><span class="small">Page 1 of 9</span><button class="btn btn-sm" type="button">Next →</button></div></div>
|
||||
</section>
|
||||
<div class="cards" id="cards" aria-label="Filament inventory cards"></div>
|
||||
</div>
|
||||
|
||||
<dialog class="confirm" id="confirm" aria-labelledby="confirm-h">
|
||||
<form method="dialog" class="nm-dialog">
|
||||
<h2 id="confirm-h">Delete <span id="confirm-name"></span>?</h2>
|
||||
<p style="margin:0">This removes the spool from inventory. <b>Its print history is kept</b>: prints that used it keep their grams and cost, and only lose the link to this spool.</p>
|
||||
<p class="small muted" style="margin:0">This can't be undone. If you just ran the spool out, you can also set remaining to 0 g and keep the record.</p>
|
||||
<div class="actions" style="justify-content:flex-end">
|
||||
<button class="btn" value="cancel" autofocus>Keep spool</button>
|
||||
<button class="btn btn-danger" value="delete">Delete spool</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
var SPOOLS = [
|
||||
{c:['#1f3fbf'], color:'Klein Blue', brand:'Sunlu', speed:null, mat:'PLA', type:'+ 2.0', fin:'Basic', rem:64, tot:250, tare:60, tsrc:'community', tfrom:'Sunlu — 250 g mini', kg:25.20, loc:'Shelf B2', store:true},
|
||||
{c:['#b87333'], color:'Copper', brand:'Sunlu', speed:null, mat:'PLA', type:'+', fin:'Silk', rem:97, tot:1000, tare:140, tsrc:'community', tfrom:'Sunlu — 1 kg plastic', kg:19.99, loc:'P1S · AMS 1 · slot 2', store:false},
|
||||
{c:['#141414'], color:'Black', brand:'Elegoo', speed:'Rapid', mat:'PETG', type:null, fin:'Basic', rem:142, tot:1000, tare:154, tsrc:'manufacturer', tfrom:'Elegoo — 1 kg cardboard', kg:11.74, loc:'H2C · AMS 1 · slot 1', store:false},
|
||||
{c:['#ece8df'], color:'Rock White', brand:'Overture', speed:null, mat:'PLA', type:null, fin:'Marble', rem:171, tot:1000, tare:155, tsrc:'community', tfrom:'Overture — cardboard, PLA lines', kg:17.99, loc:'Shelf C', store:true},
|
||||
{c:['#161616','#c0232f'], color:'Black Red', brand:'TECSONAR', speed:null, mat:'PLA', type:null, fin:'Matte', rem:188, tot:1000, tare:null, kg:15.99, loc:'Shelf C', store:true},
|
||||
{c:['#101010'], color:'Black', brand:'JAYO', speed:'Rapid', mat:'PETG', type:null, fin:'Matte', rem:655, tot:1000, tare:null, kg:12.72, loc:'P1S · AMS 1 · slot 4', store:false},
|
||||
{c:['#8a8f7a','#e9e4d8'], color:'Marble', brand:'iSANMATE', speed:null, mat:'ASA', type:null, fin:'Marble', rem:430, tot:1000, tare:180, tsrc:'estimated', tfrom:'Generic — 1 kg spool (assumed)', kg:25.99, loc:'Dry box 1', store:true},
|
||||
{c:['#1b1b1b'], color:'Black', brand:'Sunlu', speed:null, mat:'PETG', type:'CF', fin:'Matte', rem:812, tot:1000, tare:262, tsrc:'weighed', tfrom:'this spool, weighed 14 Sep', kg:22.79, loc:'A1 · AMS Lite · slot 1', store:false, weighed:true},
|
||||
{c:['#b8864b'], color:'Natural Wood', brand:'Sunlu', speed:null, mat:'PLA', type:null, fin:'Wood', rem:1000, tot:1000, tare:35, tsrc:'estimated', tfrom:'Refill — bare cardboard core', kg:16.50, loc:'Shelf A3', store:true},
|
||||
{c:['#a3402c'], color:'Brick Red', brand:'Sunlu', speed:'High Speed', mat:'PLA', type:null, fin:'Marble', rem:1000, tot:1000, tare:140, tsrc:'community', tfrom:'Sunlu — 1 kg plastic', kg:17.50, loc:'Shelf A2', store:true},
|
||||
{c:['#e8453c','#f2c230','#2a8fd6'], color:'Gradient Rainbow', brand:'AMOLEN', speed:null, mat:'PETG', type:null, fin:'Gradient', rem:1000, tot:1000, tare:180, tsrc:'estimated', tfrom:'Generic — 1 kg spool (assumed)', kg:23.99, loc:'Shelf D', store:true},
|
||||
{c:['#1d4fa8'], color:'Blue', brand:'Elegoo', speed:'Rapid', mat:'PETG', type:null, fin:'Basic', rem:1000, tot:1000, tare:154, tsrc:'manufacturer', tfrom:'Elegoo — 1 kg cardboard', kg:17.99, loc:'Shelf A1', store:true}
|
||||
];
|
||||
var LOW = 200;
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,function(ch){return {'&':'&','<':'<','>':'>','"':'"'}[ch]})}
|
||||
function sw(c){return '<span class="swatch" title="'+esc(c.join(' / '))+'">'+c.map(function(h){return '<i style="background:'+h+'"></i>'}).join('')+'</span>'}
|
||||
function fullName(s){return [s.brand,s.speed,s.mat,s.type,s.fin].filter(Boolean).join(' ')}
|
||||
function parts(s){
|
||||
function p(v,req){return v?esc(v):'<span class="opt" title="optional, not set">–</span>'}
|
||||
return '<span class="name-parts" title="brand · speed · material · type · finish">'+[p(s.brand),p(s.speed),p(s.mat),p(s.type),p(s.fin)].join(' · ')+'</span>'}
|
||||
var SRC = {manufacturer:'<span class="chip chip-ok">manufacturer</span>', community:'<span class="chip chip-info">community</span>', estimated:'<span class="chip chip-warn chip-dashed">estimated</span>', weighed:'<span class="chip chip-accent">this spool</span>'};
|
||||
function tareCell(s){
|
||||
if (s.tare == null) return '<span class="chip chip-warn chip-dashed" title="No empty-spool weight is known. Shown as unknown, never 0.">? unknown</span><span class="name-parts">weigh the bare spool</span>';
|
||||
return '<span class="wt"><b class="num">'+s.tare+' g</b><br>'+SRC[s.tsrc]+'<span class="name-parts">'+esc(s.tfrom)+'</span></span>'}
|
||||
function grossCell(s){
|
||||
if (s.tare == null) return '<span class="muted" title="Gross = tare + remaining. Tare is unknown, so gross is too.">unknown</span>';
|
||||
return '<b class="num">'+(s.tare+s.rem).toLocaleString()+' g</b><span class="name-parts">'+s.tare+' + '+s.rem+'</span>'}
|
||||
function remCell(s){
|
||||
var pct=Math.max(0,Math.min(100,s.rem/s.tot*100)); var low=s.rem<LOW;
|
||||
return '<div class="rem"><b class="num" style="'+(low?'color:var(--status-warning)':'')+'">'+(low?'⚠ ':'')+s.rem+'</b> <span class="muted">of '+s.tot+' g</span>'+
|
||||
(s.weighed?'<span class="name-parts">weighed on a scale</span>':'')+
|
||||
'<div class="track'+(low?' is-low':'')+'" role="progressbar" aria-valuenow="'+Math.round(pct)+'" aria-valuemin="0" aria-valuemax="100" aria-label="'+esc(s.color)+' remaining"><span style="width:'+pct+'%"></span></div></div>'}
|
||||
function value(s){return (s.rem/1000*s.kg)}
|
||||
function render(){
|
||||
var q=document.getElementById('q').value.trim().toLowerCase();
|
||||
var b=document.getElementById('brand').value, m=document.getElementById('mat').value, sort=document.getElementById('sort').value;
|
||||
var low=document.getElementById('t-low').getAttribute('aria-pressed')==='true';
|
||||
var store=document.getElementById('t-store').getAttribute('aria-pressed')==='true';
|
||||
var tare=document.getElementById('t-tare').getAttribute('aria-pressed')==='true';
|
||||
var list=SPOOLS.filter(function(s){
|
||||
var hay=(fullName(s)+' '+s.color+' '+s.loc).toLowerCase();
|
||||
return (!q||hay.indexOf(q)>=0)&&(!b||s.brand===b)&&(!m||s.mat===m)&&(!low||s.rem<LOW)&&(!store||s.store)&&(!tare||s.tare==null)});
|
||||
list.sort(function(a,z){return sort==='desc'?z.rem-a.rem:sort==='brand'?a.brand.localeCompare(z.brand):sort==='value'?value(z)-value(a):a.rem-z.rem});
|
||||
var rows=list.map(function(s,i){var idx=SPOOLS.indexOf(s);
|
||||
return '<tr class="'+(s.rem<LOW?'low-row':'')+'"><td><div class="colorcell">'+sw(s.c)+'<span><span class="name-main">'+esc(s.color)+' <span class="muted tiny">#'+(40+idx)+'</span></span><span class="fn">'+esc(fullName(s))+'</span>'+parts(s)+'</span></div></td>'+
|
||||
'<td class="r">'+remCell(s)+'</td><td class="r">'+tareCell(s)+'</td><td class="r">'+grossCell(s)+'</td>'+
|
||||
'<td class="r num">$'+value(s).toFixed(2)+'<span class="name-parts">$'+s.kg.toFixed(2)+'/kg</span></td>'+
|
||||
'<td class="loc">'+(s.store?'<span class="name-parts">In storage</span>':'<span class="chip chip-accent">loaded</span><br>')+esc(s.loc)+'</td>'+
|
||||
'<td class="r"><div class="acts"><a class="btn btn-sm" href="03-spool-form.html" aria-label="Edit '+esc(s.color)+', '+esc(s.brand)+'">Edit</a>'+
|
||||
'<button class="btn btn-sm btn-danger" type="button" data-del="'+idx+'" aria-label="Delete '+esc(s.color)+', '+esc(s.brand)+'">Delete</button></div></td></tr>'}).join('');
|
||||
document.getElementById('rows').innerHTML = rows || '<tr><td colspan="7"><div class="empty-note muted">No spools match that filter. <button class="btn btn-sm" type="button" id="clear">Clear filters</button></div></td></tr>';
|
||||
document.getElementById('cards').innerHTML = list.map(function(s){var idx=SPOOLS.indexOf(s);
|
||||
return '<article class="scard nm-raised"><div class="head">'+sw(s.c)+'<div><h3 style="margin:0;font-size:15px">'+esc(s.color)+'</h3><span class="name-parts">'+esc(fullName(s))+'</span></div></div>'+remCell(s)+
|
||||
'<dl><dt>Tare</dt><dd>'+tareCell(s)+'</dd><dt>Gross</dt><dd>'+grossCell(s)+'</dd><dt>Value</dt><dd>$'+value(s).toFixed(2)+'</dd><dt>Location</dt><dd>'+esc(s.loc)+'</dd></dl>'+
|
||||
'<div class="acts"><a class="btn btn-sm" href="03-spool-form.html">Edit</a><button class="btn btn-sm btn-danger" type="button" data-del="'+idx+'">Delete</button></div></article>'}).join('');
|
||||
document.getElementById('count').textContent = 'Showing '+list.length+' of 105 spools (sample page)';
|
||||
}
|
||||
document.addEventListener('input', function(e){ if (e.target.closest('.filters')) render(); });
|
||||
document.addEventListener('click', function(e){
|
||||
var t=e.target.closest('#t-low,#t-store,#t-tare');
|
||||
if (t){t.setAttribute('aria-pressed', String(t.getAttribute('aria-pressed')!=='true')); render(); return;}
|
||||
var v=e.target.closest('[data-view]');
|
||||
if (v){document.querySelectorAll('[data-view]').forEach(function(b){b.setAttribute('aria-pressed', String(b===v))});
|
||||
document.getElementById('view').className = v.getAttribute('data-view')==='cards'?'view-cards':'view-table'; return;}
|
||||
var d=e.target.closest('[data-del]');
|
||||
if (d){var s=SPOOLS[+d.getAttribute('data-del')]; document.getElementById('confirm-name').textContent=s.color+' · '+fullName(s);
|
||||
var dlg=document.getElementById('confirm'); if (dlg.showModal) dlg.showModal(); return;}
|
||||
if (e.target.id==='clear'){document.getElementById('q').value='';['t-low','t-store','t-tare'].forEach(function(id){document.getElementById(id).setAttribute('aria-pressed','false')});
|
||||
document.getElementById('brand').value='';document.getElementById('mat').value='';render();}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function(){ render(); if (matchMedia('(max-width: 640px)').matches) document.querySelectorAll('[data-view]').forEach(function(b){b.setAttribute('aria-pressed', String(b.getAttribute('data-view')==='cards'))}); });
|
||||
</script>
|
||||
@@ -0,0 +1,286 @@
|
||||
<style>
|
||||
.scenarios { padding: var(--space-4) var(--space-5); display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
|
||||
.form-wrap { position: relative; border-radius: var(--radius-xl); padding: var(--space-6); background: repeating-linear-gradient(135deg, transparent 0 14px, var(--accent-wash) 14px 15px); }
|
||||
.sheet { max-width: 880px; margin: 0 auto; padding: 0; overflow: hidden; }
|
||||
.sheet-head { display: flex; gap: var(--space-4); align-items: flex-start; padding: var(--space-6) var(--space-6) var(--space-4); border-bottom: 1px solid var(--border-hairline); }
|
||||
.sheet-head h2 { margin: 0; font-size: var(--fs-h2); }
|
||||
.sheet-body { padding: var(--space-5) var(--space-6); display: flex; flex-direction: column; gap: var(--space-6); }
|
||||
.sheet-foot { padding: var(--space-4) var(--space-6); border-top: 1px solid var(--border-hairline); display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
||||
fieldset.sec { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-4); }
|
||||
fieldset.sec > legend { padding: 0; margin-bottom: var(--space-3); font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); font-weight: var(--fw-bold); }
|
||||
.fr { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
|
||||
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
||||
.field > .lab { display: flex; gap: var(--space-2); align-items: baseline; font-size: var(--fs-label); font-weight: var(--fw-bold); }
|
||||
.field > .lab em { font-style: normal; font-weight: var(--fw-regular); color: var(--text-secondary); font-size: var(--fs-caption); }
|
||||
.req { color: var(--text-accent); font-size: var(--fs-caption); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
|
||||
.hint { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
|
||||
.has-error > .lab { color: var(--status-error); }
|
||||
.has-error > .lab::before { content: "⚠"; }
|
||||
.srv { font-size: var(--fs-caption); color: var(--text-secondary); }
|
||||
.namebar { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
|
||||
.slots { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-2); }
|
||||
.slot { padding: 8px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-control); background: var(--surface-raised); box-shadow: var(--nm-raised-1); display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.slot .k { font-size: var(--fs-caption); color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
|
||||
.slot .v { font-weight: var(--fw-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.slot.empty { border-style: dashed; box-shadow: none; background: transparent; }
|
||||
.slot.empty .v { color: var(--text-secondary); font-weight: var(--fw-regular); font-style: italic; }
|
||||
.fullname { font-size: var(--fs-h3); font-weight: var(--fw-display); }
|
||||
.weights { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: var(--space-5); align-items: start; }
|
||||
.scale { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
|
||||
.stackbar { display: flex; height: 26px; border-radius: var(--radius-pill); overflow: hidden; border: 1px solid var(--border-control); box-shadow: var(--nm-inset-1); background: var(--surface-inset); }
|
||||
.stackbar .t { background: repeating-linear-gradient(45deg, var(--border-strong) 0 5px, transparent 5px 10px); border-right: 2px solid var(--text-primary); }
|
||||
.stackbar .f { background: var(--text-accent); }
|
||||
.stackbar .u { background: transparent; }
|
||||
.eq { display: grid; grid-template-columns: auto 1fr auto; gap: 4px var(--space-3); font-size: 13px; font-variant-numeric: tabular-nums; }
|
||||
.eq .tot { border-top: 1px solid var(--border-strong); padding-top: 4px; font-weight: var(--fw-bold); }
|
||||
.key { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-control); vertical-align: -1px; }
|
||||
.colors-row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
|
||||
.conflict, .errsum { padding: var(--space-5); border: 2px solid var(--status-warning); border-radius: var(--radius-lg); background: var(--wash-warning); display: flex; flex-direction: column; gap: var(--space-3); }
|
||||
.errsum { border-color: var(--status-error); background: var(--wash-error); }
|
||||
.conflict h3, .errsum h3 { margin: 0; font-size: var(--fs-body-lg); display: flex; gap: var(--space-2); align-items: center; }
|
||||
.conflict h3 { color: var(--status-warning); } .errsum h3 { color: var(--status-error); }
|
||||
.cmp th, .cmp td { padding: 8px 10px; }
|
||||
.cmp .mine { color: var(--text-accent); font-weight: var(--fw-bold); }
|
||||
.cmp .theirs { color: var(--status-warning); font-weight: var(--fw-bold); }
|
||||
.specimen { padding: var(--space-6); }
|
||||
.spec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--space-6) var(--space-5); align-items: start; }
|
||||
.spec { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }
|
||||
.spec .cap { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
|
||||
.spec .cap b { color: var(--text-primary); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
|
||||
.btn.is-hover { border-color: var(--text-accent); }
|
||||
[data-only] { display: none; }
|
||||
body.s-conflict [data-only~="conflict"], body.s-errors [data-only~="errors"], body.s-add [data-only~="add"], body.s-tare [data-only~="tare"], body.s-edit [data-only~="edit"] { display: revert; }
|
||||
body.s-conflict .fr[data-only~="conflict"], body.s-errors .fr[data-only~="errors"] { display: grid; }
|
||||
body.s-conflict .conflict[data-only], body.s-errors .errsum[data-only] { display: flex; }
|
||||
@media (max-width: 760px) { .weights { grid-template-columns: 1fr; } .slots { grid-template-columns: repeat(2, minmax(0, 1fr)); } .form-wrap { padding: var(--space-2); } .sheet-head, .sheet-body, .sheet-foot { padding-left: var(--space-4); padding-right: var(--space-4); } }
|
||||
</style>
|
||||
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>Add / Edit spool</h1>
|
||||
<p class="muted">Opens as a dialog over Filament. Pick a scenario to see each state.</p>
|
||||
</div>
|
||||
<a class="btn" href="02-filament.html">← Back to Filament</a>
|
||||
</header>
|
||||
|
||||
<section class="scenarios nm-raised" aria-label="Scenario">
|
||||
<span class="lbl-t small muted">Scenario</span>
|
||||
<div class="seg" role="group" aria-label="Form scenario" id="scen">
|
||||
<button type="button" data-s="conflict" aria-pressed="true">409 · changed since opened</button>
|
||||
<button type="button" data-s="errors" aria-pressed="false">400 · field errors</button>
|
||||
<button type="button" data-s="tare" aria-pressed="false">Tare unknown</button>
|
||||
<button type="button" data-s="add" aria-pressed="false">Add new</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="form-wrap">
|
||||
<section class="sheet nm-dialog" role="dialog" aria-labelledby="form-h" aria-modal="false">
|
||||
<div class="sheet-head">
|
||||
<span class="swatch" style="width:40px;height:40px"><i style="background:#1b1b1b"></i></span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<h2 id="form-h"><span data-only="conflict errors tare edit">Edit spool #47</span><span data-only="add">Add spool</span></h2>
|
||||
<p class="small muted" style="margin:0"><span data-only="conflict errors tare edit">Opened 09:32 at version 4 · created 2 Aug · label printed 3 Aug</span><span data-only="add">New spools carry no version: nobody could have read one yet.</span></p>
|
||||
</div>
|
||||
<button class="btn btn-icon" type="button" aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-body">
|
||||
<!-- 409 -->
|
||||
<div class="conflict" role="alert" data-only="conflict">
|
||||
<h3>⚠ This spool changed since you opened it. Nothing was saved.</h3>
|
||||
<p style="margin:0">Someone weighed it at <b>09:39</b>, after you opened this form at 09:32 (version 4 → 5). Saving now would have put the old remaining weight back and lost that measurement.</p>
|
||||
<div class="table-wrap" style="margin:0;padding:0">
|
||||
<table class="tbl cmp">
|
||||
<thead><tr><th>Field</th><th class="r">When you opened (v4)</th><th class="r">Now (v5)</th><th class="r">Your edit</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Remaining <abbr title="net filament">net</abbr></td><td class="r num">860 g</td><td class="r num theirs">812 g · weighed</td><td class="r muted">not changed</td></tr>
|
||||
<tr><td>Empty spool (tare)</td><td class="r num">262 g</td><td class="r num">262 g</td><td class="r muted">not changed</td></tr>
|
||||
<tr><td>Location</td><td class="r">Shelf A3</td><td class="r">Shelf A3</td><td class="r mine">A1 · AMS Lite · slot 1</td></tr>
|
||||
<tr><td>Cost per kg</td><td class="r num">$22.79</td><td class="r num">$22.79</td><td class="r muted">not changed</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="small" style="margin:0">Your only change doesn't overlap theirs, so it's safe to reapply it to the new version.</p>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="button">Reload and reapply my location change</button>
|
||||
<button class="btn" type="button">Reload and discard my edit</button>
|
||||
</div>
|
||||
<p class="srv" style="margin:0">Server said (409): <code>this spool changed after you loaded it — reload it and reapply your edit, so a newer weigh-in is not overwritten (you loaded version 4, it is now version 5)</code></p>
|
||||
</div>
|
||||
|
||||
<!-- 400 summary -->
|
||||
<div class="errsum" role="alert" data-only="errors">
|
||||
<h3>⚠ 2 fields need fixing. Nothing was saved.</h3>
|
||||
<ul style="margin:0;padding-left:20px" class="small">
|
||||
<li><a href="#f-remaining">Remaining is more than the spool holds</a></li>
|
||||
<li><a href="#f-colorname">Color name is empty</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<fieldset class="sec">
|
||||
<legend>Name: 5 parts</legend>
|
||||
<div class="namebar nm-inset">
|
||||
<div class="slots" aria-label="Name parts">
|
||||
<div class="slot"><span class="k">Brand <span class="req">req</span></span><span class="v" id="sl-b">Sunlu</span></div>
|
||||
<div class="slot empty"><span class="k">Speed · opt</span><span class="v">none</span></div>
|
||||
<div class="slot"><span class="k">Material <span class="req">req</span></span><span class="v" id="sl-m">PETG</span></div>
|
||||
<div class="slot" id="sl-t"><span class="k">Type · opt</span><span class="v">CF</span></div>
|
||||
<div class="slot"><span class="k">Finish <span class="req">req</span></span><span class="v">Matte</span></div>
|
||||
</div>
|
||||
<div class="row"><span class="small muted">Reads as</span><span class="fullname">Sunlu PETG CF Matte</span><span class="chip chip-neutral">Black</span></div>
|
||||
</div>
|
||||
<div class="fr">
|
||||
<label class="field"><span class="lab">Brand <span class="req">required</span></span><select class="select"><option>Sunlu</option></select></label>
|
||||
<label class="field"><span class="lab">Print speed <em>optional</em></span><select class="select"><option>None</option><option>Rapid</option><option>High Speed</option></select></label>
|
||||
<label class="field"><span class="lab">Material <span class="req">required</span></span><select class="select"><option>PETG</option></select></label>
|
||||
<label class="field"><span class="lab">Filament type <em>optional</em></span><select class="select"><option>CF</option></select></label>
|
||||
<label class="field"><span class="lab">Finish <span class="req">required</span></span><select class="select"><option>Matte</option></select></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sec">
|
||||
<legend>Color</legend>
|
||||
<div class="fr">
|
||||
<div class="field" id="f-colorname">
|
||||
<label class="lab" for="cn">Color name <span class="req">required</span></label>
|
||||
<input class="input" id="cn" value="Black" data-only="conflict tare add edit">
|
||||
<div data-only="errors" class="field has-error" style="gap:6px">
|
||||
<input class="input" value="" placeholder="e.g. Charcoal" aria-invalid="true" aria-describedby="cn-err">
|
||||
<p class="field-error" id="cn-err" style="margin:0">Give the color a name, even "Unknown". <span class="srv">(server: <code>color_name is required</code>)</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><span class="lab">Colors <em>1–3, in order</em></span>
|
||||
<div class="colors-row"><span class="swatch"><i style="background:#1b1b1b"></i></span><input class="input" value="#1b1b1b" style="max-width:120px" aria-label="Color 1 hex"><button class="btn btn-sm" type="button">+ Add color</button></div></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sec">
|
||||
<legend>Weight: net filament vs what the scale reads</legend>
|
||||
<div class="weights">
|
||||
<div class="stack">
|
||||
<div class="fr">
|
||||
<label class="field"><span class="lab">Total <em>net, as sold</em></span><input class="input num" value="1000 g"><span class="hint">Filament only. A "1 kg spool" is 1000 g plus the spool.</span></label>
|
||||
<div class="field" id="f-remaining">
|
||||
<label class="lab" for="rem">Remaining <em>net</em></label>
|
||||
<input class="input num" id="rem" value="812 g" data-only="conflict tare edit">
|
||||
<input class="input num" value="600 g" data-only="add">
|
||||
<div data-only="errors" class="field has-error" style="gap:6px">
|
||||
<input class="input num is-error" value="1040 g" aria-invalid="true" aria-describedby="rem-err">
|
||||
<p class="field-error" id="rem-err" style="margin:0">1,040 g is more than this spool holds (1,000 g net). Lower remaining, or correct total. <span class="srv">(server: <code>remaining_grams cannot exceed total_grams</code>)</span></p>
|
||||
</div>
|
||||
<span class="hint" data-only="conflict edit">⚖ From a scale reading on 18 Sep 09:39, not typed</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="field"><span class="lab">Spool type <em>supplies the tare</em></span>
|
||||
<select class="select"><option>Sunlu — 1 kg plastic · 140 g · community</option><option>Elegoo — 1 kg cardboard · 154 g · manufacturer</option><option>Generic — 1 kg spool (assumed) · 180 g · estimated</option></select></label>
|
||||
<div class="field">
|
||||
<span class="lab">Empty spool (tare) <em>this reel</em></span>
|
||||
<div data-only="conflict errors edit">
|
||||
<input class="input num" value="262 g" aria-describedby="tare-src">
|
||||
<p class="hint" id="tare-src" style="margin:6px 0 0"><span class="chip chip-accent">this spool</span> Weighed bare on 14 Sep. It beats the spool type's 140 g (community).</p>
|
||||
</div>
|
||||
<div data-only="add">
|
||||
<input class="input num" value="140 g" aria-describedby="tare-add">
|
||||
<p class="hint" id="tare-add" style="margin:6px 0 0"><span class="chip chip-info">community</span> From the Sunlu — 1 kg plastic spool type. Measured by users and corroborated. Only saved on this spool if you weigh it.</p>
|
||||
</div>
|
||||
<div data-only="tare">
|
||||
<input class="input" value="" placeholder="unknown: type it, or pick a spool type" aria-describedby="tare-unk">
|
||||
<p class="hint" id="tare-unk" style="margin:6px 0 0"><span class="chip chip-warn chip-dashed">? unknown</span> No spool type chosen, and TECSONAR has no published tare. <b>Not treated as 0</b>: 0 would count the whole scale reading as filament.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scale nm-raised">
|
||||
<span class="lab-t small" style="font-weight:700">On the scale</span>
|
||||
<div data-only="conflict errors edit">
|
||||
<div class="stackbar" aria-hidden="true"><span class="t" style="width:20.8%"></span><span class="f" style="width:64.4%"></span><span class="u"></span></div>
|
||||
<div class="eq" style="margin-top:12px">
|
||||
<span><span class="key" style="background:repeating-linear-gradient(45deg,var(--border-strong) 0 3px,transparent 3px 6px)"></span></span><span>Empty spool (tare)</span><span>262 g</span>
|
||||
<span><span class="key" style="background:var(--text-accent)"></span></span><span>Filament remaining (net)</span><span>812 g</span>
|
||||
<span></span><span class="tot">Gross: should read</span><span class="tot">1,074 g</span>
|
||||
</div>
|
||||
<label class="field" style="margin-top:12px"><span class="lab">Weigh it <em>whole spool</em></span><input class="input num" placeholder="full reads 1,262 g"></label>
|
||||
<span class="hint">Type what the scale shows and remaining is worked out for you: reading − 262 g.</span>
|
||||
</div>
|
||||
<div data-only="add">
|
||||
<div class="stackbar" aria-hidden="true"><span class="t" style="width:12.3%"></span><span class="f" style="width:52.6%"></span><span class="u"></span></div>
|
||||
<div class="eq" style="margin-top:12px">
|
||||
<span><span class="key" style="background:repeating-linear-gradient(45deg,var(--border-strong) 0 3px,transparent 3px 6px)"></span></span><span>Empty spool (tare, community)</span><span>140 g</span>
|
||||
<span><span class="key" style="background:var(--text-accent)"></span></span><span>Filament remaining (net)</span><span>600 g</span>
|
||||
<span></span><span class="tot">Gross: should read</span><span class="tot">740 g</span>
|
||||
</div>
|
||||
<label class="field" style="margin-top:12px"><span class="lab">Weigh it <em>whole spool</em></span><input class="input num" placeholder="full reads 1,140 g"></label>
|
||||
<span class="hint">Weighing saves the 140 g tare on this spool, marked community, alongside the reading.</span>
|
||||
</div>
|
||||
<div data-only="tare">
|
||||
<div class="stackbar" aria-hidden="true" style="border-style:dashed"><span class="f" style="width:18.8%"></span></div>
|
||||
<div class="eq" style="margin-top:12px">
|
||||
<span></span><span>Empty spool (tare)</span><span class="muted">unknown</span>
|
||||
<span><span class="key" style="background:var(--text-accent)"></span></span><span>Filament remaining (net)</span><span>188 g</span>
|
||||
<span></span><span class="tot">Gross</span><span class="tot muted">unknown</span>
|
||||
</div>
|
||||
<label class="field" style="margin-top:12px"><span class="lab">Weigh it <em>whole spool</em></span><input class="input is-disabled" disabled placeholder="needs a tare first"></label>
|
||||
<span class="hint">Set an empty-spool weight (pick a spool type, or weigh the bare spool) and you can weigh the whole spool instead of guessing.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sec">
|
||||
<legend>Cost and place</legend>
|
||||
<div class="fr">
|
||||
<label class="field"><span class="lab">Cost per kg</span><input class="input num" value="$22.79"></label>
|
||||
<label class="field"><span class="lab">Location <em>optional</em></span>
|
||||
<input class="input" value="Shelf A3" data-only="errors tare add">
|
||||
<input class="input" value="A1 · AMS Lite · slot 1" data-only="conflict edit" style="border-color:var(--text-accent)">
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="sheet-foot">
|
||||
<span class="small muted" style="margin-right:auto" data-only="conflict">Save is off until you choose how to handle the newer version.</span>
|
||||
<button class="btn" type="button">Cancel</button>
|
||||
<button class="btn btn-primary is-disabled" type="button" disabled data-only="conflict">Save changes</button>
|
||||
<button class="btn btn-primary" type="button" data-only="errors tare edit">Save changes</button>
|
||||
<button class="btn btn-primary" type="button" data-only="add">Add spool</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="specimen nm-raised" aria-labelledby="spec-h">
|
||||
<h2 class="section" id="spec-h">Control states: v3 specimen</h2>
|
||||
<p class="small muted" style="margin-top:0">Every state changes something other than shadow direction. Switch themes with the toggle to check both.</p>
|
||||
<div class="spec-grid">
|
||||
<div class="spec"><button class="btn" type="button">Save</button><span class="cap"><b>Rest</b><br>raised + 1px border (3:1)</span></div>
|
||||
<div class="spec"><button class="btn is-hover" type="button">Save</button><span class="cap"><b>Hover</b><br>border turns accent</span></div>
|
||||
<div class="spec"><button class="btn is-pressed" type="button">Low stock</button><span class="cap"><b>Pressed / on</b><br>inset + wash fill + ✓ + accent border</span></div>
|
||||
<div class="spec"><button class="btn is-focused" type="button">Save</button><span class="cap"><b>Focused</b><br>2px outline, 3px offset</span></div>
|
||||
<div class="spec"><button class="btn" disabled type="button">Save</button><span class="cap"><b>Disabled</b><br>flat + dashed border + muted text</span></div>
|
||||
<div class="spec"><button class="btn btn-primary" type="button">Add spool</button><span class="cap"><b>Primary</b><br>ink on light violet (6.14:1 dark)</span></div>
|
||||
<div class="spec"><button class="btn btn-primary is-pressed" type="button">Add spool</button><span class="cap"><b>Primary pressed</b><br>deeper fill + ✓ + ring</span></div>
|
||||
<div class="spec"><button class="btn btn-danger" type="button">Delete</button><span class="cap"><b>Danger</b><br>red text + red border</span></div>
|
||||
<div class="spec" style="width:100%"><input class="input" placeholder="Rest"><span class="cap"><b>Input rest</b><br>inset + border</span></div>
|
||||
<div class="spec" style="width:100%"><input class="input is-focused" value="Focused"><span class="cap"><b>Input focus</b><br>outline, border unchanged</span></div>
|
||||
<div class="spec" style="width:100%"><input class="input" disabled value="Disabled"><span class="cap"><b>Input disabled</b><br>dashed, flat</span></div>
|
||||
<div class="spec field has-error" style="width:100%;gap:6px"><input class="input" value="1040 g" aria-invalid="true"><p class="field-error" style="margin:0">Too much</p><span class="cap"><b>Input error</b><br>2px red + ⚠ + message</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
function set(s){document.body.className='s-'+s;
|
||||
document.querySelectorAll('#scen [data-s]').forEach(function(b){b.setAttribute('aria-pressed',String(b.getAttribute('data-s')===s))});
|
||||
var tareSheet = s==='tare';
|
||||
document.querySelector('.sheet-head .swatch').innerHTML = tareSheet ? '<i style="background:#161616"></i><i style="background:#c0232f"></i>' : '<i style="background:#1b1b1b"></i>';
|
||||
document.querySelector('.fullname').textContent = tareSheet ? 'TECSONAR PLA Matte' : 'Sunlu PETG CF Matte';
|
||||
document.getElementById('sl-b').textContent = tareSheet ? 'TECSONAR' : 'Sunlu';
|
||||
document.getElementById('sl-m').textContent = tareSheet ? 'PLA' : 'PETG';
|
||||
var t = document.getElementById('sl-t'); t.className = tareSheet ? 'slot empty' : 'slot'; t.querySelector('.v').textContent = tareSheet ? 'none' : 'CF';
|
||||
document.querySelector('.sheet-head p span').textContent = tareSheet ? 'Opened 09:32 at version 2 · created 2 Aug' : 'Opened 09:32 at version 4 · created 2 Aug · label printed 3 Aug';
|
||||
document.getElementById('form-h').firstElementChild.textContent = tareSheet ? 'Edit spool #44' : 'Edit spool #47';
|
||||
}
|
||||
document.addEventListener('click',function(e){var b=e.target.closest('#scen [data-s]'); if(b) set(b.getAttribute('data-s'));});
|
||||
document.addEventListener('DOMContentLoaded',function(){ set(location.hash==='#add'?'add':'conflict'); });
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,209 @@
|
||||
<style>
|
||||
.datasrc { padding: var(--space-4) var(--space-5); display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-4); align-items: center; }
|
||||
.group-h { display: flex; align-items: center; gap: var(--space-3); margin: 0 0 var(--space-4); font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); font-weight: var(--fw-bold); }
|
||||
.group-h .n { display: inline-grid; place-items: center; min-width: 24px; height: 24px; border-radius: var(--radius-pill); background: var(--surface-inset); box-shadow: var(--nm-inset-1); border: 1px solid var(--border-hairline); color: var(--text-primary); }
|
||||
.groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: var(--space-6); align-items: start; }
|
||||
.pgrid { display: grid; gap: var(--space-6); }
|
||||
.pcard { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); }
|
||||
.pcard.attn { border: 2px solid var(--status-warning); }
|
||||
.phead { display: flex; gap: var(--space-3); align-items: flex-start; }
|
||||
.phead h3 { margin: 0; font-size: var(--fs-h3); line-height: var(--lh-snug); }
|
||||
.phead .model { font-size: var(--fs-label); color: var(--text-secondary); }
|
||||
.cam { aspect-ratio: 16 / 9; border-radius: var(--radius-md); background: radial-gradient(ellipse at 50% 90%, #5a4b3a 0 28%, transparent 29%), linear-gradient(180deg, #1c1d22 0%, #2b2c33 60%, #3a3326 100%); border: 1px solid var(--border-control); box-shadow: var(--nm-inset-2); position: relative; overflow: hidden; }
|
||||
.cam::after { content: ""; position: absolute; left: 30%; right: 30%; bottom: 20%; height: 22%; background: repeating-linear-gradient(0deg, #6b8fd6 0 3px, #5a7cc2 3px 6px); border-radius: 4px 4px 0 0; opacity: .85; }
|
||||
.cam .stamp { position: absolute; left: 10px; top: 8px; font-size: var(--fs-caption); color: #f4f4f6; background: rgba(0, 0, 0, .55); padding: 2px 8px; border-radius: var(--radius-pill); z-index: 1; }
|
||||
.cam-off { min-height: 72px; border-radius: var(--radius-md); border: 1px dashed var(--border-control); display: grid; place-items: center; text-align: center; padding: var(--space-4); color: var(--text-secondary); font-size: var(--fs-label); }
|
||||
.nowjob { padding: var(--space-4); display: grid; grid-template-columns: 1fr auto; gap: 2px var(--space-3); }
|
||||
.nowjob .jn { font-weight: var(--fw-bold); }
|
||||
.slots4 { display: grid; gap: var(--space-2); }
|
||||
.cont { padding: var(--space-3) var(--space-4); }
|
||||
.cont-h { display: flex; justify-content: space-between; font-size: var(--fs-label); margin-bottom: var(--space-2); }
|
||||
.slotrow { display: grid; grid-template-columns: 22px auto minmax(0, 1fr) auto; gap: var(--space-2) var(--space-3); align-items: center; font-size: 13px; padding: 4px 0; }
|
||||
.slotrow .i { color: var(--text-secondary); font-size: var(--fs-caption); }
|
||||
.slotrow.empty .nm { color: var(--text-secondary); font-style: italic; }
|
||||
.conn { display: grid; grid-template-columns: auto 1fr; gap: 4px var(--space-4); font-size: var(--fs-label); margin: 0; }
|
||||
.conn dt { color: var(--text-secondary); }
|
||||
.conn dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
||||
details.cfg { border-top: 1px solid var(--border-hairline); padding-top: var(--space-3); }
|
||||
details.cfg summary { cursor: pointer; font-size: var(--fs-label); font-weight: var(--fw-bold); }
|
||||
details.cfg summary:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }
|
||||
.lost { padding: var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--status-warning); background: var(--wash-warning); display: flex; flex-direction: column; gap: var(--space-2); font-size: 13px; }
|
||||
.lost h4 { margin: 0; color: var(--status-warning); font-size: 14px; }
|
||||
.lost ul { margin: 0; padding-left: 18px; }
|
||||
.stale { opacity: 1; border-style: dashed !important; }
|
||||
.stale-tag { font-size: var(--fs-caption); color: var(--status-warning); font-weight: var(--fw-bold); }
|
||||
.pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--status-success); box-shadow: 0 0 0 3px var(--wash-success); }
|
||||
.future { padding: var(--space-6); border: 2px dashed var(--border-control); border-radius: var(--radius-lg); background: transparent; }
|
||||
.future h2 { margin: 0; font-size: var(--fs-h3); display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
|
||||
.fgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-4); margin-top: var(--space-5); }
|
||||
.fitem { padding: var(--space-4); border: 1px dashed var(--border-control); border-radius: var(--radius-md); display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.fitem h3 { margin: 0; font-size: 14px; }
|
||||
.fitem .why { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
|
||||
.ghost { opacity: .9; filter: grayscale(1); pointer-events: none; }
|
||||
body.dev-only [data-feature] { display: none !important; }
|
||||
body:not(.dev-only) [data-devonly] { display: none !important; }
|
||||
</style>
|
||||
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>Printers</h1>
|
||||
<p class="muted">3 printers · status polled every 10 s · MQTT is read-only</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" type="button">Manage AMS units</button>
|
||||
<button class="btn btn-primary" type="button">+ Add printer</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="datasrc nm-raised" aria-label="Data source">
|
||||
<span class="small"><b>Data shown</b></span>
|
||||
<div class="seg" role="group" aria-label="Which build's data to show" id="ds">
|
||||
<button type="button" data-ds="dev" aria-pressed="false">dev today</button>
|
||||
<button type="button" data-ds="feat" aria-pressed="true">dev + printer-connections branch</button>
|
||||
</div>
|
||||
<span class="small muted">Fields tagged <span class="chip chip-info chip-dashed">feature</span> exist only on <code>feature/7efc2030…-printer-connections</code> (4e17f9e), not in production.</span>
|
||||
</section>
|
||||
|
||||
<div class="groups">
|
||||
<section aria-labelledby="g-progress">
|
||||
<h2 class="group-h" id="g-progress">In progress <span class="n">1</span></h2>
|
||||
<div class="pgrid">
|
||||
<article class="pcard nm-raised" aria-labelledby="p1">
|
||||
<div class="phead">
|
||||
<div style="flex:1;min-width:0"><h3 id="p1">P1S - Prints Charming</h3><span class="model">Bambu Lab · P1S</span></div>
|
||||
<span class="chip chip-accent">● Printing</span>
|
||||
<button class="btn btn-icon btn-sm" type="button" aria-label="Actions for P1S - Prints Charming" aria-haspopup="menu">⋯</button>
|
||||
</div>
|
||||
<div class="row small" data-feature><span class="pulse" aria-hidden="true"></span><span>Ready · last report <b>2s ago</b></span><span class="chip chip-info chip-dashed">feature</span></div>
|
||||
<div class="row small" data-devonly><span class="pulse" aria-hidden="true"></span><span>Connected</span></div>
|
||||
<div class="cam" role="img" aria-label="Camera snapshot from P1S - Prints Charming"><span class="stamp">snapshot 09:41:02</span></div>
|
||||
<div class="nowjob nm-inset">
|
||||
<span class="jn">Gridfinity_bins_3x2</span><span class="num">1h 04m</span>
|
||||
<span class="small muted">started 08:37 · weight read from the 3MF when it ends</span><span class="small muted">so far</span>
|
||||
</div>
|
||||
<div class="slots4" aria-label="Filament loaded in P1S - Prints Charming">
|
||||
<div class="row"><span class="small"><b>Loaded filament</b></span><span class="chip chip-neutral">not confirmed in use</span></div>
|
||||
<div class="cont nm-inset">
|
||||
<div class="cont-h"><b>AMS 1</b><span class="muted">4 / 4</span></div>
|
||||
<div class="slotrow"><span class="i">1</span><span class="swatch swatch-sm"><i style="background:#141414"></i></span><span class="nm">Sunlu · Black <span class="muted">PLA + 2.0</span></span><span class="num">1000 g</span></div>
|
||||
<div class="slotrow"><span class="i">2</span><span class="swatch swatch-sm"><i style="background:#b87333"></i></span><span class="nm">Sunlu · Copper <span class="muted">PLA + Silk</span></span><span class="num" style="color:var(--status-warning)">⚠ 97 g</span></div>
|
||||
<div class="slotrow"><span class="i">3</span><span class="swatch swatch-sm"><i style="background:#ece8df"></i></span><span class="nm">Overture · Mist Gray <span class="muted">PLA Marble</span></span><span class="num">920 g</span></div>
|
||||
<div class="slotrow"><span class="i">4</span><span class="swatch swatch-sm"><i style="background:#101010"></i></span><span class="nm">JAYO · Black <span class="muted">Rapid PETG Matte</span></span><span class="num">655 g</span></div>
|
||||
</div>
|
||||
<div class="cont nm-inset">
|
||||
<div class="cont-h"><b>External</b><span class="muted">0 / 1</span></div>
|
||||
<div class="slotrow empty"><span class="i">1</span><span></span><span class="nm">Empty</span><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="cfg"><summary>Connection details (stored)</summary>
|
||||
<dl class="conn" style="margin-top:8px">
|
||||
<dt>IP</dt><dd>10.60.1.242</dd><dt>Serial</dt><dd>01P09A••••••4821</dd><dt>Access code</dt><dd>✓ stored, never shown</dd><dt>Added</dt><dd>7 Aug 2026</dd>
|
||||
<dt data-feature>Session</dt><dd data-feature>#3 <span class="chip chip-info chip-dashed">feature</span></dd>
|
||||
</dl></details>
|
||||
<p class="small muted" style="margin:0">Last finished: Lid_v3 · failed yesterday 16:49</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="g-attn">
|
||||
<h2 class="group-h" id="g-attn">Needs attention <span class="n">1</span></h2>
|
||||
<div class="pgrid">
|
||||
<article class="pcard nm-raised attn" aria-labelledby="p3">
|
||||
<div class="phead">
|
||||
<div style="flex:1;min-width:0"><h3 id="p3">A1 - Jordyns Layer Slayer</h3><span class="model">Bambu Lab · A1</span></div>
|
||||
<span class="chip chip-warn" data-feature>⟳ Reconnecting</span>
|
||||
<span class="chip chip-warn" data-devonly>⚡ Offline</span>
|
||||
<button class="btn btn-icon btn-sm" type="button" aria-label="Actions for A1 - Jordyns Layer Slayer" aria-haspopup="menu">⋯</button>
|
||||
</div>
|
||||
|
||||
<div class="lost" role="status">
|
||||
<h4>⚡ MQTT connection lost</h4>
|
||||
<span data-feature>Disconnected, reconnecting. Last contact <b>6m ago</b> (09:35:11). <span class="chip chip-info chip-dashed">feature</span></span>
|
||||
<span data-devonly>Extrudex's MQTT session to this printer dropped. The time it dropped isn't recorded on dev.</span>
|
||||
<ul class="small">
|
||||
<li>The printer itself may still be printing. This is Extrudex's connection, not the printer's power.</li>
|
||||
<li><b>Print capture is paused.</b> A job that starts and finishes during the outage won't be recorded.</li>
|
||||
<li>On reconnect: if <i>Coaster_set</i> is still running, its record stays open and gets <b>+1 gap</b> (its duration becomes an upper bound). Otherwise it closes as <b>unknown</b>, with no duration.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nowjob nm-inset stale" style="border:1px dashed var(--status-warning)">
|
||||
<span class="jn">Coaster_set</span><span class="stale-tag">OPEN RECORD</span>
|
||||
<span class="small muted">started 08:55 · watched until the connection dropped</span><span></span>
|
||||
<span class="small" data-feature style="grid-column:1/-1"><span class="stale-tag">Last known:</span> Printing · 6m ago. Cached from the lost session, not live. <span class="chip chip-info chip-dashed">feature</span></span>
|
||||
</div>
|
||||
<div class="cam-off">Camera unavailable while the printer is offline</div>
|
||||
<div class="slots4" aria-label="Filament loaded in A1 - Jordyns Layer Slayer">
|
||||
<div class="row"><span class="small"><b>Loaded filament</b></span><span class="chip chip-neutral">from Extrudex records, not the printer</span></div>
|
||||
<div class="cont nm-inset">
|
||||
<div class="cont-h"><b>AMS Lite</b><span class="muted">2 / 4</span></div>
|
||||
<div class="slotrow"><span class="i">1</span><span class="swatch swatch-sm"><i style="background:#1b1b1b"></i></span><span class="nm">Sunlu · Black <span class="muted">PETG CF Matte</span></span><span class="num">812 g</span></div>
|
||||
<div class="slotrow"><span class="i">2</span><span class="swatch swatch-sm"><i style="background:#e8453c"></i><i style="background:#f2c230"></i><i style="background:#2a8fd6"></i></span><span class="nm">AMOLEN · Red Yellow Blue <span class="muted">PLA Matte</span></span><span class="num">744 g</span></div>
|
||||
<div class="slotrow empty"><span class="i">3</span><span></span><span class="nm">Empty</span><span></span></div>
|
||||
<div class="slotrow empty"><span class="i">4</span><span></span><span class="nm">Empty</span><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="cfg" open><summary>Connection details (stored)</summary>
|
||||
<dl class="conn" style="margin-top:8px">
|
||||
<dt>IP</dt><dd>10.60.1.x <span class="muted">(sample)</span></dd><dt>Serial</dt><dd>039••••••••7710</dd><dt>Access code</dt><dd>✓ stored, never shown</dd>
|
||||
<dt data-feature>Lifecycle</dt><dd data-feature>reconnecting · <code>stale: true</code> <span class="chip chip-info chip-dashed">feature</span></dd>
|
||||
<dt data-feature>Failure</dt><dd data-feature><span class="muted">none reported. A reconnect can have no failure detail.</span></dd>
|
||||
<dt data-feature>Session</dt><dd data-feature>#14 (increments on every reconnect)</dd>
|
||||
</dl></details>
|
||||
<div class="actions"><button class="btn btn-sm" type="button">Edit connection</button><span class="small muted">Retrying automatically. There's nothing to press.</span></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="g-idle">
|
||||
<h2 class="group-h" id="g-idle">Idle <span class="n">1</span></h2>
|
||||
<div class="pgrid">
|
||||
<article class="pcard nm-raised-sm" aria-labelledby="p2" style="padding: var(--space-5)">
|
||||
<div class="phead">
|
||||
<div style="flex:1;min-width:0"><h3 id="p2">H2C - Constipation Orion</h3><span class="model">Bambu Lab · H2C</span></div>
|
||||
<span class="chip chip-neutral">Idle</span>
|
||||
<button class="btn btn-icon btn-sm" type="button" aria-label="Actions for H2C - Constipation Orion" aria-haspopup="menu">⋯</button>
|
||||
</div>
|
||||
<div class="row small"><span class="pulse" aria-hidden="true"></span><span data-feature>Ready · last report 4s ago <span class="chip chip-info chip-dashed">feature</span></span><span data-devonly>Connected</span></div>
|
||||
<p class="small" style="margin:0">Last: <b>Benchy_x4</b> · finished 05:27 · <span style="color:var(--status-warning);font-weight:700">≤ 3h 12m</span> <span class="chip chip-warn chip-dashed">upper bound · 1 gap</span></p>
|
||||
<div class="cont nm-inset">
|
||||
<div class="cont-h"><b>AMS 1</b><span class="muted">3 / 4</span></div>
|
||||
<div class="slotrow"><span class="i">1</span><span class="swatch swatch-sm"><i style="background:#141414"></i></span><span class="nm">Elegoo · Black <span class="muted">Rapid PETG</span></span><span class="num" style="color:var(--status-warning)">⚠ 142 g</span></div>
|
||||
<div class="slotrow"><span class="i">2</span><span class="swatch swatch-sm"><i style="background:#1d4fa8"></i></span><span class="nm">Elegoo · Blue <span class="muted">Rapid PETG</span></span><span class="num">1000 g</span></div>
|
||||
<div class="slotrow"><span class="i">3</span><span class="swatch swatch-sm"><i style="background:#f4f4f0"></i></span><span class="nm">Elegoo · White <span class="muted">PLA Matte</span></span><span class="num">610 g</span></div>
|
||||
<div class="slotrow empty"><span class="i">4</span><span></span><span class="nm">Empty</span><span></span></div>
|
||||
</div>
|
||||
<details class="cfg"><summary>Connection details (stored)</summary>
|
||||
<dl class="conn" style="margin-top:8px"><dt>IP</dt><dd>10.60.1.202</dd><dt>Serial</dt><dd>31B••••••••0093</dd><dt>Access code</dt><dd>✓ stored, never shown</dd></dl></details>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<section class="future" aria-labelledby="future-h">
|
||||
<h2 id="future-h"><span class="chip chip-neutral chip-dashed">Future, not built</span> Ideas that need data Extrudex doesn't store</h2>
|
||||
<p class="small muted" style="margin:var(--space-2) 0 0">Nothing in this box can be built from today's data. Each item says what's missing.</p>
|
||||
<div class="fgrid">
|
||||
<div class="fitem"><h3>Progress % and time left</h3><div class="track ghost" aria-hidden="true"><span style="width:58%"></span></div>
|
||||
<span class="why"><code>mc_percent</code> is parsed from MQTT but used only internally. It isn't stored or returned by <code>/api/printers</code>. ETA isn't read at all.</span></div>
|
||||
<div class="fitem"><h3>Nozzle and bed temperature</h3><span class="ghost small">220 °C · 60 °C</span>
|
||||
<span class="why">Temperatures aren't parsed from the report.</span></div>
|
||||
<div class="fitem"><h3>Current layer</h3><span class="ghost small">Layer 112 / 190</span>
|
||||
<span class="why">Layer fields aren't parsed.</span></div>
|
||||
<div class="fitem"><h3>AMS remaining % and humidity</h3><span class="ghost small">82% · humidity 2</span>
|
||||
<span class="why">Not read. AMS <code>remain</code> is only meaningful for genuine RFID spools, which isn't what's mostly run here.</span></div>
|
||||
<div class="fitem"><h3>Alert when MQTT drops</h3><span class="ghost small">🔔 notify after 5 min</span>
|
||||
<span class="why">Drops only show in container logs today (CONTEXT open work #4). On dev, nothing stores <i>when</i> it dropped.</span></div>
|
||||
<div class="fitem"><h3>Which spool is feeding now</h3><span class="ghost small">▶ AMS 1 · slot 2</span>
|
||||
<span class="why">"Loaded" is recorded; "in use" isn't. That's why the list says <i>not confirmed in use</i>.</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
function set(v){document.body.classList.toggle('dev-only', v==='dev');
|
||||
document.querySelectorAll('#ds [data-ds]').forEach(function(b){b.setAttribute('aria-pressed',String(b.getAttribute('data-ds')===v))});}
|
||||
document.addEventListener('click',function(e){var b=e.target.closest('#ds [data-ds]'); if(b) set(b.getAttribute('data-ds'));});
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user