(Plan from kimi-k3 in kimi code)
Prompt:
add the ability to queue multiple labels and then print them out chained together and then perform the final cut
Let the user queue up to N labels in the web UI, then print them all in one action: labels are chained together (no cut between them) and the printer performs a single cut after the last label.
- Chaining mechanics already exist and are proven on the real printer: the current
"chain print" button sends one job per label with
chainPrint: true, which makesprinter_class_for_mode(yorkstlabelco/printer.py:237) forcechain_printing=Truein the ptouch page-control sequence (clears bit 3 of theESC i Kcommand → "no cut between labels"). A later normal print (chain_printing=False,auto_cut=True) cuts the strip. This works across separate TCP connections, one label per connection. - ptouch's
print_multi()cuts between labels — NOT what we want. We reuse the existingprint_label(..., chain_print=...)path instead: labels 1..N-1 withchain_print=True, the last withchain_print=False→ chained strip + final cut. No changes toyorkstlabelco/printer.pyare needed. - Jobs are serialized by the single
JobManagerworker, so a batch job cannot interleave with other prints. - All labels in a batch share one tape width (one physical strip), resolved once at enqueue time with the same auto/detected logic as single prints.
- Extract the
tapeWidthMmvalidation body into a module-level helper so both models can use it (keep behavior identical). - Add:
MAX_BATCH_LABELS = 32 class BatchPrintRequest(BaseModel): tapeWidthMm: str | float = Field(default="auto") labels: list[LabelRequest] = Field(min_length=1, max_length=MAX_BATCH_LABELS) # same tapeWidthMm validator, is_auto_tape property, # and resolve_tape_width() as LabelRequest
- Per-label
tapeWidthMm/chainPrintfields are simply not consulted in the batch path (the frontend won't send them); batch tape is resolved once at the batch level and chain behavior is derived from position (last label = final cut).
PrintJob.payload→payloads: list[LabelRequest](update dataclass;to_dictunchanged).enqueue(payload, tape_width_mm)keeps its signature, wraps into[payload].- New
enqueue_batch(payloads: list[LabelRequest], tape_width_mm). _processrework:- Render ALL labels up front (fail before any tape moves if rendering breaks).
Message:
"Rendering label"for n=1 (unchanged),f"Rendering {n} labels"for n>1. - Send loop:
chain = payloads[0].chainPrint if n == 1 else i < n - 1. Message per send:"Sending label to printer"for n=1 (unchanged),f"Sending label {i+1} of {n}"for n>1 (drives the existing UI progress line). - On exception during item i of a batch:
job.update("failed", f"Print failed on label {i+1} of {n}", str(exc)); single-label failure behavior unchanged.
- Render ALL labels up front (fail before any tape moves if rendering breaks).
Message:
PrinterLikeprotocol unchanged (still per-labelprint_labelwithchain_print).
- Factor the shared pre-print logic out of
print_labelinto a helper (status check → 409 with warning detail; auto tape → detected/default) returning the resolved tape width. - New endpoint:
@app.post("/api/print/batch") async def print_batch(payload: BatchPrintRequest) -> dict[str, str]: # same readiness/auto-tape logic via the helper (one check for the batch) job = await jobs.enqueue_batch(payload.labels, tape_width) return {"jobId": job.id}
- Job status endpoint (
GET /api/jobs/{id}) unchanged.
- Add an "add to queue" button in
.print-actionsnext to the existing print/chain buttons (#add-to-queue-button, plus-style icon). - Add a queue section between the form and
#bottom-status: header ("Label queue" + count),#batch-list(one row per label: text preview + small remove button), an empty-state hint, and a#print-queue-button("Print Queue (N)"). - Existing markup/buttons untouched.
- New state:
const batchQueue = [](items:{text, fontSize, marginMm, bold, italic}captured frompayload(), tape excluded — shared tape is taken from the tape selector at print time). addToQueue(): push current label (cap at 32, show a status warning when full), re-render queue list.renderQueue(): rebuild#batch-listrows with per-item remove buttons; update count + Print Queue button label/disabled (disabled when empty,printInProgress, or blocked — same gating as the existing buttons inrefreshPrintButton(), extended to the two new buttons).printQueue(): POST/api/print/batchwith{tapeWidthMm: payload().tapeWidthMm, labels: batchQueue}; on accepted, reuse the existingpollJob()flow; onsentclear the queue, on failure keep it so the user can retry. Reuses existing error/status handling patterns.- Wire the two new buttons; leave single print / chain print behavior as-is.
- Styles for the queue section, list rows, remove buttons, and the print-queue button, reusing existing button/status styling conventions.
tests/test_models.py:BatchPrintRequest— emptylabelsraises validation error; >32 labels rejected; tape width validator applied;resolve_tape_widthworks for auto and explicit widths.tests/test_jobs.py(extendFakePrinter-based tests):- batch of 3 →
printer.callsin order, chain flags exactly[True, True, False], identical tape width on all calls. - failing printer mid-batch → job state
failed, error surfaced, message notes the failing label index. - existing single-job tests keep passing (message/signature compatibility).
- batch of 3 →
tests/test_api.py:POST /api/print/batchhappy path: 3 labels → job reachessent;fake.printedchain flags ==[True, True, False], tape widths all 24.0 (auto → detected).- blocking printer warning → 409, nothing printed.
- empty
labels→ 422. - static hook test mirroring
test_chain_print_button_and_enter_submit_chain_print:index.htmlcontainsid="add-to-queue-button"andid="print-queue-button";app.jsposts to/api/print/batch.
- Run the full suite:
.venv/bin/python -m pytest(pytest config in pyproject.toml). - Sanity-check the UI manually with the mock printer
(
mock_printer=Truesettings) — add 2–3 labels, print queue, confirm one job, progress messages "Sending label i of n", and queue clears on success.
- No changes to
printer.py/ ptouch usage (batch reuses per-label connections, exactly like today's manual chain flow). - No reorder/edit of queued labels (add + remove only).
- The existing single print and manual chain-print buttons keep working unchanged.