Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Gateway routes:
| GET | `/health` | Process liveness |
| GET | `/printers` | Safe printer inventory (no addresses or credentials) |
| GET | `/status` | Aggregate gateway envelope (one component per printer) |
| GET | `/ui` | Submission page for people (see below) |

Per-printer STATUS_SPEC routes:

Expand Down Expand Up @@ -246,8 +247,28 @@ dispatches, so a running print was started by some other route to the printer
(Bambu Studio, the handset, the cloud) and the gateway reports only what it
observes.

### The page

`GET /ui` serves a submission page: pick a machine (its plate size, nozzle,
chamber and limits are shown so you know what you are targeting), upload a
file, and read the per-check verdict. It also lists that machine's queue with
finish times, and offers Approve / Cancel.

It is one static file with **no build step and no external resources** — no
CDN, no npm, no bundler — served from the same origin as the API it calls, so
it needs no CORS exemption and works on an isolated lab network. It holds no
state of its own and calls only the public endpoints below, so it can do
nothing the API would refuse. It offers Approve only on a `queued` job, which
is the same rule the server enforces: never advertise an action that would be
refused.

The page has no sign-in. The name you type is a label, not an identity — see
*Identity and approval* below.

### Submitting

The page is the easy path. Directly:

```bash
curl -sS -X POST http://127.0.0.1:8012/submissions \
-F file=@plate.gcode.3mf \
Expand Down
14 changes: 13 additions & 1 deletion deploy/bambu-server.local.service
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ User=sdl2
Group=sdl2
WorkingDirectory=/home/sdl2/caoyang/bambu-server
EnvironmentFile=/home/sdl2/caoyang/bambu-server/.env
# Bound to every interface, not loopback, for two readers that cannot share one
# address: the dashboard aggregator polls this service on 127.0.0.1:8012 (see
# ac-organic-lab/equipment.yaml), while a person opening /ui reaches it over the
# tailnet at 100.64.254.6:8012 -- loopback means the visitor's own machine in a
# browser, so a loopback-only bind serves the aggregator and nobody else.
#
# This matches the fleet's documented posture (DEVICE_PC_SETUP: device services
# bind 0.0.0.0 and access is gated by Tailscale ACLs, not by a local firewall).
# Note what it widens: port 8012 is now reachable on every interface this host
# has, and POST /submissions has no application-level auth. A tighter shape, if
# the exposure ever matters, is to keep loopback and front /ui through the Caddy
# edge like the camera gateway -- which would also put it behind ac_auth.
ExecStart=/home/sdl2/caoyang/bambu-server/.venv/bin/uvicorn bambu_server.main:application_factory \
--factory \
--host 127.0.0.1 \
--host 0.0.0.0 \
--port 8012 \
--no-server-header \
--log-level info
Expand Down
3 changes: 3 additions & 0 deletions deploy/bambu-server.service
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ User=ac
Group=ac
WorkingDirectory=/opt/bambu-server
EnvironmentFile=/opt/bambu-server/.env
# Loopback by default: the conservative choice for a fresh host. Change --host
# to 0.0.0.0 if people need to reach /ui over the tailnet -- see the note in
# bambu-server.local.service for what that widens.
ExecStart=/opt/bambu-server/.venv/bin/uvicorn bambu_server.main:application_factory \
--factory \
--host 127.0.0.1 \
Expand Down
20 changes: 19 additions & 1 deletion docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,28 @@ Open, from the design's §10 data gaps and what the build surfaced:
stay on disk and in `GET /submissions` indefinitely. Fine at current volume,
but it needs a sweep before this runs unattended for long.

## Submission page

`GET /ui` — one static file (`src/bambu_server/static/index.html`), no build
step, no external resources, served from the same origin as the API. Added
because the pipeline shipped with no human-facing surface at all: the design
assumed the lab dashboard would render these endpoints, so a UI was never in
its scope, which left `curl` and Swagger as the only way in.

Deliberate limits: it holds no state, calls only public endpoints, and offers
Approve only on a `queued` job so it can never advertise a refusal. It has no
sign-in, matching the rest of the service.

Not visually verified — there is no browser on this host, so only the HTML
structure and the script's syntax were checked. Worth a look in a real browser
before pointing users at it. The durable home is probably the lab dashboard
(`ac-organic-lab/web`) once `ac_auth` makes `requested_by` a real identity;
this page is the interim surface.

## Test suite

- `uv run ruff check .` passes.
- `uv run pytest -q` passes all 126 tests, including the FastAPI API tests and
- `uv run pytest -q` passes all 130 tests, including the FastAPI API tests and
the submission pipeline (artifact inspection, validation, store/state machine,
queue ETA, HTTP surface). Tests build their own `.3mf` and `.gcode` fixtures
and use fake backends; nothing touches hardware.
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ bambu-server = "bambu_server.__main__:main"
[tool.setuptools.packages.find]
where = ["src"]

# The submission page ships with the package so a non-editable install serves
# /ui too, not just a checkout-based deploy.
[tool.setuptools.package-data]
bambu_server = ["static/*.html"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Expand Down
18 changes: 18 additions & 0 deletions src/bambu_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
from collections.abc import AsyncIterator, Callable
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from pathlib import Path as PathLib
from pathlib import PurePosixPath
from typing import Annotated

from fastapi import Depends, FastAPI, File, Form, HTTPException, Path, Query, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field

from . import __version__
Expand Down Expand Up @@ -62,6 +64,11 @@
#: Read size for streaming an upload to disk.
_UPLOAD_CHUNK_BYTES = 1 << 20

#: The submission page. One self-contained file with no build step and no
#: external resources, served from the same origin as the API it calls, so a
#: browser needs neither a bundler nor a CORS exemption to use it.
_UI_PAGE = PathLib(__file__).parent / "static" / "index.html"

#: Leading bytes an artifact must start with, keyed by kind. A ``.3mf`` is a
#: zip container; anything else under that name is a malformed submission and
#: is refused at intake rather than carried through validation.
Expand Down Expand Up @@ -169,6 +176,17 @@ async def gateway_info() -> GatewayInfo:
printer_count=len(monitors),
)

@app.get("/ui", response_class=HTMLResponse, include_in_schema=False, tags=["gateway"])
async def submission_ui() -> HTMLResponse:
"""The operator/submitter page.

Deliberately a single static file: it calls the same public endpoints
any other client would, holds no state of its own, and cannot do
anything the API would refuse.
"""

return HTMLResponse(_UI_PAGE.read_text(encoding="utf-8"))

@app.get("/health", response_model=HealthResponse, tags=["gateway"])
async def gateway_health() -> HealthResponse:
return HealthResponse()
Expand Down
Loading
Loading