Compare commits

..
Author SHA1 Message Date
CI 61618f3345 Bump version to 0.6.0-SNAPSHOT [skip ci] 2026-11-07 19:22:54 +00:00
james.maes 835c5add68 Merge pull request 'build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)' (#32) from deps/align-gitea into develop
build / maven (push) Successful in 2m37s
maven / build (push) Successful in 2m37s
2026-11-07 19:20:28 +00:00
james.maesandClaude Opus 5.5 a9fa2bc932 build: surefire 3.5.6 (3.5.2 POM is served brotli-encoded by maven.kof22.com and fails to parse)
build / maven (push) Successful in 1m26s
maven / build (push) Successful in 1m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:18:37 +00:00
james.maesandClaude Opus 5.5 d4b2b626ca build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)
build / maven (push) Canceled after 36s
maven / build (push) Canceled after 36s
demo/pom.xml: qqq.version: 4.1.0-RC.21 -> 4.1.0-RC.26
demo/pom.xml: qqq-frontend-next: 1.0.0-RC.35 -> 1.0.0-RC.38
examples/echo/demo/pom.xml: qqq.version: 4.1.0-RC.21 -> 4.1.0-RC.26
examples/echo/demo/pom.xml: qqq-frontend-next: 1.0.0-RC.35 -> 1.0.0-RC.38
examples/echo/demo/pom.xml: generation.version: 0.1.0-SNAPSHOT -> 1.1.0-RC.1
examples/echo/demo/pom.xml: printing.version: 1.0.0 -> 1.2.0-RC.1
examples/echo/demo/pom.xml: recordfiles.version: 0.1.0-SNAPSHOT -> 0.4.0-RC.1
examples/echo/demo/pom.xml: collaboration.version: 0.1.0-record-files-SNAPSHOT -> 1.2.0-RC.1
examples/echo/pom.xml: qqq.version: 4.1.0-RC.21 -> 4.1.0-RC.26
examples/echo/pom.xml: qqq-frontend-next: 1.0.0-RC.35 -> 1.0.0-RC.38
examples/echo/pom.xml: generation.version: 0.1.0-SNAPSHOT -> 1.1.0-RC.1
examples/echo/pom.xml: printing.version: 1.0.0 -> 1.2.0-RC.1
integrations/record-files/pom.xml: collaboration.version: 1.1.0-RC.1 -> 1.2.0-RC.1
integrations/record-files/pom.xml: recordfiles.version: 0.1.0-RC.1 -> 0.4.0-RC.1
pom.xml: parent -> 2.2.1
pom.xml: qqq.version: 4.1.0-RC.22 -> 4.1.0-RC.26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:16:11 +00:00
james.maes 4f96071157 Merge pull request 'fix(echo): make approved scanner photos available immediately' (#22) from fix/approved-scanner-photo-availability into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (push) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (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-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
build / maven (push) Failing after 1m0s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-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 / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Failing after 1m0s
2026-11-07 13:12:15 +00:00
CI ad0c5da85a Bump version to 0.5.0-SNAPSHOT [skip ci] 2026-11-06 23:46:11 +00:00
james.maes 631c86b244 Merge pull request 'build: publish the Record Files bridge with the base through one reactor' (#29) from kof22/scanners-reactor into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 4s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (push) Successful in 8s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (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-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 6s
build / maven (push) Successful in 3m49s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-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 / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 3m49s
2026-11-06 23:42:35 +00:00
james.maesandClaude Sonnet 5.5 52c34a54e7 Merge develop into kof22/scanners-reactor (keep the 0.4.0-SNAPSHOT revision in the reactor root)
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 / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (pull_request) Successful in 7s
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-core-2-head (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 / java-maven-7-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: ci / ci-result (pull_request) Successful in 5s
build / maven (push) Successful in 2m44s
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: native / native-result (pull_request) Successful in 7s
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-10 (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 / 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
maven / build (push) Successful in 2m44s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:39:10 -06:00
CI df8b615c09 Bump version to 0.4.0-SNAPSHOT [skip ci] 2026-11-06 23:32:02 +00:00
james.maesandClaude Sonnet 5.5 6cb8e34c5d build: publish the Record Files bridge with the base through one reactor
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 / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (pull_request) Successful in 14s
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-core-2-head (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 / java-maven-7-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: ci / ci-result (pull_request) Waiting to run
build / maven (push) Successful in 7m17s
QRun-IO/ci-policy: native / native-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-10 (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 / 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
maven / build (push) Successful in 7m12s
The root pom becomes the packaging-pom reactor (qbit-mobile-scanners-reactor), the single <revision> source and parent of base/ and
integrations/record-files, so one root mvn deploy publishes the reactor POM, qbit-mobile-scanners and qbit-mobile-scanners-record-files at one
version. Consumer POMs are flattened so the version is resolved. Pins: QQQ 4.1.0-RC.22, qbit-record-files 0.1.0-RC.1, qbit-collaboration 1.1.0-RC.1
(all verified in the Gitea registry); the SNAPSHOT/feature coordinates and the qqq.frozen.version override are gone. Checkstyle config stays at the
repository root. demo/ and examples/ stay standalone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:30:45 -06:00
james.maesandClaude Sonnet 5.5 4be0bfe7ba refactor: move the base qbit into base/ (pure rename, no content change)
Prepares the reactor layout; the root pom.xml is recreated as the aggregator in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:28:39 -06:00
james.maes 0d4f6df850 Merge pull request 'fix: refresh device versions on heartbeat (mobile-scanners 1.6)' (#26) from fix/refresh-device-versions into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (push) Successful in 12s
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 / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 10s
build / maven (push) Successful in 10m37s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-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 / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 10m37s
2026-11-06 23:23:31 +00:00
CI 8e7fba0d65 Bump version to 0.3.0-SNAPSHOT [skip ci] 2026-11-06 16:53:27 +00:00
james.maes 63f026dfc2 Merge pull request 'ci: pin shared workflow to v1' (#24) from ci/pin-v1-develop into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (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 / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (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: native / native-result (push) Successful in 10s
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 10s
QRun-IO/ci-policy: ci / ci-result (push) Waiting to run
build / maven (push) Successful in 54m22s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-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 / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 2m11s
2026-11-06 15:34:37 +00:00
james.maes 78ae9e0722 Merge pull request 'feat(echo): carry the GG Forge pg/nav/label/recovery layers upstream' (#25) from feat/echo-forge-delta-20261106 into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (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 / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 9s
build / maven (push) Failing after 4m23s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-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 / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Failing after 4m23s
2026-11-06 15:32:41 +00:00
james.maesandClaude Fable 5.1 e7774e4dd0 feat(echo): carry the GG Forge pg/nav/label/recovery layers upstream
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 8s
build / maven (push) Successful in 2m54s
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 / 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-core-2-head (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 / java-maven-7-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: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 16s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
maven / build (push) Successful in 2m54s
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-10 (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 / 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: native / native-result (pull_request) Successful in 13s
Upstream the cumulative local delta that GG Forge has been running as
com.kingsrook.examples:qqq-echo:0.1.0-forge.1874c64b0f86-pg3-nav2-label3-recovery1
so a published build can replace that pin. Rebased over the two later
develop commits that touch examples/echo (323b8d9, ae3fd7b); both only
change examples/echo/demo, so there were no conflicts.

Layers carried locally by GG Forge, now in source:

- pg1/pg2: typed PostgreSQL 17 bindings in EchoStore, db/echo-postgres.sql,
  EchoPostgresTest (opt-in via -Decho.postgres.url), app-navigation fix.
- nav2: authorized process descriptions only for MetaDataInput and
  ProcessMetaDataInput; device authority everywhere else
  (EchoPermissions, EchoAppVisibilityTest).
- label2/label3: media display-name formatting ("4 x 1 in label - 300 dpi"),
  centered GS1 HRI compatibility, EchoCenteredLabelTest.
- recovery1: rejected-scan recovery with EchoScanRejected; EchoService,
  EchoSteps, EchoStore and EchoPrintingDestinations changes; HTTP fixture
  regression for success/reject/success.

POM: only org.postgresql:postgresql 42.7.8 (test) is added; Forge's
-forge.* coordinate pins are not carried, develop's property-based
versions stay as they are.

Verification (offline, Forge qualification m2 cache, generation cohort
0.1.0-forge.ee782b7d6ea8 supplied on the command line because the
0.1.0-SNAPSHOT generation build in the cache predates print copies):
./mvnw -f examples/echo/pom.xml verify, Checkstyle 0 violations,
88 tests, 0 failures, 0 errors, 0 skipped with a disposable
postgres:17-alpine fixture (1 skipped without it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 08:57:40 -06:00
james.maesandClaude Opus 5.5 1f7c3e63ba ci: pin shared workflow to v1
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 8s
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-core-2-head (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 / java-maven-7-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-mcp (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: ci / ci-result (pull_request) Waiting to run
build / maven (push) Successful in 2m53s
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: native / native-result (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (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-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (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 / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
maven / build (push) Successful in 2m53s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:11:45 +00:00
james.maesandClaude Sonnet 5.5 1f8e3196fe fix(echo): make approved scanner photos available immediately
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 / 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-core-2-head (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 / java-maven-7-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: ci / ci-result (pull_request) Waiting to run
build / maven (push) Successful in 4m20s
QRun-IO/ci-policy: native / native-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-10 (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 / 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
maven / build (push) Successful in 4m21s
Opt the Echo host into the scoped captured-photo policy while preserving
quarantine for other intake. Allow only the demo administrator to release
earlier captures through the normal audited action.

Cover approval, parent and retained-content access, and record the verified
device behavior and remaining public dependency and release gates.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-05 17:29:34 -06:00
88 changed files with 996 additions and 195 deletions

No files matched your search

+1 -1
View File
@@ -3,5 +3,5 @@ on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@main
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
+2 -1
View File
@@ -52,7 +52,8 @@ Android host --(/qqq/v1 + mobile-scanners extension)--> QQQ server + this qbit -
## Project layout
- `src/main`, `src/test`: the reusable qbit (enrollment routes, approval/revocation, scan process, table metadata) and its H2 behavior and permission tests.
- `pom.xml`: the `qbit-mobile-scanners-reactor` aggregator (packaging pom, single `<revision>` source) of the two published modules, `base` and `integrations/record-files`; `demo` and `examples/echo` are not part of it. One root `mvn deploy` publishes the reactor POM, `qbit-mobile-scanners` and `qbit-mobile-scanners-record-files` at the same version.
- `base/src/main`, `base/src/test`: the reusable qbit (enrollment routes, approval/revocation, scan process, table metadata) and its H2 behavior and permission tests.
- `demo`: basic local QQQ server with persistent H2 storage, native administration UI and password login. QQQ `4.0.0` lacks the password session exchange it needs.
- `e2e`: browser acceptance tests against the packaged frontend and an isolated real demo server.
- `examples/echo` and `examples/echo/demo`: the optional Echo library and its runnable composed server.
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<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.qbits</groupId>
<artifactId>qbit-mobile-scanners-reactor</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-mobile-scanners</artifactId>
<version>${revision}</version>
<name>QQQ Mobile Scanners</name>
<description>Approved mobile devices and native QQQ barcode capture processes.</description>
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners</url>
<scm>
<connection>scm:git:https://git.kof22.com/QRun-IO/qbit-mobile-scanners.git</connection>
<developerConnection>scm:git:ssh://git@git.kof22.com:22/QRun-IO/qbit-mobile-scanners.git</developerConnection>
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners</url>
<tag>HEAD</tag>
</scm>
<issueManagement>
<system>Gitea</system>
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners/issues</url>
</issueManagement>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-postgres</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>14.3.0</version>
</dependency>
</dependencies>
<configuration>
<configLocation>${maven.multiModuleProjectDirectory}/checkstyle/checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
</configuration>
<executions>
<execution>
<id>checkstyle</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
+3 -3
View File
@@ -7,7 +7,7 @@
<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.26</qqq.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -34,7 +34,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.35</version>
<version>1.0.0-RC.38</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
@@ -78,7 +78,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<version>3.5.6</version>
<configuration>
<systemPropertyVariables>
<qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled>
+1 -1
View File
@@ -49,7 +49,7 @@ Scans, sessions, bindings and Echo scans are **kept indefinitely** by default. T
## Database migration beyond H2
The supplied, tested scripts are H2: `src/main/resources/db/mobile-scanners-h2.sql` and the Echo `echo-h2.sql`. Other backends are unsupported and untested here; a host that uses one **owns the equivalent migration**; do not assume `ADD COLUMN IF NOT EXISTS` or the column types. Additive columns that older databases need before running current servers: on `mobileScan`, nullable `aimIdentifier` (`VARCHAR(3)`) and `rawDataBase64`; on Echo, nullable `echo_scan.aimIdentifier`, `rendererValue` and `humanReadable`, and **`echo_run.copies`, which is `INT NOT NULL DEFAULT 1`** (existing runs keep one copy), so it is not nullable and the equivalent migration must supply the default. Echo also has nullable template columns (`templateId`, `templateRevisionId`) in `echo-h2.sql`. Test the migration on a copy, and snapshot first.
The supplied, tested scripts are H2: `base/src/main/resources/db/mobile-scanners-h2.sql` and the Echo `echo-h2.sql`. Other backends are unsupported and untested here; a host that uses one **owns the equivalent migration**; do not assume `ADD COLUMN IF NOT EXISTS` or the column types. Additive columns that older databases need before running current servers: on `mobileScan`, nullable `aimIdentifier` (`VARCHAR(3)`) and `rawDataBase64`; on Echo, nullable `echo_scan.aimIdentifier`, `rendererValue` and `humanReadable`, and **`echo_run.copies`, which is `INT NOT NULL DEFAULT 1`** (existing runs keep one copy), so it is not nullable and the equivalent migration must supply the default. Echo also has nullable template columns (`templateId`, `templateRevisionId`) in `echo-h2.sql`. Test the migration on a copy, and snapshot first.
## Demo-only file adapters
+7 -6
View File
@@ -10,9 +10,10 @@
| Android host libraries | Published development libraries from source `0ae2101`: SDK run 3634 passed and artifacts verified. The scanner application's newer source and signed candidate are recorded below. |
| Current cohort tests | Combined five-module `verify` (2026-11-05): 196 server tests (53 / 8 / 22 / 74 / 39), zero failures, errors and skips, zero Checkstyle violations, with qualified private owner dependencies. Android 232 unit tests and lint pass. Inherited report-only guardrail findings remain visible. |
| Storage integrity and recovery | `PrivateObjectFilesTest` concurrent-writer and dot-bucket regressions; `EchoDemoAcceptanceTest.restoredSnapshotKeepsScanAndRetainedFileAndRevertedApprovalStateIsReaudited` is the isolated local-demo recovery acceptance (stopped H2 plus object snapshot/restore, `RecordFilesTables.FILE` relationship and byte hashes). See [operations](OPERATIONS.md). |
| Local runtime | One server on public QQQ RC.21 and Printing 1.0.0; API 1.10, mobile 1.5, printing 2.3, mobile-record-files 1.0; existing data preserved. |
| Local runtime | One server using the tested development aggregate: public QQQ SNAPSHOT, current Collaboration, scoped local Record Files and qualified document inputs, plus Printing 1.0.0. API 1.10, mobile 1.5, printing 2.3, mobile-record-files 1.0; existing data preserved. Final immutable public-cohort qualification remains open. |
| Device and admin processes | Enrollment, approval and revocation work from a single record and from bulk selection (checked rows, select-all, first N), with confirmation and visible results, covered by HTTP and browser E2E tests. Earlier browser run: 15 cases. |
| Hardware | TC22 manual tests: operator-reported pass on November 4 (no per-case artifacts). MC9400 configured and approved; trigger check pending. |
| Approved-scanner photos | Host-only `quarantineCapturedFiles=false` makes an approved device's captured JPEG/PNG `AVAILABLE`; other intake stays `QUARANTINED`. `record-files.release` is admin-only. Root-reported: 69 owner and 197 scanner/Echo tests passed, 13 focused Android tests passed, Checkstyle passed. A synthetic gallery PNG from the signed MC9400 app was `AVAILABLE` with exact parent/type/size/SHA-256. No camera-hardware or new full-preview proof. Depends on the unmerged Record Files owner PR 1. |
| Hardware | TC22 manual tests: operator-reported pass on November 4 (no per-case artifacts). MC9400 physical trigger done: Saved scan with backend EMDK CODE128/AIM/raw bytes and attribution confirmed (the barcode was not the requested literal). |
## Open gates
@@ -20,12 +21,12 @@ The maintained server and existing TC22 app now run the GS1, rich test-data and
| Gate | What is still required |
| --- | --- |
| Public dependency cohort | Public, coherent Generation/bridges/Record Files/Scripting artifacts from their owner; adoption of the Core/Collaboration/Next UI foundations when released. Today these are qualified owner inputs. |
| Public dependency cohort | Public, coherent Generation/bridges/Record Files/Scripting artifacts from their owner; adoption of the Core/Collaboration/Next UI foundations when released. Today these are qualified owner inputs, and the current local aggregate uses public Core SNAPSHOT, current Collaboration and a local Record Files change, not a final public cohort. |
| Release | Retest the full gate when the final public dependency cohort is adopted, with a reviewed release manifest; Maven publication stays disabled until then. Redesign of the shared CI pipeline is outside this scope. |
| Signing and distribution | Done: QRun key with backup, signed Android 1.0.1 (code 3) built from the merged PR 15 source tree and installed in place on an MC9400 (installed bytes match; login/enrollment preserved; Scan Capture reopened; HTTPS connected). Open: MC9400 physical trigger scan, public dependencies and final distribution. No signing CI. |
| Signing and distribution | Done: QRun key with backup, signed Android 1.0.1 (code 3) built from the merged PR 15 source tree and installed in place on an MC9400 (installed bytes match; login/enrollment preserved; Scan Capture reopened; HTTPS connected). Open: public dependencies, signing-APK download publication and final distribution. No signing CI. |
| Deployment | HTTPS and the host's identity provider. Single-instance procedure, recovery and default-indefinite, host-owned retention guidance is in [operations](OPERATIONS.md); isolated local-demo recovery acceptance has passed. Enterprise object-store/HA and non-H2 databases are unsupported and unqualified options, not release tasks. The HTTP loopback demo is not a production deployment. |
| Provider authentication | Native OAuth2 requires UI API 1.10 native-session and nonce-validation support on the server (verified in the Android source). Still open: native Authentik login, logout and account switching on a handheld. No such handheld test is claimed. |
| Further device qualification | MC9400 physical trigger check and MDM-applied managed configuration remain unverified. Company profile/wallpaper is separate tracked scope, not a blocker for this release goal. The TC22 camera, GPS and screen-viewing checks are covered by the accepted operator group report; no separate per-case evidence is claimed or required again for that demo. |
| Provider authentication | Native OAuth2 requires UI API 1.10 native-session and nonce-validation support on the server (verified in the Android source). Shared Authenticator server configuration is verified (trusted HTTPS, API 1.10, scanner and printing extensions and channels, exact callbacks, REQUIRED nonce). Still open: native Authentik login, logout and account switching on a handheld (needs the MC9400 wireless-debugging endpoint). No native-auth pass is claimed. The Printing client nonce fix (PR 11, `443f642`; 352 tests and build passed) awaits hosted Core. |
| Further device qualification | MDM-applied managed configuration remain unverified. Company profile/wallpaper is separate tracked scope, not a blocker for this release goal. The TC22 camera, GPS and screen-viewing checks are covered by the accepted operator group report; no separate per-case evidence is claimed or required again for that demo. |
| Barcode fidelity | Echo's supported profiles are documented ([limits](../examples/echo/README.md#fidelity-and-limits)); The committed source supports bounded GS1-128; physical print/rescan remains open. Other GS1, ECI, structured and binary profiles remain refused. No universal barcode-reproduction claim. |
| Final reviews | Reconcile the dated security, architecture and standards reports to the final revision; they are scoped historical reviews, not a fresh exhaustive scan. A dated static review of base `a9eae04`, its supplement and its scope and limits are in [review reconciliation](REVIEW-RECONCILIATION-2026-11-05.md); it is not an audit rating. |
+10 -4
View File
@@ -22,12 +22,16 @@ The maintained composed Echo server and **existing TC22 app** were updated in pl
Use **Test Data → Test Orders → a record → Actions → Add photo**. Details, Items, History and Photos tabs provide direct access. Search/filter/count and Back preserve table context. Approved photos have thumbnails/previews; quarantined uploads remain listed as awaiting review. Ordinary read/attach permissions still apply.
The [local demo](LOCAL-DEMO.md) uses public QQQ `4.1.0-RC.21`, released Printing `1.0.0`, and qualified document/files/scripting owner inputs. Host port is `57832`; device port 8080 forwards to it, retaining the app URL `http://127.0.0.1:8080`. Contract versions remain UI API 1.10, mobile-scanners 1.5, printing 2.3 and mobile-record-files 1.0. This loopback demo is separate from HTTPS production deployment.
**Approved-scanner photos.** The Echo host sets Record Files `quarantineCapturedFiles=false`, so a JPEG/PNG captured by a currently approved device is `AVAILABLE` at once. Uploads, imports and attachment-panel files stay `QUARANTINED`. Parent tables are Echo scans plus the test orders, items and events; the scanner's existing `.attach` grants are retained. Only `admin` gains `record-files.release`; scanner grants are unchanged. Three earlier quarantined photos were released through the audited admin Release process. The server's current aggregate was built from public Core SNAPSHOT, current Collaboration, the scoped local Record Files change and qualified private document inputs, not a final public cohort. The scoped Record Files change is [owner PR 1](https://git.kof22.com/QRun-IO/qbit-record-files/pulls/1) (policy commit `3f08c584`), open and unmerged; its public CI currently fails an enrollment check that the coordinator owns, so neither the check nor its merge gates are changed here. No version or POM was guessed.
The [local demo](LOCAL-DEMO.md) source defaults remain QQQ `4.1.0-RC.21` and Printing `1.0.0` pending the public-cohort update; the maintained runtime uses the verified development aggregate described above. Host port is `57832`. The TC22 development app retains its loopback forwarding; the signed MC9400 app connects through trusted HTTPS. Contract versions remain UI API 1.10, mobile-scanners 1.5, printing 2.3 and mobile-record-files 1.0. This is local development qualification, not a customer production deployment.
## Verification
- The combined five-module `verify` (2026-11-05, base `9e8685f` plus the storage fixes) passed **196 server tests** with zero failures, errors and skips and zero Checkstyle violations: scanner 53, base demo 8, mobile Record Files 22, Echo 74, Echo demo 39. It used qualified private owner dependencies; there is no public-dependency or physical-output qualification, and the gate must be repeated when the public cohort is adopted. The new build parent's report-only schema-location/contract-test findings remain visible.
- Storage evidence: `PrivateObjectFilesTest.concurrentSameKeyWritersLeaveOneWinnersBytesAndMetadata` and `dotBucketsAreRefusedAndValidBucketsStillWork`; recovery acceptance `EchoDemoAcceptanceTest.restoredSnapshotKeepsScanAndRetainedFileAndRevertedApprovalStateIsReaudited` (details in [OPERATIONS](OPERATIONS.md#isolated-local-demo-recovery-acceptance-automated-passed)).
- Photo/admin change set (root-reported, not rerun in this review): 69 owner tests and 197 scanner/Echo tests passed (runs of 31, 158 and 8), 13 focused Android tests passed, and the relevant Checkstyle passed. These groupings differ from the module split below, so no per-module counts are claimed. Public immutable dependency qualification is still pending.
- Live evidence: a fresh synthetic gallery PNG uploaded from the production-signed MC9400 app returned "Saved photo", and the server showed `AVAILABLE` with the exact parent, type, size and SHA-256. An older photo's Available state, thumbnail and Preview control were observed. The new photo's full preview was not clicked because ADB disconnected. No camera-hardware or full-preview proof is claimed.
- The earlier combined five-module `verify` (2026-11-05, base `9e8685f` plus the storage fixes) passed **196 server tests** with zero failures, errors and skips and zero Checkstyle violations: scanner 53, base demo 8, mobile Record Files 22, Echo 74, Echo demo 39. It used qualified private owner dependencies; there is no public-dependency or physical-output qualification, and the gate must be repeated when the public cohort is adopted. The new build parent's report-only schema-location/contract-test findings remain visible.
- Storage/recovery PR 21 is merged (`59959247`), with bounded cold-restore and race proof complete. Website 0.1.17 is complete. Evidence: `PrivateObjectFilesTest.concurrentSameKeyWritersLeaveOneWinnersBytesAndMetadata` and `dotBucketsAreRefusedAndValidBucketsStillWork`; recovery acceptance `EchoDemoAcceptanceTest.restoredSnapshotKeepsScanAndRetainedFileAndRevertedApprovalStateIsReaudited` (details in [OPERATIONS](OPERATIONS.md#isolated-local-demo-recovery-acceptance-automated-passed)).
- Android passed **232 unit tests**, lint with **0 errors/0 warnings**, and the debug APK build. E2E APK/instrumentation builds and eight runner guard tests also passed.
- Isolated MC9400 native acceptance passed search/filter/paging/Back, grouped/related records, camera/gallery upload, parent-scoped saved-photo list, approved preview, rotation, pending-upload navigation protection and forced-logout cleanup. All requests matched the semantic API. Process-photo regression also passed.
- The final date-only correction received unit/lint/build checks and actual TC22 verification. Actual-device screenshots confirm Actions → Add photo, dates/timezones, currency, related tabs, photo empty state and return to page two. Synthetic camera/gallery inputs in automated acceptance are not a new operator camera report.
@@ -38,7 +42,9 @@ The [local demo](LOCAL-DEMO.md) uses public QQQ `4.1.0-RC.21`, released Printing
Previous TC22 APK SHA-256: `F71033C835AC3EB361A04D78C842947C5A04C85CE1AB1B999AA8CEFBFC9D7A95`; prior preview hashes are in [history](history/RICH-RECORDS-2026-11-05.md).
Signed Android `1.0.1` (code 3) was built from the identical merged source tree (PR 15) and installed in place on the MC9400. The installed bytes match the built APK, login and enrollment were preserved, the authorized Scan Capture screen reopened, and the HTTPS connection was established. Pending: the MC9400 physical trigger scan, native Authentik login/logout/account switching, the public dependency cohort and final distribution.
Signed Android `1.0.1` (code 3) was built from the identical merged source tree (PR 15) and installed in place on the MC9400. The installed bytes match the built APK, login and enrollment were preserved, the authorized Scan Capture screen reopened, and the HTTPS connection was established. The MC9400 physical trigger is **done**: an actual operator trigger produced a Saved scan, and the backend confirmed EMDK CODE128, AIM and raw bytes with server-derived attribution. The barcode scanned was not the literal string requested, so no exact-test-string claim is made.
Shared Authenticator server configuration is verified: real trusted HTTPS, UI API 1.10, both scanner and printing extensions and channels, exact callbacks and a REQUIRED nonce. Native client login, logout and account switching are **not yet tested** (this needs the MC9400 wireless-debugging endpoint), so no native-auth pass is claimed. The Printing client required-nonce fix (PR 11, `443f642`; 352 Windows/CLI tests and build passed) awaits hosted Core. Pending: native Authentik login/logout/account switching, the final public Core/Collaboration/document/files/scripting/bridge artifacts, a public-cohort rerun, GS1 physical print/rescan, signing-APK download publication and owned cleanup.
The November 4 TC22 operator group pass remains accepted and is not requested again. Physical GS1 print/rescan, provider callback acceptance and fleet rollout remain open. Echo supports only its documented [barcode profiles](../examples/echo/README.md#fidelity-and-limits). Company profile/wallpaper is separate tracked scope, not a blocker for this release goal.
+5 -3
View File
@@ -9,6 +9,8 @@ Open work only, as of **2026-11-05**. Present status is in [session state](SESSI
Note: redesign of the shared CI pipeline is outside this scanner/local Echo scope and is not tracked here; the local demo does not depend on it.
- [ ] **Record Files `quarantineCapturedFiles`.** The Echo host (`EchoDemo`) and the optional integration test already call/require `RecordFilesQBitConfig.withQuarantineCapturedFiles(Boolean)` (nullable, null inherits `quarantineNewFiles`; applies only to CAPTURE intake with a provider-verified device identity). The owner's scoped change (policy commit `3f08c584`, PR 1) is open and unmerged, and its CI currently fails a not-enrolled check owned by the coordinator; do not change CI or weaken merge gates. Adopt the released version as a reviewed pin once public; no coordinate is known, so none was guessed and this source builds only against the local owner input until then.
## This repository
- [ ] Qualify GS1 template/media setup and **physical print/rescan** of the requested label profile. Software duplication, native acceptance and maintained-demo adoption are complete.
@@ -21,13 +23,13 @@ Retention stays indefinite by default and any change is a host-owned policy ([op
## Hardware and device follow-up
- [ ] **MC9400:** one physical trigger scan, then confirm the backend record. The signed 1.0.1 (code 3) build is installed in place and approved ([session state](SESSION-STATE.md#handhelds)).
- [ ] Native Authentik login, logout and account switching on a handheld (not yet tested).
- [ ] Native Authentik login, logout and account switching on a handheld (not yet tested; needs the MC9400 wireless-debugging endpoint). The shared Authenticator server configuration is verified; the Printing client nonce fix (PR 11) awaits hosted Core. The MC9400 physical trigger check is done ([session state](SESSION-STATE.md#handhelds-and-remaining-limits)).
- [ ] Optional: click the full preview of the newly uploaded gallery photo on a handheld (not observed because ADB disconnected). No camera-hardware proof is claimed.
- [ ] Optional: capture per-case evidence (photos, logs, a scanned-back label) on a future run. The November 4 operator pass is accepted as is and is not to be requested again.
## Release (deliberately last)
- [ ] Final distribution of the signed 1.0.1 build and the public dependencies it waits on. Signing is done: the build was installed in place on an MC9400 through ADB, preserved enrollment, and reopened Scan Capture against the HTTPS server. Remaining device checks are listed above; no automatic APK-signing CI is qualified.
- [ ] Final distribution of the signed 1.0.1 build and the public dependencies it waits on. Signing is done: the build was installed in place on an MC9400 through ADB, preserved enrollment, and reopened Scan Capture against the HTTPS server. Remaining device checks are listed above; no automatic APK-signing CI is qualified. Also open: signing-APK download publication, owned cleanup, and a rerun of the public cohort with the final public Core/Collaboration/document/files/scripting/bridge artifacts.
- [ ] Company profile/wallpaper is a separate tracked scope and not a blocker for this bounded release goal.
- [ ] HTTPS deployment by the host (any non-H2 migration is host-owned and unsupported here) and the remaining gates in [production readiness](PRODUCTION-READINESS.md).
+1 -1
View File
@@ -83,7 +83,7 @@ Schema migration (H2, idempotent, nullable): `ALTER TABLE mobileScan ADD COLUMN
## Contract checks and compatibility
The published fragment is [mobile-scanners.yaml](../src/main/resources/openapi/qqq-v1/mobile-scanners.yaml); the initial release baseline is [mobile-scanners-1.0.0.yaml](../src/test/resources/openapi/mobile-scanners-1.0.0.yaml), with additional frozen baselines such as [mobile-scanners-1.4.0.yaml](../src/test/resources/openapi/mobile-scanners-1.4.0.yaml) (the contract immediately before the 1.5.0 capture metadata) and [mobile-scanners-1.5.0.yaml](../src/test/resources/openapi/mobile-scanners-1.5.0.yaml), the frozen 1.5.0 release snapshot that later changes are checked against. `MobileScannersApiContractTest` rejects generated-document drift, unresolved contract structure, incompatible baseline changes and missing version bumps using QQQ's shared contract checker. Object required fields and scanner numeric bounds are standard OpenAPI keywords, added by small Schema subclasses because RC20's base model does not expose those setters.
The published fragment is [mobile-scanners.yaml](../base/src/main/resources/openapi/qqq-v1/mobile-scanners.yaml); the initial release baseline is [mobile-scanners-1.0.0.yaml](../base/src/test/resources/openapi/mobile-scanners-1.0.0.yaml), with additional frozen baselines such as [mobile-scanners-1.4.0.yaml](../base/src/test/resources/openapi/mobile-scanners-1.4.0.yaml) (the contract immediately before the 1.5.0 capture metadata) and [mobile-scanners-1.5.0.yaml](../base/src/test/resources/openapi/mobile-scanners-1.5.0.yaml), the frozen 1.5.0 release snapshot that later changes are checked against. `MobileScannersApiContractTest` rejects generated-document drift, unresolved contract structure, incompatible baseline changes and missing version bumps using QQQ's shared contract checker. Object required fields and scanner numeric bounds are standard OpenAPI keywords, added by small Schema subclasses because RC20's base model does not expose those setters.
For an intentional contract edit, update specs/schemas and the extension semantic version, then regenerate only the current fragment:
+1 -1
View File
@@ -16,7 +16,7 @@ Date: 2026-11-02. Scope: `examples/echo/demo/**`. Source checkpoint, not a relea
`EchoDemo` extends `MobileScannersDemo` (database, `scanner`/`admin` accounts, native server) and composes Collaboration, Record Files, Printing, Document Generation and `EchoHost.install` from their public classes on one private H2 file (`WRITE_DELAY=0`). Composition runs under a scoped system context that is restored afterwards, then `QInstanceEnricher` enriches the instance, because the server only validates and the enricher registers Collaboration's record-feature permissions. `LocalFilesystemAttachmentStorage` implements the attachment owner's storage SPI over `PrivateObjectFiles` (hashed keys, temp-file-then-link, no overwrite) and adds no policy. `TrustedLocalEchoDemo` is the explicit developer launcher; the production `EchoDemo` default fails closed. See `README.md` for build, run and environment.
Authentication is the existing demo password/session module plus exact grants. `scanner`: `echo.use`, `printing_operator`, `document-generation.view|generate|print`, `record-files.view|manage`, `echoScan.attach` and the `printing_tenant` row key (replaced, never appended, on each validation); then `CollaborationSecurity.applyToSession`. `admin` additionally: `printing_admin`, `document-generation.configure|publish`. No Record Files release/configure/audit for anyone.
Authentication is the existing demo password/session module plus exact grants. `scanner`: `echo.use`, `printing_operator`, `document-generation.view|generate|print`, `record-files.view|manage`, `echoScan.attach` and the `printing_tenant` row key (replaced, never appended, on each validation); then `CollaborationSecurity.applyToSession`. `admin` additionally: `printing_admin`, `document-generation.configure|publish`, `record-files.release` (so approved captured photos release through the normal audited process). The scanner never gets release; no Record Files configure/review/audit for anyone.
## Test isolation
+6 -5
View File
@@ -15,7 +15,7 @@ There is **no default port**: pass one as the argument or set `ECHO_DEMO_PORT`.
## Status
The demo and Echo library tests pass with Checkstyle clean (current cohort: Echo 68, composed demo 35 tests; see [session state](../../../docs/SESSION-STATE.md)). The QQQ default is 4.1.0-RC.21 and Printing defaults to released 1.0.0; the other owner qbits remain qualified owner inputs until their public release. The composed acceptance test passes with a controlled synthetic renderer and a simulated Printing queue, so it is **not physical output**. TC22 physical acceptance is separately operator-reported as passing (November 4, group report). Not production-ready and not a public SDK release. The dated [`CLAUDE-HANDOFF.md`](CLAUDE-HANDOFF.md) is a historical checkpoint.
The demo and Echo library tests pass with Checkstyle clean (counts change per run; see [session state](../../../docs/SESSION-STATE.md) for the latest reported results). The QQQ default is 4.1.0-RC.21 and Printing defaults to released 1.0.0; the other owner qbits remain qualified owner inputs until their public release. The composed acceptance test passes with a controlled synthetic renderer and a simulated Printing queue, so it is **not physical output**. TC22 physical acceptance is separately operator-reported as passing (November 4, group report). Not production-ready and not a public SDK release. The dated [`CLAUDE-HANDOFF.md`](CLAUDE-HANDOFF.md) is a historical checkpoint.
## Trusted local development launcher
@@ -88,7 +88,7 @@ The legacy `ECHO_TEMPLATE_ID`, `ECHO_TEMPLATE_MEDIA_ID` and `ECHO_TEMPLATE_MEDIA
## Accounts and permissions
`scanner` receives exactly: `echo.use`, `printing_operator`, `document-generation.view|generate|print`, `record-files.view|manage` (manage is the owner's intake permission) and the `printing_tenant` row key. `admin` receives the same plus `printing_admin` (Printing's native pairing/approval/setup processes) and `document-generation.configure|publish` to prepare templates. The scanner never gets these, and nobody gets Record Files release/configure/audit. Scans still need an enrolled and approved device, so use the existing mobile enrollment flow.
`scanner` receives exactly: `echo.use`, `printing_operator`, `document-generation.view|generate|print`, `record-files.view|manage` (manage is the owner's intake permission), `.attach` on `echoScan` and the three mobile test-data tables (`testOrder`, `testOrderItem`, `testOrderEvent`) and the `printing_tenant` row key. `admin` receives the same plus `printing_admin` (Printing's native pairing/approval/setup processes) and `document-generation.configure|publish` to prepare templates. The scanner never gets these. Only `admin` also receives `record-files.release`, so approved captured photos can be released through the normal audited process; nobody gets Record Files configure/review/audit. Scans still need an enrolled and approved device, so use the existing mobile enrollment flow.
## Operator order
@@ -101,10 +101,11 @@ The legacy `ECHO_TEMPLATE_ID`, `ECHO_TEMPLATE_MEDIA_ID` and `ECHO_TEMPLATE_MEDIA
The host also installs the existing `qbit-mobile-scanners-record-files` integration, so an approved scanner can attach photos to one of its own retained Echo scans. Nothing new is served: Record Files' registered, versioned `record-files` upload API (`/qqq/v1/qbit/record-files/...`) and the independently versioned `mobile-record-files` widget vocabulary are advertised in `/qqq/v1/version`, behind the same native auth and device guards.
- **Process:** `echoScanPhotos` ("Echo scan photos"), listed in the Echo app. It requires `echo.use`, `echoScan.attach` and a verified approved device. The first step lists the actor's own scans (the table's record security lock decides; label is the barcode and type, never the record id). The server then re-reads the chosen scan through the collaboration owner's visibility and `attach` checks and builds the `RECORD_FILES_CAPTURE` widget from the record that check returned, on the fixed `echoScan` table. The client never supplies a table or record reference, and rendering the widget directly names no parent. Record Files authorizes the parent again on every upload operation.
- **Policy:** JPEG/PNG only, 20 MiB per file, up to four files, 256 KiB chunks, private staging under the demo's `objects` directory (separate staging namespace). Retained originals follow the existing Record Files quarantine setting.
- **Needs a real scan first:** the list is empty until the scanner has Echo scans. Nothing is seeded outside this module's tests.
- **Policy:** JPEG/PNG only, 20 MiB per file, up to four files, 256 KiB chunks, private staging under the demo's `objects` directory (separate staging namespace). The Echo host sets Record Files' `quarantineCapturedFiles=false` (leaving `quarantineNewFiles=true` and the generated-file setting unchanged), so a JPEG/PNG saved from this process by a device that `MobileCaptureIdentityProvider` currently verifies as approved is registered `AVAILABLE` and can be previewed immediately. The same upload API on any other host, and every upload, import or attachment-panel file here, stay `QUARANTINED` until released. This is a host trust decision, not a content scan or human review; no release event is written and review status is unchanged. **Requires a Record Files version that contains `RecordFilesQBitConfig.withQuarantineCapturedFiles(Boolean)`. The owner's change is an open, unmerged PR; this checkout's dependency pin has not been updated and builds only against the local owner input until a public version is pinned.** Files already uploaded stay `QUARANTINED` until released through the normal `record_files_file_record_files_release` process.
- **Test-data records:** the host also registers the test orders, items and events as Record Files parents, so a scanner can add photos from a record's Actions menu. The same approved-device policy and authorization re-check apply. Release of any quarantined file is admin-only (`record-files.release`).
- **Needs a real scan first (for the `echoScanPhotos` process):** the list is empty until the scanner has Echo scans. Nothing is seeded outside this module's tests.
- **Existing databases:** the Record Files upload tables are added by the same tolerated-reapply schema step; plan the migration for a database from another version.
- **Not claimed:** no real photo upload, native capture or device run has been verified here. That needs the Android host against this server.
- **Not claimed:** one synthetic gallery PNG uploaded from the signed MC9400 app was observed `AVAILABLE` with matching parent, type, size and SHA-256 (see [session state](../../../docs/SESSION-STATE.md)). No camera-hardware capture and no full preview of that photo are claimed.
## Existing databases
+7 -7
View File
@@ -16,15 +16,15 @@
<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.26</qqq.version>
<mobile.version>0.1.0-SNAPSHOT</mobile.version>
<echo.version>0.1.0-SNAPSHOT</echo.version>
<generation.version>0.1.0-SNAPSHOT</generation.version>
<generation.version>1.1.0-RC.1</generation.version>
<renderer.version>${generation.version}</renderer.version>
<printing.version>1.0.0</printing.version>
<recordfiles.version>0.1.0-SNAPSHOT</recordfiles.version>
<printing.version>1.2.0-RC.1</printing.version>
<recordfiles.version>0.4.0-RC.1</recordfiles.version>
<mobilerecordfiles.version>0.1.0-SNAPSHOT</mobilerecordfiles.version>
<collaboration.version>0.1.0-record-files-SNAPSHOT</collaboration.version>
<collaboration.version>1.2.0-RC.1</collaboration.version>
</properties>
<dependencyManagement><dependencies>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
@@ -40,14 +40,14 @@
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners-record-files</artifactId><version>${mobilerecordfiles.version}</version></dependency>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-collaboration</artifactId><version>${collaboration.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.35</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.38</version></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.2.224</version></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>6.0.2</version><scope>test</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>6.0.2</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version><configuration><showWarnings>true</showWarnings><failOnWarning>true</failOnWarning><proc>none</proc><compilerArgs><arg>-Xlint:deprecation</arg><arg>-Xlint:unchecked</arg><arg>-Werror</arg></compilerArgs></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.8.1</version></plugin>
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version><configuration><mainClass>com.kingsrook.examples.echo.demo.EchoDemo</mainClass></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version><dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies><configuration><configLocation>${project.basedir}/../../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration><executions><execution><phase>validate</phase><goals><goal>check</goal></goals></execution></executions></plugin>
@@ -225,6 +225,11 @@ public class EchoDemo extends MobileScannersDemo implements AutoCloseable
RecordFilesQBitConfig recordFiles = new RecordFilesQBitConfig().withBackendName(BACKEND)
.withParentTables(List.of(EchoQBitProducer.SCAN_TABLE, MobileTestData.ORDERS, MobileTestData.ITEMS, MobileTestData.EVENTS))
.withQuarantineGeneratedFiles(settings.releaseGeneratedFiles() ? Boolean.FALSE : null)
// Narrow, host-only opt-in: only CAPTURE intake with the verified approved-device identity from
// MobileCaptureIdentityProvider (JPEG/PNG, bounded; see EchoPhotoCapture.uploadConfiguration) is AVAILABLE
// on registration. quarantineNewFiles stays true for uploads, imports and attachment-panel files.
// Requires the Record Files version that contains withQuarantineCapturedFiles.
.withQuarantineCapturedFiles(Boolean.FALSE)
.withDownloadConfiguration(new DownloadConfiguration("http://127.0.0.1:" + settings.port() + "/qqq/v1", 20 * 1024 * 1024, 2))
.withUploadConfiguration(EchoPhotoCapture.uploadConfiguration());
new RecordFilesQBitProducer().withQBitConfig(recordFiles).produce(instance);
@@ -22,7 +22,10 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** The existing demo password/session validation plus exactly the grants the
** owners' source requires. The validated account selects the role, never a
** request field. Printing and Record Files administration is granted to no one.
** request field. Only the fixed admin account receives Printing administration,
** template configure/publish and Record Files release (so approved scanner photos
** can be released through the normal audited process); the scanner receives none
** of them, and no one receives Record Files configure or review rights.
*******************************************************************************/
public class EchoDemoAuthentication extends DemoAuthenticationModule
{
@@ -40,9 +43,10 @@ public class EchoDemoAuthentication extends DemoAuthenticationModule
/**
** Admin also gets Printing's own administrator permission, which its native
** device pairing/approval/setup processes require, and what is needed to
** prepare and publish templates. The scanner never receives these.
** prepare and publish templates, and Record Files release for approved
** captured photos. The scanner never receives these.
*/
static final List<String> ADMIN_ADDITIONS = List.of("printing_admin", "document-generation.configure", "document-generation.publish");
static final List<String> ADMIN_ADDITIONS = List.of("printing_admin", "document-generation.configure", "document-generation.publish", "record-files.release");
private static volatile String tenant;
@@ -111,7 +111,7 @@ class EchoDemoSettingsTest
{
assertEquals(List.of("echo.use", "printing_operator", "document-generation.view", "document-generation.generate", "document-generation.print",
"record-files.view", "record-files.manage", "echoScan.attach", "testOrder.attach", "testOrderItem.attach", "testOrderEvent.attach"), EchoDemoAuthentication.SCANNER);
assertEquals(List.of("printing_admin", "document-generation.configure", "document-generation.publish"), EchoDemoAuthentication.ADMIN_ADDITIONS);
assertFalse(EchoDemoAuthentication.SCANNER.stream().anyMatch(name -> name.endsWith(".configure") || name.endsWith(".publish") || name.endsWith("admin")));
assertEquals(List.of("printing_admin", "document-generation.configure", "document-generation.publish", "record-files.release"), EchoDemoAuthentication.ADMIN_ADDITIONS);
assertFalse(EchoDemoAuthentication.SCANNER.stream().anyMatch(name -> name.endsWith(".configure") || name.endsWith(".publish") || name.endsWith("admin") || name.endsWith(".release")));
}
}
@@ -179,6 +179,12 @@ class EchoDemoStartupTest
assertFalse(scanner.hasPermission("printing_admin") || scanner.hasPermission("document-generation.publish"));
assertTrue(admin.hasPermission("printing_admin") && admin.hasPermission("document-generation.publish"));
assertFalse(stranger.hasPermission("echo.use") || stranger.hasPermission("printing_operator"));
assertTrue(admin.hasPermission("record-files.release"));
assertFalse(scanner.hasPermission("record-files.release") || stranger.hasPermission("record-files.release"));
assertFalse(admin.hasPermission("record-files.configure") || admin.hasPermission("record-files.review"));
// Revalidation keeps the role derived from the validated user, never widening or dropping it.
assertTrue(EchoDemoAuthentication.grant(instance, admin).hasPermission("record-files.release"));
assertFalse(EchoDemoAuthentication.grant(instance, scanner).hasPermission("record-files.release"));
HttpResponse<String> version = send(port, "/printing/v1/version", null, null);
assertEquals(200, version.statusCode());
@@ -171,6 +171,9 @@ class EchoPhotoCaptureTest
.filter(RecordFilesQBitConfig.class::isInstance).findFirst().orElseThrow();
assertEquals(List.of(EchoQBitProducer.SCAN_TABLE, MobileTestData.ORDERS, MobileTestData.ITEMS, MobileTestData.EVENTS), config.getParentTables());
assertEquals(Set.of("image/jpeg", "image/png"), config.getUploadConfiguration().limits().acceptedMediaTypes());
assertEquals(Boolean.FALSE, config.getQuarantineCapturedFiles(), "approved-device captures are admitted immediately");
assertEquals(Boolean.TRUE, config.getQuarantineNewFiles(), "every other origin keeps the quarantine default");
assertNull(config.getQuarantineGeneratedFiles(), "the generated-output policy is unchanged");
assertTrue(config.getUploadConfiguration().createIdentityProvider() instanceof MobileCaptureIdentityProvider);
assertTrue(config.getUploadConfiguration().createStorage() instanceof LocalStagingChunkStorage);
+6 -5
View File
@@ -16,15 +16,15 @@
<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.26</qqq.version>
<mobile.version>0.1.0-SNAPSHOT</mobile.version>
<!-- Echo's copies per scan needs a Generation build with the print-copies change (appended copies on
DocumentGenerationService.Request and GenerationPorts.PrintRequest, supportsCopies on the printing bridge,
migration 20261102-print-copies-h2.sql). Printing 1.0.0 is released and qualified; only the Generation
cohort still needs a coherent published input. Until that is published at this coordinate, override
generation.version with a coherent build that has it; no private build is named here. -->
<generation.version>0.1.0-SNAPSHOT</generation.version>
<printing.version>1.0.0</printing.version>
<generation.version>1.1.0-RC.1</generation.version>
<printing.version>1.2.0-RC.1</printing.version>
</properties>
<dependencyManagement><dependencies>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
@@ -37,14 +37,15 @@
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-record-files</artifactId><version>${generation.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.2.224</version><scope>test</scope></dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><version>42.7.8</version><scope>test</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>6.0.2</version><scope>test</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>6.0.2</version><scope>test</scope></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.35</version><scope>test</scope></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.38</version><scope>test</scope></dependency>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners-demo</artifactId><version>${mobile.version}</version><scope>test</scope></dependency>
</dependencies>
<build><resources><resource><directory>src/main/resources</directory><filtering>false</filtering></resource><resource><directory>${project.basedir}/../..</directory><filtering>false</filtering><includes><include>LICENSE</include><include>NOTICE</include></includes><targetPath>META-INF/qqq-echo</targetPath></resource></resources><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version><configuration><showWarnings>true</showWarnings><failOnWarning>true</failOnWarning><proc>none</proc><compilerArgs><arg>-Xlint:deprecation</arg><arg>-Xlint:unchecked</arg><arg>-Werror</arg></compilerArgs></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version><dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies><configuration><configLocation>${project.basedir}/../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration><executions><execution><phase>validate</phase><goals><goal>check</goal></goals></execution></executions></plugin>
</plugins></build>
</project>
@@ -10,10 +10,14 @@ import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecke
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
/*******************************************************************************
@@ -35,7 +39,13 @@ public final class EchoPermissions implements CustomPermissionChecker
{
throw new QPermissionDeniedException("Echo access is required; direct state writes are not allowed.");
}
EchoConfig.current().identities.current();
// Browsing the application performs no scan or print operation.
// Process execution, widget and record access still require approved device authority.
boolean processDiscovery = metadata instanceof QProcessMetaData && (action instanceof MetaDataInput || action instanceof ProcessMetaDataInput);
if(!(metadata instanceof QAppMetaData) && !processDiscovery)
{
EchoConfig.current().identities.current();
}
}
catch(Exception failure)
{
@@ -43,3 +53,4 @@ public final class EchoPermissions implements CustomPermissionChecker
}
}
}
@@ -118,7 +118,7 @@ public final class EchoPrintingDestinations implements EchoDestinations
String refusal = refusal(current.mode(), scan);
if(refusal != null)
{
throw new QException(refusal);
throw new EchoScanRejected(refusal);
}
return current.plan();
}
@@ -274,7 +274,8 @@ public final class EchoPrintingDestinations implements EchoDestinations
for(Object item : scene.getJSONArray("elements"))
{
JSONObject text = (JSONObject) item;
if("text".equals(text.optString("type")) && "/record/humanReadable".equals(text.optString("valuePath"))
if(("text".equals(text.optString("type")) || "rasterText".equals(text.optString("type")))
&& "/record/humanReadable".equals(text.optString("valuePath"))
&& text.optInt("y", -1) > barcode.optInt("y", -1))
{
return true;
@@ -293,15 +294,15 @@ public final class EchoPrintingDestinations implements EchoDestinations
{
if((mode == SceneMode.GS1) != "gs1-128".equals(scan.rendererSymbology()))
{
return "GS1-128 scans require the GS1-128 document; that document accepts only GS1-128. Leave Echo and choose the matching document; no print was requested.";
return "GS1-128 scans require the GS1-128 document; that document accepts only GS1-128. No print was requested. Scan a barcode matching the selected document.";
}
if(mode == SceneMode.PLAIN && EchoTemplates.SCANNER_SYMBOLOGIES.contains(scan.rendererSymbology()))
{
return "EAN and UPC need the scanner-fidelity document. Leave Echo and choose it; no print was requested.";
return "EAN and UPC need the scanner-fidelity document. No print was requested. Scan a barcode matching the selected document.";
}
if(mode == SceneMode.SCANNER && scan.aimIdentifier() == null)
{
return "The scanner-fidelity document needs the scanner's AIM identifier and this scan has none. Leave Echo and choose the plain document; no print was requested.";
return "The scanner-fidelity document needs the scanner's AIM identifier and this scan has none. No print was requested. Scan a barcode with its AIM identifier.";
}
return null;
}
@@ -349,8 +350,7 @@ public final class EchoPrintingDestinations implements EchoDestinations
for(Object item : new JSONArray(mediaProfilesJson == null ? "[]" : mediaProfilesJson))
{
PrintingMediaProfile profile = PrintingMediaProfile.parse((JSONObject) item);
choices.add(new MediaChoice(profile.id(), profile.name() + " (" + String.format(Locale.ROOT, "%.1f x %.1f mm, %d dpi",
profile.widthMicrons() / 1000.0, profile.heightMicrons() / 1000.0, profile.dpiX()) + ")"));
choices.add(new MediaChoice(profile.id(), mediaLabel(profile)));
}
}
catch(RuntimeException failure)
@@ -362,6 +362,26 @@ public final class EchoPrintingDestinations implements EchoDestinations
/***************************************************************************
** Prefer exact quarter-inch stock sizes; other stock retains metric units.
** Keep meaningful printer names without repeating a dimensions-only name.
***************************************************************************/
private static String mediaLabel(PrintingMediaProfile profile)
{
boolean inches = profile.widthMicrons() % 6350 == 0 && profile.heightMicrons() % 6350 == 0;
int divisor = inches ? 25400 : 1000;
String width = java.math.BigDecimal.valueOf(profile.widthMicrons()).divide(java.math.BigDecimal.valueOf(divisor)).stripTrailingZeros().toPlainString();
String height = java.math.BigDecimal.valueOf(profile.heightMicrons()).divide(java.math.BigDecimal.valueOf(divisor)).stripTrailingZeros().toPlainString();
String resolution = profile.dpiX() == profile.dpiY() ? String.valueOf(profile.dpiX()) : profile.dpiX() + " × " + profile.dpiY();
String name = profile.name().strip();
String remainder = name.toLowerCase(Locale.ROOT).replaceAll("\\b(inches|inch|in|mm|label|paper|dpi)\\b", "")
.replaceAll("[\\s0-9.,x×()·-]+", "");
boolean generic = remainder.isBlank() || List.of("custom", "default", "media", "userdefined", "windowsqueuedefault").contains(remainder);
return (generic ? "" : name + " · ") + width + " × " + height + (inches ? " in label · " : " mm label · ") + resolution + " dpi";
}
/***************************************************************************
** Select one reported media profile by identity; never guess dimensions.
***************************************************************************/
@@ -19,6 +19,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueRangeBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
@@ -87,7 +88,7 @@ public final class EchoQBitProducer
instance.addTable(scan);
instance.addWidget(new QWidgetMetaData().withName(WIDGET).withType("composite").withLabel("Echo barcode scanner")
.withCodeReference(new QCodeReference(EchoWidget.class)).withPermissionRules(rules()));
instance.addProcess(new QProcessMetaData().withName(PROCESS).withLabel("Echo — scan to print").withPermissionRules(rules())
instance.addProcess(new QProcessMetaData().withName(PROCESS).withLabel("Echo Printing").withIcon(new QIcon().withName("account_tree")).withPermissionRules(rules())
.withCancelStep(new QBackendStepMetaData().withName("cancel").withCode(new QCodeReference(EchoSteps.Cancel.class)))
.withStep(new QBackendStepMetaData().withName("begin").withCode(new QCodeReference(EchoSteps.Begin.class)))
.withStep(new QFrontendStepMetaData().withName("setup").withLabel("Choose printer, paper or label, and document")
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.examples.echo;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Only deterministic pre-print barcode/document incompatibility uses this type.
*******************************************************************************/
final class EchoScanRejected extends QException
{
/***************************************************************************
** The message contains guidance, never a scanned payload.
***************************************************************************/
EchoScanRejected(String message)
{
super(message);
}
}
@@ -175,12 +175,21 @@ public final class EchoService
{
requireText(runId, 36);
MobileDeviceAuthorization.Identity identity = identity();
Scan scan = Scan.from(context);
if(!identity.actorId().equals(context.get("userId")) || !identity.deviceId().equals(context.get("deviceId"))
if(context == null || !identity.actorId().equals(context.get("userId")) || !identity.deviceId().equals(context.get("deviceId"))
|| !EchoQBitProducer.APP.equals(context.get("appId")) || !EchoQBitProducer.PROCESS.equals(context.get("processName")))
{
throw new QException("Scan context does not match this approved scanner sign-in.");
}
Scan scan;
try
{
scan = Scan.from(context);
}
catch(EchoScanRejected rejection)
{
store.requireUnadmitted(runId, identity, requireText(context.get("scanId"), 64), claimedCopies);
throw rejection;
}
EchoStore.Admission admission = store.claim(runId, identity, scan, claimedCopies);
if(admission.acceptedJob() != null)
{
@@ -215,7 +224,16 @@ public final class EchoService
{
throw new QException("This Echo run has no chosen document. Leave Echo and choose again.");
}
EchoDestinations.Plan plan = destinations.plan(identity, destination, admission.document(), scan);
EchoDestinations.Plan plan;
try
{
plan = destinations.plan(identity, destination, admission.document(), scan);
}
catch(EchoScanRejected rejection)
{
store.reject(runId, admission.scanId(), scan.eventId());
throw rejection;
}
if(plan == null || plan.templateId() == null || plan.templateId() < 1 || plan.templateRevisionId() == null || plan.templateRevisionId() < 1
|| plan.format() == null || !stockMatches(plan.stock(), destination))
{
@@ -375,7 +393,7 @@ public final class EchoService
case "EAN8", "LABEL-TYPE-EAN8" -> "ean8";
case "UPCA", "LABEL-TYPE-UPCA" -> "upca";
case "UPCE0", "LABEL-TYPE-UPCE0" -> "upce";
default -> throw new QException("This barcode type cannot yet be reproduced faithfully by Echo. No print was requested.");
default -> throw new EchoScanRejected("This barcode type cannot yet be reproduced faithfully by Echo. No print was requested.");
};
///////////////////////////////////////////////////////////////////////////////////////
// the qualified first-physical profile is printable ASCII 0x20..0x7E for all types; //
@@ -385,11 +403,18 @@ public final class EchoService
if(gs1)
{
renderer = "gs1-128";
EchoGs1.parse(value);
try
{
EchoGs1.parse(value);
}
catch(QException invalidGs1)
{
throw new EchoScanRejected(invalidGs1.getMessage());
}
}
else if("gs1-128".equals(renderer) || value.chars().anyMatch(character -> character < 0x20 || character > 0x7E))
{
throw new QException("This scan needs control/binary/GS1 or non-ASCII payload handling that Echo has not qualified; only printable ASCII is supported. No print was requested.");
throw new EchoScanRejected("This scan needs control/binary/GS1 or non-ASCII payload handling that Echo has not qualified; only printable ASCII is supported. No print was requested.");
}
String aim = requireFidelityMetadata(context, renderer, value);
return new Scan(event, value, type, renderer, source, captured, mobileScan, received, aim);
@@ -409,23 +434,23 @@ public final class EchoService
boolean gs1 = "gs1-128".equals(renderer);
if(aim != null && !(gs1 ? "]C1" : PLAIN_AIM.get(renderer)).equals(aim))
{
throw new QException("This scan reports a barcode mode (GS1, ECI, structured or full-ASCII) that Echo cannot reproduce faithfully. No print was requested.");
throw new EchoScanRejected("This scan reports a barcode mode (GS1, ECI, structured or full-ASCII) that Echo cannot reproduce faithfully. No print was requested.");
}
if(EchoTemplates.SCANNER_SYMBOLOGIES.contains(renderer))
{
if(aim == null)
{
throw new QException("EAN and UPC need the scanner's AIM identifier to be reproduced faithfully, and this scan has none. No print was requested.");
throw new EchoScanRejected("EAN and UPC need the scanner's AIM identifier to be reproduced faithfully, and this scan has none. No print was requested.");
}
if(!validRetailValue(renderer, value))
{
throw new QException("This EAN or UPC value has the wrong length, number system or check digit. No print was requested.");
throw new EchoScanRejected("This EAN or UPC value has the wrong length, number system or check digit. No print was requested.");
}
}
Object raw = context.get("rawDataBase64");
if(gs1 && raw == null)
{
throw new QException("GS1-128 duplication needs the scanner's original raw bytes and AIM identifier. No print was requested.");
throw new EchoScanRejected("GS1-128 duplication needs the scanner's original raw bytes and AIM identifier. No print was requested.");
}
if(raw != null)
{
@@ -93,12 +93,16 @@ public final class EchoSteps
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
String run = null;
java.util.Map<String, java.io.Serializable> context = null;
try
{
run = boundRun(input, output);
Integer claimed = copies(input.getValue("copies"));
var context = MobileScannerActions.capture(input, EchoQBitProducer.APP);
context = MobileScannerActions.capture(input, EchoQBitProducer.APP);
EchoService.Acceptance accepted = EchoConfig.current().service.print(run, context, claimed);
output.addValue("scanRejected", false);
output.addValue("rejectedEventId", null);
output.addValue("rejectionMessage", null);
output.addValue("scanContext", new HashMap<>(context));
output.addValue("scanId", context.get("scanId"));
output.addValue("echoScanId", accepted.scanId());
@@ -110,6 +114,19 @@ public final class EchoSteps
output.addValue(EchoQBitProducer.WIDGET, new EchoWidget.Data(accepted.copies()));
output.getProcessState().setNextStepName("scan");
}
catch(EchoScanRejected rejection)
{
output.addValue("scanRejected", true);
output.addValue("rejectedEventId", context.get("eventId"));
output.addValue("rejectionMessage", rejection.getMessage());
output.addValue("scanSaved", false);
for(String name : java.util.List.of("acknowledgedEventId", "scanId", "echoScanId", "printJobId", "printStatus", "scanContext"))
{
output.addValue(name, null);
}
restorePinnedCopies(run, output);
output.getProcessState().setNextStepName("scan");
}
catch(QException failure)
{
restorePinnedCopies(run, output);
@@ -76,9 +76,56 @@ final class EchoStore
throw new QException("Echo setup is already complete or closed. Leave Echo to choose again.");
}
write(connection, "UPDATE echo_run SET state='OPEN',printerRecordId=?,mediaId=?,mediaRevision=?,widthMicrons=?,heightMicrons=?,dpiX=?,dpiY=?,templateId=?,templateRevisionId=?,copies=? WHERE id=?",
destination.printerRecordId(), destination.mediaId(), destination.mediaRevision(), Integer.toString(destination.widthMicrons()),
Integer.toString(destination.heightMicrons()), Integer.toString(destination.dpiX()), Integer.toString(destination.dpiY()),
document.templateId().toString(), document.templateRevisionId().toString(), Integer.toString(copies), id);
destination.printerRecordId(), destination.mediaId(), destination.mediaRevision(), destination.widthMicrons(),
destination.heightMicrons(), destination.dpiX(), destination.dpiY(),
document.templateId(), document.templateRevisionId(), copies, id);
connection.commit();
}
catch(Exception failure)
{
rollback(connection, failure);
if(failure instanceof QException expected)
{
throw expected;
}
throw unavailable();
}
}
catch(SQLException failure)
{
throw unavailable();
}
}
/***************************************************************************
** Canonical capture already retains invalid barcode evidence. A pre-admission
** refusal is safe only for an owned open run with no uncertain or prior work.
***************************************************************************/
void requireUnadmitted(String runId, MobileDeviceAuthorization.Identity identity, String scanId, Integer copies) throws QException
{
try(Connection connection = dataSource.getConnection())
{
connection.setAutoCommit(false);
try
{
Run run = run(connection, runId, identity);
if(!"OPEN".equals(run.state()) || run.pending() != null || copies != null && copies != run.copies())
{
throw new QException("This run cannot acknowledge a new rejected scan; resolve its current state first.");
}
try(PreparedStatement query = connection.prepareStatement("SELECT id FROM echo_scan WHERE id=?"))
{
query.setString(1, scanId);
try(ResultSet rows = query.executeQuery())
{
if(rows.next())
{
throw new QException("This scan has prior work and requires reconciliation.");
}
}
}
connection.commit();
}
catch(Exception failure)
@@ -167,6 +214,10 @@ final class EchoStore
connection.commit();
return new Admission(id, destination, acceptedJob, false, pinned, run.document(), run.copies());
}
if("REJECTED".equals(state) && pinned == null)
{
throw new EchoScanRejected("This barcode was rejected before printing. Scan a barcode matching the selected document.");
}
if(run.pending() != null && !run.pending().equals(scan.eventId()))
{
throw new QException("The previous print is unconfirmed. Retry that scan or leave Echo before starting another.");
@@ -220,11 +271,11 @@ final class EchoStore
try(Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(
"UPDATE echo_scan SET state='IN_FLIGHT',templateId=?,templateRevisionId=?,format=?,topMicrons=?,rightMicrons=?,bottomMicrons=?,leftMicrons=? WHERE id=? AND state IN ('IN_FLIGHT','ATTENTION') AND templateId IS NULL"))
{
String[] values = {plan.templateId().toString(), plan.templateRevisionId().toString(), plan.format().name(), plan.stock().topMicrons().toString(),
plan.stock().rightMicrons().toString(), plan.stock().bottomMicrons().toString(), plan.stock().leftMicrons().toString(), scanId};
Object[] values = {plan.templateId(), plan.templateRevisionId(), plan.format().name(), plan.stock().topMicrons(),
plan.stock().rightMicrons(), plan.stock().bottomMicrons(), plan.stock().leftMicrons(), scanId};
for(int index = 0; index < values.length; index++)
{
statement.setString(index + 1, values[index]);
statement.setObject(index + 1, values[index]);
}
if(statement.executeUpdate() != 1)
{
@@ -268,6 +319,43 @@ final class EchoStore
/***************************************************************************
** The pin and rejection compare-and-set compete; only one can win. Preserve
** the rejected capture, and release only its exact pending run binding.
***************************************************************************/
void reject(String runId, String scanId, String eventId) throws QException
{
try(Connection connection = dataSource.getConnection())
{
connection.setAutoCommit(false);
try(PreparedStatement statement = connection.prepareStatement(
"UPDATE echo_scan SET state='REJECTED' WHERE id=? AND runId=? AND eventId=? AND state IN ('IN_FLIGHT','ATTENTION') AND templateId IS NULL AND templateRevisionId IS NULL AND printJobId IS NULL"))
{
statement.setString(1, scanId);
statement.setString(2, runId);
statement.setString(3, eventId);
if(statement.executeUpdate() != 1)
{
connection.rollback();
throw new QException("This scan requires reconciliation; it cannot be acknowledged as rejected.");
}
write(connection, "UPDATE echo_run SET pendingEventId=NULL WHERE id=? AND pendingEventId=?", runId, eventId);
connection.commit();
}
catch(SQLException failure)
{
rollback(connection, failure);
throw unavailable();
}
}
catch(SQLException failure)
{
throw unavailable();
}
}
/***************************************************************************
** A failed or lost response keeps its event identity and pending run binding.
** No automatic retry, replacement print request or success acknowledgment.
@@ -378,13 +466,13 @@ final class EchoStore
/***************************************************************************
** All SQL identifiers are fixed source; external values are bound parameters.
***************************************************************************/
private static void write(Connection connection, String sql, String... values) throws SQLException
private static void write(Connection connection, String sql, Object... values) throws SQLException
{
try(PreparedStatement statement = connection.prepareStatement(sql))
{
for(int index = 0; index < values.length; index++)
{
statement.setString(index + 1, values[index]);
statement.setObject(index + 1, values[index]);
}
statement.executeUpdate();
}
@@ -0,0 +1,29 @@
-- Copyright (c) 2026 Kingsrook, LLC. SPDX-License-Identifier: Apache-2.0
-- Host migration input, not automatically executed by metadata registration.
CREATE TABLE IF NOT EXISTS echo_run (
id VARCHAR(36) PRIMARY KEY, actorId VARCHAR(200) NOT NULL, deviceId VARCHAR(36) NOT NULL,
publicSessionId VARCHAR(36) NOT NULL, printerRecordId VARCHAR(200) NOT NULL, mediaId VARCHAR(200) NOT NULL,
mediaRevision VARCHAR(64) NOT NULL DEFAULT '', widthMicrons INT NOT NULL DEFAULT 0, heightMicrons INT NOT NULL DEFAULT 0,
dpiX INT NOT NULL DEFAULT 0, dpiY INT NOT NULL DEFAULT 0,
state VARCHAR(30) NOT NULL, pendingEventId VARCHAR(36)
);
-- Preserving migration: the document chosen once at setup. Existing rows keep NULL and are never cleared.
ALTER TABLE echo_run ADD COLUMN IF NOT EXISTS templateId INT;
ALTER TABLE echo_run ADD COLUMN IF NOT EXISTS templateRevisionId INT;
CREATE TABLE IF NOT EXISTS echo_scan (
id VARCHAR(64) PRIMARY KEY, runId VARCHAR(36) NOT NULL REFERENCES echo_run(id),
eventId VARCHAR(36) NOT NULL, actorId VARCHAR(200) NOT NULL, deviceId VARCHAR(36) NOT NULL, mobileScanId VARCHAR(64) NOT NULL,
barcode VARCHAR(4096) NOT NULL, symbology VARCHAR(200) NOT NULL, rendererSymbology VARCHAR(100) NOT NULL,
scanSource VARCHAR(200) NOT NULL, capturedAt VARCHAR(40) NOT NULL, receivedAt VARCHAR(40) NOT NULL, state VARCHAR(30) NOT NULL,
printJobId VARCHAR(200), UNIQUE(runId, eventId)
,templateId INT, templateRevisionId INT, format VARCHAR(10), topMicrons INT, rightMicrons INT, bottomMicrons INT, leftMicrons INT
);
-- Preserving migration: the scanner's AIM identifier (for example ]E0), when the capture carried one. Existing rows keep NULL
-- (no AIM was recorded) and are never rewritten. Other databases need the equivalent additive nullable VARCHAR(3) column.
ALTER TABLE echo_scan ADD COLUMN IF NOT EXISTS aimIdentifier VARCHAR(3);
-- Preserving migration: copies printed per scan, chosen once at setup. Existing runs keep exactly one copy (their prior behaviour).
-- Other databases need the equivalent additive INT NOT NULL DEFAULT 1 column.
ALTER TABLE echo_run ADD COLUMN IF NOT EXISTS copies INT NOT NULL DEFAULT 1;
-- Derived canonical GS1 input and HRI. Original barcode is never rewritten. Other hosts need equivalent nullable columns.
ALTER TABLE echo_scan ADD COLUMN IF NOT EXISTS rendererValue VARCHAR(4096);
ALTER TABLE echo_scan ADD COLUMN IF NOT EXISTS humanReadable VARCHAR(4096);
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.examples.echo;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
/** Navigation does not perform scanning; execution retains approved-device checks. */
class EchoAppVisibilityTest
{
/** Remove the synthetic session after each test. */
@AfterEach
void clear()
{
QContext.clear();
}
/** Authorized navigation must not require hardware enrollment. */
@Test
void authorizedBrowserCanSeeAppWithoutADevice()
{
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")).withPermissions("echo.use"));
assertDoesNotThrow(() -> new EchoPermissions().checkPermissionsThrowing(new AbstractActionInput(), new QAppMetaData().withName("echo")));
}
/** Navigation remains permission protected. */
@Test
void browserWithoutEchoPermissionCannotSeeApp()
{
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")));
assertThrows(QPermissionDeniedException.class,
() -> new EchoPermissions().checkPermissionsThrowing(new AbstractActionInput(), new QAppMetaData().withName("echo")));
}
/** Seeing the app never authorizes scanning without an approved device. */
@Test
void browserWithoutApprovedDeviceCannotExecuteScanProcess()
{
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")).withPermissions("echo.use"));
assertThrows(QPermissionDeniedException.class,
() -> new EchoPermissions().checkPermissionsThrowing(new RunProcessInput(), new QProcessMetaData().withName("echoPrint")));
}
/** Catalog discovery exposes the process without performing its device-bound setup. */
@Test
void authorizedBrowserCanDiscoverProcessWithoutADevice()
{
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")).withPermissions("echo.use"));
assertDoesNotThrow(() -> new EchoPermissions().checkPermissionsThrowing(new MetaDataInput(), new QProcessMetaData().withName("echoPrint")));
assertDoesNotThrow(
() -> new EchoPermissions().checkPermissionsThrowing(new ProcessMetaDataInput(), new QProcessMetaData().withName("echoPrint")));
}
/** Catalog discovery still requires Echo permission. */
@Test
void browserWithoutPermissionCannotDiscoverProcess()
{
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")));
assertThrows(QPermissionDeniedException.class,
() -> new EchoPermissions().checkPermissionsThrowing(new MetaDataInput(), new QProcessMetaData().withName("echoPrint")));
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.examples.echo;
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.assertNull;
/*******************************************************************************
** Published centered GS1 scenes retain the same data and placement contract.
*******************************************************************************/
class EchoCenteredLabelTest
{
static final String SCENE = """
{"schema":"qqq-zpl-scene/1","elements":[
{"type":"barcode","symbology":"gs1-128","valuePath":"/record/rendererValue","quiet":10,"x":36,"y":36,
"fit":{"widthMicrons":95504,"heightMicrons":16000},"align":"center"},
{"type":"rasterText","valuePath":"/record/humanReadable","x":36,"y":206,"fontAsset":"forge-echo-abel-regular",
"fontSize":44,"width":1128,"height":58,"align":"center"}]}
""";
/** Both renderer-supported human-readable element forms are admitted. */
@Test
void centeredRasterAndLegacyTextAreBothGs1Documents()
{
assertEquals(EchoPrintingDestinations.SceneMode.GS1, EchoPrintingDestinations.sceneMode(SCENE));
assertEquals(EchoPrintingDestinations.SceneMode.GS1, EchoPrintingDestinations.sceneMode(SCENE.replace("rasterText", "text")));
}
/** Text remains derived from GS1 data and below the barcode origin. */
@Test
void wrongHumanReadableBindingOrPlacementStillRefuses()
{
for(String type : java.util.List.of("text", "rasterText"))
{
JSONObject wrong = new JSONObject(SCENE.replace("rasterText", type));
wrong.getJSONArray("elements").getJSONObject(1).put("valuePath", "/record/barcode");
assertNull(EchoPrintingDestinations.sceneMode(wrong.toString()));
for(int y : java.util.List.of(35, 36))
{
JSONObject above = new JSONObject(SCENE.replace("rasterText", type));
above.getJSONArray("elements").getJSONObject(1).put("y", y);
assertNull(EchoPrintingDestinations.sceneMode(above.toString()));
}
}
}
/** Raster text never relaxes the barcode binding or bounded fit requirement. */
@Test
void barcodeBindingAndFitRemainMandatory()
{
assertNull(EchoPrintingDestinations.sceneMode(SCENE.replace("/record/rendererValue", "/record/barcode")));
JSONObject scene = new JSONObject(SCENE);
scene.getJSONArray("elements").getJSONObject(0).remove("fit");
assertNull(EchoPrintingDestinations.sceneMode(scene.toString()));
}
/** Human-readable stock dimensions preserve native profile identities. */
@Test
void readableStockNamesPreserveIdentityAndBothResolutions() throws Exception
{
assertEquals("4 × 1 in label · 300 dpi", label("4 x 1", 101600, 25400, 300, 300));
assertEquals("4 × 1 in label · 300 dpi", label("Windows queue default (101.6 × 25.4 mm, 300 × 300 DPI)", 101600, 25400, 300, 300));
assertEquals("100 × 50 mm label · 203 dpi", label("100 x 50 mm", 100000, 50000, 203, 203));
assertEquals("Shipping · 4 × 1 in label · 300 × 600 dpi", label("Shipping", 101600, 25400, 300, 600));
}
/** Build one synthetic printer profile using the existing owner wire schema. */
private static String label(String name, int width, int height, int dpiX, int dpiY) throws Exception
{
JSONObject profile = new JSONObject().put("id", "synthetic-media").put("revision", "a".repeat(64)).put("name", name)
.put("widthMicrons", width).put("heightMicrons", height).put("dpiX", dpiX).put("dpiY", dpiY)
.put("source", "WINDOWS_QUEUE_DEFAULT").put("applyMode", "DRIVER_DEFAULT");
var choice = EchoPrintingDestinations.choices(new org.json.JSONArray().put(profile).toString()).getFirst();
assertEquals("synthetic-media", choice.id());
return choice.label();
}
}
@@ -191,9 +191,9 @@ class EchoOwnerAdaptersTest
List<EchoDestinations.MediaChoice> choices = EchoPrintingDestinations.choices(PROFILES);
assertEquals(1, choices.size());
assertEquals("4x6", choices.getFirst().id());
assertTrue(choices.getFirst().label().contains("101.6 x 152.4 mm, 300 dpi"));
assertEquals(1, EchoMediaChoices.select(choices, null, "4 x", 0, 50).size());
assertEquals(0, EchoMediaChoices.select(choices, null, "label", 0, 50).size());
assertEquals("4 × 6 in label · 300 dpi", choices.getFirst().label());
assertEquals(1, EchoMediaChoices.select(choices, null, "4 ×", 0, 50).size());
assertEquals(1, EchoMediaChoices.select(choices, null, "label", 0, 50).size());
assertEquals(0, EchoMediaChoices.select(choices, List.of("2x1"), null, 0, 50).size());
assertEquals("4x6", EchoMediaChoices.select(choices, List.of("4x6"), null, 0, 50).getFirst().getId());
assertEquals(0, EchoMediaChoices.select(choices, null, null, 1, 50).size());
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.examples.echo;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import com.kingsrook.qbits.documentgeneration.GenerationPorts;
import com.kingsrook.qbits.mobilescanners.EchoMobileFixture;
import com.kingsrook.qbits.mobilescanners.MobileDeviceAuthorization;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.postgresql.ds.PGSimpleDataSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Isolated PostgreSQL 17 contract: numeric bindings, preserving DDL and pin CAS.
*******************************************************************************/
class EchoPostgresTest
{
/***************************************************************************
** No production database credentials are used; the caller supplies a fixture.
***************************************************************************/
@Test
@EnabledIfSystemProperty(named = "echo.postgres.url", matches = ".+")
void setupAndPinUseNativeIntegerBindings() throws Exception
{
PGSimpleDataSource dataSource = new PGSimpleDataSource();
dataSource.setURL(System.getProperty("echo.postgres.url"));
dataSource.setUser("postgres");
String schema = "echo_fixture_" + UUID.randomUUID().toString().replace("-", "");
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
{
assertEquals(17, connection.getMetaData().getDatabaseMajorVersion());
statement.execute("CREATE SCHEMA " + schema);
}
dataSource.setCurrentSchema(schema);
try
{
String sql;
try(var resource = getClass().getResourceAsStream("/db/echo-postgres.sql"))
{
sql = new String(resource.readAllBytes(), StandardCharsets.UTF_8);
}
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
{
statement.execute(sql);
}
new EchoMobileFixture(new QInstance(), "jdbc:h2:mem:" + schema + ";DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1");
var identity = MobileDeviceAuthorization.captureCurrent().orElseThrow();
EchoStore store = new EchoStore(dataSource);
store.begin("synthetic-run", identity);
var destination = new EchoDestinations.Destination("synthetic-printer", "4x6", "synthetic-revision", 101600, 152400, 300, 300);
store.select("synthetic-run", identity, destination, new EchoDestinations.Document(7, 11), 3);
assertEquals(3, store.copies("synthetic-run", identity));
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
{
statement.execute("INSERT INTO echo_scan(id,runId,eventId,actorId,deviceId,mobileScanId,barcode,symbology,rendererSymbology,scanSource,capturedAt,receivedAt,state)"
+ " VALUES ('synthetic-scan','synthetic-run','synthetic-event','synthetic-actor','synthetic-device','synthetic-mobile-scan','001234','CODE128','code128','fixture','fixture','fixture','IN_FLIGHT')");
}
var plan = new EchoDestinations.Plan(7, 11, new GenerationPorts.Stock(101600, 152400, 3000, 3000, 3000, 3000, 300), GenerationPorts.Format.ZPL);
store.requireUnadmitted("synthetic-run", identity, "new-scan", 3);
assertThrows(QException.class, () -> store.requireUnadmitted("synthetic-run", identity, "synthetic-scan", 3));
store.pin("synthetic-scan", plan);
assertThrows(EchoStore.PinNotWon.class, () -> store.pin("synthetic-scan", plan));
assertThrows(QException.class, () -> store.reject("synthetic-run", "synthetic-scan", "synthetic-event"));
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
{
statement.execute("INSERT INTO echo_scan(id,runId,eventId,actorId,deviceId,mobileScanId,barcode,symbology,rendererSymbology,scanSource,capturedAt,receivedAt,state)"
+ " VALUES ('rejected-scan','synthetic-run','rejected-event','synthetic-actor','synthetic-device','rejected-mobile','synthetic','QRCODE','qr','fixture','fixture','fixture','IN_FLIGHT')");
statement.execute("UPDATE echo_run SET pendingEventId='rejected-event' WHERE id='synthetic-run'");
}
assertThrows(QException.class, () -> store.requireUnadmitted("synthetic-run", identity, "new-scan", 3));
store.reject("synthetic-run", "rejected-scan", "rejected-event");
assertThrows(EchoStore.PinNotWon.class, () -> store.pin("rejected-scan", plan));
store.requireUnadmitted("synthetic-run", identity, "new-scan", 3);
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
{
statement.execute(sql);
try(var rows = statement.executeQuery("SELECT templateId,templateRevisionId,topMicrons,barcode,rendererValue,humanReadable FROM echo_scan WHERE id='synthetic-scan'"))
{
assertTrue(rows.next());
assertEquals(7, rows.getInt(1));
assertEquals(11, rows.getInt(2));
assertEquals(3000, rows.getInt(3));
assertEquals("001234", rows.getString(4));
assertEquals(null, rows.getString(5));
assertEquals(null, rows.getString(6));
}
}
}
finally
{
EchoMobileFixture.clear();
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
{
statement.execute("DROP SCHEMA " + schema + " CASCADE");
}
}
}
}
@@ -67,6 +67,7 @@ class EchoTest
private Runnable afterAccept;
private Runnable afterLookup;
private QInstance metadata;
private boolean rejectQr;
private boolean documentWithdrawn;
private int documentRevisionShift;
private final List<Integer> plannedDocuments = new java.util.concurrent.CopyOnWriteArrayList<>();
@@ -156,6 +157,10 @@ class EchoTest
@Override
public Plan plan(MobileDeviceAuthorization.Identity identity, Destination destination, Document document, EchoService.Scan scan) throws QException
{
if(rejectQr && "QRCODE".equals(scan.symbology()))
{
throw new EchoScanRejected("Wrong barcode type; no print was requested.");
}
plannedDocuments.add(document.templateId());
if(!authorizeDocument(identity, destination, document.templateId()).equals(document))
{
@@ -273,6 +278,21 @@ class EchoTest
** Setup is a real frontend, then both scans return to the same scan step.
***************************************************************************/
@Test
void echoPrintingUsesProcessIdentityAndIcon()
{
var process = QContext.getQInstance().getProcess(EchoQBitProducer.PROCESS);
assertEquals("echoPrint", process.getName());
assertEquals("Echo Printing", process.getLabel());
assertEquals("account_tree", process.getIcon().getName());
assertEquals("Echo Scans", QContext.getQInstance().getTable(EchoQBitProducer.SCAN_TABLE).getLabel());
}
/*******************************************************************************
** Existing runtime behavior remains unchanged by presentation metadata.
*******************************************************************************/
@Test
void processChoosesOnceAndLoopsWithExactSymbology() throws Exception
{
RunProcessOutput setup = new RunProcessAction().execute(new RunProcessInput().withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS));
@@ -310,6 +330,93 @@ class EchoTest
/***************************************************************************
** Rejection releases scanning without losing setup or retaining receipts.
***************************************************************************/
@Test
void rejectedBarcodeKeepsSetupAndClearsReceipts() throws Exception
{
rejectQr = true;
RunProcessOutput setup = new RunProcessAction().execute(new RunProcessInput().withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS));
RunProcessOutput ready = new RunProcessAction().execute(resume(setup, "setup").withValue("printerId", "printer").withValue("mediaId", "4x6").withValue("templateId", 7).withValue("copies", 3));
for(int index = 0; index < 2; index++)
{
RunProcessInput wrong = resume(ready, "scan");
wrong.getValues().putAll(event("QRCODE", "synthetic-wrong-type"));
String rejectedEvent = wrong.getValueString("eventId");
RunProcessOutput rejected = new RunProcessAction().execute(wrong);
assertEquals(true, rejected.getValue("scanRejected"));
assertEquals(rejectedEvent, rejected.getValue("rejectedEventId"));
assertEquals(false, rejected.getValue("scanSaved"));
assertNull(rejected.getValue("acknowledgedEventId"));
assertNull(rejected.getValue("scanId"));
assertNull(rejected.getValue("printJobId"));
assertEquals(index, requests.size());
for(String field : List.of("echoRunId", "printerId", "mediaId", "templateId", "copies"))
{
assertEquals(ready.getValue(field), rejected.getValue(field));
}
ready = scan(rejected, "CODE128", "synthetic-valid");
assertEquals(true, ready.getValue("scanSaved"));
assertEquals(false, ready.getValue("scanRejected"));
assertNull(ready.getValue("rejectedEventId"));
assertNull(ready.getValue("rejectionMessage"));
assertEquals(index + 1, requests.size());
}
}
/***************************************************************************
** Rejected replays never print; a won pin cannot be relabeled as rejection.
***************************************************************************/
@Test
void rejectionCannotReleasePinnedWorkAndReplayRemainsRejected() throws Exception
{
rejectQr = true;
String run = config.service.start("printer", "4x6", 7, 3);
var wrong = captured("QRCODE", "synthetic-wrong-type");
assertThrows(EchoScanRejected.class, () -> config.service.print(run, wrong));
assertThrows(EchoScanRejected.class, () -> config.service.print(run, wrong));
assertTrue(requests.isEmpty());
var valid = captured("CODE128", "synthetic-valid");
var receipt = config.service.print(run, valid);
var store = new EchoStore(dataSource);
assertThrows(QException.class, () -> store.reject(run, receipt.scanId(), receipt.eventId()));
assertEquals(receipt, config.service.print(run, valid));
assertEquals(1, requests.size());
assertThrows(QException.class, () -> store.pin(wrong.get("scanId").toString(), acceptedPlans.get("echo:" + receipt.scanId())));
}
/***************************************************************************
** Unsupported types and invalid GS1 return ready without losing setup.
***************************************************************************/
@Test
void unsupportedAndInvalidGs1RecoverButUncertainWorkDoesNot() throws Exception
{
RunProcessOutput setup = new RunProcessAction().execute(new RunProcessInput().withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS));
RunProcessOutput ready = new RunProcessAction().execute(resume(setup, "setup").withValue("printerId", "printer").withValue("mediaId", "4x6").withValue("templateId", 7));
RunProcessOutput unsupported = scan(ready, "MAXICODE", "synthetic");
assertEquals(true, unsupported.getValue("scanRejected"));
RunProcessInput invalid = resume(unsupported, "scan");
invalid.getValues().putAll(event("CODE128", "invalid-gs1"));
invalid.addValue("aimIdentifier", "]C1");
invalid.addValue("rawDataBase64", java.util.Base64.getEncoder().encodeToString("invalid-gs1".getBytes(StandardCharsets.US_ASCII)));
RunProcessOutput rejected = new RunProcessAction().execute(invalid);
assertEquals(true, rejected.getValue("scanRejected"));
assertTrue(requests.isEmpty());
assertEquals(true, scan(rejected, "CODE128", "synthetic-valid").getValue("scanSaved"));
String run = ready.getValueString("echoRunId");
uncertain = true;
assertThrows(QException.class, () -> config.service.print(run, captured("CODE128", "uncertain")));
QException blocked = assertThrows(QException.class, () -> config.service.print(run, captured("MAXICODE", "unsupported")));
assertFalse(blocked instanceof EchoScanRejected);
}
/***************************************************************************
** Runs without a chosen count print exactly one copy, and the run stores it.
***************************************************************************/
@@ -1481,6 +1588,13 @@ class EchoTest
config.service.close(run);
desktop("operator", true);
assertEquals("000001", source(acceptedScan.scanId()).getValueString("barcode"));
var metadataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput();
metadataInput.setProcessName(EchoQBitProducer.PROCESS);
var processView = new com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction().execute(metadataInput);
assertTrue(processView.getProcess().getHasPermission());
assertThrows(QException.class, () -> new RunProcessAction().execute(new RunProcessInput()
.withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS)));
assertThrows(QException.class, () -> config.service.begin(UUID.randomUUID().toString()));
assertThrows(QException.class, () -> config.service.print(run, context));
assertThrows(QException.class, () -> new EchoPermissions().checkPermissionsThrowing(
new RunProcessInput().withProcessName(EchoQBitProducer.PROCESS), QContext.getQInstance().getProcess(EchoQBitProducer.PROCESS)));
@@ -1617,3 +1731,5 @@ class EchoTest
"eventId", UUID.randomUUID().toString(), "capturedAt", "2026-11-01T17:00:00Z"));
}
}
@@ -111,7 +111,23 @@ class EchoHttpFixtureTest
assertTrue(firstValues.getString("printJobId").startsWith("fixture-job-"));
JSONObject retry = complete(form("/qqq/v1/processes/echoPrint/" + process + "/step/scan", scanner, firstEvent, true), EchoQBitProducer.PROCESS, scanner, true);
assertEquals(firstValues.getString("printJobId"), retry.getJSONObject("values").getString("printJobId"));
JSONObject unsupported = event("MAXICODE");
JSONObject rejected = complete(form("/qqq/v1/processes/echoPrint/" + process + "/step/scan", scanner, unsupported, true), EchoQBitProducer.PROCESS, scanner, true);
assertEquals("scan", rejected.getString("nextStep"));
JSONObject rejection = rejected.getJSONObject("values");
assertTrue(rejection.getBoolean("scanRejected"));
assertFalse(rejection.getBoolean("scanSaved"));
assertEquals(unsupported.getString("eventId"), rejection.getString("rejectedEventId"));
assertFalse(rejection.getString("rejectionMessage").isBlank());
assertEquals(run, rejection.getString("echoRunId"));
for(String cleared : java.util.List.of("acknowledgedEventId", "scanId", "printJobId"))
{
assertTrue(rejection.isNull(cleared), "No prior positive receipt survives rejection.");
}
JSONObject second = complete(form("/qqq/v1/processes/echoPrint/" + process + "/step/scan", scanner, event("QRCODE"), true), EchoQBitProducer.PROCESS, scanner, true);
assertFalse(second.getJSONObject("values").getBoolean("scanRejected"));
assertTrue(second.getJSONObject("values").isNull("rejectedEventId"));
assertTrue(second.getJSONObject("values").isNull("rejectionMessage"));
assertEquals(run, second.getJSONObject("values").getString("echoRunId"));
assertEquals("scan", second.getString("nextStep"));
JSONArray scans = query(EchoQBitProducer.SCAN_TABLE, scanner);
@@ -58,8 +58,13 @@ function Initialize-IsolatedScannerSource
Remove-IsolatedBuildDirectory -ModuleRoot $ModuleRoot -LeafName 'scanner-source'
$destination = Join-Path $ModuleRoot 'target/scanner-source'
New-Item -ItemType Directory -Path $destination -Force | Out-Null
foreach($name in @('pom.xml', 'mvnw.cmd', 'mvnw', '.mvn', 'src', 'checkstyle'))
foreach($name in @('mvnw.cmd', 'mvnw', '.mvn', 'checkstyle'))
{
Copy-Item -LiteralPath (Join-Path $ScannerRoot $name) -Destination $destination -Recurse -Force
}
# The scanner base module (pom.xml, src) lives in base/ since the reactor layout; the scratch tree keeps the single-module shape.
foreach($name in @('pom.xml', 'src'))
{
Copy-Item -LiteralPath (Join-Path (Join-Path $ScannerRoot 'base') $name) -Destination $destination -Recurse -Force
}
}
+15 -8
View File
@@ -70,16 +70,23 @@ OpenAPI identifiers are `getQbitRecordFilesCapabilities`,
`postQbitRecordFilesUploadsByUploadIdCancel`. Native invoker/credential-scope IDs
instead use `METHOD /qbit/record-files/...`; capability gating must distinguish them.
## Publication preparation
## Publication
This module is a standard QBit module (parent `qbit-build-parent` 2.1.0, Apache-2.0
metadata, canonical Gitea URLs) and produces a separate JAR from the root
scanner; the root has no module or dependency on it. Preparing the POM is not
publication. Publishing still requires a full immutable (non-SNAPSHOT) dependency set
and the normal hosted qualification and publication path. No public version of this
module exists, and no release test is claimed here.
This module is a module of the repository's reactor (root `pom.xml`, artifact
`qbit-mobile-scanners-reactor`), next to `base` (`qbit-mobile-scanners`). It inherits
the root's single `<revision>` and the same `qbit-build-parent` 2.1.0, Apache-2.0
metadata and canonical Gitea URLs. One root `mvn deploy` (the shared Gitea Actions
`maven` workflow) tests and publishes the reactor POM, `qbit-mobile-scanners` and
`qbit-mobile-scanners-record-files` at one version to the Gitea Maven registry.
Its dependencies are immutable releases from that registry: `qbit-record-files`
0.1.0-RC.1 and `qbit-collaboration` 1.1.0-RC.1, with QQQ 4.1.0-RC.22 from the root.
Build only this module and the base with `./mvnw -pl integrations/record-files -am verify`.
## Reproducible isolated verification
## Reproducible isolated verification (legacy local harness)
The isolated scripts below date from the pre-publication frozen-candidate adoption on a
Windows workstation and are not part of the release path; they predate the reactor
layout and are kept as evidence tooling only.
From this directory, run `./Verify-Isolated.ps1`. It verifies every checksum in the
retained bundle, seeds a private repository from the local Maven cache, copies and
@@ -8,12 +8,12 @@ $ErrorActionPreference = 'Stop'
$test = Join-Path $PSScriptRoot ('target/path-regression-' + [Guid]::NewGuid().ToString('N'))
$inputRoot = Join-Path $test 'input'
$moduleRoot = Join-Path $test 'module-root'
New-Item -ItemType Directory -Force -Path $inputRoot, $moduleRoot, (Join-Path $inputRoot '.mvn'), (Join-Path $inputRoot 'src'), (Join-Path $inputRoot 'checkstyle'), (Join-Path $moduleRoot 'target/isolated-m2') | Out-Null
foreach($name in @('pom.xml', 'mvnw.cmd', 'mvnw')) { Set-Content -LiteralPath (Join-Path $inputRoot $name) -Value 'fixture' }
$removedSource = Join-Path $inputRoot 'src/Removed.java'
New-Item -ItemType Directory -Force -Path $inputRoot, $moduleRoot, (Join-Path $inputRoot '.mvn'), (Join-Path $inputRoot 'base/src'), (Join-Path $inputRoot 'checkstyle'), (Join-Path $moduleRoot 'target/isolated-m2') | Out-Null
foreach($name in @('mvnw.cmd', 'mvnw', 'base/pom.xml')) { Set-Content -LiteralPath (Join-Path $inputRoot $name) -Value 'fixture' }
$removedSource = Join-Path $inputRoot 'base/src/Removed.java'
Set-Content -LiteralPath $removedSource -Value 'old-source'
New-Item -ItemType Directory -Force -Path (Join-Path $inputRoot 'src/main/resources') | Out-Null
$removedResource = Join-Path $inputRoot 'src/main/resources/removed.txt'
New-Item -ItemType Directory -Force -Path (Join-Path $inputRoot 'base/src/main/resources') | Out-Null
$removedResource = Join-Path $inputRoot 'base/src/main/resources/removed.txt'
Set-Content -LiteralPath $removedResource -Value 'old-resource'
$sentinel = Join-Path $moduleRoot 'target/isolated-m2/keep.txt'
Set-Content -LiteralPath $sentinel -Value 'repository-must-survive'
@@ -23,7 +23,7 @@ New-Item -ItemType Directory -Force -Path (Join-Path $scratch 'target/classes')
Set-Content -LiteralPath (Join-Path $scratch 'target/classes/Removed.class') -Value 'stale-class'
Remove-Item -LiteralPath $removedSource
Remove-Item -LiteralPath $removedResource
Set-Content -LiteralPath (Join-Path $inputRoot 'src/Current.java') -Value 'current-source'
Set-Content -LiteralPath (Join-Path $inputRoot 'base/src/Current.java') -Value 'current-source'
Initialize-IsolatedScannerSource -ScannerRoot $inputRoot -ModuleRoot $moduleRoot
if((Test-Path -LiteralPath (Join-Path $scratch 'src/Removed.java')) -or (Test-Path -LiteralPath (Join-Path $scratch 'target/classes/Removed.class'))) { throw 'Removed source/class survived the fresh snapshot.' }
if(Test-Path -LiteralPath (Join-Path $scratch 'src/main/resources/removed.txt')) { throw 'Removed resource survived the fresh snapshot.' }
@@ -110,7 +110,7 @@ try
Remove-IsolatedBuildDirectory -ModuleRoot $module -LeafName 'module'
$runtimeClasspath = Join-Path $target 'runtime-classpath.txt'
if(Test-Path -LiteralPath $runtimeClasspath) { Remove-Item -LiteralPath $runtimeClasspath }
Invoke-Maven @('-B', '-ntp', 'clean', 'verify', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath', '-Dmdep.includeScope=test', '-Dmdep.outputFile=target/runtime-classpath.txt')
Invoke-Maven @('-B', '-ntp', '-Pisolated-module', 'clean', 'verify', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath', '-Dmdep.includeScope=test', '-Dmdep.outputFile=target/runtime-classpath.txt')
$runtime = (Get-Content -LiteralPath (Join-Path $target 'runtime-classpath.txt') -Raw).Trim().Split(';')
$manifest = foreach($path in $runtime) { @{path=$path; sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() } }
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $target 'runtime-sha256.json') -Encoding utf8
+15 -23
View File
@@ -1,12 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.0</version>
<relativePath />
</parent>
<parent><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners-reactor</artifactId><version>${revision}</version><relativePath>../../pom.xml</relativePath></parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-mobile-scanners-record-files</artifactId>
<version>${revision}</version>
@@ -33,23 +28,13 @@
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners/issues</url>
</issueManagement>
<properties>
<!-- effective version 0.1.0-SNAPSHOT; the parent's own revision (2.1.0) is overridden here -->
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- the same property names the root POM and the Echo demo use, with their defaults -->
<qqq.version>4.1.0-RC.21</qqq.version>
<mobile.version>0.1.0-SNAPSHOT</mobile.version>
<recordfiles.version>0.1.0-SNAPSHOT</recordfiles.version>
<collaboration.version>0.1.0-record-files-SNAPSHOT</collaboration.version>
<!-- developer-script override for core and rdbms only; by default it is the single qqq line, never a second one -->
<qqq.frozen.version>${qqq.version}</qqq.frozen.version>
<!-- the base module is built in the same reactor; the others are published releases from the Gitea registry -->
<mobile.version>${project.version}</mobile.version>
<recordfiles.version>0.4.0-RC.1</recordfiles.version>
<collaboration.version>1.2.0-RC.1</collaboration.version>
</properties>
<dependencyManagement><dependencies>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-core</artifactId><version>${qqq.frozen.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId><version>${qqq.frozen.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-middleware-javalin</artifactId><version>${qqq.version}</version></dependency>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-collaboration</artifactId><version>${collaboration.version}</version></dependency>
</dependencies></dependencyManagement>
<dependencies>
@@ -62,13 +47,20 @@
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-params</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.27.7</version><scope>test</scope></dependency>
</dependencies>
<build><directory>${project.basedir}/target/module</directory><plugins>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version>
<dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies>
<configuration><configLocation>../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration>
<configuration><configLocation>${maven.multiModuleProjectDirectory}/checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration>
<executions><execution><id>style</id><phase>validate</phase><goals><goal>check</goal></goals></execution></executions>
</plugin>
</plugins></build>
<profiles>
<!-- Isolated verification (Verify-Isolated.ps1) keeps its module output out of target/ itself; a normal reactor build and the standard capture use target/. -->
<profile>
<id>isolated-module</id>
<build><directory>${project.basedir}/target/module</directory></build>
</profile>
</profiles>
</project>
@@ -78,6 +78,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -408,6 +409,58 @@ public class MobileRecordFilesIntegrationTest extends RecordFilesFixture
/***************************************************************************
** A host that sets quarantineCapturedFiles=false (and leaves quarantineNewFiles
** true) makes only an approved device's camera item AVAILABLE. The device is
** the one the real provider verified, an unapproved device is still refused,
** an UPLOAD item stays QUARANTINED, and the permitted preview read returns the
** exact retained bytes only for the AVAILABLE version. Requires the owner
** version that contains withQuarantineCapturedFiles.
***************************************************************************/
@Test
void approvedDeviceCameraPhotoIsAvailableOnlyWhenHostOptsIn() throws Exception
{
config.withQuarantineCapturedFiles(false);
assertTrue(config.getQuarantineNewFiles(), "the broad default stays quarantined");
String scanner = signIn("scanner");
String admin = signIn("admin");
enrollHttp(scanner);
assertEquals(403, send("POST", FILES + "/uploads", scanner, manifest(UUID.randomUUID().toString(), "CAMERA", "photo.jpg", "image/jpeg", CONTENT), true, Map.of()).statusCode(),
"an unapproved device cannot start a capture upload even with the host opt-in");
approveHttp(admin, "mobileDeviceApprove");
byte[] photo = {(byte) 0xFF, (byte) 0xD8, 9, 8, 7};
byte[] other = {4, 5, 6};
assertEquals("AVAILABLE", finalizeLifecycle(scanner, "CAMERA", "photo.jpg", "image/jpeg", photo));
assertEquals("QUARANTINED", finalizeLifecycle(scanner, "UPLOAD", "upload.jpg", "image/jpeg", other));
QContext.init(instance, authenticated(scanner));
var retained = files.listForRecord("document", "1");
assertEquals(2, retained.size());
var available = retained.stream().filter(file -> "AVAILABLE".equals(file.lifecycle())).findFirst().orElseThrow();
var quarantined = retained.stream().filter(file -> "QUARANTINED".equals(file.lifecycle())).findFirst().orElseThrow();
var reader = new com.kingsrook.qbits.recordfiles.api.RetainedContentReader(
new com.kingsrook.qbits.collaboration.attachments.AttachmentsService(collaboration, new DiskAttachmentStorage()));
var reference = reader.reference(available.id(), "preview-" + available.id());
assertArrayEquals(photo, reader.read(reference, 1024).bytes());
assertThrows(QException.class, () -> reader.reference(quarantined.id(), "preview-" + quarantined.id()));
}
/***************************************************************************
** Create, upload one chunk and finalize one item; returns its commit lifecycle.
***************************************************************************/
private String finalizeLifecycle(String scanner, String origin, String fileName, String mediaType, byte[] bytes) throws Exception
{
String request = UUID.randomUUID().toString();
String id = ok(send("POST", FILES + "/uploads", scanner, manifest(request, origin, fileName, mediaType, bytes), true, Map.of())).path("uploadId").asText();
ok(send("PUT", FILES + "/uploads/" + id + "/items/20000000-0000-0000-0000-000000000001/chunks/0", scanner, bytes, true,
Map.of("Content-Type", "application/octet-stream", "X-Chunk-SHA256", UploadManifest.hash(bytes))));
return ok(send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)))
.at("/receipt/items/0/lifecycleAtCommit").asText();
}
/***************************************************************************
** Different actors, missing device proof and revocation cannot resume intake.
***************************************************************************/
@@ -822,10 +875,20 @@ public class MobileRecordFilesIntegrationTest extends RecordFilesFixture
** Immutable manifest has no device, user or session identity fields.
***************************************************************************/
private byte[] manifest(String request) throws Exception
{
return manifest(request, "UPLOAD", "fixture.pdf", "application/pdf", CONTENT);
}
/***************************************************************************
** The same manifest with a chosen client-declared origin and content.
***************************************************************************/
private byte[] manifest(String request, String origin, String fileName, String mediaType, byte[] bytes) throws Exception
{
return JSON.writeValueAsBytes(Map.of("requestId", request, "parent", Map.of("tableName", "document", "recordId", "1"),
"items", List.of(Map.of("itemId", "20000000-0000-0000-0000-000000000001", "position", 0, "fileName", "fixture.pdf", "declaredMediaType", "application/pdf",
"sizeBytes", CONTENT.length, "sha256", UploadManifest.hash(CONTENT), "origin", "UPLOAD"))));
"items", List.of(Map.of("itemId", "20000000-0000-0000-0000-000000000001", "position", 0, "fileName", fileName, "declaredMediaType", mediaType,
"sizeBytes", bytes.length, "sha256", UploadManifest.hash(bytes), "origin", origin))));
}
+33 -80
View File
@@ -1,17 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<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>
<!-- Reactor root AND parent of every module. The release pipeline rewrites only the version property below in THIS file, so it is the single version
source of every published module. The root keeps the modules' former parent (qbit-build-parent 2.1.0), so inherited build configuration is unchanged. -->
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.0</version>
<version>2.2.1</version>
<relativePath />
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-mobile-scanners</artifactId>
<artifactId>qbit-mobile-scanners-reactor</artifactId>
<version>${revision}</version>
<name>QQQ Mobile Scanners</name>
<description>Approved mobile devices and native QQQ barcode capture processes.</description>
<packaging>pom</packaging>
<name>QQQ Mobile Scanners (reactor)</name>
<description>Aggregator for the published Mobile Scanners modules: the base qbit and its optional Record Files integration.</description>
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners</url>
<scm>
<connection>scm:git:https://git.kof22.com/QRun-IO/qbit-mobile-scanners.git</connection>
@@ -31,91 +34,41 @@
</license>
</licenses>
<properties>
<revision>0.2.0-SNAPSHOT</revision>
<revision>0.6.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<qqq.version>4.1.0-RC.21</qqq.version>
<qqq.version>4.1.0-RC.26</qqq.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-postgres</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Every published module, so one root deploy builds every GAV. demo/ and examples/ are runnable demonstrations with their own coordinates and are intentionally
not part of this reactor. Build a subset with -pl <module> -am. -->
<modules>
<module>base</module>
<module>integrations/record-files</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>14.3.0</version>
</dependency>
</dependencies>
<!-- Publish consumer POMs with the version resolved (the parent reference and the reactor root itself would otherwise carry the unresolved property). -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.7.3</version>
<configuration>
<configLocation>checkstyle/checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<updatePomFile>true</updatePomFile>
<flattenMode>ossrh</flattenMode>
</configuration>
<executions>
<execution>
<id>checkstyle</id>
<phase>validate</phase>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>check</goal>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>