Skip to content

Pieces API + records/dashboard/bins overhaul#186

Draft
spencerhhubert wants to merge 10 commits into
spencer/hive-sync-from-sorthive-01from
spencer/piece-images-01
Draft

Pieces API + records/dashboard/bins overhaul#186
spencerhhubert wants to merge 10 commits into
spencer/hive-sync-from-sorthive-01from
spencer/piece-images-01

Conversation

@spencerhhubert

Copy link
Copy Markdown
Contributor

Draft — stacked on spencer/hive-sync-from-sorthive-01 (its commits are excluded from this diff).

Overview

A clean, unified Pieces API that the records page, the recent-pieces dropdown, exports, and the websocket all draw from — replacing a read surface that had grown into four divergent paths (piece_records, the in-memory known-object LRU, piece_image_store, and a 10-item recent-objects blob). One piece shape, tiered payloads, lazy subresources. Plus the records/dashboard/bins UI work that rides on it. Net effect is smaller: old endpoints are deleted, not left in parallel.

Backend — unified read API

  • server/routers/pieces.py (new):
    • GET /api/pieces — keyset cursor pagination (on the autoincrement id, since seen_at is nullable), filters (status, part_id, color_id, run_id, dead, date range), recent/oldest sort. Returns light PieceSummary rows (uuid, timestamps, classification, bin, price, has_images, preview_url) — no images, no metadata blob.
    • GET /api/pieces/{uuid} — tiered {origin, summary, detail, detail_available}: memory-first (full known-object detail) with disk fallback (durable summary), the same pattern the image store already uses. Memory hits do zero SQLite work — this endpoint is polled per active piece during sorting.
    • GET /api/pieces/{uuid}/images — unchanged, remains the lazy image subresource.
    • GET /api/pieces/export.csv — streamed CSV of the whole filtered set (keyset-chunked), not capped to a page.
    • GET /api/pieces/aggregates — cached chart series (pieces/day, status breakdown, unique parts over time, PPM/day, per-color, top parts, value/day).
    • overview / value / lifetime (+ lifetime/export.csv) relocated under /api/pieces.
  • Deleted /api/records/* and /api/known-objects/{uuid}, plus the recent_known_objects blob and its websocket-connect replay. Clients refill from GET /api/pieces instead.
  • piece_records gains a nullable brickognize_preview_url column (guarded ALTER) so summary rows carry a thumbnail without an image fetch.

Backend — performance & correctness

  • Metric snapshots moved to their own local_metrics.sqlite (buffered flush, bounded retention). These per-second, write-only diagnostic rows had grown into millions and bloated the shared DB, slowing every query against it; the legacy tables drain and drop in the background at idle priority.
  • Value stats cached — previously recomputed uncached on every request with a per-part price lookup (~1.2s); now a process-wide price cache + short-TTL memo brings warm requests to tens of ms.
  • Lifetime best-hour PPM de-correlated from a per-bucket subquery to a single grouped query.
  • Thread-safe price DB access — the shared read connection to the parts metadata DB was not safe for concurrent execute(); parallel lookups could error and cache bad results. Serialized with a lock; the price cache is also pre-warmed off the request path at startup.

Backend — classification status (Area 8)

  • New failed ClassificationStatus for transport-error identifications (persisted, distributable so a failed piece discards rather than jamming the channel). Previously "failed to identify" was indistinguishable from a genuine classified outcome in some UI paths.

Frontend

  • One client piece store (lib/pieces/) — REST summaries and live known_object websocket events reduce into a single per-machine Piece shape with one upsert reducer. The recent-pieces dropdown, the records list's live rows, and the tracked detail page all read it; the dropdown refills from GET /api/pieces instead of the removed websocket replay.
  • Records page consumes the new API and is componentized (~370 lines, down from ~1040): merged History + Lifetime into one stat header, cursor pagination, live rows on page one, per-day table paginated by two-week blocks, backend CSV export buttons, and lazy hand-rolled SVG charts (no new dependency). Top cards load first; images and charts hydrate after.
  • Tracked detail page gains the disk fallback (no longer 404s when the piece has aged out of memory).
  • PieceStatusBadge — one shared badge replacing hand-rolled chips that had diverged across pages. The green "Classified" is now gated strictly on classification_status === 'classified'; a failed/unidentified piece can no longer read as classified anywhere (records, dropdown, tracked, bin modal). Fixes a dropdown lifecycle chip that showed green for any piece whose classification simply finished, success or not.
  • Dashboard — removed the Bins card and the Logs tab; added per-view rotate 180° / nudge ±1° C-channel buttons in the three camera title bars (operator-clicked UI, wired to the existing move-degrees control with the correct gear ratio).
  • Settings — the incident-handling block moved off the dashboard into its own component under the Models settings page.
  • Bins page — grid cells now preview server-grouped items with a ×N quantity badge instead of duplicate thumbnails (matching the modal); per-section grouping within each layer, a client-side find-a-part search that highlights matching bins, and at-a-glance count/fill/category/recency on each card.

Notes

  • Draft: intended for review while stacked on its parent branch; not yet exercised end-to-end against live hardware.
  • events.ts carries a one-line failed union addition; a full codegen regen is deferred to its own change (the generator rewrites unrelated types).
  • The metrics-DB split stops future growth but doesn't reclaim already-allocated pages; a manual VACUUM during downtime would shrink the file.

🤖 Generated with Claude Code

spencerhhubert and others added 10 commits July 2, 2026 16:36
Wiping bin contents now flushes each bin's load (aggregates + category
assignment at wipe time) into an auto-managed snapshot: single-bin/layer
wipes accumulate layers in the open snapshot, empty-all closes it. Pieces
stay in piece_events keyed by (session, bin, epoch) - nothing is copied.

New endpoints: list/detail snapshots, CSV export per snapshot and for
current bin contents (bl_part_id, bl_color_id, category_id_in_profile,
profile from the sorting session, created/classified/distributed unix
timestamps). piece_events grows created_at/classified_at columns.

/api/bins/contents drops inline base64 images by default (8.1MB -> 480KB
per poll; a backgrounded tab was saturating the GBL uplink), the bins page
stops polling while hidden, and piece_events gets a bin-lookup index
(contents build 5.5s -> 1.1s). Snapshots modal + export buttons on /bins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…files)

Every recognition crop (C4 burst + upstream C2/C3) is persisted as a plain
JPEG under software/sorter/backend/piece_images/<uuid>/ and indexed in
local_state.sqlite, so piece images survive restarts and LRU eviction.
Broadcaster enqueues (bounded, drop-on-full); a single daemon worker does
decode + file/DB writes and a 500MB retention sweep (oldest first, synced-
to-hive files preferentially). Rows outlive evicted files and carry
synced_at/hive_image_id for the upcoming hive uploader.

New endpoints: GET /api/pieces/{uuid}/images (index),
GET /api/pieces/{uuid}/images/{id} (file), GET /api/piece-images/stats.
C4 crop encode quality 80 -> 90 since these now feed training data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…disk-fallback)

The /records page previously fetched crops only from the in-memory
known-objects lookup, so every restart blanked images for all history.
Now it falls back to the piece-image store: file URLs served with
Cache-Control: immutable (the browser is the cache — repeat visits render
without re-fetching), hydrated 6-at-a-time instead of 100 concurrent
fetches, with Skeleton placeholders while loading.

The store also persists used/excluded_from_result/score per image now —
flushed once per piece when the applied Brickognize result settles — so
the used/dropped badges survive reboots too. Reclassify stays memory-only
(needs the base64 payloads).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add /api/bins/contents/version, a cheap change-token endpoint
(session_id + row count + max updated_at + summed epoch/piece_count)
so the bins page can poll frequently for auto-update without pulling
the full contents payload (with images) every tick. Also pause
polling while the tab is hidden and force-refresh on visibility
regain.

Split the ~2000-line bins +page.svelte into components: BinCard,
LayerPanel, ColumnsPanel, SnapshotsModal, BinDetailsModal, shared
types/pieces helpers, a bricklinkParts rune-based store, and new
Skeleton/ToggleSwitch primitives (replacing 5x copy-pasted switch
markup). No behavior change — same endpoints, confirm dialogs, and
busy/disabled semantics as before.

Co-Authored-By: Claude <noreply@anthropic.com>
…ed status

- New server/routers/pieces.py: GET /api/pieces (keyset cursor, filters,
  PieceSummary rows w/ has_images, preview_url, est_value), tiered
  /api/pieces/{uuid} (memory-first zero-sqlite hot path, disk fallback),
  streamed /api/pieces/export.csv, /api/pieces/aggregates chart data, and
  overview/value/lifetime(+csv) relocated from /api/records/*.
- Deleted /api/records/* and /api/known-objects/{uuid}; one read API.
- Per-second metric snapshots move to their own local_metrics.sqlite
  (buffered 15s flush, 24h/2.5M-row retention); legacy 6.8M-row tables in
  local_state.sqlite drain+drop in the idle-priority pruner thread.
- getValueStats: price cache + 60s memo (was ~1.2s/request); lifetime
  best-hour PPM de-correlated to one grouped query.
- recent_known_objects blob + WS-connect replay removed (dropdown refills
  from GET /api/pieces).
- ClassificationStatus gains 'failed' for transport-error identifications
  (persisted, distributable so failed pieces discard instead of jamming C4);
  piece_records persists brickognize_preview_url.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dges

- New lib/pieces store: one client Piece shape that REST summaries and live
  known_object WS events reduce into; RecentObjects, the records list, and
  tracked detail all read it. Dropdown refills via GET /api/pieces?limit=32.
- Records page componentized (~370 lines from ~1040): merged lifetime stats
  header, cursor pagination, live rows on page 1, per-day table in two-week
  blocks + CSV export buttons, lazy hand-rolled SVG charts from
  /api/pieces/aggregates.
- Tracked page gains disk fallback (no more 404 after backend restart).
- New shared PieceStatusBadge: green Classified strictly gated on
  classification_status === 'classified' — failed/unidentified pieces no
  longer wear the green chip (RecentObjects lifecyclePhase bug), 'failed'
  renders as ID failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bins card, logs tab

- CameraFeed gains an optional headerActions snippet; the three split-feeder
  views get right-justified -1 deg / +1 deg / 180 deg CW buttons (motor
  degrees via 130/12 gear ratio, stored per-stepper speed, one in-flight
  action, Popover tooltips).
- Incident handling extracted to IncidentHandlingSection under the Models
  settings page; dashboard block removed.
- Dashboard bins card removed (SortingStatusCard deleted); Logs tab removed
  from the top bar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… search

- Grid cells preview server-grouped items with a xN QuantityBadge instead of
  duplicate recent-piece images; same badge in the modal (theme token, raw
  hex removed).
- Layers render per-section groups with enable/point controls inline; cards
  show types/total counts and last-updated recency; client-side search
  highlights bins by part id/name, color, category; modal items get
  PieceStatusBadge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-safe

Concurrent price lookups (value-stats cold fill racing aggregates and the
classification pipeline) hit "bad parameter or other API misuse" on the
shared check_same_thread=False connection and permanently cached those parts
as unpriced. Python-level execute() on one connection needs a lock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First value/aggregates computation walks every historical (part,color) pair
through parts.db — ~60s serialized on the Pi eMMC (part_bricklink_ids has no
index). Warm it in a background daemon thread at startup so no records-page
request ever pays it; steady-state requests stay ~50ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sorter-v2-docs Ready Ready Preview, Comment Jul 3, 2026 2:58am
sorteros-setup Ready Ready Preview, Comment Jul 3, 2026 2:58am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant