james.maes eb2c16785d
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Failing after 1s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (push) Failing after 1s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Waiting to run
Shared Java Maven CI / secrets (push) Successful in 17s
Shared Java Maven CI / verify (push) Successful in 3m35s
Shared Java Maven CI / gate (push) Successful in 11s
qrun-release / release (push) Successful in 3m49s
Shared QRun source version / version (push) Successful in 17s
qrun-release / postpublish (push) Successful in 18s
Merge pull request 'feat(printers): remove printers and devices as archives, purged by retention' (#28) from feat/remove-printers-devices-main-20261105 into main
2026-11-05 22:59:43 +00:00

QQQ Printing

Cloud printing for QQQ 4.1+, paired with the Windows AppKit connector 1.2.0 and the shared protocol 1.1.0 (1.0.1 / 1.0.0 remain compatible). The Windows connector opens an outbound, authenticated two-way WebSocket; documents download separately over HTTPS. The cloud needs no inbound route into the facility. Version 1.1.0 supports native printing 2.4, wire payload v1, real-time jobs and controls, native browser sign-in through QQQ, and pushes each exposed printer's approval to connectors that advertise printer-approval-v1. Upgrade the server before the matching Windows connector.

Released: QQQ Printing 1.0.0, with a library JAR, matching POM and SHA256SUMS. The same JAR/POM are published in the QRun-IO Gitea Maven registry. Gitea requires sign-in for downloads. The QBit itself is not published to Maven Central and is not a standalone executable/fat JAR. See installation and release access.

Runtime: Java 21 and QQQ 4.1.0-RC.21, published on Maven Central with native session login. The optional native UI demo uses QQQ frontend 1.0.0-RC.37. Product version 1.1.0, native extension 2.4.0 and wire payload v1 are separate version contracts.

Implemented: single-use enrollment, device credentials, inventory/approval, PDF/ZPL submission, durable JDBC queue, idempotency, claim/content/events, native QQQ setup/test-print processes, read-only device/printer/job tables and a durable enqueue effect target.

Native record/selection actions expose per-device printers, jobs, queue and history, with desired versus observed status. The discovered /qqq/v1/qbit/printing extension uses standard QQQ session authentication. Device credentials are secondary identity proof alongside the current QQQ user's permissions. Existing unbound pairings require explicit association during sign-in. See the authentication contract; bound devices cannot fall back to legacy /printing/v1 authentication.

flowchart LR
  A[QQQ application / durable job] --> B[Printing QBit and JDBC queue]
  C[Windows service] <-->|Client-initiated WSS: commands and status| B
  C -->|Separate outbound HTTPS document streams| B
  C --> D[Windows spooler]
  D --> E[Local laser / Zebra]

See the real-time host integration, screen walkthrough, storage and retention, operations and troubleshooting, contributing, security reporting, changelog, and Apache-2.0 license.

Local development

Start an isolated native QQQ application, MinIO and optional Authentik with the Docker development commands. The default test runs synthetic Java/.NET WebSocket jobs and S3 checks without an installed connector or printer.

Build and verify

Install Java 21. Clone this repository and qqq-print-protocol as siblings from Gitea. Check out protocol commit 42c0d249f81395f00408ec0f92638dcbbf5deb03, the immutable fixture ref in .gitea/workflows/qrun-release.yaml. Use the checked-in wrapper (Maven 3.9.11).

.\mvnw.cmd -Pui-demo verify
.\mvnw.cmd dependency:build-classpath '-Dmdep.outputFile=target/runtime-classpath.txt' '-DincludeScope=runtime'

On Linux/macOS use bash ./mvnw -Pui-demo verify and the corresponding wrapper commands. First builds download dependencies; -o is only appropriate after they are cached.

Checkstyle runs during validate. Tests exercise real H2 persistence, native QQQ metadata/processes and real loopback HTTP through Javalin, including the shared fixtures and withdrawal of a locally exposed printer. Keep ../qqq-print-protocol checked out or pass -Dprinting.protocol.fixtures=<fixture-directory>.

Legacy protocol demo

The new Windows client needs the authenticated local application below, or a QQQ application hosting the native printing extension. This protocol-only demo is for explicitly enabled legacy diagnostics; it does not provide native user sign-in.

Set QQQ_PRINT_ADMIN_TOKEN to a random value of at least 24 characters in the process environment; never put it in source or command arguments. Then:

.\mvnw.cmd exec:java

The demo binds 127.0.0.1:18081, persists H2 at .local/printing, and exposes /printing/v1. Optional QQQ_PRINT_PORT and QQQ_PRINT_JDBC_URL override those defaults. It does not log the administrative token. Local HTTP is for development; terminate TLS for remote connectors.

For a directly managed Java process, classpath is target/classes; followed by target/runtime-classpath.txt, and main class is com.kingsrook.qbits.printing.PrintingDemo.

Native QQQ UI preview

A separate local demo serves the real QQQ frontend with synthetic device/printer/job records. It binds only to 127.0.0.1:18082 and uses mock authentication. Keep it isolated from real pairing data and physical connectors; it is a screen preview, not an authenticated deployment.

.\mvnw.cmd -Pui-demo package dependency:build-classpath '-Dmdep.outputFile=target/ui-classpath.txt' '-DincludeScope=runtime'
$uiClasspath = 'target/classes;' + (Get-Content target/ui-classpath.txt -Raw).Trim()
java -cp $uiClasspath com.kingsrook.qbits.printing.PrintingUiDemo

Open http://127.0.0.1:18082. On Linux/macOS use : as the classpath separator. Set JVM property -Dprinting.ui.port=<port> before the main class to choose another port. This preview is distinct from the credential-protected protocol demo on port 18081; neither replaces deployment-specific authentication and authorization.

Authenticated local application

PrintingLocalHost combines the real QQQ dashboard, native processes, authenticated sign-in and device routes against one durable H2 database. It binds only to loopback, uses QQQ table-based authentication, and starts the native Quartz retention schedule through QApplicationLauncher. Existing pairing and job history are retained when the database and tenant are preserved. Use the pinned H2 2.4.240 runtime; older 2.3.232 file-backed deployments require a closed backup and cold-reopen qualification. See local host setup for explicit credentials, the required frontend/runtime dependencies and a reviewed cutover procedure.

The protocol-only PrintingDemo and synthetic, mock-authenticated PrintingUiDemo remain separate. Do not point the synthetic demo at real paired printer data. The local host retains database document storage until an actual dedicated S3 binding is configured in a deployment-owned application; it does not claim a remote cloud deployment.

The local setup uses the same native authorization and transport paths as an integrated application. Production authentication mappings, S3/IAM and database operation belong to the deploying host; the local fixture does not certify those settings. Dated test evidence is retained in qualification notes.

Add to a QQQ application

Use the authenticated Gitea Maven registry, or download the release JAR, matching POM and SHA256SUMS and verify both files. For a local installation of the downloaded artifacts:

.\mvnw.cmd install:install-file '-Dfile=qbit-printing-1.0.0.jar' '-DpomFile=qbit-printing-1.0.0.pom'

After the artifact is available in your configured repository, add:

<dependency>
   <groupId>com.kingsrook.qbits</groupId>
   <artifactId>qbit-printing</artifactId>
   <version>1.0.0</version>
</dependency>

Supply an existing JDBC DataSource/pool and a QQQ RDBMS backend pointing to that same database. Initial schema is created by initialize(); the implementation uses portable H2/PostgreSQL SQL, but only H2 has been tested so far.

Device and printer names, descriptions and locations are edited in the Windows client and synchronized through heartbeat/inventory. QQQ's native record forms and selectors show the effective friendly name; the physical computer/queue name remains visible in Details. These synchronized fields are read-only in QQQ so two editors do not overwrite each other. Approval remains a separate server control. Friendly names/locations allow 512 characters, descriptions 4096, and physical names/job titles 1024. Clearing a friendly name restores the technical name without changing IDs or routing.

initialize() adds profile columns, backfills physical names, widens existing name/title columns and creates dispatch/history indexes without deleting records. The existing name column becomes the effective display label; computer_name/queue_name retain technical names and display_name holds the optional override. Back up the database and run schema initialization during a maintenance/deployment window; column widening and index creation can lock populated tables.

Job history uses GET /printing/v1/admin/jobs?limit=200&before=<nextCursor> with opaque keyset cursors and a maximum 200 rows per page. Claims read one candidate, pending inspection reads 50, and all these queries project metadata without fetching stored documents. Native dashboard lists already cap attention rows at 10 per category and recent work at 25. Dispatch/history indexes support growing histories; the synthetic 10,000-row regression test checks bounded pages and claim correctness, not a certified 10,000-print/day throughput measurement. Driver speed, document size, database hardware and retention policy still determine capacity. Document cleanup follows the configured retention policy and host schedule; the audit does not run a live purge or delete job history.

PrintingService printing = new PrintingService(dataSource, tenantId);
printing.initialize();
new PrintingQBitProducer().withConfig(new PrintingQBitConfig()
   .withBackendName("appRdbms")
   .withStorageTableName("printing_documents") // existing dedicated S3 storage table
   .withSchedulerName("appScheduler")          // existing host scheduler
   .withService(printing)).produce(qInstance, "printing");
server.withAdditionalRouteProviders(List.of(new PrintingRealtimeRoutes(printing)));

The producer registers the native printing API; the additional provider supplies its WebSocket transport. Preserve any other host route providers and close the printing provider during shutdown. The same service instance must be used for metadata and transport. The service is bound to one tenant; use a separate host service instance for each tenant. Device token verification and job ownership are enforced on every device operation. Management service methods are intended for trusted application code; enforce user/facility authorization before calling them. See real-time integration.

Configure a dedicated private printing bucket for the customer/environment through existing QQQ S3 metadata; no bucket credentials belong in printing forms. Admin retention defaults to 365 days after successful completion. Scheduled cleanup handles bounded batches every five minutes and preserves active, failed and uncertain work plus all receipts/history. Without a configured storage binding, legacy database documents remain supported; without a host scheduler, automatic cleanup does not run. Review storage configuration, bucket isolation and S3 versioning before deployment.

Enrollment uses a named facility selector. Set withFacilityPossibleValueSourceName("your_existing_facility_source") to reuse an already registered host source and its record security. Without it, the standalone choice is Default facility. The backend validates the selection under the current user's permissions before issuing an enrollment code; typing or forging an unknown/foreign-tenant ID cannot bypass that check. Existing stored facility IDs are preserved; configure their original source to resolve their names.

For native QQQ screens, grant printing_admin and populate the session's printing_tenant security key with the tenant ID. The Printing home shows device health, queue status and work needing attention, with Devices, Printers and Print jobs beneath it. It nests under an existing admin app when available. Processes are dashboard/table/record actions, not navigation entries. Secret/token and document columns are excluded from metadata. Table mutations are rejected; use the service/processes instead.

Typical setup: run Enroll a print device from Printing home or Print devices, enroll the Windows connector, and select local queues to expose in its companion/tray. Use Set printer approval to choose a named device and one of its printers. Test-print forms offer only exposed, approved printers supporting the chosen format. Device and printer choices respect the current user's tenant access.

For label printers, configure the machine's Windows Printing Defaults for the actual stock size and print method before testing PDFs through the service. A test profile selects document size and ZPL resolution; it does not change the Windows driver or printer's media configuration. Our 300 dpi Zebra required 4 × 6-inch stock and Thermal Direct instead of its previous 4.09 × 2-inch Thermal Transfer defaults. After correcting those settings and resuming the printer, the user confirmed the built-in PDF label, alongside the already confirmed ZPL label and laser PDF. Barcode scanning and measured alignment remain separate checks; SPOOLED alone is not proof of physical output.

On Printers, select one or more rows or open a printer record to Approve printers, Disable printers, Remove printers, Print a test page, or Print a test label. Standard QQQ review/result screens show the selected scope; preview does not print. Each selected printer is read again under the current user's permissions before execution. Jobs display the related printer and device names; approval, availability, format and status fields have named choices.

Removing is archiving, not deleting. Remove printers sets Removed at on the selected printers, clears their approval and exposure, and takes them out of the operator catalog, claims, the PRINTERS frame and every device/printer selector; their rows and job history stay. The Printers and Print devices tables have no server-side default filter, so add the filter Removed at is empty to the view you use day to day. If the device shares a removed queue again, the next inventory brings it back exposed but not approved, exactly like a newly discovered queue. Remove device (a device action that requires confirmation) revokes the device's access through the existing REVOKE control, then archives the device and all of its printers; an already revoked device is archived without a new command. A removed device cannot be restored; connect the computer again with a fresh enrollment, which creates a new device. Retention cleanup later hard-deletes removed printers and devices that no job references (see storage and retention).

Metadata names follow QBit conventions: tables and data sources use printing_*, while processes/widgets use printing.... The initial namespace is fixed to printing. The QBit preserves the host's QBrandingMetaData; customer identity belongs in the host branding, not a custom QBit build.

Two independent settings govern dispatch: available means the queue is present in the latest locally selected inventory; enabled means a QQQ administrator approved it. New submissions and claims require both. A connector whose HELLO advertises printer-approval-v1 (extension 2.4.0, connector 1.2.0) receives a PRINTERS frame listing { printerId, enabled } for every queue in its latest accepted inventory after each accepted inventory and whenever an approval changes through any path; older connectors never receive it and keep reading the operator catalog. Removing a queue from local selection makes it unavailable without deleting its approval/history; removing a printer in QQQ additionally drops it from the frame until the device shares it again. An empty inventory withdraws all queues. Withdrawing a queue cannot retract a job already accepted by Windows.

Application code calls printing.submit(JSONObject) with the shared SubmitJobRequest fields. A stable business idempotency key returns the same job when retried; changing document/options/target under the same key fails. PDF is sent as bytes encoded once in base64; ZPL is UTF-8 printer-ready bytes. Default decoded limits are 100 MiB for PDF and 20 MiB for ZPL, both configurable in the native Printing connection and limits process. Queue approval and format compatibility are checked before insertion.

JSONObject job = printing.submit(new JSONObject()
   .put("deviceId", deviceId)
   .put("printerId", printerId)
   .put("title", "Packing slip")
   .put("format", "PDF")
   .put("contentBase64", Base64.getEncoder().encodeToString(pdfBytes))
   .put("idempotencyKey", "shipment:" + shipmentId + ":packing-slip:v1")
   .put("options", new JSONObject().put("copies", 1).put("scale", "ACTUAL_SIZE")));
String printJobId = job.getString("jobId");

For terminal and web clients the QBit also offers ready-made print processes: PrintingRecordProcesses.registerAll(instance) (after all host tables are added) puts Print on every readable table, rendering a record sheet from the table's sections and child lists; printingPrintTableList prints a grid of any table (columns, filter, sort, page or all rows, paper and orientation); printingPrintText prints text such as a screen capture. Opt a table out with PrintingTableSettings().withPrintable(false). Each returns the queued job id in printingJobIds. printingRenderToPdf (extension 2.2.0) renders the same documents to a PDF download for the caller instead ("Save as PDF"), and every document carries the app header and footer the client sends (headerApp, headerTitle, headerServer, headerUser, headerPrintedAt, footerLeft, footerNote).

The returned ID identifies durable queue acceptance. Check job status asynchronously. The protocol v1 specification defines HTTP endpoints, JSON fields, integrity checks and lifecycle semantics.

For durable QQQ jobs register PrintEnqueueEffectTarget as an IDEMPOTENT_EXTERNAL target in DurableJobsMetaData, then emit the submit-request payload. The target replaces the payload idempotency key with the durable effect key. Its SENT result means accepted into the queue, not physical output. This implementation uses its own JDBC transaction; do not register it as LOCAL_TX. Authorize the destination in the emitting process before writing the effect. QQQ's durable dispatcher runs with a QSystemUserSession and does not propagate the user's tenant keys. For that trusted outbox path only, the target resolves the service configured on the dispatcher's current QQQ instance; that service fixes the tenant and rejects devices belonging to another tenant. The payload cannot choose a service or tenant. A cached target resolves the current instance on every call. Inline user calls retain the printing_tenant scope check, and native management processes (including enrollment) still require both administrator permission and matching tenant scope. Do not expose direct effect emission through an arbitrary user-supplied target/payload endpoint.

Deployment boundaries

Jobs target an explicit device/printer. Logical destination routing and a distributed real-time bus belong to the integrating host. The release includes cancellation of queued jobs, explicit reprints, owned spool-cancellation commands, durable receipt reconciliation, S3 delivery storage and scheduled retention. Received/submitting jobs are not automatically offered again; SPOOLED never asserts physical delivery.

Job paper (paperWidthMm and paperHeightMm, both positive and at most 2000, never combined with mediaProfile) is supported by the matching Windows 1.0.1 connector. The built-in 4 × 1 and 4 × 2 PDF tests use 101.6 × 25.4 / 50.8 mm. Without explicit paper or a reported media selection, jobs follow the Windows queue default. See media selection.

The protocol demo and synthetic native UI preview are separate local hosts. Attaching the QBit to a deployed application requires that host's real authentication, facility policy, database, storage and supervision. PostgreSQL and multiple QQQ nodes are not qualified by the local H2/MinIO fixture.

Physical output

The user confirmed physical laser PDF and Zebra ZPL/PDF output during development, including a 4 × 1 test. A configured stock profile does not prove a physical result on another printer or stock; inspect the first output after changing media. Historical acceptance records remain in session history.

CI and releases

Gitea is the source, pull-request, CI and release authority. The thin .gitea/workflows/qrun-release.yaml delegates to the protected qrun-release-v1 channel with Java 21, the Maven wrapper, ui-demo and the immutable protocol fixture checkout. Required verify, secrets and gate checks precede merge; successful main builds capture the tested Maven reactor for the trusted registry publisher. Maintainers publish the corresponding tagged Gitea release from those verified bytes. See release operations.

GitHub is a shadow/archive. Its Actions were retired for this repository on 2026-11-04; checked-in .github workflows describe the former pipeline and do not publish current printing releases. The separate Windows repository retains its signing/update-feed workflow. The 1.0.0 server release does not claim GitHub attestations or Maven Central publication. The JAR includes LICENSE, NOTICE and font/diagnostic asset licenses; publication does not deploy a QQQ host or print to hardware.

Printing home

The admin-only Printing home uses stock QQQ widgets: a purpose/scope/help header, actionable exceptions, four health tiles, and a full-width pending/recent work list. Counts and record links use QQQ queries under the viewer's tenant. Device disconnection means no heartbeat for 90 seconds; job failure tiles cover jobs created in the past 24 hours. Older failures remain in Print jobs. An empty installation explains enrollment and printer setup.

The home uses the standard QQQ frontend widgets and scoped queries. Its timed dashboard refresh updates the visible summary; job delivery uses the independent real-time WebSocket and does not wait for a dashboard refresh. No custom frontend or HTML dashboard is required.

Operator actions

The Windows companion can request test prints and explicit reprints through device-scoped authenticated routes. The same printer approval/exposure rules apply. Upgrade the QBit and connector together for these additive protocol features. Reprints create a new job; uncertain/spooled originals require duplicate-output consent and remain unchanged.

QQQ's Print jobs table and records expose Print again and Cancel queued jobs using native selection/review/result screens. Cancellation is allowed only before dispatch; later work must be inspected in the Windows queue. The actions require printing administration permission and the configured tenant context.

Warehouse diagnostic pages

All built-in printer test actions share PrintingTestDocuments: QQQ branding, the official Query dog mascot, large warehouse-style identifiers and embedded JetBrains Mono Bold with a dotted zero and distinct 1/I/l. Query's original artwork and aspect ratio are preserved in PDF and thermal output; asset provenance ships with the resource. The font is included under SIL OFL 1.1; its license and provenance ship in the JAR. PDF embeds a font subset. ZPL renders the same glyphs into document-local, row-aligned graphics below Zebra's 99,999-byte per-field limit; it does not download or save a printer font.

Native record and selected-table actions expose a possible-value stock/resolution selector. Letter and A4 are PDF-only. Label profiles cover 4 × 1, 4 × 2 and 4 × 6 inches at 203, 300 or 600 dpi, with corresponding PDF dimensions of 101.6 × 25.4, 50.8 or 152.4 mm. Select matching loaded stock and printer resolution. Bulk actions apply the chosen profile to every selected destination, so group compatible printers. The document does not change the Windows queue defaults, calibrate the printer or save device configuration; ZPL width/length are transient format settings. Direct-thermal stock needs the driver's Direct Thermal setting, without a ribbon.

The device test-print API accepts optional testProfile with the same choices. Existing clients default to Letter PDF or 203-dpi 4 × 6 ZPL. Invalid profiles fail before enqueueing. Test actions use the ordinary durable queue and existing printer exposure/approval rules; discovery never automatically prints. A ZPL barcode, glyph comparisons and size/alignment marks support physical inspection. Rendering previews and spool acceptance do not prove paper or scanner quality.

S
Description
QQQ 4.1+ cloud-printing QBit: devices, printers, durable jobs, and native administration
https://github.com/QRun-IO/qbit-printing#readme Readme
2.5 MiB
0 Stars 3 Watchers 0 Forks
2026-11-06 20:46:30 +00:00
Languages
Java 87.4%
C# 4.5%
PowerShell 4.3%
Python 3.7%
Shell 0.1%