Skip to content

Add the headless print engine: rotula CLI and REST API - #13

Merged
glenntosco merged 4 commits into
mainfrom
claude/project-thread-4ng9io-print-engine
Oct 24, 2026
Merged

glenntosco merged 4 commits into
mainfrom
claude/project-thread-4ng9io-print-engine

Conversation

@glenntosco

@glenntosco glenntosco commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Glenn · project thread

What changed and why

This implements the top roadmap item: unattended printing for WMS, ERP and scripts, with the ~HS printer status from #5.

  • src/Rotula.Engine (net10.0 library, no UI). It turns a print request into a print through the app's own pieces: IPrintService, LabelPrintRun/VariableJob, PromptSet, LabelDpiConverter and PrinterSetup. Output is byte-identical to the designer's Print dialog and data merge.
    • DPI conversion, darkness and speed, copies, and records with the label's saved merge settings.
    • Serial numbers are reserved before a job and confirmed only for labels that were sent.
    • Questions take the designer's default. A sample value is never printed, and a required question with no default stops the job.
    • ~HS check before every job, and verification after the last one.
    • One job at a time per printer (a SemaphoreSlim per queue or host:port).
    • Results are values (outcome, error code and notices in the caller's language), never exceptions.
  • src/Rotula.Cli builds rotula.exe:
    • print: --set for values, --data for CSV, xlsx or JSON, --answer, --dry-run/--output, --json.
    • status, printers, new-key, help and version.
    • Exit codes 0 to 6.
  • rotula serve, an ASP.NET Core minimal API:
    • Routes: GET /api/v1/health, GET /api/v1/printers, GET /api/v1/printers/{name}/status, POST /api/v1/printers/status, POST /api/v1/jobs, and OpenAPI at /openapi/v1.json.
    • API keys go in X-Api-Key or a Bearer header. They are stored only as SHA-256 hashes and compared in constant time.
    • Only allow-listed label folders and printers can be used; there is no rooted path, .. or UNC, and inline labels are off by default.
    • Size limits on request bodies and label files.
    • Unknown names are refused in both the settings file and request bodies.
    • Listens on 127.0.0.1:9180 by default and can run as a Windows service.
    • An optional counterFile setting shares the designer's serial numbers with a service account.
    • An audit log line per job, with no key or label data in it.
  • PrintEngine policy, in the ADMX and in the en/es ADML files, switches the engine off.
  • Strings: 52 new Engine.* strings in en, es, pt-BR and fr-CA. The engine embeds the app's resx files, so there is one source of text.
  • Docs:
    • New docs/print-engine.md.
    • Updated README (new section, layout and privacy), CHANGELOG, THIRD-PARTY-NOTICES, deployment.md, decisions.md, and CONTRIBUTING's offline-first rule (the server only listens, opt-in).
  • New packages, all MIT:
    • Microsoft.AspNetCore.OpenApi 10.0.13
    • Microsoft.OpenApi 2.12.2 (this version avoids the vulnerable 2.0.0)
    • Microsoft.Extensions.Hosting.WindowsServices 10.0.13

Not in this PR: adding rotula.exe to the installer, MSI and release workflow. That is a follow-up, and the README says it is built from source for now.

How it was tested

  • 42 new xUnit tests (Facts and Theories) in EngineTests.cs and EngineCliTests.cs. They cover:
    • the engine (single label, records, paper out, no-status, remembered serials, dry run, questions, invalid label, limits, network target, per-printer serialization, verification);
    • the CLI (JSON, CSV, dry-run output, exit codes, policy, languages, serve --init, counterFile);
    • the REST API through TestServer (auth, folders, refusals, inline labels, 409 in Spanish, allow-lists, policy, OpenAPI, the status code map);
    • every Engine.* key in all four languages.
  • On Linux, these tests ran together with the existing printer-status tests: 127 of 127 passed.
  • dotnet build Rotula.sln -c Release -p:EnableWindowsTargeting=true gives 0 warnings and 0 errors.
  • Windows CI still has to run the ADMX/ADML parity tests and the app localization tests.
  • Not tested on hardware: a real ~HS check over USB and over the network, and installing the Windows service with sc.exe.

Checklist

  • Every new user-visible string is in en, es, pt-BR and fr-CA
  • Offline-first: no outbound calls except to printers, and the server is opt-in
  • Tests added
  • Docs and CHANGELOG updated

🤖 Generated with Claude Code

https://claude.ai/code/session_01C3aU29K6sbAADYvWLjb7hB

rotula.exe prints .rotula labels without the designer, for scripts and
warehouse systems, through the app's own print path (DPI conversion,
darkness/speed, variables, questions, data merge, serial numbers used once).

- rotula print / status / printers with JSON output and exit codes
- rotula serve: REST API (/api/v1/jobs, printer status and lists, OpenAPI),
  API keys stored as SHA-256 hashes, label-folder and printer allow-lists,
  size limits, strict settings and request bodies, Windows service hosting
- ~HS status check before every job and verification after the last;
  one job at a time per printer
- PrintEngine policy (ADMX/ADML) to switch it off
- 52 new strings in en, es, pt-BR and fr-CA
- docs/print-engine.md, README, CHANGELOG, notices, deployment, decisions
@glenntosco glenntosco self-assigned this Oct 24, 2026
The request path, reference and printer name are written to the audit
log; control characters (CR, LF and others) now become spaces and long
values are cut, so a caller cannot start a fake log entry.
The tests now also reference rotula.exe, which runs on ASP.NET Core, so
the build leaves the Microsoft.Extensions.* files that framework provides
out of the test folder. Rotula.Mcp.exe runs on plain .NET and could not
start from there; the tests now use src/Rotula.Mcp/bin first.
@glenntosco
glenntosco merged commit dda13a1 into main Oct 24, 2026
6 checks passed
glenntosco added a commit that referenced this pull request Oct 27, 2026
Add the headless print engine: rotula CLI and REST API
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.

3 participants