Skip to content

Prints: a 3D print library with folders and groups - #341

Merged
sushilm20 merged 7 commits into
mainfrom
feat/prints-library
Oct 29, 2026
Merged

sushilm20 merged 7 commits into
mainfrom
feat/prints-library

Conversation

@sushilm20

@sushilm20 sushilm20 commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

What

Prints: a 3D print library per workspace (Build → Prints, /prints), shaped like a file explorer with grouping at its heart.

  • Folders are where a file lives; groups are how the team thinks about it. Each file is stored once, in one folder, and also belongs to:
    • smart groups computed from its details: season and robot, subsystem (from the linked CAD part's section, or set by hand), material (PLA, PETG, TPU, …), and print status (To print, Printing, Printed, Failed or reprint);
    • collections the team makes ("Comp spares", "Worlds kit"). A file can be in any number of them; nothing is copied.
  • The sidebar shows folders and groups together. Picking a group lists its files with counts ("Intake · 12 files · 4 still to print"); picks in different groups combine ("Intake + PETG + To print"), shown as removable chips. The view lives in the URL, so a group can be linked and Back works.
  • From any group or folder: Download all (one ZIP), or select files and mark them printed or failed (or printing / to print), move them, add them to a collection, download or delete them.
  • Each file carries copies needed (the linked CAD part's quantity unless set by hand) and copies printed (a stepper; reaching the count marks it printed; marking printed fills the count).
  • Explorer basics: upload by button or drag and drop (STL, 3MF, STEP/STP, OBJ, G-code), new folder, nested folders with a breadcrumb path bar, list and grid views, sort by name / date / size, rename, move (drag onto a folder or the path bar, or the "Move to" picker), delete with confirmation, a folder delete that says how many files it holds, single download and folder ZIP, search across the library.
  • 3D preview for STL, 3MF and OBJ with three.js's own loaders, loaded with React.lazy / import() exactly like CadModelViewer. Grid tiles show a thumbnail the uploader's browser draws at upload (or the first time an editor opens the preview); STEP and G-code show an icon.
  • Details sheet: who added it and when, size, notes, robot and season, the CAD part it prints (from that robot's BOM), subsystem, material, status, copies, collections, and its history.
  • Permissions: new role permission manage_prints ("Upload and organize 3D prints"). Migration 155 grants it to every role that already has manage_cad; new teams' Member and Verified Member roles get it; admins have *. Members without it view and download only (the UI is read-only for them; the server answers 403). Every route binds auth.teamId; another workspace's ids are 404. Every change (upload, replace, rename, move, edit, status, collect/uncollect, delete, folder and collection create/rename/move/delete) is written to print_activity.
  • Keyboard and screen reader: every row is a real button; Delete deletes (after asking), F2 renames, Ctrl/Cmd+A selects all, Escape clears the selection; the 3D view turns with the arrow keys; the sidebar uses aria-pressed, the path bar aria-current, and a polite live region announces counts and results. "Move to" is the non-drag way to move.
  • Phone width: the sidebar becomes a "Folders and groups" sheet, rows collapse to name plus one detail line, 44 px targets, no horizontal scroll (checked at 390 px: scrollWidth - clientWidth = 0).
  • Prints is part of the CAD module switch (a team that turns CAD off also hides Prints) and is removed with the rest of a deleted workspace (rows, then bytes on disk).

Storage and limits

  • Where: the VM's disk, under PRINTS_DIR (default <cwd>/.data/prints, so /opt/control-point/.data/prints on the VM). .data/ is already git-ignored and outside every statically served directory (not uploads/, which express.static serves publicly). Bytes are stored at random keys <team>/<32 hex>; names people type never become disk paths, and anything that isn't a key is refused before it reaches path.join. The deploy workflow only touches files in its manifest, so it never touches this directory. R2 is not used for prints.
  • Limits (all environment-configurable, no .env change needed for the defaults):
    • PRINTS_MAX_FILE_MB = 100 MB per file (a large G-code; STL/3MF robot parts are usually far smaller).
    • PRINTS_QUOTA_MB = 2 GB per workspace, enforced inside the INSERT itself so two uploads at once can't both pass.
    • PRINTS_MIN_FREE_MB = 5 GB disk reserve: an upload that would leave less free space (checked with statfs before and after the body arrives) gets 507 with a plain message.
  • Free space was not checked on the VM: I was not able to reach the VM (SSH was not permitted from this session). The Oracle Always Free boot volume defaults to about 47 GB; the reserve check is what protects the disk whatever the real free space is. Please check df -h /opt/control-point and tune the three variables in /opt/control-point/.env if needed.
  • ZIPs stream from disk (deflate with data descriptors), never held in memory; one ZIP is capped at 3.5 GB (no ZIP64).

Control Kit wiring

#340 (Control Kit downloads and part pictures, contract v1.2) merged while this PR was open; this branch merges it and wires Prints into it. When Control Kit uploads a send's STEP zip through POST …/imports/:importId/files (stored as a CAD doc on the import's robot, unchanged), controlKitLink.ts now also hands the zip to prints.fileControlKitStepZip after answering (best effort, logged on failure; the dependency is the optional fileStepZip, passed in server.ts):

  • Every STEP under the zip's 3D_print/ folder is filed under " / / Control Kit", linked to the robot. CNC, 2D-cut and fabricate files are not prints and stay only in the zip.
  • It is named after its part from MANIFEST.txt ("Claw finger.step") and linked to the robot's CAD part whose identity the file name carries (Control Kit's stepFileName: <name>__<partId>[__cfg<hash>].step, matched by part id and configuration hash; no link when none or several fit). So subsystem and copies needed follow the BOM.
  • A re-send replaces Control Kit's file for the same part in place: status, notes and collections stay, a Printed file goes back to To print only when the bytes changed, identical bytes change nothing. A file a person uploaded under the same name is never overwritten.
  • The per-file limit, workspace quota and disk reserve apply; when there's no room, the remaining files are skipped.

docs/control-kit-link.md is not changed: the contract between the apps is the same; this is Control Point's own handling of a file it already receives. fileControlKitPrint and filePrint remain exported for any other caller.

Data

Migration 155-prints-library.sql (after #340's 154): print_folders, print_files, print_collections, print_collection_files, print_activity, plus the manage_prints grant. New tables only.

Tests

  • server/__tests__/prints.test.ts (real server): name and path validation and traversal (.., separators, reserved characters, a traversal name field, storage keys), per-file limit, quota (including after deletes), disk reserve (507), permissions (viewer vs manage_prints vs admin) and cross-workspace 404s, folder uniqueness (case-insensitive), moving a folder into itself or a descendant, rename and move collisions, "(2)" on moves and uploads, folder delete asking with the count (and a stale count asking again) and removing bytes, collections without copies, bulk status and copies, CAD-derived subsystem and copies, downloads and ZIPs (entries verified by CRC), the activity record, and fileControlKitPrint (path, links, replace, unchanged, a person's file kept, errors).
  • src/modern/__tests__/prints.test.tsx: folder view and path, combined groups with counts and chips, search, selection → mark printed, group "Download all", Move to picker, folder delete question with its count, drag onto a collection, Delete key, details from the CAD part, read-only view.
  • npm run lint passes. Related suites pass (publicRoutes, teamModules, modern, sidebarFooter, controlKitLink, CAD). pitDisplay.test.tsx times out the same way on main on this machine.

Screenshots

Local production build (npm run build, NODE_ENV=production) against a throwaway SQLite file with fixture data (team "Fixture Robotics", generated STL/OBJ/STEP/G-code files uploaded through the page, so thumbnails are the real browser-drawn ones). Captured with Playwright at 1x, dark theme. No production account or data was used.

A folder, list view:
Folder list view

A subsystem group, and three groups combined:
Intake group
Robot + PETG + To print

Grid with thumbnails (STEP and G-code show icons):
Grid view

A collection with a selection, ready to mark printed or failed:
Selection actions

Details, with the subsystem and copies taken from the linked CAD part:
Details sheet

3D preview:
3D preview

Deleting a folder that holds files:
Folder delete question

Phone (390 px): folder, folders-and-groups sheet, and a combined group in grid view:
Phone folder
Phone sidebar
Phone grid

Not verified

  • Free disk space on the VM (no access from this session); limits are configurable.
  • Behaviour on production (not deployed; nothing was run against tryctrlpoint.org).
  • Drag and drop of files from the operating system was exercised by the drop handler's code path and a test of in-app drags, not by a real OS drag in a browser.
  • Thumbnails need WebGL in the uploader's browser; without it a model file shows its icon until an editor opens its preview on a browser that has WebGL.

Each file lives once, in a folder, and belongs to smart groups worked out
from its details (season and robot, subsystem, material, print status) and
to any collections the team makes, without copies. The explorer shows
folders and groups side by side; picks combine ("Intake + PETG + To print").

- Upload by button or drag and drop (STL, 3MF, STEP/STP, OBJ, G-code),
  rename, move (drag or "Move to"), delete with confirmation, folder delete
  that says how many files, single and ZIP downloads, search, list and grid
  views, sorting by name, date or size.
- 3D preview for STL/3MF/OBJ with three.js (lazy), and browser-drawn
  thumbnails on grid tiles; STEP and G-code show an icon.
- Details: who added it and when, size, notes, robot/season, the CAD part it
  prints (its section is the subsystem and its quantity the copies needed),
  material, status and copies printed, collections, history.
- Bytes on the VM's disk under PRINTS_DIR at random keys; per-file limit,
  per-workspace quota checked inside the insert, and a free-disk reserve.
- manage_prints role permission (granted wherever manage_cad is); members
  view and download. Every change goes to print_activity.
- fileControlKitPrint() files a Control Kit print under
  "Season / Robot / Control Kit" for the import to call.
@sushilm20

Copy link
Copy Markdown
Collaborator Author

@greptile review

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The latest changes appear safe to merge.

Summary

Adds a workspace print library with folders, groups, previews, downloads, permissions, and Control Kit imports.

The latest changes address the two most recent review threads:

  • Prints stores each workspace’s files and tracks how they change.
  • The Prints page lets teams browse files by folder or group.
  • STL, 3MF, and OBJ files open as turnable 3D models.
  • Control Kit STEP ZIPs also file their print parts in Prints.

No new actionable issues were found in these changes.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Choose files] --> B[Check workspace files]
  B --> C[Apply ticket limits]
  C --> D[Return ticket URL]
  D --> E[Check member and workspace]
  E --> F[Consume ticket]
  F --> G[Stream ZIP]
Loading

Reviews (5) · Last reviewed commit: "Greptile: download tickets hold only exi..." · Reviewed by Greptile

Comment thread server/prints.ts Resolved
Comment thread src/modern/pages/prints/usePrints.ts Resolved
Comment thread server/prints.ts Resolved
Comment thread server/prints.ts Resolved
Comment thread server/prints.ts Resolved
Comment thread server/prints.ts Resolved
Comment thread src/modern/pages/prints/PrintsPage.tsx Resolved
Comment thread src/modern/pages/prints/PrintsPage.tsx Resolved
Comment thread server/zipStream.ts Resolved
Comment thread server/prints.ts Resolved
…tions; workspace-bound writes; thumbnails in the quota

- Bulk move keeps the destination's names taken, so a same-named file becomes "(2)".
- Uploads, moves and new folders check the destination inside the write; a folder move
  re-checks that the new parent is not inside it in the same statement; a folder delete
  re-reads its subtree and stops when the confirmed file count changed meanwhile.
- The page names its workspace on every write (X-CP-Prints-Team); a write after a
  workspace switch is refused, and a queued upload stops there or when the page closes.
- Marking one file printed fills its copies printed, as the bulk action does.
- Thumbnails count toward the workspace quota and the disk reserve.
- ZIPs refuse more than 65,535 entries; New folder shows on phones; F2 and Delete work
  on grid folders.
@sushilm20

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread server/prints.ts Resolved
Comment thread server/prints.ts Resolved
The zip Control Kit uploads after a send (contract v1.2 §6) is read after the
answer: each STEP in its 3D_print folder is filed for the import's robot under
"Season / Robot / Control Kit", named after its part from MANIFEST.txt and linked
to the CAD part its file name carries. A re-send replaces Control Kit's file for
the same part. CNC, 2D-cut and fabricate files stay only in the zip.
@sushilm20

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread server/controlKitLink.ts Resolved
Comment thread server/prints.ts Resolved
…any size by ticket; the copies stepper counts every click
@sushilm20

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread server/prints.ts Resolved
…, one use; a waiting copies count never overrides Printed
@sushilm20

Copy link
Copy Markdown
Collaborator Author

@greptile review

@sushilm20
sushilm20 merged commit 953128b into main Oct 29, 2026
1 check passed
@sushilm20

Copy link
Copy Markdown
Collaborator Author

Production verification (merge commit 953128b):

  • "Deploy to Oracle VM" run 40818280863 on the merge commit: success.
  • https://tryctrlpoint.org/ and /prints return 200.
  • Signed out, GET /api/prints/library and POST /api/prints/files are refused (see the status codes in the session log).
  • Not checked: the signed-in Prints page and a real upload, which need an owner sign-in; the VM's free disk against PRINTS_MIN_FREE_MB (5 GB default); and a Control Kit send landing in Season / Robot / Control Kit. Migration 155 ran as part of the deploy.

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