- JavaScript 85.3%
- CSS 8.1%
- HTML 6.3%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| k8s | ||
| lib | ||
| public | ||
| test | ||
| .dockerignore | ||
| .gitignore | ||
| build.mjs | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| server.mjs | ||
printit
A family app for adding a model, sizing it for the printer, reviewing a sliced
preview, and starting a print through bambuddy. It is reachable
only on the tailnet at https://printit.<tailnet>.ts.net.
Using PrintIt
The printer panel stays visible throughout the workflow. It shows availability, preparation, printing, pauses, progress, layers, and alerts, including jobs started outside this browser. Show live camera opens the printer's MJPEG feed through PrintIt. Hidden tabs release their camera connection and reconnect when reopened; closing a viewer does not stop other viewers. Stalled images are hidden and can be reconnected. The camera and printer remain behind the tailnet boundary.
- Drop an STL, OBJ, or 3MF (up to 20 MB / 150,000 triangles), paste a MakerWorld model URL or direct HTTPS download, or describe an object.
- Pick a printer, adjust the size or orientation, and choose a loaded PLA spool, finish, and installed plate. Oversized models shrink proportionally; small models stay at their original size. P1/X1 printers use a conservative 20 mm inset to clear their cutter exclusion; A1 models use 5 mm. STL and OBJ coordinates are mm; 3MF units and object transforms are honored.
- Prepare the preview. PrintIt sends the transformed STL, with explicit printer, process, filament, and plate presets, to Bambuddy's async slicer.
- Review the slicer's plate image or interactive extrusion layers, time and filament estimates. Confirm the bed is clear and filament is loaded, then start printing. Acceptance of the command is reported separately from the live printer state.
Describe → Make my model asks a modeling agent to create and compile a new parametric OpenSCAD design in a fresh Sprite through Fountain. Attach up to three PNG, JPEG, or WebP reference pictures; the browser resizes them before sending. Include measurements for fitted parts: pictures guide shape, not exact dimensions. The finished model includes assumptions and print notes, downloadable STL and editable CAD, and Describe a change to revise the source in a fresh workspace. The separate library search matches saved filenames/notes and links to MakerWorld. Use a sample tray remains available without a model-generation service.
The server downloads the compiled binary STL, source, and design metadata through Fountain's sandbox files API. It independently checks file completeness, finite geometry, closed and consistently oriented triangle edges, and positive volume. Invalid deliverables get up to two repair turns. These checks do not establish wall thickness, freedom from self-intersections, support requirements, or physical fit; inspect the model and sliced layers before printing. Generation never starts a print. The worker receives no Fountain callback token or printer credentials.
Design jobs survive reloads and server restarts. A lost start response is recovered by its saved conversation channel, without blindly starting another paid run. Each browser session can make eight designs, one at a time; the app permits two active designs in total, with a 15-minute timeout per design. Workers are terminated after artifact collection or failure. Accepted output is limited to 60,000 triangles and 4 MiB of STL, with 128 KiB of source. Images are limited to 2 MiB each and 4 MiB total after resizing. These are household limits, not per-person billing controls.
This first version prepares single-color PLA with a 0.4 mm nozzle. Supported bed families are P1S, P1P, X1C, A1, and A1 Mini; preset availability is validated at slice time. The household P1S is the live integration target. Unknown printers, missing profiles/materials, and already sliced G-code are rejected. 3MF project colors/settings are discarded; all printable build objects are flattened into one geometry with their relative positions preserved. Check assemblies before printing, or export the desired part as STL. This is not an automatic support-design, mesh-repair, or multi-plate/multicolor workflow.
App layout
| path | role |
|---|---|
server.mjs |
HTTP API, session-scoped projects, slice polling, preview and print lifecycle |
lib/geometry.mjs |
STL/OBJ/3MF parsing, unit/transform handling, fitting, template geometry |
lib/printers.mjs |
printer and material allowlists, compatible preset selection, dispatch checks |
lib/sources.mjs |
bounded downloads with public-address validation and DNS pinning |
lib/designs.mjs |
durable Fountain jobs, image validation, artifact checks, repair and cleanup |
lib/design-agent.mjs |
modeling agent instructions and Sprite environment definition |
public/ |
responsive family UI and interactive Three.js bed preview |
test/ |
geometry and lifecycle tests, simulated Bambuddy, local UI preview server |
k8s/ |
Kubernetes manifests (Kustomize), applied by Flux from home-cloud |
.forgejo/workflows/test.yml |
app tests and browser bundle build on PRs |
.forgejo/workflows/build.yml |
arm64 container build into the Forgejo registry on main, then the image pin |
Project lifetime and printing behavior
Projects and print receipts live under PRINTIT_DATA_DIR (/data on a 2 GiB
Longhorn volume in the cluster; /tmp/printit by default for local development) with a random HttpOnly, SameSite session cookie. Reloading the
same tab resumes the project. Project directories expire after 24 hours and
survive pod replacement until expiry; imported and sliced files remain in
Bambuddy's library. Print dispatch reservations do not expire automatically.
The server requires the sliced preview to have been loaded and reviewed,
(toolpaths are read from the sliced file when the slicer omits a thumbnail),
checks the model revision, printer connection/state/alerts, nozzle and spool,
and reserves the printer before dispatch. Duplicate requests return the
recorded receipt. A lost dispatch response is uncertain, never retried.
A reservation remains until a later status read observes this model printing. The app tracks
progress, pauses, and completion only when the reported filename matches this
project, so an unrelated print does not appear as its progress.
If an observed print returns to idle without reporting completion (as the P1S
does after a homing failure), the app reports that it stopped and preserves the
original receipt. It never automatically retries that attempt.
If the command never starts (including Developer Mode failures), an operator
must verify the printer and Bambuddy queue before removing the corresponding
dispatch-<printer-id>.json receipt under the data directory. Restarting the deployed app preserves these receipts and never retries a
print command.
The app uses the tailnet as its access boundary. Session cookies isolate projects between browsers; they are not user accounts. Mutations require a same-origin custom header. Public model downloads reject private/LAN/tailnet addresses, pin DNS resolutions, validate each redirect, and enforce byte/time limits. Health/readiness probes remain independent of Bambuddy availability.
How it deploys
- Merge to
main.build.ymltests, builds and pushes the arm64 image, then commits its:sha-<commit>@<digest>intok8s/deployment.yamlonmain(expect those bot commits). - home-cloud's Flux
printitKustomization (source: theprintitGitRepository, cloned from Forgejo over its in-cluster Service) applies./k8sinto namespaceprintit. - The Tailscale operator creates a
ts-printit-*proxy and serveshttps://printit.<tailnet>.ts.net. The first HTTPS request hangs ~30s while it does ACME; pre-warm from any tailnet host withtailscale cert printit.<tailnet>.ts.net.
Changes under k8s/ alone don't rebuild the image; Flux picks them up on its
next source poll.
Contract between the app and k8s/
Keep these when you replace server.mjs, or update k8s/ in the same change:
- Listens on
PORT(default 3000), container port namehttp. GET /healthz(liveness) andGET /readyz(readiness) return 200 when up. Keep/readyzabout this process, not bambuddy. If bambuddy being down made printit unready, the tailnet Ingress would 502 instead of showing an error.- Reads
BAMBUDDY_URLfor the bambuddy base URL. Default in-cluster:http://bambuddy.bambuddy.svc.cluster.local. For local dev, point it athttps://bambuddy.tailb2e8f2.ts.net(npm run devdoes this). - Model generation requires explicit
FOUNTAIN_URL,FOUNTAIN_AGENT_ID, andFOUNTAIN_API_KEY. The server does not fall back to personal CLI credentials. Without configuration, upload, links, sample geometry, and printing still work. The manifest references optionalprintit-secrets/FOUNTAIN_API_KEY, supplied by the Infisical projectprintit(printit-d-xv-z). Keep this account-wide key server-side. The modeler uses ephemeral workspaces withsandbox_api_access: none. - Runs as uid 1000 (
node) with a read-only root filesystem./tmpis an emptyDir and/dataholds the persistent projects and print receipts. Anything that writes elsewhere needs a volume ink8s/deployment.yaml.
Talking to bambuddy
bambuddy is FastAPI under /api/v1, and auth is disabled on this cluster,
so no token is needed. Its pod is hostNetwork on one node but the ClusterIP
Service fronts it normally. Verified reachable from an ordinary pod.
Endpoints exercised so far (see server.mjs for the client):
| call | notes |
|---|---|
GET /printers/ |
list. Includes access_code, a credential. Do not surface it. |
GET /printers/{id}/status |
live state, temps, hms_errors, progress. |
GET /library/files?folder_id=1 |
the file library. |
POST /library/files/{id}/slice |
body selects printer/process/filament presets; async, 202. |
POST /library/files/{id}/print?printer_id=1 |
uploads over FTP and sends the MQTT print command. |
POST /printers/{id}/logging/enable, GET /printers/{id}/logging |
raw MQTT log, the only way to see the printer's reply to a print command. |
Gotchas learned while proving the pipeline (2026-09-06):
- The slice dialog's default presets can be for the wrong printer (an H2D
process and ABS filament for a P1S with PLA loaded). There is no P1S process
preset; the P1S uses the
@BBL X1Cprocess presets. - bambuddy's
developer_modeflag for a printer is a false positive. If the printer ignores a print command and never heats, read the MQTT log:err_code 84033543means Developer Mode is off on the printer's own screen. kubectl rollout restarton any Deployment here is reverted by Flux. Delete the pod.
Local dev
npm ci
npm test
npm run dev # http://localhost:3000, talks to bambuddy over the tailnet
docker build --platform linux/arm64 -t printit:dev .
docker run --rm -p 3000:3000 --read-only --tmpfs /tmp \
-e BAMBUDDY_URL=https://bambuddy.tailb2e8f2.ts.net printit:dev
For UI testing with no physical printer access:
npm run build
node test/preview-server.mjs # http://127.0.0.1:3188 (simulated printer)
The simulator uses deterministic agent/model/preview/estimate fixtures; it never contacts Fountain or Bambuddy. Integration tests verify reference-image forwarding, agent isolation, lost-response recovery, revisions, repair and bounded downloads, as well as fitted geometry being the actual uploaded file, compatible presets, review gates, session isolation, stale revisions, printer preflight, and ambiguous/duplicate dispatch behavior.
Check manifest changes render before pushing:
kubectl kustomize k8s
Runtime secrets
The printit-service Fountain key lives in the printit Infisical project's
prod environment. The printit-operator identity has project viewer access
and Kubernetes auth restricted to printit/printit-infisical. The app receives
the operator-managed printit-secrets Secret; the modeling agent receives no
callback token. Rotation uses Infisical's auto-reload annotation. Use the
/add-secret-via-infisical skill (in home-cloud) when adding runtime keys.