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

194 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.