Compare commits

..
Author SHA1 Message Date
james.maes 787a227602 Merge pull request 'fix(preferences): an action that answers with an address opens it; a section says how a connection ended' (#214) from fix/process-link-and-preference-redirect into main
build / maven (push) Successful in 10m17s
maven / build (push) Successful in 10m17s
2026-11-11 12:38:10 +00:00
james.maesandClaude Fable 5.2 30f4f9d586 fix(preferences): an action that answers with an address opens it; a section says how a connection ended
build / maven (push) Successful in 5m2s
maven / build (push) Successful in 5m2s
A preference action that starts an OAuth connection (Connect Slack)
answers with the consent page's address in the action result's message,
because the UI API has no redirect field. The address was the text of a
success toast: long, not clickable and gone in seconds.

- A message that is nothing but one absolute https address is where the
  browser goes, in this tab, and is never shown (continuationUrl,
  leaveTo). A message that only mentions an address is still a message.
  Plain http, credentials in the address and other schemes are not
  followed.
- A flow that returns to /app/preferences?section=<name>&result=<code>
  or &error=<code> gets its outcome shown once above the section's form,
  as the section's label and the code as words ("Slack: connected.",
  "Slack: authorization denied."), as a status or an alert, and taken
  off the address. Only a fixed lowercase code is read, never a sentence.
- Links in an HTML process component use the link color and an
  underline; they looked like the text around them. A link's own inline
  style still wins.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 06:31:38 -06:00
