From 5fb713042126386ab102951f15a81f10528520d9 Mon Sep 17 00:00:00 2001 From: geb Date: Thu, 3 Sep 2026 19:16:13 +0800 Subject: [PATCH 1/2] web-redesign: exercise-book theme (ruled sheet, pen states, jobs list) - restyle GET / per docs/WEBDESIGN_PLAN.md rev2: warm sheet, light-blue ruled lines, red margin line; entries written on the rules, state marks in the margin column (the old print-shop card look is replaced) - self-hosted assets inlined at import: Patrick Hand woff2 as a base64 data-URI, Phosphor SVGs (app/static/icons) as one currentColor symbol sprite; page stays a single self-contained HTML string, no static mount - job status is now icon + exact API state word + pen color (graphite queued, ink-blue converting/scanning, cyan printing, green done, red failed, struck-through cancelled); emoji status strings removed - signature element: pencil writing a wavy ink line while a job works (prefers-reduced-motion shows a static drawn line) - new Jobs section: GET /jobs rendered with per-format glyphs, per-state margin marks and cancel (DELETE) buttons for active jobs; refreshed on load, on job events and every 5 s - new pins in tests/api/test_health_web.py: jobs list present, no emoji anywhere, assets inlined/self-hosted; all previously pinned hooks kept verbatim (345 tests pass, coverage 95.5%, ruff clean) - PRODUCT.md (product truth), DESIGN.md + .impeccable/design.json (the recorded visual system); .gitignore now covers downloads/ like uploads/ --- .gitignore | 4 + .impeccable/design.json | 123 ++++ DESIGN.md | 216 ++++++ PRODUCT.md | 102 +++ app/api/web.py | 781 +++++++++++++++++----- app/static/fonts/patrick-hand-latin.woff2 | Bin 0 -> 23944 bytes app/static/icons/arrow-clockwise.svg | 1 + app/static/icons/clock.svg | 1 + app/static/icons/copy.svg | 1 + app/static/icons/crosshair.svg | 1 + app/static/icons/download-simple.svg | 1 + app/static/icons/drop.svg | 1 + app/static/icons/file-csv.svg | 1 + app/static/icons/file-doc.svg | 1 + app/static/icons/file-image.svg | 1 + app/static/icons/file-pdf.svg | 1 + app/static/icons/file-ppt.svg | 1 + app/static/icons/file-txt.svg | 1 + app/static/icons/file-xls.svg | 1 + app/static/icons/lock-key.svg | 1 + app/static/icons/pencil.svg | 1 + app/static/icons/printer.svg | 1 + app/static/icons/prohibit.svg | 1 + app/static/icons/rows.svg | 1 + app/static/icons/scan.svg | 1 + app/static/icons/sliders-horizontal.svg | 1 + app/static/icons/trash.svg | 1 + app/static/icons/upload-simple.svg | 1 + app/static/icons/wifi-high.svg | 1 + app/static/icons/wifi-slash.svg | 1 + app/static/icons/x-circle.svg | 1 + docs/WEBDESIGN_PLAN.md | 486 ++++++++++++++ downloads/.gitkeep | 0 tests/api/test_health_web.py | 26 + 34 files changed, 1614 insertions(+), 149 deletions(-) create mode 100644 .impeccable/design.json create mode 100644 DESIGN.md create mode 100644 PRODUCT.md create mode 100644 app/static/fonts/patrick-hand-latin.woff2 create mode 100644 app/static/icons/arrow-clockwise.svg create mode 100644 app/static/icons/clock.svg create mode 100644 app/static/icons/copy.svg create mode 100644 app/static/icons/crosshair.svg create mode 100644 app/static/icons/download-simple.svg create mode 100644 app/static/icons/drop.svg create mode 100644 app/static/icons/file-csv.svg create mode 100644 app/static/icons/file-doc.svg create mode 100644 app/static/icons/file-image.svg create mode 100644 app/static/icons/file-pdf.svg create mode 100644 app/static/icons/file-ppt.svg create mode 100644 app/static/icons/file-txt.svg create mode 100644 app/static/icons/file-xls.svg create mode 100644 app/static/icons/lock-key.svg create mode 100644 app/static/icons/pencil.svg create mode 100644 app/static/icons/printer.svg create mode 100644 app/static/icons/prohibit.svg create mode 100644 app/static/icons/rows.svg create mode 100644 app/static/icons/scan.svg create mode 100644 app/static/icons/sliders-horizontal.svg create mode 100644 app/static/icons/trash.svg create mode 100644 app/static/icons/upload-simple.svg create mode 100644 app/static/icons/wifi-high.svg create mode 100644 app/static/icons/wifi-slash.svg create mode 100644 app/static/icons/x-circle.svg create mode 100644 docs/WEBDESIGN_PLAN.md create mode 100644 downloads/.gitkeep diff --git a/.gitignore b/.gitignore index 55223fb..4672007 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ htmlcov/ # Z-Code .zcode/ + +# Scan results (temp scan files) +downloads/* +!downloads/.gitkeep diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..3936ca4 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,123 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-09-03T19:00:00Z", + "title": "Design System: printerService", + "extensions": { + "colorMeta": { + "paper": { + "role": "neutral", + "displayName": "The Sheet", + "canonical": "#FBF7EE", + "tonalRamp": ["oklch(0.15 0.02 90)", "oklch(0.27 0.02 90)", "oklch(0.39 0.02 90)", "oklch(0.51 0.02 90)", "oklch(0.63 0.02 90)", "oklch(0.75 0.02 90)", "oklch(0.85 0.02 90)", "oklch(0.95 0.02 90)"] + }, + "paper-raised": { + "role": "neutral", + "displayName": "Raised Field", + "canonical": "#FFFFFF", + "tonalRamp": ["oklch(0.15 0 0)", "oklch(0.27 0 0)", "oklch(0.39 0 0)", "oklch(0.51 0 0)", "oklch(0.63 0 0)", "oklch(0.75 0 0)", "oklch(0.85 0 0)", "oklch(0.95 0 0)"] + }, + "rule": { + "role": "tertiary", + "displayName": "Ruled Line", + "canonical": "#B7D3EE", + "tonalRamp": ["oklch(0.15 0.06 250)", "oklch(0.27 0.06 250)", "oklch(0.39 0.06 250)", "oklch(0.51 0.06 250)", "oklch(0.63 0.06 250)", "oklch(0.75 0.06 250)", "oklch(0.85 0.06 250)", "oklch(0.95 0.06 250)"] + }, + "rule-strong": { + "role": "tertiary", + "displayName": "Heavy Rule", + "canonical": "#8FB8E4", + "tonalRamp": ["oklch(0.15 0.09 250)", "oklch(0.27 0.09 250)", "oklch(0.39 0.09 250)", "oklch(0.51 0.09 250)", "oklch(0.63 0.09 250)", "oklch(0.75 0.09 250)", "oklch(0.85 0.09 250)", "oklch(0.95 0.09 250)"] + }, + "margin-red": { + "role": "tertiary", + "displayName": "Margin Line (decorative, never errors)", + "canonical": "#F0776D", + "tonalRamp": ["oklch(0.15 0.15 25)", "oklch(0.27 0.15 25)", "oklch(0.39 0.15 25)", "oklch(0.51 0.15 25)", "oklch(0.63 0.15 25)", "oklch(0.75 0.15 25)", "oklch(0.85 0.15 25)", "oklch(0.95 0.15 25)"] + }, + "ink-blue": { + "role": "primary", + "displayName": "Ballpoint Ink", + "canonical": "#24418E", + "tonalRamp": ["oklch(0.15 0.13 265)", "oklch(0.27 0.13 265)", "oklch(0.39 0.13 265)", "oklch(0.51 0.13 265)", "oklch(0.63 0.13 265)", "oklch(0.75 0.13 265)", "oklch(0.85 0.13 265)", "oklch(0.95 0.13 265)"] + }, + "graphite": { + "role": "secondary", + "displayName": "Pencil Graphite", + "canonical": "#566072", + "tonalRamp": ["oklch(0.15 0.05 250)", "oklch(0.27 0.05 250)", "oklch(0.39 0.05 250)", "oklch(0.51 0.05 250)", "oklch(0.63 0.05 250)", "oklch(0.75 0.05 250)", "oklch(0.85 0.05 250)", "oklch(0.95 0.05 250)"] + }, + "cyan-wet": { + "role": "primary", + "displayName": "Fresh Ink (printing)", + "canonical": "#0072A3", + "tonalRamp": ["oklch(0.15 0.13 230)", "oklch(0.27 0.13 230)", "oklch(0.39 0.13 230)", "oklch(0.51 0.13 230)", "oklch(0.63 0.13 230)", "oklch(0.75 0.13 230)", "oklch(0.85 0.13 230)", "oklch(0.95 0.13 230)"] + }, + "green-pen": { + "role": "primary", + "displayName": "Teacher's Tick (done only)", + "canonical": "#1E7A4E", + "tonalRamp": ["oklch(0.15 0.12 155)", "oklch(0.27 0.12 155)", "oklch(0.39 0.12 155)", "oklch(0.51 0.12 155)", "oklch(0.63 0.12 155)", "oklch(0.75 0.12 155)", "oklch(0.85 0.12 155)", "oklch(0.95 0.12 155)"] + }, + "red-pen": { + "role": "primary", + "displayName": "Teacher's Red Pen (failed)", + "canonical": "#C0392B", + "tonalRamp": ["oklch(0.15 0.17 25)", "oklch(0.27 0.17 25)", "oklch(0.39 0.17 25)", "oklch(0.51 0.17 25)", "oklch(0.63 0.17 25)", "oklch(0.75 0.17 25)", "oklch(0.85 0.17 25)", "oklch(0.95 0.17 25)"] + } + }, + "typographyMeta": { + "display": { "displayName": "Handwriting Title", "purpose": "The page title only — written like a notebook header." }, + "heading": { "displayName": "Handwriting Heading", "purpose": "Section heads and status words; sits on a heavy rule." }, + "body": { "displayName": "Body", "purpose": "Helper copy and error detail — anything that needs re-reading." }, + "mono": { "displayName": "Machine Values", "purpose": "Job IDs, timestamps, DPI, error codes — nothing else." } + }, + "shadows": [], + "breakpoints": { "mobile": "0px", "desktop": "768px" }, + "motion": { + "pencil-write": "stroke-dashoffset 140 loop, 1.6s ease-in-out infinite — the only looping motion; prefers-reduced-motion shows a static drawn line", + "state-crossfade": "150-200ms opacity crossfade on status changes" + }, + "componentSnippets": { + "button-primary": { + "html": "", + "css": ".btn{display:inline-flex;align-items:center;gap:10px;min-height:48px;padding:8px 22px;font-family:'Patrick Hand',cursive;font-size:19px;color:#24418E;background:transparent;border:1.5px solid #24418E;border-radius:12px 14px 12px 14px/14px 12px 14px 12px}.btn:disabled{opacity:.4}" + }, + "job-entry": { + "html": "
  • report.pdfa1b2c3done
  • ", + "css": ".job{display:grid;grid-template-columns:52px minmax(0,1fr);align-items:center;min-height:46px}.jstate{font-family:'Patrick Hand',cursive;font-size:16.5px}.st-done{color:#1E7A4E}.st-failed{color:#C0392B}.st-queued{color:#566072}.jid{font-family:ui-monospace,monospace;font-size:12.5px;color:#566072}" + } + }, + "narrative": { + "northStar": "The page IS the paper.", + "overview": "The service moves pixels to paper and back; the UI is an elementary-school exercise book. The interface is written on the sheet: headings sit on the lines, job entries are lines of handwriting, and the machine marks the margin like a teacher — green tick for done, red-pen note for failed, strike-through for cancelled.", + "keyCharacteristics": [ + "The ruled background is the subject, not decoration (The World-Earned Stripe Rule).", + "Job state is always three signals: icon + the API's exact word + pen color (The Three-Signal Rule).", + "The margin column holds only marks; content never enters it.", + "One motion element: the pencil writing a wavy ink line while a job works." + ], + "rules": [ + { "name": "The World-Earned Stripe Rule", "body": "The ruled background is world-earned — this page literally is ruled paper — but it stays faint, 1px, and never competes with text.", "section": "Colors" }, + { "name": "The Margin-Red-Is-Not-Error Rule", "body": "Decorative margin-red is visibly lighter than red-pen; errors are always red-pen.", "section": "Colors" }, + { "name": "The Short-Handwriting Rule", "body": "Handwriting carries labels and single words only; anything needing re-reading is set in the body face.", "section": "Typography" }, + { "name": "The Empty-Margin Rule", "body": "The margin column holds state marks only — never content, never controls.", "section": "Layout" }, + { "name": "The Three-Signal Rule", "body": "Status is always icon + exact API word + pen color; color never speaks alone.", "section": "Overview" } + ], + "dos": [ + "Keep the sheet flat: two CSS gradients draw everything; no texture images.", + "Keep status = icon + exact word + pen color, everywhere.", + "Keep every asset inlined/self-hosted — zero runtime CDN calls.", + "Keep the margin for marks only." + ], + "donts": [ + "No emoji anywhere (test-enforced).", + "No cards, shadows, or boxes around job entries.", + "No handwriting on long/error/numeric copy; no mono on ordinary labels.", + "No graph grid, sticky-note panels, torn edges, or a second paper texture.", + "No margin-red in error semantics; no decorative-trio colors on text." + ] + } + } +} + + diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..0f32dc3 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,216 @@ +--- +name: printerService +description: The phone-facing control panel for a USB-only printer, styled as an elementary-school exercise book — the interface is written on ruled paper. +colors: + paper: "#FBF7EE" + paper-raised: "#FFFFFF" + rule: "#B7D3EE" + rule-strong: "#8FB8E4" + margin-red: "#F0776D" + ink-blue: "#24418E" + graphite: "#566072" + cyan-wet: "#0072A3" + green-pen: "#1E7A4E" + red-pen: "#C0392B" +typography: + display: + fontFamily: "\"Patrick Hand\", \"Segoe Print\", \"Comic Sans MS\", cursive" + fontSize: "24px" + fontWeight: 400 + lineHeight: "32px" + letterSpacing: "normal" + heading: + fontFamily: "\"Patrick Hand\", \"Segoe Print\", \"Comic Sans MS\", cursive" + fontSize: "19px" + fontWeight: 400 + lineHeight: "32px" + letterSpacing: "normal" + body: + fontFamily: "system-ui, sans-serif" + fontSize: "14px" + fontWeight: 400 + lineHeight: 1.5 + letterSpacing: "normal" + mono: + fontFamily: "ui-monospace, Consolas, monospace" + fontSize: "12.5px" + fontWeight: 400 + lineHeight: 1.4 + letterSpacing: "normal" +rounded: + box: "12px 14px 12px 14px / 14px 12px 14px 12px" + field: "8px" + pick: "10px 12px 10px 12px / 12px 10px 12px 10px" +spacing: + xs: "8px" + sm: "12px" + md: "16px" + section: "28px" + margin-column: "52px" +components: + button-primary: + backgroundColor: "transparent" + textColor: "{colors.ink-blue}" + rounded: "{rounded.box}" + padding: "8px 22px" + height: "48px" + button-scan: + backgroundColor: "transparent" + textColor: "{colors.cyan-wet}" + rounded: "{rounded.box}" + padding: "8px 22px" + height: "48px" + file-picker: + backgroundColor: "{colors.paper-raised}" + textColor: "{colors.graphite}" + rounded: "{rounded.pick}" + padding: "8px 12px" + height: "44px" + text-input: + backgroundColor: "{colors.paper-raised}" + textColor: "{colors.ink-blue}" + rounded: "{rounded.field}" + padding: "8px 10px" + height: "44px" +--- + +# printerService — Design System + +## Overview + +**Creative North Star: "The page IS the paper."** + +The service moves pixels to paper and back; the UI is an elementary-school +exercise book. A warm sheet carries light-blue ruled lines every 32px and +a 2px red margin line at x=52px (72px on desktop, rule pitch 36px). The +interface is *written on* the sheet: headings sit on the lines, job +entries are lines of "handwriting," and the machine marks the margin like +a teacher — green tick for done, red-pen note for failed, strike-through +for cancelled. + +**Key Characteristics:** +- The ruled background is the subject, not decoration (The World-Earned + Stripe Rule). +- Job state is always three signals: icon + the API's exact word + pen + color. Color never speaks alone (The Three-Signal Rule). +- The margin column holds only marks; content never enters it. +- One motion element in the whole product: the pencil writing a wavy ink + line while a job works. + +## Colors + +Grouped by role: + +- **The sheet (surfaces):** `paper` is the page; `paper-raised` (#FFFFFF) + is reserved for controls that need clean edges — inputs, selects, the + file picker. Nothing content-shaped gets a white card. +- **The decorative trio (never text, never status):** `rule` (#B7D3EE) + draws the ruled lines; `rule-strong` (#8FB8E4) underlines section + headings and borders fields; `margin-red` (#F0776D) draws the vertical + margin line only. +- **The ink (static text):** `ink-blue` (#24418E, ballpoint) for primary + text, headings, buttons; `graphite` (#566072, pencil) for secondary + text, helper copy, and all monospace machine values. +- **The pens (job states):** `graphite` = queued/received, + `ink-blue` = converting/scanning, `cyan-wet` (#0072A3) = printing, + `green-pen` (#1E7A4E) = done (the only success color), + `red-pen` (#C0392B) = failed, `graphite` + strike-through = cancelled. + +**The Margin-Red-Is-Not-Error Rule.** The decorative `margin-red` is +visibly lighter than `red-pen`; errors are always `red-pen`. If a mockup +can't tell them apart at a glance, the shades are wrong. No gradient or +wash may reuse these hues. + +All six ink/pen colors pass 4.5:1 on both surfaces and are the only +text colors. The decorative trio never carries text. + +## Typography + +Three voices, one job each: + +- **Handwriting** — self-hosted Patrick Hand (OFL, inlined as a base64 + woff2; fallback `"Segoe Print", "Comic Sans MS", cursive`). Page title + (24px), section headings (19px, sitting on a 2px `rule-strong` + underline), button labels (19px), status words (16.5px), empty-state + notes. Never for error detail, helper copy, URLs, or copyable numbers. +- **Body** — system-ui 14px/1.5 in `graphite` for helper and error text. +- **Mono** — ui-monospace 12.5px in `graphite`, only for + machine-generated values (job IDs, timestamps, DPI, error codes). + +**The Short-Handwriting Rule.** If a string needs re-reading, it is set +in the body face. Handwriting carries labels and single words only. + +## Layout + +Mobile-first; the 390px viewport is the primary surface. + +- Header zone: plain paper (no rules behind it), title left, wifi health + icon right (24px, `cyan-wet` when reachable, `red-pen` when not). +- The sheet starts below the header: red margin line at 52px, ruled lines + every 32px, content begins at 64px. +- Jobs: one entry per ruled line (two when error text wraps) — a 52px + margin column for the state mark, then icon + filename + mono id + + status word written inline. Entries are never boxed or carded; the + ruled page is the container. +- Desktop (≥768px): same sheet widened to 8vw side margins, margin column + 72px, rule pitch 36px, print/scan actions max-width 560px, jobs flow in + two columns. + +**The Empty-Margin Rule.** The margin column holds state marks only — +never content, never controls. + +## Elevation & Depth + +None. The world is flat ink on paper; there are no shadows and no +elevation tokens. Controls distinguish themselves with borders and the +`paper-raised` surface, not depth. + +## Shapes + +- Action buttons: 1.5px ink border with the barely-wobbled "drawn once by + hand" radius `12px 14px 12px 14px / 14px 12px 14px 12px`, transparent + fill. +- Fields/picker: 8px radius (picker: slightly wobbled variant), 1px + `rule-strong` border on `paper-raised`. +- Icons: Phosphor (MIT), inline `` sprite, `currentColor`, sizes + 16/20/24px only, weights regular / fill (done tick) — never duotone. + Icons draw in ink or the state's pen color, never in the decorative + trio. + +## Components + +- **Action button** (Print / Scan / Retry): hand-drawn box, handwriting + label + trailing 20px icon, 48px min height. Scan's box draws in + `cyan-wet`; disabled = 0.4 opacity; pressed = 7% ink tint fill. +- **File picker**: dashed `rule-strong` border on `paper-raised`, upload + icon + truncated filename, 44px min height. +- **Job entry**: margin mark (state icon in pen color) + written line + (format glyph, filename, mono id, handwritten status word). Failed + entries add graphite error text on a second line. Active entries add a + 44px trash button (DELETE). Cancelled entries strike through name and + status word. +- **The pencil progress**: inline SVG — pencil + wavy ink line drawn via + a `stroke-dashoffset` loop (1.6s ease-in-out; `cyan-wet` for printing, + `ink-blue` for converting/scanning). The product's only looping motion; + `prefers-reduced-motion` swaps to a static fully-drawn line. Status + lines lead with the API's exact state word (`queued`, `printing`, + `done`, `failed`, `cancelled`) — never a synonym, never an emoji. + +## Do's and Don'ts + +**Do** +- Keep the sheet flat: two CSS gradients draw everything (red margin line + + ruled lines); no texture images. +- Keep status = icon + exact word + pen color, everywhere. +- Keep every asset inlined/self-hosted — zero runtime CDN calls. +- Keep the margin for marks only. + +**Don't** +- No emoji anywhere (test-enforced). +- No cards, shadows, or boxes around job entries. +- No handwriting on long/error/numeric copy; no mono on ordinary labels. +- No graph grid, sticky-note panels, torn edges, or a second paper + texture. +- No `margin-red` in error semantics; no decorative-trio colors on text. + + diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..3588e46 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,102 @@ +# Product + + + +## Platform + +web + +## Users + +One person — the owner — on their Android phone, at home, on the same LAN as +the print server. Their job: "send this file to the printer" and "get that +scan onto my phone," then a quick glance to confirm it finished. Household +members on the same Wi-Fi are occasional secondary users (trusted, shared +PIN). Nobody uses this from outside the home network, ever. + +## Product Purpose + +Turn a USB-only Epson L3210 all-in-one into a network print/scan station: +the phone's browser sends files to a Python service on an old Windows PC, +which hands them to the Windows print spooler and out over USB; the flatbed +scanner sends pages back the other way as downloads. Success is: open a URL +on the phone, pick a file, tap Print, paper comes out — without ever +touching the PC. + +## Positioning + +A browser page served by the print server itself — no app to install, no +cloud, no account. A real network printer's firmware plays the role this +Python service plays; on a USB-only printer, the service IS the missing +network half. That "your old PC pretends to be the smart part of the +printer" mechanism is the product. + +## Operating Context + +- LAN-only home Wi-Fi. The router's internet may be down while the LAN is + fine — the page must fully work with zero internet reachability. +- The service runs headless on an old/low-spec Windows PC (Task Scheduler, + before logon). The page is served by FastAPI on port 8000. +- The printer may or may not have a working scanner; the page must degrade + quietly based on live hardware detection, never by error. +- A shared PIN gates state-changing routes; read-only GETs stay open. + +## Capabilities and Constraints + +- Print: PDF, images, office documents, TXT/CSV — every format normalized + to PDF internally. Optional per-job options (copies, pages, paper, + color_mode). Scan: flatbed, one page per job, optional dpi/color_mode/ + format. Retry and cancel for print jobs. +- Job states use the API's exact vocabulary: + `received → queued → converting/scanning → printing → done | failed | + cancelled`. The UI never invents synonyms. +- Technical constraints (binding): vanilla HTML/CSS/JS, no build step, no + framework; the page is a single self-contained HTML string in + `app/api/web.py`; no runtime CDN/external calls — all assets self-hosted; + mobile-first (~390px primary surface); printing is the first thing on the + page (the Scan section renders only when a scanner is detected). +- Automated tests (`tests/api/test_health_web.py`) pin structural hooks: + `>Print<`, `id="printBtn"`, `id="scanSection" style="display:none`, + `id="scanBtn"`, `onclick="startScan()"`, `pollScan`, + `getElementById("scanDpi"/"scanColorMode"/"scanFormat")`, and the + `/favicon.svg` link. A restyle must keep these hooks or deliberately + update the tests in the same change. + +## Brand Commitments + +- Name: **printerService** (lowercase, one word). +- The existing favicon SVG mark (printer + Wi-Fi waves, cyan/magenta/ + yellow accents) is shipped, referenced by tests, and stays. +- UI copy bans emoji entirely (established design rule — the current page's + 📨/❌/⏳/✅ status markers are legacy debt to remove at the next restyle). +- Status text always uses the API's exact state words. + +## Evidence on Hand + +- `docs/SOURCE_OF_TRUTH.md` — central decision record (print pipeline, + architecture, testing). +- `docs/MULTI_FORMAT_PLAN.md`, `docs/SCAN_PLAN.md` — format and scan + decision records incl. real-hardware spike timings. +- `docs/WEBDESIGN_PLAN.md` — the design brief (restyled 2026-09-03 to the + elementary-notebook world; v1's print-shop world is retired). +- Live page: `app/api/web.py`; suite: 342 tests, incl. web-page + structural tests. No marketing assets, testimonials, or press exist — + none may be fabricated. + +## Product Principles + +1. The machine's vocabulary is the UI's vocabulary. +2. Degrade by hardware truth, never by error — absent capability means + quiet absence, not a message. +3. Works with the router's internet down: every asset self-hosted. +4. One phone, one glance: "did it finish?" answered within seconds. +5. Printing first, always. + +## Accessibility & Inclusion + +- Body text contrast ≥ 4.5:1 against paper surfaces; accent colors get + darkened text-safe variants. +- Status is never color-only: icon + exact status word always accompany + color. +- Minimum 44×44px tap targets; visible non-default keyboard focus ring; + `prefers-reduced-motion` respected everywhere motion is used. diff --git a/app/api/web.py b/app/api/web.py index b535087..3721448 100644 --- a/app/api/web.py +++ b/app/api/web.py @@ -1,29 +1,118 @@ -""" -GET / — the mobile web page (Phase 6, SOURCE_OF_TRUTH Section 6 "Option B"). - -The page is a single self-contained HTML string (inline CSS + vanilla JS — -no build tools, no frameworks) served directly by FastAPI. The phone's -browser IS the app: open http://:8000, pick a PDF or image, tap -Print. The accept list mirrors the categories registered in -app/processors — new formats update both. +"""GET / — the mobile web page (SOURCE_OF_TRUTH §6 "Option B"; redesign per +docs/WEBDESIGN_PLAN.md **revision 2**: the elementary-school exercise-book +theme — a warm sheet with light-blue ruled lines and a red margin line that +the interface is written ON; job states are ink colors; the one motion +element is a pencil writing a wavy line while a job works). + +The page is still a single self-contained HTML string (inline CSS + vanilla +JS — no build tools, no frameworks) served directly by FastAPI, per the +codebase's "prefer simple technologies" rule. That includes its two assets: + +- **Font** — Patrick Hand (OFL), committed at + app/static/fonts/patrick-hand-latin.woff2 and inlined below as a base64 + data-URI at import time, so the page needs no static-file mount and works + even when the router's internet (or the static dir) is gone. +- **Icons** — Phosphor (MIT), committed as flat SVGs under + app/static/icons/ and assembled into one inline sprite at import + time, so icons take their color from currentColor and job states can + recolor them. + +Both asset steps degrade silently: missing files fall back to system fonts +/ an empty sprite without breaking the page. How the upload works (worth reading slowly — this is HTTP from the browser's point of view): - 1. lets the user pick a file; JS gets a File object. + 1. the styled picker is a