{"id":49761314,"url":"https://github.com/strausmann/label-printer-hub","last_synced_at":"2026-06-05T07:01:06.693Z","repository":{"id":356968567,"uuid":"1234785134","full_name":"strausmann/Label-Printer-Hub","owner":"strausmann","description":"Self-hosted label printer hub for Brother PT-Series and QL-Series — multi-printer queue management, Snipe-IT/Grocy/Spoolman integration, plugin-based architecture, PWA-ready","archived":false,"fork":false,"pushed_at":"2026-05-30T16:12:19.000Z","size":886,"stargazers_count":0,"open_issues_count":20,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T16:15:04.819Z","etag":null,"topics":["brother-printer","brother-pt","brother-ql","docker","grocy","homelab","label-printer","print-server","ptouch","self-hosted","snipe-it","spoolman","tailwindcss","thermal-printer"],"latest_commit_sha":null,"homepage":"https://github.com/strausmann/label-printer-hub","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/strausmann.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":"CLA.md"}},"created_at":"2026-05-10T16:32:44.000Z","updated_at":"2026-05-30T14:55:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/strausmann/Label-Printer-Hub","commit_stats":null,"previous_names":["strausmann/label-printer-hub"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/strausmann/Label-Printer-Hub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strausmann%2FLabel-Printer-Hub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strausmann%2FLabel-Printer-Hub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strausmann%2FLabel-Printer-Hub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strausmann%2FLabel-Printer-Hub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/strausmann","download_url":"https://codeload.github.com/strausmann/Label-Printer-Hub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strausmann%2FLabel-Printer-Hub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33932048,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["brother-printer","brother-pt","brother-ql","docker","grocy","homelab","label-printer","print-server","ptouch","self-hosted","snipe-it","spoolman","tailwindcss","thermal-printer"],"created_at":"2026-05-11T07:28:22.551Z","updated_at":"2026-06-05T07:01:06.676Z","avatar_url":"https://github.com/strausmann.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Label Printer Hub\n\n[![CI](https://github.com/strausmann/Label-Printer-Hub/actions/workflows/ci.yml/badge.svg)](https://github.com/strausmann/Label-Printer-Hub/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/strausmann/Label-Printer-Hub/actions/workflows/codeql.yml/badge.svg)](https://github.com/strausmann/Label-Printer-Hub/actions/workflows/codeql.yml)\n[![codecov](https://codecov.io/github/strausmann/Label-Printer-Hub/graph/badge.svg?token=JRG4PDU2QX)](https://codecov.io/github/strausmann/Label-Printer-Hub)\n[![CLA assistant](https://cla-assistant.io/readme/badge/strausmann/Label-Printer-Hub)](https://cla-assistant.io/strausmann/Label-Printer-Hub)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)\n[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)\n[![GitHub Issues](https://img.shields.io/github/issues/strausmann/label-printer-hub)](https://github.com/strausmann/label-printer-hub/issues)\n\n\u003e Self-hosted multi-printer hub for Brother PT-Series and QL-Series label printers. Pull-mode (user scans barcode) and push-mode (Spoolman/Grocy webhooks). Integrates with Snipe-IT, Grocy, Spoolman. Plugin-based architecture for additional printer models. PWA-ready for smartphone use.\n\n## Status\n\n**Early development.** See [open issues](https://github.com/strausmann/label-printer-hub/issues) for progress and the [master tracking issue #22](https://github.com/strausmann/label-printer-hub/issues/22) for the phase roadmap.\n\nThis project is being designed against the [Brother PT-E550W/P750W/P710BT Raster Command Reference v1.02](https://download.brother.com/welcome/docp100064/cv_pte550wp750wp710bt_eng_raster_102.pdf) and [QL-800/810W/820NWB Raster Command Reference](https://download.brother.com/welcome/docp100278/cv_ql800_eng_raster_101.pdf). Hardware tested: Brother PT-P750W (verified). Brother QL-820NWBc (in progress).\n\n## Features (planned)\n\n- **Multi-printer support** via plugin architecture (PT-Series and QL-Series, more on request)\n- **Pull-mode**: Open the web UI on your phone, scan a barcode, hit print\n- **Push-mode**: Spoolman/Grocy webhook → automatic label print\n- **App integrations**: Snipe-IT (asset tags), Grocy (product labels), Spoolman (3D-print spool labels)\n- **Print queue** with pause/resume/cancel/retry/priority operations\n- **Live status pages** per printer via Server-Sent Events (no page reload)\n- **Tape detection** via Brother native status block (no manual configuration)\n- **PWA-installable** for smartphone use\n- **Pluggable**: drop a new `printer_models/your_model.py` to add a new device\n\n## Tech Stack\n\n**Two-container split:** backend (printer protocols, queue, API) and frontend (UI, PWA) are separate containers. They release together at the same semver version, communicate over HTTP/JSON, and the frontend proxies SSE for live status updates.\n\n- **Backend** (`label-printer-hub-backend`): Python 3.12+, FastAPI, SQLModel (SQLite), asyncio\n- **Printer protocols**: `nbuchwitz/ptouch` (PT-Series), `pklaus/brother_ql` (QL-Series), `pysnmp` for status polling\n- **Frontend** (`label-printer-hub-frontend`): Go web server, Tailwind CSS, HTMX, PWA (manifest + service worker + Web Notifications API)\n- **Container**: Docker (multi-stage per service), GHCR + Docker Hub publishing, multi-arch (amd64 + arm64)\n- **CI/CD**: GitHub Actions, semantic-release, Dependabot\n\n## Container images and tags\n\nEvery stable release publishes to **GitHub Container Registry (GHCR)** and **Docker Hub** with this tag scheme:\n\n| Tag | Example for `1.0.0` | Use when |\n|---|---|---|\n| `1.0.0` | exact version | You want full reproducibility |\n| `1.0` | latest patch in 1.0.x | Auto-update bug fixes |\n| `1` | latest minor.patch in 1.x.x | Stay on major version, get features |\n| `latest` | most recent stable | You're fine with anything new |\n\nPre-releases (`1.0.0-rc.1` etc.) publish **only the full version tag** — never `latest`, `\u003cmajor\u003e`, or `\u003cmajor\u003e.\u003cminor\u003e` — so a pre-release can never silently become the default.\n\nBoth registries receive identical multi-arch images (`linux/amd64`, `linux/arm64`).\n\n## Quick Start\n\nSee [`examples/README.md`](examples/README.md) for sample compose files (standalone / Traefik / Pangolin / Caddy). For early-development testing of the REST API alone — without the frontend — see [`examples/compose.backend-only.yml`](examples/compose.backend-only.yml):\n\n```bash\n# Backend-only (builds the image from source — no GHCR pull required):\ngit clone --branch main https://github.com/strausmann/label-printer-hub.git\ncd label-printer-hub\ncp backend/.env.example .env\n$EDITOR .env                # set PRINTER_HUB_PT750W_HOST and friends\ndocker compose -f examples/compose.backend-only.yml up -d --build\n\n# REST API smoke\ncurl http://localhost:8090/healthz\ncurl -X POST http://localhost:8090/print -H 'Content-Type: application/json' \\\n  -d '{\"template_id\":\"qr-only-12mm\",\"data\":{\"title\":\"Smoke\",\"primary_id\":\"SMOKE-001\",\"qr_payload\":\"https://example.test\"}}'\n```\n\nTo build and run the **full stack** (backend + frontend) from source without any real printer hardware, use the smoke-test compose file:\n\n```bash\n# Full-stack local smoke test (mock printer, no hardware required):\ndocker compose -f dev/docker-compose.smoke.yml up --build\n\n# UI is served at http://localhost:8080\n# Verify both services are healthy:\ncurl http://localhost:8080/healthz   # frontend → backend_reachable: true\n```\n\n## REST API surface\n\n| Method | Path | Purpose | Body |\n|---|---|---|---|\n| `POST` | `/print` | Submit a print job | `PrintRequest` (see below) |\n| `GET` | `/jobs/{job_id}` | Poll job status (includes live SNMP block while printing) | — |\n| `POST` | `/jobs/{job_id}/resume` | Resume a job paused by tape mismatch (after the user changed the tape physically) | — |\n| `POST` | `/printer/resume` | Resume the printer queue after a recoverable error halted it (tape empty / cover open / offline) | — |\n| `GET` | `/healthz` | Liveness probe for orchestrators | — |\n| `GET` | `/readiness` | Readiness probe — deep check for reverse-proxy routing | — |\n\n### Health Probes\n\nThe backend exposes two HTTP probes with different semantics:\n\n| Endpoint | Purpose | What it answers |\n|----------|---------|-----------------|\n| `GET /healthz` | Liveness — Docker / Kubernetes container restart signal | \"the process and the event loop are alive\" |\n| `GET /readiness` | Readiness — reverse-proxy routing signal | \"the process can serve traffic right now\": database connectable, alembic at head, templates seeded, runtime printer matches DB, SNMP probe fresh, queue worker alive, SSE bus capacity ok |\n\n`/readiness` returns HTTP 200 with `status` of `ready` (all checks ok) or `degraded` (non-critical checks failing — still routable), and HTTP 503 with `not-ready` when a critical check (database, alembic, template_seed) fails.\n\nPangolin's `targets[0].healthcheck.path` can use `/readiness` for deep checks instead of `/healthz`; Docker container healthchecks should stay on `/healthz` to avoid restart loops on transient DB failures.\n\nSee `docs/superpowers/specs/2026-05-17-phase-7b-foundation-design.md` for the full check list and rationale.\n\n### `POST /print` request body\n\n```jsonc\n{\n  \"template_id\": \"qr-only-12mm\",         // id from app/seed/templates/\u003cid\u003e.yaml\n  // Exactly one of `lookup` or `data` is required.\n  \"lookup\":  { \"app\": \"snipeit\", \"identifier\": \"123\" },\n  \"data\":    { \"title\": \"Asset 123\",\n               \"primary_id\": \"ASSET-123\",\n               \"qr_payload\": \"https://snipe.example/assets/123\",\n               \"secondary\": [\"optional\", \"extra lines\"] },\n  \"options\": { \"copies\": 1,              // 1..10, default 1\n               \"auto_cut\": true,\n               \"high_resolution\": false },\n  // Default \"fail\" → synchronous 409 + error_detail{expected_mm, loaded_mm}.\n  // \"queue\"        → 202 + job_id; job lands in PAUSED until the user POSTs\n  //                  /jobs/{id}/resume after physically swapping the tape.\n  \"on_tape_mismatch\": \"fail\"\n}\n```\n\n### Synchronous error codes (`POST /print`)\n\n| HTTP | `error_code` | When |\n|---|---|---|\n| 404 | `template_not_found` | unknown `template_id` |\n| 409 | `tape_mismatch` | loaded tape ≠ template tape, `on_tape_mismatch=\"fail\"` |\n| 409 | `tape_empty` | preflight detects no media |\n| 409 | `printer_cover_open` | preflight detects cover open |\n| 502 | `integration_lookup_failed` | integration plugin raised |\n| 503 | `printer_offline` | SNMP preflight could not reach the printer |\n\n`tape_mismatch` responses include `error_detail: {expected_mm, loaded_mm}` so the client can build a \"swap the tape\" dialog.\n\n## Environment variables\n\nFull reference lives in [`backend/.env.example`](backend/.env.example). The most-used variables grouped by purpose:\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `PRINTER_HUB_DATABASE_URL` | `sqlite:////data/printer-hub.db` | SQLite path (Phase-5 persistence; ignored today) |\n| `PRINTER_HUB_PT750W_HOST` | _empty_ | Brother PT-P750W IP/hostname (required when `printer_backend=ptouch`) |\n| `PRINTER_HUB_PT750W_PORT` | `9100` | TCP print port |\n| `PRINTER_HUB_QL820_HOST` | _empty_ | Brother QL-820NWB IP (when QL backend lands) |\n| `PRINTER_HUB_QL820_PORT` | `9100` | TCP print port |\n| **`PRINTER_HUB_PRINTER_BACKEND`** | `ptouch` | Transport: `ptouch` \\| `mock` \\| third-party entry-point id |\n| **`PRINTER_HUB_PRINTER_MODEL`** | `PT-P750W` | Fallback model id when SNMP discovery is off / unreachable |\n| **`PRINTER_HUB_PRINTER_DISCOVER_VIA_SNMP`** | `true` | SNMP-first model discovery via Brother private OID, fall back to `PRINTER_HUB_PRINTER_MODEL` on failure |\n| **`PRINTER_HUB_PRINTER_SNMP_COMMUNITY`** | `public` | SNMPv2c community (LAN-only — read-only) |\n| **`PRINTER_HUB_PRINTER_QUEUE_TIMEOUT_S`** | `30` | Graceful shutdown timeout for the print queue |\n| `PRINTER_HUB_WEBHOOK_API_KEY` | _empty_ | Bearer for inbound integration webhooks (≥ 32 chars; generate with `openssl rand -hex 32`) |\n| `PRINTER_HUB_SNIPEIT_URL` | _empty_ | Snipe-IT base URL |\n| `PRINTER_HUB_SNIPEIT_API_KEY` | _empty_ | Snipe-IT bearer token |\n| `PRINTER_HUB_GROCY_URL` | _empty_ | Grocy base URL |\n| `PRINTER_HUB_GROCY_API_KEY` | _empty_ | Grocy API key |\n| `PRINTER_HUB_SPOOLMAN_URL` | _empty_ | Spoolman base URL (no auth) |\n| `PRINTER_HUB_SERVER_PORT` | `8090` | Internal port (overridden to `8000` in the container — see Dockerfile) |\n| `PRINTER_HUB_LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |\n\nAll variables share the `PRINTER_HUB_` prefix and map 1:1 to the `Settings` model in `backend/app/config.py`.\n\n## Documentation\n\n**In this repository (engineering — change with code):**\n\n- [Architecture overview](docs/architecture.md) — how the pieces fit\n- [Decisions (ADRs)](docs/decisions/) — *why* each architectural choice was made\n- [Plugin development](docs/plugin-development.md) (TBD) — adding new printer models\n- [Privacy policy](docs/policies/privacy.md), [Trademark policy](docs/policies/trademarks.md)\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — Conventional Commits, TDD, PR workflow\n\n**On the wiki (tutorials and platform recipes — community-friendly):**\n\n- [Getting started](https://github.com/strausmann/label-printer-hub/wiki/Getting-Started)\n- [Snipe-IT integration](https://github.com/strausmann/label-printer-hub/wiki/Snipe-IT-Integration)\n- [Grocy integration](https://github.com/strausmann/label-printer-hub/wiki/Grocy-Integration)\n- [Spoolman integration](https://github.com/strausmann/label-printer-hub/wiki/Spoolman-Integration)\n- [Install as PWA](https://github.com/strausmann/label-printer-hub/wiki/Install-as-PWA) (TBD)\n- [Troubleshooting](https://github.com/strausmann/label-printer-hub/wiki/Troubleshooting) (TBD)\n\n**Live API reference** (when running): `/openapi.json`, `/docs` (Swagger UI), `/redoc` — see [ADR 0011](docs/decisions/0011-openapi-as-api-contract.md).\n\n## Contributing\n\nContributions are welcome — especially **printer model plugins**. See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow (Conventional Commits, TDD, semantic-release).\n\n## Trademarks and disclaimer\n\n\u003e **Brother**, **P-touch**, **PT-Series**, and **QL-Series** are trademarks or registered trademarks of **Brother Industries, Ltd.** All other trademarks are the property of their respective owners.\n\u003e\n\u003e This project is **not affiliated with, endorsed by, or sponsored by Brother Industries, Ltd.** It is an independent open-source project that interoperates with Brother label printers via documented protocols (Brother Raster Command Reference, IEEE 802.3, RFC 3805 SNMP Printer-MIB, RFC 8011 IPP).\n\u003e\n\u003e \"Brother\" is used in this README solely for the purpose of describing the hardware that this software is compatible with. No commercial use of the Brother trademarks is intended.\n\n## License\n\nThis project is licensed under the **MIT License** — see [LICENSE](LICENSE) for details.\n\nThe Brother Raster Command Reference PDFs distributed in this repository (under `docs/research/brother-spec/`, if present) remain the property of Brother Industries, Ltd. Their inclusion is a verbatim, unmodified copy redistributed under fair use for development reference. The Brother documentation license terms apply to those files.\n\n## Acknowledgements\n\n- [`pklaus/brother_ql`](https://github.com/pklaus/brother_ql) — QL-Series Python library\n- [`nbuchwitz/ptouch`](https://github.com/nbuchwitz/ptouch) — PT-Series Python library\n- [`donkie/Spoolman`](https://github.com/donkie/Spoolman), [Grocy](https://github.com/grocy/grocy), [Snipe-IT](https://github.com/snipe/snipe-it) — apps this hub integrates with\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstrausmann%2Flabel-printer-hub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstrausmann%2Flabel-printer-hub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstrausmann%2Flabel-printer-hub/lists"}