Files
design-v3/00-understanding.md
Joshua KingandClaude Opus 5 d9ed1898b9 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>
2026-09-19 11:31:50 -04:00

13 KiB
Raw Permalink Blame History

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 dev code, 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 / is Floor.tsx.
  • The Printers screen uses the fields stored on dev plus 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.tsx embedded; 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 +gap marker when connection_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.

  1. Identity: Brand* · Material* · Type (optional) · Finish* · Speed (optional) · Spool type (optional, "sets the empty-spool weight") · Location (optional, "Shelf A")

  2. 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)

  3. 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.

  4. 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 error line, 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/md are all 0px ("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.

  1. The Dashboard shows weight_source, even though GET /api/prints does not return it today. It is stored in the print column, and the cost payload has it. The mockup marks it as needing an API field.
  2. Weight source has four values (typed, sliced, recalled, unknown). That follows the code, not DECISIONS.MD's three.
  3. 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).
  4. The v3 light theme is new. v2.0 has none, so every light token counts as "added".
  5. 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.