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>
13 KiB
Extrudex UI redesign: what exists today
Written 2026-09-18, before any design work. Every source here was read, not changed.
| Source | Version read |
|---|---|
CTXD extrudex: CONTEXT.MD v3, DECISIONS.MD v4, GLOSSARY.MD v3, HANDOFFS.MD v32 (skimmed) |
2026-09-07 / 2026-09-17 |
Repository dev at 321ce0d (2026-09-17) |
read only |
Printer-connections feature branch, local ref origin/feature/7efc2030-…-printer-connections at 4e17f9e (2026-09-17) |
read with git diff, no checkout, no fetch |
Design System v2.0: http://pages.ccl/design-v2.0/ |
HTTP 200, tokens extracted |
Scope agreed on 2026-09-18
- Designs are based on the current
devcode, not on the 2026-08-12 picture in CTXD. CTXD is out of date (see "Differences between CTXD and the code" at the end). - Screen 1 is a new standalone Dashboard. Today, the home route
/isFloor.tsx. - The Printers screen uses the fields stored on
devplus the lifecycle and staleness fields from the unmerged printer-connections feature branch. The branch fields are labeled "on feature branch, not in production".
1. Fields shown on each existing screen
Floor (pages/Floor.tsx, route /, the de facto dashboard)
Polls every 10 s. It has two columns.
Primary column
- Printers view (
pages/Printers.tsxembedded; see below) - "Today's prints", from midnight America/New_York, with an "All prints" link
- Print list (
components/PrintList.tsx), one row per print:- job name (click to edit; locked while running with "name is captured from the printer")
- printer name · started at
- status badge:
running/finished/failed/unknown - duration (
—when null), plus a+gapmarker whenconnection_gaps > 0. The tooltip says the figure is "an upper bound rather than a measurement". - grams used (click to edit) or a grams input; multi-color prints show "N g total"
- spool picker ("274 g · Sunlu Mint Green · PLA · Matte"), with a suggested spool
- cost, or "cost pending" on multi-color prints
- use type select: Personal / Business / "Use not recorded"
- archive control
- per-color rows for multi-color prints: slot, spool, grams, color hex, suggested spool
- Empty states: "Nothing printed today." / "No prints captured yet."
Secondary column
- "Running low": spools under
LOW_STOCK_GRAMS= 200 g, each with a color swatch, color name, brand and grams - "Inventory" stats: Spools, Filament left (g), Stock value ($), Running low (count, warning style)
- Empty state: "No filament tracked yet."
Not shown today:
weight_source(see §2 gaps), tare, gross weight.
Filament (pages/Filament.tsx + components/SpoolTable.tsx, route /filament)
- Header: "Labels outstanding: N", Download unlabeled labels, Manage lists, Add spool
- Filters: search ("Search brand, colour, material, location…"), Low stock only, In storage only, brand select with counts, material select with counts, sort (Least remaining / Most remaining / Brand A–Z / Colour A–Z / Highest value), Group identical, view toggle Table / Cards, pagination
- Table columns: swatch · color name (click to edit) + brand · Filament (material · type · finish · speed; "Standard"/"Basic" hidden) · Remaining ("N of M g" + bar, red below 200 g) · $/kg ("Not set" when 0) · Value · Location ("In storage · detail" or container name) · Delete
- Grouped rows: combined total, "N of M g each", "$X/kg each", expand/collapse
- Card view: swatch, color name, brand, taxonomy line, bar, "N g left", value, "In storage" / "Loaded · container", storage detail, Edit, Delete
- Delete opens
ConfirmDelete - Error and status lines, plus empty states
Not shown today: tare, tare source, gross weight, remaining source (
typed/weighed), label printed date.
Add/Edit spool (components/SpoolForm.tsx, a modal <dialog>)
Title: "Add spool" / "Edit spool #ID". It has four sections.
-
Identity: Brand* · Material* · Type (optional) · Finish* · Speed (optional) · Spool type (optional, "sets the empty-spool weight") · Location (optional, "Shelf A")
-
Colour: Colour name (with a suggestion from the pinned FilamentColors.xyz catalog: unique, ambiguous or no match) · 1–3 colors, each with a swatch picker and hex text, plus Add colour / Remove / "Colour not known yet" (stores #888888)
-
Quantity and cost: Total grams ("filament, not incl. spool") · Remaining grams · Empty spool (tare, optional, "e.g. 250") with a tare origin note:
spool: "This spool's own weight. Saved with it."spool_type: "From the X spool type", plus "(estimated — weigh your own to be sure)"brand: "Guessed from the X brand…" (warning style)unknown: "Recorded on this spool, but nothing says whether it was weighed…"none: no note
Then Cost per kg.
-
Weigh it (only when tare > 0): scale reading, placeholder "full reads N", hint "Remaining is filled in for you: reading minus N g of spool". Otherwise: "Set an empty-spool weight… and you can weigh the whole spool".
- Footer: client-side errors (scale below tare, capacity contradiction), a single server
errorline, Cancel, Save changes / Add spool - 409 today: the server message ("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)") appears as plain text in the footer. There is no reload action and no side-by-side comparison.
- 400 today: also only in the footer, never next to the field that caused it.
Real 400 messages that can be mapped to a field: color_name is required · cost_per_kg cannot be negative · remaining_grams cannot be negative · remaining_grams cannot exceed total_grams · gross_grams must be greater than zero — it is the whole spool on the scale · a tare of 0 is not a measurement — leave it unset instead · scale_grams is required and must be greater than zero.
Printers (pages/Printers.tsx, embedded in Floor; /printers redirects to /)
This screen already exists on dev. It is not new.
- Header: Add printer / Manage AMS units (admin only; others see "Admin role required…")
- Grouped into In progress, Needs attention (paused, failed), Idle, Offline, with counts
- Full card (busy/paused/failed): name · state badge (Printing, Paused, Failed, Idle, Preparing, Slicing, Waiting…, Offline) · actions menu (Edit, Remove) · job name or "Job name unavailable" · camera snapshot (or "disabled" / "unavailable while offline" / "unavailable") · Loaded filament, "not confirmed in use": each container's label, used/capacity, and each spool's swatch, brand, color and grams (or "Empty") · last print line
- Compact row (idle/offline): state marker, name, badge, last job + "Ended …", or "Last contact unavailable" when offline
- Printer form (
PrinterForm.tsx): name, brand (bambu/elegoo), model, IP, serial, access code (write-only)
2. Printer data Extrudex stores today
Stored in SQLite on dev (schema.sql)
| Table | Field | Notes |
|---|---|---|
printer |
id, name (unique), brand (bambu/elegoo/null), model, ip, serial (unique), access_code (never returned; API gives has_access_code), created_at |
brand and model are null together (rows imported from printers.json) |
filament_container |
id, printer_id, kind (ams/ams_lite/external), label, created_at, version; capacity derived (4/4/1) |
every printer gets one "External" holder |
spool |
container_id |
which holder a spool is in; NULL = storage |
spool_location_event |
spool_id, container_id, at, principal_id |
move history, not shown in UI |
print |
printer (name text), printer_id, job_name, job_key, status, started_at, ended_at, duration_seconds, connection_gaps, grams_used, weight_source, cost_usd, deducted_grams, charged_rate_per_kg, spool_id, slice_objects, archived_at, use_type |
per-print history |
print_filament |
slot, spool_id, suggested_spool_id, grams_used, color_hex, weight_source, deducted_grams, charged_rate_per_kg, cost_usd |
multi-color components |
job_grams |
job_key, grams, updated_at |
remembered weights |
Live, in memory, served by GET /api/printers on dev (PrinterStatus)
id, name, connected (the transport session, not power), state (gcode_state: IDLE, PREPARE, RUNNING, PAUSE, FINISH, FAILED, SLICING, or "" until first seen), job_name (only while a job runs), camera (snapshot/unsupported/disabled), last_print. Also GET /api/printers/{id}/snapshot returns a JPEG.
Parsed but not stored or exposed
mc_percent (progress) and gcode_file_prepare_percent are read from MQTT and used internally only. There are no temperatures, ETA, layer or AMS remain %. Bambu never reports grams.
On the feature branch only (not on dev, not in production)
Added to PrinterStatus: lifecycle (connecting / synchronizing / ready / reconnecting / invalid_config / unsupported / stopped), ready, stale (state and job are cached from a session that is not ready), last_report_at, last_report_age_seconds, failure_category (setup_failed / setup_timeout / synchronization_timeout), failure_detail (a fixed sanitized phrase), session (generation counter). There are UI helpers for "Last known: Printing · 4m ago", "Disconnected — reconnecting" and "Last contact 4m ago".
The dropped-MQTT state the brief asks for is connected=false, which shows as "Offline" on dev. On the feature branch it becomes lifecycle=reconnecting with stale=true and a report age.
3. Design System v2.0 color palette (from http://pages.ccl/design-v2.0/)
It matches web/src/theme.css on dev exactly. v2.0 has one (dark) theme only. There is no light theme.
| Token | Hex | Usage |
|---|---|---|
--ccl-charcoal |
#2c2c30 |
page background, near-black (--surface-page) |
--ccl-charcoal-2 |
#36363a |
raised surface (--surface-raised) |
--ccl-charcoal-3 |
#1b1b1e |
input / code block, recessed (--surface-code) |
--ccl-gold |
#8f7ae0 |
brand accent, violet despite the name (--accent, --focus-ring, alias --ccl-purple) |
--ccl-gold-dim |
#7a63cc |
accent hover / pressed (--accent-hover) |
--ccl-text |
#f4f4f6 |
primary text |
--ccl-text-muted |
#b2b2ba |
secondary text |
--ccl-border |
#3d3d44 |
hairline borders |
--ccl-border-2 |
#4e4e57 |
stronger border / grid lines |
--ccl-green |
#a9d88b |
success |
--ccl-red |
#f08b93 |
error |
--ccl-white |
#ffffff |
white |
--ccl-ink |
#2a2431 |
text on accent |
--ccl-purple-wash |
rgba(143,122,224,0.12) |
accent tint |
Other v2.0 rules the redesign has to deal with:
- Radius:
--radius-none/sm/mdare all0px("the brand is square. 0 is the rule"). - Shadows: flat brand. The only shadow is
--shadow-raised: 0 1px 0 0 var(--ccl-border). - Font: JetBrains Mono for everything. Sizes are 11/12/14/15/18/22/28/40 px and weights 400/500/700/800.
- Spacing runs from 4 to 64 px. Motion is 100/160 ms with
cubic-bezier(0.2,0,0,1). - There is no warning or amber token. Low stock currently reuses red.
4. Differences between CTXD and the code
These are recorded so the design follows the code and not the docs.
| CTXD says | dev has |
|---|---|
pages/Dashboard.tsx |
No such file. / is Floor |
| Printer status pages are out of scope | Printers view with camera, AMS containers and CRUD |
| No authentication | OIDC, sessions, API tokens, reader/operator/admin roles |
| Bambu only | Bambu MQTT + Elegoo Centauri SDCP |
weight_source is typed/recalled/unknown |
Also sliced (read from the 3MF over FTPS). The code says recalled is "no longer written" |
| Tare source: manufacturer/community/estimated | Two separate things: spool.tare_source/tare_origin (where this spool's tare came from: spool, spool type, brand, unknown, none) and spool_type.source (how trustworthy the published figure is: manufacturer, community, estimated) |
| Design system at pages.cubecraftlabs.com | 404. The live guide is at pages.ccl |
5. Assumptions I'm working from
None of these blocks the design. Each is a default I've picked, and each is repeated in SUMMARY.md so you can overturn it.
- The Dashboard shows
weight_source, even thoughGET /api/printsdoes not return it today. It is stored in theprintcolumn, and the cost payload has it. The mockup marks it as needing an API field. - Weight source has four values (typed, sliced, recalled, unknown). That follows the code, not DECISIONS.MD's three.
- Tare is shown as two facts: where this spool's tare came from (origin) and, when it came from a spool type, that type's trust level (manufacturer, community, estimated).
- The v3 light theme is new. v2.0 has none, so every light token counts as "added".
- Neumorphism overturns two stated v2.0 brand rules: 0 radius and flat shadows. This is recorded as a deliberate departure, not as editing a token to suit a component.