Compare commits

...
21 Commits
Author SHA1 Message Date
james.maes 40089937bf Merge pull request 'fix(jobs): store the print job creation instant as a real date-time (created_time)' (#38) from fix/print-job-created-date-time into main
build / maven (push) Successful in 3m13s
maven / build (push) Successful in 3m13s
2026-11-11 14:09:21 +00:00
james.maesandClaude Fable 5.2 d9f0c357a7 fix(jobs): store the print job creation instant as a real date-time (created_time)
build / maven (push) Successful in 2m14s
maven / build (push) Successful in 2m14s
printing_job.created_at is ISO-8601 text and its QQQ field is a string, so QQQ
could not apply a relative date filter ("created in the last day"), a date sort
or a date bucket to print jobs.

- printing_job gains created_time, a TIMESTAMP without time zone holding UTC
  (the QQQ date-time convention), written by the same INSERT from the same
  instant as created_at.
- The job table exposes it as the read-only DATE_TIME field "Created"; it
  replaces the text field in the default columns and Summary section. The text
  field stays (relabelled "Created at (ISO text)") because the queue order,
  history cursors, admission seeding, the Printing home and the job API read it.
- initialize() adds the column, backfills existing rows from created_at and
  creates printing_job_created_time_idx (tenant_id, created_time, id).
- A host that owns the schema must add the column first (PostgreSQL statements
  in CHANGELOG.md). Without it every submission is rolled back with the
  missing-column error and QQQ cannot read the table; nothing is stored silently.

No wire or contract change (extension 2.4.0, wire v1).

Tests: PrintingJobCreatedTimeTest (5 tests; all fail without the change).
./mvnw -B verify -Pui-demo: 209 tests, 0 failures, 0 errors, 1 skipped
(opt-in workload test), 0 Checkstyle violations.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 07:21:31 -06:00
james.maes bd08d03654 Merge pull request 'build(deps): align with Gitea releases (parent 2.2.1, QQQ 4.1.0-RC.25)' (#35) from deps/align-gitea into main
build / maven (push) Successful in 5m31s
maven / build (push) Successful in 5m31s
2026-11-07 17:18:31 +00:00
james.maesandClaude Opus 5.5 107a566891 build(deps): align with Gitea releases (parent 2.2.1, QQQ 4.1.0-RC.25)
build / maven (push) Successful in 4m41s
maven / build (push) Successful in 4m41s
pom.xml: parent -> 2.2.1
pom.xml: qqq.version: 4.1.0-RC.22 -> 4.1.0-RC.25
pom.xml: qqq-frontend-next: 1.0.0-RC.37 -> 1.0.0-RC.38

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 17:09:59 +00:00
james.maes 31e36b0b5b Merge pull request 'docs(release): record QQQ Printing 1.1.2 as the current stable library' (#34) from docs/release-1.1.2-20261106 into main
QRun-IO/ci-policy: native / dotnet-0 (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-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (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 / ci-result (push) Successful in 15s
build / maven (push) Successful in 5m49s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
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: native / native-result (push) Successful in 9s
QRun-IO/ci-policy: ci / java-docker-4 (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 / original-business (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 14s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (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 / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 5m49s
2026-11-06 20:47:58 +00:00
james.maesandClaude Fable 5.1 c2f6fc300a docs(release): record QQQ Printing 1.1.2 as the current stable library
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
build / maven (push) Successful in 7m16s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Successful in 7m16s
1.1.2 = the 1.1.1 code on QQQ 4.1.0-RC.22 from the Gitea registry, built and
published by the shared ci-workflows pipeline (push build 6435, tag build 6475).
README, RELEASES, REALTIME and SESSION-STATE point at it with the verified hashes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 14:45:40 -06:00
james.maes f5639a98be Merge pull request 'build: QQQ 4.1.0-RC.22 from the Gitea registry; shared ci-workflows Maven pipeline; 1.1.2' (#33) from ci/gitea-registry-rc22-20261106 into main
QRun-IO/ci-policy: native / dotnet-0 (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-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (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 / ci-result (push) Successful in 13s
build / maven (push) Successful in 9m36s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 13s
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 / java-docker-4 (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 / original-business (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 17s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (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 / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 9m36s
2026-11-06 20:18:16 +00:00
james.maesandClaude Fable 5.1 c31a8f5f33 docs: frontend-next 1.0.0-RC.37 is served from the Gitea registry
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
build / maven (push) Successful in 6m52s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 6m52s
Mirrored byte-for-byte from Maven Central (verified against Central's SHA-1 and read
back from the registry), so the ui-demo profile resolves from Gitea too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 13:32:16 -06:00
james.maesandClaude Fable 5.1 8d5b72302c test: pin the vendored protocol fixtures README to protocol 1.1.0 (5005f3c)
The copied files are byte-identical between 42c0d249 and 5005f3c9; only the
documented pin moves, matching the released protocol the server pairs with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 13:32:16 -06:00
james.maesandClaude Fable 5.1 3c39a6af1b build: QQQ 4.1.0-RC.22 from the Gitea registry; shared ci-workflows Maven pipeline; 1.1.2
Everything moves to the QRun-IO Gitea Maven registry. QQQ 4.1.0-RC.22 exists only
there, and the protected qrun-release channel resolves only Maven Central, so the
repository now builds on the shared KofTwentyTwo/ci-workflows maven workflow (@v1):
every push runs -Pui-demo verify against the registry with the job token; a vX.Y.Z
tag tests once and deploys the JAR/POM under that version. The qrun-release caller
and its trusted-publisher path are retired for this repository; PrintingContractTest
keeps using the vendored protocol fixtures. No printing contract change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 13:32:15 -06:00
james.maes c761eca970 Merge pull request 'ci: use shared ci-workflows Maven workflow' (#31) from ci/ci-workflows into main
QRun-IO/ci-policy: native / dotnet-0 (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-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (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 / ci-result (push) Waiting to run
build / maven (push) Successful in 5m55s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (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 / original-business (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 10s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (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 / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 5m55s
2026-11-06 18:02:15 +00:00
james.maes f1ed178355 Merge pull request 'docs(release): record QQQ Printing 1.1.1 as the current stable library' (#32) from docs/release-1.1.1-20261106 into main
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI / secrets (push) Successful in 39s
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-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (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 / ci-result (push) Successful in 10s
Shared Java Maven CI / verify (push) Successful in 6m12s
qrun-release / postpublish (push) Successful in 30s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 12s
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 / java-docker-4 (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 / original-business (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 12s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (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 / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
Shared Java Maven CI / gate (push) Successful in 16s
qrun-release / release (push) Successful in 6m45s
Shared QRun source version / version (push) Successful in 29s
2026-11-06 16:36:22 +00:00
james.maesandClaude Fable 5.1 f10cc03d9e docs(release): record QQQ Printing 1.1.1 as the current stable library
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 44s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
Shared Java Maven CI / verify (push) Successful in 8m7s
qrun-release / postpublish (push) Successful in 40s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 34s
qrun-release / release (push) Successful in 8m43s
Shared QRun source version / version (push) Successful in 40s
README, RELEASES, REALTIME and SESSION-STATE now point at 1.1.1 (source 2a56181,
main run 5976, reactor artifact 4426, release 1381) with the verified JAR/POM hashes,
and mark 1.1.0 as superseded by the realtime upgrade header-case fix. The release
caller is untouched, so the publisher enrollment hash stays valid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 10:33:25 -06:00
james.maesandClaude Opus 5.5 454410081d ci: use shared ci-workflows Maven workflow
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 18s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 4m55s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 19s
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Successful in 5m18s
build / maven (push) Successful in 5m1s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 8s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Successful in 5m1s
Replace the platform-ci release callers with one caller of
KofTwentyTwo/ci-workflows maven.yaml (qqq-orb port).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:51:08 +00:00
james.maes 2a561815fc Merge pull request 'fix(realtime): resolve the device on the authorized upgrade (lowercase headers); 1.1.1' (#30) from fix/realtime-header-case-20261106 into main
QRun-IO/ci-policy: native / dotnet-0 (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-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (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 / ci-result (push) Successful in 6s
Shared Java Maven CI / secrets (push) Successful in 38s
Shared Java Maven CI / verify (push) Successful in 6m56s
qrun-release / postpublish (push) Successful in 43s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 7s
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
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 / java-docker-4 (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 / original-business (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 31s
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 4m8s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (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 / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 13s
QRun-IO/ci-policy: ci / java-maven-7 (push) Successful in 4m28s
Shared Java Maven CI / gate (push) Successful in 23s
qrun-release / release (push) Successful in 7m28s
Shared QRun source version / version (push) Successful in 43s
2026-11-06 15:30:59 +00:00
james.maesandClaude Fable 5.1 e7721bdf90 fix(realtime): resolve the device on the authorized upgrade so lowercase headers connect; 1.1.1
Shared Java Maven CI / secrets (push) Successful in 21s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 20s
Shared Java Maven CI / verify (push) Successful in 4m14s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 3m51s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 13s
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Successful in 4m8s
qrun-release / postpublish (push) Successful in 27s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 15s
qrun-release / release (push) Successful in 4m32s
Shared QRun source version / version (push) Successful in 27s
The WebSocket callbacks read X-QQQ-Device-Id from the socket context, whose
header lookup is case-sensitive, while the HTTP upgrade matches names
case-insensitively. Behind an ingress that lowercases header names the device
resolved to null on connect and channels.put threw before WELCOME (Forge's
adoption test of released 1.1.0; Forge had carried a local patch on 1.0.0).
Resolve the device once after authorization on the upgrade and carry it as a
connection attribute for connect, message and close. The new
lowercaseUpgradeHeadersReachWelcomeAndFrames test fails on the previous code
with the same NullPointerException and passes here. No contract change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 08:24:44 -06:00
james.maes c197c5acab Merge pull request 'docs(release): record QQQ Printing 1.1.0 and pin protocol fixtures to 1.1.0' (#29) from docs/release-1.1.0-20261105 into main
Shared Java Maven CI / secrets (push) Successful in 26s
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-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (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 / ci-result (push) Successful in 8s
Shared Java Maven CI / verify (push) Successful in 5m34s
qrun-release / postpublish (push) Successful in 31s
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 11s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 36s
QRun-IO/ci-policy: native / native-result (push) Successful in 10s
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 / java-docker-4 (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 / original-business (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 4m55s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (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 / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 17s
QRun-IO/ci-policy: ci / java-maven-7 (push) Successful in 5m59s
qrun-release / release (push) Successful in 5m54s
Shared QRun source version / version (push) Successful in 31s
Shared Java Maven CI / gate (push) Successful in 13s
2026-11-06 14:16:35 +00:00
james.maesandClaude Fable 5.1 ba7fdc23d0 docs(realtime): connector 1.2.1
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Successful in 4m2s
Shared Java Maven CI / secrets (push) Successful in 20s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 3m43s
Shared Java Maven CI / verify (push) Successful in 3m45s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
qrun-release / postpublish (push) Successful in 14s
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 23s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 14s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 7s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 8s
qrun-release / release (push) Successful in 3m58s
Shared QRun source version / version (push) Successful in 14s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:55:28 -06:00
james.maesandClaude Fable 5.1 ab51f8da06 docs: the paired Windows connector is 1.2.1 (1.2.0 was never published)
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 28s
Shared Java Maven CI / verify (push) Successful in 3m57s
qrun-release / postpublish (push) Successful in 15s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 4m45s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Successful in 4m57s
Shared Java Maven CI / secrets (push) Successful in 18s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Canceled after 0s
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 10s
qrun-release / release (push) Successful in 4m8s
Shared QRun source version / version (push) Successful in 15s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:54:57 -06:00
james.maesandClaude Fable 5.1 50d643d959 docs(readme): released 1.1.0, install snippets and the 1.2.1 connector pairing
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 17s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Canceled after 0s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Canceled after 27s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Canceled after 34s
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Canceled after 33s
Shared Java Maven CI / verify (push) Successful in 3m49s
qrun-release / postpublish (push) Successful in 17s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 6s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Canceled after 0s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 11s
qrun-release / release (push) Successful in 4m3s
Shared QRun source version / version (push) Successful in 17s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:54:20 -06:00
james.maesandClaude Fable 5.1 102fde779c docs(release): record QQQ Printing 1.1.0 and pin protocol fixtures to 1.1.0
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
Shared Java Maven CI / secrets (push) Failing after 0s
QRun-IO/ci-policy: ci / Scoped workflow [workflow d20a578c7d818bc7ca32a2ac2c675da4e4f27385] (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
Shared Java Maven CI / verify (push) Successful in 5m27s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Waiting to run
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
Shared Java Maven CI / gate (push) Failing after 13s
qrun-release / release (push) Failing after 5m49s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-05 17:09:05 -06:00
15 changed files with 561 additions and 61 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# Shared QRun Maven CI/CD (KofTwentyTwo/ci-workflows): every push verifies; vX.Y.Z tags publish the
# tested JAR/POM to the QRun-IO Gitea Maven registry. QQQ resolves from that registry with the job token.
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
with:
maven-args: -Pui-demo
secrets: inherit
-26
View File
@@ -1,26 +0,0 @@
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
name: qrun-release
on:
push: {}
workflow_dispatch: {}
permissions:
contents: read
actions: read
jobs:
release:
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-maven.yaml@qrun-release-v1
with:
runner: linux-small
maven-profiles: ui-demo
fixture-repository: QRun-IO/qqq-print-protocol
fixture-ref: 42c0d249f81395f00408ec0f92638dcbbf5deb03
fixture-path: qqq-print-protocol
postpublish:
if: ${{ needs.release.outputs.capture == 'true' }}
needs: release
permissions:
contents: write
actions: write
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
with:
runner: linux-small
+62 -1
View File
@@ -1,10 +1,71 @@
# Changelog
## Unreleased (1.2.0)
Print jobs have a real creation date-time. No wire or contract change (extension 2.4.0, wire
v1, subprotocol unchanged); connectors need no update. **Schema change: a host that owns the
printing schema must add one column before deploying this version** (below).
- `printing_job.created_at` is ISO-8601 text and its QQQ field is a string, so QQQ could not
apply a relative date filter ("created in the last day"), a date sort or a date bucket to
print jobs. The table gains `created_time`, a `TIMESTAMP` without time zone holding UTC
(the QQQ date-time convention), written by the same `INSERT` from the same instant as
`created_at`. It is the read-only QQQ field **Created** (`created_time`, `DATE_TIME`) and
replaces the text field in the Print jobs default columns and Summary section.
- `created_at` is unchanged: the queue order, history cursors, admission seeding, the Printing
home and the `createdAt` value of the job API still read it. Its QQQ field stays in Details,
relabelled **Created at (ISO text)**.
- `initialize()` adds the column (`ADD COLUMN IF NOT EXISTS`), backfills every existing row
from `created_at` on each start (so a row written by an older node during a rolling restart
is repaired) and creates `printing_job_created_time_idx (tenant_id, created_time, id)`.
- A host that owns the schema (it never calls `initialize()`) appends the equivalent to its
own migrations. For PostgreSQL, in one transaction:
```sql
ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS created_time TIMESTAMP WITHOUT TIME ZONE;
UPDATE printing_job SET created_time = (created_at::timestamptz AT TIME ZONE 'UTC') WHERE created_time IS NULL;
ALTER TABLE printing_job ALTER COLUMN created_time SET DEFAULT (now() AT TIME ZONE 'UTC');
ALTER TABLE printing_job ALTER COLUMN created_time SET NOT NULL;
CREATE INDEX IF NOT EXISTS printing_job_created_time_idx ON printing_job(tenant_id, created_time, id);
```
The default and `NOT NULL` are optional hardening for a rolling deployment: a node still on
an older library omits the column, and the default gives its rows the database clock in UTC
instead of a null that date filters would skip. The library itself needs only the nullable
column and always writes it.
- On a schema without the column this version fails loudly and stores nothing: every
submission is rolled back with the database's missing-column error for `created_time`, and
QQQ cannot read the Print jobs table. Apply the schema step first; the previous library
keeps working on the new schema.
## 1.1.2
Runtime and publication move to the QRun-IO Gitea Maven registry; no printing contract
change (extension 2.4.0, wire v1, subprotocol unchanged). The server now runs on QQQ
**4.1.0-RC.22**, published only in the Gitea registry (it carries the session-logout,
process-identity and scoped-session fixes the Forge cohort depends on). CI moves from the
`qrun-release` caller and trusted publisher to the shared `ci-workflows` Maven workflow,
which resolves QQQ from the Gitea registry with the job token and publishes `vX.Y.Z` tags
straight to it. The ui-demo frontend (`qqq-frontend-next` 1.0.0-RC.37) was mirrored into the
registry byte-for-byte from Central, so every QRun artifact resolves from Gitea; third-party
dependencies still resolve through the Maven Central mirror until the registry proxies them.
Same test gate (`-Pui-demo verify`).
## 1.1.1
Realtime upgrade fix; no contract change. The WebSocket callbacks read the device
identifier from the socket context's headers, which are matched case-sensitively, while the
HTTP upgrade matches them case-insensitively. Behind an ingress that lowercases header names
the device resolved to null on connect and the connection failed before WELCOME (Forge had
carried a local patch for this on 1.0.0). The device is now resolved once on the authorized
upgrade and carried on the connection; a regression test connects with lowercase header
names and verifies WELCOME, an acknowledged inventory, the PRINTERS push and socket replacement.
## 1.1.0
Native printing extension **2.4.0** (additive; wire payload v1 and the agent
subprotocol `qqq-printing.v1` are unchanged). Pairs with protocol 1.1.0 and Windows
connector 1.2.0; 1.0.x connectors keep working unchanged.
connector 1.2.1; 1.0.x connectors keep working unchanged.
- New HELLO and HTTP control-poll capability `printer-approval-v1` (HELLO accepts up to
five capabilities, the control poll up to four; it is stored in the device's control
+10 -8
View File
@@ -1,10 +1,10 @@
# QQQ Printing
Cloud printing for QQQ 4.1+, paired with the [Windows AppKit connector](https://git.kof22.com/QRun-IO/qqq-print-agent) 1.2.0 and the [shared protocol](https://git.kof22.com/QRun-IO/qqq-print-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.
Cloud printing for QQQ 4.1+, paired with the [Windows AppKit connector](https://git.kof22.com/QRun-IO/qqq-print-agent) 1.2.1 and the [shared protocol](https://git.kof22.com/QRun-IO/qqq-print-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](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.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](docs/RELEASES.md).
**Released:** [QQQ Printing 1.1.2](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.2), with a library JAR, matching POM and `SHA256SUMS`, built on QQQ 4.1.0-RC.22 from the Gitea registry ([1.1.1](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.1) is the same code on RC.21 from Maven Central; [1.1.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.0) is superseded by the realtime upgrade fix; [1.0.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.0.0) remains available for rollback). 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](docs/RELEASES.md).
**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.
**Runtime:** Java 21 and QQQ **4.1.0-RC.22**, published in the QRun-IO Gitea Maven registry (not on Maven Central) with native session login. The optional native UI demo uses QQQ frontend **1.0.0-RC.37**, also served from that registry. Product version 1.1.2, 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.
@@ -29,7 +29,7 @@ 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).
Install Java 21. Clone this repository and `qqq-print-protocol` as siblings from Gitea. Check out protocol commit `5005f3c9e27053b6c327bb923695012832c0fdcc`, the immutable fixture ref in `.gitea/workflows/qrun-release.yaml`. Use the checked-in wrapper (Maven 3.9.11).
```powershell
.\mvnw.cmd -Pui-demo verify
@@ -79,7 +79,7 @@ The local setup uses the same native authorization and transport paths as an int
Use the [authenticated Gitea Maven registry](docs/RELEASES.md#use-the-maven-registry), or download the release JAR, matching POM and `SHA256SUMS` and verify both files. For a local installation of the downloaded artifacts:
```powershell
.\mvnw.cmd install:install-file '-Dfile=qbit-printing-1.0.0.jar' '-DpomFile=qbit-printing-1.0.0.pom'
.\mvnw.cmd install:install-file '-Dfile=qbit-printing-1.1.2.jar' '-DpomFile=qbit-printing-1.1.2.pom'
```
After the artifact is available in your configured repository, add:
@@ -88,7 +88,7 @@ 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>
<version>1.1.2</version>
</dependency>
```
@@ -98,6 +98,8 @@ Device and printer names, descriptions and locations are edited in the Windows c
`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.
Print jobs carry their creation instant twice: `created_at` is the ISO-8601 text the queue, history cursors and job API use, and `created_time` is the same instant as a UTC `TIMESTAMP` (no time zone, the QQQ convention), exposed as the date-time field **Created** so native filters ("created in the last day"), sorting and date buckets work. `initialize()` adds the column, backfills existing rows from `created_at` and creates `printing_job_created_time_idx (tenant_id, created_time, id)`. A host that owns the schema must add the column before deploying this version (see the [changelog](CHANGELOG.md) for the statements): without it every submission fails with the database's missing-column error and stores nothing, and the Print jobs table cannot be read.
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.
```java
@@ -129,7 +131,7 @@ Removing is archiving, not deleting. **Remove printers** sets **Removed at** on
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.
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.1) 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.
@@ -167,7 +169,7 @@ The user confirmed physical laser PDF and Zebra ZPL/PDF output during developmen
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](docs/RELEASES.md#maintainer-release-path).
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.
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 and 1.1.0 server releases do 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
+2 -2
View File
@@ -1,8 +1,8 @@
# Real-time printing host integration
QQQ Printing 1.1.0 (the released library is [1.0.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.0.0))
QQQ Printing 1.1.2 (the released library is [1.1.2](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.2))
implements native printing extension **2.4.0** (wire envelope v1), paired with Windows
connector **1.2.0**; connector 1.0.1 remains compatible. Upgrade the server before the connector; a real-time connector
connector **1.2.1**; connector 1.0.1 remains compatible. Upgrade the server before the connector; a real-time connector
rejects a server that advertises only extension 1.x. There is no timed job/control
polling fallback.
+16 -8
View File
@@ -1,6 +1,6 @@
# Installation and release operations
The stable server library is [QQQ Printing 1.0.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.0.0), paired with protocol 1.0.0 and Windows connector 1.0.1. It requires Java 21 and the QQQ 4.1.0-RC.21 dependency tuple. Its native printing extension is 2.3.0; the WebSocket subprotocol remains `qqq-printing.v1`.
The stable server library is [QQQ Printing 1.1.2](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.2) (1.1.1 is the same code on QQQ RC.21 from Maven Central; 1.1.0 is superseded: its realtime upgrade fails behind an ingress that lowercases header names), paired with protocol 1.1.0 and Windows connector 1.2.1 (1.0.x/1.1.x connectors keep working on the unchanged 2.3 behaviour). It requires Java 21 and the QQQ 4.1.0-RC.22 dependency tuple, which is published only in the QRun-IO Gitea Maven registry. Its native printing extension is 2.4.0; the WebSocket subprotocol remains `qqq-printing.v1`. The previous stable library, [1.0.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.0.0), remains available for rollback.
Gitea is primary for source, pull requests, CI, packages and server releases. This forge requires sign-in even for download links on public repositories. An HTML sign-in page is not an artifact. Use your existing approved forge identity; never commit its token or put it in a command line.
@@ -18,11 +18,11 @@ Configure the consuming application's existing Maven repository setup with repos
</servers>
```
Merge this entry into the existing settings; preserve other servers, mirrors and profiles. Supply the environment through your approved credential loader. The credential needs package read access. Add `com.kingsrook.qbits:qbit-printing:1.0.0` as shown in the [README](../README.md#add-to-a-qqq-application). QQQ's RC.21 dependencies resolve from Maven Central; the printing 1.0.0 library resolves from Gitea.
Merge this entry into the existing settings; preserve other servers, mirrors and profiles. Supply the environment through your approved credential loader. The credential needs package read access. Add `com.kingsrook.qbits:qbit-printing:1.1.1` as shown in the [README](../README.md#add-to-a-qqq-application). QQQ 4.1.0-RC.22 and the printing library both resolve from this registry; third-party dependencies still come from the Maven Central mirror until the registry proxies them.
## Download and verify release files
Sign in to Gitea and download all three assets from the stable release: `qbit-printing-1.0.0.jar`, `qbit-printing-1.0.0.pom` and `SHA256SUMS`. In their download directory:
Sign in to Gitea and download all three assets from the stable release: `qbit-printing-1.1.2.jar`, `qbit-printing-1.1.2.pom` and `SHA256SUMS`. In their download directory:
```powershell
Get-Content ./SHA256SUMS | ForEach-Object {
@@ -37,11 +37,19 @@ On Linux, use `sha256sum -c SHA256SUMS`; on macOS, use `shasum -a 256 -c SHA256S
## Maintainer release path
1. Update the POM version and active documentation on a branch in Gitea. Keep the immutable protocol fixture ref in `.gitea/workflows/qrun-release.yaml` aligned with the reviewed contract.
2. Open a normal pull request and satisfy required CI checks. The caller delegates to the protected `qrun-release-v1` channel; shared CI changes belong to its owner, not an application-specific bypass.
3. Merge normally and wait for the successful main run. Its `maven-reactor` artifact contains tested JAR/POM bytes, a source commit, manifest and checksums. The trusted publisher verifies that evidence before publishing immutable Maven packages. A passing build alone does not prove registry publication.
4. Verify the exact version's registry JAR/POM against the captured bytes. Create `v<POM version>` at the qualified source without moving an existing tag. Create a draft Gitea release, attach the same JAR/POM and a flat `SHA256SUMS`, read them back with the approved forge identity, then publish. Reconcile identical existing artifacts; never replace different bytes under a released version.
5. Record the source, successful run, artifact, release and verified hashes. Publication is separate from installing a host or accepting physical printer output.
Since 1.1.2 the repository builds on the shared `KofTwentyTwo/ci-workflows` Maven workflow (`.gitea/workflows/build.yaml`, pinned to `@v1`): every push runs `-Pui-demo verify`; a `vX.Y.Z` tag runs the same tests once and deploys the JAR/POM to the QRun-IO Gitea Maven registry under exactly that version. The former caller's pinned protocol checkout is gone; `PrintingContractTest` reads the vendored copy in `src/test/resources/protocol-fixtures` (kept byte-identical to the reviewed protocol commit named in its README), which is also what local `verify` uses. Earlier releases (1.0.0, 1.1.0, 1.1.1) went through the protected `qrun-release-v1` channel and the trusted publisher; their evidence below stands.
1. Update the POM version (the tag must equal it), `qqq.version` if the runtime moves, and active documentation on a branch in Gitea. Open a normal pull request; the push build must be green.
2. Merge normally and wait for the green main build (verify only; main never publishes).
3. Create `v<POM version>` at the qualified main commit without moving an existing tag and push it. The tag build tests and deploys `com.kingsrook.qbits:qbit-printing:<version>` to the registry; a tag build that fails publishes nothing and the next attempt is the next patch version (tags are immutable).
4. Read the registry JAR/POM back with the approved forge identity, create the Gitea release on the tag, attach the same JAR/POM and a flat `SHA256SUMS`, read them back, then publish. Never replace different bytes under a released version.
5. Record the source, tag build, release and verified hashes here. Publication is separate from installing a host or accepting physical printer output.
For 1.1.2, source `f5639a98be9a2f15d4c61cf0955fe4ecef85824f` (main; merge of PR 33: QQQ 4.1.0-RC.22 from the Gitea registry, shared ci-workflows pipeline), push build `6435` (`-Pui-demo verify`, 200 tests, 0 failures, 0 errors, 1 skipped, Checkstyle clean), tag build `6475` (tests again, deploy to the registry), release `v1.1.2`. Registry files and release assets match byte-for-byte on authenticated readback. The JAR SHA-256 is `786e6e570c5df29b01529562efb246a4f4562bb1d9fff6bea77d264e0d7536ce`; the POM SHA-256 is `7691a4e1cb7b03c7a559b0e2b92eacd92f86545b98bddafac86fc70f8602b03d`. RC.22 is not on Maven Central, so the green builds prove resolution from the registry.
For 1.1.1, source `2a561815fcec8b7c95e76bb4b36fba314f512027` (main; merge of PR 30, the realtime upgrade header-case fix), main run `5976` (verify, secrets scan, gate and post-publish succeeded), reactor artifact `4426`. The trusted publisher first refused the run because the 1.1.0 documentation merge had re-pinned the protocol fixture in `qrun-release.yaml`, changing the caller hash its enrollment verifies; the enrollment was re-reviewed in cluster-gitops (PRs 283 and 285) and the capture was then published unchanged. Registry files and release assets match the capture byte-for-byte. The JAR SHA-256 is `156720da6928f62d77101521a2953f6e1026d58d02e7fc7815a8c8fcbcedaefa`; the POM SHA-256 is `2fa451260a26f5b5ede30800d46c78050e223694d3d8dda640f9ca875d120ab0`.
For 1.1.0, source `eb2c16785d0723a47d4a22c68b8b04503108fc58` (main; merges of PR 25 printer approval and PR 28 printer/device removal), main run `5473` (verify, secrets scan, gate and post-publish all succeeded after the CI quota incident of the same afternoon), reactor artifact `4306`, release assets read back by the approved forge identity and matching the registry files byte-for-byte. The JAR SHA-256 is `3a51b7e7cf457cce5db16f138c4cc65b901730bda17ccaf1d3c714cf11d2bbf4`; the POM SHA-256 is `52e5a588e788bbe123dad574455493aea0a7df6cc473c78f15be09343048a5d4`.
For 1.0.0, source `2906c2bd5049924abd2114c8ef51a0ac9aac325e`, main run `3594`, reactor artifact `2741` and release `1356` were verified. Release assets and registry files matched byte-for-byte on authenticated readback. The JAR SHA-256 is `7ecb00e1f49272d6e885dc5f515bd4832aa52dd84e0613d9b6751f9e235a0961`; the POM SHA-256 is `8be7c30208a7fbb875dcebedb0ba6baf0e172aa0e7cf478a3d2863d1316ddf5f`.
+11 -5
View File
@@ -1,10 +1,16 @@
# Session state — 2026-11-05
# Session state — 2026-11-06
## In progress — 2026-11-05: printer approval over realtime (1.1.0 / extension 2.4.0)
## Current — 1.1.2 released (2026-11-06)
Branch `feat/printer-approval-20261105` adds the `printer-approval-v1` capability and the `PRINTERS` frame
(see [CHANGELOG](../CHANGELOG.md) 1.1.0 and [real-time integration](REALTIME.md)). Product version is bumped to
1.1.0 in the POM; nothing is tagged, released or published, and the released state below is unchanged.
[QQQ Printing 1.1.2](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.2) is published in the QRun-IO Gitea Maven registry (`com.kingsrook.qbits:qbit-printing:1.1.2`) and as a Gitea release. It is the 1.1.1 code built on QQQ **4.1.0-RC.22**, which exists only in that registry (James's direction: everything moves to Gitea, no more Maven Central), on the shared `ci-workflows` Maven pipeline (`build.yaml@v1`, `-Pui-demo`; the protected `qrun-release` caller and the trusted-publisher enrollment are retired, cluster-gitops PR 288). `qqq-frontend-next` 1.0.0-RC.37 was mirrored into the registry byte-for-byte from Central so the ui-demo profile resolves from Gitea too; only third-party libraries still come through the Central mirror. Source `f5639a98be9a2f15d4c61cf0955fe4ecef85824f` (PR 33), push build 6435 (200 tests, 0 failures), tag build 6475 (deploy); JAR `786e6e570c5df29b01529562efb246a4f4562bb1d9fff6bea77d264e0d7536ce`, POM `7691a4e1cb7b03c7a559b0e2b92eacd92f86545b98bddafac86fc70f8602b03d`. No printing contract change.
## Superseded — 1.1.1 released (2026-11-06)
[QQQ Printing 1.1.1](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.1) is published on primary Gitea and its Maven registry (`com.kingsrook.qbits:qbit-printing:1.1.1`). It supersedes 1.1.0: Forge's adoption qualification of 1.1.0 (`~/.cache/gg-forge-printing-cohort-20261106/HANDOFF.md`) reproduced a connection failure before WELCOME when the WebSocket upgrade headers arrive lowercase, because the socket callbacks read `X-QQQ-Device-Id` from the WebSocket context, whose header lookup is case-sensitive. 1.1.1 resolves the device once on the authorized HTTP upgrade and carries it as a connection attribute; `PrintingRealtimeTest.lowercaseUpgradeHeadersReachWelcomeAndFrames` fails on the old code and passes on the fix. No contract change: extension 2.4.0, wire v1, subprotocol unchanged. Source `2a561815fcec8b7c95e76bb4b36fba314f512027` (PR 30), main run `5976`, reactor artifact `4426`, release `1381`; JAR `156720da6928f62d77101521a2953f6e1026d58d02e7fc7815a8c8fcbcedaefa`, POM `2fa451260a26f5b5ede30800d46c78050e223694d3d8dda640f9ca875d120ab0`, read back against the capture and the registry. The publisher had refused the run until its enrollment hash was re-reviewed (cluster-gitops PRs 283 and 285) after the 1.1.0 docs merge re-pinned the protocol fixture in the release caller.
## Superseded — 1.1.0 released (2026-11-05)
[QQQ Printing 1.1.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.0) is published on primary Gitea and its Maven registry: native printing extension **2.4.0** (`printer-approval-v1` capability and the `PRINTERS` frame) and the Remove printers / Remove device archives purged by retention. Source `eb2c16785d0723a47d4a22c68b8b04503108fc58`, main run `5473`, reactor artifact `4306`; JAR/POM hashes in [release operations](RELEASES.md). Pairs with [protocol 1.1.0](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.1.0); the Windows connector 1.2.1 is published (https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1). The CI fixture pin now follows protocol 1.1.0 source `5005f3c9e27053b6c327bb923695012832c0fdcc`. Forge adoption is a separate, coordinated step; 1.0.0 stays available for rollback.
## Current — 1.0.0 released
+4 -4
View File
@@ -6,16 +6,16 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>com.kingsrook</groupId><artifactId>qbit-build-parent</artifactId><version>2.0.0</version><relativePath/></parent>
<parent><groupId>com.kingsrook</groupId><artifactId>qbit-build-parent</artifactId><version>2.2.1</version><relativePath/></parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-printing</artifactId>
<version>1.1.0</version>
<version>1.1.2</version>
<name>QQQ Printing</name>
<licenses><license><name>Apache License, Version 2.0</name><url>https://www.apache.org/licenses/LICENSE-2.0</url></license></licenses>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<qqq.version>4.1.0-RC.21</qqq.version>
<qqq.version>4.1.0-RC.25</qqq.version>
<maven.compiler.proc>none</maven.compiler.proc>
</properties>
<dependencyManagement><dependencies>
@@ -37,7 +37,7 @@
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>6.0.1</version><scope>test</scope></dependency>
</dependencies>
<profiles><profile><id>ui-demo</id><dependencies>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.37</version><scope>runtime</scope></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.38</version><scope>runtime</scope></dependency>
</dependencies></profile></profiles>
<build>
<resources>
@@ -116,6 +116,9 @@ public class PrintingQBitProducer implements QBitProducer
printers.withField(new QFieldMetaData("media_profiles_json", QFieldType.STRING).withLabel("Reported queue default media"));
QTableMetaData jobs = table(PrintingTables.JOB, "Print jobs", List.of("id", "tenant_id", "device_id", "printer_row_id", "title", "format", "content_hash", "options_json", "idempotency_key", "created_at", "status", "message", "spooler_job_id"));
jobs.withField(new QFieldMetaData("content_length", QFieldType.INTEGER).withLabel("Document size (bytes)"));
// created_at stays the ISO-8601 text the queue SQL orders by; created_time is the same instant as a real date-time for relative filters, sorting and date buckets.
jobs.withField(new QFieldMetaData("created_time", QFieldType.DATE_TIME).withLabel("Created"));
jobs.getField("created_at").withLabel("Created at (ISO text)");
QTableMetaData commands = table(PrintingTables.COMMAND, "Device control history", List.of("id", "tenant_id", "device_id", "request_id", "action", "job_id", "actor", "status", "created_at", "expires_at", "result_code"));
commands.withField(new QFieldMetaData("cancelled_job_count", QFieldType.INTEGER).withLabel("Queued jobs canceled"));
commands.getField("device_id").withLabel("Print device").withPossibleValueSourceName(PrintingTables.DEVICE);
@@ -163,7 +166,7 @@ public class PrintingQBitProducer implements QBitProducer
instance.addPossibleValueSource(com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource.newForTable(PrintingTables.JOB));
layout(devices, List.of("name", "device_status", "control_status", "location", "facility_id", "last_seen_at"));
layout(printers, List.of("name", "location", "description", "device_id", "status", "enabled", "available", "last_seen_at"));
layout(jobs, List.of("title", "device_id", "printer_row_id", "status", "created_at", "format"));
layout(jobs, List.of("title", "device_id", "printer_row_id", "status", "created_time", "format"));
layout(commands, List.of("device_id", "action", "command_status", "created_at", "expires_at", "result_code", "cancelled_job_count"));
instance.addTable(devices);
instance.addTable(printers);
@@ -27,6 +27,7 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -59,6 +60,8 @@ public final class PrintingRealtimeRoutes implements QJavalinRouteProviderInterf
{
public static final String PATH = "/qqq/v1/qbit/printing/device/realtime";
public static final String SUBPROTOCOL = "qqq-printing.v1";
/** Device identifier resolved on the authorized HTTP upgrade and carried to the WebSocket callbacks. */
static final String DEVICE_ATTRIBUTE = "printingRealtimeDevice";
private final PrintingService service;
private final Map<String, Channel> channels = new ConcurrentHashMap<>();
private final ExecutorService workers = Executors.newVirtualThreadPerTaskExecutor();
@@ -97,6 +100,10 @@ public final class PrintingRealtimeRoutes implements QJavalinRouteProviderInterf
{
validateUpgrade(context);
authorize(context);
// The HTTP upgrade context resolves header names case-insensitively; the WebSocket context does not, and an ingress
// that lowercases header names would otherwise leave the device null on connect. Resolve it once here, after
// authorization, and carry it on the connection.
context.attribute(DEVICE_ATTRIBUTE, Objects.requireNonNull(context.header(PrintingAgentAuthorization.DEVICE_HEADER), "authorized upgrade without a device header"));
String cookieName = context.cookie("sessionId") != null && !context.cookie("sessionId").isBlank() ? "sessionId" : "sessionUUID";
context.attribute("printingRealtimeCredential", new Credential(cookieName, context.cookie(cookieName), context.header(PrintingAgentAuthorization.TOKEN_HEADER), context.header("X-QQQ-Client")));
}
@@ -113,7 +120,7 @@ public final class PrintingRealtimeRoutes implements QJavalinRouteProviderInterf
{
socket.onConnect(context ->
{
String device = context.header(PrintingAgentAuthorization.DEVICE_HEADER);
String device = Objects.requireNonNull(context.attribute(DEVICE_ATTRIBUTE), "WebSocket connected without the authorized device attribute");
Channel next = new Channel(context, device, context.attribute("printingRealtimeCredential"));
next.policy = service.realtimePolicy();
Channel previous = channels.put(device, next);
@@ -149,7 +156,8 @@ public final class PrintingRealtimeRoutes implements QJavalinRouteProviderInterf
@SuppressWarnings("ReferenceEquality")
private @Nullable Channel current(WsContext context)
{
Channel channel = channels.get(context.header(PrintingAgentAuthorization.DEVICE_HEADER));
String device = context.attribute(DEVICE_ATTRIBUTE);
Channel channel = device == null ? null : channels.get(device);
return channel != null && channel.socket.session == context.session ? channel : null;
}
@@ -297,6 +297,8 @@ public class PrintingService
"ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS content_available INTEGER DEFAULT 1 NOT NULL",
"ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS completed_at VARCHAR(40)",
"UPDATE printing_job SET completed_at=(SELECT MAX(e.created_at) FROM printing_event e WHERE e.job_id=printing_job.id AND e.status IN ('SPOOLED','SPOOLER_COMPLETED')) WHERE completed_at IS NULL AND status IN ('SPOOLED','SPOOLER_COMPLETED')",
"ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS created_time TIMESTAMP",
"UPDATE printing_job SET created_time=CAST(REPLACE(REPLACE(created_at, 'T', ' '), 'Z', '') AS TIMESTAMP) WHERE created_time IS NULL",
"ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS content_storage_table VARCHAR(200)",
"ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS content_reference VARCHAR(500)",
"ALTER TABLE printing_job ADD COLUMN IF NOT EXISTS content_binding VARCHAR(64)",
@@ -323,6 +325,7 @@ public class PrintingService
"UPDATE printing_printer SET queue_name=name WHERE queue_name IS NULL",
"CREATE INDEX IF NOT EXISTS printing_job_dispatch_idx ON printing_job(tenant_id, device_id, status, created_at, id)",
"CREATE INDEX IF NOT EXISTS printing_job_history_idx ON printing_job(tenant_id, created_at, id)",
"CREATE INDEX IF NOT EXISTS printing_job_created_time_idx ON printing_job(tenant_id, created_time, id)",
"CREATE INDEX IF NOT EXISTS printing_job_retention_completed_idx ON printing_job(tenant_id, content_available, status, completed_at, id)",
"CREATE INDEX IF NOT EXISTS printing_event_job_idx ON printing_event(job_id)",
"CREATE INDEX IF NOT EXISTS printing_device_tenant_idx ON printing_device(tenant_id, last_seen_at)",
@@ -1223,7 +1226,9 @@ public class PrintingService
configuredStorage().put(storageTableName, reference, format, content);
}
checkSubmission(guard);
update(connection, "INSERT INTO printing_job (id, tenant_id, device_id, printer_id, title, format, content_hash, content_base64, content_length, options_json, idempotency_key, created_at, status, printer_row_id, content_storage_table, content_reference, content_binding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'QUEUED', ?, ?, ?, ?)", id, tenantId, deviceId, printerId, title, format, digest, storageTableName == null ? Base64.getEncoder().encodeToString(content) : null, content.length, options.toString(), key, Instant.now().toString(), printers.getFirst().getString("id"), storageTableName, storageTableName == null ? null : reference, storageTableName == null ? null : binding);
// One instant, two columns: created_at is the ISO-8601 text the queue SQL and protocol compare; created_time is the UTC timestamp QQQ filters as a date-time.
Instant createdAt = Instant.now();
update(connection, "INSERT INTO printing_job (id, tenant_id, device_id, printer_id, title, format, content_hash, content_base64, content_length, options_json, idempotency_key, created_at, created_time, status, printer_row_id, content_storage_table, content_reference, content_binding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'QUEUED', ?, ?, ?, ?)", id, tenantId, deviceId, printerId, title, format, digest, storageTableName == null ? Base64.getEncoder().encodeToString(content) : null, content.length, options.toString(), key, createdAt.toString(), java.time.LocalDateTime.ofInstant(createdAt, java.time.ZoneOffset.UTC), printers.getFirst().getString("id"), storageTableName, storageTableName == null ? null : reference, storageTableName == null ? null : binding);
if(bound != null)
{
String eventId = UUID.randomUUID().toString();
@@ -0,0 +1,364 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.printing;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.DateBucket;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.DateBucketUnit;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Print jobs carry their creation instant as a real UTC timestamp that QQQ
** filters, sorts and date-buckets, beside the ISO-8601 text the queue SQL uses.
*******************************************************************************/
class PrintingJobCreatedTimeTest
{
/***************************************************************************
** A submitted job stores one instant in both columns, the timestamp in UTC.
***************************************************************************/
@Test
void submittedJobStoresTheCreationInstantAsAUtcTimestamp() throws Exception
{
DriverDataSource source = source();
PrintingService service = new PrintingService(source, "tenant-a");
service.initialize();
String device = enroll(service);
Instant before = Instant.now();
String job = submit(service, device, "recent");
Instant after = Instant.now();
Instant text = Instant.parse(text(source, job));
Instant timestamp = timestamp(source, job).toInstant(ZoneOffset.UTC);
assertFalse(text.isBefore(before) || text.isAfter(after), text + " is outside " + before + " to " + after);
assertTrue(Duration.between(text, timestamp).abs().compareTo(Duration.ofNanos(1000)) <= 0, text + " and " + timestamp + " are different instants");
}
/***************************************************************************
** The job table exposes a DATE_TIME field in the default columns; the text stays available.
***************************************************************************/
@Test
void metadataExposesCreatedAsADateTimeField() throws Exception
{
QInstance instance = instance(source());
QFieldMetaData created = instance.getTable(PrintingTables.JOB).getField("created_time");
assertEquals(QFieldType.DATE_TIME, created.getType());
assertEquals("Created", created.getLabel());
assertEquals(QFieldType.STRING, instance.getTable(PrintingTables.JOB).getField("created_at").getType());
var columns = (PrintingQBitProducer.DefaultColumns) instance.getTable(PrintingTables.JOB).getSupplementalMetaData("nextUi");
assertTrue(columns.getDefaultColumns().contains("created_time"));
assertFalse(columns.getDefaultColumns().contains("created_at"));
}
/***************************************************************************
** "Created in the last day" selects, sorts and counts by format through QQQ.
***************************************************************************/
@Test
void relativeDateCriteriaSelectRecentJobsOnly() throws Exception
{
DriverDataSource source = source();
QInstance instance = instance(source);
PrintingService service = new PrintingService(source, "tenant-a");
String device = enroll(service);
String recent = submit(service, device, "recent");
String old = submit(service, device, "old");
age(source, old, Instant.now().minus(3, ChronoUnit.DAYS));
QContext.init(instance, viewer());
try
{
QQueryFilter lastDay = new QQueryFilter(new QFilterCriteria("created_time", QCriteriaOperator.GREATER_THAN_OR_EQUALS, NowWithOffset.minus(1, ChronoUnit.DAYS)));
List<QRecord> rows = new QueryAction().execute(new QueryInput().withTableName(PrintingTables.JOB).withFilter(lastDay)).getRecords();
assertEquals(List.of(recent), ids(rows));
Instant created = rows.getFirst().getValueInstant("created_time");
assertTrue(Duration.between(created, Instant.now()).abs().compareTo(Duration.ofMinutes(5)) < 0, created + " is not the current UTC instant");
QQueryFilter earlier = new QQueryFilter(new QFilterCriteria("created_time", QCriteriaOperator.LESS_THAN, NowWithOffset.minus(1, ChronoUnit.DAYS)));
assertEquals(List.of(old), ids(new QueryAction().execute(new QueryInput().withTableName(PrintingTables.JOB).withFilter(earlier)).getRecords()));
QQueryFilter newestFirst = new QQueryFilter().withOrderBy(new QFilterOrderBy("created_time", false));
assertEquals(List.of(recent, old), ids(new QueryAction().execute(new QueryInput().withTableName(PrintingTables.JOB).withFilter(newestFirst)).getRecords()));
Aggregate count = new Aggregate("id", AggregateOperator.COUNT);
GroupBy format = new GroupBy(QFieldType.STRING, "format");
List<AggregateResult> byFormat = new AggregateAction().execute(new AggregateInput(PrintingTables.JOB).withFilter(lastDay).withAggregate(count).withGroupBy(format)).getResults();
assertEquals(1, byFormat.size());
assertEquals("ZPL", byFormat.getFirst().getGroupByValue(format));
assertEquals(1, ValueUtils.getValueAsInteger(byFormat.getFirst().getAggregateValue(count)));
GroupBy day = new GroupBy(QFieldType.DATE_TIME, "created_time").withBucket(new DateBucket(DateBucketUnit.DAY));
List<AggregateResult> byDay = new AggregateAction().execute(new AggregateInput(PrintingTables.JOB).withAggregate(count).withGroupBy(day)).getResults();
assertEquals(2, byDay.size());
for(AggregateResult bucket : byDay)
{
assertNotNull(bucket.getGroupByValue(day));
assertEquals(1, ValueUtils.getValueAsInteger(bucket.getAggregateValue(count)));
}
}
finally
{
QContext.clear();
}
}
/***************************************************************************
** Rows written before the column existed receive their timestamp from the text.
***************************************************************************/
@Test
void initializeBackfillsExistingJobs() throws Exception
{
DriverDataSource source = source();
PrintingService service = new PrintingService(source, "tenant-a");
service.initialize();
String device = enroll(service);
String fractional = submit(service, device, "fractional");
String whole = submit(service, device, "whole");
dropCreatedTime(source);
execute(source, "UPDATE printing_job SET created_at=? WHERE id=?", "2026-01-02T03:04:05.123456Z", fractional);
execute(source, "UPDATE printing_job SET created_at=? WHERE id=?", "2025-12-31T23:59:59Z", whole);
service.initialize();
service.initialize();
assertEquals(LocalDateTime.parse("2026-01-02T03:04:05.123456"), timestamp(source, fractional));
assertEquals(LocalDateTime.parse("2025-12-31T23:59:59"), timestamp(source, whole));
assertNotNull(timestamp(source, submit(service, device, "after")));
}
/***************************************************************************
** A host schema without the column refuses submissions and reads; no job is stored without its timestamp.
***************************************************************************/
@Test
void schemaWithoutTheColumnFailsLoudly() throws Exception
{
DriverDataSource source = source();
QInstance instance = instance(source);
PrintingService service = new PrintingService(source, "tenant-a");
String device = enroll(service);
dropCreatedTime(source);
SQLException refused = assertThrows(SQLException.class, () -> submit(service, device, "refused"));
assertTrue(String.valueOf(refused.getMessage()).toLowerCase(Locale.ROOT).contains("created_time"), String.valueOf(refused.getMessage()));
try(Connection connection = source.getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM printing_job"); ResultSet rows = statement.executeQuery())
{
assertTrue(rows.next());
assertEquals(0, rows.getInt(1));
}
QContext.init(instance, viewer());
try
{
assertThrows(QException.class, () -> new QueryAction().execute(new QueryInput().withTableName(PrintingTables.JOB)));
}
finally
{
QContext.clear();
}
}
/***************************************************************************
** Independent shared-connection H2 database for each test.
***************************************************************************/
private static DriverDataSource source()
{
return new DriverDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE", "sa", "");
}
/***************************************************************************
** A real RDBMS instance binds the QBit and service to one isolated database.
***************************************************************************/
private static QInstance instance(DriverDataSource source) throws Exception
{
PrintingService service = new PrintingService(source, "tenant-a");
service.initialize();
QInstance instance = new QInstance();
String backend = "printing-test-" + UUID.randomUUID();
instance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK));
instance.addBackend(new RDBMSBackendMetaData().withName(backend).withVendor("h2").withHostName("mem").withDatabaseName("test").withUsername("sa").withPassword("").withJdbcUrl(source.url()));
new PrintingQBitProducer().withConfig(new PrintingQBitConfig().withBackendName(backend).withService(service)).produce(instance, "printing");
new QInstanceValidator().validate(instance);
return instance;
}
/***************************************************************************
** An ordinary scoped administrator, never the all-powerful system session.
***************************************************************************/
private static QSession viewer()
{
return new QSession().withUser(new QUser().withIdReference("viewer")).withSecurityKeyValue("printing_tenant", "tenant-a").withPermission("printing_admin");
}
/***************************************************************************
** Enroll one device with an approved synthetic label queue.
***************************************************************************/
private static String enroll(PrintingService service) throws Exception
{
JSONObject code = service.createEnrollment("warehouse", "station");
String device = service.enroll(code.put("deviceName", "station").put("agentVersion", "test")).getString("deviceId");
service.inventory(device, new JSONArray().put(new JSONObject().put("printerId", "labels").put("name", "Labels").put("status", "READY").put("formats", new JSONArray(List.of("ZPL")))));
service.enablePrinter(device, "labels", true);
return device;
}
/***************************************************************************
** Submit one synthetic label through the service; nothing leaves the test database.
***************************************************************************/
private static String submit(PrintingService service, String device, String key) throws Exception
{
return service.submit(new JSONObject().put("deviceId", device).put("printerId", "labels").put("title", key + " job").put("format", "ZPL")
.put("contentBase64", Base64.getEncoder().encodeToString("^XA^XZ".getBytes(StandardCharsets.UTF_8))).put("idempotencyKey", key)).getString("jobId");
}
/***************************************************************************
** Record identifiers in result order.
***************************************************************************/
private static List<String> ids(List<QRecord> rows)
{
return rows.stream().map(row -> row.getValueString("id")).toList();
}
/***************************************************************************
** Move the creation of one job to an earlier instant in both columns.
***************************************************************************/
private static void age(DriverDataSource source, String job, Instant created) throws SQLException
{
execute(source, "UPDATE printing_job SET created_at=?, created_time=? WHERE id=?", created.toString(), LocalDateTime.ofInstant(created, ZoneOffset.UTC), job);
}
/***************************************************************************
** Return the schema to its shape before the column was added.
***************************************************************************/
private static void dropCreatedTime(DriverDataSource source) throws SQLException
{
execute(source, "DROP INDEX printing_job_created_time_idx");
execute(source, "ALTER TABLE printing_job DROP COLUMN created_time");
}
/***************************************************************************
** The stored ISO-8601 text of one job.
***************************************************************************/
private static String text(DriverDataSource source, String job) throws SQLException
{
try(Connection connection = source.getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT created_at FROM printing_job WHERE id=?"))
{
statement.setString(1, job);
try(ResultSet rows = statement.executeQuery())
{
assertTrue(rows.next());
return rows.getString(1);
}
}
}
/***************************************************************************
** The stored timestamp of one job, without any time zone conversion.
***************************************************************************/
private static LocalDateTime timestamp(DriverDataSource source, String job) throws SQLException
{
try(Connection connection = source.getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT created_time FROM printing_job WHERE id=?"))
{
statement.setString(1, job);
try(ResultSet rows = statement.executeQuery())
{
assertTrue(rows.next());
return rows.getObject(1, LocalDateTime.class);
}
}
}
/***************************************************************************
** Run one statement with bound values.
***************************************************************************/
private static void execute(DriverDataSource source, String sql, Object... values) throws SQLException
{
try(Connection connection = source.getConnection())
{
PrintingService.update(connection, sql, values);
}
}
}
@@ -57,6 +57,55 @@ public class PrintingRealtimeTest
Path directory;
/** Lowercase upgrade header names (an ingress that normalizes them) must still resolve the device for every socket callback. */
@Test
void lowercaseUpgradeHeadersReachWelcomeAndFrames() throws Exception
{
String jdbc = "jdbc:h2:file:" + directory.resolve("lowercase-realtime").toString().replace('\\', '/');
PrintingLocalHost host = new PrintingLocalHost(new PrintingLocalSettings(jdbc, "sa", "", "demo", 0, "synthetic-management-token-long-enough", "operator", "synthetic-realtime-password-1234"));
AtomicReference<Javalin> app = new AtomicReference<>();
QApplicationLauncher launcher = QApplicationLauncher.run(host, new QApplicationLauncherConfig().withRegisterShutdownHook(false).withServerCustomizer(server ->
{
host.configure(server, false);
server.withJavalinConfigurationCustomizer(app::set);
}));
PrintingService service = Objects.requireNonNull(launcher.getQInstance().getQBits().values().stream().map(value -> value.getConfig()).filter(PrintingQBitConfig.class::isInstance)
.map(PrintingQBitConfig.class::cast).findFirst().orElseThrow().getService(), "The printing service was not configured");
String base = "http://127.0.0.1:" + Objects.requireNonNull(app.get(), "Test HTTP server was not started").port();
JSONObject enrolled = service.enrollForUser(service.createEnrollment("default", "Lowercase fixture").put("deviceName", "Lowercase fixture").put("agentVersion", "test"), "operator");
String device = enrolled.getString("deviceId");
String proof = enrolled.getString("deviceToken");
try(HttpClient http = HttpClient.newHttpClient())
{
String cookie = login(http, base);
// An ingress that lowercases header names must not leave the device unresolved on connect: WELCOME, an acknowledged
// inventory and the capability-gated PRINTERS push all work exactly as with the canonical spelling.
try(PrintingSocketFixture lowercase = PrintingSocketFixture.open(http, base, cookie, device, proof, true))
{
lowercase.initialize(new JSONArray().put("realtime-v1").put("REFRESH_INVENTORY").put("CANCEL_SPOOL").put("printer-approval-v1"));
lowercase.send("INVENTORY", "inventory-lower", new JSONObject().put("printers", new JSONArray().put(new JSONObject().put("printerId", "queue").put("name", "Synthetic queue")
.put("driverName", "Synthetic").put("portName", "SYNTHETIC").put("status", "READY").put("formats", new JSONArray().put("PDF")))));
assertEquals("inventory-lower", lowercase.take("ACK").getJSONObject("payload").getString("acknowledgedMessageId"));
JSONObject printers = lowercase.take("PRINTERS").getJSONObject("payload");
assertEquals("queue", printers.getJSONArray("printers").getJSONObject(0).getString("printerId"));
assertFalse(printers.getJSONArray("printers").getJSONObject(0).getBoolean("enabled"));
// A replacement socket with canonical spelling supersedes the lowercase one through the same device key.
try(PrintingSocketFixture canonical = PrintingSocketFixture.connect(http, base, cookie, device, proof))
{
assertTrue(lowercase.closed.get(10, TimeUnit.SECONDS) > 0);
canonical.send("PING", "ping-1", new JSONObject());
canonical.take("PONG");
}
}
}
finally
{
launcher.stop();
}
}
/** An old client cannot ignore media intent, including after replacing a capable connection. */
@Test
void mediaCapabilityIsReplacedOnEveryAuthenticatedHello() throws Exception
@@ -55,11 +55,20 @@ final class PrintingSocketFixture implements WebSocket.Listener, AutoCloseable
/** Separate upgrade from HELLO so a commit in that interval can be tested deterministically. */
static PrintingSocketFixture open(HttpClient client, String base, String cookie, String device, String proof) throws Exception
{
return open(client, base, cookie, device, proof, false);
}
/** Lowercase header names model an ingress that normalizes them, which the device lookup must survive. */
static PrintingSocketFixture open(HttpClient client, String base, String cookie, String device, String proof, boolean lowercaseHeaders) throws Exception
{
PrintingSocketFixture fixture = new PrintingSocketFixture();
String deviceHeader = lowercaseHeaders ? PrintingAgentAuthorization.DEVICE_HEADER.toLowerCase(java.util.Locale.ROOT) : PrintingAgentAuthorization.DEVICE_HEADER;
String tokenHeader = lowercaseHeaders ? PrintingAgentAuthorization.TOKEN_HEADER.toLowerCase(java.util.Locale.ROOT) : PrintingAgentAuthorization.TOKEN_HEADER;
fixture.socket = client.newWebSocketBuilder().subprotocols(PrintingRealtimeRoutes.SUBPROTOCOL).header("Origin", base)
.header("X-QQQ-Client", "api/qqq-print-agent")
.header("Cookie", cookie).header(PrintingAgentAuthorization.DEVICE_HEADER, device).header(PrintingAgentAuthorization.TOKEN_HEADER, proof)
.header(lowercaseHeaders ? "x-qqq-client" : "X-QQQ-Client", "api/qqq-print-agent")
.header("Cookie", cookie).header(deviceHeader, device).header(tokenHeader, proof)
.buildAsync(URI.create(base.replace("http:", "ws:") + PrintingRealtimeRoutes.PATH), fixture).get(10, TimeUnit.SECONDS);
return fixture;
}
@@ -5,7 +5,7 @@
# Protocol fixtures used by PrintingContractTest
Unaltered copies of files from the `fixtures/` directory of the canonical `qqq-print-protocol` repository
at commit `42c0d249f81395f00408ec0f92638dcbbf5deb03` (the commit this source pins), so the contract tests run
at commit `5005f3c9e27053b6c327bb923695012832c0fdcc` (protocol 1.1.0, the commit this source pins; the files below are unchanged since `42c0d249f81395f00408ec0f92638dcbbf5deb03`), so the contract tests run
without a sibling checkout. Only the files the test reads are copied:
- `action-refresh-inventory.request.json`