james.maes 24bbe4a038 Merge pull request 'fix(errors): the error banner says what failed and no longer stacks' (#210) from fix/error-banner-names-cause into main
build / maven (push) Successful in 5m7s
maven / build (push) Successful in 5m7s
2026-11-11 00:58:17 +00:00
james.maes 8903f272c4 Merge pull request 'fix(header): the Ask button's activity dot and unread badge sit on its icon' (#213) from fix/ask-indicator-anchor into main
build / maven (push) Successful in 8m37s
maven / build (push) Successful in 8m37s
2026-11-11 00:58:15 +00:00
james.maesandClaude Fable 5.2 2345039911 fix(header): the Ask button's activity dot and unread badge sit on its icon
build / maven (push) Successful in 14m44s
maven / build (push) Successful in 14m45s
The dot shown while a reply is being written was positioned against the
button (absolute bottom-0 right-0). The button's box is its hit area: 10 px
of padding around the icon and, from 1440 px, the "Ask" label, with no
visible edge at rest. The dot therefore landed past the label and under its
baseline, detached from the icon; the unread badge landed over the end of
the label.

Header indicators are now positioned against the icon: HeaderIconAnchor is
a box exactly the size of the icon, with HEADER_BADGE_CLASS (top-right) and
HEADER_STATUS_DOT_CLASS (bottom-right). Ask, the bell and Jobs share it; on
the 40 px icon-only buttons the badge keeps its pixels (2 px inside the
button's top-right corner).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 18:40:45 -06:00
james.maesandClaude Fable 5.2 4347924658 docs(releases): note the 403 fallback and the silent cases of the error banner
build / maven (push) Successful in 4m13s
maven / build (push) Successful in 4m13s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 11:39:00 -06:00
james.maesandClaude Fable 5.2 523f59cf38 fix(errors): the error banner says what failed and no longer stacks
build / maven (push) Successful in 4m33s
maven / build (push) Successful in 4m33s
The global query/mutation error handler showed "Something went wrong" for
every failure without an expected status, and a polling query that kept
failing added one banner per attempt.

- No response: "No response from the server" (or "The server took too long
  to respond" for a timeout), with the request below it as METHOD /path:
  no query string, and ids shown as {id}.
- Unexpected status: the server's own message, else "Request failed (HTTP 409)".
- An error raised by the application shows its own sentence; a runtime error
  shows only its type.
- The banner's sonner id is its text plus the request, so a repeat updates
  the banner on screen. A lost connection has one banner for every request.
- 401, cancelled requests and aborted fetches stay silent. 403 falls back to
  "Permission denied" instead of the axios transport text.
- Each banner logs status, axios code, error name and request to the console,
  never a message, body, header or query string.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 11:29:49 -06:00
james.maes fefd5be0d4 Merge pull request 'fix(auth): every sign-in forces re-authentication (prompt=login, max_age=0)' (#209) from fix/signin-always-asks-credentials into main
build / maven (push) Successful in 4m22s
maven / build (push) Successful in 4m22s
2026-11-09 20:59:19 +00:00
james.maesandClaude Sonnet 5.5 c0e5b285db fix(auth): every sign-in forces re-authentication (prompt=login, max_age=0)
build / maven (push) Successful in 7m37s
maven / build (push) Successful in 7m37s
Sign out already ends the provider session (RP-initiated logout); the authorize
request now also sends prompt=login and max_age=0 so a lingering provider
session can never sign the next user in silently. prompt=login alone was seen
reusing retained SSO on real Authentik.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 14:43:02 -06:00
james.maes 42b509f35c Merge pull request 'ci: use shared ci-workflows Maven workflow' (#208) from ci/ci-workflows into main
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
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 / repository-policy-12 (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: Shared Node CI with Playwright / e2e (push) Successful in 29m53s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (push) Successful in 16s
QRun-IO/ci-policy: ci / node-playwright-11 (push) Successful in 30m15s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 6s
build / maven (push) Successful in 13m3s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-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-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (push) Successful in 27s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (push) Successful in 12m7s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
maven / build (push) Successful in 13m3s
2026-11-06 21:50:54 +00:00
james.maesandClaude Opus 5.5 37c4234324 ci: pin v1, build the Next export before Maven, drop the old release caller
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / 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
build / maven (push) Successful in 10m42s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 10s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (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-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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: Shared Node CI with Playwright / e2e (pull_request) Successful in 26m38s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (pull_request) Successful in 23s
maven / build (push) Successful in 10m42s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Successful in 12m17s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Successful in 18s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Successful in 27m2s
Every push verifies (pnpm typecheck/lint/test/build:export, then Maven);
vX.Y.Z and vX.Y.Z-RC.N tags publish to the Gitea registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:38:06 +00:00
james.maesandClaude Opus 5.5 412d884402 ci: use shared ci-workflows Maven workflow
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow d20a578c7d818bc7ca32a2ac2c675da4e4f27385] (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / 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
build / maven (push) Failing after 43s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 4s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (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-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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 7s
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (pull_request) Failing after 23m41s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (pull_request) Successful in 27s
maven / build (push) Failing after 43s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Successful in 11m58s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Failing after 12s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Failing after 28m35s
Replace the platform-ci release callers with one caller of
KofTwentyTwo/ci-workflows maven.yaml (qqq-orb port).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:15:45 +00:00
james.maes 27e8754f32 Merge pull request 'Preserve workflow, presence and dependency updates in the canonical Next release' (#207) from kof22/next-workflow-assets-on-pr206 into main
QRun-IO/ci-policy: native / dotnet-0 (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 / repository-policy-12 (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: Shared Node CI with Playwright / e2e (push) Successful in 21m37s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (push) Successful in 8s
QRun-IO/ci-policy: ci / node-playwright-11 (push) Successful in 21m49s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 5e6bfd71ee8a5674882f7680c695a0ada599622f] (push) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (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-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (push) Successful in 36s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (push) Successful in 11m12s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
2026-11-05 17:22:27 +00:00
james.maesandClaude Sonnet 5.5 00c00b0e71 Merge owner PR206 46e9f121 into the PR207 branch: scoped record filter control lookups
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 5e6bfd71ee8a5674882f7680c695a0ada599622f] (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / 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 4s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: 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: native / native-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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: Shared Node CI with Playwright / secrets (pull_request) Successful in 49s
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (pull_request) Successful in 23m10s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Successful in 11m59s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Successful in 23m21s
Takes the owner's test-only fix (src/components/query/RecordQuery.test.tsx scopes role queries with within(); docs/QBIT-RELEASE-STATUS.md)
for the 'confirms clearing filters and preserves the selected sort' case that timed out at the default 5000 ms on the loaded CI runner
(5150/5239 ms in CI 4993 node, twice; 1.3-1.5 s unloaded locally with the fix vs 1.5-1.7 s without). No assertion, user-semantic or
sort behavior changes; no f49, Tools, owner 205/206, zod 4 or lockfile content touched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 10:46:00 -06:00
james.maesandClaude Sonnet 5.5 3a72c8b88a fix(authentik): give the access editor's inherited-permissions schema its zod 4 key type
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / 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) Failing after 8s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (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-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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 / Scoped workflow [workflow 3d75c629c2706be133505268b6696fe9b1ae420c] (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (pull_request) Successful in 21m26s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (pull_request) Successful in 36s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Failing after 12m36s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Failing after 16s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Failing after 12m57s
The owner's access editor (PR205/206) was written against zod 3's single-argument z.record(value). With zod ^4.6.7 (the reviewed f49
dependency set) z.record needs the key schema too: TS2554 at (32,16), then 'inherited' infers Record<..., unknown> giving TS18046 at (84,147)
and TS2322 at (103,128) (CI 4726, node 75004 and e2e 75005, the only three type errors). Use z.record(z.string(), z.array(z.string())), the
same validation and the exact Record<string, string[]> type the component already declares; no cast, no behavior or permission change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 08:29:27 -06:00
James MaesandCodex 46e9f1212c test: scope record filter control lookups
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: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / source-config-13 (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
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow cf802461ca4ebfcf5c500440715cb333d92ea606] (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (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 / node-10 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (pull_request) Successful in 24m36s
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (pull_request) Successful in 1m32s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Successful in 13m0s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Successful in 22s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Successful in 1h18m32s
Preserve all filter, sorting, confirmation and accessibility assertions while avoiding repeated whole-page role scans. Focused RecordQuery tests and lint pass; release CI remains required.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-11-05 07:22:21 -06:00
James MaesandClaude Sonnet 5.5 bfcc008ca2 test: carry the reviewed f49 test-only fixes that keep upstream coverage equivalent
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / 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-head (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) Failing after 7s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 60e5a80ed7a5a67877bfb3e129b1929c83464b6e] (pull_request) Successful in 13s
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-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (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-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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: Shared Node CI with Playwright / e2e (pull_request) Failing after 2m20s
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (pull_request) Successful in 1m11s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Failing after 2m13s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Failing after 17s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Failing after 1h10m50s
WidgetRenderer lazy-chunk wait, the mark-all-mentions-read My Work test (dce92cfb) and the help-results wait before
search navigation (8d372142), taken from f49. Owner-side test changes in PR205/206 (keyboard shortcuts, security headers
with blob: connect-src) are kept as they are newer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 07:02:39 -06:00
James MaesandClaude Sonnet 5.5 fdf12d3c40 fix(deps,test): zod 4 migration and portable standalone tests from the reviewed f49 commit 0c156b22
The schema, form-adjuster, possible-values, assistant-normalize and zod-from-metadata changes zod 4 needs, plus the
existing Windows-portable standalone acceptance-stage and the two dev/write page test adjustments. Taken from f49;
on these files PR206 differed from f49 only by the zod 3 forms.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 07:01:30 -06:00
James MaesandClaude Sonnet 5.5 04ab9ad6c9 fix(deps): apply the reviewed f49 dependency advisory updates and zod 4 to the 206 line
next 16.4.1 -> 16.4.3, dompurify ^3.4.17, storybook 10.6.2, vitest/coverage/ui ^4.1.12, vite 7.3.7, zod ^4.6.7 and the
pnpm overrides (follow-redirects, undici@>=7 <8, fast-uri@>=3 <4) exactly as in the reviewed f49de335 package.json
dependency sections and pnpm-lock.yaml. PR206 carried no dependency change of its own beyond f49's (codemirror,
pdfjs-dist, typescript-editor are identical), so the f49 lockfile is used as is; `pnpm install --lockfile-only
--frozen-lockfile` (pnpm 9.15.9, Node 22.20.0) accepts it unchanged. Scripts, version RC.38 and the caller are untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 07:01:29 -06:00
James MaesandClaude Sonnet 5.5 70618bd143 build: pin the workflow bundle lock to the component main merge ede4eba
Component PR 5 merged normally as ede4eba875ca17c5921ad4caf61a08bdff6f794d; its tree 9efc0921... equals the qualified
source tree, so the qualified bundle and map (sha256 unchanged) are the canonical main output; no rebuild.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 06:57:56 -06:00
james.maesandCodex aaa77d7e6e fix: retain Auth0 display profile with canonical presence identity
Keep the backend user ID without dropping provider display fields on sign-in or reload. Do not reuse a different user's cached profile. Reproduced callback/reload failures; full 3664-test suite, TypeScript and lint pass. Real mobile flow assertions pass locally, with Windows export diagnostics tracked separately; Linux acceptance remains required.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-05 06:57:31 -06:00
James MaesandClaude Sonnet 5.5 73b1ea16d7 feat(collaboration): use server-issued presence leases in the hook (port of published RC.37 de5c1bd0)
The current line has the lease API client (renew/open/beacon with sequence) but the hook still generated a client
connection id and never opened or ordered a lease. Port the hook, auth-storage, realtime invalidation, header and
mock changes of de5c1bd0 from the published v1.0.0-RC.37, so RC.38 does not regress against qbit-collaboration's
issued-lease protocol. CI, version and acceptance-stage conflicts resolved to the current line (no version or
dependency change); the people-tasks beacon test keeps the current line's equivalent assertion.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 06:57:30 -06:00
James MaesandClaude Sonnet 5.5 94726bb52c build: package the qualified precompiled QSequentialWorkflow bundle as tracked Next library assets
Track the component bundle and map compiled from the reviewed component source e6036d2 (Node 22.20.0, npm ci
--ignore-scripts, existing 3 suites passed, webpack) in dynamic-components/q-sequential-workflow/ with a sha256 lock
(scripts/dynamic-components.lock.json: source commit/tree, toolchain image digest, lockfile hash). The stage script
copies them into the export (build, dev and build:export) and refuses a missing or altered file; the pom enforcer
requires the staged bundle and map, so a release without the workflow editor fails packaging. No network, token or
component fetch is needed in the producer. The component main commit is recorded after its merge (pending in the lock).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 06:34:45 -06:00
James MaesandClaude Sonnet 5.5 56ab7fe21c fix(widgets): run dynamic components on the React 19 host with Material's defaultValues Map and qController.processRun
Port of the KOF22 bridge fixes onto the current line: widget defaultValues are handed to dynamic components as a
Map that still allows property access, and the bridge exposes Material's qController.processRun (real process
init/run through the existing UI API). Needed by the QSequentialWorkflow editor bundle.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 06:33:21 -06:00
James MaesandCodex 0f2eb173c1 chore(release): prepare QBit UI candidate RC.38
QRun-IO/ci-policy: ci / ci-result (pull_request) Canceled after 0s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 13053bb397bf07f3eaacd6e51ef0ffad7f9f1eb3] (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-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: Shared Node CI with Playwright / e2e (pull_request) Canceled after 20m6s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (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-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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: Shared Node CI with Playwright / secrets (pull_request) Successful in 47s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Failing after 15m13s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Canceled after 1h27m5s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (pull_request) Canceled after 0s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
Use the next unused Maven version and record the deployed publication route and current integration evidence. Release remains sequenced after Core and Collaboration.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-05 05:55:49 -06:00
James MaesandCodex 22f5749e36 chore: preserve current permission editor in QBit UI release
Merge current main without changing the permission editor. Both branches set maxWorkers to 4; retain a single setting. All 110 affected widget/process tests and typecheck passed.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-05 05:51:34 -06:00
james.maes 090f5febea Merge pull request 'feat(authentik): application permission editor RC checkpoint' (#205) from james.maes/authentik-access-rc-20261105 into main
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (push) Successful in 26m10s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (push) Successful in 26s
QRun-IO/ci-policy: ci / node-playwright-11 (push) Successful in 1h29m54s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 10s
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 13053bb397bf07f3eaacd6e51ef0ffad7f9f1eb3] (push) Successful in 9s
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: native / native-result (push) Successful in 9s
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 / 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: Shared Node CI with Playwright / node (push) Successful in 13m54s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (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-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (push) Successful in 52s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
2026-11-05 11:40:13 +00:00
james.maes afb9d75c75 Merge remote-tracking branch 'origin/main' into feature/authentik-access-ui
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 11s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / 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-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (pull_request) Successful in 34m33s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow f1d46718955078331817c6206b822a94bc7d769f] (pull_request) Successful in 7s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (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-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (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: Shared Node CI with Playwright / gate (pull_request) Failing after 29s
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Failing after 1h39m12s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (pull_request) Successful in 37s
QRun-IO/ci-policy: Shared Node CI with Playwright / node (pull_request) Failing after 14m6s
2026-11-05 05:23:23 -06:00
james.maesandGPT-6 119f1211d0 feat(authentik): add application access editor and paged permission matrix
Provide user, group and QQQ application-role permission editing with searchable,
paged controls and inherited-source visibility. Reuse the matrix in policy
editing and preserve form context for process widgets.

Co-Authored-By: GPT-6 <noreply@openai.com>
2026-11-05 05:23:11 -06:00
James MaesandCodex aba43263b2 ci: wire the supported Next dashboard Maven release
Pin the reviewed frontend-next producer and document the qualified publisher and remaining deployment gates. The immutable baseline version is unchanged until final release preparation.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-05 05:22:01 -06:00
James MaesandGPT-6 ddc7de731b chore: reconcile current main with QBit UI integration
Preserve the upstream companion Tools workspace alongside the prepared
standard QQQ file, script, and process controls. Record the focused merge
checks and the verified missing Next Maven Central publication integration.

Validation: typecheck, changed-file lint, license headers, staged secret
scan, and 61 focused cases across the completed runs. The unchanged lazy
chart file passed with one worker after a cold parallel timeout; no test
assertion or timeout changed. Full release CI remains pending dependencies.

Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2026-11-05 04:43:42 -06:00
james.maes 3d162dce74 feat: add companion Tools setup workspace (#204)
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 / repository-policy-12 (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 / dotnet-0 (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-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / e2e (push) Successful in 21m28s
QRun-IO/ci-policy: Shared Node CI with Playwright / gate (push) Successful in 10s
QRun-IO/ci-policy: ci / node-playwright-11 (push) Successful in 21m41s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (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-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / secrets (push) Successful in 25s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: Shared Node CI with Playwright / node (push) Successful in 10m44s
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
2026-11-05 09:41:12 +00:00
James MaesandCodex d186ad7086 docs: clarify Next UI publication prerequisites
Co-Authored-By: Codex <noreply@openai.com>
2026-11-04 16:49:23 -06:00
James MaesandGPT-6 Codex 2186561950 fix: complete file previews and portable UI verification
Allow local PDF blob reads in standalone CSP and preview signed attachment PDFs without forwarding credentials. Make static export portable on Windows, bound test workers, cover transfer cancellation and size limits, and update integration evidence. Full coverage gates, 12 browser scenarios, live file/PDF flows, lint, typecheck and snapshot packaging pass.

Co-Authored-By: GPT-6 Codex <noreply@openai.com>
2026-11-04 12:02:20 -06:00
40ee257847 chore: preserve paused QBit work and document remaining issues
Save the current implementation and README, status, and known-issue documentation at the user-requested stop. No completed release or deployment is claimed.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-Authored-By: GPT-6 Codex <noreply@openai.com>
2026-11-04 10:45:31 -06:00
James MaesandClaude Sonnet 5.5 b67fb5fd20 feat(ui): file preview/delivery, PDF preview, isolated TypeScript editing and result fields
Reconcile the reviewed private UI source onto canonical main d04cfa4.
Source: private kof22-clients frontend-next commit
f49de335e8bfc88b2e96aa8a34343ac6b5e93003 (feature/kof22-rc37), files
limited to the file-preview/open/download/actions, PDF/document preview,
compiled TypeScript editor and generic API widget support
(commits b4542dbe, 0ab1f692, d0705481, 21caa18d of that branch).

Not taken: the sequential-workflow bundle staging and qfmd bridge changes,
the zod 4 migration, the RC.37 tag merge (presence, auth, My Work),
version/pom bumps and the old lockfile. Canonical next 16.4.1, zod 3 and
dompurify are unchanged; package.json and pnpm-lock.yaml add only the
editor/PDF dependencies, worker build scripts and CodeMirror pins
(lock change is additions only).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:28:55 -06:00
forge-recovery d04cfa4b58 Merge pull request 'ci: baseline reviewed historical fixture findings' (#203) from ci/historical-finding-baseline-20261101 into main
QRun-IO/ci-policy: ci / ci-result (push) Failing after 3s
QRun-IO/ci-policy: ci / gradle-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (push) Skipped
QRun-IO/ci-policy: ci / node-3 (push) Skipped
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (push) Skipped
QRun-IO/ci-policy: ci / node-4 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 4s
QRun-IO/ci-policy: ci / source-config-5 (push) Skipped
2026-11-02 01:46:35 +00:00
forge-recoveryandOpenAI GPT-6 94424b3f46 ci: baseline reviewed historical fixture findings
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 5s
QRun-IO/ci-policy: ci / gradle-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Failing after 5s
Co-Authored-By: OpenAI GPT-6 <noreply@openai.com>
2026-11-02 01:13:24 +00:00
e31716aa52 fix: update Next and harden the standalone runtime image (#201)
* fix: update Next and harden the standalone runtime image

Upgrade Next and eslint-config-next to 16.4.1; build and run on the same pinned Alpine Node image, exclude pnpm cache from context, and omit build-only package managers from production.

Co-Authored-By: Codex <noreply@openai.com>

* test: make standalone staging checks portable to Windows

Use platform paths for symlink comparison and the export-presence mock. Keep Unix permission-bit checks in an explicit Unix-only case; preserve file contents and relative-link behavior on Windows.

Co-Authored-By: Codex <noreply@openai.com>

* test: load the real developer view before DOM timing assertions

The first developer-view test also fails on unchanged main under cold Windows transforms. Compile the real lazy component in setup while retaining its rendering, API assertions and original timeouts. All nine targeted tests pass.

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: James Maes <jmaes@dmdbrands.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-29 17:19:37 -05:00
james.maesandCodex 9756b9d93b ci: adopt managed orb 0.8 patch track (#193)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 16:20:56 -05:00
james.maesandCodex f205992878 ci: adopt shared orb 0.7.17 (#192)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:37:37 -05:00
james.maesandCodex 5f503fa7e9 test: disambiguate async help from process search results (#191)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 09:15:38 -05:00
james.maesandCodex 09828ab1cc ci: adopt central orb 0.7.12 and stable gates (#190)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 03:49:28 -05:00
james.maesandCodex c39748b1a0 test: stabilize widget layout and report reload acceptance (#189)
* test: assert widget layouts at container breakpoints

Co-Authored-By: Codex <noreply@openai.com>

* test: wait for saved report readiness before reloading

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 03:08:45 -05:00
james.maesandCodex 829ef6b384 ci: align canonical shared orb workers (#188)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:49:08 -05:00
james.maesandCodex a94e1e39c8 ci: adopt shared orb 0.7.11 (#187)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:12:27 -05:00
james.maesandCodex 9bea8cc63f ci: centralize workflows in QQQ orb (#185)
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 20:04:31 -05:00
james.maes ada9946eea Merge pull request #179 from QRun-IO/feature/assistant-conversation-list
feat(assistant): conversation row actions and thread state indicators
2026-10-19 08:16:52 -05:00
james.maesandCodex fa960580f6 fix(assistant): integrate attachment and conversation recovery
Preserve both configuration normalizers and restore the loading status accessible name required by logout acceptance.

Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-19 07:45:41 -05:00
james.maes 596bee88cc Merge pull request #178 from QRun-IO/feature/security-findings-2026-10
fix(security): harden plain-text sanitizers and pin source-map-js
2026-10-19 07:41:18 -05:00
james.maes df764362b8 Merge pull request #184 from QRun-IO/fix/assistant-attachment-recovery
fix(assistant): contain upload failures and isolate conversation files
2026-10-19 07:38:03 -05:00
james.maesandCodex 07b4a973de fix(assistant): isolate conversation action rollback
Co-Authored-By: Codex <noreply@openai.com>
2026-10-19 07:11:40 -05:00
james.maes e36afde222 Merge pull request #183 from QRun-IO/fix/startup-branding
fix: show the dashboard only after application branding is ready
2026-10-19 07:11:37 -05:00
james.maesandCodex 69836fc62f fix(security): parse help and widget text with the shared sanitizer
Co-Authored-By: Codex <noreply@openai.com>
2026-10-19 07:07:22 -05:00
james.maes 3cb3659a41 Merge branch 'main' into feature/assistant-conversation-list 2026-10-19 07:06:27 -05:00
james.maes b30ff4e916 Merge remote-tracking branch 'origin/feature/security-findings-2026-10' into fix/source-map-security 2026-10-19 07:03:16 -05:00
james.maesandCodex d558c1378d fix(assistant): contain upload failures and isolate conversation files
Co-Authored-By: Codex <noreply@openai.com>
2026-10-19 07:01:25 -05:00
james.maes f1c5f09b4a Merge pull request #182 from QRun-IO/feature/audit-fallback-fixture
test: exercise the audit table fallback with independent permissions
2026-10-19 06:55:22 -05:00
james.maesandCodex c9a56b0d9f fix: wait for application branding before painting the dashboard
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 06:48:51 -05:00
james.maesandCodex 3bab5596ab test: exercise audit fallback with independent process permission
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 06:29:02 -05:00
james.maesandClaude Opus 5.5 ae2498868d feat(assistant): conversation row actions, thread state indicators, unread correctness
Rail rows are now a list of buttons with Archive/Restore and Delete (confirm,
optimistic with rollback and toast), a Manage all conversations link to the
conversations table when the server names one, a spinner for running threads,
an unread dot and bold title, and a dot on the History toggle for activity in
other conversations. A reply that finishes in the open conversation is marked
read (PATCH read:true). Fields are optional so older servers keep working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:40:18 -05:00
james.maesandClaude Opus 5.5 ea21505e96 fix(security): harden plain-text sanitizers and pin source-map-js
- Strip HTML tags until stable and decode &amp; last in helpPlainText and
  widget plainText, so nested or split tags cannot survive.
- Match closing script tags with attributes or whitespace in the CSP
  inline-script hasher.
- Replace every newline in the child-record export aria-label.
- Override source-map-js to >=1.2.2 (GHSA advisory, dev dependency).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:36:24 -05:00
james.maes 1b37e5efa9 Merge pull request #176 from QRun-IO/feature/next-rc36-prep
chore(#712): prepare Next UI 1.0.0-RC.36
2026-10-16 10:39:55 -05:00
james.maesandClaude Opus 5.5 ba20307fa0 chore(#712): prepare Next UI 1.0.0-RC.36
PR175: accessibility fixes (Dashboard Wave 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:22:41 -05:00
james.maes 81a03eab2d Merge pull request #175 from QRun-IO/feature/home-a11y-contrast
fix(a11y): readable link text, 24px clear target, h2 widget titles, skip link focus
2026-10-16 10:19:17 -05:00
james.maesandClaude Sonnet 5.5 ab9cf837df fix(a11y): readable text on accent-filled surfaces
White text on a light brand accent (for example #059669, 3.8:1) fails
contrast on the active sidebar item, selected tabs and primary buttons.
applyBrandingTheme now sets --color-primary-foreground to white when that
reaches 4.5:1 on the accent and black otherwise, so the brand fill is kept.
The axe spec no longer excludes the sidebar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 09:24:32 -05:00
james.maesandClaude Sonnet 5.5 5c9307015c fix(a11y): readable link text, 24px clear target, h2 widget titles, skip link focus
App homes failed axe "serious" checks on 18 kof22 pages (A10/L10 = 0):

- Accent-colored text (the table widget link row, cell links, widget links)
  used text-primary, which fails 4.5:1 for light brand colors such as #059669.
  Add a --color-link token (text-link utility) derived per theme from the
  accent: applyBrandingTheme darkens (light theme) or lightens (dark theme)
  the accent until it reaches 4.5:1 on the card and muted surfaces; without
  branding the stylesheet derives it from --color-primary.
- The widget dropdown icon buttons (Clear range, previous/next) are at least
  24 x 24 px (WCAG 2.2 target size).
- Widget titles on an app home render as h2 under the page h1 (a nested
  widget stays h3); no visual change.
- main is focusable (tabIndex -1) and the skip link moves focus to the first
  widget of the page, else to main.

Adds unit tests and a Playwright axe spec (light and dark, 390 and 1440 px).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 09:20:21 -05:00
james.maes 55c150136b Merge pull request #174 from QRun-IO/feature/next-rc35-prep
chore(#712): prepare Next UI 1.0.0-RC.35
2026-10-15 22:13:38 -05:00
james.maesandClaude Opus 5.5 d644995c85 docs(release): RC35 notes state only the verified requirement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:04:28 -05:00
james.maesandClaude Opus 5.5 6f95554362 chore(#712): prepare Next UI 1.0.0-RC.35
PR164: profile photo uploader and avatars everywhere (UI API 1.8.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:04:15 -05:00
james.maes ba38367244 Merge pull request #164 from QRun-IO/feature/user-profile-avatars
feat(profile): profile photo uploader and avatars everywhere (UI API 1.8.0)
2026-10-15 22:03:46 -05:00
james.maes c623b3bc87 Merge remote-tracking branch 'origin/main' into feature/user-profile-avatars 2026-10-15 21:32:25 -05:00
james.maes 17f605261a Merge pull request #173 from QRun-IO/feature/next-rc34-prep
chore(#712): prepare Next UI 1.0.0-RC.34
2026-10-15 21:17:33 -05:00
james.maesandClaude Opus 5.5 0db8ab0bfa chore(#712): prepare Next UI 1.0.0-RC.34
PR172: fail soft (error boundaries, validated assistant payloads, unstuck streams).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:10:37 -05:00
james.maes 68a2b9de0c Merge pull request #172 from QRun-IO/feature/fail-soft-boundaries
feat: fail soft — layered error boundaries, validated assistant payloads, unstuck streams
2026-10-15 21:10:07 -05:00
james.maesandClaude Opus 5.5 af42c7613f chore(openapi): vendor the assistant UI API extension 1.5.0 (qbit-assistant#60)
Re-vendors extensions/assistant.json from qbit-assistant#60's assistant.yaml:
POST /assistant/client-errors (AssistantClientErrorReportV1) replaces the
provisional entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:46:00 -05:00
james.maesandClaude Opus 5.5 f5170ed130 Merge origin/main (IA phase 1): the avatar moves to the header AccountMenu
The user menu moved from the sidebar footer to the header AccountMenu
(#168). Take main's Sidebar; AccountMenu now draws UserAvatar from the
profile (photo, chosen Gravatar via the server, else initials) with the
profile's display name, and no longer builds Gravatar URLs from branding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:38:02 -05:00
james.maesandClaude Opus 5.5 77fc832f9e test(e2e): a crashing assistant result and a crashing widget leave the page usable
Injects a real render-time crash (no app test hook): an init script makes
Number.prototype.toLocaleString throw for one sentinel value, which the
count render and the table widget format. Asserts the failure stays in its
part/widget with details and Retry, the rest of the reply/dashboard and the
shell keep working, records renders without rows/columns and unknown render
types still show, and the client error report reaches the backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:34:56 -05:00
james.maesandClaude Opus 5.5 627ad7ae34 feat(assistant): fail soft at panel, message, part and render level
- The panel sits behind its own boundary: a crash shows "The assistant
  hit a problem" with Retry, Copy details, Report a bug (QA capture) and
  Close, while the page keeps working; reopening retries.
- Each message, part and tool render has its own boundary with a small
  inline "couldn't be displayed" note and a details toggle; unknown parts
  and renders show as raw JSON.
- Failed replies offer Retry (asks again); running replies offer Stop
  inline. A second Stop, or a failed stop request, stops listening at once
  and keeps the reply shown as stopped, so the input is never locked.
  A reply left running with no live connection can be stopped too.
- A stream event the reducer cannot apply is skipped and reported instead
  of breaking the stream.
- Failures are reported (console with request id; POST
  /assistant/client-errors when the assistant extension is 1.5.0+, so they
  show in Admin). Vendors the operation into the assistant extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:26:58 -05:00
james.maesandClaude Opus 5.5 03f67def30 feat(assistant): validate payloads at the API boundary; streams never hang
- lib/api/assistant-normalize: zod validation of conversation messages,
  parts, renders, presentation blocks and stream events against the
  /qqq/v1 assistant shapes. Missing lists become [], missing text '',
  bad list items are dropped, and unknown part or render types become
  `unknown` (shown as raw JSON) instead of throwing.
- getAssistantConversation and the SSE reader apply it, so a stored or
  streamed malformed payload can no longer reach a renderer.
- A connection that delivers nothing (not even the 15 s ping) for 45 s is
  treated as lost and resumed; refused turns carry their X-Request-Id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:26:57 -05:00
james.maesandClaude Opus 5.5 cc64297d83 fix(avatars): read the profile from the preferences index (no 404 without profiles)
The real-backend acceptance apps advertise the avatars feature but install
no profile section, so GET /preferences/profile answered 404 on every page.
useMyProfile now finds the profile section in the shared preferences index
(which saving any section already invalidates); nothing is requested that
can 404, and other users' avatars are only looked up when profiles exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:15:42 -05:00
james.maesandClaude Opus 5.5 1d7c1a5ebf feat(errors): layered fail-soft boundaries with PII-safe client error reports
- lib/errors/client-errors: one reporter for every boundary; logs with the
  X-Request-Id correlation id, redacts and bounds text, dedupes, caps per
  session, and sends to a registered sink when one exists.
- ErrorBoundary gains scope reporting, resetKeys and fallbackRender
  (backward compatible); InlineFailure is the small in-place fallback with
  a details toggle.
- (dashboard)/error.tsx: a page that throws is replaced inside the app
  shell, so navigation, header and assistant keep working.
- Every ConnectedWidget, SeededWidget and record section renders behind its
  own boundary, so one failing widget or section cannot take down a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:13:46 -05:00
james.maes 960538ebc0 Merge pull request #171 from QRun-IO/feature/next-rc33-prep
chore(#712): prepare Next UI 1.0.0-RC.33
2026-10-15 20:02:15 -05:00
james.maesandClaude Opus 5.5 67eca54900 chore(#712): prepare Next UI 1.0.0-RC.33
PR168 (navigation, IA phase 1) and PR170 (assistant empty-results crash fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:56:17 -05:00
james.maes b4b59cee6e Merge pull request #170 from QRun-IO/fix/assistant-empty-records
fix(assistant): empty query results no longer crash the page
2026-10-15 19:55:53 -05:00
james.maes 28fdb2a781 Merge pull request #168 from QRun-IO/feature/ia-phase1-nav
feat(nav): sidebar sections and favorites, one search, header Search/Ask/Jobs/Notifications/Help/Account, single-key shortcut switch
2026-10-15 19:48:09 -05:00
james.maes 524adff0d5 Merge remote-tracking branch 'origin/main' into feature/user-profile-avatars
# Conflicts:
#	docs/releases/unreleased.md
#	src/lib/api/openapi/qqq-v1.json
2026-10-15 19:42:02 -05:00
james.maesandClaude Opus 5.5 893bbf26df fix(assistant): empty query results no longer crash the page
A records render with no rows or columns (servers omit empty lists when a
query matches nothing) threw "Cannot read properties of undefined
(reading 'length')" and took down the whole page. Renders now treat
missing lists as empty and show "no rows match", and each reply part
renders inside an error boundary, so one malformed part cannot crash the
reply or the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:30:51 -05:00
james.maesandClaude Opus 5.5 1df522097f test(acceptance): NAV-031 checks that no record is listed (the search box always offers help and Search everything)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:25:24 -05:00
james.maesandClaude Opus 5.5 f5a56eaf7e test(keyboard): name the query client wrapper (react/display-name)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:00:10 -05:00
james.maesandClaude Opus 5.5 9b475797a0 chore(avatars): UI API 1.8.0 (QQQ 4.1.0-RC.17); re-vendor the 1.8.0 document
QQQ 4.1.0-RC.16 shipped UI API 1.7.0 without profiles; they are 1.8.0 in
RC.17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:59:27 -05:00
james.maesandClaude Opus 5.5 5283503e89 fix(keyboard): read the Keyboard section from the preferences index (no 404 on servers without it); NAV-032 on the search box
A server without the Keyboard preference section answered the per-page
GET /preferences/keyboard with 404, which acceptance rightly counts as a
failed request. The setting now comes from the preferences index (shared
with the account menu, skipped without the preferences feature), so no
request fails; the switch saves there and updates both caches.

NAV-032: the search box always offers Search everything once text is
typed, so the spec checks that and that no record is listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:48:41 -05:00
james.maesandClaude Opus 5.5 bb9e083539 chore(perf): drop the quickSearchPanel lazy-chunk budget (the inline header search and / dialog are retired)
Record results render in the search box (CommandRecordResults), which
is already in the palette's lazy chunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:36:33 -05:00
james.maesandClaude Opus 5.5 d5003ebdd5 Merge main (RC.32: What's new) into feature/ia-phase1-nav; What's new moves to the Help menu
The release-notes What's new item (#165) moves from the retired sidebar
user menu into the header Help menu, with the same extension probe,
path and unread summary; its tests move with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:34:26 -05:00
james.maes 9a63173efe Merge pull request #167 from QRun-IO/feature/next-rc32-prep
chore(#712): prepare Next UI 1.0.0-RC.32
2026-10-15 17:04:55 -05:00
james.maesandClaude Opus 5.5 93e0fd7303 chore(#712): prepare Next UI 1.0.0-RC.32
PR165: What's new page and user-menu entry (release-notes UI API 1.0.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:58:59 -05:00
james.maes b24b74e30d Merge pull request #165 from QRun-IO/feature/whats-new
feat(release-notes): What's new page and user-menu entry (release-notes UI API 1.0.0)
2026-10-15 16:58:08 -05:00
james.maesandClaude Opus 5.5 ea028dc86f docs(releases): avatars via lookup
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:25:21 -05:00
james.maesandClaude Opus 5.5 3e5b097667 fix(avatars): opaque avatar URLs via batched POST /avatars/lookup
Follows the core security review (qqq#1238): other people's avatars come
from one POST /avatars/lookup per tick (user ids never in a URL), answers
are kept for the session, and every image URL is GET /avatars/{avatarId}.
Re-vendor the 1.7.0 document (lookupAvatars, opaque avatarId); e2e mocks
the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:25:14 -05:00
james.maes 98229202f3 Merge remote-tracking branch 'origin/main' into feature/whats-new
# Conflicts:
#	docs/releases/unreleased.md
2026-10-15 16:19:09 -05:00
james.maesandClaude Opus 5.5 9df3324687 fix(nav): menu actions run after focus returns; drawer search; palette active option; specs for the new header
- HeaderMenu close(then): a chosen item's action (search help, keyboard
  shortcuts, Ask, sign out) runs once focus is back on the menu button,
  so the dialog it opens returns focus there.
- The drawer's search field opens search after the drawer has closed
  and focus is back on the menu button.
- The search box input names the highlighted option
  (aria-activedescendant), which cmdk does not track for a controlled
  value.
- e2e and acceptance specs move to the one search box, the Help and
  account menus, sidebar favorites, the 1024 px drawer breakpoint,
  Shift+D and the single-key switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:14:58 -05:00
james.maes ae6649f8e1 Merge pull request #166 from QRun-IO/feature/next-rc31-prep
chore(#712): prepare Next UI 1.0.0-RC.31
2026-10-15 16:10:23 -05:00
james.maesandClaude Opus 5.5 a504a21718 chore(#712): prepare Next UI 1.0.0-RC.31
PR162: adopt QQQ UI API 1.6.0. Requires QQQ 4.1.0-RC.15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:00:12 -05:00
james.maes 9aff198a60 Merge pull request #162 from QRun-IO/feature/ui-api-1.6
feat(ui-api): adopt QQQ UI API 1.6.0 - clientCapabilities, field validation, error schema, v1 branding
2026-10-15 15:49:54 -05:00
james.maesandClaude Opus 5.5 2d125e4f90 feat(nav): one search, header Search/Ask/Jobs/Notifications/Help/Account, sidebar sections and favorites
The IA review of 2026-10-15 (Phase 1), rendered generically from
metadata (no application names):

- Sidebar sections from app section roles (UI API 1.7.0, inferred for
  older servers): Primary without a heading, History and group headings
  as role=group labels, Settings collapsed with a count. A single-child
  app is one leaf. A section name used as a path (an old group app URL)
  redirects to the app that heads it.
- Sidebar Favorites (collaboration): collapsed, up to five.
- One search box: Cmd/Ctrl+K, /, the header field and the drawer field
  open the palette (pages with search aliases, records, actions, help,
  Ask). The inline header search and the / dialog are retired.
- Header, from general to personal: Search, Ask (labelled from 1440 px),
  Jobs (always shown, idle or with a count), Notifications (severity in
  the name), Help, Account avatar. 40 px targets, 44 px on touch.
  Compact screens fold Help and Ask into the avatar menu.
- Help opens a Help menu (WCAG 3.2.6): search help, keyboard shortcuts,
  Report a bug. The user menu moves from the sidebar footer to the avatar.
- The persistent sidebar starts at 1024 px; below that it is a drawer.
- Single-key shortcuts can be turned off (WCAG 2.1.4), via the core
  Keyboard preference section or this browser. Record delete moves
  from d to Shift+D (still confirmed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:32:53 -05:00
james.maesandClaude Opus 5.5 ec974fa121 fix(processes): keep the 403 permission refusal message; carry its traceId
A refused step (HTTP 403) again becomes the user-facing 'You do not have
permission to run this process.' even when the 1.6.0 body has a message
(PRC-042); the body's traceId is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:26:18 -05:00
james.maesandClaude Opus 5.5 4f1641cb72 fix(auth): abort and discard the old session's requests at logout
A /qqq/v1/preferences response landing after the logout response set the
sessionId cookie again (SEC-037, SEC-048). Logout now ends the API
client's session generation before calling /logout: requests in flight
are aborted through a session AbortController (also passed to the
realtime and assistant fetch streams), and any response of the ended
generation is discarded as a cancellation, so it can neither set the
cookie nor trigger the 401 flow. Cancelled requests raise no error toast.
Replaces the earlier wait-for-idle approach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:02:41 -05:00
james.maesandClaude Opus 5.5 154be4f827 feat(release-notes): What's new page and user-menu entry (release-notes UI API 1.0.0)
Vendored release-notes extension contract; hand-written client for releases,
summary and seen; /app/whats-new lists published releases newest first,
grouped by category, with New badges, deep link highlight (?release=id),
paging and safe links/screenshots, and marks them seen once loaded. The user
menu shows What's new with an unread count when the extension is installed.
The bell needs no change: release notifications link to /app/whats-new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:59:25 -05:00
james.maesandClaude Opus 5.5 04fc4d7692 fix(ui-api): keep legacy behaviour where 1.6.0 changed it; settle requests before logout
- error text keeps userFacingError, error, then message; process errors are
  user-facing only from userFacingError
- the full materialDashboardTheme wins over the uiSettings.theme subset
- uiSettings are read setting by setting, falling back to materialDashboard
- audits fall back to instance metadata when the table capability is
  unavailable (RC.15 marks unaudited tables unavailable)
- global error toasts unchanged from main (no trace description)
- logout waits for in-flight queries and mutations so a late response
  cannot refresh the expired sessionId cookie
- acceptance specs accept the 1.6.0 error and icon fields, and use a
  server-only uniqueness error now that exclusive bounds are published

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:54:30 -05:00
james.maesandClaude Opus 5.5 81fa126305 test(profile): photo uploader unit and e2e coverage, full-row image fields
- ImageUploadField and UserAvatar unit tests (crop, upload, save ref,
  remove, SVG refused, a11y; initials, misses, sizes).
- tests/e2e/profile-avatars.spec.ts: upload flow against mocks and the
  6-width light/dark screenshot matrix (SCREENSHOTS_DIR).
- Image fields span the whole form row so they never squeeze at 768.
- Release notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:39:21 -05:00
james.maes 1fc984a3c6 Merge remote-tracking branch 'origin/main' into feature/ui-api-1.6
# Conflicts:
#	src/lib/query-client.ts
2026-10-15 14:39:05 -05:00
james.maesandClaude Opus 5.5 a5fb8dd8d5 feat(profile): avatars everywhere and a photo uploader on the Profile preferences
- Profile (core 'profile' section, UI API 1.7.0) renders generically; any
  FILE_UPLOAD field with valueIsUploadRef gets ImageUploadField: drag and
  drop or choose, square crop with zoom (pointer and keyboard), preview,
  remove; the crop goes to POST /uploads and the ref is saved with the form.
- One UserAvatar (photo via GET /avatars/{userId}, else initials; misses
  remembered for the session) for the user menu, presence, comments,
  mentions, people picker, activity and the assistant's user messages.
- useMyProfile reads the profile section through the shared query, so a
  save updates the header at once.
- Privacy: branding gravatarDefault no longer sends the user's email hash
  to Gravatar; a Gravatar shows only when the user chose it (server side).
- Vendor the UI API 1.7.0 core document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:23:24 -05:00
james.maes 692d0cb166 Merge pull request #161 from QRun-IO/feature/next-rc30-prep
chore(#712): prepare Next UI 1.0.0-RC.30
2026-10-15 13:22:52 -05:00
james.maes e9208fc34c Merge branch 'main' into feature/next-rc30-prep 2026-10-15 13:14:31 -05:00
james.maes 2cbfb6d962 Merge pull request #163 from QRun-IO/feature/qa-capture-spec-sync
chore(qa-capture): re-vendor qa-capture.json from qbit-qa-capture 0.1.0-RC.1
2026-10-15 13:14:02 -05:00
james.maesandClaude Opus 5.5 b2a41b3eb9 chore(qa-capture): re-vendor qa-capture.json from qbit-qa-capture 0.1.0-RC.1
Replaces the hand-written document with the QBit's generated
openapi/qqq-v1/qa-capture.yaml. Same paths, operations and fields; the
document now has the QBit's schema names (QaCaptureReportResultV1),
descriptions and nullable markers. OFF_SPEC_CEILING stays 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:42:26 -05:00
james.maes 35d937ed30 Merge pull request #160 from QRun-IO/feature/qa-capture
feat(qa-capture): optional Jam QA capture on the qa-capture UI API extension
2026-10-15 12:41:30 -05:00
james.maesandClaude Opus 5.5 a00473c305 chore: target QQQ 4.1.0-RC.15 (UI API 1.6.0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:27:09 -05:00
james.maes e76f0325a5 Merge branch 'feature/qa-capture' into feature/next-rc30-prep 2026-10-15 12:15:46 -05:00
james.maesandClaude Opus 5.5 de1d4a5295 test(standalone): include the qa backend prefix in staged-build fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:15:44 -05:00
james.maesandClaude Opus 5.5 42ee6b87ce feat(ui-api): adopt QQQ UI API 1.6.0 (QQQ 4.1.0-RC.14)
- vendor core 1.6.0 and the extension documents (alerts 1.2.0, assistant
  1.3.0, collaboration 1.1.0, data-analysis 1.5.0, edi 1.1.0, esb 1.1.0,
  quick-search 1.1.0, secured-data 1.1.0, sql-workbench 1.1.0)
- saved views, audits, sharing and bulk processes come from the table's
  clientCapabilities (fallback to the old names only for pre-1.6.0 servers)
- record forms use field.validation (required, maxLength/tooLongBehavior,
  inclusive/exclusive min/max, pattern + patternMessage)
- errors prefer BasicErrorResponseV1.message and show the traceId
- standalone branding uses GET /qqq/v1/branding/{role} (falls back on 404)
- uiSettings preferred over materialDashboard(Theme)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:09:20 -05:00
james.maesandClaude Opus 5.5 a73e4e245c chore(#712): prepare Next UI 1.0.0-RC.30
PR160: optional Jam QA capture on the qbit-qa-capture UI API extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:04:31 -05:00
james.maesandClaude Opus 5.5 e0ca5b1bb4 perf(qa-capture): load the sanitizers with the Jam chunk
The page URL filter moves to page-url.ts; sanitizers.ts is now imported only
by the dynamic Jam import, so users without capture never download it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:02:59 -05:00
james.maesandClaude Opus 5.5 c9773cf45f docs(qa-capture): QA capture guide and acceptance matrix row SEC-050
SEC-050 is recorded as excluded (pending owner approval) until
qbit-qa-capture ships a real-backend fixture; Vitest covers it meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:00:51 -05:00
james.maesandClaude Opus 5.5 15cdffe7f3 feat(standalone): opt-in Jam CSP origins and forward /qa to the backend
QQQ_QA_CAPTURE_CSP=true adds js.jam.dev to script-src and *.jam.dev to
frame-src and connect-src. /qa/recorder (the qbit's Jam domain verification
page) is a backend route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:51 -05:00
james.maesandClaude Opus 5.5 41462cc4f6 feat(qa-capture): mark secure field values for Jam blur
Password, REVEAL and secured-data masked/revealable values and inputs carry
data-jam-blur and data-qqq-secure (inert when capture is off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:50 -05:00
james.maesandClaude Opus 5.5 039fa56a0b feat(qa-capture): load Jam for enabled users and add Report a bug
The root providers load the capability once after sign-in when /version lists
qa-capture. Only an enabled capability dynamic-imports the Jam SDKs, adds the
jam:team/jam:blur meta, initializes Jam once with the sanitizers and registers
jam.metadata (route context, versions, backend context, request ids). The user
menu gains Report a bug (POST report, then Recorder.open with jam-title and
jam-reference) and QA reports; the header shows QA mode on; saving the QA mode
preference section refetches the capability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:48 -05:00
james.maesandClaude Opus 5.5 f93a1d3c8f feat(qa-capture): keep Jam Recording Link parameters across sign-in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:38 -05:00
james.maesandClaude Opus 5.5 600af361f4 feat(qa-capture): keep the last 50 request ids while capture is enabled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:37 -05:00
james.maesandClaude Opus 5.5 0969231c95 feat(qa-capture): build Jam sanitizers from the server's rules
Header removal, JSON (any depth, case-insensitive) and form field redaction,
query parameter redaction, value patterns, same-origin body drops, plus a
credential/JWT baseline. Pure and synchronous; an invalid pattern fails closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:36 -05:00
james.maesandClaude Opus 5.5 99170dbff8 feat(qa-capture): vendor the qa-capture UI API extension and its client
Hand-written per the shared qa-capture 1.0.0 contract (qaCaptureCapability,
qaCaptureReportCreate) until qbit-qa-capture publishes its generated fragment.
The capability is normalized so anything incomplete reads as disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:58:35 -05:00
james.maesandClaude Opus 5.5 74cf6e88d7 build(deps): add Jam recording-links 0.4.0 and sdk 1.0.1 for QA capture
Pinned exactly. Electron (an optional peer of the recording-links electron
entry point) is ignored so installs stay small.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:41:11 -05:00
james.maes 0f6c37c998 Merge pull request #159 from QRun-IO/feature/next-rc29-prep
chore(#712): prepare Next UI 1.0.0-RC.29
2026-10-15 05:14:29 -05:00
james.maesandClaude Opus 5.5 4306da9cdd chore(#712): prepare Next UI 1.0.0-RC.29
PR136: EDI mapper page on the qbit-edi UI API extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:51:02 -05:00
james.maes abfbbb2e55 Merge pull request #136 from QRun-IO/feature/edi-mapper
feat(edi): EDI mapper page on the qbit-edi UI API extension
2026-10-15 03:50:37 -05:00
james.maesandClaude Opus 5.5 4139316789 test(edi): mock a UI API 1.4.0 server in the mapper e2e
Next main requires UI API 1.4.0 or later (sign-in refuses older servers),
so the mapper spec's version mock moves from 1.1.0 to 1.4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:25:44 -05:00
james.maesandClaude Opus 5.5 aa0bc65774 Merge origin/main into feature/edi-mapper
Resolves src/lib/query-client.ts by keeping main's jobs, secured-data and
preference keys alongside the EDI mapper keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:25:42 -05:00
james.maes 86b54181f7 Merge pull request #158 from QRun-IO/feature/next-rc28-prep
chore(#712): prepare Next UI 1.0.0-RC.28
2026-10-15 01:33:47 -05:00
james.maesandClaude Opus 5.5 b32141cf65 chore(#712): prepare Next UI 1.0.0-RC.28
PR141: zero off-spec UI API calls (minimum UI API 1.4.0). Requires QQQ
4.1.0-RC.9 (RC.8's /table/{t}/apis was shadowed, qqq#1216).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:59:19 -05:00
james.maes 16699aedfa Merge pull request #141 from QRun-IO/feature/ui-api-zero-off-spec
feat(ui-api): zero off-spec calls (OFF_SPEC_CEILING 0, UI API >= 1.4.0)
2026-10-15 00:58:51 -05:00
james.maesandClaude Opus 5.5 e0ff899cb6 fix(developer): table spec URL ends in json so RapiDoc offers "View OpenAPI spec"
RapiDoc shows the View button only for a spec URL ending in "json"; the
v1 document operation is /openapi, so add ?format=json (ignored by the
server). Fixes acceptance REC-053.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:37:25 -05:00
james.maesandClaude Opus 5.5 e807d30209 test(acceptance): ESB-003 allows the refused table-APIs lookup
Without person READ, the table developer view's GET /table/person/apis
is refused (403) like the ESB section's request; /apis.json used to
answer 200 for any caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:10:07 -05:00
james.maesandClaude Opus 5.5 11d0f34639 test(e2e): the route mocks answer GET /version as a 1.4.0 server
With SUPPORTED_UI_API.minimum at 1.4.0, a mocked app without a /version
route falls back to the pre-semver 1.0.0 and refuses to load. Specs that
need an extension still register their own /version route on top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:57:08 -05:00
james.maesandClaude Opus 5.5 45928e8abf test(dev): give the associated-script card test room for its three 5 s waits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:14:32 -05:00
james.maesandClaude Opus 5.5 1a381c2a68 Merge origin/main; vendor the RC.8 documents (UI API 1.4.0)
Re-vendor qqq-v1.json and api-aware.json from the v4.1.0-RC.8 tag and
move main's new e2e /version mocks to 1.4.0 (SUPPORTED_UI_API.minimum).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:12:21 -05:00
james.maes ee3682e859 Merge pull request #157 from QRun-IO/feature/next-rc27-prep
chore(#712): prepare Next UI 1.0.0-RC.27
2026-10-14 21:08:42 -05:00
james.maesandClaude Opus 5.5 62ec386035 chore(#712): prepare Next UI 1.0.0-RC.27
PR124 (secured-data UI), PR155 (jobs tray and live job view), PR156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:19:11 -05:00
james.maes 427a037d53 Merge pull request #155 from QRun-IO/feature/jobs-tray
feat(jobs): live job view, header Jobs tray and Jobs page
2026-10-14 20:18:29 -05:00
james.maes 2381579a52 Merge remote-tracking branch 'origin/main' into feature/jobs-tray
# Conflicts:
#	src/lib/query-client.ts
2026-10-14 19:49:22 -05:00
james.maes 112bbc557a Merge pull request #156 from QRun-IO/fix/large-table-default-columns
test(acceptance): prfWide declares its 40 default columns (PRF-001/002 after #142)
2026-10-14 19:45:22 -05:00
james.maesandClaude Opus 5.5 463e9cc8e9 test(acceptance): the 40-column performance table names all columns as its default
#142 made tables wider than twelve fields open with a short default column
set, so PRF-001/PRF-002 saw 6 of prfWide's 40 columns (and 250 x 6 = 1,500
cells instead of 10,000). The budgets measure the full 40-column grid, so
the fixture now declares every column through the nextUi.defaultColumns
supplemental hint, which is the metadata route #142 added for this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:16:16 -05:00
james.maes 20fd1fa3d3 Merge pull request #124 from QRun-IO/feature/secured-data-ui
feat(secured-data): masked values, reveal, restricted filters and break-glass banner
2026-10-14 19:14:52 -05:00
james.maesandClaude Opus 5.5 4f685d200b test(acceptance): PRC-019 and PRF-005 read the live job view
The Working screen is replaced by the live job view, so the progress
assertions use its hooks: job-progress-message, job-progress-stats
(N / M and elapsed) and the Job progress bar. Feature matrix and
Material parity rows updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:14:20 -05:00
james.maes 9fc9cab48f Merge remote-tracking branch 'origin/main' into feature/secured-data-ui
# Conflicts:
#	src/app/(dashboard)/layout.tsx
#	src/components/forms/DynamicFormField.tsx
#	src/components/forms/EntityForm.tsx
#	src/components/query/DataGrid.tsx
#	src/components/query/FilterBuilder.tsx
#	src/lib/query-client.ts
#	src/lib/utils/filter-utils.ts
#	src/types/metadata.ts
2026-10-14 18:48:55 -05:00
james.maesandClaude Opus 5.5 aafeabfb33 fix(jobs): only call the jobs operations when the instance has the jobs tables
durableJobs is listed by every 1.3+ server, but the jobs operations exist
only where the instance defines jobs_job and jobs_log. Gate the tray,
page and inline tracking on both, so older servers and apps without
durable jobs never see a jobs request (acceptance flagged GET /jobs 404).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:47:33 -05:00
james.maes 4663557ca4 Merge pull request #154 from QRun-IO/feature/next-rc26-prep
chore(#712): prepare Next UI 1.0.0-RC.26
2026-10-14 18:08:31 -05:00
james.maesandClaude Opus 5.5 3cf64ce24c chore(#712): prepare Next UI 1.0.0-RC.26
PR142: tables open with a short default column set (audit High, #114).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:59:34 -05:00
james.maesandClaude Opus 5.5 b19d4c09b4 feat(jobs): live job view, header Jobs tray and Jobs page
- Header Jobs button next to the bell: active count badge, a subtle ring
  while jobs run, and a popover listing running jobs (eased mini bars,
  percent, ETA) and recently finished ones; clicking reopens a job
- Live job view replaces Working / Executing Process: phase stepper,
  eased determinate bar (shimmer when indeterminate), percent, counts,
  bytes, ETA, elapsed, message, collapsible live log with copy
- Run in background (POST /jobs/track for inline runs) and auto-detach
  for durableJobAlways processes after 5 s unless the user stays
- /app/jobs (Active, Recent, All for jobs_admin) and /app/jobs/{id}
- Realtime job.progress with since-cursor polling fallback; only
  /qqq/v1 operations from the re-vendored 1.4.0 document
- Motion on transform/opacity only, static under reduced motion;
  light and dark; Vitest, mocked e2e and screenshots at 390 and 1440

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:59:19 -05:00
james.maes caa89dc24f Merge pull request #142 from QRun-IO/feature/default-list-columns
feat(query): open tables with a short default column set
2026-10-14 17:56:20 -05:00
james.maes 5f3b7c697d Merge pull request #153 from QRun-IO/feature/next-rc25-prep
chore(#712): prepare Next UI 1.0.0-RC.25
2026-10-14 17:45:49 -05:00
james.maesandClaude Opus 5.5 bebb4f2444 chore(#712): prepare Next UI 1.0.0-RC.25
PR151 (assistant re-attach, never block asking, progress, Stop), paired
with qbit-assistant 0.1.0-RC.13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:35:13 -05:00
james.maesandClaude Opus 5.5 5afd785a22 test(search): wait for the first record suggestion to be selected
GlobalSearch's grouped-suggestions test asserted the selected option
synchronously after the list appeared. Under the full CI suite the
selection had not settled yet and the search-everything row was still
selected. The assertion now waits, as the next one already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:35:09 -05:00
james.maes 2d6a33ea9f Merge remote-tracking branch 'origin/main' into feature/default-list-columns 2026-10-14 17:33:00 -05:00
james.maes 70594c40ba Merge pull request #151 from QRun-IO/feature/assistant-reattach
feat(assistant): never block asking, re-attach after refresh, live progress and Stop
2026-10-14 17:20:08 -05:00
james.maes 9f3c48f4c7 Merge pull request #152 from QRun-IO/feature/next-rc24-prep
chore(#712): prepare Next UI 1.0.0-RC.24
2026-10-14 17:04:15 -05:00
james.maesandClaude Opus 5.5 38d2c62f3a chore(#712): prepare Next UI 1.0.0-RC.24
RC23's tag acceptance failed on tablet (REC-004, REC-019, NAV-015);
PR149 fixes it. Also includes PR150, PR148 and PR145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:57:15 -05:00
james.maes 3ab4815526 Merge pull request #150 from QRun-IO/feature/empty-state-table-help
feat(query): empty tables explain what they are for and how rows get created
2026-10-14 16:56:28 -05:00
james.maes f0b4c775f0 Merge pull request #149 from QRun-IO/fix/record-tabs-tablet
fix(record): section tabs wrap instead of folding into More (tablet regression from #144)
2026-10-14 16:56:07 -05:00
james.maesandClaude Opus 5.5 f29bedd437 test(e2e): the turn POST now carries concurrency (supersede by default)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:51:32 -05:00
james.maesandClaude Opus 5.5 336affb9e2 chore(openapi): vendor the assistant UI API extension 1.2.0 (qbit-assistant#49)
turns/active, concurrency on a new turn, 503 SHUTTING_DOWN, turn.phase and
message.start.startedAt; the contract test now covers the new call.
Also: text buttons in the background-run banner, a check icon in the
concurrency menu, and the Enter hint kept to fine pointers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:37:57 -05:00
james.maesandClaude Opus 5.5 92b165eb9c feat(query): an empty, unfiltered table explains itself from its help slot
When a table has no records and no filter or search is active, the empty
state shows 'No <Table> yet' and the table's help (the 'help' slot: what the
table is for and how its rows get created) instead of 'Try adjusting your
filters'. Filtered lists and tables without help keep the existing text.
Adds helpPlainText (whole help as plain text); helpSummary reuses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:33:56 -05:00
james.maesandClaude Opus 5.5 42fa4c3c5f feat(assistant): never block asking; re-attach after refresh; live progress
Several runs per conversation: sending while an answer runs replaces it
(default) or runs both (panel menu choice). On load, focus and online the
panel re-attaches to running answers found through the UI API, shows a
banner with Open and Stop for answers in other conversations, and the
header button shows a busy state. Each running answer shows its phase
(thinking, tool, writing) with elapsed time; Stop is always available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:26:26 -05:00
james.maesandClaude Opus 5.5 72872f5ccd feat(assistant): find active turns, retry restarts, resume until the turn ends
listActiveAssistantTurns (GET /assistant/turns/active, [] on an older
server), the optional concurrency on a new turn, a retried 503
SHUTTING_DOWN start, the turn.phase event, an idempotent cancel (409 is
finished), and a resume that keeps going with capped backoff for a time
budget instead of giving up after three attempts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:26:23 -05:00
james.maesandClaude Opus 5.5 b89029c0ff test(record): import QRecord from the types barrel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:05:10 -05:00
james.maesandClaude Opus 5.5 bb81e3c9a4 test(acceptance): NAV-015 waits for each list before navigating on
WebKit reports an API read that a navigation cuts off as "due to access
control checks", which the diagnostics fixture counts as an error
(tablet failure on the RC.23 tag). The title test now lets the Person and
Nav Deep Item lists load before opening the next page, as NAV-016 does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:04:01 -05:00
james.maesandClaude Opus 5.5 413c736050 fix(record): section tabs wrap onto rows instead of folding into More
#144 folded tabs that did not fit into a More menu. At tablet widths that
hid sections from the tab bar, so they could not be opened by touch or
reached by the arrow keys, and their panels were unnamed (acceptance
REC-004, plus REC-019, whose Time Zones tab was folded). Every section is
now a tab again. The tab bar wraps onto more rows and never scrolls
sideways (#116), and a label longer than the bar wraps inside its tab.
The More menu component is removed. Unit tests cover the wrapping, the
keyboard navigation and tapping the last section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:03:57 -05:00
james.maes c07fba1d4b Merge pull request #148 from QRun-IO/feature/policy-editor-full-access
feat(authentik): Full access checkbox in the policy editor
2026-10-14 16:03:44 -05:00
james.maesandClaude Opus 5.5 8437aeb0eb test(acceptance): REC-030 shows the two columns it measures
Record Lab has more than twelve fields, so it now opens with the short
default column set; the SIZE test turns on Short Code and Website before
measuring them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:48:24 -05:00
james.maes e39d4b847b Merge pull request #145 from QRun-IO/feature/home-no-duplicate-grids
fix(app-home): no automatic card grids on homes that declare widgets
2026-10-14 15:48:01 -05:00
james.maesandClaude Opus 5.5 9b596289af feat(authentik): Full access checkbox in the policy editor
A prominent "Full access (all current and future resources)" checkbox emits
the single {Action:["*"],Resource:["*"]} statement and disables the matrix.
Resource "*" with any other action is reported invalid. Opening a
full-access group shows it ticked; unticking returns an empty matrix. The
member preview says "Full access: everything in this app".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:27:30 -05:00
james.maesandClaude Opus 5.5 e434716cbd chore(ui-api): UI API 1.4.0 - re-vendor and require 1.4.0
RC.7 shipped UI API 1.3.0; the operations this PR uses ship in 1.4.0
(QRun-IO/qqq#1188, #1189, #1206). Re-vendor qqq-v1.json and
api-aware.json at 1.4.0, set SUPPORTED_UI_API.minimum to 1.4.0 and move
the mocked /version responses to 1.4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:20:32 -05:00
james.maes dcc3a47522 Merge pull request #147 from QRun-IO/feature/next-rc23-prep
chore(#712): prepare Next UI 1.0.0-RC.23
2026-10-14 15:18:43 -05:00
james.maesandClaude Opus 5.5 90fc22327c chore(#712): prepare Next UI 1.0.0-RC.23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:11:52 -05:00
james.maes 39b8f8b7ae Merge pull request #127 from QRun-IO/feature/help-everywhere
feat(help): help everywhere — icons, long-help panel, help search, author links
2026-10-14 15:09:41 -05:00
james.maesandClaude Opus 5.5 8f90df5ae9 fix(app-home): keep section grids by default; hint hides both
Hiding the section grids on every home with widgets removed the only way
to launch app-level processes on some homes (acceptance NAV-008/009,
PRC-004, SEC-008). By default only the Apps grid now gives way to widgets;
nextUi.showChildGrid=false hides both grids for homes whose widgets cover
them (kof22 sets it on its own and the qbit homes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:06:15 -05:00
james.maesandClaude Opus 5.5 e8bcb0bbe3 fix(query): keep every column on tables with at most twelve fields
Narrow tables do not have the wide-grid problem, so the short default set
applies only above twelve visible fields (or when the table names its own
set). Existing tables of that size, and the acceptance fixtures, open as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:05:16 -05:00
james.maes 28b2de402c Merge pull request #146 from QRun-IO/feature/nested-app-urls
fix(routing): redirect app-nested URLs to their canonical path
2026-10-14 15:01:23 -05:00
james.maes acd324e410 Merge pull request #144 from QRun-IO/feature/record-tabs-wrap
fix(record): section tabs fold into a More menu instead of scrolling
2026-10-14 15:01:14 -05:00
james.maes 4c9d0ab9e1 Merge pull request #139 from QRun-IO/fix/breadcrumb-no-hscroll
fix(layout): breadcrumb without inner horizontal scroll
2026-10-14 14:59:26 -05:00
james.maes cd2754b93c Merge pull request #138 from QRun-IO/fix/dark-mode-os-default
feat(theme): default to the OS color scheme (System / Light / Dark)
2026-10-14 14:59:19 -05:00
james.maes be72703bd7 Merge pull request #143 from QRun-IO/feature/quick-search-suggest
feat(search): typeahead suggestions from the quick-search extension in the global search box
2026-10-14 14:56:10 -05:00
james.maes 219db989ed Merge pull request #140 from QRun-IO/feature/policy-editor-features
feat(authentik): policy editor feature permissions and member preview
2026-10-14 14:56:00 -05:00
james.maes 4cfb03c6b7 Merge pull request #99 from QRun-IO/dependabot/github_actions/github-actions-56a18c2c80
build(deps): Bump the github-actions group across 1 directory with 4 updates
2026-10-14 13:54:23 -05:00
james.maes 496cb0a14c Merge pull request #68 from QRun-IO/dependabot/npm_and_yarn/rapidoc-10.1.0
build(deps): Bump rapidoc from 9.3.8 to 10.1.0
2026-10-14 13:54:14 -05:00
james.maesandClaude Opus 5.5 996b0271de fix(routing): redirect app-nested URLs to their canonical path (#117)
The breadcrumb shows app > table > record, but /app/{app}/{table} said
"Table metadata is unavailable" and /app/{app}/{table}/{id} was a 404.
When the first segment is an app (and not also a table) and the second is a
table, process or app, the page now replaces the URL with /app/{second}/...,
keeping the rest of the path, the query string and the hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:53:52 -05:00
james.maesandClaude Opus 5.5 2580cf95a6 fix(app-home): no automatic card grids on homes that declare widgets (#115)
App homes appended automatic section (Actions, Reports, Data) and child-app
grids under their own widgets, repeating the nav as cards (a screen or two
on a phone). Homes that declare widgets now skip both grids; homes without
widgets keep them. The app hint nextUi.showChildGrid overrides either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:36:16 -05:00
james.maesandClaude Opus 5.5 a07213f633 fix(record): section tabs fold into a More menu instead of scrolling (#116)
The record view's section tab strip scrolled sideways inside its box when a
record had many sections (13 on a blog post). Tabs that do not fit now move
into a More dropdown, measured with ResizeObserver; the active section always
stays visible, arrow-key navigation and tab roles are kept, and the phone
accordion is unchanged. Nothing in the strip scrolls horizontally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:36:13 -05:00
james.maesandClaude Sonnet 5.5 17203f9876 test(search): mocked e2e for typeahead suggestions at 390 and 1440 px, light and dark
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:30:51 -05:00
james.maesandClaude Sonnet 5.5 e30c23dcbf feat(search): header search, dialog and palette use typeahead suggestions when installed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:30:48 -05:00
james.maesandClaude Sonnet 5.5 3fb7002a52 feat(search): useRecordSearch suggest source with core-search fallback
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:30:46 -05:00
james.maesandClaude Sonnet 5.5 05ceeb1925 feat(search): quick-search suggest API client and vendored extension contract
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:30:44 -05:00
james.maesandClaude Opus 5.5 305d85bf61 feat(query): open tables with a short default column set (#114)
With no saved view, the query screen shows the primary key, then T1 and T2
section fields, skipping hidden, heavy and long-text fields, capped at six;
every other column starts unchecked. A table can name its own set with the
supplemental hint nextUi.defaultColumns. Saved and legacy views are kept;
reset goes back to the short set. Blog Posts no longer opens with 29 columns
and an inner horizontal scroll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:20:40 -05:00
james.maesandClaude Opus 5.5 4bd2c95d01 feat(ui-api): zero off-spec calls - every UI call is a spec'd /qqq/v1 operation
- Form adjusters: POST /table/{t}/formAdjuster/{id}/{event} (JSON) instead
  of the legacy multipart /material-dashboard-backend route.
- API discovery: GET /table/{t}/apis (versions included) instead of
  /apis.json and {api}/versions.json; the table document through
  GET /table/{t}/apis/{api}/{version}/openapi.
- Vendor the refreshed UI API document (1.3.0) and the API-aware
  document (extensions/api-aware.json) for the API-versioned
  metadata/query/count routes.
- OFF_SPEC_CALLS is empty and OFF_SPEC_CEILING is 0.
- SUPPORTED_UI_API.minimum is 1.3.0 (the operations above are new in it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:08:43 -05:00
james.maesandClaude Opus 5.5 863099b95f feat(authentik): policy editor feature permissions and member preview
- Table features (Analyze, Comment, ..., and READ_WRITE `write`) are
  selectable checkboxes per table, included in the row, column and app
  select-alls and in search; policy-document parses and generates them
  in canonical order and round-trips documents that use them.
- "Preview for a member" (qbit-authentik#29 payload): pick a member and
  see their effective permissions with this document, with added and
  removed markers against the saved document and a mapper-off note.
  Lazy-rendered; no extra network calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:03:19 -05:00
james.maesandClaude Opus 5.5 9d57861672 fix(layout): keep every breadcrumb a visible link; truncate and wrap instead of collapsing
The ellipsis menu removed middle crumbs from the DOM, breaking the
acceptance specs that click each crumb (NAV-014, 007, 018, 019, 028) and
the 44 px touch-target check on the header (NAV-038). Every crumb is a link
again, in order. Long labels truncate with an ellipsis and a title; the
trail wraps onto another line (the header grows by min-height) so it never
scrolls sideways and never drops below the touch-target width.

e2e still checks no overflow at 320, 390, 768 and 1440 px, and now that
all crumbs stay visible links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:00:04 -05:00
james.maesandClaude Opus 5.5 ce8da2d2a4 fix(theme): keep the appearance control's Light/Dark behavior; System is the unset state
NAV-056 acceptance failed because the control now reported the saved
preference instead of the active mode. Under an application theme the
Light radio is again the active one (Dark stays disabled, the explanation
still shows, the saved choice is kept). System remains a third radio that
means no saved key.

Spec changes, because the old assertions encode the retired "OS never
matters" default: with the OS dark and no saved choice the app is dark and
System is selected; Reset to Defaults removes the key (it no longer writes
"false") and follows the OS, including a live OS change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:55:50 -05:00
james.maes 4c40679023 Merge remote-tracking branch 'origin/main' into feature/help-everywhere
# Conflicts:
#	src/components/forms/DynamicForm.tsx
#	src/components/forms/DynamicFormField.tsx
2026-10-14 12:55:38 -05:00
james.maes 40cc60b1d4 Merge pull request #120 from QRun-IO/feature/contingent-possible-values
feat(forms): contingent possible values across forms, bulk edit, processes and filters
2026-10-14 12:42:43 -05:00
james.maes 292a2a9aea Merge remote-tracking branch 'origin/main' into feature/help-everywhere
# Conflicts:
#	tests/setup.ts
2026-10-14 12:26:20 -05:00
james.maesandClaude Opus 5.5 771e46ecc3 fix(layout): breadcrumb never scrolls sideways; middle crumbs collapse into a menu
Removes the breadcrumb's horizontal scroll. At narrow widths the middle
crumbs collapse into an ellipsis menu, the current page truncates with a
tooltip, and the header keeps one line. e2e checks no overflow at 320,
390, 768 and 1440 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:14:35 -05:00
james.maesandClaude Opus 5.5 51775ab0cb feat(theme): follow the OS color scheme unless the user picked light or dark
With no qqq-dark-mode preference the app follows prefers-color-scheme and
reacts to OS changes; an explicit choice still wins. Display preferences
offer System / Light / Dark, and a pre-hydration script applies the same
rule to avoid a flash of the wrong theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:14:29 -05:00
james.maesandClaude Opus 5.5 4b8226a4cc Merge origin/main into feature/contingent-possible-values
Picks up the realtime-singleton test isolation fix (#137).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:06:31 -05:00
james.maes db941a8cc5 Merge pull request #137 from QRun-IO/fix/association-metadata-dedupe
test: isolate the realtime client between tests (fixes the metadata dedupe flake)
2026-10-14 12:06:20 -05:00
james.maes 65db5355af Merge remote-tracking branch 'origin/main' into fix/association-metadata-dedupe
# Conflicts:
#	tests/setup.ts
2026-10-14 11:44:59 -05:00
james.maes 085430e17f Merge remote-tracking branch 'origin/main' into feature/help-everywhere
# Conflicts:
#	src/lib/api/openapi/qqq-v1.json
2026-10-14 11:38:28 -05:00
james.maesandClaude Opus 5.5 daf4005186 test: isolate the realtime singleton between tests; preload the lazy RecordView
"keeps arbitrary aliases distinct and deduplicates target metadata loads"
intermittently saw the pet metadata handler run twice under load. The
second request was not a product duplicate: it came from the realtime
tab singleton, which outlived each test. Once a test held a record view
past the 150 ms leader election, the singleton connected and kept the
stream cursor. The next test's first connection then resumed with
Last-Event-ID, the MSW realtime mock answered it with `resync` (a replay
miss), and invalidateRealtime refetched every active query in that
test's cache, including the already-loaded pet metadata.

Each test is a fresh page load, so the test setup now closes and forgets
the singleton after every test (new resetRealtimeClient). The page test
also preloads the lazy RecordView in beforeAll, as other page tests do,
so a single case run with -t no longer misses its first deadline on cold
compilation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:33:47 -05:00
james.maesandClaude Opus 5.5 de51fcbf83 Merge origin/main into feature/contingent-possible-values
Picks up the record-page association dedupe fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:27:44 -05:00
james.maes a521be57cc Merge pull request #121 from QRun-IO/feature/user-preferences
feat(preferences): Preferences page, notification settings drawer and section actions
2026-10-14 11:17:02 -05:00
james.maesandClaude Opus 5.5 4e6473aae2 test(help): WID-044 checks the summary tooltip and the help panel; HLP-003 is desktop-only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:11:22 -05:00
james.maesandClaude Opus 5.5 34efebb434 fix(edi): exact field labels in the preview pane; stabilize mapper e2e
- Sample and Guide revision selects get standalone labels, so their
  accessible names are exact.
- The overflow check ignores visually hidden (1px) elements, and the
  axe run disables color-contrast like the shared a11y helper (the
  mocked indigo accent fails repo-wide).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:08:38 -05:00
james.maesandClaude Opus 5.5 439c4b00e4 test(edi): open the guide segment through its summary element
The segment label is a span inside the summary, so pressing Enter on it
never toggled the details element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:49:44 -05:00
james.maesandClaude Opus 5.5 648d2d8bf8 Merge origin/main into feature/user-preferences
Preferences now use the shared UI API discovery (#133): a core-feature
check isUiApiFeatureSupported sits next to the extension check, replacing
the separate cache; the version mock helper announces preferences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:36:32 -05:00
james.maesandClaude Opus 5.5 f95baaf8c7 feat(edi): EDI mapper page on the qbit-edi UI API extension
- Vendor the edi extension document (qbit-edi 1.0.0) with generated
  types, API client and hooks; all calls are on spec.
- "Open Mapper" record action on edi_map (only when the edi extension is
  registered and the map is readable); route /app/edi_map/{id}/mapper.
- Guide tree, target fields and associations, keyboard and drag rule
  creation, transform argument forms, validation of unsaved rules,
  sandbox preview with rule overrides and a PHI-suppressed notice, and
  Save through the core table API for DRAFT maps only.
- Three panes at xl, two at lg, tabs below; saved rules collapsed.
- Unit and Playwright tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:27:01 -05:00
james.maes e78537ad36 Merge remote-tracking branch 'origin/main' into feature/help-everywhere
# Conflicts:
#	src/components/forms/DynamicFormField.tsx
#	src/components/forms/EntityForm.tsx
#	src/components/process/ProcessRun.tsx
#	src/components/process/ProcessStepScreen.tsx
#	src/components/query/RecordQuery.tsx
#	src/components/records/RecordViewHeader.tsx
#	src/components/records/RecordViewSection.tsx
#	src/lib/api/openapi/qqq-v1.json
#	src/types/metadata.ts
2026-10-14 10:26:06 -05:00
james.maesandClaude Opus 5.5 8478404802 Merge origin/main into feature/contingent-possible-values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:13:20 -05:00
james.maes 4ba1bb4cae Merge pull request #135 from QRun-IO/feature/next-rc22-prep
chore(#712): prepare Next UI 1.0.0-RC.22
2026-10-14 10:09:20 -05:00
james.maesandClaude Opus 5.5 c50675109a test: reset the preferences support cache synchronously in setup
The cache lives in an import-free module, so the global afterEach no longer
awaits a dynamic import between tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:08:20 -05:00
james.maes ab9245e66c Merge pull request #133 from QRun-IO/feature/108-qbit-presence-from-version
feat(ui-api): qbit presence from GET /version; vendor assistant extension (#108, off-spec 7)
2026-10-14 10:02:18 -05:00
james.maes be2d0fc66e Merge branch 'main' into feature/next-rc22-prep 2026-10-14 09:56:31 -05:00
james.maes 652ade887f Merge pull request #134 from QRun-IO/fix/collaboration-e2e-conflict-race
test(e2e): harden the collaboration conflict test against the mock stream attach race
2026-10-14 09:54:47 -05:00
james.maesandClaude Opus 5.5 3330f1bb63 Merge origin/main into feature/108-qbit-presence-from-version
Off-spec ceiling: main dropped POST /uploads (20); minus the 13
assistant calls is 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:31:25 -05:00
james.maes 1bfebd09fb Merge remote-tracking branch 'origin/main' into feature/user-preferences
# Conflicts:
#	src/lib/api/openapi/ui-api-calls.ts
2026-10-14 09:29:39 -05:00
james.maesandClaude Opus 5.5 fb8b4efa6a Merge origin/main into feature/contingent-possible-values
Kept main's ui-api-calls.ts (#128 carries the same off-spec fix); re-applied the one-line formMethods prop on main's EntityForm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:36 -05:00
james.maesandClaude Opus 5.5 d3629d72b1 chore(#712): prepare Next UI 1.0.0-RC.22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:25:55 -05:00
james.maes 48e81b4d12 Merge pull request #128 from QRun-IO/feature/collaboration-presence-share-tasks
feat(collaboration): realtime presence, conflict review, sharing, tasks and My work
2026-10-14 09:24:02 -05:00
james.maesandClaude Opus 5.5 94494fc34f Merge origin/main into feature/user-preferences
Re-vendors the core document from qqq develop (1.3.0, with preferences)
plus main's vendored jobs and uploads operations; POST /uploads is now
spec'd, so its off-spec entry goes and the ceiling drops to 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:18:10 -05:00
james.maesandClaude Opus 5.5 2bdd7d980b test(e2e): resend the concurrent edit until the conflict banner shows (mock stream attach race on mobile)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:12:33 -05:00
james.maesandClaude Opus 5.5 ff1ea58cf1 fix(ui-api): drop the POST /uploads off-spec exception now that it is vendored
Main vendored /qqq/v1/uploads (#132) but kept the exception, which fails the contract test's 'only shrinks' check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:57:35 -05:00
james.maesandClaude Opus 5.5 58cca84fda Merge origin/main into feature/contingent-possible-values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:55:43 -05:00
james.maesandClaude Opus 5.5 91b335a42d fix(ui-api): extension discovery does not import the query client
The test setup resets discovery after each test; importing the query
client there pulled sonner into every test document and broke the
toast-observation tests. The version query key now lives beside the
discovery code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:53:38 -05:00
james.maesandClaude Opus 5.5 617bd1962d chore(ui-api): drop the POST /uploads off-spec exception now that the vendored document has it (ceiling 20)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:40:20 -05:00
james.maes cfba697339 Merge remote-tracking branch 'origin/main' into fix/collaboration-live-review
# Conflicts:
#	src/components/records/RecordView.tsx
#	src/lib/api/openapi/qqq-v1.json
2026-10-14 08:38:41 -05:00
james.maes 3db5a0507d Merge pull request #132 from QRun-IO/feature/vendor-alerts-sqlwb-ui-api
feat(ui-api): vendor the alerts and sql-workbench extension documents (off-spec 46 -> 21)
2026-10-14 08:30:26 -05:00
james.maes 36c07c7aa7 Merge pull request #123 from QRun-IO/feature/data-analysis-saved
feat(data-analysis): save, share, star, export and watch analyses
2026-10-14 08:29:50 -05:00
james.maesandClaude Opus 5.5 2ba63346f0 Merge origin/main into feature/contingent-possible-values
Picks up the off-spec ceiling fix (#131) that broke the UI API contract test on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:28:11 -05:00
james.maes a1c4a7de1a Merge pull request #126 from QRun-IO/feature/run-in-background
feat(processes): run in background, background-job banner, widget refresh
2026-10-14 08:28:03 -05:00
james.maes 4d4ba49e8f Merge pull request #129 from QRun-IO/fix/chart-sr-table-overflow
fix(widgets): chart data table overflow and doubled widget section headings
2026-10-14 08:27:50 -05:00
james.maesandClaude Opus 5.5 4f2cf6eb64 feat(ui-api): qbit presence from GET /version; no hard-coded qbit names (#108)
- ui-api-extensions: one shared /version discovery (isUiApiExtensionInstalled,
  useUiApiExtensionInstalled, UI_API_EXTENSION names); data-analysis
  delegates to it; reset on sign-out and user change.
- Alerts: presence from the "alerts" extension instead of the hard-coded
  alerts app (per-user access still from the inbox summary's 403/404);
  record-panel actions come from the panel payload (permission-aware
  hrefs) instead of hard-coded alertsAcknowledge / alertsSnooze /
  alerts_alert.
- Assistant: /assistant/config is probed only when the "assistant"
  extension is registered.
- Mocks announce their extensions from one /version handler; tests and
  mocked e2e routes updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:23:53 -05:00
james.maesandClaude Opus 5.5 4ad866286b feat(ui-api): vendor the assistant extension document
qbit-assistant#19 serves its /qqq/v1/assistant operations as the
assistant UI API extension (1.0.0). Vendor it and drop its 13 calls
from the off-spec list (ceiling 21 -> 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:05:12 -05:00
james.maesandClaude Opus 5.5 dc6b1e2373 feat(ui-api): vendor the alerts and sql-workbench extension documents
The Alerts (qbit-alerts#19) and SQL Workbench (qbit-sql-workbench#21)
qbits now serve their /qqq/v1 operations as UI API extensions (1.0.0).
Vendor both documents and drop their 25 calls from the off-spec list
(ceiling 46 -> 21).

- Inbox actions call their literal operations (/alerts/inbox/read,
  /unread, /dismiss).
- The alerts extension removed the record-panel watch operation; the
  record panel drops its "Watch alerts for this record" button and
  keeps showing the deprecated watching state read-only. Following a
  record belongs to the collaboration panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:54:15 -05:00
james.maes 96b789088e Merge remote-tracking branch 'origin/main' into feature/data-analysis-saved 2026-10-14 07:53:09 -05:00
james.maes a75fd8b5f6 Merge remote-tracking branch 'origin/main' into fix/collaboration-live-review 2026-10-14 07:51:46 -05:00
james.maes 635def9ba8 Merge remote-tracking branch 'origin/main' into feature/data-analysis-saved 2026-10-14 07:49:51 -05:00
james.maesandClaude Opus 5.5 54a2db37b7 Merge origin/main into feature/contingent-possible-values
Resolved FilterBuilder.tsx: keep both the contingent dependencyField and main's supportedOperators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:44:30 -05:00
james.maes 3dfc93612f Merge remote-tracking branch 'origin/main' into fix/collaboration-live-review 2026-10-14 07:20:24 -05:00
james.maes 1e82e8696d Merge remote-tracking branch 'origin/feature/data-analysis-compare' into feature/data-analysis-saved 2026-10-14 07:09:07 -05:00
james.maesandClaude Opus 5.5 e2838496b6 test(api): cover directory search, presence beacon, my tasks and the realtime transport
Lifts src/lib/api function coverage above its 93% threshold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:08:46 -05:00
james.maesandClaude Opus 5.5 05be095a8d fix(realtime): stop the subscription sync loop when the server neither activates nor refuses a channel
The sync loop re-added a channel forever (never yielding to the event loop) when an update
response listed it as neither active nor refused, exhausting the heap of a test worker.
Channels a response leaves unresolved are now treated as refused until the next reconnect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:08:45 -05:00
james.maesandClaude Opus 5.5 3fcefdb6b4 fix(collaboration): keep the established edit payload and not-found copy outside collaboration tables
Versioned, dirty-only edit saves apply only to tables with PRESENCE or
FOLLOW (acceptance REC-009, REC-016, REC-023 expect the full payload and
defaults on other tables). The not-found view keeps its original wording
and Back button; the neutral wording and Request access appear only on
SHARING tables, below the Back button (REC-003).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:41:18 -05:00
james.maesandClaude Opus 5.5 a0a26b547b fix(collaboration): reset the touch minimum on tag chip controls with important
The global 44 px touch-target rule is unlayered CSS, so it outranks the
layered min-h-0/min-w-0 utilities whatever their specificity, and tag
chips on touch screens were still 50 px tall. The chip controls now use
the important modifier (min-h-0!, min-w-0!): chips measure 26 px on a
Pixel 7 while each control keeps a 44 x 44 px transparent touch overlay.
The record-panel screenshot test asserts the chip height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:28:57 -05:00
james.maesandClaude Opus 5.5 8408f6c445 fix(collaboration): hide empty activity details and word old audit summaries
- An activity entry shows "Details" only when at least one permitted
  detail has a message or a value change, so audits whose details are
  all empty no longer leave a Details header over nothing.
- Summaries are shown as the service sends them. Older core audit
  summaries of the form "Record was Edited" read "Edited", and "Record
  was Inserted" reads "Created" (other verbs in sentence case).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:16:32 -05:00
james.maesandClaude Opus 5.5 ebcd8aec30 feat(collaboration): show people by display name, falling back to the id
The service is adding display names beside user ids. The UI now renders
them when present and falls back to the id otherwise:

- Tasks: "Assigned to <assigneeDisplayName>", plus "Created by ..." and,
  for done tasks, "Completed by ...".
- Files: the uploader's createdByDisplayName.
- Comments already used authorDisplayName.
- Share results prefer the share's toDisplayName.

src/types/collaboration.ts gains assigneeDisplayName,
createdByDisplayName and completedByDisplayName (tasks),
createdByDisplayName (attachments, kept by the normalizer),
fromDisplayName/toDisplayName (shares) and requesterDisplayName (access
requests). The vendored extensions/collaboration.json is unchanged until
the service publishes them. The MSW fixtures carry the names, and the
mock task service records who completed a task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:15:54 -05:00
james.maesandClaude Opus 5.5 ed0f056c85 test(collaboration): type the self-exclusion people fixture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:15:04 -05:00
james.maesandClaude Opus 5.5 acfd28eea1 fix(collaboration): leave yourself out of share recipients and keep the dialog on screen
- The share picker drops the signed-in user from the people results,
  matching the app's user id (the session email) against each entry's
  user id and email, case-insensitively. The service is leaving the
  caller out too; this guards older services. The assign picker still
  lists you, since you may assign a record to yourself.
- Collaboration dialogs are a flex column no taller than the viewport
  less 2rem: the title bar stays put and only the body scrolls, and only
  when it does not fit, so there is no second page scroll.
- The link field shows the URL exactly as the service returns it.
- The MSW directory carries the mock session's own entry so the
  exclusion is exercised; unit and e2e tests cover it, and the e2e
  screenshot run checks the dialog fits a 1000 px viewport.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:12:26 -05:00
james.maesandClaude Opus 5.5 365f57c21a fix(collaboration): agree counts with their nouns
"1 people viewing", "1 unread mentions" and "1 followers" now read
"1 person viewing", "1 unread mention" and "1 follower". A small
pluralize(count, singular, plural?) helper in string-utils (there was
none) formats each count; a paged follower count still reads "N+
followers".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:10:41 -05:00
james.maesandClaude Opus 5.5 b0e30cfe36 fix(collaboration): compact tag chips on touch screens and keep copy-id by the title
- On touch screens the global 44 px touch-target rule lifted each tag's
  remove button (and Add tag) to 44 x 44, so chips grew into ~50 px pills,
  two per row. The chip controls now keep their drawn size and carry a
  transparent overlay of at least 44 x 44 px as the touch target, so chips
  stay compact inline chips that wrap naturally at every width.
- The record collaboration header (presence, owner/assignee, Assign and
  Share) was mounted between the follow bell and the copy-record-id
  button; when it wrapped it left the copy button alone on its own row.
  The header now comes after the copy button and inline actions and takes
  its own full-width row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:09:53 -05:00
james.maesandClaude Opus 5.5 9aacbf0052 fix(records): one heading for a widget section titled like its card
A record section that shows a card widget drew the section label as a
heading and then the card's own title, so "Live Progress", "Chunks" and
other child lists read twice. When the card's label equals the section
label, the card's title stands alone and the section keeps the label as
its accessible name. Non-card widgets and differing labels are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:08:34 -05:00
james.maesandClaude Opus 5.5 3e140cb6c6 test(collaboration): expect the "@Name" mention chip in the record panel e2e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:07:27 -05:00
james.maesandClaude Opus 5.5 d7266d2951 fix(collaboration): render a mention as one "@Name" chip
A mention token `@[Name](user:id)` rendered the literal "@" outside the
chip, followed by a gap, and the chip could wrap across two lines. The
comment renderer now drops the token's "@" and the chip draws "@Name"
itself, with no wrapping inside the chip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:07:21 -05:00
james.maesandClaude Opus 5.5 b2a9e28ce0 fix(collaboration): separate Show and Reopen and keep disabled Post readable
- A collapsed resolved thread now reads "Resolved thread", then two
  separate actions, Show and Reopen, instead of "ShowReopen" run together.
  Resolve and Reopen sit in the thread's action row with Edit, Delete
  and Reply.
- The thread actions use the primary text token, which is lightened on
  dark surfaces (about 9.6:1 on the dark card, 5.2:1 on the light card).
- A disabled Post comment button uses the muted surface and muted
  foreground tokens with a border, instead of a 50% faded blue fill that
  was hard to read in dark mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:06:43 -05:00
james.maesandClaude Opus 5.5 c6f0fc87f6 fix(widgets): keep the hidden chart data table from widening the page
The screen-reader table of chart values had sr-only on the table itself.
A table cannot shrink to the 1px sr-only sets, so on chart pages it widened
main-content (915px at a 390px viewport), a horizontal scroll at phone widths.
Wrap it in an sr-only div instead; the table stays in the accessibility tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:37 -05:00
james.maesandClaude Opus 5.5 f1bff85157 fix(help): no RSC prefetch on dashboard create links; NAV-017 includes the Help Lab fixture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:51:57 -05:00
james.maesandClaude Opus 5.5 33f67fa8a8 feat(help): help everywhere — shared help icons, long-help panel, help search, author links
- HelpIcon (summary tooltip, keyboard, More help) and one lazy HelpPanel
- Help on app, table, section, field labels, list columns, widgets,
  processes, steps, reports and record/table actions
- Help search in the command palette and search page (GET /help/search,
  gated on the 'help' feature)
- Author mode: Add help / Edit help links for helpContent authors
- Vendored UI API 1.2.0 with typed HelpContentV1; acceptance HLP-001..006

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:40:14 -05:00
james.maes 69aef5f05c Merge remote-tracking branch 'origin/main' into feature/collaboration-presence-share-tasks 2026-10-13 22:40:11 -05:00
james.maes d8bc9282db Merge branch 'feature/data-analysis-compare' into feature/data-analysis-saved 2026-10-13 22:31:44 -05:00
james.maes 67cf8ed57e Merge remote-tracking branch 'origin/feature/data-analysis-compare' into feature/data-analysis-saved 2026-10-13 22:30:11 -05:00
james.maesandClaude Opus 5.5 7c755d5741 test(acceptance): contingent specs accept trailing-slash record URLs and use field labels in the filter builder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:26:11 -05:00
james.maesandClaude Opus 5.5 38204cdacc feat(processes): run in background, background-job banner, widget refresh
Generic support for QQQ durable jobs (QRun-IO/qqq#1178, #1181); names no
qbit.

- Run in background: on a non-final step of a process whose metadata says
  durableJobEligible, a secondary button POSTs /processes/{name}/jobs with
  the values collected so far, the launch selection (filter or record ids)
  and startAfterStep, then opens the job record. Server errors (400, 403,
  404, 429) are shown.
- When a step's values carry durableJobId (a run over the server's
  threshold), the process shows "Running as a background job" with an Open
  job link, plus its summary lines.
- Widgets whose metadata sets defaultValues.refreshSeconds re-fetch on that
  interval (minimum 2s) while the page is visible, keeping the old data on
  screen; this is how the Jobs app home shows live progress.
- Types for durableJobEligible / durableJobAlways; the processJob client.
- Re-vendored the UI API 1.2.0 document with the jobs and uploads
  operations.

Bundle (report-only budget): total JS +1.8 KB gzip (1217.5 -> 1219.3 KB),
/app/_/_ first-load +0.9 KB (350.4 -> 351.3 KB), 126 chunks unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:24:59 -05:00
james.maesandClaude Opus 5.5 622ae28ceb fix(collaboration): keep record header controls together and readable at phone width
- Presence avatars, owner/assignee and the Assign/Share buttons are now
  three wrapping groups, so Share no longer drops onto its own line beside
  free space. Owner and Assignee render as muted label plus value.
- The avatar row is omitted when nobody else is viewing.
- The people search shows a "Search by name" placeholder.
- My work uses the page gutter instead of doubling it at 390 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:19:10 -05:00
james.maesandClaude Opus 5.5 9d2ece4228 test(secured-data): open the filter builder like the other specs; wait for the record before the banner shot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:09:14 -05:00
james.maesandClaude Opus 5.5 1bb7db1924 fix(tasks): show a task's done/open toggle immediately
The checkbox was bound to the server status, so a click snapped back
until the refetch landed: users saw no response and the Playwright check
failed on every browser. The toggled state now shows at once, the box is
disabled while the write is in flight, a failed write rolls it back, and
the entry clears when the server status agrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:09:03 -05:00
james.maesandClaude Opus 5.5 cce505e5a5 fix(data-analysis): readable correlation heatmap in dark mode, repair text encoding
The heatmap uses translucent tints over the theme background with the
theme's text colour (AA checked over both themes) and aligned row
labels; restore the en and em dashes an earlier edit had mis-encoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:03:38 -05:00
james.maesandClaude Opus 5.5 2625355d88 fix(palette): list "Go to My work" after favorites
Favorites lead the command palette; the Work group came first and broke
the ordering test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:56:32 -05:00
james.maesandClaude Opus 5.5 6fa2ccf301 fix(realtime): hand over leadership on pagehide and never revive an ended session
- A closing or bfcache-frozen leader tab posts "bye" and drops its stream
  on pagehide, so a follower takes over at once (React cleanups never run
  on tab close; before, the handover waited for the 6.5 s lease expiry).
- An ended client stops leasing: a still-mounted surface could otherwise
  re-announce channels and make live tabs defer to a leader that never
  connects.
- A closed client drops its BroadcastChannel so late releases cannot
  post to a closed channel.
- realtimeClient() replaces an ended singleton, so signing in again as the
  same user restores realtime; the cache bridge is per client.
- The serialization test asserted against a transport mock that lost its
  state; it now checks the remove follows the in-flight add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:56:31 -05:00
james.maesandClaude Opus 5.5 ac5aa368d7 fix(collaboration): load the edit, presence and header layers only for tables that enable them
With the qbit installed, every edit form and record screen loaded the
collaboration edit layer and subscribed a record channel, whatever the
table's features. The edit layer then threw outside a QContextProvider,
which broke 26 EntityForm and RecordViewHeader tests.

- The edit layer loads for PRESENCE or FOLLOW tables, or once a save is
  rejected with 409; the header for realtime, sharing, assignment or
  ownership; table presence for realtime tables only.
- Record and table channels subscribe only where core publishes events.
- Realtime and presence hooks read the user id without requiring the
  provider.
- "Keep mine" closes the review before re-sending; a newer stream hint
  never replaces a pending 409 and its server version.
- The theirs/yours comparison stacks on phones.
- Test: a 409 sends nothing else and keeps the typed value; "Keep mine"
  re-sends the dirty values with the new expectedVersion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:56:30 -05:00
james.maesandClaude Opus 5.5 3509ff8452 feat(data-analysis): save, share, star, export and watch analyses
Every analysis view can be saved (optionally shared) as a
data_analysis_saved record, opened again with ?view=saved (re-run as the
viewer, with a star toggle), exported as CSV, XLSX or chart PNG, and
handed to SQL Workbench as a draft when it is installed. Profiles offer
Watch this metric, which creates a data_analysis_watch and previews its
series against the detector's baseline. Charts clip their tooltips so
nothing widens the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:56:20 -05:00
james.maesandClaude Opus 5.5 46436866be feat(secured-data): masked values, reveal, restricted filters and the break-glass banner
Renders fields the server marks MASKED or REVEALABLE as masked values with a
lock chip; offers Reveal (reason dialog, countdown, re-mask on expiry) when
the secured-data extension is present; filter builders, quick filters and
column menus offer only the allowed operators; restricted fields are
read-only and never sent in insert/update payloads; a break-glass banner
shows an active session with a countdown. Reveal dialog and banner are
lazy-loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:55:14 -05:00
james.maesandClaude Opus 5.5 1a7b8e371f wip(collaboration): realtime, presence, conflicts, sharing, tasks and My work (Codex draft, tests pending)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:46:44 -05:00
james.maesandClaude Opus 5.5 d011c546c0 test(acceptance): follow the sample rename to placeAddress / placeAddressPick
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:28:21 -05:00
james.maesandClaude Opus 5.5 236fa03923 feat(forms): contingent possible values across forms, bulk edit, processes and filters
- Contingent fields (possibleValueSourceDependsOn, or ${input.x} in a
  legacy filter) re-query only when their dependencies change, are
  disabled with "Choose <A> first" until set (DISABLE_UNTIL_SET), and
  clear with an inline notice when A changes and the value no longer
  fits; clears cascade A -> B -> C. Loading a saved record never clears.
- Server "not a valid choice" errors map to the dependent field.
- Bulk edit: B requires A enabled and set; process forms use the
  screen's metadata and values; quick-create gets sibling metadata.
- Filter builder: sends useCase filter (contingent fields showed no
  options before) and narrows B from A's EQUALS/IN criteria.
- Re-vendored UI API 1.2.0 document.
- Unit tests and real-backend acceptance specs (addressBook sample).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:40:14 -05:00
dependabot[bot] 68eb31d18d build(deps): Bump rapidoc from 9.3.8 to 10.1.0
Bumps [rapidoc](https://github.com/rapi-doc/RapiDoc) from 9.3.8 to 10.1.0.
- [Release notes](https://github.com/rapi-doc/RapiDoc/releases)
- [Commits](https://github.com/rapi-doc/RapiDoc/compare/v9.3.8...v10.1.0)

---
updated-dependencies:
- dependency-name: rapidoc
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-14 01:32:26 +00:00
dependabot[bot] 7a75f5762c build(deps): Bump the github-actions group across 1 directory with 4 updates
Bumps the github-actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-node](https://github.com/actions/setup-node), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/checkout` from 6.1.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.1.0...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

Updates `actions/download-artifact` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-14 01:16:56 +00:00
744 changed files with 109212 additions and 16842 deletions

No files matched your search

+10 -76
View File
@@ -1,80 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6
# ─── Jobs ────────────────────────────────────────────────────────────────────
# The dashboard jar (static export under next-dashboard/) is published to Maven
# Central on release tags, with the same orb commands and credentials as the
# Material Dashboard. QApplicationJavalinServer serves it at / when it is present.
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job; avoid the interactive needrestart prompt.
sudo sed -i 's|http://security.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
jobs:
publish_dashboard_jar:
executor: qqq-orb/java
steps:
- configure-package-install
- checkout
- qqq-orb/install_java:
install_node: true
node_version: "22"
- run:
name: Verify the immutable release version
command: |
release_version="${CIRCLE_TAG#v}"
frontend_version=$(node -p "require('./package.json').version")
maven_version=$(mvn -B -q help:evaluate -Dexpression=project.version -DforceStdout)
test "$frontend_version" = "$release_version"
test "$maven_version" = "$release_version"
- run:
name: Build the static export
command: |
corepack enable
pnpm install --frozen-lockfile
pnpm typecheck
pnpm lint
pnpm test
pnpm build:export
- qqq-orb/mvn_jar_deploy
# ─── Workflows ───────────────────────────────────────────────────────────────
# Trunk-based: main is the only long-lived branch (see CONTRIBUTING.md).
# test_only → every branch except main (the PR check ci/circleci: qqq-orb/node_app_test_only)
# publish_release → main builds; v* version tags publish the dashboard jar
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
test_only:
setup:
jobs:
- qqq-orb/node_app_test_only:
filters:
branches:
ignore: main
tags:
only: []
publish_release:
jobs:
- qqq-orb/node_app_build:
filters:
branches:
only: [ main ]
tags:
only: [ /v.*/ ]
- publish_dashboard_jar:
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /.*/
tags:
only: /^v[0-9]+\.[0-9]+\.[0-9]+(-RC\.[0-9]+)?$/
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: /.*/
tags:
only: /.*/
+2
View File
@@ -6,3 +6,5 @@ playwright-report
coverage
.env*
*.log
.pnpm-store
+1
View File
@@ -0,0 +1 @@
dynamic-components/** -text
+13
View File
@@ -0,0 +1,13 @@
# Copyright 2026 QRun.IO, Inc.
# Licensed under the Apache License, Version 2.0.
# https://www.apache.org/licenses/LICENSE-2.0
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
with:
node-version: '22.20.0'
prebuild: corepack enable && pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm test && pnpm build:export
-281
View File
@@ -1,281 +0,0 @@
name: Real-backend acceptance
# One production build feeds independent, serial-worker backends. Require "acceptance gate".
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
inputs:
qqq_ref:
description: QRun-IO/qqq branch or tag to build the sample backend from
required: false
permissions:
contents: read
concurrency:
group: acceptance-${{ github.ref }}
cancel-in-progress: true
env:
QQQ_REF: ${{ inputs.qqq_ref || vars.QQQ_ACCEPTANCE_REF || 'develop' }}
NEXT_TELEMETRY_DISABLED: '1'
MAVEN_ARGS: -Dmaven.wagon.http.retryHandler.count=5 -Daether.transport.http.retryHandler.count=5 -Daether.connector.http.retryHandler.count=5
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
mode: ${{ steps.select.outputs.mode }}
matrix: ${{ steps.select.outputs.matrix }}
specs: ${{ steps.select.outputs.specs }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Select projects, shards and changed areas
id: select
run: node scripts/acceptance-select.mjs
build:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: QRun-IO/qqq
ref: ${{ env.QQQ_REF }}
path: qqq-backend
- name: Resolve QQQ source revision
id: qqq
run: echo "sha=$(git -C qqq-backend rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '21'
cache: maven
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: qqq-cache
with:
path: ~/.m2/repository/com/kingsrook
key: qqq-source-${{ runner.os }}-java21-v1-${{ steps.qqq.outputs.sha }}
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Bundled UI API document matches the server's (src/lib/api/openapi/qqq-v1.json)
run: node scripts/check-ui-api-spec.mjs --server-spec qqq-backend/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml
- name: Build QQQ from source
if: steps.qqq-cache.outputs.cache-hit != 'true'
run: bash scripts/ci/retry.sh 4 mvn -B -q -f qqq-backend/pom.xml install -DskipTests -Dspotbugs.skip=true -Dpmd.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true
- name: Build and install the dashboard jar (static export)
run: |
# Equivalent to build:jar, with retries around its Maven invocation too.
pnpm build:export
bash scripts/ci/retry.sh 4 mvn -B -q clean package
bash scripts/ci/retry.sh 4 mvn -B -q install
# Failed-attempt Maven stdout must not become part of the version value.
bash scripts/ci/retry.sh 4 mvn -B -q help:evaluate -Dexpression=project.version -Doutput="$RUNNER_TEMP/acceptance-next-version"
next_version=$(cat "$RUNNER_TEMP/acceptance-next-version")
echo "NEXT_JAR_VERSION=$next_version" >> "$GITHUB_ENV"
- name: Build the sample application jar
working-directory: qqq-backend
run: |
# QQQ checkstyle/pmd paths are relative to this checkout.
bash "$GITHUB_WORKSPACE/scripts/ci/retry.sh" 4 mvn -B -q -f qqq-sample-project/pom.xml package -DskipTests -Djacoco.skip=true \
-Dqqq.frontend.next.version="$NEXT_JAR_VERSION"
- name: Stage runtime artifacts
run: |
sample_jars=(qqq-backend/qqq-sample-project/target/*-jar-with-dependencies.jar)
test "${#sample_jars[@]}" -eq 1
sample_version="${sample_jars[0]##*/}"
sample_version="${sample_version#qqq-sample-project-}"
sample_version="${sample_version%-jar-with-dependencies.jar}"
mkdir -p test-results/acceptance-jars
cp "${sample_jars[0]}" test-results/acceptance-jars/
# javac and the security fixture need the API JAR in addition to the fat sample JAR.
cp "$HOME/.m2/repository/com/kingsrook/qqq/qqq-middleware-api/$sample_version/qqq-middleware-api-$sample_version.jar" \
test-results/acceptance-jars/qqq-middleware-api.jar
tar -cf test-results/acceptance-build.tar out test-results/acceptance-jars
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: acceptance-build
path: test-results/acceptance-build.tar
if-no-files-found: error
retention-days: 30
shards:
name: acceptance ${{ matrix.project }} ${{ matrix.shard }}/${{ matrix.total }}
needs: [plan, build]
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '21'
cache: maven
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Installed Playwright version
id: playwright
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: browser-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-${{ matrix.browser }}
- run: pnpm exec playwright install-deps ${{ matrix.browser }}
- if: steps.browser-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install ${{ matrix.browser }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: acceptance-build
path: test-results
- name: Restore runtime artifacts
run: |
tar -xf test-results/acceptance-build.tar
echo "QQQ_SAMPLE_JAR=$(ls "$PWD"/test-results/acceptance-jars/*-jar-with-dependencies.jar)" >> "$GITHUB_ENV"
echo "QQQ_MIDDLEWARE_API_JAR=$PWD/test-results/acceptance-jars/qqq-middleware-api.jar" >> "$GITHUB_ENV"
- name: Run shard
env:
QQQ_ACCEPTANCE_BROWSERS: ${{ matrix.project }}
QQQ_ACCEPTANCE_SPEC_PATHS: ${{ needs.plan.outputs.specs }}
QQQ_ACCEPTANCE_BLOB: '1'
# Environment-selected projects must not produce colliding default blob names.
QQQ_ACCEPTANCE_BLOB_NAME: report-${{ matrix.project }}-${{ matrix.shard }}.zip
QQQ_ACCEPTANCE_TRANSPORT_TRACE: '1'
QQQ_ACCEPTANCE_BROWSER_TRACE: ${{ (matrix.project == 'webkit' || matrix.project == 'mobile' || matrix.project == 'tablet') && 'navigation' || '0' }}
DEBUG: pw:browser
run: node scripts/acceptance.mjs --skip-build --shard=${{ matrix.shard }}/${{ matrix.total }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: acceptance-blob-${{ matrix.project }}-${{ matrix.shard }}
path: test-results/acceptance/blob-report/
if-no-files-found: error
retention-days: 30
gate:
name: acceptance gate
if: always()
needs: [plan, build, shards]
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: acceptance-blob-*
path: test-results/acceptance/blobs
merge-multiple: true
- name: Merge shard reports
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: test-results/acceptance/report.json
PLAYWRIGHT_HTML_OUTPUT_DIR: test-results/acceptance/html
PLAYWRIGHT_HTML_OPEN: never
run: npx playwright merge-reports --reporter=json,html test-results/acceptance/blobs
- name: Evaluate acceptance matrix
if: always()
env:
ACCEPTANCE_MODE: ${{ needs.plan.outputs.mode }}
run: |
args=()
if [[ "$ACCEPTANCE_MODE" == pr ]]; then args+=(--partial); fi
node scripts/acceptance-gate.mjs "${args[@]}"
- name: Require every planned shard and successful build
if: always()
env:
PLAN_RESULT: ${{ needs.plan.result }}
BUILD_RESULT: ${{ needs.build.result }}
SHARDS_RESULT: ${{ needs.shards.result }}
PLANNED_MATRIX: ${{ needs.plan.outputs.matrix }}
run: |
test "$PLAN_RESULT" = success
test "$BUILD_RESULT" = success
test "$SHARDS_RESULT" = success
node --input-type=module -e 'import { existsSync } from "node:fs"; for (const { project, shard } of JSON.parse(process.env.PLANNED_MATRIX).include) { const file = "test-results/acceptance/blobs/report-" + project + "-" + shard + ".zip"; if (!existsSync(file)) throw new Error("Missing shard report: " + file); }'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: acceptance
path: test-results/acceptance/
if-no-files-found: error
retention-days: 30
quarantine:
name: acceptance quarantine (chromium)
needs: [plan, build]
if: needs.plan.outputs.mode == 'full'
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '21'
cache: maven
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Installed Playwright version
id: playwright
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: browser-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-chromium
- run: pnpm exec playwright install-deps chromium
- if: steps.browser-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: acceptance-build
path: test-results
- name: Restore runtime artifacts
run: |
tar -xf test-results/acceptance-build.tar
echo "QQQ_SAMPLE_JAR=$(ls "$PWD"/test-results/acceptance-jars/*-jar-with-dependencies.jar)" >> "$GITHUB_ENV"
echo "QQQ_MIDDLEWARE_API_JAR=$PWD/test-results/acceptance-jars/qqq-middleware-api.jar" >> "$GITHUB_ENV"
- name: Run quarantined scenarios
env:
QQQ_ACCEPTANCE_BROWSERS: chromium
QQQ_ACCEPTANCE_QUARANTINE: only
QQQ_ACCEPTANCE_TRANSPORT_TRACE: '1'
QQQ_ACCEPTANCE_BROWSER_TRACE: '0'
DEBUG: pw:browser
run: node scripts/acceptance.mjs --skip-build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: acceptance-quarantine
path: test-results/acceptance/
retention-days: 30
+36 -153
View File
@@ -1,158 +1,41 @@
name: CI
# Fast PR checks (QRun-IO/qqq#709, bundle budget #710). The real-backend gate, including the
# runtime performance budgets (specs/performance), runs in acceptance.yml.
# Each job installs on its own so they run in parallel; none needs a backend.
on:
name: Next centralized CI
'on':
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
- unlabeled
push:
branches: [main]
branches:
- main
tags:
- v*
workflow_dispatch:
inputs:
qqq_ref:
description: QQQ source ref for acceptance
type: string
required: false
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
NEXT_TELEMETRY_DISABLED: '1'
STORYBOOK_DISABLE_TELEMETRY: '1'
jobs:
unit:
name: Typecheck, lint, unit tests and coverage
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm lint
- name: Unit tests with src/lib coverage thresholds (vitest.config.ts)
run: pnpm test:coverage
e2e:
name: Mocked e2e (Playwright, chromium)
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- name: Installed Playwright version
id: playwright
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: browser-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-chromium
- run: pnpm exec playwright install-deps chromium
- if: steps.browser-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium
- name: Production build
run: pnpm build
- name: Mocked e2e suite
env:
QQQ_E2E_SERVER: production
run: pnpm test:e2e
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: e2e-report
path: |
playwright-report/
test-results/
retention-days: 14
collaboration-e2e:
name: Collaboration e2e (MSW; chromium, firefox, webkit, mobile, tablet)
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- name: Installed Playwright version
id: playwright
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: browser-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-all
- run: pnpm exec playwright install-deps chromium firefox webkit
- if: steps.browser-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium firefox webkit
- name: Collaboration suite (mock API, every browser project)
run: pnpm exec playwright test --config playwright.collaboration.config.ts
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: collaboration-e2e
path: |
test-results/collaboration/
test-results/collab-shots/
retention-days: 14
storybook:
name: Storybook build
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- run: pnpm build-storybook --quiet
performance:
name: Bundle budget (static export)
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- name: Static export
run: pnpm build:export
- name: First-load JS per route, total JS and largest chunk against perf-budget.json
run: pnpm perf:budget
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: bundle-budget
path: test-results/performance/bundle.json
retention-days: 30
ci:
if: (github.event_name == 'pull_request' && !contains(fromJSON('["labeled","unlabeled"]'),
github.event.action)) || (github.event_name == 'push' && github.ref == 'refs/heads/main')
uses: QRun-IO/qqq-orb/.github/workflows/next-ci.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
acceptance:
if: github.event_name != 'pull_request' || !contains(fromJSON('["labeled","unlabeled"]'),
github.event.action) || github.event.label.name == 'full-acceptance'
uses: QRun-IO/qqq-orb/.github/workflows/next-acceptance.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
with:
qqq_ref: ${{ inputs.qqq_ref || '' }}
dashboard-image:
if: (github.event_name == 'pull_request' && !contains(fromJSON('["labeled","unlabeled"]'),
github.event.action)) || (github.event_name == 'push' && startsWith(github.ref,
'refs/tags/v'))
permissions:
contents: read
packages: write
uses: QRun-IO/qqq-orb/.github/workflows/next-dashboard-image.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
-75
View File
@@ -1,75 +0,0 @@
name: Dashboard image
on:
pull_request:
push:
tags: ['v*']
permissions:
contents: read
env:
IMAGE: ghcr.io/qrun-io/qqq-frontend-next
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
if: github.event_name == 'push'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
with:
context: .
file: docker/quickstart/Dockerfile
platforms: linux/${{ matrix.arch }}
push: ${{ github.event_name == 'push' }}
load: ${{ github.event_name == 'pull_request' }}
tags: ${{ env.IMAGE }}:${{ github.sha }}-${{ matrix.arch }}
cache-from: type=gha,scope=dashboard-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=dashboard-${{ matrix.arch }}
- name: Verify runnable dashboard
if: github.event_name == 'pull_request'
env:
TAG: ${{ env.IMAGE }}:${{ github.sha }}-${{ matrix.arch }}
run: |
container=$(docker run -d --rm -p 127.0.0.1:3000:3000 "$TAG")
trap 'docker rm -f "$container"' EXIT
curl -fsS --max-time 3 --retry 30 --retry-delay 1 --retry-all-errors \
--retry-max-time 90 http://127.0.0.1:3000/app/person >/dev/null \
|| { docker logs "$container"; exit 1; }
publish:
if: github.event_name == 'push'
needs: build
runs-on: ubuntu-24.04
permissions:
packages: write
steps:
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish versioned multi-architecture image
run: |
docker buildx imagetools create \
--tag "$IMAGE:${GITHUB_REF_NAME#v}" \
"$IMAGE:$GITHUB_SHA-amd64" "$IMAGE:$GITHUB_SHA-arm64"
+5
View File
@@ -10,6 +10,8 @@
# next.js
/.next/
/.next-collaboration/
/.next-presence-live/
/out/
# production
@@ -41,3 +43,6 @@ target/
/log/
/.playwright-mcp/
/storybook-static/
/public/scripting-generated/
/public/pdf-generated/
/public/dynamic-qfmd-components/
+5
View File
@@ -0,0 +1,5 @@
# Reviewed findings committed before the Gitea migration; exact historical fingerprints only.
0969231c95f876bb0a41529e8c6c4621a0257aee:src/lib/qa-capture/sanitizers.test.ts:jwt:33
23a855297de0f84b564b20d782e01180a68b5257:tests/acceptance/specs/security/restrictions.spec.ts:generic-api-key:248
+3
View File
@@ -0,0 +1,3 @@
profile: frontend-next
orb_version: '0.8'
default_branch: main
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
## This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+50 -3
View File
@@ -1,8 +1,11 @@
# QQQ Frontend Next
> **QBit integration branch (2026-11-04).** File delivery/preview, TypeScript editing and generic process controls use the QQQ UIUX APIs. See [current verification and dependency setup](docs/QBIT-RELEASE-STATUS.md) and [remaining release requirements](docs/QBIT-KNOWN-ISSUES.md). These changes are source/local snapshots, not part of the published RC.36 baseline described below.
A metadata-driven admin UI for the QQQ low-code application framework, rewritten from React + Material UI to Next.js 16 + Tailwind CSS + shadcn/ui. The UI renders entirely from backend metadata — no table names, field lists, or navigation items are hardcoded in the frontend.
**This checkout targets Next UI 1.0.0-RC.19**, which adds the related-record picker (single, multi-child and link modes) and the assistant Read-only / Write switch to RC18. Confirm availability in the [RC19 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.19) before installing; see the [RC19 notes](docs/releases/1.0.0-RC.19.md) for the preparation checkpoint and verification scope. [RC18](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.18) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
**Published baseline: Next UI 1.0.0-RC.36**, which adds accessibility fixes: readable link text, 24px clear targets, h2 widget titles and skip-link focus to RC35. Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing; see the [RC36 notes](docs/releases/1.0.0-RC.36.md) for the preparation checkpoint and verification scope. [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
**Current roadmap and known issues:** [Next UI 1.0 tracker](https://github.com/QRun-IO/qqq/issues/713). GitHub issues are the source of truth for status and remaining work; see [issue tracking](docs/ISSUE-TRACKING.md).
@@ -14,11 +17,11 @@ The dashboard is the default admin UI for QQQ 4.1+. Add the jar (managed by `qqq
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.21</version>
<version>1.0.0-RC.36</version>
</dependency>
```
The explicit version overrides an older BOM-managed frontend. RC21 requires a published QQQ 4.1 candidate: 4.1.0-RC.1 or later; 4.1.0-RC.2 or later for standalone branding images (QQQ #1009); and **4.1.0-RC.3 or later** for FILE_PREVIEW thumbnails (QQQ #1056) and the instance home app (QQQ #1059); **4.1.0-RC.4 or later** for record developer panels (QQQ #1076); **4.1.0-RC.5 or later** for the record picker (QQQ #1099), form validation limits (QQQ #1100) and date filter presets (QQQ #1115). The Alerts bell appears only with qbit-alerts (0.1.0-RC.1 or later); the notification center needs qbit-alerts 0.1.0-RC.2 or later. The assistant panel appears only with the qbit-assistant backend (0.1.0-RC.3 or later). The SQL Workbench needs the qbit-sql-workbench QBit (1.0.0-RC.1 or later); see the release notes for the verified backend. Select Next explicitly with `-Dqqq.javalin.frontend=next`. This does not release QQQ 4.1.
The explicit version overrides an older BOM-managed frontend. RC36 requires a published QQQ 4.1 candidate: 4.1.0-RC.1 or later; 4.1.0-RC.2 or later for standalone branding images (QQQ #1009); and **4.1.0-RC.3 or later** for FILE_PREVIEW thumbnails (QQQ #1056) and the instance home app (QQQ #1059); **4.1.0-RC.4 or later** for record developer panels (QQQ #1076); **4.1.0-RC.5 or later** for the record picker (QQQ #1099), form validation limits (QQQ #1100) and date filter presets (QQQ #1115). The Alerts bell appears only with qbit-alerts (0.1.0-RC.1 or later); the notification center needs qbit-alerts 0.1.0-RC.2 or later. The assistant panel appears only with the qbit-assistant backend (0.1.0-RC.3 or later). The SQL Workbench needs the qbit-sql-workbench QBit (1.0.0-RC.1 or later); QA capture appears only with qbit-qa-capture (the `qa-capture` UI API extension); see the release notes for the verified backend. Select Next explicitly with `-Dqqq.javalin.frontend=next`. This does not release QQQ 4.1.
The jar holds the static export (`pnpm build:export`) under `next-dashboard/`. Deep links such as `/app/person/1` are served from placeholder pages and read their route from the browser path, so no Node.js server is involved. The Material Dashboard remains available: select it with `withServeFrontendMaterialDashboard(true)` or `-Dqqq.javalin.frontend=material`. Material routes are `/<app>/<table>/<id>`; Next routes are `/app/<table>/<id>`.
@@ -63,6 +66,18 @@ To upgrade between RCs, change only the `qqq-frontend-next` version and check th
Served from the jar there is nothing to configure in the frontend: it calls the API on the same origin at `/qqq/v1`, and the Javalin server sends the security headers and Content-Security-Policy. External embeds (for example QuickSight frames) need an allowed CSP source, not a disabled policy. The build-time variables under [Environment Variables](#environment-variables) apply only to frontend development and the standalone container image.
### App home navigation grids
App homes show automatic section ("Actions", "Reports", "Data") and child-app ("Apps")
card grids. When the app declares one or more widgets, the "Apps" grid is left out (even if
a declared widget is unavailable to the current user), so widget-based navigation is not
duplicated; the section grids stay.
Set `supplementalAppMetaData.nextUi.showChildGrid` on the app to override this default:
`true` shows both grids alongside widgets; `false` hides both grids, for a home whose
widgets already cover its sections and child apps.
Only boolean values override the default. Existing visibility and permission rules still apply.
### Known limitations
- Release candidates only; final 1.0 is held for application soak and owner approval ([#712](https://github.com/QRun-IO/qqq/issues/712), tracker [#713](https://github.com/QRun-IO/qqq/issues/713)).
@@ -178,3 +193,35 @@ src/
- **Charts:** Recharts
- **Testing:** Vitest (unit), Playwright (E2E), axe-core (a11y)
- **Package Manager:** pnpm
## Default query columns
A new query view shows at most six fields: the visible primary key first, then
eligible fields from T1 sections followed by T2 sections, preserving section and
field order within each tier. Tables without sections use metadata field order.
Hidden fields, heavy fields, TEXT/HTML/BLOB/JSON-like types, and CODE_EDITOR or
RENDER_HTML adornments are excluded from the automatic selection (the primary key
is retained unless hidden). Fields outside T1/T2 are initially unchecked.
Other available columns remain selectable in the column picker.
Applications can override this selection with a string-array hint:
```json
{
"supplementalMetaData": {
"nextUi": {
"defaultColumns": ["id", "title", "status"]
}
}
}
```
The legacy `supplementalTableMetaData.nextUi.defaultColumns` path is also accepted;
the modern `supplementalMetaData.nextUi` object takes precedence. A valid hint
selects exactly the named available columns, without the six-field cap or automatic
primary-key inclusion. Unknown or unavailable fields are ignored; an empty array
selects no columns, and malformed hints fall back to the automatic selection.
Saved views and legacy stored visibility (including `{}`, meaning all base columns)
are preserved. New View and Reset All Changes on an unsaved view restore these
metadata defaults. Quick search searches only visible string fields. Responsive
card/grid behavior is unchanged.
+6 -2
View File
@@ -1,4 +1,4 @@
FROM registry-1.docker.io/library/node:22-bookworm-slim AS frontend
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS frontend
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
@@ -9,8 +9,12 @@ ENV QQQ_BACKEND_URL=http://host.docker.internal:8000
ENV NEXT_PUBLIC_MOCK_API=false
RUN pnpm build
FROM registry-1.docker.io/library/node:22-bookworm-slim AS runtime
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS runtime
LABEL org.opencontainers.image.source="https://github.com/QRun-IO/qqq-frontend-next"
# Standalone production starts node directly; package managers are build-only.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn-* \
&& rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
/usr/local/bin/pnpm /usr/local/bin/pnpx /usr/local/bin/yarn /usr/local/bin/yarnpkg
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000
COPY --from=frontend --chown=node:node /app/.next/standalone ./
+37 -49
View File
@@ -23,10 +23,12 @@ Axios interceptor
▼
TanStack Query cache (onError)
│ 401 → silently ignored (already handled)
│ 403 → toast "Permission denied" / "You do not have permission…"
│ 403 → toast the server's message, or "Permission denied"
│ 404 → toast "Resource not found"
│ 5xx → toast "Server error — retrying…" / "Server error — please try again"
│ network → toast "Something went wrong"
│ other status → toast the server's message, or "Request failed (HTTP 409)"
│ no response → toast "No response from the server" / "The server took too long to respond"
│ every toast names the request, and a repeat of the same failure updates it
▼
Component (optional per-query onError)
│ Only needed when generic toast is wrong for the specific UX
@@ -66,52 +68,38 @@ The `unauthorizedCallback` is wired to the auth provider's logout/redirect funct
**Location:** `src/lib/query-client.ts` — `handleQueryError` function
```typescript
function handleQueryError(error: unknown, context: 'query' | 'mutation'): void {
const status = getErrorStatusCode(error)
It is registered on both `QueryCache.onError` and `MutationCache.onError`. A query or mutation whose
component renders its own error opts out with `meta: HANDLES_OWN_ERRORS`.
if (status === 401) return // handled by axios interceptor
| Failure | Banner text |
|---|---|
| Cancelled request (an axios cancel or an aborted `fetch`), 401 | none |
| 403 | the server's message (of a JSON body), or `Permission denied` |
| 404 | `Resource not found` |
| 5xx | `Server error — retrying…` (query), `Server error — please try again` (mutation) |
| Any other status (400, 409, 422, …) | the server's message (`userFacingError`, `error` or `message` of a JSON body), or `Request failed (HTTP 409)` |
| No response (connection failed) | `No response from the server` |
| No response (the 30 s timeout) | `The server took too long to respond` |
| An error the application raised (`throw new Error('Invalid audit response')`) | that sentence |
| A runtime error (`TypeError`, a schema error, …) | `The page hit an unexpected error (TypeError)` |
if (status === 403) {
toast.error(
context === 'mutation'
? 'You do not have permission to perform this action'
: 'Permission denied'
)
return
}
Under the text, every banner names what failed: the request as `POST /qqq/v1/table/order/query (HTTP 409)`,
or `While loading records` (the query key's namespace in plain words: `possibleValues` reads
`possible values`) for a query that failed outside a request. The
request is written without its query string, and every path segment that is not a plain word (a record
id, a UUID, an encoded value) is shown as `{id}`. Nothing else of the request or response is shown.
if (status === 404) {
toast.error('Resource not found')
return
}
**One banner per failure.** The banner's sonner `id` is its text plus the request, so a failure that
repeats (a polling query that fails on every attempt) updates the banner already on screen instead of
adding one. A different failure gets its own banner. A lost connection is one cause whichever request
met it, so it has a single banner, which names the request that failed most recently. Without the id
every failed poll added a banner, and sonner pauses a toast's timer while the tab is hidden or the
pointer rests on the stack, so the banners could pile up.
if (status !== undefined && status >= 500) {
toast.error(
context === 'mutation'
? 'Server error — please try again'
: 'Server error — retrying…'
)
return
}
// Network failure or unexpected status
toast.error('Something went wrong')
}
```
This handler is registered on both `QueryCache` and `MutationCache`:
```typescript
export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => handleQueryError(error, 'query'),
}),
mutationCache: new MutationCache({
onError: (error) => handleQueryError(error, 'mutation'),
}),
})
```
**Console.** Each banner is also logged once with `console.warn('[API] Request failed:', { context,
status, code, name, request, area })`: the HTTP status, the axios error code (`ERR_NETWORK`,
`ECONNABORTED`, `ERR_BAD_REQUEST`), the error's name, the request as above and the query namespace.
It never logs a message, body, header or query string, because those can carry record data.
**Implication for components:** For most queries and mutations, you do not need `onError` at all. The global handler covers toasting. Add per-query handling only when you need to do something the global handler cannot — such as mapping a 400 error's field messages back to form fields.
@@ -139,7 +127,7 @@ Returns `undefined` for anything else (non-object errors, null, network failures
### 400 — Validation Error
The global handler does NOT toast on 400 because 400s typically carry field-level validation messages that must be mapped back to individual form fields. Handle 400 explicitly wherever you submit a form.
The global handler toasts the server's message for a 400 (or `Request failed (HTTP 400)` when it sent none). 400s typically carry field-level validation messages that belong beside individual form fields, so a form handles 400 explicitly, and sets `meta: HANDLES_OWN_ERRORS` on the mutation when the toast would repeat what the form shows.
```typescript
import { getErrorStatusCode } from '@/lib/utils/error-utils'
@@ -207,7 +195,7 @@ No additional per-component handling is needed for 5xx unless you want a custom
### Network Failure (no response)
Treated as non-4xx by the retry logic — queries retry up to 3 times. The global handler toasts "Something went wrong" after all retries are exhausted. No per-component handling needed for the common case.
Treated as non-4xx by the retry logic — queries retry up to 3 times. After all retries are exhausted the global handler toasts "No response from the server" (or "The server took too long to respond" for the 30 s timeout) with the request that failed. No per-component handling needed for the common case.
---
@@ -274,8 +262,8 @@ export function EditOrderForm({ orderId, schema, defaultValues }: Props) {
onError: (error) => {
if (getErrorStatusCode(error) === 400) {
// Map field errors from the server response body to form fields.
// The global handler does NOT toast on 400, so we must do it here or
// rely on inline field error messages in the form UI.
// The global handler toasts the server's message for a 400; the form
// places each field's message beside it.
const body = (error as AxiosError<{ errors: Record<string, string> }>).response?.data
if (body?.errors) {
for (const [field, message] of Object.entries(body.errors)) {
@@ -305,6 +293,6 @@ export function EditOrderForm({ orderId, schema, defaultValues }: Props) {
| Layer | File | Handles |
|---|---|---|
| Axios interceptor | `src/lib/api/client.ts` | 401 → redirect to login |
| Global query handler | `src/lib/query-client.ts` | 403, 404, 5xx, network → toast |
| Global query handler | `src/lib/query-client.ts` | Every status but 401, no response, page errors → one toast per failure, naming the request |
| Per-component `onError` | Your component | 400 field errors, custom empty states, navigation |
| `getErrorStatusCode` | `src/lib/utils/error-utils.ts` | Normalize `AxiosError` and plain-object errors to HTTP status code |
+1
View File
@@ -28,6 +28,7 @@ The ID may be on a layout-neutral wrapper around a native button or menu item. S
- Record view: `.recordView`, `record-sidebar`, `record-view-header-{table}`, `record-view-title-{table}`, `record-view-button-bar-{table}`, `record-view-actions-menu`, and `delete-confirmation-*` with `button-delete-yes`/`button-delete-no`.
- File previews: a `FILE_PREVIEW` value keeps its cell or value ID (`grid-cell-{field}`, `field-value-{field}`) on the thumbnail button with `data-preview-kind`; the box is `{id}-thumbnail`. The dialog uses `file-preview-dialog`, `file-preview-image`/`-pdf`/`-video`/`-audio`/`-unavailable`, `file-preview-details`, and `file-preview-{previous,next,zoom-in,zoom-out,zoom-fit,open,download,close}`.
- Forms: `.entityForm`, `record-create-{table}`/`record-edit-{table}`, `form-section-{section}`, `.field-wrapper.is-visible`, and `.stickyBottomButtonBar`. Default action buttons expose `data-button-variant` values such as `gradient`, `contained`, and `outlined`.
- Jobs: `button-jobs`, `jobs-active-badge`, `jobs-popover`, `link-job-{jobId}`, `link-all-jobs`, `jobs-page`, `tab-jobs-{active,recent,all}`, `job-progress-view`, `job-status`, `job-phases`, `job-progress-bar`, `job-progress-stats`, `job-progress-message`, `job-log-panel`, `job-log-scroll`, `button-job-log-{toggle,copy,latest}`, `job-background-countdown`, `button-job-{stay,background,cancel,retry,done}`, and `link-job-record`. Jobs motion is confined to `.qqq-job-{fill,shimmer,pulse,ring}` and is static under reduced motion.
The record view has a phone accordion and action sheet, a tablet tab list without the large-screen sidebar, and a large-screen tab list with the sidebar. A selector can be present in the DOM while its control is hidden at a narrower width. Test custom CSS at all three widths, including focus and contrast, before shipping it.
+21
View File
@@ -0,0 +1,21 @@
# Help everywhere — implementation and verification
Backend: QRun-IO/qqq#1177 (`docs/HELP-CONTENT.md`).
## Implementation
- Vendored the supplied help-core `openapi.yaml` as the existing one-space-indented JSON document. The prior vendoring commits are `01dd7e9` and `8a0fbbd`; neither contains a generator script. Conversion uses the installed ESLint dependency `js-yaml`, then `JSON.stringify(document, null, 1)` with a final newline. The full core document is retained, including its other changes within UI API 1.2.0.
- Added `summary` to `QHelpContent`, reused that type for widget slots, and added app/process/report slot maps. `src/lib/api/help.ts` uses the typed API client and checks the advertised `help` feature before calling search. No off-spec exception was added. Search and coverage are optional spec operations; coverage is not called.
- `src/components/help/HelpIcon.tsx` selects role-specific help, shows the plain summary, and opens the shared long-help panel by click, Enter, or Space. Existing customization IDs are retained on the corresponding control or tooltip. Input descriptions remain available while the tooltip is closed.
- `HelpPanelProvider` is mounted in the authenticated dashboard. One `next/dynamic` panel renders sanitized long content using the existing `HelpContent` renderer, with related help, safe navigation, Escape dismissal, focus trapping, and return focus. Help controls use theme tokens and coarse-pointer touch targets.
- Existing field, section, grid-column, widget-title, widget-column, and process-step help now uses the shared icon. New placements cover app, table query/view/edit/create/copy, process, report, and record/table action headers or labels. Action help is a sibling of the action control; Radix menu help participates in roving focus.
- `HelpSearchResults` provides 200 ms debouncing, a two-character minimum, feature gating, and loading/error/empty states. Both `CommandMenu` and the global `SearchPage` use it. The page's existing grouped-results area is a natural place for help; the compact header/search-dialog listbox retains its current record/navigation model. The palette also offers **Search help…**.
- Author mode retains key suffixes. **Add help** / **Edit help** links require the core table's insert permission and use `/app/helpContent/create#/defaultValues=…` with `key` and the most specific `role`. `HELP_CONTENT_TABLE` is the one production constant for that core-defined table.
## Tests and fixtures
- New Vitest tests cover the API gate, role precedence, summaries, keyboard controls, author URL presets and permission checks, panel sanitization/focus, and palette help search.
- `tests/acceptance/fixture/HelpFixtures.java` defines synthetic Help Lab metadata, Markdown bodies and distinct summaries, an owned data table, and the core authoring table. It is wired into the normal define/reset lifecycle.
- `tests/acceptance/specs/help/help.spec.ts` and `matrix/help.json` add HLP-001 through HLP-006, light/dark cases, mobile tags, and 320/1920-pixel panel checks. Column/table-menu coverage remains desktop-only because mobile query results use cards. The area is registered in `acceptance-select.mjs`.
- Existing REC-039/040, PRC-015, and widget-column assertions now focus the help button and inspect formatted long content in the panel. This is necessary because tooltips now contain plain summaries and touch opens the panel. Existing scenarios and matrix requirements remain; none was waived.
- Historical results in `docs/acceptance/feature-matrix.md` are preserved. The new help rows are appended as **not run**.
+29
View File
@@ -0,0 +1,29 @@
# Isolated scripting editor
ScriptViewer routes explicit server-controlled ISOLATED projects before legacy hooks mount. Unknown nonempty modes fail closed. The native scripting extension must be advertised; otherwise isolated source stays unavailable. Developer page provides semantic project creation and opens the same IDE; project names never imply execution mode or actor identity.
The isolated CodeMirror editor supports multi-file TypeScript, immutable source history, explicit save/build/test/publish, server compiler diagnostics, scoped API declarations, JSON context tests and exact-commit GitHub imports with retained provenance. Edits invalidate build/test evidence. Saves and imports carry the observed latest revision. No browser credential fields or background repository synchronization.
## Semantic language service
Pinned editor-only typescript-editor alias6.0.3 powers a same-origin classic worker. It receives a bounded virtual filesystem and host-generated SDK declarations, with57 trusted ES2022 standard-library files bundled at build time. Diagnostics, completion, hover, go-to-definition(F12), formatting(Ctrl+Shift+F), file navigation and compiler diagnostic selection operate on the same workspace. Text remains ordinary undoable editor state. Tab moves focus.
There is no source execution, compiler emit, runtime fetch, arbitrary package loading, filesystem, DOM/Node declarations or user npm hooks. Browser analysis is advisory; authoritative strict builds still use the qualified isolated server. Worker cold deadline10s/warm3s terminates it on timeout; bounded request/result sizes, four pending calls, idle30s termination and recycling after200 requests limit work. The next explicit request may start a new worker; no hidden retry of timed-out source analysis. A browser worker supplies termination responsiveness, not an OS-enforced memory sandbox. Debugging and automatic imports are not implemented.
scripts/build-scripting-worker.mjs assembles only trusted package/compiler and our own session source into public/scripting-generated/typescript-6.0.3.worker.js, with license/notices. Generated9.6MB artifact is ignored and built before dev/build/build:export. Runtime worker requests no other resource. Exact compiler and library dependencies remain independent from server runtime qualification.
The alias package also exports tsc and would shadow the existing compiler under pnpm exec tsc. Use **pnpm typecheck** or **pnpm tsc**: both explicitly call node_modules/typescript/bin/tsc (the existing5.9 build compiler). Existing CI typecheck scripts follow that path. Do not replace frontend build TypeScript with the editor compiler. Compatible CodeMirror versions are pinned through pnpm overrides to avoid duplicate nominal EditorView/state classes.
## Authorization and uncertainty
Server permissions and worker availability control lifecycle actions, with backend checks authoritative. Use the matching Core `4.1.0-SNAPSHOT` dependency set described in [current integration status](QBIT-RELEASE-STATUS.md); the old mixed private-artifact/RC20 setup is superseded. Project creation and publication use typed core authority; isolated revision/file content and ownership are immutable. Real native HTTP lifecycle succeeds with protocol-only fixture worker; no production provider is implied.
Stop waiting or network timeout makes mutation outcome uncertain and prevents another mutation. History refresh does not prove completion; inspect history/reopen. Creation uncertainty directs the user to Scripts before retrying. Abort does not undo committed work. GitHub import needs a server-installed managed access provider and full immutable commit.
## Verification
2026-10-30: **114 focused tests across12 files**, existing build-compiler typecheck and changed-file ESLint passed. Real Playwright Chromium worker check exercises strict diagnostics, multi-file completion/hover/definition/format, rejects remote types and proves source does not execute or fetch. Final optimized production build with typecheck, static generation and standalone packaging passed at 21:36, including the creation panel (parent run20209, editor-build-verification.log). Native server tests separately cover create/save/build/context-test/publish and permission/association/legacy denials. No live browser-to-deployed-worker end-to-end or production sandbox qualification claim.
Run pnpm scripting:worker:check after generating the worker; this environment uses PLAYWRIGHT_BROWSERS_PATH=C:/Users/james/AppData/Local/ms-playwright. The harness serves only a local trusted page/worker with connect-src none and closes browser/server on completion.
Current integration verification (2026-11-04): the real Chromium offline-worker check passes strict diagnostics, multi-file completion/hover/definition and formatting, with no source execution or external requests. Typecheck, lint and production worker packaging pass on the canonical integration branch. See [current status](QBIT-RELEASE-STATUS.md) for the final full-suite evidence. This frontend result does not qualify the server's untrusted-code runtime.
+47
View File
@@ -0,0 +1,47 @@
# PDF preview boundary
The generic file lightbox uses pinned Mozilla `pdfjs-dist` **6.4.299**, loaded only for
PDFs, and a locally packaged worker. It no longer embeds uploaded PDF responses in an
application-origin iframe or offers a PDF "Open original" navigation. Downloads retain
the existing owner authorization callback.
Retained attachment previews reach it only after the tracked-download helper verifies
server receipt, byte length and SHA-256, and assigns `application/pdf` from the fixed
PDF signature. The generic renderer accepts local same-origin blob URLs or native
file endpoints at the exact configured API origin and base path. Both dashboard hosts must permit `blob:` in CSP `connect-src` so the verified local bytes can be read; the current Javalin and standalone defaults include it. It never forwards API
credentials to arbitrary external URLs. Uploaded MIME labels alone are insufficient.
The renderer supplies bytes (not a document URL) to an explicitly created parser
Worker, preventing the library's fake-worker fallback. It creates a canvas and an
optional plain-text view only: no PDF annotation/action, form, XFA, scripting or HTML
viewer layer. Auxiliary resource requests are rejected, worker fetching and WASM are
disabled, and no document-selected external URL is followed. Missing auxiliary fonts
or unsupported resources can limit fidelity; the retained original is downloadable.
Limits: 20 MiB input, 1000 pages, one visible page at a time, 8 million canvas pixels,
8192 pixels per edge, 30 seconds to load/parse, 15 seconds per page render/text read.
Text is streamed and stopped at 10,000 items or 100,000 characters, slicing each
string before aggregation. The stream is cancelled on page change, close or timeout;
page controls remain busy through text completion.
Closing the dialog aborts input loading, cancels rendering, destroys the loading task
and terminates the worker. These are application limits, not a browser-process memory
quota: compressed PDF complexity can allocate parser memory before page checks.
Canvas rendering and a Web Worker do **not** constitute a full hostile-parser security
sandbox. Browser/library patching, CSP and deployment qualification remain necessary.
Primary references checked on 2026-10-30:
- [Mozilla release 6.4.299](https://github.com/mozilla/pdf.js/releases/tag/v6.4.299)
- [Mozilla PDF.js API](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html)
- [Mozilla prior arbitrary-JavaScript advisory](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq)
The prior advisory affected versions through 4.1.392 and was fixed in 4.2.67. The current
6.4.299 API no longer exposes the old `isEvalSupported` option; this integration does
not pretend an obsolete option supplies isolation. It uses the pinned current release,
no PDF scripting manager, and the application's restrictive script CSP.
## Current verification (2026-11-04)
The production export and local Maven JAR include the pinned worker. Twelve Chromium browser scenarios pass against that packaged UI, including actual PDF worker/canvas pixels, page navigation, no executed PDF OpenAction, separate retained preview/download receipts, refusal of incomplete receipts, and ordered-file submission. Real Core/Collaboration API sessions separately pass ordinary signed-PDF canvas preview, page controls and PDF download.
Signed attachment PDFs are read with credentials omitted and redirects refused, bounded to 20 MiB and 30 seconds, and checked by byte signature before a local preview URL is exposed. Their storage origin needs host CSP/CORS configuration. Tracked deliveries retain the receipt/hash path. Cancellation and streaming-limit tests cover both paths. See [current status](QBIT-RELEASE-STATUS.md) for the full suite and [release requirements](QBIT-KNOWN-ISSUES.md) for remaining deployment qualifications.
+13
View File
@@ -0,0 +1,13 @@
# Metadata-driven process result fields
`ViewFormComponent` delegates adorned result fields to the shared `FieldValue` component. Hidden fields remain in the result value map so native metadata can supply display companions; they are not rendered as separate controls. Ordinary process values and validation errors keep their existing behavior.
`LINK` supports either `toRecordFromTable` (a static table name) or `toRecordFromTableField` (the name of another result field containing the table name). The field value is the record identity. Navigation uses the native encoded table/record route. A missing companion table produces no link. Possible-value sources that identify a known instance table also retain native record navigation.
`FILE_DOWNLOAD` can specify `fileNameField` and `deliveryModeField`. If the latter is present, its companion value must be `SIGNED_URL` or `AUTHENTICATED_FETCH`; missing or unknown modes render no access action. Older fields without that metadata retain their existing behavior.
For `AUTHENTICATED_FETCH`, the issued URL is never placed in an `href`, image source or iframe. Download uses the shared authenticated transfer helper, which checks the configured origin, response bounds, exact digest/length and completed durable receipt. An additional `FILE_PREVIEW` adornment enables an explicit preview action. Only verified PDF/raster blobs reach the shared preview component; component-owned blob URLs are revoked when closed or unmounted. Unsupported content and transfer/audit errors remain visible and never fall back to direct access.
`CODE_EDITOR` result fields use the shared read-only source display, including renderer-supplied plain catalog help and HTML/JSON examples. No template or script evaluation occurs in a process result.
Verification for this increment: 178 focused tests across 18 files, owned ESLint and the explicit build-compiler typecheck passed. Each actual transfer creates a fresh correlation UUID only after validating the configured endpoint, preserving the immutable file coordinate and using that same fresh attempt for its receipt. Source changes and unmount abort active body/receipt work. Final integrated production-build/browser acceptance remains pending. These generic components do not embed a QBit name or depend on its backend implementation.
+106
View File
@@ -0,0 +1,106 @@
# QA capture (Jam)
Optional QA bug capture for the Next UI. The backend is the `qbit-qa-capture` QBit, which publishes the `qa-capture` UI API extension (1.0.0). [Jam](https://jam.dev) is the first provider. The UI calls only the two operations in [`src/lib/api/openapi/extensions/qa-capture.json`](../src/lib/api/openapi/extensions/qa-capture.json):
| Operation | Call | Used for |
|---|---|---|
| `qaCaptureCapability` | `GET /qqq/v1/qbit/qa-capture/capability` | Whether capture is on for the signed-in user, and the Jam team, sanitizer rules, blur selectors and backend context |
| `qaCaptureReportCreate` | `POST /qqq/v1/qbit/qa-capture/report` | "Report a bug": creates a report and returns the Recording Link `jam-*` parameters |
The vendored document is hand-written from the shared contract until the QBit publishes its generated `openapi/qqq-v1/qa-capture.yaml`. Re-vendor it from the QBit before release.
## When anything loads
Capture is off unless all of the following hold. When it is off, the UI loads no Jam chunk, adds no Jam meta tags and makes no Jam request:
1. `GET /qqq/v1/version` lists the `qa-capture` extension. Otherwise the capability is never requested.
2. The user is signed in. `QaCaptureLoader` in the root providers then requests the capability once (TanStack Query, `['qqq', 'qa-capture', 'capability']`).
3. The capability has `enabled: true`, provider `jam`, a team id and sanitizer rules. Anything else counts as disabled.
When capture is enabled, `src/lib/qa-capture/jam-loader.ts` (the only module that imports Jam, and only through `import()`) does the following:
- adds `<meta name="jam:team">` and `<meta name="jam:blur">` (the server's `blurSelectors` plus `[data-qqq-secure]`);
- dynamic-imports `@jam.dev/recording-links/sdk` (0.4.0) and `@jam.dev/sdk` (1.0.1);
- calls `jam.initialize({ teamId, blurSelectors, network, console })` once, with the sanitizers;
- registers `jam.metadata(...)`.
A Vitest check fails if any other file imports Jam at runtime.
What the SDKs fetch was checked in their published source. `initialize` loads `https://js.jam.dev/recorder.js` only in two cases: the page URL has `jam-recording` (checked on load and on every `pushState`, `replaceState` or `popstate`), or `loadRecorder()` is called. It loads `https://js.jam.dev/capture.js` only while some tab has an active recorder, which it tracks with the `jam:numRecorders` localStorage counter. `jam.metadata` only stores a callback on `window.__jam__`.
The browser cannot undo the Jam SDK, so turning capture off for a user (QA mode off) takes effect on the next page load. Request-id collection stops immediately.
## QA mode
When `capability.qaMode.preferenceSection` is set, the Preferences page renders that section like any other. Saving it refetches the capability, so turning QA mode on loads Jam without a reload. While `reason` is `QA_MODE`, the header shows a "QA mode on" badge that links to the section.
## Report a bug
The user menu shows **Report a bug** when `canReport` is true, and **QA reports** when `reportsHref` is an in-app path. The UI follows that link as given and never builds table paths itself. The Next UI has no help menu (the header help button opens the keyboard shortcuts), so both entries are in the user menu only.
Report a bug sends `POST /report` with:
- the page URL, with the sanitizer `queryParamNames`, the OAuth `code`, `state` and token parameters, and every `jam-*` parameter removed;
- the title `Bug: <page title>`;
- the app, table, record id or process, resolved from `/app/<slug>[/<id>[/<process>]]` against the instance metadata;
- the last request ids;
- the frontend version, user agent and viewport.
When the response has `recordingParams`, the UI opens the recorder with `await jam.loadRecorder({ openImmediately: false })` and then `Recorder.open(params['jam-recording'], { jamParams: { 'jam-title', 'jam-reference', ... } })`. The SDK puts those parameters on the URL while recording and removes them when the recorder closes, which lets the QBit's `jam.created` webhook match the capture to the report by `jam-reference`.
Without `recordingParams`, a toast explains that an administrator must set the Recording Link in the QA capture settings. It includes a QA reports action when `reportsHref` is present.
## Sanitizers
`src/lib/qa-capture/sanitizers.ts` builds the four Jam sanitizers from `capability.sanitizer`. They are pure, synchronous and deterministic, and they never log or fetch. The server's rules always get a baseline added: the `authorization`, `proxy-authorization`, `cookie` and `set-cookie` headers; the `password`, `sessionUUID`, `accessToken`, `refreshToken`, `idToken`, `clientSecret` and `code_verifier` fields; the `code`, `state`, `access_token` and `id_token` parameters; and JWTs.
| Data | Rule |
|---|---|
| Request and response headers | `headerNames` (lowercase) are deleted |
| JSON bodies | values of `bodyFieldNames` keys are replaced at any depth (case-insensitive); `valuePatterns` are applied to every string; the body stays valid JSON |
| Form-encoded bodies | the same, field by field |
| Other text bodies, WebSocket data | `valuePatterns` |
| Request URLs | `queryParamNames` values are redacted; `valuePatterns` are applied |
| Same-origin paths under `dropPathPrefixes` | request and response bodies are dropped |
| Console arguments | object keys and string patterns as for JSON |
A `valuePatterns` entry that is not a valid RegExp makes the affected sanitizers throw. Jam then redacts that data instead of sending it (fail closed).
## Secure fields
The UI knows a value is secure from its field metadata:
- the field type is `PASSWORD`;
- the field has the `REVEAL` adornment;
- secured data restricts the field (`accessTier` is `MASKED` or `REVEALABLE`).
Elements that show or edit such a value carry `data-jam-blur` and `data-qqq-secure`. This covers `MaskedValue`, a revealed restricted value, grid password and reveal cells, record view reveal and password values, password inputs (including while shown as text) and secure text inputs. The attributes are always present and have no effect when capture is off.
## Request ids
While capture is enabled, an axios interceptor keeps the last 50 `{ id, method, path, status, at }` entries from the response header named by `capability.requestIdHeader` (default `X-Request-Id`). The path is stored without its query string. They go into the Jam metadata and the report. Disabling capture clears the buffer.
## Jam metadata
At capture time Jam receives the following, kept under 10 KB by dropping the oldest request ids first:
- the app, table, record id and process;
- the frontend version (`NEXT_PUBLIC_APP_VERSION`);
- the backend `environment`, `backendVersion` and `node` from the capability;
- the request ids;
- the report reference;
- the page path, filtered as above.
## Recording Links and sign-in
A Recording Link can open a protected page as `?jam-recording=...&jam-title=...`. The login redirect, the OIDC/OAuth2 callback and the 401 redirect already carry the query in `returnTo`. Every page load also stashes the `jam-*` parameters in sessionStorage. After sign-in they are put back on the URL with `history.replaceState`, unless the page already has a `jam-recording`. The stash expires after 30 minutes. The Jam SDK watches `replaceState`, so the recorder then opens.
The public `/qa/recorder` page for Jam domain verification is served by the QBit, not by Next. The standalone server forwards `/qa/*` to the backend.
## Content-Security-Policy
Next adds no CSP of its own. In a jar deployment the dashboard CSP comes from the QQQ server (`NextDashboardSecurityHeaders`), and the QBit or application must add the Jam origins listed in `capability.cspOrigins`. The standalone container server (`standalone/qqq-server.mjs`) adds them only when `QQQ_QA_CAPTURE_CSP=true`:
- `script-src https://js.jam.dev`
- `frame-src https://*.jam.dev`
- `connect-src https://*.jam.dev`
+11
View File
@@ -0,0 +1,11 @@
# QBit integration known issues and release requirements
Updated: 2026-11-05.
- The Core, Collaboration and Next UI changes are verified source branches and local snapshots, not a published dependency set. Public Core RC.21 / Collaboration RC.6 lack the required retention contracts. Merge and publication through the existing release process remain separate from local verification.
- The frontend checks include real Core/Collaboration sessions and file storage, plus browser protocol fixtures for retained receipts. They do not establish end-to-end document generation/printing or deployed untrusted-script execution. Those QBits and workers retain their own qualification requirements.
- External signed PDF storage needs CORS for the dashboard origin and its origin in CSP `connect-src`. Application credentials are never sent to those URLs. Hosts replacing dashboard CSP must retain `blob:` in `connect-src` for verified local previews.
- Lint reports six existing warnings in unchanged files. The unit suite has one existing skipped test. Canonical Next 16.4.1 and Zod 3 are preserved; dependency patch upgrades remain a separate release review.
- The local artifact is `qqq-frontend-next:1.0.0-SNAPSHOT`, built with an explicit Maven revision override. It includes generated PDF and TypeScript workers. The prepared release-managed version is `1.0.0-RC.38`; RC.36 and RC.37 already exist in Central and must never be overwritten.
- Next publisher support and the shared producer are merged (Praetor PR238, platform-ci PR67); the local caller pins327e9b3a9d45a08665fa9468f0f9741e972d8e3e. Runtime qualification passed736 Linux tests and its vulnerability scan. GitOps PR269 deployed the qualified runtime and exact workflow/GAV enrollment; ArgoCD adoption and the scheduled publisher startup are verified. The signed application tag and resulting Central artifact still remain. The existing tag, package.json and POM version agreement, typecheck/lint/tests/static export and signed Central delivery are preserved. Neither the local JAR nor source merge proves publication.
- The full application acceptance matrix across all browsers and deployment-specific storage/printer/scanner behavior is not claimed by the scoped QBit integration checks.
+58
View File
@@ -0,0 +1,58 @@
# Next UI QBit integration status
Updated: 2026-11-05. Source integration is prepared; no published release or deployment is claimed.
Current main `090f5febeaacf3f9c39a22952e5c93c859cdbeb7` (permission editor and companion Tools workspace) is integrated. The latest merge passed all 110 affected Authentik/widget/process tests, typecheck and the copyright-header gate. Both branches supplied the same worker limit; the merged configuration retains one setting. Prior focused Tools/UI-API checks covered 61 cases. These are integration checks, not completed release CI.
Core PR1343 merged as7825261ab after full CI4482 passed; promotion PR1345 merged as1bef44cf and immutable publication4629 is running. Collaboration must consume the resulting published version. Next publisher support merged in Praetor PR238 and platform-ci PR67. GitOps PR269 merged as883e1ce3; ArgoCD adopted the exact configuration and a scheduled publisher is running image sha256:ea068b2a02147280cacedd095fbc55114daa9e3db72f1b0ca8511bc43bfd12c7 with the exact Next enrollment. Its 736 Linux tests and vulnerability scan passed. The dashboard candidate version is 1.0.0-RC.38; this source preparation is not a published release.
Candidate CI4602 passed 3,800 tests but hit the unchanged five-second timeout in two `RecordQuery` filter tests. These also timed out on an earlier baseline; the newer main baseline's Node job passed. Profiling the focused cases identified repeated whole-page accessibility lookups. Scoping those lookups to the existing filter controls and confirmation dialog reduced their sampled CPU cost from 387 to 261 samples; the same assertions, interactions and timeouts are preserved. All 17 tests in the affected file and focused lint pass after the change. Release CI for this correction remains required; these focused results do not replace it.
## Changes
The canonical Next UI integration renders file preview/download adornments, retained-transfer receipts, isolated TypeScript editing and generic process results through QQQ metadata and UIUX APIs. It adds no table-specific application routes. The completion pass fixes Windows static-export execution, local PDF reads under dashboard CSP, and ordinary signed-storage PDF previews. Signed storage reads omit credentials, refuse redirects and enforce byte/time limits before canvas rendering. Retained transfers preserve their receipt/hash checks. Tests now use four workers by default to avoid high-core host contention; no timeout or coverage threshold was relaxed.
## Verified dependencies
- Core `feat/standalone-scripting-api`, code commit `d384d1c753cb0ac2ee4d0f6ccd564a3e47488a1c`, locally installed as `4.1.0-SNAPSHOT`.
- Collaboration `feat/record-files-standalone`, `53bd8f00d8a180b0fa4eb6cd029dc31044b6d4b3`, locally installed as `0.1.0-SNAPSHOT`.
- Next UI retains canonical Next 16.4.1, Zod 3 and existing dependencies. Local packaging explicitly overrides Maven revision to `1.0.0-SNAPSHOT`; it does not replace public RC.36.
## Completed verification
| Check | Result |
| --- | --- |
| Full `pnpm test:coverage` | 359 files; **3,784 passed**, 1 existing skip; all coverage gates passed |
| Coverage | 93.14% statements, 87.00% branches, 94.57% functions, 94.95% lines; API/utility sub-gates also passed |
| Lint and license headers | Zero errors, six existing warnings in unchanged files; Apache headers passed |
| Typecheck | Passed |
| Offline TypeScript worker in Chromium | Strict diagnostics, multi-file completion/hover/definition and formatting passed; no source execution or external fetch |
| Windows production static export | Passed using `pnpm build:export` |
| Local Maven snapshot JAR | Built successfully with PDF/TypeScript workers included |
| Browser file/process regressions | **12 passed** against the packaged production UI; API fixtures exercise PDF canvas, separate audit receipts, incomplete-receipt refusal and keyboard ordering |
| Live Core + Collaboration file workflow | Upload, thumbnail, preview, open, exact-byte download, rename/describe, reload and delete passed with synthetic data |
| Live PDF and authorization | Signed PDF canvas/page controls and download passed; another session was denied access to the parent record |
| Live UI API specification check | Bundled 1.8.0 compatible with running Core 1.10.0 |
The live tests used the existing disposable `CollaborationDevServer`, synthetic Alice/Bob sessions and the current UI JAR on loopback. No API mocks were used for those file/permission checks. The separate 12-case browser suite uses its existing API fixtures, not a deployed Record Files or scripting worker. Test-owned services and browsers were stopped after verification.
## Reproduce
```shell
pnpm install --frozen-lockfile
pnpm test:coverage
pnpm lint
pnpm typecheck
pnpm scripting:worker:check
pnpm build:export
mvn -B -Drevision=1.0.0-SNAPSHOT package
pnpm ui-api:check --server-url http://localhost:18098
```
Start the matching Collaboration demo using its README and place the resulting UI JAR first on its classpath. The live check used port 18098. The standard `pnpm test:e2e` owns its frontend fixture for the browser scenarios. Maven was invoked through the sibling Collaboration wrapper during this Windows run.
Local JAR SHA-256: `0626B33DF49569476E17FA080E2777A7AD9C7AF717CB75DFABF9EB40BE0A46A8`.
## Release boundary
See [known issues and release requirements](QBIT-KNOWN-ISSUES.md). Compatible public artifact publication, the complete application acceptance matrix and deployed document/script worker qualification are not implied by these source and local integration results.
+21
View File
@@ -0,0 +1,21 @@
# Record Files client integration
Updated: 2026-11-04. Current canonical integration branch: `feat/standalone-qbits-ui`. See [verification and dependencies](QBIT-RELEASE-STATUS.md).
## Standard controls and APIs
The Files tab uses standard themed buttons for Preview, Open, Download and Rename / describe. Navigation uses Next links with keyboard focus. Attachment metadata supplies independent `canEdit` and `canDelete` permissions; an explicitly denied edit capability never falls back to deletion permission. The server remains authoritative.
Every download requests a fresh owner-issued descriptor. `AUTHENTICATED_FETCH` deliveries use the configured native Record Files route, a new correlation identifier, and a separately verified completion receipt, byte count and SHA-256. Preview and save are separate attempts. A failed receipt never becomes a direct link or a successful save. Generic `FILE_DOWNLOAD` and `FILE_PREVIEW` adornments use the same controls; they do not require table-specific pages.
Tracked raster/PDF previews expose a local object URL only after verification. Ordinary signed PDF previews read bounded bytes with credentials omitted, redirects refused and a 30-second deadline. Both use the same canvas PDF renderer; uploaded PDFs are never framed. Closing the preview aborts work and revokes local URLs. Hosts serving signed files from another origin must allow that storage origin in CSP `connect-src` and configure storage CORS for the dashboard origin. This never forwards QQQ credentials to storage.
Open previews supported media; unsupported types use the authorized download action. The UI does not claim that a browser saved bytes or that a printer physically printed them. Retention, upload sessions, collections and durable audits belong to their server QBits.
## Dependency order
Install matching Core, then Collaboration, then Record Files and other feature QBits before the matching frontend. Public Core RC.21 and Collaboration RC.6 do not contain the new retention contracts. Local verification uses Core `4.1.0-SNAPSHOT` and Collaboration `0.1.0-SNAPSHOT`; see their branch status files for exact source commits. Core's dashboard CSP must include `blob:` in `connect-src` for local PDF-byte reads.
## Verification scope
The full component/API suite and coverage gates, production export, browser PDF/receipt/ordering scenarios and live Collaboration file workflow are recorded in [current status](QBIT-RELEASE-STATUS.md). Live tests use disposable synthetic data and real Core/Collaboration APIs. Browser mocks of the retained-transfer protocol do not establish production storage or worker qualification.
+17
View File
@@ -0,0 +1,17 @@
# UI API 1.6 adoption
Adopts QQQ UI API 1.6.0 (QQQ 4.1.0-RC.15): table clientCapabilities, field validation, BasicErrorResponseV1, /qqq/v1/branding.
- Vendors the supplied core, api-aware, ESB, alerts, collaboration, data-analysis and EDI documents; regenerates EDI types. Assistant, quick-search, secured-data and sql-workbench documents are unchanged.
- Saved views, audits, sharing and bulk actions use per-user table capabilities. Legacy discovery is used only when `clientCapabilities` is absent; a missing or unavailable feature in a present capability object does not enable legacy access.
- Forms use `field.validation` for required values, length policies, inclusive/exclusive bounds and whole-value patterns. Existing record-write error mapping remains intact.
- Errors prefer `message` over legacy error fields. Global error toasts and process error details expose the trace identifier. Session-expiration handling remains in place.
- Standalone branding aliases call `/qqq/v1/branding/{role}` and retry the old public route only after a 404. Instance consumers prefer `supplementalInstanceMetaData.uiSettings`, with deprecated settings as a fallback.
The app version is unchanged. No commit, push, PR or deployment was made.
## Verification
The supplied server specification comparison passes at 1.6.0. `OFF_SPEC_CALLS` remains empty and `OFF_SPEC_CEILING` remains 0.
Vitest startup is blocked by this session's Windows sandbox (`spawn EPERM` launching esbuild), so no Vitest pass count is claimed. The production bundle compiles, but `pnpm build` stops at its TypeScript child-process launch with `spawn EPERM`; no completed build or bundle-budget delta is available. Standalone `pnpm typecheck` passes. `pnpm lint` passes with six pre-existing warnings. Twelve direct Node smoke checks pass: seven lossless JSON vendoring comparisons and five branding status/fallback cases. The focused contract/extension Vitest invocation and full `pnpm test` both stop before running tests.
+24 -2
View File
@@ -91,7 +91,7 @@ The additional ESB integration scenarios and their focused three-browser results
| PRC-016 | Back to the backend back step with entered values | Back restarts at the back step with values (possible-value label shown); resubmitting saves again | no Back without a backend backStep | prcComponents review, prcWizard longDetails, greetInteractive | pass (chromium) | [#655](https://github.com/QRun-IO/qqq/issues/655) [#667](https://github.com/QRun-IO/qqq/issues/667) |
| PRC-017 | Dynamic step list from processMetaDataAdjustment.updatedFrontendStepList | short route drops a screen from the stepper; values logged (SQL) | | prcWizard | pass (chromium) | [#662](https://github.com/QRun-IO/qqq/issues/662) |
| PRC-018 | Updated field metadata from processMetaDataAdjustment.updatedFields | field relabelled and required on the later screen | empty relabelled required field blocks | prcWizard long route | pass (chromium) | [#662](https://github.com/QRun-IO/qqq/issues/662) |
| PRC-019 | Async job progress (message, N of M, progress bar, updated time) then continue | RUNNING progress shown while polling; finished screen and SQL log | | prcProgress | pass (chromium) | [#657](https://github.com/QRun-IO/qqq/issues/657) |
| PRC-019 | Async job progress in the live job view (message, N / M, determinate progress bar, elapsed) then continue | RUNNING progress shown while polling; finished screen and SQL log | | prcProgress | pass (chromium) | [#657](https://github.com/QRun-IO/qqq/issues/657) |
| PRC-020 | Cancel from a screen and mid-job runs the cancel step and leaves | confirm dialog; cancel step row (SQL) from a screen and while working | Stay on Page keeps the run | prcProgress cancelStep | pass (chromium) | |
| PRC-021 | Step errors: user-facing messages directly, internal errors behind a detail toggle, Close | user-facing message; internal detail toggle; expanded details meet contrast requirements in light, dark and application themes; simpleThrow "I always throw."; Close returns to the app | | prcFailures, prcComponents, sample simpleThrow | pass (chromium) | |
| PRC-022 | Retry after an error restarts with the original record selection | retry loads the selected records 2 and 4 | | prcFlaky (fails once) | pass (chromium) | [#658](https://github.com/QRun-IO/qqq/issues/658) |
@@ -190,7 +190,7 @@ The additional ESB integration scenarios and their focused three-browser results
| QRY-066 | Variant options come from the v1 variants route | the variant picker lists the stores from GET /qqq/v1/table/{table}/variants; the v1 route lists every option with its type and id; a table without variants lists none | an unknown table is refused (403) | qryStock (qryStore options) | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| QRY-067 | Go To opens a record by its primary key or a configured unique key (Material gotoFieldNames) | v1 table metadata carries supplementalMetaData.materialDashboard.gotoFieldNames (API); Go To... on the query toolbar and the record view header offers the primary key, then each key, labelled by field labels; primary key, single-field and two-field keys each open the one matching record (EQUALS query, id from SQL); no match shows "Record not found."; a partial key matching two bins shows "More than 1 record was found..." (counts from SQL); Enter in an input submits its option; Escape closes and returns focus to the Go To button; a table with GET but no QUERY capability opens Go To automatically and it cannot be dismissed | a table without gotoFieldNames has no Go To button on its query or record screen; no navigation on no match or several matches | QueryFixtures.java qryBin (gotoFieldNames [[code], [aisle, shelf]]), qryLocker (GET without QUERY), qryItem (no Go To) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| QRY-068 | Default grid columns follow the table sections (section order, each field once), not the metadata field order (Material bySection order) | Headers read Id, Name, Code, Price, Quantity; Rows equal SQL select ordered by id desc | The metadata field-map order (price first) is not used | QueryFixtures.java qryOrdered (qry_item rows; fields declared price, quantity, code, id, name; sections identity [id, name, code], details [price], stock [quantity]) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| QRY-069 | Pagination numbers are locale formatted in the shown range and the total | Page 5 at 250 per page reads Showing 1,001–1,234 of 1,234; The page shows exactly the last 234 ids by SQL | No unformatted range such as 1001–1234 | QueryFixtures.java qryManyRow (1,234 rows) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| QRY-069 | Pagination numbers are locale formatted in the shown range and the total | Page 5 at 250 per page reads Showing 1,001–1,234 of 1,234; The page shows exactly the last 234 ids by SQL | No unformatted range such as 1001–1234 | QueryFixtures.java qryManyRow (1,234 rows) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| QRY-070 | Query screen keyboard shortcuts n (new record), r (refresh the query), f (open the filter builder) | r sends a new query request and shows a value changed through the API (SQL confirms the change); f opens the Advanced Filters panel and a second f keeps it open; n opens the create form | keys typed in quick search are text (no filter panel, no navigation); viewer persona: n does nothing (f still works); the backend refuses the insert (403, SQL unchanged) | sample person | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| QRY-071 | Processes launched from a variant table run with the chosen variant (tableVariant on init and every step) and return to the query | Bulk Delete of the South store's full query result counts its one record; Every init and step request carries tableVariant {type: qryStore, id: 2}; Return comes back to the variant query, which is empty for South while North keeps its two rows | Without the variant the backend could not read the variant table's records | QueryFixtures.java qryStock (memory backend keyed by qryStore variants) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| QRY-080 | Basic mode displays default quick filters from table metadata and applies their criteria | First Name quick filter finds the SQL-matching records; Type-specific defaults gate Boolean until chosen, default possible values to any-of and date-time to after; A multi-value chip summarizes extra values; A saved Basic view restores its criterion and SQL rows | No application-specific field names are embedded in the UI; Cancel discards an un-applied quick-filter draft | sample person and QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
@@ -348,6 +348,7 @@ The additional ESB integration scenarios and their focused three-browser results
| INT-012 | Stale state: changes and deletions made elsewhere are shown after refresh/navigation, not stale cache | record edited through the API shows the new value on revisit | record deleted through the API shows not found | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#683](https://github.com/QRun-IO/qqq/issues/683) |
| INT-013 | Empty state: a query with no results explains it and offers the create action | no-match filter shows the empty message and Create | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
| INT-014 | Backend unavailable at sign-in: the login page explains the failure and recovers with Try again | Try again after recovery signs in | network failure shows "Sign-in failed" | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
| SEC-050 | Optional QA capture (Jam, qbit-qa-capture qa-capture extension): Jam loads only for enabled users; Report a bug opens the recorder; secure values blurred; captures sanitized | extension absent or capability disabled: no Jam chunk, meta or request; enabled: jam:team meta, one initialize with sanitizers, jam.metadata; Report a bug: POST report then Recorder.open with jam-title and jam-reference; Recording Link jam-* parameters survive the OIDC sign-in; QA mode preference refetches the capability | no Recording Link configured: toast, no recorder; Authorization/Cookie headers, secure body fields and JWTs redacted | qbit-qa-capture (not yet released); Vitest: src/lib/qa-capture/*.test.ts, src/components/qa-capture/QaCapture.test.tsx | excluded (Pending: not yet approved by the owner. No real-backend fixture until qbit-qa-capture is released; the row is covered by Vitest until then (QA capture PR, 2026-10-15).) | |
## Dashboards, widgets, reports, saved and scheduled reports, and sharing
@@ -442,3 +443,24 @@ The additional ESB integration scenarios and their focused three-browser results
| RPT-020 | Report files download from the v1 download and report routes | a process report links its file at /qqq/v1/download/... and the file has every row; a report without a process streams from /qqq/v1/reports/{name}?format=csv with every row | an unregistered server file or storage reference is refused (403); a restricted report is 403, an unknown one 404, a missing format 400 | accPersonReport, accStreamedReport, accRestrictedReport | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| WID-074 | Standalone composite scalar controls and metadata editors | Seeded text, number, date, boolean and native file controls; currency prefix, named possible values and multiline code editor; Choice width and center-click behavior with system and monospace fonts, including a 320px viewport; Backend uppercase/lowercase metadata preserves middle-of-value edits and caret position in standalone blocks; Multiline TEXT case conversion preserves the middle-edit caret and inserted newline; Desktop wheel scrolling preserves plain and shared numeric block values; intentional ArrowUp edits still work and text inputs retain focus; Native standalone TIME controls increment by one second and retain second-level values. | Code-editor Enter leaves the widget on the same page; Multiline Enter does not submit or navigate away | widgetInputEditors / accTypedInputs | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
| WID-075 | Standalone DATE_TIME blocks localize seeded instants without losing seconds | UTC timestamp displays the correct local date, time and seconds | Do not slice UTC digits into a local-time field | accTypedInputs in America/New_York | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
## Contextual help, help search and authoring
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| HLP-001 | App and widget help | summary tooltip; keyboard panel; sanitized long markdown; focus restoration; light and dark; desktop and mobile | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
| HLP-002 | Table, field and section help | view screen; edit screen; create screen; role selection | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
| HLP-003 | Query columns and table action help | column summary; query table header; action is not invoked by help | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
| HLP-004 | Process, step and record action help | process header; step header; record menu keyboard control | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
| HLP-005 | Help search | static Search help command; debounced help query; result summary and panel; Go to link | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
| HLP-006 | Help author affordance | key and specific role URL presets; standard create form; read-only users cannot author | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
The help rows above have not been executed locally; historical results above predate this change.
## Release notes UI API extension
`release-notes` 1.0.0 (qbit-release-notes 0.1.0-RC.1+) provides What's new at
`/app/whats-new`: category groups, new badges, append paging, safe links/screenshots,
release deep links, seen tracking and the user-menu unread count. Extension discovery
hides the menu and prevents requests on unsupported servers. Covered by the release-notes
client/page and Sidebar Vitest suites; real-backend acceptance remains pending.
+1 -1
View File
@@ -545,7 +545,7 @@ Process and report launch, the step lifecycle, process form fields, step compone
| Load instance/process/table metadata, with load-error messages | `material:src/qqq/pages/processes/ProcessRun.tsx:1858-1897` | `src/app/(dashboard)/app/[slug]/page.tsx` ("Failed to load process metadata.") | none | Done |
| Init, then handle JobComplete / Started / Running / Error | `material:src/qqq/pages/processes/ProcessRun.tsx:1595-1740` | `src/lib/hooks/use-process.ts:handle` | PRC-001, PRC-019 | Done |
| Async polling: 1.5 s, ×1.5 backoff to 12 s on 5xx, then "Could not connect to server" | `material:src/qqq/pages/processes/ProcessRun.tsx:98-100, 1745-1795` | `src/lib/hooks/use-process.ts` pollRef / POLL_* | PRC-019 | Done |
| Working screen: spinner, message, "N of M", progress bar, "Updated at" | `material:src/qqq/pages/processes/ProcessRun.tsx:631-670` | `src/components/process/ProcessRun.tsx` working block | PRC-019 | Done |
| Working screen: spinner, message, "N of M", progress bar, "Updated at" | `material:src/qqq/pages/processes/ProcessRun.tsx:631-670` | `src/components/jobs/InlineJobProgress.tsx` (live job view: phases, message, N / M, eased bar, ETA, elapsed and live log in place of "Updated at") | PRC-019 | Done |
| Loading shown before the first step | `material:src/qqq/pages/processes/ProcessRun.tsx:631, 2079` | `src/components/process/ProcessRun.tsx` (phase ≠ step) | PRC-040 | Done |
| userFacingError vs internal error | `material:src/qqq/pages/processes/ProcessRun.tsx:1720-1734` | `src/lib/hooks/use-process.ts` ERROR case | PRC-021 | Done |
| Error screen: "An error occurred…", show/hide detail toggle, Close/Cancel | `material:src/qqq/pages/processes/ProcessRun.tsx:589-629` | `src/components/process/ProcessErrorState.tsx` | PRC-021, PRC-022 | Done: Next adds Retry |
+1 -1
View File
@@ -1,6 +1,6 @@
# Next UI agent guide
> **This checkout targets RC21.** Confirm availability in the [RC21 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.21) before installing. [RC21 notes](../releases/1.0.0-RC.21.md) record the preparation checkpoint and verification scope; RC20 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
> **This checkout targets RC36.** Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing. [RC36 notes](../releases/1.0.0-RC.36.md) record the preparation checkpoint and verification scope; RC35 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
> **Live roadmap and known issues:** [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713). GitHub issues own current status and remaining work. Follow the [issue-tracking policy](../ISSUE-TRACKING.md) when reporting or resolving findings; keep this guide focused on usage and evidence.
+3 -3
View File
@@ -1,12 +1,12 @@
# Next UI developer guide
> **This checkout targets RC21.** Confirm availability in the [RC21 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.21) before installing. [RC21 notes](../releases/1.0.0-RC.21.md) record the preparation checkpoint and verification scope; RC20 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
> **This checkout targets RC36.** Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing. [RC36 notes](../releases/1.0.0-RC.36.md) record the preparation checkpoint and verification scope; RC35 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
> **RC9:** application code is unchanged from RC8. See [RC9 notes](../releases/1.0.0-RC.9.md) and [published releases](https://github.com/QRun-IO/qqq-frontend-next/releases) for versioned verification and public delivery status. Earlier RC8 receipts below remain historical evidence.
> **Live roadmap and known issues:** [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713). GitHub issues own current status and remaining work. Follow the [issue-tracking policy](../ISSUE-TRACKING.md) when reporting or resolving findings; keep this guide focused on usage and evidence.
This guide describes the `feature/next-1.0` release worktree as of 2026-10-01. It explains the current frontend and backend metadata contract. The previously verified public candidate at the RC21 preparation checkpoint was [1.0.0-RC.20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20) (commit `60d0b0f`); final `1.0.0` remains on hold. Later branch changes are not retroactive changes to that immutable release. For behavior proven against a real server use the [feature matrix](../acceptance/feature-matrix.md); for the full Material inventory use the [parity ledger](../acceptance/material-parity.md). Some ledger rows predate recent work, so verify an individual status against current source and acceptance results before making a release claim.
This guide describes the `feature/next-1.0` release worktree as of 2026-10-01. It explains the current frontend and backend metadata contract. The previously verified public candidate at the RC36 preparation checkpoint was [1.0.0-RC.35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) (commit `55c1501`); final `1.0.0` remains on hold. Later branch changes are not retroactive changes to that immutable release. For behavior proven against a real server use the [feature matrix](../acceptance/feature-matrix.md); for the full Material inventory use the [parity ledger](../acceptance/material-parity.md). Some ledger rows predate recent work, so verify an individual status against current source and acceptance results before making a release claim.
The visual baseline is the September 25 Next UI design (`e42ad2b2`). Material remains the reference for backend contracts and functional behavior. The layout defaults below describe current source; they do not certify a completed visual review.
@@ -35,7 +35,7 @@ The preferred QQQ-hosted distribution is the `com.kingsrook.qqq:qqq-frontend-nex
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.21</version>
<version>1.0.0-RC.36</version>
</dependency>
```
+2 -2
View File
@@ -10,11 +10,11 @@ RC1 remains immutable: [v1.0.0-RC.1](https://github.com/QRun-IO/qqq-frontend-nex
The owner authorizes publishing new RCs at verified stability or daily milestones. Each RC gets a new immutable version and notes describing its fixes, backend prerequisites, verification, and known gaps. RC publication does not authorize final 1.0.
Previously verified public candidate at the RC21 preparation checkpoint: **[1.0.0-RC.20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC19 remain available and unchanged.
Previously verified public candidate at the RC36 preparation checkpoint: **[1.0.0-RC.35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC20 and RC22 through RC35 remain available and unchanged (RC21 was never published).
## Fixes during testing
This checkout targets [RC21](1.0.0-RC.21.md); confirm availability in the [RC21 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.21) before installing. RC21 adds file attach in the assistant panel (bulk load) ([PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125) (attach a file in the assistant panel for bulk load)). RC20 added readable assistant answers (GFM and presentation blocks), data-analysis profile panels, and favorites and recents ([PR92](https://github.com/QRun-IO/qqq-frontend-next/pull/92) (UI API version contract), [PR104](https://github.com/QRun-IO/qqq-frontend-next/pull/104) (UI API contract picker paths), [PR109](https://github.com/QRun-IO/qqq-frontend-next/pull/109) (acceptance ESB extension fix), [PR103](https://github.com/QRun-IO/qqq-frontend-next/pull/103) (data-analysis column and table profile panels), [PR105](https://github.com/QRun-IO/qqq-frontend-next/pull/105) (favorites, server recents and command-palette favorites), [PR111](https://github.com/QRun-IO/qqq-frontend-next/pull/111) (readable assistant answers: GFM markdown and presentation blocks)). RC19 added the related-record picker (single, multi-child and link modes) and the assistant Read-only / Write switch ([PR96](https://github.com/QRun-IO/qqq-frontend-next/pull/96) (related-record picker: single, multi-child and link modes), [PR101](https://github.com/QRun-IO/qqq-frontend-next/pull/101) (assistant Read-only / Write switch per conversation)). RC18 added the Authentik policy editor widget and the assistant pending-action card with persona ([PR91](https://github.com/QRun-IO/qqq-frontend-next/pull/91) (bundle budget report-only (owner decision)), [PR97](https://github.com/QRun-IO/qqq-frontend-next/pull/97) (faster sharded acceptance CI), [PR98](https://github.com/QRun-IO/qqq-frontend-next/pull/98) (budget script honors "enforce": false), [PR95](https://github.com/QRun-IO/qqq-frontend-next/pull/95) (Authentik policy editor widget), [PR94](https://github.com/QRun-IO/qqq-frontend-next/pull/94) (assistant pending-action card, persona and refresh after a write)). RC17 added the notification center (bell inbox, notification list and preferences for qbit-alerts user notifications) ([PR89](https://github.com/QRun-IO/qqq-frontend-next/pull/89) (notification center)). RC16 added the record picker for large possible-value fields, metadata-driven form validation, date-first filters with presets, the Alerts bell and record panel, resizable navigation and search preview thumbnails ([PR79](https://github.com/QRun-IO/qqq-frontend-next/pull/79) (resizable desktop navigation), [PR84](https://github.com/QRun-IO/qqq-frontend-next/pull/84) (search preview thumbnails), [PR86](https://github.com/QRun-IO/qqq-frontend-next/pull/86) (CSS budget 24 KB), [PR87](https://github.com/QRun-IO/qqq-frontend-next/pull/87) (range-error wording follows the backend), [PR85](https://github.com/QRun-IO/qqq-frontend-next/pull/85) (Alerts bell and record panel), [PR82](https://github.com/QRun-IO/qqq-frontend-next/pull/82) (date-first filters), [PR81](https://github.com/QRun-IO/qqq-frontend-next/pull/81) (quiet, metadata-driven form validation), [PR80](https://github.com/QRun-IO/qqq-frontend-next/pull/80) (record picker for large possible-value fields)). RC15 added the QQQ Assistant panel, the record developer view, search-as-you-type quick search and a search page, readable dashboard widget widths, one page scroll container and SQL Workbench dark mode ([PR58](https://github.com/QRun-IO/qqq-frontend-next/pull/58) (README adoption guide), [PR55](https://github.com/QRun-IO/qqq-frontend-next/pull/55) (readable dashboard widget widths), [PR54](https://github.com/QRun-IO/qqq-frontend-next/pull/54) (one page scroll container), [PR77](https://github.com/QRun-IO/qqq-frontend-next/pull/77) (Dependabot limited to three open PRs per ecosystem), [PR78](https://github.com/QRun-IO/qqq-frontend-next/pull/78) (SQL Workbench dark mode and standard controls), [PR61](https://github.com/QRun-IO/qqq-frontend-next/pull/61) (record developer view), [PR60](https://github.com/QRun-IO/qqq-frontend-next/pull/60) (QQQ Assistant panel, phase 1), [PR59](https://github.com/QRun-IO/qqq-frontend-next/pull/59) (search-as-you-type quick search, richer results and a search page)). RC14 was an urgent fix: record search no longer fails when one result has no label ([PR57](https://github.com/QRun-IO/qqq-frontend-next/pull/57)). RC13 added the SQL Workbench ([PR47](https://github.com/QRun-IO/qqq-frontend-next/pull/47)), FILE_PREVIEW thumbnails and lightbox ([PR49](https://github.com/QRun-IO/qqq-frontend-next/pull/49), QQQ #1056), responsive layouts with readable child lists and compact dates ([PR50](https://github.com/QRun-IO/qqq-frontend-next/pull/50)) and the instance home app ([PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53), QQQ #1059), and the other changes listed in the RC13 notes. These changes are absent from immutable RC12; the RC14 search fix isLine truncated
This checkout targets [RC36](1.0.0-RC.36.md); confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing. RC36 adds accessibility fixes: readable link text, 24px clear targets, h2 widget titles and skip-link focus ([PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175) (a11y: readable link text, 24px clear target, h2 widget titles, skip link focus)). RC35 added a profile photo uploader and avatars everywhere (UI API 1.8.0) ([PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164) (profile photo uploader and avatars everywhere, UI API 1.8.0)). RC34 added fail-soft error handling: layered error boundaries, validated assistant payloads and streams that can no longer lock the panel ([PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172) (layered error boundaries, zod payload repair, streaming can no longer lock the assistant panel)). RC33 added the new navigation (sidebar sections and favorites, one search, a new header, single-key shortcuts) and an assistant crash fix ([PR168](https://github.com/QRun-IO/qqq-frontend-next/pull/168) (sidebar sections and favorites, one search, new header, single-key shortcut switch), [PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170) (empty assistant query results no longer crash the page)). RC32 added a What's new page and user-menu entry for release notes ([PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165) (What's new page and user-menu entry, release-notes UI API 1.0.0)). RC31 added UI API 1.6.0 adoption: client capabilities, field validation, one error schema and v1 branding ([PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162) (adopt QQQ UI API 1.6.0: clientCapabilities, field validation, error schema, v1 branding)). RC30 added optional Jam QA capture on the qbit-qa-capture UI API extension ([PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160) (optional Jam QA capture on the qbit-qa-capture qa-capture extension)). RC29 added the EDI mapper page on the qbit-edi UI API extension ([PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136) (EDI mapper page on the qbit-edi edi extension, all calls on spec)). RC28 added zero off-spec UI API calls, so every backend call is a /qqq/v1 operation in the spec (minimum UI API 1.4.0) ([PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141) (zero off-spec UI API calls; OFF_SPEC_CEILING 0; minimum UI API 1.4.0)). RC27 added the secured-data UI (masked values, reveal, restricted filters, break-glass banner) and the live job view with a header Jobs tray and Jobs page ([PR124](https://github.com/QRun-IO/qqq-frontend-next/pull/124) (secured-data UI: masked values, reveal, restricted filters, break-glass banner), [PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155) (live job view, header Jobs tray and Jobs page), [PR156](https://github.com/QRun-IO/qqq-frontend-next/pull/156) (large-table acceptance fixture declares its default columns)). RC26 added tables that open with a short default column set instead of every field ([PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142) (tables open with a short default column set)). RC25 added an assistant panel that never blocks asking, re-attaches after refresh, shows live progress and can always Stop ([PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151) (assistant: never block asking, re-attach after refresh, live progress and Stop)). RC24 added the tablet record-tab fix, empty tables that explain themselves, a Full access checkbox in the policy editor and homes without duplicate card grids ([PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149) (record section tabs wrap instead of folding into More; tablet regression from PR144), [PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150) (empty tables explain what they are for and how rows get created), [PR148](https://github.com/QRun-IO/qqq-frontend-next/pull/148) (Full access checkbox in the authentik policy editor), [PR145](https://github.com/QRun-IO/qqq-frontend-next/pull/145) (no automatic card grids on homes that declare widgets)). RC23 added help everywhere, the Preferences page, contingent possible values in forms, search typeahead and System/Light/Dark theming ([PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127) (help everywhere: help icons, long-help panel, help search, author links), [PR121](https://github.com/QRun-IO/qqq-frontend-next/pull/121) (Preferences page, notification settings drawer and section actions), [PR120](https://github.com/QRun-IO/qqq-frontend-next/pull/120) (contingent possible values across forms, bulk edit, processes and filters), [PR143](https://github.com/QRun-IO/qqq-frontend-next/pull/143) (typeahead suggestions from qbit-quick-search in the global search box), [PR140](https://github.com/QRun-IO/qqq-frontend-next/pull/140) (authentik policy editor feature permissions and member preview), [PR138](https://github.cLine truncated
RC9's immutable-tag acceptance run completed with known Firefox and tablet failures; its release notes preserve the exact results. Follow [qqq#985](https://github.com/QRun-IO/qqq/issues/985) for browser investigations and [qqq#713](https://github.com/QRun-IO/qqq/issues/713) for the complete backlog. The [original Next visual review](../acceptance/next-ui-visual-review.md) and owner approval remain separate requirements.
+2
View File
@@ -1,5 +1,7 @@
# Next UI 1.0.0-RC.21 — preparation
> **RC21 was never published.** Its tag build failed the UI API off-spec ceiling test, so the Maven Central and GHCR publishes were cancelled. Its changes ship in [RC22](1.0.0-RC.22.md).
At this preparation checkpoint RC21 **is not yet published**. [RC20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.21` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.21`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
+38
View File
@@ -0,0 +1,38 @@
# Next UI 1.0.0-RC.22 — preparation
At this preparation checkpoint RC22 **is not yet published**. [RC20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20) remains the latest verified public candidate and is immutable. RC21 was tagged but never published (its tag build failed the off-spec ceiling test and publishing was cancelled); its content is included here. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.22` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.22`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `48e81b4` (merge of [PR128](https://github.com/QRun-IO/qqq-frontend-next/pull/128)). Merged since RC20: [PR113](https://github.com/QRun-IO/qqq-frontend-next/pull/113) (collaboration record panel: comments, files, activity, tags and follow), [PR128](https://github.com/QRun-IO/qqq-frontend-next/pull/128) (collaboration: realtime presence, conflict review, sharing, tasks and My work), [PR106](https://github.com/QRun-IO/qqq-frontend-next/pull/106) (data-analysis pivot / group-by explorer), [PR118](https://github.com/QRun-IO/qqq-frontend-next/pull/118) (data-analysis compare, correlation, crosstab and shift explorers), [PR123](https://github.com/QRun-IO/qqq-frontend-next/pull/123) (data-analysis save, share, star, export and watch), [PR126](https://github.com/QRun-IO/qqq-frontend-next/pull/126) (run processes in background, background-job banner, widget refresh), [PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125) (attach a file in the assistant panel for bulk load), [PR132](https://github.com/QRun-IO/qqq-frontend-next/pull/132) (vendored alerts and sql-workbench extension documents, off-spec 46 to 21), [PR131](https://github.com/QRun-IO/qqq-frontend-next/pull/131) (off-spec ceiling for POST /uploads), [PR129](https://github.com/QRun-IO/qqq-frontend-next/pull/129) (chart data table overflow and doubled widget section headings).
## Backend requirements
QQQ **4.1.0-RC.6** or later. The collaboration screens need qbit-collaboration **0.1.0-RC.1** or later. The data-analysis explorers need qbit-data-analysis **0.1.0-RC.1** or later (extension 1.4.0). Assistant file attach uses the bulk-load UI API operations in QQQ **4.1.0-RC.7** and the tools in qbit-assistant **0.1.0-RC.10**; the upload endpoint (qqq#1181) is not yet released, so an attach attempt fails with a toast until it ships. Background process runs appear only when the server advertises `durableJobEligible` (qqq#1178 and qqq#1181); against older servers nothing new shows. RC22 does not publish QQQ 4.1.
## Features
- **Collaboration** ([PR113](https://github.com/QRun-IO/qqq-frontend-next/pull/113), [PR128](https://github.com/QRun-IO/qqq-frontend-next/pull/128)): record panel (comments, files, activity), tags and follow; realtime presence, conflict review, sharing, tasks and My work.
- **Data analysis** ([PR106](https://github.com/QRun-IO/qqq-frontend-next/pull/106), [PR118](https://github.com/QRun-IO/qqq-frontend-next/pull/118), [PR123](https://github.com/QRun-IO/qqq-frontend-next/pull/123)): pivot / group-by, compare, correlation, crosstab and shift explorers; save, share, star, export and watch.
- **Background process runs** ([PR126](https://github.com/QRun-IO/qqq-frontend-next/pull/126)): run in background, background-job banner, widget refresh.
- **Assistant file attach** ([PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125)) for assisted bulk load.
## Product fixes
- Chart data table overflow and doubled widget section headings ([PR129](https://github.com/QRun-IO/qqq-frontend-next/pull/129)).
## UI API contract
- Vendored alerts and sql-workbench extension documents; the off-spec list drops from 46 to 21 ([PR132](https://github.com/QRun-IO/qqq-frontend-next/pull/132), [PR131](https://github.com/QRun-IO/qqq-frontend-next/pull/131)).
## Bundle budget
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
## Verification checkpoint
PR checks on the RC22 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC22 GitHub release after delivery.
## Remaining RC limits
The RC20 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904). Collaboration task assignees show the user id until qbit-collaboration adds a display name.
+43
View File
@@ -0,0 +1,43 @@
# Next UI 1.0.0-RC.23 — preparation
At this preparation checkpoint RC23 **is not yet published**. [RC22](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.22) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.23` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.23`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `39b8f8b` (merge of [PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127)). Merged since RC22: [PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127) (help everywhere: help icons, long-help panel, help search, author links), [PR121](https://github.com/QRun-IO/qqq-frontend-next/pull/121) (Preferences page, notification settings drawer and section actions), [PR120](https://github.com/QRun-IO/qqq-frontend-next/pull/120) (contingent possible values across forms, bulk edit, processes and filters), [PR143](https://github.com/QRun-IO/qqq-frontend-next/pull/143) (typeahead suggestions from qbit-quick-search in the global search box), [PR140](https://github.com/QRun-IO/qqq-frontend-next/pull/140) (authentik policy editor feature permissions and member preview), [PR138](https://github.com/QRun-IO/qqq-frontend-next/pull/138) (theme defaults to the OS color scheme), [PR144](https://github.com/QRun-IO/qqq-frontend-next/pull/144) (record section tabs fold into a More menu), [PR139](https://github.com/QRun-IO/qqq-frontend-next/pull/139) (breadcrumb without inner horizontal scroll), [PR146](https://github.com/QRun-IO/qqq-frontend-next/pull/146) (app-nested URLs redirect to their canonical path), [PR137](https://github.com/QRun-IO/qqq-frontend-next/pull/137) (realtime client isolated between tests), [PR68](https://github.com/QRun-IO/qqq-frontend-next/pull/68) (rapidoc 10.1.0), [PR99](https://github.com/QRun-IO/qqq-frontend-next/pull/99) (GitHub Actions updates).
## Backend requirements
Everything in RC22 needs QQQ **4.1.0-RC.6** or later. The new screens use QQQ **4.1.0-RC.7** features: help everywhere needs core typed help and help bundles (qqq#1177, qqq#1198); the Preferences page needs `/qqq/v1/preferences` (qqq#1162); contingent possible values need qqq#1175. Against RC.6 these screens stay hidden or fall back to the previous behaviour. Search typeahead needs qbit-quick-search **1.1.0-RC.8** or later (the `suggest` endpoint); without it the global search box behaves as before. The authentik policy editor additions need qbit-authentik 0.1.0-RC.4 or later. RC23 does not publish QQQ 4.1.
## Features
- **Help everywhere** ([PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127)): help icons, a long-help panel, help search and author links, driven by core typed help.
- **Preferences** ([PR121](https://github.com/QRun-IO/qqq-frontend-next/pull/121)): Preferences page, notification settings drawer and section actions.
- **Contingent possible values** ([PR120](https://github.com/QRun-IO/qqq-frontend-next/pull/120)) across forms, bulk edit, processes and filters.
- **Search typeahead** ([PR143](https://github.com/QRun-IO/qqq-frontend-next/pull/143)) from the qbit-quick-search `suggest` endpoint.
- **Theme** ([PR138](https://github.com/QRun-IO/qqq-frontend-next/pull/138)): defaults to the OS color scheme (System / Light / Dark).
- **Authentik** ([PR140](https://github.com/QRun-IO/qqq-frontend-next/pull/140)): policy editor feature permissions and member preview.
## Product fixes
- Record section tabs fold into a More menu instead of scrolling ([PR144](https://github.com/QRun-IO/qqq-frontend-next/pull/144)).
- Breadcrumb without inner horizontal scroll ([PR139](https://github.com/QRun-IO/qqq-frontend-next/pull/139)).
- App-nested URLs redirect to their canonical path ([PR146](https://github.com/QRun-IO/qqq-frontend-next/pull/146)).
## Browser testing and infrastructure
- The realtime client is isolated between tests, which fixes the metadata dedupe flake ([PR137](https://github.com/QRun-IO/qqq-frontend-next/pull/137)).
- Dependency updates: rapidoc 10.1.0 ([PR68](https://github.com/QRun-IO/qqq-frontend-next/pull/68)), GitHub Actions ([PR99](https://github.com/QRun-IO/qqq-frontend-next/pull/99)).
## Bundle budget
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
## Verification checkpoint
PR checks on the RC23 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC23 GitHub release after delivery.
## Remaining RC limits
The RC22 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+37
View File
@@ -0,0 +1,37 @@
# Next UI 1.0.0-RC.24 — preparation
At this preparation checkpoint RC24 **is not yet published**. [RC23](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.23) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.24` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.24`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `3ab4815` (merge of [PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150)). Merged since RC23: [PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149) (record section tabs wrap instead of folding into More; tablet regression from PR144), [PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150) (empty tables explain what they are for and how rows get created), [PR148](https://github.com/QRun-IO/qqq-frontend-next/pull/148) (Full access checkbox in the authentik policy editor), [PR145](https://github.com/QRun-IO/qqq-frontend-next/pull/145) (no automatic card grids on homes that declare widgets).
## Why RC24
RC23's tag real-backend acceptance failed on two tablet shards in both attempts (REC-004 record sections by touch and keyboard, REC-019 zoned date-times, NAV-015 document title). [PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149) restores wrapping section tabs (the More menu from PR144 is removed), deflakes NAV-015 and adds tablet to the full-acceptance label.
## Backend requirements
Unchanged from RC23: QQQ **4.1.0-RC.6** or later for everything in RC22, and **4.1.0-RC.7** for help everywhere, Preferences and contingent possible values. The Full access checkbox needs qbit-authentik **0.1.0-RC.5** or later (full-access `*` statement). Empty-table help uses core typed help (QQQ 4.1.0-RC.7); against older cores the table shows the previous empty state. RC24 does not publish QQQ 4.1.
## Features
- **Empty tables explain themselves** ([PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150)): what the table is for and how rows get created, from core help.
- **Full access** ([PR148](https://github.com/QRun-IO/qqq-frontend-next/pull/148)): a Full access checkbox in the authentik policy editor.
## Product fixes
- Record section tabs wrap instead of folding into a More menu; fixes the RC23 tablet regression ([PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149)).
- Homes that declare widgets no longer get automatic card grids ([PR145](https://github.com/QRun-IO/qqq-frontend-next/pull/145)).
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC24 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC24 GitHub release after delivery.
## Remaining RC limits
The RC23 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.25 — preparation
At this preparation checkpoint RC25 **is not yet published**. [RC24](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.24) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.25` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.25`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `70594c4` (merge of [PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151)). Merged since RC24: [PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151) (assistant: never block asking, re-attach after refresh, live progress and Stop).
## Backend requirements
Unchanged from RC24. The assistant changes pair with qbit-assistant **0.1.0-RC.13** (durable turns: lease, heartbeat, cross-pod Stop); with older assistants the panel behaves as before. RC25 does not publish QQQ 4.1.
## Features
- **Assistant panel** ([PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151)): asking is never blocked while a turn runs, the panel re-attaches to a running turn after refresh, shows live progress, and Stop is always available.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC25 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC25 GitHub release after delivery.
## Remaining RC limits
The RC24 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.26 — preparation
At this preparation checkpoint RC26 **is not yet published**. [RC25](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.25) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.26` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.26`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `caa89dc` (merge of [PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142)). Merged since RC25: [PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142) (tables open with a short default column set).
## Backend requirements
Unchanged from RC25. RC26 does not publish QQQ 4.1.
## Features
- **Default list columns** ([PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142)): with no saved view, a table opens with a short default column set (primary key, then the first-tier section fields, capped at 6, skipping heavy, text, HTML, BLOB and JSON fields) instead of every field, so the grid no longer scrolls sideways. Fixes [#114](https://github.com/QRun-IO/qqq-frontend-next/issues/114) (kof22 admin UX audit, High).
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC26 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC26 GitHub release after delivery.
## Remaining RC limits
The RC25 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+29
View File
@@ -0,0 +1,29 @@
# Next UI 1.0.0-RC.27 — preparation
At this preparation checkpoint RC27 **is not yet published**. [RC26](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.26) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.27` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.27`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `427a037` (merge of [PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155)). Merged since RC26: [PR124](https://github.com/QRun-IO/qqq-frontend-next/pull/124) (secured-data UI: masked values, reveal, restricted filters, break-glass banner), [PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155) (live job view, header Jobs tray and Jobs page), [PR156](https://github.com/QRun-IO/qqq-frontend-next/pull/156) (large-table acceptance fixture declares its default columns).
## Backend requirements
Unchanged from RC26 for existing screens. The Jobs tray and live job view need QQQ **4.1.0-RC.8** (jobs UI API, UI API 1.4.0); against older cores the Jobs button stays hidden. The secured-data UI renders what QQQ **4.1.0-RC.7** field access tiers enforce and needs qbit-secured-data **0.1.0-RC.1** for reveal and break-glass. RC27 does not publish QQQ 4.1.
## Features
- **Secured-data UI** ([PR124](https://github.com/QRun-IO/qqq-frontend-next/pull/124)): masked values shown with a lock chip, reveal with reason, restricted filters, and a break-glass banner. The server masks values; the client never receives the real value.
- **Jobs** ([PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155)): a header Jobs button next to alerts, a live job view with progress, phases and log, the ability to leave a job running, and a Jobs page.
- Acceptance: the large-table fixture declares its 40 default columns after PR142 ([PR156](https://github.com/QRun-IO/qqq-frontend-next/pull/156)).
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC27 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC27 GitHub release after delivery.
## Remaining RC limits
The RC26 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.28 — preparation
At this preparation checkpoint RC28 **is not yet published**. [RC27](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.27) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.28` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.28`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `16699ae` (merge of [PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141)). Merged since RC27: [PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141) (zero off-spec UI API calls; OFF_SPEC_CEILING 0; minimum UI API 1.4.0).
## Backend requirements
**Requires QQQ 4.1.0-RC.9 or later** (UI API 1.4.0 operations; RC.8's `GET /table/{t}/apis` was shadowed by the record route, fixed in qqq#1216). RC28 does not publish QQQ 4.1.
## Features
- **Zero off-spec calls** ([PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141)): the form adjuster, table API discovery, table API documents and the remaining legacy routes now use spec'd `/qqq/v1` operations. `OFF_SPEC_CEILING` is 0.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC28 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC28 GitHub release after delivery.
## Remaining RC limits
The RC27 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.29 — preparation
At this preparation checkpoint RC29 **is not yet published**. [RC28](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.28) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.29` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.29`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `abfbbb2` (merge of [PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136)). Merged since RC28: [PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136) (EDI mapper page on the qbit-edi edi extension, all calls on spec).
## Backend requirements
Requires QQQ **4.1.0-RC.9** or later (as RC28). The EDI mapper needs qbit-edi with the `edi` UI API extension (qbit-edi#15); without it the Open Mapper action stays hidden. RC29 does not publish QQQ 4.1.
## Features
- **EDI mapper** ([PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136)): an Open Mapper action on `edi_map` records opens a mapper page built on the vendored qbit-edi `edi` UI API extension, with generated types and an API client. Every call is on spec.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC29 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC29 GitHub release after delivery.
## Remaining RC limits
The RC28 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+34
View File
@@ -0,0 +1,34 @@
# Next UI 1.0.0-RC.30 — preparation
At this preparation checkpoint RC30 **is not yet published**. [RC29](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.29) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.30` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.30`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main after [PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160), which this preparation depends on. Merged since RC29: [PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160) (optional Jam QA capture on the qbit-qa-capture `qa-capture` extension, all calls on spec).
## Backend requirements
RC30 needs QQQ **4.1.0-RC.9** or later, the same as RC29. QA capture also needs qbit-qa-capture with the `qa-capture` 1.0.0 UI API extension. Without it the UI makes no QA capture call and loads no Jam code. The vendored `qa-capture.json` must match the published QBit fragment before tagging. RC30 does not publish QQQ 4.1.
## Features
- **QA capture (Jam)** ([PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160)), shown only to users the QBit enables:
- Jam's Recording Links SDK is loaded on demand and initialized with sanitizers built from the server's rules (headers, JSON and form fields, query parameters, value patterns, dropped paths, fail closed).
- Jam receives metadata with the page context and the last request ids.
- "Report a bug" and "QA reports" are in the user menu, and a "QA mode on" badge appears in the header.
- Password, reveal and secured-data values are blurred in recordings.
- Recording Link `jam-*` parameters are kept through sign-in.
- The standalone image adds the Jam CSP origins when `QQQ_QA_CAPTURE_CSP=true`.
- Dependencies: `@jam.dev/recording-links` 0.4.0 and `@jam.dev/sdk` 1.0.1, pinned. See [QA capture](../QA-CAPTURE.md).
## Bundle budget
Report-only (owner decision 2026-10-13). PR160 measured +3.4 to +4.2 KB gzip first-load JS per route and +9.2 KB gzip total JS. The Jam chunks load only for enabled users.
## Verification checkpoint
PR checks on the RC30 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC30 GitHub release after delivery. Acceptance row SEC-050 (QA capture) is excluded pending owner approval until qbit-qa-capture has a real-backend fixture; Vitest covers it meanwhile.
## Remaining RC limits
The RC29 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.31 — preparation
At this preparation checkpoint RC31 **is not yet published**. [RC30](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.30) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.31` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.31`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `9aff198` (merge of [PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162)). Merged since RC30: [PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162) (adopt QQQ UI API 1.6.0: clientCapabilities, field validation, error schema, v1 branding).
## Backend requirements
**Requires QQQ 4.1.0-RC.15 or later** (UI API 1.6.0). RC31 does not publish QQQ 4.1.
## Features
- **UI API 1.6.0** ([PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162)): vendors the core 1.6.0 and API-aware documents and the updated qbit extensions (alerts 1.2.0, assistant 1.3.0, collaboration 1.1.0, data-analysis 1.5.0, edi 1.1.0, esb 1.1.0, quick-search 1.1.0, secured-data 1.1.0, sql-workbench 1.1.0); uses table client capabilities, server field validation, the single error schema and v1 branding. The off-spec ceiling stays 0.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC31 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC31 GitHub release after delivery.
## Remaining RC limits
The RC30 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.32 — preparation
At this preparation checkpoint RC32 **is not yet published**. [RC31](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.31) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.32` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.32`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `b24b74e` (merge of [PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165)). Merged since RC31: [PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165) (What's new page and user-menu entry, release-notes UI API 1.0.0).
## Backend requirements
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC31). What's new needs qbit-release-notes **0.1.0-RC.1** (release-notes UI API extension 1.0.0); without it the feature hides itself. RC32 does not publish QQQ 4.1.
## Features
- **What's new** ([PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165)): an `/app/whats-new` page and a user-menu entry with an unread count, on the release-notes UI API extension 1.0.0 (vendored). The page is a separate lazy route; off-spec calls stay at 0.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC32 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC32 GitHub release after delivery.
## Remaining RC limits
The RC31 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+31
View File
@@ -0,0 +1,31 @@
# Next UI 1.0.0-RC.33 — preparation
At this preparation checkpoint RC33 **is not yet published**. [RC32](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.32) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.33` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.33`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `b4b59ce` (merge of [PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170)). Merged since RC32: [PR168](https://github.com/QRun-IO/qqq-frontend-next/pull/168) (sidebar sections and favorites, one search, new header, single-key shortcut switch), [PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170) (empty assistant query results no longer crash the page).
## Backend requirements
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC32). The navigation works against any 1.x server (it infers section roles when absent); QQQ **4.1.0-RC.16** (UI API 1.7.0) adds the server-declared section roles, section apps and search keywords. RC33 does not publish QQQ 4.1.
## Features
- **Navigation** ([PR168](https://github.com/QRun-IO/qqq-frontend-next/pull/168)): sidebar sections and favorites, one search, a header with Search, Ask, Jobs, Notifications, Help and Account, a single-key shortcut switch and Shift+D.
## Product fixes
- Empty assistant query results no longer crash the page ([PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170)).
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC33 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC33 GitHub release after delivery.
## Remaining RC limits
The RC32 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.34 — preparation
At this preparation checkpoint RC34 **is not yet published**. [RC33](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.33) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.34` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.34`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `68a2b9d` (merge of [PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172)). Merged since RC33: [PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172) (layered error boundaries, zod payload repair, streaming can no longer lock the assistant panel).
## Backend requirements
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC33). The navigation works against any 1.x server (it infers section roles when absent); QQQ **4.1.0-RC.16** (UI API 1.7.0) adds the server-declared section roles, section apps and search keywords. RC34 does not publish QQQ 4.1.
## Product fixes
- **Fail soft** ([PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172)): layered error boundaries keep a failing widget, section or panel from taking down the page; assistant payloads are validated and repaired with zod; a stream can no longer lock the assistant panel. Pairs with qbit-assistant 0.1.0-RC.22 (fail-soft server, extension 1.5.0).
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC34 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC34 GitHub release after delivery.
## Remaining RC limits
The RC33 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.35 — preparation
At this preparation checkpoint RC35 **is not yet published**. [RC34](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.34) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.35` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.35`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `ba38367` (merge of [PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164)). Merged since RC34: [PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164) (profile photo uploader and avatars everywhere, UI API 1.8.0).
## Backend requirements
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC34). Profiles and avatars need QQQ **4.1.0-RC.17** (UI API 1.8.0). RC35 does not publish QQQ 4.1.
## Features
- **Profiles and avatars** ([PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164)): a profile photo uploader on the Profile section and user avatars shown throughout the UI.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC35 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC35 GitHub release after delivery.
## Remaining RC limits
The RC34 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.36 — preparation
At this preparation checkpoint RC36 **is not yet published**. [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.36` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.36`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
## Source
Main at `81a03ea` (merge of [PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175)). Merged since RC35: [PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175) (a11y: readable link text, 24px clear target, h2 widget titles, skip link focus).
## Backend requirements
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC35). Profiles and avatars need QQQ **4.1.0-RC.17** (UI API 1.8.0); against older servers avatars fall back to initials and the uploader stays hidden. RC36 does not publish QQQ 4.1.
## Product fixes
- **Accessibility** ([PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175)): link text meets 4.5:1 contrast through a new derived link color, clear buttons have 24px targets, widget titles are h2 headings, and the skip link takes focus correctly.
## Bundle budget
Report-only (owner decision 2026-10-13).
## Verification checkpoint
PR checks on the RC36 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC36 GitHub release after delivery.
## Remaining RC limits
The RC35 limits carry forward unchanged; see [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904).
+11
View File
@@ -0,0 +1,11 @@
# 1.0.0-RC.38 — prepared candidate
This candidate is not yet released. Merge and publication follow the compatible Core and Collaboration releases.
- Record file controls support previews, opening and downloading through the documented delivery APIs, including PDF previews.
- Script authoring includes the isolated TypeScript editor, server lifecycle actions and record hook controls.
- Process results support file delivery, PDF preview and ordered record lists through standard metadata.
- Record queries respect authorization-filtered pagination; record layouts preserve section metadata.
- The current permission editor and companion Tools workspace remain integrated.
The release uses the standard UI API and extension documents. Worker execution, storage configuration and printer/scanner qualification remain responsibilities of the respective QBits and host application. See [verification and release status](../QBIT-RELEASE-STATUS.md) and [known requirements](../QBIT-KNOWN-ISSUES.md).
+19 -3
View File
@@ -1,14 +1,30 @@
# Next UI — unreleased
[RC21 preparation](1.0.0-RC.21.md) collects every change merged since [RC20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20). RC20 is published and immutable. Changes merged after the RC21 preparation commit are listed below. Central tracking: [qqq#713](https://github.com/QRun-IO/qqq/issues/713), [browser investigations #985](https://github.com/QRun-IO/qqq/issues/985), and [QQQ Roadmap Project 12](https://github.com/orgs/QRun-IO/projects/12).
[RC36 preparation](1.0.0-RC.36.md) collects every change merged since [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35). RC35 is published and immutable. Changes merged after the RC36 preparation commit are listed below. Central tracking: [qqq#713](https://github.com/QRun-IO/qqq/issues/713), [browser investigations #985](https://github.com/QRun-IO/qqq/issues/985), and [QQQ Roadmap Project 12](https://github.com/orgs/QRun-IO/projects/12).
## Features
None yet.
- **Tools:** the `QQQ_TOOLS` widget provides a shared setup workspace for CLI/TUI, mobile scanners and Print Client, with platform-specific install commands, copyable scripts and instance connection details. An account-menu shortcut follows visible Tools metadata. Requires the Tools Qbit; existing device enrollment and permissions remain unchanged.
- **UI API 1.6.0 (QQQ 4.1.0-RC.15).** Next reads the typed `clientCapabilities` of table metadata for saved views, sharing, bulk processes and audits, applies `field.validation` (required, length, inclusive and exclusive bounds, patterns) before a request, reads client-neutral `uiSettings` setting by setting with the deprecated `materialDashboard` values as fallback, and shows the process error `traceId`. The full `materialDashboardTheme` still wins over the `uiSettings.theme` subset. Error messages keep their order (`userFacingError`, `error`, then the new `message`). Audits fall back to the permission-filtered instance metadata when a table's capability is unavailable, since RC.15 marks unaudited tables unavailable although explicit audits can exist. Off-spec calls stay at 0.
- **What's new:** a paged release history with new badges, safe links and screenshots, notification deep links, and an unread count in the user menu. Uses UI API extension `release-notes` 1.0.0; requires qbit-release-notes 0.1.0-RC.1+.
- **Profile photo and avatars everywhere** (QQQ 4.1.0-RC.17, UI API 1.8.0 `avatars`):
- The core **Profile** preferences section renders generically. Its photo is a `FILE_UPLOAD` field with `valueIsUploadRef`, so you can drag and drop or choose a picture, crop it square (drag, arrow keys or the zoom slider), preview it and remove it. The cropped PNG goes to `POST /uploads`, and the ref saves with the form.
- One `UserAvatar` (photo via one batched `POST /avatars/lookup` and `GET /avatars/{avatarId}`, otherwise initials) shows people in the user menu, presence, comments, mention candidates, the people picker, activity and your own assistant messages. A missing avatar is remembered for the session, so it isn't requested again.
- **Privacy:** branding `gravatarDefault` no longer sends your email hash to Gravatar. A Gravatar appears only if you turn it on in your Profile, and the server serves it.
- Image fields take a full form row at every width. e2e coverage is `tests/e2e/profile-avatars.spec.ts`, including the 6-width light/dark screenshot matrix.
## Product fixes
None yet.
- **A preference action that connects another service takes you there.** A provider that starts an OAuth connection (Connect Slack, for example) answers with the address of the service's consent page in the action result's `message`, since the UI API has no redirect field. That address was shown as the text of a success toast: long, not clickable and gone in seconds. A message that is nothing but one absolute `https:` address is now where the browser goes, in the same tab, and is never shown. A message that only mentions an address is still a message. Plain `http:`, addresses with credentials and every other scheme are not followed.
- **Preferences says how a connection ended.** A flow that left the dashboard returns to `/app/preferences?section=<name>&result=<code>` when it succeeded or `&error=<code>` when it did not. The section now shows the outcome once, above its form, as the section's label and the code as words (`Slack: connected.`, `Slack: authorization denied.`), as a status or an alert, and takes it off the address so a reload does not repeat it. Only a fixed lowercase code is read from the address, never a sentence.
- **Links in process HTML look like links.** An `HTML` process component rendered links in the text color with no underline, so a link the backend put on a screen (a bulk load template, a "Continue to Slack" link) could not be told from the words around it. They now use the link color and an underline; a link's own inline style still wins.
- **The Ask button's activity dot sits on its icon.** While a reply was being written, the dot was placed in the bottom-right corner of the button's hit area, which has no visible edge and, from 1440 px, extends past the "Ask" label, so the dot floated under and to the right of the label. The dot and the unread badge are now positioned against the sparkles icon, at every width and with the panel open or closed. The bell and Jobs badges use the same anchor and keep their place.
- **Logout no longer races an in-flight request.** Logout ends the API client's session generation first: every request still in flight (axios and the realtime and assistant streams) is aborted, and any response of the old generation that still arrives is discarded, so none can refresh the `sessionId` cookie the logout expires or change client auth state (SEC-037, SEC-048).
- **The error banner says what failed, and no longer stacks.** The banner for a failed request used to read "Something went wrong" for every failure without an expected status, and a polling request that kept failing added one banner per attempt. It now reads `No response from the server`, `The server took too long to respond`, the server's own message, or `Request failed (HTTP 409)`, and names the request below it (`POST /qqq/v1/table/order/query`, without the query string, with ids shown as `{id}`). A repeat of the same failure updates the banner on screen; a lost connection shows one banner whichever requests met it. The 403, 404 and 5xx banners keep their wording and gain the request line; a 403 without a server message reads `Permission denied`. A cancelled request, an aborted `fetch` and a 401 show no banner. Each banner logs its status, axios code, error name and request to the console once (`[API] Request failed:`), never a message, body, header or query string.
## Browser testing and infrastructure
+22
View File
@@ -0,0 +1,22 @@
# Jobs screenshots
The mocked browser spec is `tests/e2e/jobs-tray.spec.ts`. Capture the actual UI with:
```powershell
$env:QQQ_E2E_SERVER='production'
$env:QQQ_JOBS_SCREENSHOTS='docs/screenshots/jobs-tray'
$env:QQQ_E2E_PORT='3117'
pnpm exec playwright test tests/e2e/jobs-tray.spec.ts --workers=1 --reporter=list
```
Captured 2026-10-14 from the production build (`QQQ_E2E_SERVER=production`), 1000 px tall:
| View | 390 light | 390 dark | 1440 light | 1440 dark |
| --- | --- | --- | --- | --- |
| Live job | ![](live-390-light.png) | ![](live-390-dark.png) | ![](live-1440-light.png) | ![](live-1440-dark.png) |
| Open tray | ![](tray-390-light.png) | ![](tray-390-dark.png) | ![](tray-1440-light.png) | ![](tray-1440-dark.png) |
| Jobs page | ![](page-390-light.png) | ![](page-390-dark.png) | ![](page-1440-light.png) | ![](page-1440-dark.png) |
The spec also checks horizontal overflow, popover bounds, keyboard focus restoration, axe at 1440
light, and reduced motion; its lifecycle case runs an inline process, detaches it, watches it
finish in the tray and reopens the finished view.
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"file":"q-sequential-workflow.bundle.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kDAAkD;AAClD;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gCAAgC,0CAA0C;AAC1E;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,0BAA0B;AACtD;AACA;AACA;AACA;AACA;AACA,4BAA4B,4BAA4B;AACxD;AACA;AACA;AACA;AACA;AACA,CAAC;AACkB;AACnB,mC;;;;;;;;;;;;;;AC/DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,yCAAyC,iBAAiB;AAC1D;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;;;;;ACzDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,SAAI,IAAI,SAAI;AACjC,6EAA6E,OAAO;AACpF;AACA;AACA;AACA;AACA;AACA;AACA;AACmD;AACE;AACH;AACJ;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,8BAA8B;AAC1D,yCAAyC,2DAAc;AACvD;AACA;AACA;AACA;AACA,4BAA4B,yGAAyG;AACrI,wDAAwD,2DAAc;AACtE;AACA;AACA,4BAA4B,uDAAY;AACxC;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6CAA6C,6DAAY;AACzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,4BAA4B;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kEAAkE,gBAAgB;AAClF;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,gBAAgB;AACnE;AACA;AACA;AACA;AACA;AACA;AACA,qDAAqD,gBAAgB;AACrE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA,2DAA2D,WAAW,iJAAiJ;AACvN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;AC5JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,yBAAyB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,yBAAyB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAoC,wBAAwB;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,gDAAgD;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACuB;AACxB,wC;;;;;;;;;;;;;;AC3GA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC,8CAA8C;AAC/C,6C;;;;;;;;;;;;;;AC9CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,SAAI,IAAI,SAAI;AACjC,6EAA6E,OAAO;AACpF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AAC0B;AAC3B,2C;;;;;;;;;;;;;;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;;;ACvCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACoD;AACF;AAClD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0CAA0C;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,6EAA6E;AACrG;AACA,8BAA8B,6DAAe;AAC7C;AACA,wBAAwB,6EAA6E;AACrG;AACA,8BAA8B,2DAAc;AAC5C;AACA,wBAAwB,+EAA+E;AACvG;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,gBAAgB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,gBAAgB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,gBAAgB;AACnE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACuB;AACxB,wC;;;;;;;;;;;;;;;;;;ACjHA;AACgG;AACjB;AAC/E,8BAA8B,sEAA2B,CAAC,+EAAqC;AAC/F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACALine truncated
+2
View File
@@ -20,6 +20,8 @@ const BACKEND_PREFIXES: string[] = backendPrefixes
const nextConfig: NextConfig = {
reactStrictMode: true,
// Allow acceptance servers to run without sharing the developer server's lock/cache.
distDir: process.env.QQQ_NEXT_DIST_DIR || '.next',
// Reported to the backend as frontendVersion and shown on the developer page.
env: { NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION || packageJson.version },
...(exportBuild
+45 -21
View File
@@ -1,12 +1,15 @@
{
"name": "qqq-frontend-next",
"version": "1.0.0-RC.21",
"version": "1.0.0-RC.38",
"private": true,
"packageManager": "pnpm@9.15.9",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"build:export": "QQQ_NEXT_OUTPUT=export NEXT_PUBLIC_MOCK_API=false next build",
"scripting:worker": "node scripts/build-scripting-worker.mjs",
"scripting:worker:check": "node scripts/build-scripting-worker.mjs && node scripts/check-scripting-worker.mjs",
"dev": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && next dev --turbopack",
"build": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && next build",
"build:export": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && node scripts/build-export.mjs",
"workflow:bundle": "node scripts/stage-dynamic-components.mjs",
"build:jar": "pnpm build:export && mvn -B -q clean package",
"start": "next start",
"lint": "eslint src && node scripts/check-license-headers.mjs",
@@ -22,20 +25,25 @@
"test:acceptance": "node scripts/acceptance.mjs",
"perf:budget": "node scripts/perf-budget.mjs",
"ui-api:check": "node scripts/check-ui-api-spec.mjs",
"tsc": "tsc",
"typecheck": "tsc --noEmit",
"tsc": "node node_modules/typescript/bin/tsc",
"typecheck": "node node_modules/typescript/bin/tsc --noEmit",
"prepare": "git config core.hooksPath .githooks || true",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "storybook build",
"pdf:worker": "node scripts/build-pdf-worker.mjs"
},
"dependencies": {
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
"@codemirror/lang-javascript": "6.2.5",
"@codemirror/lang-sql": "6.9.0",
"@codemirror/language": "6.11.2",
"@codemirror/lint": "6.8.5",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.1",
"@hookform/resolvers": "^5.2.2",
"@jam.dev/recording-links": "0.4.0",
"@jam.dev/sdk": "1.0.1",
"@lezer/highlight": "1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
@@ -52,11 +60,12 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dompurify": "^3.4.16",
"dompurify": "^3.4.17",
"lodash": "^4.18.1",
"lucide-react": "^0.485.0",
"next": "16.3.8",
"rapidoc": "9.3.8",
"next": "16.4.3",
"pdfjs-dist": "6.4.299",
"rapidoc": "10.1.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.2",
@@ -68,15 +77,16 @@
"sonner": "^2.0.7",
"sql-formatter": "15.6.6",
"tailwind-merge": "^3.5.0",
"zod": "^3.25.76"
"typescript-editor": "npm:typescript@6.0.3",
"zod": "^4.6.7"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.1",
"@eslint/eslintrc": "^3.3.4",
"@playwright/test": "1.64.0-alpha-2026-10-01",
"@storybook/addon-docs": "10.6.0",
"@storybook/nextjs-vite": "10.6.0",
"@storybook/react": "10.6.0",
"@storybook/addon-docs": "10.6.2",
"@storybook/nextjs-vite": "10.6.2",
"@storybook/react": "10.6.2",
"@tailwindcss/postcss": "^4.2.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -87,11 +97,11 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^4.1.11",
"@vitest/ui": "^4.1.11",
"@vitest/coverage-v8": "^4.1.12",
"@vitest/ui": "^4.1.12",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.3",
"eslint-config-next": "16.3.6",
"eslint-config-next": "16.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^62.7.1",
"jest-axe": "^10.0.0",
@@ -99,10 +109,11 @@
"msw": "^2.12.10",
"postcss": "^8.5.28",
"prettier": "^3.8.1",
"storybook": "10.6.0",
"storybook": "10.6.2",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vitest": "^4.1.11"
"vite": "7.3.7",
"vitest": "^4.1.12"
},
"msw": {
"workerDirectory": [
@@ -111,9 +122,22 @@
},
"pnpm": {
"overrides": {
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
"@codemirror/language": "6.11.2",
"@codemirror/lint": "6.8.5",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.1",
"serialize-javascript": ">=7.0.3",
"@humanfs/node": ">=0.16.8",
"@babel/core": ">=7.29.6 <8"
}
"@babel/core": ">=7.29.6 <8",
"source-map-js": ">=1.2.2",
"follow-redirects": ">=1.16.2",
"undici@>=7 <8": ">=7.30.1 <8",
"fast-uri@>=3 <4": ">=3.1.9 <4"
},
"ignoredOptionalDependencies": [
"electron"
]
}
}
-5
View File
@@ -53,11 +53,6 @@
"marker": "search-result-card",
"gzipKB": 14.5
},
"quickSearchPanel": {
"$comment": "The quick search results panel of the header box and the '/' dialog (groups, highlights, completions, failure and fallback notices), loaded when the panel first opens: import() in GlobalSearch and SearchDialog. Measured 9.4 KB gzip at introduction (#59); budget about 8% above.",
"marker": "search-results-status",
"gzipKB": 10.2
},
"commandRecordResults": {
"$comment": "The command palette's record groups, loaded when the palette searches records: import() in CommandMenu. Measured 7.5 KB gzip at introduction (#59); budget about 8% above.",
"marker": "command-menu-searching",
+2 -2
View File
@@ -10,7 +10,7 @@ const port = Number(process.env.QQQ_E2E_PORT || 3107)
const baseURL = `http://localhost:${port}`
export default defineConfig({
testDir: './tests/e2e',
testMatch: ['collaboration-favorites.spec.ts', 'collaboration-record-panel.spec.ts'],
testMatch: ['collaboration-favorites.spec.ts', 'collaboration-record-panel.spec.ts', 'collaboration-presence-share-tasks.spec.ts'],
outputDir: './test-results/collaboration/artifacts',
fullyParallel: false,
workers: 1,
@@ -34,7 +34,7 @@ export default defineConfig({
webServer: {
command: `pnpm dev --port ${port}`,
url: baseURL,
env: { NEXT_PUBLIC_MOCK_API: 'true' },
env: { NEXT_PUBLIC_MOCK_API: 'true', QQQ_NEXT_DIST_DIR: '.next-collaboration' },
reuseExistingServer: false,
timeout: 120_000,
},
+719 -1253
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -47,7 +47,7 @@
</developers>
<properties>
<revision>1.0.0-RC.21</revision>
<revision>1.0.0-RC.38</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-09-24T00:00:00Z</project.build.outputTimestamp>
</properties>
@@ -89,8 +89,10 @@
<files>
<file>${project.basedir}/out/index.html</file>
<file>${project.basedir}/out/app/_/_/index.html</file>
<file>${project.basedir}/out/dynamic-qfmd-components/q-sequential-workflow.bundle.js</file>
<file>${project.basedir}/out/dynamic-qfmd-components/q-sequential-workflow.bundle.js.map</file>
</files>
<message>Run "QQQ_NEXT_OUTPUT=export pnpm build" (or pnpm build:jar) before packaging.</message>
<message>Run pnpm build:export (or pnpm build:jar) before packaging; the export must contain the QSequentialWorkflow editor bundle staged from dynamic-components/.</message>
</requireFilesExist>
</rules>
</configuration>
+2 -1
View File
@@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process'
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
export const AREAS = ['esb', 'navigation', 'performance', 'processes', 'query', 'records', 'security', 'widgets']
export const AREAS = ['esb', 'help', 'navigation', 'performance', 'processes', 'query', 'records', 'security', 'widgets']
export const SMOKE = [
'navigation/landing.spec.ts',
'navigation/navigation.spec.ts',
@@ -19,6 +19,7 @@ export const SMOKE = [
].map((file) => `tests/acceptance/specs/${file}`)
const sourceAreas = [
[/help|commandmenu/, 'help'],
[/widget|dashboard|report|chart|pivot/, 'widgets'],
[/query|grid|table|filter|search/, 'query'],
[/record|form|field|input|validation|attachment|preview/, 'records'],
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
// Pass build settings through the child environment on Windows and POSIX alike.
import { spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const result = spawnSync(process.execPath, [require.resolve('next/dist/bin/next'), 'build'], {
stdio: 'inherit',
env: { ...process.env, QQQ_NEXT_OUTPUT: 'export', NEXT_PUBLIC_MOCK_API: 'false' },
})
if (result.error) console.error(result.error.message)
process.exit(result.status ?? 1)
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Copy the pinned trusted PDF worker; never copy uploaded files. */
import { createRequire } from 'node:module'
import { readFile, mkdir, copyFile } from 'node:fs/promises'
import path from 'node:path'
const require = createRequire(import.meta.url)
const entry = require.resolve('pdfjs-dist/package.json')
const root = path.dirname(entry)
const pkg = JSON.parse(await readFile(entry, 'utf8'))
if (pkg.version !== '6.4.299') throw new Error('Pinned PDF.js 6.4.299 is required')
const output = new URL('../public/pdf-generated/', import.meta.url)
await mkdir(output, { recursive: true })
await copyFile(path.join(root, 'build/pdf.worker.min.mjs'), new URL('pdf-6.4.299.worker.js', output))
await copyFile(path.join(root, 'LICENSE'), new URL('LICENSE', output))
console.log('Prepared pinned PDF.js 6.4.299 worker and Apache-2.0 license')
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Build the offline editor worker from pinned trusted dependencies; never reads a script project. */
import { createRequire } from 'node:module'
import { readFile, writeFile, mkdir } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const require = createRequire(import.meta.url)
const ts = require('typescript-editor')
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
if (ts.version !== '6.0.3') throw new Error('Pinned editor TypeScript 6.0.3 is required')
const libraryDirectory = path.dirname(require.resolve('typescript-editor'))
const libraries = Object.create(null)
/** Include only the standard ES2022 library closure, never DOM, Node or project packages. */
async function includeLibrary(name) {
if (!/^lib\.[a-z0-9.]+\.d\.ts$/.test(name)) throw new Error('Invalid shipped library name')
if (libraries['/lib/' + name] !== undefined) return
const source = await readFile(path.join(libraryDirectory, name), 'utf8')
libraries['/lib/' + name] = source
for (const reference of source.matchAll(/<reference lib="([a-z0-9.]+)"\s*\/>/g)) await includeLibrary('lib.' + reference[1] + '.d.ts')
}
await includeLibrary('lib.es2022.d.ts')
const compiler = (await readFile(require.resolve('typescript-editor'), 'utf8')).replace(/\/\/# sourceMappingURL=.*$/gm, '')
const source = await readFile(path.join(root, 'src/lib/scripting/typescript-session.ts'), 'utf8')
const engine = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText
if (/require\s*\(/.test(engine)) throw new Error('Editor engine must have no runtime module imports')
const worker = compiler + '\n;(() => {\nconst exports = {};\n' + engine + '\n' +
'const session = new exports.TypeScriptSession(ts, ' + JSON.stringify(libraries) + ');\n' +
`self.onmessage = (event) => {
const id = event.data?.id;
try {
const result = session.run(event.data);
if (new TextEncoder().encode(JSON.stringify(result)).length > 262144) throw new Error("Result limit");
self.postMessage({ protocol: "qqq-typescript-ide/1", id, version: "6.0.3", result });
} catch {
self.postMessage({ protocol: "qqq-typescript-ide/1", id, version: "6.0.3", error: "Editor analysis could not be completed" });
}
};\n})();\n`
const destination = path.join(root, 'public/scripting-generated')
await mkdir(destination, { recursive: true })
await writeFile(path.join(destination, 'typescript-6.0.3.worker.js'), worker)
await writeFile(path.join(destination, 'LICENSE.txt'), await readFile(path.join(libraryDirectory, '../LICENSE.txt'), 'utf8'))
await writeFile(path.join(destination, 'ThirdPartyNoticeText.txt'), await readFile(path.join(libraryDirectory, '../ThirdPartyNoticeText.txt'), 'utf8'))
console.log('Generated offline TypeScript 6.0.3 worker (' + Buffer.byteLength(worker) + ' bytes, ' + Object.keys(libraries).length + ' standard libraries)')
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Real Chromium smoke test for the generated offline worker, served only on loopback. */
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import http from 'node:http'
import { chromium } from '@playwright/test'
const workerBytes = await readFile(new URL('../public/scripting-generated/typescript-6.0.3.worker.js', import.meta.url))
const server = http.createServer((request, response) => {
if (request.url === '/worker.js') {
response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-store' }); response.end(workerBytes)
} else if (request.url === '/') {
response.writeHead(200, { 'Content-Type': 'text/html', 'Content-Security-Policy': "default-src 'none'; script-src 'self'; worker-src 'self'; connect-src 'none'" })
response.end('<!doctype html><title>Offline TypeScript fixture</title>')
} else { response.writeHead(404); response.end() }
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
let browser
try {
browser = await chromium.launch({ headless: true })
const page = await browser.newPage()
const requested = []
page.on('request', (request) => requested.push(new URL(request.url()).pathname))
await page.goto('http://127.0.0.1:' + server.address().port)
const result = await page.evaluate(async () => {
const worker = new Worker('/worker.js')
let id = 0
const request = (operation, files, position = 0) => new Promise((resolve, reject) => {
const requestId = ++id
const timer = setTimeout(() => { worker.terminate(); reject(new Error('Worker deadline exceeded')) }, 10_000)
worker.onmessage = (event) => { clearTimeout(timer); if (event.data.id !== requestId || event.data.error) reject(new Error('Worker protocol failed')); else resolve(event.data.result) }
worker.onerror = () => { clearTimeout(timer); reject(new Error('Worker failed')) }
worker.postMessage({ id: requestId, operation, files, file: 'src/main.ts', position, declarations: '' })
})
try {
const diagnostics = await request('diagnostics', [{ path: 'src/main.ts', content: 'export const count: number = "wrong";' }])
const source = 'import { order } from "./order.js";\norder.status;\norder.'
const files = [{ path: 'src/main.ts', content: source }, { path: 'src/order.ts', content: 'export const order = { status: "READY" };' }]
const completion = await request('completion', files, source.length)
const hover = await request('hover', files, source.indexOf('status') + 1)
const definition = await request('definition', files, source.indexOf('order.status') + 1)
const formatting = await request('format', [{ path: 'src/main.ts', content: 'export const value={a:1,b:2};' }])
await request('diagnostics', [{ path: 'src/main.ts', content: 'fetch("https://example.invalid/leak"); globalThis.__qqqNeverExecute = true;' }])
return { diagnostics, completion, hover, definition, formatting, executed: globalThis.__qqqNeverExecute === true }
} finally { worker.terminate() }
})
assert(result.diagnostics.diagnostics.some((item) => item.code === 2322))
assert(result.completion.completions.some((item) => item.label === 'status'))
assert.match(result.hover.hover.text, /string/)
assert.equal(result.definition.definitions[0].file, 'src/order.ts')
assert(result.formatting.edits.length > 0)
assert.equal(result.executed, false)
assert.deepEqual(requested.sort(), ['/', '/worker.js'])
console.log('PASS real Chromium: strict diagnostics, multi-file completion/hover/definition, formatting, no source execution or external requests')
} finally {
await browser?.close()
await new Promise((resolve) => server.close(resolve))
}
+1 -1
View File
@@ -20,7 +20,7 @@ import path from 'node:path'
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete']
// operations the app works without (it checks for them), so an older server may lack them
const OPTIONAL_OPERATIONS = new Set(['GET /qqq/v1/version'])
const OPTIONAL_OPERATIONS = new Set(['GET /qqq/v1/version', 'GET /qqq/v1/help/search', 'GET /qqq/v1/help/coverage'])
const bundled = JSON.parse(readFileSync(path.resolve('src/lib/api/openapi/qqq-v1.json'), 'utf-8'))
/** @param {string} version @returns {number[] | null} */
+27
View File
@@ -0,0 +1,27 @@
{
"component": {
"repository": "QRun-IO/qqq-frontend-component-sequential-workflow",
"sourceCommit": "e6036d2a474a90d80c72bc4c5f242e666b3415f6",
"sourceTree": "9efc09210a81dc4121aa53e156443b7f4fcb6dd7",
"mainCommit": "ede4eba875ca17c5921ad4caf61a08bdff6f794d",
"build": {
"node": "v22.20.0",
"npm": "10.9.3",
"image": "node@sha256:b21fe589dfbe5cc39365d0544b9be3f1f33f55f3c86c87a76ff65a02f8f5848e",
"commands": [
"npm ci --ignore-scripts",
"npm test (existing 3 suites, 62 tests)",
"npx webpack"
],
"packageLockSha256": "737cc1a3e416d942c050076dfae36345e9ba3f62d679dec71f04c2a2382b1a21",
"qualifiedUtc": "2026-11-05T12:31Z"
},
"mainTree": "9efc09210a81dc4121aa53e156443b7f4fcb6dd7",
"mainMerge": "normal merge of PR 5 (parents a32a7b4d437f09c09b03eb8828fccfd7de4eae7a, e6036d2a474a90d80c72bc4c5f242e666b3415f6); its tree equals the qualified source tree, so the qualified assets are the canonical main build output"
},
"assetDirectory": "dynamic-components/q-sequential-workflow",
"files": {
"q-sequential-workflow.bundle.js": "9f8358b105d0c157cb32a476313a7750281549f8e9423dd7d125ceaaafe8f32d",
"q-sequential-workflow.bundle.js.map": "776c716690a357213e00d109652ab7eb9c9025073b362c995564e11ca5c5ee92"
}
}
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
// Vendor the qbit YAML as lossless JSON, like the other extension documents, then run
// node scripts/generate-edi-types.mjs. No runtime schema or generator dependency ships.
import { readFileSync, writeFileSync } from 'node:fs'
const document = JSON.parse(readFileSync(new URL('../src/lib/api/openapi/extensions/edi.json', import.meta.url), 'utf8'))
const header = readFileSync(new URL('../src/lib/api/tables.ts', import.meta.url), 'utf8').split('/**')[0]
function typeOf(schema) {
let type = schema.$ref ? schema.$ref.split('/').at(-1)
: schema.enum ? schema.enum.map(JSON.stringify).join(' | ')
: schema.type === 'array' ? `Array<${typeOf(schema.items)}>`
: schema.type === 'object' ? schema.properties
? `{\n${Object.entries(schema.properties).map(([name, value]) => ` ${name}${schema.required?.includes(name) ? '' : '?'}: ${typeOf(value)}`).join('\n')}\n}`
: `Record<string, ${typeof schema.additionalProperties === 'object' ? typeOf(schema.additionalProperties) : 'unknown'}>`
: ['integer', 'number'].includes(schema.type) ? 'number'
: ['string', 'boolean'].includes(schema.type) ? schema.type : 'unknown'
return type + (schema.nullable && type !== 'unknown' ? ' | null' : '')
}
writeFileSync(new URL('../src/types/edi.ts', import.meta.url), header + '/** @file Generated from the vendored edi.json. Run scripts/generate-edi-types.mjs; do not edit. */\n\n' + Object.entries(document.components.schemas).map(([name, schema]) => `/** ${schema.description ?? name} */\nexport type ${name} = ${typeOf(schema)}\n`).join('\n'))
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
/**
* @file Stage the versioned, precompiled QSequentialWorkflow editor bundle (the qbit-workflows `QSequentialWorkflow` editor)
* under `public/dynamic-qfmd-components/`, which is where qbit-workflows' widget metadata points
* (`/dynamic-qfmd-components/q-sequential-workflow.bundle.js`) and where the single-origin host serves the UI from.
* The bundle and its map are tracked library assets in `dynamic-components/q-sequential-workflow/`, compiled from the
* canonical component repository (source commit and toolchain recorded in `dynamic-components.lock.json`). Nothing is
* downloaded or built here, and a file is refused unless its sha256 matches the lock. A missing or altered bundle fails the
* build, so a release can never silently omit the workflow editor.
*/
import { createHash } from 'node:crypto'
import { copyFile, mkdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const here = path.dirname(fileURLToPath(import.meta.url))
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
/** Copies every locked file from `assetDir` to `outDir`; throws on a missing file or a hash mismatch. */
export async function stageDynamicComponents({ assetDir, lock, outDir }) {
await mkdir(outDir, { recursive: true })
const staged = []
for (const [name, expected] of Object.entries(lock.files)) {
if (path.basename(name) !== name) throw new Error(`Locked file name must be a plain file name: ${name}`)
if (!/^[0-9a-f]{64}$/.test(expected ?? '')) throw new Error(`${name} has no sha256 in the dynamic component lock`)
const source = path.join(assetDir, name)
const actual = sha256(await readFile(source).catch(() => { throw new Error(`Missing ${name} in ${assetDir}`) }))
if (actual !== expected) throw new Error(`${name} sha256 ${actual} does not match the lock ${expected}`)
await copyFile(source, path.join(outDir, name))
staged.push(name)
}
return staged
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const lock = JSON.parse(await readFile(path.join(here, 'dynamic-components.lock.json'), 'utf8'))
const staged = await stageDynamicComponents({ assetDir: path.join(here, '..', lock.assetDirectory), lock, outDir: path.join(here, '../public/dynamic-qfmd-components') })
console.log(`Staged dynamic components compiled from ${lock.component.repository}@${lock.component.sourceCommit.slice(0, 8)}: ${staged.join(', ')}`)
}
@@ -22,6 +22,7 @@
'use client'
import React from 'react'
import dynamic from 'next/dynamic'
import { useQuery } from '@tanstack/react-query'
import { useRouteParams } from '@/lib/hooks/use-route-params'
@@ -32,6 +33,10 @@ import { launchTableName } from '@/lib/utils/process-utils'
import { NotFoundState } from '@/components/layout/NotFoundState'
import { RouteRedirect } from '@/components/layout/RouteRedirect'
const EdiMapperPage = dynamic(() => import('@/components/edi-mapper/EdiMapperPage'), {
loading: () => <p role="status">Loading mapper…</p>,
})
/**
* Opens the named process for the record, returning to the record afterwards, or shows the
* in-shell not-found state when the segment is not one of the table's processes.
@@ -42,6 +47,8 @@ export default function RecordProcessPage() {
const { slug, recordId, action } = useRouteParams<{ slug: string; recordId: string; action: string }>()
const { data: metaData, isError } = useQuery({ queryKey: queryKeys.metadataAll(), queryFn: loadMetaData, staleTime: 1000 * 60 * 30 })
if (slug === 'edi_map' && action === 'mapper') return <EdiMapperPage key={recordId} mapId={Number(recordId)} />
if (isError) {
return <div role="alert" className="py-12 text-center text-destructive">Failed to load application metadata.</div>
}
@@ -19,7 +19,7 @@
import { render, screen, waitFor, within } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, HttpResponse } from 'msw'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type { QInstance } from '@/types'
import { QContextProvider, useQContext } from '@/lib/context/q-context'
@@ -55,7 +55,7 @@ vi.mock('@/lib/api/developer', () => ({
getTableApis: vi.fn(async () => []),
getApiVersions: vi.fn(),
getApiTableSpec: vi.fn(),
apiTableSpecUrl: () => '/api/v1/person/openapi.json',
apiTableSpecUrl: () => '/qqq/v1/table/person/apis/siteApi/v1/openapi',
apiMiddlewareBaseURL: () => '',
}))
@@ -113,6 +113,9 @@ function renderPage() {
}
describe('RecordDeveloperViewPage', () => {
// The page lazy-loads this view; transforming its module graph cold must not count against the first test's waits
beforeAll(async () => { await import('@/components/developer/RecordDeveloperView') }, 120_000)
beforeEach(() => {
vi.mocked(getRecord).mockResolvedValue(record)
vi.mocked(getRecordDeveloperData).mockResolvedValue({
@@ -140,7 +143,7 @@ describe('RecordDeveloperViewPage', () => {
expect(within(card).getAllByRole('tab').map((tab) => tab.textContent)).toEqual(['Code', 'Logs', 'Test', 'Docs'])
expect(screen.getByTestId('page-header')).toHaveTextContent('Ada Lovelace Developer Mode')
expect(getRecordDeveloperData).toHaveBeenCalledWith('person', '1')
})
}, 15_000) // three waits of up to 5 s each: the 5 s default could expire before they do
it('shows the raw values of the developer-mode record, not a separate record read (Material getRecordDeveloperMode)', async () => {
serveInstance([])
@@ -17,7 +17,7 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, HttpResponse } from 'msw'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { deserializeFilter } from '@/lib/utils/filter-utils'
import apiClient from '@/lib/api/client'
@@ -61,6 +61,12 @@ function renderPage() {
}
describe('RecordViewPage independent association loading', () => {
beforeAll(async () => {
// Keep cold compilation of the lazy RecordView outside the first assertion's deadline,
// so any case passes alone (-t) and under load.
await import('@/components/records/RecordView')
}, 30_000)
beforeEach(() => { localStorage.clear(); vi.restoreAllMocks(); navigation.search = 'tab=related' })
it('keeps the base and child Add, and requests no expansion, when the child table is listed without read permission (QRun-IO/qqq#671)', async () => {
@@ -59,7 +59,7 @@ const RecordView = lazy(() => import('@/components/records/RecordView').then((mo
*
* Loads the base record and optional whole-association expansion independently. Once
* the record loads it is registered in the recently-viewed history (via
* `addRecentRecord`) so it surfaces in the GlobalSearch and SearchDialog.
* `addRecentRecord`) so it surfaces in the search box (recently viewed records).
* The page header in QContext is updated to the record label.
*
* @returns A composed view that assembles:
@@ -202,6 +202,7 @@ export default function RecordViewPage() {
)}
<AlertsRecordPanelSlot metaData={metaData} tableName={slug} recordId={recordId} />
<Suspense fallback={<div role="status" aria-label="Loading record view" className="py-12 text-center">Loading record…</div>}><RecordView
requestedPrimaryKey={recordId}
instance={metaData}
tableMetaData={tableMetaData}
record={displayRecord}
@@ -217,7 +218,7 @@ export default function RecordViewPage() {
allTables={metaData?.tables}
widgetMetaDataMap={metaData?.widgets}
associationTables={associationTables}
auditSource={auditSource(metaData)}
auditSource={auditSource(metaData, tableMetaData.clientCapabilities)}
/></Suspense>
</>
)
@@ -88,7 +88,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
window.localStorage.clear()
getTableApisMock.mockReset()
getApiVersionsMock.mockReset()
getApiVersionsMock.mockImplementation(async (path: string) => versions[path])
getApiVersionsMock.mockImplementation((api) => versions[api.path])
})
it('selects the first API and its current version and embeds RapiDoc for the table spec', async () => {
@@ -108,7 +108,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
await waitFor(() => expect(container.querySelector('rapi-doc')).not.toBeNull())
const rapiDoc = container.querySelector('[data-qqq-id="table-dev-api-reference"] rapi-doc')!
expect(rapiDoc).toHaveAttribute('spec-url', '/person-api/2026.Q2/person/openapi.json')
expect(rapiDoc).toHaveAttribute('spec-url', '/qqq/v1/table/person/apis/person-api/2026.Q2/openapi?format=json')
expect(rapiDoc).toHaveAttribute('primary-color', '#6366f1')
expect(rapiDoc).toHaveAttribute('show-header', 'false')
expect(rapiDoc).toHaveAttribute('render-style', 'view')
@@ -128,7 +128,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
expect(await screen.findByLabelText('API')).toHaveValue('admin-api')
expect(await screen.findByLabelText('Version')).toHaveValue('v2')
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/admin-api/v2/person/openapi.json'))
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/qqq/v1/table/person/apis/admin-api/v2/openapi?format=json'))
})
it('falls back to the first API and the current version when the remembered ones are gone', async () => {
@@ -151,7 +151,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
expect(await screen.findByLabelText('Version')).toHaveValue('v1')
await user.selectOptions(screen.getByLabelText('Version'), 'v2')
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/admin-api/v2/person/openapi.json'))
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/qqq/v1/table/person/apis/admin-api/v2/openapi?format=json'))
expect(window.localStorage.getItem('qqq.tableDeveloperView.lastApiName')).toBe('admin-api')
expect(window.localStorage.getItem('qqq.tableDeveloperView.lastApiVersion')).toBe('v2')
})
+4 -3
View File
@@ -11,6 +11,7 @@
*/
import type { ReactNode } from 'react'
import { NestedAppRedirect } from '@/components/layout/NestedAppRedirect'
import { EXPORT_PLACEHOLDER } from '@/lib/utils/export-placeholder'
/**
@@ -23,12 +24,12 @@ export function generateStaticParams() {
}
/**
* Pass-through layout that exists to carry generateStaticParams.
* Resolves app-prefixed URLs before rendering the resource route.
*
* @param props - Layout props.
* @param props.children - Route content.
* @returns The children unchanged.
* @returns The route content or its canonical redirect.
*/
export default function Layout({ children }: { children: ReactNode }) {
return children
return <NestedAppRedirect>{children}</NestedAppRedirect>
}
@@ -17,7 +17,7 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, HttpResponse } from 'msw'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import apiClient from '@/lib/api/client'
import { QContextProvider } from '@/lib/context/q-context'
@@ -54,6 +54,8 @@ function renderPage(page: React.ReactNode) {
}
describe('Create and base edit use full metadata and actual write contracts', () => {
// The pages lazy-load the form; transforming its module graph cold must not count against the first test's waits
beforeAll(async () => { await import('@/components/forms/EntityForm') }, 120_000)
beforeEach(() => { vi.restoreAllMocks(); vi.clearAllMocks(); localStorage.clear(); fixture() })
it('edits the base without requesting denied children or replacing associations', async () => {
@@ -0,0 +1,34 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Shared access-request deep link; never fetches the referenced record. */
'use client'
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
const Request = dynamic(
() => import('@/components/collaboration/RequestAccess').then((m) => m.RequestAccessPage),
{ ssr: false }
)
/** Renders the opaque request form.
* @returns Access page.
*/
export default function AccessRequestRoute() {
return (
<Suspense>
<Request />
</Suspense>
)
}
@@ -43,6 +43,7 @@ import { loadMetaData } from '@/lib/api/metadata'
import apiClient from '@/lib/api/client'
import { queryKeys } from '@/lib/query-client'
import { cn } from '@/lib/utils/cn'
import { CreateIsolatedScript } from '@/components/scripts/CreateIsolatedScript'
/** Duration in milliseconds before the "copied!" feedback resets to the copy icon. */
const COPY_RESET_MS = 2000
@@ -252,6 +253,8 @@ export default function DeveloperPage() {
</p>
</div>
<CreateIsolatedScript />
{/* Session Info */}
<SectionCard
title="Session Info"
@@ -0,0 +1,30 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Static export placeholder for job detail routes. */
import type { ReactNode } from 'react'
import { EXPORT_PLACEHOLDER } from '@/lib/utils/export-placeholder'
/** Generates the export placeholder.
* @returns One job path.
*/
export function generateStaticParams() { return [{ jobId: EXPORT_PLACEHOLDER }] }
/** Pass-through layout.
* @param props - Children.
* @returns Children unchanged.
*/
export default function Layout({ children }: { children: ReactNode }) { return children }
@@ -0,0 +1,30 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Live job route, separate from metadata table records. */
'use client'
import { useRouteParams } from '@/lib/hooks/use-route-params'
import { JobDetailView } from '@/components/jobs/JobDetailView'
/** Renders a job by id.
* @returns Job view or invalid-id message.
*/
export default function JobRoute() {
const { jobId } = useRouteParams<{ jobId: string }>()
const id = Number(jobId)
if (!Number.isSafeInteger(id) || id <= 0) return <p role="alert">Invalid job identifier.</p>
return <div className="w-full min-w-0 rounded-xl border border-border bg-card"><JobDetailView key={id} jobId={id} /></div>
}
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Static Jobs page, taking precedence over the metadata slug route. */
'use client'
import dynamic from 'next/dynamic'
const JobsPage = dynamic(() => import('@/components/jobs/JobsPage').then((module) => module.JobsPage))
/** Renders the Jobs page.
* @returns Page content.
*/
export default function JobsRoute() { return <JobsPage /> }
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file Static My work route, loaded like the notifications page. */
'use client'
import dynamic from 'next/dynamic'
const MyWork = dynamic(() => import('@/components/collaboration/MyWork').then((m) => m.MyWork), {
ssr: false,
})
/** Renders personal work.
* @returns Lazy page.
*/
export default function MyWorkRoute() {
return <MyWork />
}
+1
View File
@@ -240,6 +240,7 @@ export default function DashboardPage() {
<Link
key={target.key}
href={`${target.path}/create`}
prefetch={false}
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-accent"
data-qqq-id={`dashboard-create-${target.key}`}
>
@@ -0,0 +1,43 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file What's new route, split from the dashboard's first load. */
'use client'
import React from 'react'
import dynamic from 'next/dynamic'
import { Loader2 } from 'lucide-react'
/** The page loads on demand, so its code stays out of every route's first load. */
const WhatsNewPage = dynamic(() => import('@/components/release-notes/WhatsNewPage').then((module) => module.WhatsNewPage), {
ssr: false,
loading: () => (
<p className="flex items-center gap-2 p-6 text-sm text-muted-foreground" role="status" data-qqq-id="whats-new-page-loading">
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
Loading release notes...
</p>
),
})
/**
* Renders the whats-new page at `/app/whats-new`.
*
* @returns The page.
*/
export default function WhatsNewRoute() {
return <WhatsNewPage />
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { resetClientErrorsForTesting, setClientErrorSink } from '@/lib/errors/client-errors'
import PageError from './error'
describe('page error boundary', () => {
afterEach(() => {
resetClientErrorsForTesting()
vi.restoreAllMocks()
})
it('reports the failure and offers Try again inside the shell', async () => {
vi.spyOn(console, 'error').mockImplementation(() => undefined)
const sink = vi.fn().mockResolvedValue(undefined)
setClientErrorSink(sink)
const reset = vi.fn()
render(<PageError error={new Error("Cannot read properties of undefined (reading 'length')")} reset={reset} />)
expect(screen.getByText('This page hit a problem')).toBeVisible()
expect(sink).toHaveBeenCalledWith(expect.objectContaining({ scope: 'page' }))
await userEvent.click(screen.getByRole('button', { name: 'Try again' }))
expect(reset).toHaveBeenCalledOnce()
})
})
+89
View File
@@ -0,0 +1,89 @@
/*
* Copyright 2026 QRun.IO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file Page error boundary — a page that throws is replaced inside the app shell, so the
* navigation, header and assistant keep working.
*/
'use client'
import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { AlertCircle, RefreshCw } from 'lucide-react'
import { clientErrorDetails, reportClientError, type ClientErrorReport } from '@/lib/errors/client-errors'
/**
* Whether the error means the session ended (the root boundary sends the user to log in).
*
* @param error - The thrown error.
* @returns True for 401 / unauthorized / session expired.
*/
function isSessionError(error: Error): boolean {
const message = error.message?.toLowerCase() ?? ''
return message.includes('401') || message.includes('unauthorized') || message.includes('session expired')
}
/**
* The page-level fallback, rendered by Next.js inside the dashboard layout.
*
* @param props - The Next.js error boundary props.
* @param props.error - The thrown error.
* @param props.reset - Renders the page again.
* @returns A card with Try again, Go home and the error's details.
*/
export default function PageError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
const [report, setReport] = useState<ClientErrorReport | null>(null)
const sessionExpired = isSessionError(error)
useEffect(() => {
if (!sessionExpired) setReport(reportClientError(error, { scope: 'page' }))
}, [error, sessionExpired])
// the root boundary owns the "Session Expired" screen
if (sessionExpired) throw error
return (
<div className="mx-auto max-w-lg rounded-xl border border-border bg-card p-6 text-center shadow-sm" role="alert" data-qqq-id="page-error">
<AlertCircle className="mx-auto mb-3 h-10 w-10 text-destructive" aria-hidden="true" />
<h1 className="text-lg font-semibold text-foreground">This page hit a problem</h1>
<p className="mt-1 text-sm text-muted-foreground">The rest of the app still works. Try again, or go to another page.</p>
<div className="mt-4 flex flex-wrap justify-center gap-3">
<button
type="button"
onClick={reset}
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:min-h-11"
data-qqq-id="button-page-try-again"
>
<RefreshCw className="h-4 w-4" aria-hidden="true" />
Try again
</button>
<Link
href="/"
className="inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:min-h-11"
data-qqq-id="link-page-go-home"
>
Go home
</Link>
</div>
{report && (
<details className="mt-4 text-left text-xs text-muted-foreground">
<summary className="cursor-pointer select-none">Details</summary>
<pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono" data-qqq-id="page-error-details">{clientErrorDetails(report)}</pre>
</details>
)}
</div>
)
}
+95 -72
View File
@@ -22,8 +22,10 @@
/** Dashboard layout — authenticated route group shell providing sidebar, header, banners, command palette, and global keyboard shortcuts. */
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { SecuredDataProvider, SecuredDataBannerSlot } from '@/components/secured-data/SecuredDataProvider'
import React, { useEffect, useLayoutEffect, useState, useCallback, useMemo, useRef } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import { useQuery } from '@tanstack/react-query'
import { useAuth } from '@/lib/auth/use-auth'
@@ -31,15 +33,17 @@ import { useQContext, QContextProvider } from '@/lib/context/q-context'
import { useAppTreeRoutes } from '@/lib/hooks/use-routes'
import { useDocumentTitle } from '@/lib/hooks/use-document-title'
import { loadMetaData } from '@/lib/api/metadata'
import { ApplicationLoading } from '@/components/layout/ApplicationLoading'
import { applyBrandingTheme } from '@/lib/theme/apply-branding'
import { skipToFirstZone } from '@/lib/utils/skip-link'
import { readMaterialTheme } from '@/lib/theme/material-theme'
import { useTheme } from '@/lib/theme/theme-provider'
import { recordAnalytics } from '@/lib/analytics'
import { useAnalytics } from '@/lib/analytics/use-analytics'
import { queryKeys } from '@/lib/query-client'
import { searchableTables } from '@/lib/utils/record-search'
import { scrollPageToTop } from '@/lib/utils/scroll-to-top'
import dynamic from 'next/dynamic'
import { Sparkles } from 'lucide-react'
import Sidebar from '@/components/layout/Sidebar'
import Header from '@/components/layout/Header'
import BannerComponent from '@/components/layout/Banner'
@@ -47,18 +51,19 @@ import BrandedHeaderBar from '@/components/layout/BrandedHeaderBar'
import CompanyFooter from '@/components/layout/CompanyFooter'
import { buildBreadcrumbs, buildDocumentTitle } from '@/components/layout/Breadcrumbs'
import { CommandMenu } from '@/components/feedback/CommandMenu'
import { SearchDialog } from '@/components/feedback/SearchDialog'
import { KeyboardShortcutsDialog } from '@/components/feedback/KeyboardShortcutsDialog'
import { AccountMenu } from '@/components/layout/AccountMenu'
import { HelpMenu, HelpMenuItems } from '@/components/layout/HelpMenu'
import { HEADER_MENU_ITEM_CLASS } from '@/components/layout/HeaderMenu'
import { singleKeyShortcutsEnabled } from '@/lib/keyboard/single-key-shortcuts'
import { useSingleKeyShortcutsSync } from '@/lib/keyboard/use-single-key-shortcuts-sync'
import { HelpPanelProvider } from '@/components/help/HelpPanelProvider'
import { AssistantProvider, useAssistantController } from '@/components/assistant/AssistantProvider'
import { AssistantHeaderButton } from '@/components/assistant/AssistantHeaderButton'
import { AssistantMount } from '@/components/assistant/AssistantMount'
import { AlertsBell, isAlertsInstalled } from '@/components/alerts/AlertsBell'
import { AlertsBell, useAlertsInstalled } from '@/components/alerts/AlertsBell'
import { NotificationBannerSlot } from '@/components/alerts/NotificationBannerSlot'
// The favorites menu checks for collaboration and loads after hydration, so neither it nor the
// collaboration hooks are in the dashboard's first load (the slot stays empty until then).
const LazyFavoritesMenu = dynamic(() => import('@/components/collaboration/FavoritesMenuSlot').then((module) => module.LazyFavoritesMenu), { ssr: false })
/**
* Inner layout that renders the full dashboard chrome.
*
@@ -70,7 +75,8 @@ const LazyFavoritesMenu = dynamic(() => import('@/components/collaboration/Favor
* - Applies the application theme (tokens, rules, customCss) and the branded header bar.
* - Configures analytics and records a page view per screen.
* - Redirects unauthenticated users to `/login`.
* - Registers global keyboard shortcuts: Cmd+K (command palette), `/` (search), `?` (help).
* - Registers global keyboard shortcuts: Cmd+K and `/` (search), `?` (keyboard shortcuts); the
* single-key ones only while the user has not turned single-key shortcuts off (WCAG 2.1.4).
* - Renders the desktop sidebar, mobile sidebar drawer, banners, header, and main content slot.
*
* @param children - The authenticated page content to render in the main slot.
@@ -85,15 +91,17 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
// Mobile sidebar drawer state
const [sidebarOpen, setSidebarOpen] = useState(false)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const helpButtonRef = useRef<HTMLButtonElement>(null)
// Command palette state
// The one search box (pages, records, actions, help, Ask): Cmd/Ctrl+K, `/`, the header field
const [commandOpen, setCommandOpen] = useState(false)
// opened from Help > Search help: scoped to help articles
const [commandHelpOnly, setCommandHelpOnly] = useState(false)
const openSearch = useCallback((helpOnly = false) => {
setCommandHelpOnly(helpOnly)
setCommandOpen(true)
}, [])
// Search dialog state (/ key)
const [searchOpen, setSearchOpen] = useState(false)
// Keyboard shortcuts help dialog state
// Keyboard shortcuts sheet (`?`, or Help > Keyboard shortcuts)
const [helpOpen, setHelpOpen] = useState(false)
// Load full application metadata
@@ -101,6 +109,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
data: metaData,
isLoading: metaLoading,
error: metaError,
refetch: reloadMetaData,
} = useQuery({
queryKey: queryKeys.metadataAll(),
queryFn: loadMetaData,
@@ -109,19 +118,28 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
})
// QQQ Assistant (qbit-assistant): hidden and inert unless its /assistant/config probe enables it
const alertsInstalled = useAlertsInstalled()
const assistant = useAssistantController(metaData, isAuthenticated)
const { enabled: assistantEnabled, toggle: toggleAssistant } = assistant
// The user's single-key shortcut setting (server Keyboard section, or this browser's choice)
useSingleKeyShortcutsSync(isAuthenticated)
// Generate sidebar routes and path map from app tree
const { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, homePath } = useAppTreeRoutes(metaData)
const { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, homePath, sectionRedirects } = useAppTreeRoutes(metaData)
// A section heading is not a page: its name as a path (an old link to a group that used to be an
// app) goes to the app that heads it, at the section
useEffect(() => {
const target = sectionRedirects[(pathname ?? '').replace(/\/+$/, '')]
if (target) router.replace(target)
}, [pathname, sectionRedirects, router])
// Every route change starts at the top of the page (Material App scrolls to 0 on each pathname)
useEffect(() => {
scrollPageToTop()
}, [pathname])
// Tables the backend record search covers for this user (none: search stays local)
const searchTables = useMemo(() => searchableTables(metaData), [metaData])
// Sync pathToLabelMap to QContext
useEffect(() => {
@@ -130,8 +148,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
}
}, [pathToLabelMap, setPathToLabelMap])
// Sync branding to QContext and inject theme
useEffect(() => {
// Install branding before the first branded shell is painted.
useLayoutEffect(() => {
if (metaData?.branding) {
setBranding(metaData.branding)
// accent colors (validated, MED-3) and the favicon
@@ -154,7 +172,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
// customCss; removed again when the dashboard unmounts (sign-out)
const { theme, setTheme } = useTheme()
const applicationTheme = useMemo(() => readMaterialTheme(metaData), [metaData])
useEffect(() => { setTheme(applicationTheme) }, [applicationTheme, setTheme])
useLayoutEffect(() => { setTheme(applicationTheme) }, [applicationTheme, setTheme])
useEffect(() => () => setTheme(null), [setTheme])
// Analytics (QRun-IO/qqq#730): off unless configured; one page view per screen
@@ -162,10 +180,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
// Sync user ID to QContext
useEffect(() => {
if (user?.email) {
setUserId(user.email)
}
}, [user?.email, setUserId])
setUserId(user?.id ?? user?.email)
}, [user?.id, user?.email, setUserId])
// Redirect to login if not authenticated
useEffect(() => {
@@ -193,18 +209,19 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
/**
* Global `keydown` handler that drives the application-level keyboard shortcuts.
*
* - Cmd+K / Ctrl+K — toggles the command palette.
* - Cmd+K / Ctrl+K — toggles the search box.
* - Cmd+J / Ctrl+J — toggles the assistant panel (when available; works in text fields too).
* - Escape — closes all overlays (command palette, search dialog, help dialog, mobile sidebar).
* - `.` — opens the command palette (only when not in a text field).
* - `/` — opens the search dialog (only when not in a text field).
* - `?` — opens the keyboard shortcuts dialog (only when not in a text field).
* - Escape — closes all overlays (search box, shortcuts sheet, mobile sidebar).
* - `/` — opens the search box (not in a text field; off with single-key shortcuts).
* - `.` — the same, kept quietly for Material habits (off with single-key shortcuts).
* - `?` — opens the keyboard shortcuts sheet (not in a text field; off with single-key shortcuts).
*
* @param e - The native DOM keyboard event.
*/
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setCommandHelpOnly(false)
setCommandOpen((prev) => !prev)
}
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'j' && assistantEnabled) {
@@ -214,46 +231,35 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
}
if (e.key === 'Escape') {
setCommandOpen(false)
setSearchOpen(false)
setHelpOpen(false)
// The navigation drawer is a modal dialog and handles its own Escape
}
// Single-key shortcuts — only when not focused in a text field
if (!isInputFocused() && !e.metaKey && !e.ctrlKey && !e.altKey) {
// Single-key shortcuts — only when not focused in a text field, and not turned off (WCAG 2.1.4)
if (!isInputFocused() && !e.metaKey && !e.ctrlKey && !e.altKey && singleKeyShortcutsEnabled()) {
if (e.key === '.') {
e.preventDefault()
recordAnalytics({ category: 'globalEvents', action: 'dotMenuKeyboardShortcut' })
setCommandOpen(true)
openSearch()
}
if (e.key === '/') {
e.preventDefault()
setSearchOpen(true)
openSearch()
}
if (e.key === '?') {
e.preventDefault()
setHelpOpen(true)
}
}
}, [isInputFocused, assistantEnabled, toggleAssistant])
}, [isInputFocused, assistantEnabled, toggleAssistant, openSearch])
useEffect(() => {
document.addEventListener('keydown', handleGlobalKeyDown)
return () => document.removeEventListener('keydown', handleGlobalKeyDown)
}, [handleGlobalKeyDown])
// Show loading state while auth initializes
if (authLoading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div
className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
role="status"
aria-label="Loading"
/>
</div>
)
}
// Use the same neutral screen through authentication and metadata loading.
if (authLoading) return <ApplicationLoading />
// Auth loaded but not authenticated — redirect handled above
if (!isAuthenticated) {
@@ -265,23 +271,28 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<p className="text-destructive">Failed to load application metadata.</p>
<p role="alert" className="text-destructive">Unable to load your workspace.</p>
<p className="mt-1 text-sm text-muted-foreground">
{metaError instanceof Error ? metaError.message : 'Unknown error'}
</p>
<button type="button" className="mt-4 rounded-md border px-4 py-2" onClick={() => { void reloadMetaData() }}>Try again</button>
</div>
</div>
)
}
if (!metaData) return <ApplicationLoading />
// One scroll container: the shell is exactly the viewport and only <main> scrolls. Both are
// positioned (`relative`) so they contain absolutely positioned descendants (`sr-only` text,
// tooltips): otherwise such an element's containing block is the viewport, `overflow-hidden`
// cannot clip it, and one near the end of a long page (the company footer's "opens in a new
// tab") makes the document taller than the window, a second scrollbar beside main's.
return (
<AssistantProvider value={assistant}>
<HelpPanelProvider instance={metaData}>
<SecuredDataProvider enabled={isAuthenticated}><AssistantProvider value={assistant}>
<div className="relative flex h-screen flex-col overflow-hidden bg-background" data-qqq-id="app-shell">
<SecuredDataBannerSlot />
{/* Top-of-site banner spans the whole window, above the sidebar */}
<BannerComponent banners={metaData?.branding?.banners} slot="QFMD_TOP_OF_SITE" className="border-x-0 border-t-0" />
<BrandedHeaderBar theme={theme} />
@@ -291,6 +302,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[9999] focus:inline-flex focus:items-center pointer-coarse:focus:min-h-11 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
data-qqq-id="skip-to-content"
onClick={(event) => { if (skipToFirstZone()) event.preventDefault() }}
>
Skip to main content
</a>
@@ -300,9 +312,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
routes={sidebarRoutes}
branding={metaData?.branding}
homePath={homePath}
logout={logout}
userName={user?.name}
userEmail={user?.email}
showFavorites={isAuthenticated}
data-qqq-id="sidebar-container"
/>
@@ -311,9 +321,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
routes={sidebarRoutes}
branding={metaData?.branding}
homePath={homePath}
logout={logout}
userName={user?.name}
userEmail={user?.email}
showFavorites={isAuthenticated}
onSearchOpen={() => openSearch()}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
returnFocusRef={menuButtonRef}
@@ -328,22 +337,37 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
onMenuOpen={() => setSidebarOpen(true)}
menuOpen={sidebarOpen}
menuButtonRef={menuButtonRef}
onSearchOpen={() => setSearchOpen(true)}
onCommandOpen={() => setCommandOpen(true)}
onHelpOpen={() => setHelpOpen(true)}
helpButtonRef={helpButtonRef}
onSearchOpen={() => openSearch()}
pathToLabelMap={pathToLabelMap}
ancestorAppMap={ancestorAppMap}
homePath={homePath}
navTargets={navTargets}
searchTables={searchTables}
assistantButton={<AssistantHeaderButton />}
favoritesButton={isAuthenticated ? <LazyFavoritesMenu /> : null}
notificationsButton={isAuthenticated && isAlertsInstalled(metaData) ? <AlertsBell metaData={metaData} /> : null}
notificationsButton={isAuthenticated && alertsInstalled ? <AlertsBell metaData={metaData} /> : null}
helpMenu={<HelpMenu onSearchHelp={() => openSearch(true)} onShortcuts={() => setHelpOpen(true)} />}
accountMenu={(
<AccountMenu
userName={user?.name}
userEmail={user?.email}
logout={logout}
compactItems={(close) => (
<>
{/* the compact header has no room for Help and Ask: they lead this menu (help stays in one place) */}
<HelpMenuItems close={close} onSearchHelp={() => openSearch(true)} onShortcuts={() => setHelpOpen(true)} />
{assistantEnabled && (
<button type="button" role="menuitem" className={HEADER_MENU_ITEM_CLASS} data-qqq-id="menu-item-ask"
onClick={() => close(toggleAssistant)}>
<Sparkles className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
Ask the assistant
</button>
)}
</>
)}
/>
)}
/>
{/* Page content */}
<main className="relative flex-1 overflow-y-auto p-6" id="main-content" data-qqq-id="main-content">
<main className="relative flex-1 overflow-y-auto p-6 focus:outline-none" id="main-content" tabIndex={-1} data-qqq-id="main-content">
{metaLoading ? (
<div
className="flex items-center justify-center py-12"
@@ -374,16 +398,15 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
onClose={() => setCommandOpen(false)}
navTargets={navTargets}
metaData={metaData}
helpOnly={commandHelpOnly}
onAskAssistant={assistantEnabled ? (text) => assistant.openWith(text) : undefined}
/>
{/* Search Dialog */}
<SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} navTargets={navTargets} searchTables={searchTables} />
{/* Keyboard Shortcuts Help Dialog */}
<KeyboardShortcutsDialog open={helpOpen} onClose={() => setHelpOpen(false)} returnFocusRef={helpButtonRef} assistantEnabled={assistantEnabled} />
{/* Keyboard Shortcuts Sheet */}
<KeyboardShortcutsDialog open={helpOpen} onClose={() => setHelpOpen(false)} assistantEnabled={assistantEnabled} />
</div>
</AssistantProvider>
</AssistantProvider></SecuredDataProvider>
</HelpPanelProvider>
)
}
+2
View File
@@ -29,6 +29,7 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Providers from './providers'
import { themeScript } from '@/lib/theme/theme-script'
import '@/styles/globals.css'
/** Inter font configuration with the CSS variable `--font-inter`. */
@@ -71,6 +72,7 @@ export default function RootLayout({
return (
<html lang="en" className={inter.variable} suppressHydrationWarning>
<body className="font-sans antialiased" suppressHydrationWarning>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Providers>{children}</Providers>
</body>
</html>
+22 -19
View File
@@ -29,6 +29,7 @@
import Link from 'next/link'
import { SearchX } from 'lucide-react'
import { NestedAppRedirect } from '@/components/layout/NestedAppRedirect'
import { LegacyPathRedirect } from '@/components/layout/LegacyPathRedirect'
/**
@@ -39,26 +40,28 @@ import { LegacyPathRedirect } from '@/components/layout/LegacyPathRedirect'
export default function NotFound() {
return (
// A Material dashboard URL (nested under its apps) opens at its path here instead
<LegacyPathRedirect>
<div className="flex min-h-screen items-center justify-center bg-muted">
<div className="flex flex-col items-center gap-6 text-center">
<SearchX className="h-16 w-16 text-muted-foreground" aria-hidden="true" />
<div>
<h1 className="text-6xl font-bold text-foreground">404</h1>
<p className="mt-2 text-xl text-muted-foreground">Page not found</p>
<p className="mt-1 text-sm text-muted-foreground">
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<NestedAppRedirect>
<LegacyPathRedirect>
<div className="flex min-h-screen items-center justify-center bg-muted">
<div className="flex flex-col items-center gap-6 text-center">
<SearchX className="h-16 w-16 text-muted-foreground" aria-hidden="true" />
<div>
<h1 className="text-6xl font-bold text-foreground">404</h1>
<p className="mt-2 text-xl text-muted-foreground">Page not found</p>
<p className="mt-1 text-sm text-muted-foreground">
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
</div>
<Link
href="/"
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 pointer-coarse:min-h-11 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
data-qqq-id="link-go-home"
>
Go Home
</Link>
</div>
<Link
href="/"
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 pointer-coarse:min-h-11 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
data-qqq-id="link-go-home"
>
Go Home
</Link>
</div>
</div>
</LegacyPathRedirect>
</LegacyPathRedirect>
</NestedAppRedirect>
)
}
+6 -1
View File
@@ -31,6 +31,7 @@ import { AuthProvider } from '@/lib/auth/auth-provider'
import { ThemeProvider } from '@/lib/theme/theme-provider'
import { injectThemeTokens } from '@/lib/theme/tokens'
import { AppToaster } from '@/components/feedback/Toast'
import { QaCaptureLoader } from '@/components/qa-capture/QaCaptureLoader'
// Inject default theme tokens on first render
if (typeof window !== 'undefined') {
@@ -79,7 +80,11 @@ export default function Providers({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
<AuthProvider>
{/* QA capture (qa-capture extension): nothing is loaded unless the server enables it for this user */}
<QaCaptureLoader />
{children}
</AuthProvider>
</ThemeProvider>
<AppToaster />
</QueryClientProvider>
+23 -28
View File
@@ -14,22 +14,21 @@
* limitations under the License.
*/
// Tests for the Alerts record panel: metadata gating (no request without the qbit's app) and the
// Tests for the Alerts record panel: extension gating (no request without the extension) and the
// panel's hidden / open / watch states. The bell, popover and banners are in
// NotificationCenter.test.tsx.
import React from 'react'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render, screen, within } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, HttpResponse } from 'msw'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { axe } from 'jest-axe'
import { uiApiVersionHandler } from '@/mocks/ui-api-version'
import { server } from '@/mocks/node'
import type { AlertsRecordPanel } from '@/types/alerts'
import type { QInstance } from '@/types/metadata'
import { isAlertsInstalled } from './AlertsBell'
import { AlertsRecordPanelSlot } from './AlertsRecordPanelSlot'
const router = vi.hoisted(() => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn(), prefetch: vi.fn() }))
@@ -45,10 +44,10 @@ const SUMMARY = { unreadCount: 0, highestUnreadSeverity: null, notificationsHref
const PANEL: AlertsRecordPanel = {
visible: true,
openCount: 1,
open: [{ alertId: 9, title: 'Post has no excerpt', severity: 'WARNING', status: 'OPEN', group: 'Content', age: '1 d', href: '/app/alerts_alert/9' }],
open: [{ alertId: 9, title: 'Post has no excerpt', severity: 'WARNING', status: 'OPEN', group: 'Content', age: '1 d', href: '/app/alerts_alert/9', actions: [{ name: 'ack', label: 'Acknowledge', href: '/app/customAck?record=9' }, { name: 'later', label: 'Snooze', href: '/app/customSnooze/9' }, { name: 'api', label: 'API only', operationId: 'customOperation' }] }],
resolvedLast30Days: 2,
viewAllHref: '/app/alerts_alert?filter=x',
canManage: true,
canManage: false,
watching: false,
}
@@ -69,16 +68,11 @@ function renderWithClient(ui: React.ReactElement) {
return render(<QueryClientProvider client={client}>{ui}</QueryClientProvider>)
}
describe('isAlertsInstalled', () => {
it('is true only when the metadata lists the alerts app', () => {
expect(isAlertsInstalled(WITH_ALERTS)).toBe(true)
expect(isAlertsInstalled(WITHOUT_ALERTS)).toBe(false)
expect(isAlertsInstalled(undefined)).toBe(false)
})
})
beforeEach(() => server.use(uiApiVersionHandler('alerts')))
describe('AlertsRecordPanelSlot', () => {
it('sends no request when the metadata does not announce Alerts', async () => {
it('sends no request when the version does not announce Alerts', async () => {
server.use(uiApiVersionHandler())
let calls = 0
server.use(
http.get('/qqq/v1/alerts/inbox/summary', () => {
@@ -123,8 +117,7 @@ describe('AlertsRecordPanelSlot', () => {
expect(none.container).toBeEmptyDOMElement()
})
it('shows open alerts with actions, recent resolutions, and watches the record', async () => {
let watched: unknown = null
it('shows open alerts with actions and recent resolutions; watching is read-only', async () => {
let panel = PANEL
server.use(
http.get('/qqq/v1/alerts/inbox/summary', () => HttpResponse.json(SUMMARY)),
@@ -133,24 +126,26 @@ describe('AlertsRecordPanelSlot', () => {
expect(url.searchParams.get('tableName')).toBe('blog_post')
expect(url.searchParams.get('recordId')).toBe('5')
return HttpResponse.json(panel)
}),
http.post('/qqq/v1/alerts/record-panel/watch', async ({ request }) => {
watched = await request.json()
panel = { ...PANEL, watching: true }
return HttpResponse.json({ watching: true, created: true })
})
)
const user = userEvent.setup()
renderWithClient(<AlertsRecordPanelSlot metaData={WITH_ALERTS} tableName="blog_post" recordId="5" />)
const view = renderWithClient(<AlertsRecordPanelSlot metaData={WITH_ALERTS} tableName="blog_post" recordId="5" />)
const section = await screen.findByRole('region', { name: 'Open alerts (1)' })
expect(await axe(section)).toHaveNoViolations()
expect(within(section).getByRole('link', { name: 'Post has no excerpt' })).toHaveAttribute('href', '/app/alerts_alert/9')
expect(within(section).getByRole('link', { name: 'Acknowledge' })).toHaveAttribute('href', '/app/alertsAcknowledge?recordsParam=recordIds&recordIds=9&tableName=alerts_alert')
expect(within(section).getByRole('link', { name: 'Snooze' })).toBeInTheDocument()
expect(within(section).getByRole('link', { name: 'Acknowledge' })).toHaveAttribute('href', '/app/customAck?record=9')
expect(within(section).getByRole('link', { name: 'Snooze' })).toHaveAttribute('href', '/app/customSnooze/9')
expect(within(section).queryByRole('link', { name: 'API only' })).not.toBeInTheDocument()
expect(within(section).getByText('2 resolved in the last 30 days')).toBeInTheDocument()
await user.click(within(section).getByRole('button', { name: 'Watch alerts for this record' }))
await waitFor(() => expect(watched).toEqual({ tableName: 'blog_post', recordId: '5' }))
expect(await within(section).findByText("You watch this record's alerts")).toBeInTheDocument()
// the alerts extension (1.0.0) removed the watch operation: no button, only the legacy state
expect(within(section).queryByRole('button')).not.toBeInTheDocument()
expect(within(section).queryByText("You watch this record's alerts")).not.toBeInTheDocument()
view.unmount()
panel = { ...PANEL, watching: true, open: PANEL.open.map(({ actions: _actions, ...alert }) => alert) }
renderWithClient(<AlertsRecordPanelSlot metaData={WITH_ALERTS} tableName="blog_post" recordId="5" />)
expect(await screen.findByText("You watch this record's alerts")).toBeInTheDocument()
expect(screen.queryByRole('link', { name: 'Acknowledge' })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: 'Snooze' })).not.toBeInTheDocument()
})
})
+29 -34
View File
@@ -16,10 +16,9 @@
/**
* @file AlertsBell — the header bell of the notification center (qbit-alerts). Like the QQQ
* Assistant, it is gated on instance metadata: unless the metadata lists the qbit's `alerts`
* app (which QQQ includes only when qbit-alerts is installed and the user holds `alerts.view`,
* the same permission its `/alerts/*` routes require), it renders nothing and sends no request
* at all. When enabled it polls the inbox summary every `pollSeconds` (60) while the tab is
* Assistant, it is gated on the server's registered UI API extensions. Per-user access is
* checked by the inbox summary (403/404 stops polling). It sends no inbox request
* when the extension is absent. When enabled it polls the inbox summary every `pollSeconds` (60) while the tab is
* visible (TanStack Query pauses interval refetches in a hidden tab and refetches when it shows
* again), backing off exponentially to 5 minutes after errors. The popover's code loads on
* first open, so the shell stays small.
@@ -31,28 +30,20 @@ import React, { Suspense, lazy, useRef, useState } from 'react'
import { Bell } from 'lucide-react'
import { useQuery } from '@tanstack/react-query'
import { useUiApiExtensionInstalled, UI_API_EXTENSION } from '@/lib/api/ui-api-extensions'
import { getAlertsInboxSummary } from '@/lib/api/alerts'
import { queryKeys } from '@/lib/query-client'
import type { AlertsInboxSummary } from '@/types/alerts'
import type { QInstance } from '@/types/metadata'
import { HEADER_BADGE_CLASS, HEADER_BUTTON_CLASS, HeaderIconAnchor } from '@/components/layout/HeaderMenu'
const AlertsBellPopover = lazy(() => import('./AlertsBellPopover'))
/** The app qbit-alerts publishes (`AlertsQBitConfig.APP_NAME`), gated on `alerts.view`. */
export const ALERTS_APP_NAME = 'alerts'
/**
* Whether the instance metadata announces qbit-alerts to this user: its `alerts` app is
* present. QQQ leaves an app out of the metadata when the qbit is not installed or the user
* lacks its permission (`alerts.view`, which the `/alerts/*` routes also require), so this is
* the precondition for any Alerts request.
*
* @param metaData - Instance metadata.
* @returns True when the Alerts app is present.
/** Whether the server registers the Alerts extension.
* @returns True when Alerts is installed.
*/
export function isAlertsInstalled(metaData: QInstance | undefined): boolean {
const app = metaData?.apps?.[ALERTS_APP_NAME]
return Boolean(app) && typeof app === 'object'
export function useAlertsInstalled(): boolean {
return useUiApiExtensionInstalled(UI_API_EXTENSION.alerts)
}
/** The longest wait between polls after errors (5 minutes). */
@@ -96,7 +87,7 @@ export function pollDelay(data: AlertsInboxSummary | null | undefined, failed: n
* Shared with the record panel slot and the banner slot. It never fetches (nor polls) unless
* `enabled`.
*
* @param enabled - Whether to load it: authenticated and {@link isAlertsInstalled}.
* @param enabled - Whether to load it: authenticated and {@link useAlertsInstalled}.
* @returns The TanStack Query result.
*/
export function useAlertsInbox(enabled: boolean) {
@@ -115,40 +106,44 @@ export function useAlertsInbox(enabled: boolean) {
/**
* The header bell with its unread badge (red when a CRITICAL alert is unread).
*
* @param props - `metaData`: instance metadata, which must announce Alerts ({@link isAlertsInstalled}).
* @param _props - `metaData`: retained for caller compatibility; registration comes from the version endpoint.
* @returns The bell, or null when Alerts is not available.
*/
export function AlertsBell({ metaData }: { metaData: QInstance | undefined }) {
const { data } = useAlertsInbox(isAlertsInstalled(metaData))
export function AlertsBell(_props: { metaData: QInstance | undefined }) {
const { data } = useAlertsInbox(useAlertsInstalled())
const [open, setOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
if (!data) return null
const unread = data.unreadCount
const label = unread > 0 ? `Notifications, ${unread} unread` : 'Notifications, none unread'
const critical = data.highestUnreadSeverity === 'CRITICAL'
// severity is in the name, not only in the badge's color
const label = unread > 0 ? `Notifications, ${unread} unread${critical ? ', critical' : ''}` : 'Notifications, none unread'
return (
<div className="relative">
<button
ref={buttonRef}
type="button"
onClick={() => setOpen((value) => !value)}
className="relative flex items-center justify-center rounded-lg p-2 hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className={HEADER_BUTTON_CLASS}
aria-label={label}
title="Notifications"
aria-haspopup="dialog"
aria-expanded={open}
data-qqq-id="button-notifications"
>
<Bell className="h-5 w-5 text-foreground/60" aria-hidden="true" />
{unread > 0 && (
<span
className={`absolute right-0.5 top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-semibold leading-none ${critical ? 'bg-destructive text-white' : 'bg-primary text-primary-foreground'}`}
aria-hidden="true"
data-qqq-id="alerts-unread-badge"
>
{unread > 99 ? '99+' : unread}
</span>
)}
<HeaderIconAnchor>
<Bell className="h-5 w-5" aria-hidden="true" />
{unread > 0 && (
<span
className={`${HEADER_BADGE_CLASS} ${critical ? 'bg-destructive text-white' : 'bg-primary text-primary-foreground'}`}
aria-hidden="true"
data-qqq-id="alerts-unread-badge"
>
{unread > 99 ? '99+' : unread}
</span>
)}
</HeaderIconAnchor>
</button>
<span className="sr-only" aria-live="polite" data-qqq-id="alerts-unread-announcer">{label}</span>
{open && (
Loaded 100 of 744 files, more files were not shown because too many files have changed in this diff. Show more