Compare commits

...
Author SHA1 Message Date
james.maesandCodex e97dab74f5 docs(frontend): distinguish RC37 tag and corrected CI
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-29 09:56:41 -05:00
james.maesandGPT-6 c8f79847f3 fix(ci): bound browser dependency downloads
Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2026-10-26 11:17:48 -05:00
james.maesandGPT-6 4d28309be7 docs: record verified RC37 public artifacts
Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2026-10-26 10:06:24 -05:00
james.maesandGPT-6 13abaa795d fix: patch frontend RC development tool advisories
Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2026-10-26 08:48:27 -05:00
james.maesandGPT-6 2f556afadf release: prepare RC37 collaboration frontend candidate
Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2026-10-26 08:44:24 -05:00
james.maesandCodex dce92cfba1 test: verify marking mentions read refreshes My Work
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 19:30:44 -05:00
james.maesandCodex 8d372142f8 test: wait for help results before search navigation
Retain the destination and response assertions while synchronizing the click with asynchronous help content. The failed Firefox trace shows help results moving the target during the pointer gesture.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-21 17:03:13 -05:00
james.maesandCodex 0f0ebc524f 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-10-21 16:26:33 -05:00
james.maesandCodex dad2a79824 fix: patch frontend dependencies for collaboration candidate
Update follow-redirects and Vite without unrelated package upgrades. Await the real lazy widget import in its fallback test. Full 3660-test suite, TypeScript and touched-test lint pass.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 15:56:46 -05:00
james.maesandCodex de5c1bd066 feat: integrate issued collaboration presence leases
Use server-issued presence handles, preserve drafts and unavailable states, and invalidate scoped realtime hints. Align the bundled collaboration 1.3 contract and retain focused/browser acceptance coverage. Carry forward current main CI and navigation test fixes.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 15:48:31 -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.maes 09d1c2b793 Merge remote-tracking branch 'origin/main' into feature/user-preferences 2026-10-14 07:57:19 -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 728dd2b559 Merge pull request #131 from QRun-IO/fix/off-spec-ceiling-uploads
fix(ui-api): off-spec ceiling 46 while POST /uploads waits for qqq#1181
2026-10-14 07:51:31 -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.maes bdc38f08e9 Merge pull request #118 from QRun-IO/feature/data-analysis-compare
feat(data-analysis): compare, correlation, crosstab and shift explorers
2026-10-14 07:49:24 -05:00
james.maes df9265e877 Merge pull request #106 from QRun-IO/feature/data-analysis-pivot
feat(data-analysis): pivot / group-by explorer
2026-10-14 07:49:10 -05:00
james.maesandClaude Opus 5.5 bb05d81cdb chore(ui-api): vendor the 1.3.0 core document (preferences, bulk load, realtime)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:46:59 -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.maesandClaude Opus 5.5 89c35e9f2c fix(ui-api): off-spec ceiling 46 while POST /uploads waits for qqq#1181
POST /uploads: vendor once QRun-IO/qqq#1181 ships in an RC, then lower to 45.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:16:05 -05:00
james.maes 2e43dc23d8 Merge pull request #130 from QRun-IO/feature/next-rc21-prep
chore(#712): prepare Next UI 1.0.0-RC.21
2026-10-14 07:13:24 -05:00
james.maes 5efb0782cd Merge remote-tracking branch 'origin/main' into feature/user-preferences
# Conflicts:
#	src/lib/query-client.ts
2026-10-14 07:13:10 -05:00
james.maes bac9215e32 Merge pull request #112 from QRun-IO/feature/ui-api-off-spec-ceiling
feat(ui-api): cap off-spec calls, vendor the ESB extension, CODEOWNERS (closes #107)
2026-10-14 07:09:22 -05:00
james.maes cc13b045f4 Merge pull request #113 from QRun-IO/feature/collaboration-record-panel
feat(collaboration): record panel (comments, files, activity), tags and follow
2026-10-14 07:09:09 -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.maes 62d76af238 Merge remote-tracking branch 'origin/feature/data-analysis-pivot' into feature/data-analysis-compare 2026-10-14 07:08:32 -05:00
james.maes 974407da47 Merge remote-tracking branch 'origin/main' into feature/data-analysis-pivot 2026-10-14 07:07:52 -05:00
james.maesandClaude Opus 5.5 bfe8a6e31c chore(#712): prepare Next UI 1.0.0-RC.21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:07:41 -05:00
james.maes a6c95b1ec7 Merge pull request #125 from QRun-IO/feature/assistant-file-attach
feat(assistant): attach a file in the assistant panel (bulk load)
2026-10-14 07:05:59 -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 5cafa460b0 test(acceptance): flip the playground theme live from the user menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:30:52 -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.maesandClaude Opus 5.5 db0b421678 test(assistant): list POST /uploads as an off-spec call until the UI API document is re-vendored
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:36:25 -05:00
james.maesandClaude Opus 5.5 01fb7f6965 fix(preferences): only call /preferences when the server lists the feature
Servers before QQQ's preferences release 404 on /qqq/v1/preferences; the
user menu, bell gear and Preferences page now check GET /version features
first (once per page load) and show only the device Display section there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:35:19 -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.maesandClaude Opus 5.5 1a6c126721 chore(ui-api): re-vendor the data-analysis extension document built on QQQ RC.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:31:41 -05:00
james.maes f833454c17 Merge branch 'feature/data-analysis-pivot' into feature/data-analysis-compare 2026-10-13 22:31:35 -05:00
james.maesandClaude Opus 5.5 d01d470b6e chore(ui-api): re-vendor the data-analysis extension document built on QQQ RC.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:31:31 -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.maes 0997deb12c Merge remote-tracking branch 'origin/feature/data-analysis-pivot' into feature/data-analysis-compare 2026-10-13 22:29:31 -05:00
james.maes 196c899389 Merge remote-tracking branch 'origin/main' into feature/data-analysis-pivot 2026-10-13 22:28:49 -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 63dcf7e144 feat(assistant): attach a file in the assistant panel (bulk load)
A paperclip in the composer uploads the file with POST /qqq/v1/uploads as
the user (QRun-IO/qqq#1181) and attaches its opaque ref. The file stays
on the server, in object storage. Attachment chips can be removed. Each
message carries the attachments in pageContext.attachments
({uploadRef, fileName}, at most 5), so the assistant's bulk-load tools
(QRun-IO/qbit-assistant#38) can profile, map, dry-run and load the file.

The attach button is offered only where saved bulk load profiles exist
(storeSavedBulkLoadProfile in the metadata). Attachments belong to one
conversation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:15:11 -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 35cc9da17c feat(preferences): dark mode toggle in the user menu
The old Preferences dialog let a user flip the theme without leaving the
page; the menu now keeps that as a live Dark mode switch next to the
Preferences link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:52:09 -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.maes e2e0c984ce Merge remote-tracking branch 'origin/main' into feature/user-preferences
# Conflicts:
#	src/lib/query-client.ts
#	src/mocks/handlers/index.ts
2026-10-13 21:21:23 -05:00
james.maes 60d0b0ffc7 Merge pull request #119 from QRun-IO/feature/next-rc20-prep
chore(#712): prepare Next UI 1.0.0-RC.20
2026-10-13 21:05:42 -05:00
james.maesandClaude Opus 5.5 805ce7d72c refactor(preferences): export OneTimeSecretDialog for reuse by process results
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:44:52 -05:00
james.maesandClaude Opus 5.5 690737fa46 chore: normalize line endings to LF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:43:45 -05:00
james.maesandClaude Opus 5.5 1f4a38b9ff feat(preferences): Preferences page, notification settings drawer and section actions
Renders every /qqq/v1/preferences section generically from its metadata:
scalar fields with the standard editors, row lists as a table (cards on
narrow widths), read-only reasons, partial saves, section/list/row actions
with input and confirmation dialogs, and a show-once dialog for one-time
secrets. The user menu opens /app/preferences (device display settings are
its Display section) plus any section marked showInUserMenu; the bell
popover and /app/notifications get a gear that opens the alerts section in
a drawer. Merges main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:42:59 -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
james.maesandClaude Opus 5.5 1afd72ad32 chore(#712): prepare Next UI 1.0.0-RC.20
Source is main at 438eda3 (merge of #111). Merged since RC19: #92,
#104, #109, #103, #105 and #111. The new screens need QQQ 4.1.0-RC.6;
assistant presentation blocks need qbit-assistant 0.1.0-RC.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:40:08 -05:00
james.maesandClaude Opus 5.5 7c130187b9 fix(assistant): resolve the markdown merge (GFM and breaks from main, mention schema from collaboration)
The previous merge commit left conflict markers in AssistantMarkdown.tsx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:39:25 -05:00
james.maesandClaude Opus 5.5 35de9ef8bb test(e2e): keep every collaboration spec (MSW) out of the route-mocked e2e config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:37:00 -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
james.maesandClaude Opus 5.5 1f624a2544 feat(data-analysis): compare, correlation, crosstab and shift explorers
An Explore menu on the query toolbar opens Pivot, Compare (current query
vs the whole table, a saved view or another filter, with deltas and
plain significance sentences), Correlation (an accessible heatmap with a
scatter and trend line per pair), Crosstab (counts, shares, chi-square)
and Shift over time. Layouts live in the URL, every number drills to the
records, and tables are the only horizontal scrollers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:31:46 -05:00
james.maes 438eda36e3 Merge pull request #111 from QRun-IO/feature/assistant-answer-presentation
feat(assistant): readable answers: GFM markdown and presentation blocks
2026-10-13 20:29:51 -05: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
james.maes d1c181aae9 Merge pull request #105 from QRun-IO/feature/collaboration-favorites
feat(collaboration): favorites, server recents and palette Favorites
2026-10-13 20:11:58 -05:00
james.maes c09dd23fc3 Merge pull request #103 from QRun-IO/feature/data-analysis-profiles
feat(data-analysis): column and table profile panels with drill-through
2026-10-13 20:11:32 -05:00
james.maesandClaude Opus 5.5 9b006df8b2 fix(collaboration): replace the native file picker text with a themed button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:11:01 -05:00
james.maesandClaude Opus 5.5 0ef9fa7273 test(collaboration): count CONTAINS_ALL and separate HTML blocks in the sanitizer test
OPERATOR_CONFIG now covers the 25 operators of the UI API FilterCriteria enum.
The markdown test put HTML and markdown on adjacent lines, which CommonMark
parses as one HTML block that skipHtml drops whole; blank lines end each block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:11:00 -05:00
james.maesandClaude Opus 5.5 3d30490859 fix(collaboration): make record calls resolvable against the UI API document
The contract test could not match ${recordPath(record)}/... or the computed
resolve/reopen and preview/download segments to operations of the vendored
collaboration extension. Register recordPath as a path helper and write the
two-way branches as literal calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:58 -05:00
james.maesandClaude Opus 5.5 4376c28896 feat(ui-api): cap the off-spec calls, vendor the ESB extension, CODEOWNERS (#107)
- OFF_SPEC_CEILING (45) caps OFF_SPEC_CALLS; the contract test fails if it grows.
- Vendor the qqq-esb extension document (extensions/esb.json, 1.0.0): the five
  ESB calls are now spec'd and leave the off-spec list.
- CODEOWNERS on src/lib/api/openapi/; CONTRIBUTING documents the merge order
  (extension, then vendored document, then UI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:04:50 -05:00
james.maesandClaude Opus 5.5 5fd1015b80 fix(assistant): concrete chart palette (unemitted --color-chart-1 rendered black)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:03:03 -05:00
james.maesandClaude Opus 5.5 4604312b18 feat(assistant): readable answers: GFM markdown and presentation blocks
Owner priority: an answer table rendered on one line with no links.

- Root cause: AssistantMarkdown rendered react-markdown without
  remark-gfm, so GFM tables never parsed and their lines collapsed into
  one paragraph. Add remark-gfm and remark-breaks (sanitize schema, link
  and image rules unchanged); table styling for light and dark.
- SSE: multi-line data fields keep their newlines.
- New presentation message part (qbit-assistant contract): summary,
  metrics, table with row links, lazy-loaded recharts bar/pie/line,
  filter and record links, follow-up actions; unknown kinds ignored,
  markdown fallback. Links only for internal /app hrefs.
- Tests with the owner's exact answer, split across stream deltas, and
  every block kind; Storybook example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:50:37 -05:00
james.maes 295cf16bbf Merge remote-tracking branch 'origin/main' into feature/data-analysis-profiles 2026-10-13 19:42:16 -05:00
james.maes fc5f5d946d Merge remote-tracking branch 'origin/feature/collaboration-favorites' into feature/collaboration-record-panel 2026-10-13 19:42:00 -05:00
james.maesandClaude Opus 5.5 fce8aad1ad wip(collaboration): record panel, tags and follow bell (Codex draft, tests pending)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:41:58 -05:00
james.maes 14ec5cc390 Merge remote-tracking branch 'origin/main' into feature/collaboration-favorites 2026-10-13 19:41:26 -05:00
james.maes 35249098bb Merge pull request #109 from QRun-IO/fix/acceptance-esb-extension
fix(acceptance): check the esb UI API extension instead of EsbRouteProvider
2026-10-13 19:40:24 -05:00
james.maesandClaude Opus 5.5 d4f5bf9b7b fix(acceptance): check the esb UI API extension instead of EsbRouteProvider
QRun-IO/qqq#1129 moved the /qqq/v1/esb endpoints onto spec classes registered as
the esb UI API extension and removed EsbRouteProvider, so the fixture no longer
compiled against qqq develop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:19:15 -05:00
james.maesandClaude Opus 5.5 8f7e741bac chore(ui-api): vendor the qbit-collaboration 1.0.0 extension document
Generated from QRun-IO/qbit-collaboration src/main/resources/openapi/qqq-v1/collaboration.yaml
(PR #8) for the UI API contract test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:13:39 -05:00
james.maes 77d61670c3 Merge remote-tracking branch 'origin/main' into feature/collaboration-favorites 2026-10-13 19:12:32 -05:00
james.maes 33d7d6e9c8 Merge remote-tracking branch 'origin/feature/data-analysis-profiles' into feature/data-analysis-pivot 2026-10-13 19:12:08 -05:00
james.maesandClaude Opus 5.5 dc1afc146f test(data-analysis): pivot e2e mocks the version endpoint and only the metaData root
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:12:07 -05:00
james.maes 343e603874 Merge remote-tracking branch 'origin/main' into feature/data-analysis-profiles 2026-10-13 19:09:57 -05:00
james.maes df8e27708f Merge pull request #104 from QRun-IO/fix/ui-api-contract-picker-paths
fix(ui-api): literal related-record picker paths; vendor UI API 1.2.0
2026-10-13 19:08:19 -05:00
james.maesandClaude Opus 5.5 e598437ef5 feat(data-analysis): pivot / group-by explorer
A Pivot button on the query toolbar opens a full-width explorer for the
current table and filter: rows, columns, date buckets and numeric bins,
measures (count, sum, average, min, max, distinct, percentile), totals,
Other groups, hidden small cells, a drill on every cell, and an optional
chart. The layout is in the URL (view=pivot&pivot=...), so it can be
shared and Back returns to the records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:02:52 -05:00
james.maesandClaude Opus 5.5 816dad4a37 fix(data-analysis): ask capabilities only when the server registers the extension
Apps without qbit-data-analysis no longer see a failed capabilities
request (the real-backend acceptance diagnostics flagged the 404): the UI
reads GET /version once per page load and asks capabilities only when the
data-analysis extension is registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:02:40 -05:00
james.maesandClaude Opus 5.5 277116e35e feat(collaboration): favorites, server recents and palette Favorites (record collaboration phase b)
Star toggle on record, table and app headers; a header Favorites menu
(folders, Unavailable entries, empty state, Manage link); the command
palette's Favorites group first, server-side recently viewed records with
the local fallback, and Star/Unstar this page. Everything collaboration
loads lazily and only when the qbit's UI API extension is present (GET
/version extensions, or a favorites probe on older backends). MSW mocks,
unit tests, and a Playwright suite with screenshots run on chromium,
firefox, webkit, mobile and tablet in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:45:19 -05:00
james.maesandClaude Opus 5.5 8a0fbbd334 fix(ui-api): literal related-record picker paths; vendor UI API 1.2.0
The picker (#96) and the UI API contract test (#92) crossed on main: the picker
built its paths through a helper, which the contract test reports as unlisted
computed call paths, and the vendored document predated the picker operations.
The four picker calls now use literal templates, and the vendored document is
QQQ develop's 1.2.0 (picker operations, client channels).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:43:02 -05:00
james.maesandClaude Opus 5.5 eb3ad6853c chore(ui-api): vendor the data-analysis extension document
The contract test now finds every data-analysis call in the vendored
extension document (qbit-data-analysis src/main/resources/openapi/qqq-v1/data-analysis.yaml,
as JSON); call paths are written as literals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:35:25 -05:00
james.maesandClaude Opus 5.5 b7f3c71b74 fix(data-analysis): themed chart palette, combined time series, formatted cards
Charts use the shared widget palette (theme variables were dropped by
Tailwind), the time series draws count and its moving average in one
chart with labels from the API, metric cards show the API's labels and
formatted values, Run exact appears only for estimates, and warnings
render as notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:33:44 -05:00
james.maesandClaude Opus 5.5 6316df0b4a feat(data-analysis): column and table profile panels with drill-through
When qbit-data-analysis is installed and the user can analyze a table,
the query grid offers Analyze on each analyzable column header and an
Analyze table button. The panels render the extension's typed results
(metrics, charts as data, top and distinct values, completeness,
candidate keys, duplicates), run exact analyses with progress, and every
number drills through to the filtered record list (applied in place on
the same table). Charts are recharts in a lazily loaded chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:33:44 -05:00
james.maes 455d8a587c Merge pull request #92 from QRun-IO/feature/ui-api-version-contract
feat(ui-api): UI API version check, bundled OpenAPI document and contract test
2026-10-13 18:32:30 -05:00
james.maes 88ac531e50 Merge pull request #102 from QRun-IO/feature/next-rc19-prep
chore(#712): prepare Next UI 1.0.0-RC.19
2026-10-13 17:55:31 -05:00
james.maesandClaude Opus 5.5 16a3c0797d chore(#712): prepare Next UI 1.0.0-RC.19
Source is main at a2f2318 (merge of #101). Merged since RC18: #96
(related-record picker) and #101 (assistant Read-only / Write switch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:51:25 -05:00
james.maes a2f231862c Merge pull request #101 from QRun-IO/feature/assistant-write-mode
feat(assistant): Read-only / Write switch per conversation
2026-10-13 17:50:01 -05:00
james.maes 11a8f275d4 Merge pull request #96 from QRun-IO/feature/related-record-picker
feat(picker): generic related-record picker (single, multi-add with review, link)
2026-10-13 17:44:20 -05:00
james.maesandClaude Opus 5.5 248bca9f43 test(ui-api): list the new assistant action calls as off-spec until qbit-assistant ships its extension
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:27:11 -05:00
james.maesandClaude Opus 5.5 220f106d62 Merge main into feature/ui-api-version-contract
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:26:44 -05:00
james.maesandClaude Opus 5.5 c2d0f61c15 feat(ui-api): name the client on every request (X-QQQ-Client: next-ui)
For audit attribution (QRun-IO/qqq docs/UI-API-CLIENT-CHANNELS.md); servers
that predate client channels ignore the header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:30:59 -05:00
james.maes 509ab237cf Merge pull request #100 from QRun-IO/feature/next-rc18-prep
chore(#712): prepare Next UI 1.0.0-RC.18
2026-10-13 16:30:03 -05:00
james.maesandClaude Opus 5.5 641f4718e5 feat(assistant): Read-only / Write switch per conversation
When the server offers write mode (config.features.writeMode), the panel
header shows a switch for the active conversation: Read-only (default)
or Write. It PATCHes writeMode; the server decides whether the user may
(policy, role, writable tables) and answers 403 otherwise. In Write mode
the assistant may propose changes, which the user still confirms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:28:46 -05:00
james.maesandClaude Opus 5.5 46c38f5c05 chore(#712): prepare Next UI 1.0.0-RC.18
Source is main at 0282328 (merge of #94). Merged since RC17: #91, #97,
#98, #95 and #94. The bundle budget is report-only (owner decision).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:16:32 -05:00
james.maes 0282328bb0 Merge pull request #94 from QRun-IO/feature/assistant-pending-actions
feat(assistant): confirmation card for proposed writes, and the configured persona
2026-10-13 16:08:57 -05:00
james.maesandClaude Opus 5.5 7fcb6ac301 Merge main into feature/ui-api-version-contract
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:07:57 -05:00
james.maesandClaude Opus 5.5 4b65167cb9 ci: re-run checks against qqq develop with GET /qqq/v1/version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:06:45 -05:00
james.maes 385aa693b1 Merge branch 'main' into feature/assistant-pending-actions 2026-10-13 15:31:12 -05:00
james.maes 347d314ab0 Merge pull request #95 from QRun-IO/feature/authentik-policy-editor
feat(widgets): AUTHENTIK_POLICY_EDITOR, the qbit-authentik policy editor
2026-10-13 15:30:45 -05:00
james.maesandClaude Opus 5.5 6989970011 Merge origin/main (#98 honor enforce:false in the bundle budget) into feature/related-record-picker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:26:05 -05:00
james.maesandClaude Opus 5.5 d62a63eb2e fix(ui-api): treat a non-JSON /version answer as a pre-semver server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:59:56 -05:00
james.maes 39acafc61d Merge branch 'main' into feature/authentik-policy-editor 2026-10-13 14:57:18 -05:00
james.maes 4792a5b66f Merge pull request #98 from QRun-IO/fix/perf-budget-enforce-flag
fix(perf): honor "enforce": false in the bundle budget script
2026-10-13 14:57:03 -05:00
james.maes be6217537d Merge pull request #97 from QRun-IO/feature/faster-acceptance-ci
ci: build once, shard acceptance, PR-scoped specs, quarantine WID-029
2026-10-13 14:48:15 -05:00
james.maesandClaude Opus 5.5 6d0558653f fix(perf): honor "enforce": false in the bundle budget script
#91 added "enforce": false to perf-budget.json and documented it, but
perf-budget.mjs never read the flag and still exited 1 on any breach, so
PR95 (total JS 1102.4 KB) fails the required Bundle budget check. Size
overruns are now warnings while enforce is false; structural problems
(a lazy chunk on a first load, a marker no chunk contains) still fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:34:02 -05:00
james.maesandClaude Opus 5.5 ffde7af138 fix(assistant): refresh the page's data after a confirmed write
After the user confirms a proposed write, the card invalidates the cached
queries, so the record screen behind the panel shows the saved values
without a reload (seen against the demo server: the page kept its old
meta description).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:32:17 -05:00
james.maesandClaude Opus 5.5 30b61917fc Merge origin/main (#91 report-only bundle budget) into feature/related-record-picker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:25:54 -05:00
james.maes 705defe5f6 Merge branch 'main' into feature/authentik-policy-editor 2026-10-13 14:21:33 -05:00
james.maesandClaude Opus 5.5 2b84663d7b ci: build once, shard acceptance, PR-scoped specs, quarantine WID-029
- One build job (QQQ from source, static export, dashboard and sample
  jars) feeds every shard; QQQ's Maven install is cached per commit.
- Full mode (main, v* tags, dispatch, or the full-acceptance label):
  chromium 3, firefox 3, webkit 4, mobile 4, tablet 4 shards. PRs run the
  changed areas plus a smoke set on chromium and mobile, 2 shards each.
- Shards emit blob reports; a single "acceptance gate" job merges them
  and runs the matrix gate (partial on PRs).
- WID-029 is tagged @quarantine (#93): excluded from the gate, run in a
  separate non-blocking job in full mode.
- pnpm and Playwright browser caches; Maven Central retries with backoff
  (scripts/ci/retry.sh plus resolver retry handlers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:17:34 -05:00
james.maesandClaude Opus 5.5 820f5e2e21 feat(assistant): show the configured persona in the panel
/assistant/config now returns the admin-configured profile. The panel
header and the empty state use its display name and greeting, and its
starter prompts are buttons that send the prompt. The empty state also
says the assistant sees and does only what the user can, through the same
API as the screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:12:11 -05:00
james.maesandClaude Opus 5.5 aab7acf7a6 feat(picker): generic related-record picker (single, multi-add with review, link)
One picker dialog family: PickerDialogFrame is the shared chrome, used by the
media RecordPickerDialog (no behaviour change; its tests are unchanged) and
the new RelatedRecordPickerDialog, which serves a table's
relatedRecordPickers (QQQ docs/RELATED-RECORD-PICKER.md):

- SINGLE: Browse/Change under a foreign-key field opens the picker. It lists
  the target with the picker's columns, the target's filter builder (seeded
  from the picker's resolved default filter), saved views, search, sorting,
  Load more and a preview pane. Enter, a double click or Select fills the
  field. Quick-create shows the target's quick-create section (or its
  required fields), saves through the insert route and selects the new
  record. A link opens the full create screen.
- MULTI_CHILD: the parent's child list offers the picker. It is
  multi-select (targets already on the record are marked). Next prepares one
  row per pick, the user fills the review fields (child metadata, standard
  form controls), and Add saves all or nothing.
- LINK: linked targets start checked; checking links and unchecking unlinks
  in one save.

Every call goes through the v1 relatedRecordPicker routes. The dialog is
lazy-loaded, keyboard operable (arrows, Space, Enter, Escape, which leaves
quick-create or the review first), full screen on phones, and uses theme
tokens only. Quick-create's submit never reaches a form hosting the picker.

Tests: dialog unit tests (modes, search, default filter, quick-create,
review, errors, host form, axe), utils tests, and real-backend acceptance
REC-066 to REC-068 against the sample's Sales tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:09:55 -05:00
james.maesandClaude Opus 5.5 1056674285 feat(widgets): AUTHENTIK_POLICY_EDITOR, the qbit-authentik policy editor
A lazy widget type for qbit-authentik's Edit Policy step: the app's
resources grouped by app with search, a per-resource action matrix with
select-all per row, column and app, resources the app does not define listed
for removal, and a live read-only JSON preview generated exactly as the qbit
does (canonical form, fail-closed parser, round-trip check). On a process
step it hands { policyDocument: compactJson } to the step.

Container queries keep the matrix readable in any column width. Acceptance
PRC-067 (fixture prcPolicyEditor) passes on chromium, firefox, webkit and
mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:59:49 -05:00
james.maesandClaude Opus 5.5 b6688d780b feat(assistant): confirmation card for writes the assistant proposes
The assistant's create/update/delete tools now only propose a change
(qbit-assistant pending actions). The panel renders the pending_action
payload as a card with the proposed values and Confirm / Cancel. Confirm
calls POST /assistant/actions/{id}/confirm, and the server runs the write
through the UI API with the user's own session. The card loads the
action's state, so a reloaded conversation shows the outcome. It is
code-split, so the panel's first load is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:59:18 -05:00
james.maes c63510e200 Merge pull request #91 from QRun-IO/feature/bundle-budget-report-only
build(perf): make the bundle budget report-only (owner decision)
2026-10-13 13:40:19 -05:00
james.maesandClaude Opus 5.5 01dd7e9586 feat(ui-api): UI API version check, bundled OpenAPI document and contract test
- Check the server's GET /qqq/v1/version before sign-in; a server outside the
  supported range (>=1.0.0 <2.0.0) stops with a clear message. Servers without the
  endpoint (QQQ 4.1.0-RC.5 and earlier) are the pre-semver 1.0.0 surface.
- Vendor the UI API document (src/lib/api/openapi/qqq-v1.json, 1.1.0).
- ui-api-contract.test.ts fails on any src call that is not an operation of the
  document or a listed, shrinking exception (ui-api-calls.ts), on unlisted computed
  call paths, and on stale exceptions.
- scripts/check-ui-api-spec.mjs: acceptance CI checks the bundled document against
  the QQQ checkout under test (major, newer-than-server, missing operations).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:32:55 -05:00
james.maes c7fe584911 Merge pull request #90 from QRun-IO/feature/next-rc17-prep
chore(#712): prepare Next UI 1.0.0-RC.17
2026-10-13 12:19:16 -05:00
james.maesandClaude Opus 5.5 6abfb8bdaa build(perf): make the bundle budget report-only
Owner decision 2026-10-13: "we can remove the size limits". perf-budget.json
gains "enforce": false. scripts/perf-budget.mjs still measures and prints every
route, the total, the largest chunks and the lazy chunks, writes
test-results/performance/bundle.json, and lists every size over its reference
value as a warning. It no longer fails CI. Setting enforce back to true (or
removing it) restores the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:17:42 -05:00
james.maesandClaude Opus 5.5 05479bfcdf chore(#712): prepare Next UI 1.0.0-RC.17
Source is main at 6ed2575 (merge of #89, notification center). Total JS
1098.0/1100 KB and /app first load 349.2/350 KB are within 2 KB of their
caps; the next feature needs a measured raise or a code-split pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:13:36 -05:00
james.maes 6ed257565a Merge pull request #89 from QRun-IO/feature/notification-center
feat(alerts): notification center - badge, popover, page and banners
2026-10-13 12:11:23 -05:00
james.maesandClaude Opus 5.5 9169112f18 fix(alerts): refuse open-redirect notification links
linkKind treated `/\evil.com` as an in-app path, and browsers normalize
`\` to `/`. Refuse any href with a backslash, whitespace or a control
character, or whose second character is `/` or `\`; parse it against the
page origin and treat it as in-app only when the origin matches and it
starts with a single `/`; external only for https:. Everything else is
plain text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:15:06 -05:00
james.maesandClaude Opus 5.5 03d7bae9e4 feat(alerts): notification center - badge, popover, page and banners
Turn the Alerts bell into the notification center (qbit-alerts
NOTIFICATIONS.md section 8, phase b):

- Badge: unread count (99+), red for an unread CRITICAL, labelled
  "Notifications, N unread". Polls the summary every pollSeconds, backs
  off exponentially to 5 minutes after errors, pauses while the tab is
  hidden and refetches when it shows. Still no bell and no request unless
  the metadata lists the alerts app.
- Popover (lazy): severity icon, title, source, relative time, link
  (marks read), per-item mark read/unread and dismiss, Mark all read and
  View all. Focus-trapped; Escape closes.
- /app/notifications (lazy, fixed route like /app/search): All/Unread,
  category and severity filters and paging in the URL, markdown bodies
  through MarkdownView (no HTML; relative app links now allowed), same
  actions.
- Banners: the summary's sticky items above the page until dismissed.
- Actions are optimistic on the summary and every loaded page, rolled
  back with a toast on failure, then refetched.
- Older backends (0.1.0-RC.1: alertId, group, no id) still render, with
  per-item actions hidden and read-by-alertId on open.

Bundle: /app routes +0.7 KB first load (/app/_/_ 349.2 of 350 KB); the
three lazy chunks (15.2 KB) get a notificationCenter lazyChunks budget;
total JS 1098.0 KB of 1100; CSS unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:13:12 -05:00
james.maes ef7f1c5714 Merge pull request #88 from QRun-IO/feature/next-rc16-prep
chore(#712): prepare Next UI 1.0.0-RC.16
2026-10-13 06:20:09 -05:00
james.maesandClaude Opus 5.5 42ddc06864 chore(#712): prepare Next UI 1.0.0-RC.16
Source is main at 6dea1c3 (merge of #80). Merged since RC15: #79, #84,
#86, #87, #85, #82, #81 and #80. The record picker, validation limits
and date presets need QQQ 4.1.0-RC.5; the Alerts bell needs qbit-alerts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:09:38 -05:00
james.maes 6dea1c3a86 Merge pull request #80 from QRun-IO/feature/record-picker
feat(forms): record picker for possible-value fields over large tables (thumbnails, paged dialog)
2026-10-13 06:01:41 -05:00
james.maesandClaude Sonnet 5.5 ebc734545e Merge main (#85, #82, #81) into feature/record-picker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 01:06:13 -05:00
james.maes 67e817adfa Merge pull request #81 from QRun-IO/feature/form-validation-ux
feat(forms): quiet, metadata-driven validation on record create and edit
2026-10-13 01:06:10 -05:00
james.maesandClaude Sonnet 5.5 47dcd6ff50 Merge main (#85, #82) into feature/form-validation-ux
Keeps this branch's zod-from-metadata.ts: it carries the same backend-worded range
messages as #87, plus the label-aware required, number and too-long messages.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 00:04:02 -05:00
james.maes 7720ce2fbe Merge pull request #82 from QRun-IO/feature/date-filters
feat(query): date-first filters for DATE and DATE_TIME fields (dates, presets, Include time)
2026-10-13 00:03:08 -05:00
james.maes e7ab2f8f9b Merge pull request #85 from QRun-IO/feature/alerts-bell-panel
feat(alerts): header bell and record "Open alerts" panel for the Alerts qbit
2026-10-12 23:04:44 -05:00
james.maesandClaude Sonnet 5.5 bc79580464 Merge main (#85) into feature/date-filters
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 23:04:44 -05:00
james.maesandClaude Sonnet 5.5 008aa8daf8 perf(forms): one lazy import site for the process runner and the file preview lightbox
The record picker dialog lazy-imports ProcessRun (upload) and FilePreviewLightbox, which
other screens already lazy-import. Each import site made the bundler emit its own variants of
those chunk groups: stubbing the two in the dialog saved 12 KB, and sharing one import site
for each saves 25.2 KB of total JS (1099.7 KB to 1074.5 KB on this branch, same tree).
The picker dialog itself stays a dynamic import, budgeted under lazyChunks.recordPicker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 22:18:00 -05:00
james.maesandClaude Sonnet 5.5 22648a5c79 Merge main (#87) into feature/record-picker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 22:04:01 -05:00
james.maesandClaude Sonnet 5.5 78edbdf4c3 Merge main (#87) into feature/alerts-bell-panel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 22:03:45 -05:00
james.maes 4d673edb5a Merge pull request #87 from QRun-IO/fix/range-error-backend-wording
fix(forms): word client-side range errors as the backend does
2026-10-12 22:03:28 -05:00
james.maesandClaude Sonnet 5.5 a3ad88bc6f Merge the range-error wording fix (#87) into feature/date-filters
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:49:21 -05:00
james.maesandClaude Sonnet 5.5 12b2eee0a3 fix(query): keep the quick filter operator name, and walk the specs through the date-first flow
The date quick filter's comparison select lost its "Operator for <Field>" accessible name
(it was "Filter operator"); it keeps the name the quick filters always had.

The advanced rows keep "Filter operator", but date fields now offer whole-day comparisons
("is on", "is not on") first. QRY-012, QRY-013, QRY-080 and QRY-086 follow the new flow:
"is on"/"is not on" for the equality cases, "is relative to today" (dates) or "Include time"
(date-times) to reach exact values and relative expressions, and a new date quick filter starting as
"is between". Their assertions on the rows returned are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:49:08 -05:00
james.maesandClaude Sonnet 5.5 6bc5ea875f fix(forms): backend wording for too-long values, and focus the schedule editor on a failed save
An over-long value on a field with the ERROR policy is now stopped at the form with the
backend's own sentence ("The value for <Label> is too long (max allowed length=N)").
REC-024 asserts the typing cap and sends the over-long value straight to the API to
keep covering the backend refusal.

A failed save moved focus only to fields the form renders itself, so the schedule editor
(WID-064) lost the focus it had on main; fall back to the field's own ref.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:35:18 -05:00
james.maesandClaude Sonnet 5.5 9a5acd9e9e fix(forms): word client-side range errors as the backend does
QQQ #1100 now publishes inclusive ERROR value ranges in field metadata, so the
form refuses an out-of-range number before any request and the backend
message never appeared (REC-026, REC-048, INT-007 fail on main). The client
message now reads exactly as the backend's ("The value for <Label> is too large
(maximum allowed value is N)"), so users see one text either way.

Those specs sent Bounded Value 150, which the form now blocks first. They assert
the client message for that, and exercise the real backend rejection through
Exclusive Bounded Value 0, a bound the metadata does not publish.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:32:36 -05:00
james.maesandClaude Sonnet 5.5 363f68eb40 fix(forms): keep the standard "<Field> is required" and backend range wording
Quiet validation had shortened the messages to "Required", "Enter a whole number" and
"Must be between a and b", which broke the acceptance expectations for the standard
sentences. Validation stays quiet (checked on blur or save, one line under the field),
but the text is the standard one again: "<Label> is required", "<Label> must be a
whole number", and range errors worded exactly as the backend words them ("The value
for <Label> is too large (maximum allowed value is N)"). Server messages mapped to a
field keep the backend's range sentence.

Read-only fields now render as plain values (a quieter screen), so REC-010 asserts the
value and the absence of an input instead of a disabled input. REC-026, REC-048 and
INT-007 sent Bounded Value 150, which QQQ #1100's published range makes the form refuse
first; they assert the client message for that and exercise the backend rejection
through Exclusive Bounded Value 0, a bound the metadata does not publish.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 19:49:50 -05:00
james.maesandClaude Sonnet 5.5 1187660904 fix(forms): word client-side range errors as the backend does
QQQ #1100 now publishes inclusive ERROR value ranges in field metadata, so the
form refuses an out-of-range number before any request and the backend
message never appeared. The client message now reads exactly as the
backend's ("The value for <Label> is too large (maximum allowed value is N)"),
so users see one text either way.

The REC-026, REC-048 and INT-007 acceptance tests asserted the backend
rejection by sending Bounded Value 150, which the form now blocks first. They
assert the client message for that, and exercise the real backend rejection
through Exclusive Bounded Value 0, a bound the metadata does not publish.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 19:49:30 -05:00
james.maesandClaude Opus 5.5 92bcefddb8 fix(alerts): gate the bell and record panel on the Alerts app in metadata
Merge origin/main into feature/alerts-bell-panel, then stop the bell and
the record "Open alerts" panel from probing /alerts/inbox/summary on
instances without qbit-alerts. That probe 404'd on every page against the
QQQ sample app and failed 599 real-backend acceptance tests on the
console-error guard.

Like the Assistant, the gate is now instance metadata: isAlertsInstalled
checks for the qbit's "alerts" app, which QQQ publishes only when the
qbit is installed and the user holds alerts.view (the permission its
/alerts/* routes require). qbit-alerts publishes no supplemental metadata
or frontend setting, so its app is the cleanest signal. Without it the
bell and panel render nothing and send no request or poll. The popover
and record panel stay lazy-loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:30:42 -05:00
james.maes be6ea9a723 Merge pull request #84 from QRun-IO/feature/search-preview-thumbnails
feat(search): file thumbnails, chips and dates in the record preview
2026-10-12 19:23:24 -05:00
james.maesandClaude Opus 5.5 6e7c908c06 Merge branch 'main' into feature/form-validation-ux
Takes main's budget caps (CSS 24 KB, total JS 1100 KB, owner decision
2026-10-12) instead of this branch's 1060 KB. Merged tree: CSS 23.2 KB,
total JS 1050.4 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:38:30 -05:00
james.maes ae47adc03b Merge branch 'main' into feature/date-filters 2026-10-12 18:37:24 -05:00
james.maes 15a7e43e6f Merge branch 'main' into feature/record-picker 2026-10-12 18:37:21 -05:00
james.maesandClaude Opus 5.5 8d02617aad Merge origin/main (#86 CSS budget 24 KB) into feature/search-preview-thumbnails
perf-budget.json keeps main's #79/#86 CSS note (cap 24 KB) and this
branch's preview measurement note. After the merge: total JS 1053.0 KB
(cap 1100), CSS 23.1 KB (cap 24, identical to main), preview chunk 7.1 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:17:30 -05:00
james.maes 54a67f69ef Merge branch 'main' into feature/alerts-bell-panel 2026-10-12 18:12:42 -05:00
james.maes 8ea0c89312 Merge pull request #86 from QRun-IO/feature/css-budget-24
chore(perf): CSS budget 24 KB after the resizable navigation
2026-10-12 18:12:11 -05:00
james.maesandClaude Opus 5.5 ed079d632b chore(perf): CSS budget 24 KB after the resizable navigation
Main measures 23.07 KB of shared CSS after #79, over the 23 KB cap, and
the RC.16 candidates each add a little. Raised under the owner's budget
decision of 2026-10-12, with the measurement in the comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:05:45 -05:00
james.maesandClaude Opus 5.5 ebdd4c2289 Merge origin/main (#79 resizable nav, RC.15 prep) into feature/search-preview-thumbnails
perf-budget.json: main's owner-decision note (total cap 1100 KB) and this
branch's measurement note are both kept. Measured on the static export
after the merge: total JS 1053.0 KB against main f11cc85's 1043.7 KB
(+9.3 KB, as before); route first loads and CSS identical to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:00:00 -05:00
james.maesandClaude Opus 5.5 64cd1e6dc9 feat(alerts): header bell and record "Open alerts" panel for the Alerts qbit
- AlertsBell replaces the placeholder bell: polls /alerts/inbox/summary
  every 60 s while the tab is visible, renders nothing when Alerts is not
  available (403/404/non-summary), badge red for unread CRITICAL, polite
  live announcement; the popover (lazy) lists the latest items, marks read
  on open, Mark all read, Open Alerts, focus trap and Escape
- AlertsRecordPanelSlot on the record view: only when the probe succeeds,
  never on alerts_* tables; the lazy AlertsRecordPanel shows open alerts
  (Acknowledge / Snooze links for managers), resolved in 30 days, View all
  and Watch alerts for this record; hidden when there is nothing to show
- inline styles for the severity dots and popover placement so no new CSS
  utilities ship (shared CSS stays under the 23 KB cap)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:15:10 -05:00
james.maes f11cc85685 Merge pull request #79 from QRun-IO/feature/resizable-nav
feat(layout): resizable desktop navigation (drag, keyboard, persisted width)
2026-10-12 17:03:42 -05:00
james.maes 7398f1aa01 Merge pull request #83 from QRun-IO/feature/next-rc15-prep
chore(#712): prepare Next UI 1.0.0-RC.15
2026-10-12 17:01:35 -05:00
james.maesandClaude Opus 5.5 061a7cd09b feat(search): file thumbnails, chips and dates in the record preview
The search record preview shows FILE_PREVIEW thumbnails, CHIP values and
compact dates again, as the record view does. It stays a lazy chunk and
keeps its own queries, so no route's first load grows.

Measured on the static export against main 5de69be (total JS 1042.7 KB):
total JS 1052.0 KB (+9.3 KB, the shared file-preview, gallery, adornment
and date modules re-bundled into more on-demand screen chunks), the
preview chunk 7.1 KB (was 3.7), every route first load and the CSS
(23.0 KB) unchanged. The owner approved a 1100 KB total JS cap; the
first-load and CSS caps stay. perf-budget.json records the numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:54:34 -05:00
james.maesandClaude Opus 5.5 596815c605 chore(#712): prepare Next UI 1.0.0-RC.15
Source is main at 5de69be (merge of #59). Merged since RC14: #58, #55,
#54, #77, #78, #61, #60 and #59. The total JS budget is 1100 KB (owner
decision 2026-10-12); route, CSS and lazy-chunk caps are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:49:32 -05:00
james.maes 5de69be7e3 Merge pull request #59 from QRun-IO/feature/search-ux
feat(search): search-as-you-type quick search, richer results and a URL-addressable search page
2026-10-12 16:48:24 -05:00
james.maesandClaude Opus 5.5 8880ec7974 fix(query): keep exact date values where a filter value may be a variable
A report setup lets a criterion value be a filter variable, which a whole-day range (its end is
the next day's start) cannot take, so those rows keep the exact-value inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:08:17 -05:00
james.maesandClaude Opus 5.5 df59430694 feat(query): date-first filters for DATE and DATE_TIME fields
A DATE_TIME field is now filtered by picking dates: "is on", "is between" (whole days,
inclusive), "is before/after" and "is within" a relative range (Today, Last 7 days, This month,
Last quarter, Last N days/weeks/months, ...). Dates become exact bounds in the user's time zone:
>= the local midnight that starts the first day and < the local midnight that starts the day after
the last, so there is no 23:59:59 edge and a 23 or 25 hour day around daylight saving is covered.
Relative ranges are stored as StartOfPeriod expressions (QRun-IO/qqq), so a saved view stays
relative. An unobtrusive "Include time" switch gives exact date-time pickers; older saved views
with plain datetime bounds still work and open with it on. DATE fields get the same comparisons
and presets, without the switch.

The filter builder shows a day or range as one condition, quick filter chips say it in plain
language (Created: Oct 1 - Oct 12, 2026; Created: Last 7 days), a tooltip names the time zone and
week start, and the API client sends X-QQQ-UserTimezone so the backend evaluates relative dates in
the zone the dates are shown in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:05:29 -05:00
james.maesandClaude Opus 5.5 4644b4023d chore(budget): raise the total JS cap to 1060 KB for form validation
Measured +6.6 KB gzip across all chunks (1042.7 to 1049.3 KB) with +0.1 KB on
the create and edit routes and +0.1 KB CSS; recorded in perf-budget.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:17:58 -05:00
james.maes 9842bc6554 Merge remote-tracking branch 'origin/main' into feature/form-validation-ux 2026-10-12 15:13:47 -05:00
james.maesandClaude Opus 5.5 f23e03e417 feat(forms): quiet, metadata-driven validation on record create and edit screens
Fields are checked when the user leaves them (or on save), then live while they
show an error. A refused too-long value (ValueTooLongBehavior ERROR) caps typing;
a counter appears only in the last 20% of a limit; a trimming field notes extra
characters. Whole-number and decimal inputs refuse non-numeric keys; unreadable
dates and numbers become field errors. Messages are plain (Required, Enter a
whole number, Must be 200 characters or fewer (currently 214)). Save moves focus
to the first invalid field and shows a compact summary only when others are out
of sight. The backend's record errors are matched to fields by label and shown
beside them, keeping the user's input. Read-only fields show as values. Error
lines open smoothly, are announced politely and link to the control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:13:46 -05:00
james.maesandClaude Opus 5.5 3508c505f5 Merge origin/main (#60 assistant, #61, #78) into feature/search-ux; preview fits the budget
Conflicts: CommandMenu keeps #60's Ask Assistant entry (last, and it
suppresses "No results found") and Cmd+J alongside the search group and
record groups; perf-budget.json keeps main's cssGzipKB 23, its assistant
lazy chunk and $comments, and adds the search lazy chunks.

Budget, measured on the static export against main b32a29d (total JS
1042.7 KB, CSS 22.6 KB): the merged tree measured 1065.5 KB. Bisecting
showed the record preview's imports re-bundled into more screen chunks:
without the preview 1042.5; with the useRecord/useTableMetaData hooks +12.4;
with file thumbnails and chips +9.3. The preview now runs its own two
queries (same query keys, so caches are shared) and renders values as
text, Yes/No and links; thumbnails and chips stay on the record view.

Result: total JS 1042.7 KB (budget 1050, 7.3 KB headroom, equal to main),
CSS 23.0 KB (cap 23, main 22.6); no cap raised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:12:13 -05:00
james.maes b32a29d25b Merge pull request #60 from QRun-IO/feature/assistant-panel
feat(assistant): QQQ Assistant panel, phase 1 (read-only Q&A and navigation)
2026-10-12 14:48:48 -05:00
james.maesandClaude Opus 5.5 fec62ec611 feat(forms): record picker for possible-value fields over large tables
A field whose possible value source declares a picker (field.picker, from
QQQ's QPossibleValuePicker) gets an image-aware selection and a picker
dialog built for 100,000+ records.

Inline field (PossibleValueSelect): the typeahead asks for a limited page
(20) of picker options and shows their thumbnails and subtitles. The
selection shows its thumbnail with Change/Browse, Preview (the full-size
lightbox) and View (the record, in a new tab).

Picker dialog (RecordPickerDialog, loaded by import() on Browse/Change):
- debounced server-side search
- filters generated from the picker table's field types
- the declared sorts
- grid and list views with lazy thumbnails
- infinite scroll with a Load more fallback, keyset paging (afterId) for
  the primary-key sort and offset paging otherwise, never a count
- a preview pane with detail fields
- upload-and-select through the picker's upload process (ProcessRun
  gains an onComplete callback)
- single or multiple selection
- filters, sort and view remembered per field in local storage
- arrow-key navigation by row, Enter or double-click to confirm, focus
  trap and Escape (Radix)
- full screen on phones, light and dark tokens only

Bundle: nothing added to any route's first load or to the shared CSS.
Total JS grows 2.5 KB (main 1005.4 KB, branch 1007.9 KB). The dialog is
budgeted as the recordPicker lazy chunk: Turbopack emits one variant per
parent context, 17.0 KB across three, and a session loads one of about
5.9 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:47:57 -05:00
james.maesandClaude Opus 5.5 75c9c5d64d feat(layout): resizable desktop navigation
The left navigation gets a drag handle on its right edge, like a normal
resizable vertical panel:

- Drag (pointer capture, mouse, pen and touch) sets the width; it is
  written to a CSS custom property once per animation frame, so the
  content reflows without a React render per pixel.
- Double-click resets to the default (the theme's --qqq-sidebar-width,
  256 px).
- The handle is a focusable vertical separator with aria-valuenow/min/
  max: Left/Right step 16 px, Shift 64 px, Home/End jump to min/max,
  with a visible focus ring and a grip on hover/focus.
- Width is held to 200 px .. min(480 px, 40% of the viewport), in CSS
  too, so a window resize needs no script.
- The chosen width persists in localStorage (wrapped in try/catch) and
  is applied in a layout effect, before the first paint.
- The phone drawer is unchanged and has no handle.

Bundle: first-load JS +1.0 KB gzip on the app routes, total JS 1005.4
-> 1006.4 KB (cap 1050), shared CSS 21.5 KB unchanged (cap 22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:41:54 -05:00
james.maes 98409c85af Merge pull request #61 from QRun-IO/feature/record-developer-view
feat(developer): record developer view with record navigation, API access and provider panels
2026-10-12 14:38:32 -05:00
james.maesandClaude Opus 5.5 ddc66c6b63 perf(search): load the results page on demand; lighter preview; lazy chunk budgets
Measured on the static export after merging main (a724fc1), against main:
total JS was 1067.0 KB (main 1005.4), over the 1050 KB cap.

- /app/search loads SearchPage through import(), so the results page is no
  longer in any route's first load (/app/search first load 334.5 KB, main
  335.2).
- The record preview renders values itself (file thumbnails, chips, Yes/No,
  dates, links, text) instead of through FieldValue, whose widget imports
  pulled two re-bundled chunks (+17.1 KB) into the preview. Secrets
  (REVEAL), widgets, code, HTML and binary fields are not previewed.
- The record search response schemas move from the shared schemas module to
  record-search.ts, their only user.
- perf-budget.json budgets the four on-demand search chunks under
  lazyChunks with their measured sizes (search page 13.6, quick search panel
  9.7, palette record groups 7.8, record preview 7.3 KB gzip; caps about 8%
  above), so they leave the total but keep their own caps and must stay out
  of every first load.
- A few one-off utility classes are swapped for ones main already ships.

Result: total JS 1012.7 KB (main +7.3), CSS 21.9 KB (main +0.4), every route
first load within 0.5 KB of main; no cap raised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:10:55 -05:00
james.maesandClaude Opus 5.5 2ed5239642 Merge origin/main (#54, #55, #58, #77, #78) into feature/search-ux
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:54:46 -05:00
james.maesandClaude Opus 5.5 725ef87efd Merge branch 'main' into feature/assistant-panel; CSS budget 23 KB
Main now includes SQL Workbench dark mode (#78, shared CSS 21.5 KB).
With the assistant panel the static export measures 22.3 KB of shared
CSS, over the 22 KB cap, so the cap becomes 23 KB (about 3% headroom).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:46:07 -05:00
james.maes a724fc191c Merge pull request #78 from QRun-IO/feature/sql-workbench-theme
feat(sql-workbench): theme-aware workbench built from the standard controls (light and dark)
2026-10-12 13:45:08 -05:00
james.maesandClaude Opus 5.5 5b4ef76b01 fix(search): keep "nothing selected" when records arrive after ArrowUp
ArrowUp above the first option clears the selection so Enter opens the
results page. When record results arrived after that (the options
changed and the stale flag flipped), the first option was preselected
again, so Enter opened a hit instead. An explicit "nothing selected"
choice now holds until the term changes, like any other choice.

Caught by NAV-025 in WebKit acceptance, where the records answer after
the keypress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:23:14 -05:00
james.maesandClaude Opus 5.5 c1be9c61b3 Merge branch 'main' into feature/assistant-panel
Keeps the AssistantProvider wrapper around #54's positioned single
scroll shell (relative, data-qqq-id=app-shell).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:13:37 -05:00
james.maesandClaude Opus 5.5 c8b7e9b16a Merge branch 'main' into feature/record-developer-view
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:10:47 -05:00
james.maes 72377ac30b Merge pull request #77 from QRun-IO/chore/dependabot-pr-limit
chore(deps): limit Dependabot to three open PRs per ecosystem
2026-10-12 12:40:58 -05:00
james.maes 4d8b93ed42 Merge pull request #54 from QRun-IO/fix/single-page-scroll-container
fix(layout): one page scroll container; contain absolute descendants
2026-10-12 12:40:54 -05:00
james.maesandClaude Opus 5.5 cf6a326bca Merge main into feature/sql-workbench-theme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:23:58 -05:00
james.maesandClaude Opus 5.5 c2aa695044 feat(sql-workbench): theme-aware workbench built from the standard controls
Dark mode and the hand-rolled controls made the SQL Workbench look unlike
the rest of Next UI. Every color now comes from a theme token, so both
modes work by construction:

- Editor: CodeMirror theme from the app tokens and the shared --qqq-code-*
  palette (gutter, cursor, selection, active line, autocomplete popup);
  follows the theme toggle live without reconfiguring.
- Standard controls: buttons, selects, inputs, tabs, menus, badges and
  alerts from a new design-system module (components/ui/controls.ts; the
  script panel classes now live there and are re-exported).
- Result grid in the record grid look; Export menu (CSV, Excel, JSON)
  like the record query; lucide icons replace text glyphs.
- Charts use the theme chart palette, popover tooltips, muted axes.
- Write confirmation and notebook delete use the app dialog pattern
  (notebook delete no longer uses window.confirm).
- Layout: one column until the widget is wide enough (container query);
  cell controls in the cell header, not floating over other controls; no
  inner horizontal scroll outside the result grid; plain labels; dates
  via DateTimeText.
- Primary and danger buttons darken on hover, and the danger fill is
  darkened in dark mode, keeping white labels at 4.5:1.

Tests: e2e checks AA contrast in the workbench in light and dark mode,
the live theme switch of the editor, and no sideways scroll on a phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:23:44 -05:00
james.maesandClaude Opus 5.5 6c1e3a67b1 feat(search): detail chips on result cards; phones keep only Preview
Result cards show the backend's secondary field values (details) as
label/value chips instead of a run of text, so a record's key fields
read at a glance; screen readers still hear "Label: value".

On phones (below sm) the card keeps only its Preview button: New tab and
Copy link are in the preview sheet, and the snippet gets the width
instead of breaking mid-word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:13:32 -05:00
james.maesandClaude Opus 5.5 9d87b6dfee fix(developer): section bar never scrolls sideways and follows the section in view
- Phones get a "Go to" section picker; wider screens get a wrapping row of
  links. Neither scrolls sideways.
- The bar sticks to the top of the main scroll container (-top-6 offsets its
  p-6), so no page content shows between the app header and the bar.
- The highlighted section comes from the scroll position, using the same
  line a jump lands on (scroll-mt-24). At the bottom of the page the
  section last picked stays highlighted while it is in view.
- The API operation rows label their two copy buttons "Copy URL" and
  "Copy example" instead of two identical "Copy" buttons.
- e2e: the bar does not overflow at any width, phone picker, sticky bar and
  highlight, dark mode at 390 and 1440, and screenshots of the SQL
  Workbench and Assistant panels. The unit test waits up to 5 s for the
  lazy script cards, which timed out under full-suite load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:09:03 -05:00
james.maesandClaude Opus 5.5 21ba08bcd5 Merge branch 'main' into feature/assistant-panel
Resolves the perf-budget comment conflict with the widget width ladder
(#55). Both raise the shared CSS cap to 22 KB; the combined tree measures
21.3 KB locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:56:09 -05:00
james.maes 89add474cf Merge pull request #55 from QRun-IO/feature/widget-width-readability
feat(widgets): readable dashboard widths (per-widget min width, ultra-wide caps, table card layout)
2026-10-12 11:55:08 -05:00
james.maes 3f161f6fd5 Merge pull request #58 from QRun-IO/docs/adopting-in-another-app
docs: add "Adopting in another app" section to the README
2026-10-12 11:55:03 -05:00
james.maesandClaude Opus 5.5 c183249a4b chore(deps): limit Dependabot to three open PRs per ecosystem
Twelve version-update PRs each ran the hour-long real-backend acceptance
workflow and starved release PRs of runners. Minor and patch updates are
already grouped per ecosystem; cap open version-update PRs at three for
npm (was 10), GitHub Actions and Maven (default 5). Security updates are
unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:18:00 -05:00
james.maes 9ce3b81b04 Merge remote-tracking branch 'origin/feature/search-ux' into feature/search-ux 2026-10-12 10:33:37 -05:00
james.maesandClaude Opus 5.5 a62b10ae12 test(e2e): target the previewed record's heading in the sheet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:33:24 -05:00
james.maes 71bcc37c9e Merge remote-tracking branch 'origin/main' into feature/search-ux 2026-10-12 10:30:51 -05:00
james.maesandClaude Opus 5.5 a9975487f6 feat(search): record preview, term completions, relevance and Shift+Enter
Results page: a record preview beside the results on wide screens (>=1280px)
that follows hover, focus and the arrow keys (which now move between
results), and a side sheet elsewhere (Preview button, or a long press on
touch screens). The preview shows the record's first-tier fields as on the
record view (FILE_PREVIEW thumbnails included), the fields that matched,
and open / new tab / copy link / edit (when permitted). It loads on demand.
Cards show a relevance meter when the provider sent scores.

Quick search: completions of the last word from matching labels (never
preselected, so Enter still opens the top hit), and Shift+Enter opens the
results page from anywhere in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:30:49 -05:00
james.maesandClaude Opus 5.5 95931a7c7e fix(developer): ask a backend without developer panels once; REC-054 follows the new page structure
Real-backend acceptance failed REC-054 on every browser:

- The acceptance backend (QQQ develop checked out before QRun-IO/qqq#1076
  merged) answered GET /table/{t}/{pk}/developer/panels with 404. The view
  already showed no panels, but each 404 is a failed request and a console
  error. A 404/405 now marks the endpoint unsupported for the session, so a
  QQQ before 4.1.0-RC.4 sees one such request, not one per record.
- "a table without associated scripts shows none" assumed the old page's h3
  headings and no tablist at all. Sections are h2s under the record title now
  and API Access has source tabs, so the spec checks the h2 headings, no
  Associated Scripts section, no script-card tabs, and the backend's built-in
  Record Security panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:29:05 -05:00
james.maesandClaude Opus 5.5 38207b3ef5 Merge main into fix/single-page-scroll-container
Resolve docs/releases/unreleased.md against the RC14 reset: keep only
this branch's single-scroll-container entry (the others shipped in RC13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:28:09 -05:00
james.maesandClaude Opus 5.5 1f4841bf12 Merge main into docs/adopting-in-another-app
Resolve the README status paragraph against the RC14 preparation:
RC14 is published, so the line keeps this branch's published wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:27:48 -05:00
james.maes 5629bcb3c7 Merge branch 'main' into feature/search-ux 2026-10-12 09:27:34 -05:00
james.maes 4fe5ee94f2 Merge branch 'main' into feature/widget-width-readability 2026-10-12 09:27:31 -05:00
james.maes ed1dc64330 Merge branch 'main' into feature/assistant-panel 2026-10-12 09:27:27 -05:00
james.maes ef6f0a9219 Merge branch 'main' into feature/record-developer-view 2026-10-12 09:27:24 -05:00
james.maes 3ae29246c9 Merge pull request #62 from QRun-IO/chore/repo-hygiene-trunk
chore: adopt main-only trunk model and add repo hygiene config
2026-10-12 09:27:01 -05:00
james.maes 6eac444a22 Merge pull request #63 from QRun-IO/fix/nav056-webkit-navigation-race
test(acceptance): finish table reads before deliberate document navigations (NAV-056, NAV-014)
2026-10-12 09:26:56 -05:00
james.maesandClaude Opus 5.5 46158c42cc Merge origin/main (#57 record search tolerance, RC.14 prep) into feature/search-ux
Conflicts in src/lib/api/schemas*.ts and tables*.ts resolved in favor of this
branch: record search moved to src/lib/api/record-search.ts, which keeps #57's
behavior (labels may be missing, null or empty; results without a table name or
record id are skipped with a warning) and its tests (record-search.test.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:15:33 -05:00
james.maes 03e7415aa2 Merge pull request #64 from QRun-IO/feature/next-rc14-prep
chore(#712): prepare Next UI 1.0.0-RC.14
2026-10-12 08:44:03 -05:00
james.maesandClaude Opus 5.5 b89d406419 chore(#712): prepare Next UI 1.0.0-RC.14
Urgent fix candidate: record search no longer fails when one result has
no label (#57), plus the Next.js 16.3.8 security update (#52). Source is
main at b699204d (merge of #57). The WebKit NAV-056 diagnostic failure,
which also fails on main, is recorded as an owner-accepted known failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:35:59 -05:00
james.maes b699204d87 Merge pull request #57 from QRun-IO/fix/record-search-tolerant-results
fix(search): one result without a label no longer fails record search
2026-10-12 08:35:12 -05:00
james.maes 51c2db74f8 Merge remote-tracking branch 'origin/main' into fix/nav056-home-app-regression 2026-10-12 07:56:51 -05:00
james.maesandClaude Opus 5.5 f34269bd38 test(acceptance): finish table reads before deliberate document navigations (NAV-056, NAV-014)
The recurring webkit/tablet NAV-056 failure ("appearance preferences explain an
application theme override") and the NAV-014/NAV-016/NAV-023 failures of the same shape are
a race in the specs, not a product regression; they predate #53 (first seen 2026-10-02,
run 36981311218).

Each spec replaced the document (reload, goto, goBack) as soon as an early assertion
passed, while the table page was about to send its initial reads (saved-view init, query,
count, or table metadata). WebKit refuses an XHR the old document sends after a navigation
has started and reports it as a page error "<url> due to access control checks." (Chromium
and Firefox report an ignorable abort). The CI trace of run 38238500908 shows those three
URLs never reached the network from the first document; the errors fire 42 ms after the
reload starts. A probe on Linux WebKit reproduces the exact message for an XHR sent at
reload start, and none for a request already in flight.

Add expectRecordsLoaded(): it waits until the table lists records, by which point the page has
sent all of its initial reads. Use it, and the existing greetInteractive response barrier,
before each hard navigation in these specs. Diagnostics stay strict: any API page error
still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:56:50 -05:00
james.maesandClaude Opus 5.5 930804d36c chore: adopt main-only trunk model and add repo hygiene config
- Add .github/dependabot.yml: weekly grouped npm, GitHub Actions and Maven updates.
- Add .github/CODEOWNERS (* @KofTwentyTwo).
- Remove the dead CircleCI develop, release/* and hotfix/* workflows (no such branches
  exist); test_only runs on every branch except main, tag-based publish is unchanged.
- Drop the develop push trigger from ci.yml.
- Document the trunk model, required checks and tag-based releases in CONTRIBUTING.md
  and CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:55:17 -05:00
james.maesandClaude Opus 5.5 2f745d578b test(acceptance): reach the one-table view through its filter chip
"Show all" appears only when a table has more matches than the overview lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:43:39 -05:00
james.maes b50513c3b4 Merge pull request #52 from QRun-IO/dependabot/npm_and_yarn/npm_and_yarn-446c67f130
chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates
2026-10-12 07:37:24 -05:00
james.maesandClaude Opus 5.5 60b110a0b1 test(acceptance): search specs follow the new quick search and results page
Records now sit in Top hits and per-table groups, the first result is
preselected (ArrowUp clears it), the dialog has a Close button in its
Tab order, Escape keeps focus in the header combobox, pages rank exact
and prefix matches first, the quick search asks for 6 per table, and the
results page shows per-table sections (8) with Show all (25).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:35:24 -05:00
james.maesandClaude Opus 5.5 148f9d653b fix(assistant): probe /assistant/config only when metadata announces the qbit
The real-backend acceptance suite fails any page with an unexplained 4xx, and the probe answered
404 on every page load of an application without qbit-assistant (also a console error in every
browser). The controller now probes only when instance metadata carries
supplementalInstanceMetaData.assistant (the spec's recommended metadata signal, published by the
qbit; the v1 allow-list in core needs the matching entry). Without it, no assistant request is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:08:58 -05:00
james.maesandClaude Opus 5.5 ace6579fc0 feat(developer): record developer view with record navigation, API access and provider panels
Rebuild /app/{table}/{id}/dev (owner request 2026-10-12):

- Record screen navigation: back to the record, the shared RecordViewHeader
  (title, copy id, Edit, Actions menu, Audit, share), no view-mode toggle
  (new showViewModeToggle prop), and a sticky section navigation.
- Fields: label, name, type with PVS/adornments/flags, the value as the record
  screen shows it and the raw value; filterable; cards on phones.
- API Access: v1 routes this screen uses and every application API exposing
  the table (operations read from its per-table OpenAPI spec), filtered by
  the user's table permissions and capabilities, with curl, fetch and Java
  examples built from the record's values (placeholder credentials only),
  plus API docs / spec links.
- Developer panels from QQQ's provider contract (QRun-IO/qqq#1076), rendered
  generically from their blocks (key/values, text, code, table, links,
  process actions; safe links only). Older backends (404) show none.
- Relations (references, associations, exposed joins), recent audits with the
  full history dialog, associated scripts, raw JSON with copy, and a
  searchable table metadata explorer (fields with PVS and adornments,
  sections, menus, supplemental metadata, raw JSON).
- The view loads on demand (first load 333.5 KB, budget 360 KB).
- SQL Workbench: ?sql= (and ?backend=) opens the SQL as a new cell, once,
  without running it - the target of developer panel links.

Tests: unit (operations/snippets, panel parsing, page sections, panels
present/absent, workbench URL SQL) and Playwright (navigation parity, panels
present/absent, API sources, 360-3200px without sideways scrolling).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:45:09 -05:00
james.maesandClaude Opus 5.5 66edf51719 fix(search): no stale or hidden picks on Enter; keep text typed past a late URL update
Review follow-ups: Enter on a closed header dropdown searches everything
instead of opening a hidden option; while results belong to the previous
term only a current entry is preselected; a new term drops a pointer or
arrow choice made for the old one; the search page ignores the echo of
its own debounced URL update (keeping text typed meanwhile) and builds
that update from the current filters and sort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:22:51 -05:00
james.maesandClaude Opus 5.5 3175b47165 feat(assistant): QQQ Assistant panel, phase 1 (read-only Q&A and navigation)
The Next UI side of qbit-assistant (spec docs/specs/2026-10-11-qqq-assistant-qbit-design.md):

- src/lib/api/assistant.ts: typed client for every /qqq/v1/assistant route; turns stream over
  fetch + an incremental SSE parser (sse-parser.ts), deduplicate by seq, resume through
  /turns/{id}/events?after=seq after a dropped connection, and cancel through POST
  /turns/{id}/cancel. The /config probe (assistant-probe.ts) follows the ESB getOrNull pattern:
  403, 404 or a non-config body hides everything.
- src/components/assistant: AssistantProvider (controller created in the dashboard layout),
  header button with unread badge, Ctrl/Cmd+J (also from text fields), a command palette
  "Ask Assistant" item, and the lazily loaded panel: docked and resizable (360-640 px) from
  1024 px with its own scrolling message list, a full-height Radix sheet below that following
  the visual viewport. Conversation rail (listbox), rename/pin/archive/delete, context chip,
  Stop, throttled live announcements, tool steps, record chips with hover previews, records,
  record, count, history and chart widget renders, feedback thumbs.
- Markdown: react-markdown + rehype-sanitize, raw HTML skipped, images never load, links only
  to same-origin /app paths (others plain text with a copy button).
- ?assistant=open&assistantPrompt=... opens a new conversation with the prompt pre-filled
  (not sent) and removes both parameters.

Bundle: the shell adds 1.7 KB first-load JS (/app/_ 335.5 -> 337.2 KB); the panel is a
50.8 KB lazy chunk budgeted under lazyChunks.assistant (55 KB). Its Tailwind utilities add
0.9 KB to the shared CSS (21.3 KB against the 21 KB cap), so the CSS cap is raised to 22 KB.

Tests: vitest for the SSE parser, client (resume, cancel, errors), link safety, reducer and
the panel with MSW streams (gating, streaming, cancel, markdown safety, deep link, axe);
Playwright tests/e2e/assistant.spec.ts at desktop, tablet and phone widths (docked vs sheet,
one scroll container per region, no horizontal page scroll, axe).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:15:49 -05:00
james.maesandClaude Opus 5.5 fabbad15a7 feat(search): search-as-you-type quick search and a URL-addressable results page
Header search, the "/" dialog and the Cmd+K palette now search records as
you type: debounced, superseded requests aborted, grouped by table with
counts and icons, exact/prefix matches promoted to "Top hits", matched
terms highlighted (backend ranges when the extended contract sends them,
else local), matched-field snippets, secondary values and last update.
Keyboard: arrows, Enter (first result preselected), Ctrl/Cmd+Enter opens a
new tab. Recent searches and recently viewed records show before typing.

Failures are honest: the failure class (server error with HTTP status,
timeout, unreachable, permission, unsupported, rejected, invalid response)
with the backend message and a retry, keeping page and recent matches; a
failed search provider is reported above its fallback results.

/app/search is rebuilt around URL state (q, tables, view=pages,
sort=recent): filter chips with counts, per-table sections with "Show
all", 25-per-page paging while the backend reports more, result cards
with snippets and quick actions (open, new tab, copy link).

All extended response fields are optional, so older QQQ backends keep
working; malformed results are skipped instead of failing the search.
The results panels load on demand: first load grows ~1.0 KB per route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:09:42 -05:00
james.maesandClaude Opus 5.5 baf7e0a554 docs: link the QQQ adoption checklist from the README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:47:01 -05:00
james.maesandClaude Opus 5.5 85da466255 docs: add "Adopting in another app" section to the README
Document how a QQQ backend app switches to or upgrades the Next
dashboard: Maven coordinates and versions, minimum QQQ per feature
(SQL Workbench, FILE_PREVIEW, home app, responsive layouts), the
dashboard selection switches, auth redirect URIs, route changes and
known limitations. Also mark RC13 as published, point at .env.mock
(there is no .env.example) and correct the Next.js major in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:27:18 -05:00
james.maesandClaude Opus 5.5 a7acea381a fix(search): one result without a label no longer fails record search
QQQ serializes with NON_EMPTY, so a record whose label fields are empty
comes back from POST /search without recordLabel. RecordSearchResponseSchema
required recordLabel on every result, so a single such result rejected the
whole response ("Invalid record search response") and global search showed
"Record search failed. Try again." with no results (prod admin.kof22.com,
term "maes": two page_section rows with no name).

- Results are validated one by one. tableLabel/recordLabel may be missing,
  null or empty; recordId may be a number.
- A result without a label is labeled "{table label} {record id}".
- A result without a table name or record id is skipped with a console
  warning; the rest still show. A non-envelope response still throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:25:55 -05:00
dependabot[bot] 4d2b9f685c chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [next](https://github.com/vercel/next.js) and [sharp](https://github.com/lovell/sharp).


Updates `next` from 16.3.6 to 16.3.8
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.6...v16.3.8)

Updates `sharp` from 0.35.4 to 0.35.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.4...v0.35.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.8
  dependency-type: direct:production
- dependency-name: sharp
  dependency-version: 0.35.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-12 07:54:25 +00:00
james.maes 36382059bc Merge pull request #56 from QRun-IO/feature/next-rc13-prep
chore(#712): prepare Next UI 1.0.0-RC.13
2026-10-12 02:51:25 -05:00
james.maesandClaude Opus 5.5 51e00b4195 chore(#712): prepare Next UI 1.0.0-RC.13
Bump package.json and the Maven revision to 1.0.0-RC.13, point the README,
guides and 1.0 testing page at RC13, add docs/releases/1.0.0-RC.13.md and
reset unreleased.md.

RC13 source: main 84c95e5 (merge of PR53). Merged since RC12: PR47 (SQL
Workbench), PR49 (FILE_PREVIEW thumbnails and lightbox), PR50 (responsive
layouts, readable child lists, compact dates), PR53 (instance home app).
PR54 and PR55 follow in RC14.

Backend: QQQ 4.1.0-RC.1 or later; 4.1.0-RC.2+ for standalone branding
images; 4.1.0-RC.3+ for FILE_PREVIEW thumbnails and the instance home app;
qbit-sql-workbench 1.0.0-RC.1+ for the SQL Workbench page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:50:47 -05:00
james.maes 84c95e524a Merge pull request #53 from QRun-IO/feature/home-app
feat(navigation): instance home app at /app, sidebar logo and breadcrumb root
2026-10-11 22:55:31 -05:00
james.maes 06d5aacd91 Merge branch 'main' into feature/widget-width-readability 2026-10-11 21:37:58 -05:00
james.maesandClaude Opus 5.5 0449ff315a Merge main (SQL Workbench, FILE_PREVIEW, responsive layouts) into fix/single-page-scroll-container
Resolves docs/releases/unreleased.md by keeping every Product fixes entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:37:39 -05:00
james.maesandClaude Opus 5.5 577bd3dee8 Merge main (responsive layouts #50) into feature/home-app
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:37:05 -05:00
james.maes 4586f5b4f2 Merge pull request #50 from QRun-IO/feature/responsive-readable-layout
feat: responsive record/dashboard layouts, readable child lists, compact dates
2026-10-11 21:36:59 -05:00
james.maesandClaude Opus 5.5 effee583b7 test(e2e): card-layout headers are clipped column headers, not hidden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:26:55 -05:00
james.maesandClaude Opus 5.5 1f1c5f546a fix(widgets): keep card-layout table headers for screen readers; layout-aware acceptance
- Card layout hides the table header visually (clip) instead of display:none, so
  column headers stay in the accessibility tree.
- WID-019 / WID-068 acceptance: on phones and tablets the table widgets now use
  the card layout, so assert no sideways scroll and per-value labels there, and
  keep the sticky-header, fr-width and header-help checks for table layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:26:21 -05:00
james.maesandClaude Opus 5.5 a42887f6e5 chore(perf): CSS budget 22 KB for the widget width ladder and table cards
CI measured shared CSS at 21.1 KB gzip (PR #50: 20.4 KB): the gridMinWidth
container-query classes and the table-widget card layout add 0.7 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:08:02 -05:00
james.maesandClaude Opus 5.5 a163dc53b1 feat(widgets): readable dashboard widths from phone to ultra-wide
- Per-widget minimum width: widgetColumnClasses honors defaultValues.gridMinWidth
  (rem or px; snapped up to 16-64rem tiers) with container-query thresholds that
  give the widget, and a neighbour of the same minimum, at least that width; and
  defaultValues.gridProfile ('widgets' | 'cards' | 'table' | 'fields') to pick a
  ladder. gridCols:sizeClass:* still wins.
- Ultra-wide: table and multi-table widget content stops at 96rem
  (defaultValues.tableMaxWidth overrides, 'none' lifts it); an app can cap the
  dashboard page with supplementalAppMetaData.dashboardLayout.maxContentWidth.
- QqqTableWidget card layout: narrower than 30rem, or than its declared column
  widths need, rows render as stacked label: value cards with no forced min
  width, fixed columns or sideways scroll.
- Unit tests (threshold table regenerated from the formula) and a Playwright
  dashboard spec at 390/768/1024/1440/1920/3200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:56:15 -05:00
james.maesandClaude Opus 5.5 77eef1c816 chore(perf): give the table developer route /app/_/dev a 360 KB cap
CI's bundle budget on the combined tree (SQL Workbench #47, FILE_PREVIEW
#49 and this PR) measured /app/_/dev at 350.8 KB first-load JS gzip, 0.8 KB
over the 350 KB default (each change fit alone). The record developer
route /app/_/_/dev measured 354.9 KB under its 360 KB cap. Give /app/_/dev
the same 360 KB route cap (about 2.6% headroom); every other route keeps
the 350 KB default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:42:45 -05:00
james.maesandClaude Opus 5.5 9a905890a8 fix(layout): one page scroll container; contain absolute descendants
On long pages the document grew a second vertical scrollbar beside the
<main> scroller, and scrolling it slid the header off-screen. The shell is
a fixed-height column (h-screen overflow-hidden) where only <main> scrolls,
but nothing in it was positioned: an absolutely positioned descendant
(sr-only text such as the company footer's "opens in a new tab") then has
the viewport as its containing block, overflow-hidden cannot clip it, and
its static position at the end of a long page stretches the document.

Make the shell root and <main> `relative` so they contain such elements.
Adds a mocked e2e spec asserting exactly one vertical scroll container
(main) on a tall app home and none on the document for a table page at
390, 1024, 1440, 1920 and 3200px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:40:56 -05:00
james.maesandClaude Opus 5.5 fe0077e86c Merge responsive layouts (#50, with #47 and #49) into feature/home-app
Resolves docs/releases/unreleased.md (the home-app bullet joins the
merged Features section) and perf-budget.json (the shared 360 KB
/app/_/_/dev cap with the combined measurement note from #50).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:29:28 -05:00
james.maesandClaude Opus 5.5 61be012fc2 Merge main (FILE_PREVIEW #49) into feature/responsive-readable-layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:28:51 -05:00
james.maes bc01d10a27 Merge pull request #49 from QRun-IO/feature/image-preview-lightbox
feat(preview): FILE_PREVIEW thumbnails and preview lightbox (QRun-IO/qqq#1056)
2026-10-11 18:28:38 -05:00
james.maesandClaude Opus 5.5 d3daa4d067 Merge SQL Workbench (#47) and FILE_PREVIEW (#49) into feature/responsive-readable-layout
Combines #49's preview handling with #50's compact columns:
- ChildRecordListWidget keeps #50's measured compact columns, long-text
  clamp and top-aligned cells, and gains #49's FILE_PREVIEW gallery
  (shared cellRecords, joined-field preview references, child table
  variant). The gallery covers only the columns on screen, so lightbox
  navigation never steps into a hidden column. A preview column in the
  first position is not turned into the row link (as in #49).
- ChildFieldMetaData.adornments is typed as FieldAdornment[] so it serves
  both CHIP detection (#50) and FILE_PREVIEW (#49).
- DataCell / FieldValue keep both DateTimeText and the preview imports.
- perf-budget.json: one shared 360 KB /app/_/_/dev cap; the note records
  the SQL Workbench, FILE_PREVIEW and #50/#53 measurements (expected
  about 355 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:35:17 -05:00
james.maesandClaude Opus 5.5 49788dd4e8 Merge main (SQL Workbench, #47) into feature/image-preview-lightbox
Resolves perf-budget.json: keep one /app/_/_/dev route cap of 360 KB and
combine the FILE_PREVIEW (+0.6 KB) and SQL Workbench (+3.1 KB) measurements
in the note (expected about 353.7 KB). Also corrects the unreleased note:
inline PDF frames are limited to backend /qqq/ paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:31:11 -05:00
james.maes 1412af5f69 Merge pull request #47 from QRun-IO/feature/sql-workbench
feat(sql-workbench): SQL Workbench notebook UI (SQL_WORKBENCH widget)
2026-10-11 16:30:07 -05:00
james.maesandClaude Opus 5.5 9150fb732f chore(perf): share the developer-route budget note with PR #53
Same 360 KB /app/_/_/dev cap; the note now records both measurements (this
PR 351.1 KB, home app PR #53 350.1 KB, main 350.0 KB) so both PRs carry an
identical perf-budget.json and merge cleanly in either order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:11:14 -05:00
james.maesandClaude Opus 5.5 e540b6fcbd chore(perf): give the record developer route its own 360 KB first-load cap
Measured in CI (static export + scripts/perf-budget.mjs): main is exactly at
the 350 KB default on /app/_/_/dev (350.0 KB, no headroom); this branch's home
app code makes it 350.1 KB. That route gets its own 360 KB cap; every other
route keeps the 350 KB default. The file change is identical to PR #50's, so
the two merge cleanly in either order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:11:11 -05:00
james.maesandClaude Opus 5.5 7f03ba1f4c fix(preview): table variant in gallery URLs; frame only backend PDFs
Thumbnails already carry the stored table variant on backend download
URLs, but the preview galleries did not, so the lightbox Open and
Download links for variant tables omitted it. Pass tableVariant to the
gallery in RecordView, AssociatedRecords (join table), ChildRecordListWidget
(child table from the instance metadata), DataGrid and RecordCardView.

AssociatedRecords now memoizes the shown page of records and its visible
fields so the gallery items are not rebuilt on every render.

The lightbox frames a PDF only when it is a same-origin /qqq/ backend path;
any other same-origin URL (e.g. a dashboard page stored in a field) opens
in a new tab instead of running in a frame with the dashboard's origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:36:20 -05:00
james.maesandClaude Opus 5.5 4e17d720f4 feat(navigation): one landing page - the home app is the single top nav entry
With homeAppName set, the home app is moved to the top of the sidebar in place
of the built-in Dashboard entry, whatever the backend's app order (kof22 labels
its home app "Dashboard"), so there is exactly one landing entry. Without
homeAppName the built-in Dashboard overview entry stays first, as before.

Test: no duplicate landing entry when homeAppName is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:13:05 -05:00
james.maesandClaude Opus 5.5 984c26b62b Merge origin/main (Next UI 1.0.0-RC.12) into feature/responsive-readable-layout
Resolves docs/releases/unreleased.md: RC.12 moved the earlier product fixes
into its release notes; this branch's three entries are the only unreleased
product fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:10:04 -05:00
james.maesandClaude Opus 5.5 d1eae03894 feat(navigation): instance home app at /app, the sidebar logo and breadcrumb root
When the backend designates a home app (QInstance.homeAppName, QRun-IO/qqq#1059)
that the user may open, it is the instance's home page:

- /app client-side redirects to /app/{homeAppName} (backends such as
  Website-Backend already redirect full page loads of /app/ server-side; this
  covers client-side navigation), rendering a busy placeholder meanwhile
- the sidebar logo and the breadcrumb Home crumb link there
- the sidebar drops its separate Dashboard entry (the home app is in the tree)

Without homeAppName, or with an app missing from the user's metadata, /app is
Next's overview exactly as before. The route map exposes the result as
`homePath` (lib/utils/home-app.ts).

Tests: home-app helper, route map (home path, Dashboard entry), Breadcrumbs
and Sidebar links, /app page redirect vs overview. Docs: developer guide
routes and metadata, unreleased notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:08:27 -05:00
james.maesandClaude Opus 5.5 310c23f9cd fix(layout): apply the 24rem default record field threshold
The previous commit added the test but missed the constant itself: default
record fields now pair from a 24rem grid (tablet REC-004).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:55:24 -05:00
james.maesandClaude Opus 5.5 de387a61f7 fix(layout): pair default record fields from a 24rem grid
On a tablet the selected-section field grid beside the sidebar is ~416px, below
the 30rem form threshold, so two default-width read-only fields stacked where
they used to share a row (acceptance REC-004 on the tablet profile). Read-only
label/value pairs fit at ~180px each, so the default record field layout pairs
them from 24rem; phones still stack and explicit gridColumns keep the form
thresholds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:55:01 -05:00
james.maes e333223470 Merge pull request #48 from QRun-IO/feature/GH-712-next-rc12
chore(#712): prepare Next UI 1.0.0-RC.12
2026-10-11 12:03:09 -05:00
james.maesandClaude Opus 5.5 c064048c6f chore(perf): give the record developer route its own 360 KB first-load cap
Measured with `QQQ_NEXT_OUTPUT=export next build` + scripts/perf-budget.mjs:

- main b0b0d46: /app/_/_/dev 350.0 KB gzip (exactly at the 350 KB default cap)
- this branch:  /app/_/_/dev 351.1 KB (+1.1 KB: compact date formatter,
  child-list column ranking, container-query span tables)
- other routes stay under the unchanged 350 KB default (largest /app/_/dev
  347.1 KB); total JS 966.6 KB of 1050 KB; CSS 20.0 KB of 21 KB

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:49:23 -05:00
james.maesandClaude Opus 5.5 2b4d82309c test(acceptance): assert compact dates and the key-behind-toggle child list
- Record-view DATE and DATE_TIME values show compactly; assertions now check
  the text and the full zoned timestamp in the tooltip (or the <time dateTime>
  calendar day for DATE).
- A value in a declared zone other than the viewer's keeps its time and zone
  even in an older year (`Mar 10, 2024, 3:30 AM CDT`), so fixed- and
  per-record-zone fields still read in their zone (REC-019).
- Audit entries and script log/line timestamps: compact text, full title.
- WID-024: the key column is behind the "more columns" toggle while the name
  identifies the row; the toggle restores Id + Name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:48:52 -05:00
james.maesandClaude Opus 5.5 4fc3dfd7aa docs: note responsive layout, readable child lists and compact dates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:43:57 -05:00
james.maesandClaude Opus 5.5 8c9619e7f7 test(mocks): CMS app with wide child lists for layout and date checks
Adds a mock CMS app (sites with domain and blog child lists, long descriptions
and meta tags, zoned timestamps including "today") so record views, embedded
child lists, the query grid and an app home can be checked at phone, laptop,
desktop and ultra-wide widths with `NEXT_PUBLIC_MOCK_API=true pnpm dev`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:43:00 -05:00
james.maesandClaude Opus 5.5 d3298cca2b feat(child-records): readable embedded child lists that fit their panel
A child-record list in a record-view panel showed every field of the child
table, so long text wrapped into tall, narrow columns. It now measures its
width and shows the columns that matter most and fit, ranked from metadata and
the rows on screen (compact-columns):

1. the record's name (first short text column, preferring T1 sections)
2. status columns (CHIP / possible values) and booleans
3. one date: the date column with the latest value (modified beats created)
4. other short text, numbers, other dates
5. long text last (TEXT / HTML / heavy, or values that run long in the rows)

The primary key is left out while a name column identifies the row (the row
still links to its record); it leads only when nothing else names the row. An
application that sets the widget's existing `onlyIncludeFieldNames` has chosen
its columns, and that order is their priority, so no new QQQ metadata is needed.

Hidden columns are one click away (`+N more columns` / `Fewer columns`, plus
View All). Cells align to the top, long text is clamped to three lines with the
full text on hover, and headers stay on one line. The CSV export keeps every
column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:59 -05:00
james.maesandClaude Opus 5.5 edaa1ffb33 feat(layout): size record, dashboard and form grids by their own width
QQQ sizes cards, widgets and fields in twelfths (gridColumns). Applied at the
viewport's `lg` breakpoint, a half-width card is half of what is left after the
sidebar, so two table panels sat side by side at ~350px on a laptop and stayed
side by side however narrow they got.

Grids are now CSS containers (`@container`) and their items take their declared
span only when the grid itself is wide enough for items of that kind:

- record-view cards (tables, field lists): stacked below 66rem, half from 66rem,
  at least a third from 100rem, the declared span from 120rem; overview cards
  keep their own height instead of stretching to the tallest in the row
- dashboard widgets: 40rem / 60rem / 76rem; table widgets use the card widths
- form and record fields: 30rem / 44rem / 56rem, so a create dialog or a narrow
  panel no longer crams three fields into a row; the default two-column record
  field layout widens to thirds and quarters on ultra-wide screens
- the record header's identity fields flow in a row, each as wide as its value

Material per-breakpoint widget sizes (gridCols:sizeClass:*) keep their
viewport behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:47 -05:00
james.maesandClaude Opus 5.5 c564a29ce8 feat(dates): one compact date/time presentation for every table, record view and widget
A shared formatter (compactDateTime / compactDate / relativeDateTime) and a
DateTimeText component replace the long `yyyy-MM-dd hh:mm:ss AM TZ` text that
wrapped over three lines in narrow cells:

- today: `6:33 PM`; this year: `Jul 5, 6:33 PM`; before: `Jul 5, 2025`
- DATE values never show a time and are never shifted by the viewer zone
  (DataCell read `2024-06-15` as UTC midnight and showed the 14th in the US)
- one line (nowrap, tabular figures), the full zoned timestamp as the tooltip,
  a machine-readable <time dateTime>
- the viewer's zone and locale; a backend display value zoned by a field
  behavior in another zone keeps that zone (`Jul 5, 4:33 PM PDT`)
- relative time (`3h ago`) for activity lists (ESB messages and counters)
- grid date columns default to widths that fit the compact form

Exports are unchanged: server exports and the child-list CSV still write the
raw or backend display values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:35 -05:00
james.maesandClaude Opus 5.5 3ab0ed88fa fix(preview): stack the record-view file name under its thumbnail (QRun-IO/qqq#1056)
In narrow record-view columns the name beside a 96/160 px thumbnail wrapped
character by character; it now sits under the thumbnail, truncated to its
width (full name in the title). Found in a local smoke against a real backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:39:20 -05:00
james.maesandClaude Opus 5.5 12ef8571f1 feat(preview): FILE_PREVIEW thumbnails and preview lightbox (QRun-IO/qqq#1056)
Fields with the backend FILE_PREVIEW adornment render as fixed-size, lazily
loaded thumbnails (file-type icons for non-images) wherever values are shown:
query grid and card view, record view (larger, with file name), associated
records, child-record list and record-grid widgets. Clicking or pressing Enter
opens an accessible preview dialog (Radix: focus trap, Escape, focus restore)
with zoom/fit, open-original and download, file details, arrow-key
navigation across the screen's previews, same-origin PDF, video and audio
players, and a file card fallback. Only explicitly adorned fields are
previewed; URLs are limited to http(s), same-origin paths and inline images.
Join columns resolve their prefixed values.

The lightbox is a lazy chunk (~3.5 KB gzip). Main measured /app/_/_/dev at
350.0 KB (the default cap); the thumbnail resolver, thumbnail and gallery
context add 0.6 KB to record routes (350.6 KB), so that route gets an explicit
360 KB cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:28:13 -05:00
james.maesandClaude Opus 5.5 a4c8eaf8c9 chore(#712): prepare Next UI 1.0.0-RC.12
Version 1.0.0-RC.12 (package.json, pom revision), RC12 preparation notes
collecting everything merged since RC11 (PR25-PR46, incl. the #45
always-available record Actions menu), reset unreleased notes, and updated
version references in README, guides and the testing policy. Backend
requirement moves to published QQQ 4.1 candidates (RC.1+, RC.2+ for
standalone branding images, QQQ #1009).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:52:03 -05:00
james.maes b0b0d469fe Merge pull request #46 from QRun-IO/fix/45-record-actions-menu
fix(record-view): always show the Actions menu when it has entries (#45)
2026-10-11 10:04:06 -05:00
james.maesandClaude Opus 5.5 3ceed91214 test(acceptance): PRC-050 record menu also offers Audit (#45)
The default record Actions menu ends with Audit for users who can read
audits, so assert Developer Mode and Audit are present rather than
Developer Mode being last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:58:42 -05:00
james.maesandClaude Opus 5.5 4e12a5d524 fix(record-view): always show the Actions menu when it has entries (#45)
RecordViewHeader passed an empty entry list to the desktop RecordActions
unless the table had processes or a custom VIEW_SCREEN_ACTIONS menu, so
plain tables lost Developer Mode and Audit. The Actions controls (desktop
and phone) were also gated on write permissions or processes, hiding them
from read-only tables that still offer Developer Mode and Audit.

Pass the resolved default/custom menu entries through unchanged and show
the Actions controls whenever that menu has items. RecordActions keeps
de-duplicating Copy/Delete via menuOffersAction, so those now live in the
menu instead of also appearing as buttons, matching Material.

Update the component tests and the PRC-050 / REC-046 acceptance specs
that asserted the old missing-menu behavior.

Closes #45

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:53:46 -05:00
874 changed files with 146685 additions and 9342 deletions

No files matched your search

+10 -100
View File
@@ -1,104 +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 ───────────────────────────────────────────────────────────────
# Branch strategy mirrors the QRun-IO orb convention:
# test_only → all feature / non-protected branches
# publish_snapshot → develop
# publish_release_candidate → release/*
# publish_release → main / version tags
# publish_hotfix_release → hotfix/*
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
test_only:
setup:
jobs:
- qqq-orb/node_app_test_only:
filters:
branches:
ignore: /(develop|main|release\/.*|hotfix\/.*|integration.*)/
tags:
only: []
publish_snapshot:
jobs:
- qqq-orb/node_app_build:
filters:
branches:
only: [ develop ]
publish_release_candidate:
jobs:
- qqq-orb/node_app_build:
filters:
branches:
only: [ /release\/.*/ ]
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]+)?$/
publish_hotfix_release:
jobs:
- qqq-orb/node_app_build:
filters:
branches:
only: [ /hotfix\/.*/ ]
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: /.*/
tags:
only: /.*/
+5
View File
@@ -0,0 +1,5 @@
# Default reviewers for every change.
* @KofTwentyTwo
# The UI API contract (QRun-IO/qqq-frontend-next#107): the vendored documents and the off-spec list
/src/lib/api/openapi/ @KofTwentyTwo
+32
View File
@@ -0,0 +1,32 @@
# Weekly, grouped dependency updates. Minor and patch updates arrive as one PR per ecosystem;
# major updates stay separate so each gets its own review. At most three version-update PRs per
# ecosystem are open at once, so their real-backend acceptance runs do not starve release PRs of runners. Security updates are enabled in the
# repository settings and are not limited by this schedule.
version: 2
updates:
- package-ecosystem: npm # pnpm-lock.yaml
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
npm-minor-and-patch:
update-types: [minor, patch]
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
github-actions:
patterns: ['*']
- package-ecosystem: maven # pom.xml (the dashboard jar)
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
maven-minor-and-patch:
update-types: [minor, patch]
-86
View File
@@ -1,86 +0,0 @@
name: Real-backend acceptance
# Full QRun-IO/qqq#649 acceptance: the QRun-owned sample application built from QQQ source,
# serving this repository's production static export, driven by Playwright in each browser.
# The matrix gate fails on any required row without a first-attempt pass.
on:
pull_request:
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:
# The released QQQ version this dashboard is accepted against; override per run.
QQQ_REF: ${{ inputs.qqq_ref || vars.QQQ_ACCEPTANCE_REF || 'develop' }}
NEXT_TELEMETRY_DISABLED: '1'
jobs:
acceptance:
strategy:
fail-fast: false
matrix:
# Chromium provides the primary workflow gate independently of touch coverage.
browser: [chromium, 'mobile,tablet', firefox, webkit]
runs-on: ubuntu-24.04
timeout-minutes: 90
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
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '21'
cache: maven
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install dependencies
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm exec playwright install --with-deps chromium firefox webkit
- name: Build QQQ from source
run: 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: |
pnpm build:jar
mvn -B -q install
echo "NEXT_JAR_VERSION=$(mvn -B -q help:evaluate -Dexpression=project.version -DforceStdout)" >> "$GITHUB_ENV"
- name: Build the sample application jar
run: |
# Run from the QQQ checkout: its checkstyle/pmd configuration paths are relative to it.
(cd qqq-backend && mvn -B -q -f qqq-sample-project/pom.xml package -DskipTests -Djacoco.skip=true \
-Dqqq.frontend.next.version="$NEXT_JAR_VERSION")
echo "QQQ_SAMPLE_JAR=$(ls "$PWD"/qqq-backend/qqq-sample-project/target/*-jar-with-dependencies.jar)" >> "$GITHUB_ENV"
- name: Acceptance (${{ matrix.browser }})
env:
QQQ_ACCEPTANCE_BROWSERS: ${{ matrix.browser }}
QQQ_ACCEPTANCE_TRANSPORT_TRACE: '1'
# Fixed NAV023/NAV056 ownership capture only; original assertions and diagnostics remain strict.
QQQ_ACCEPTANCE_BROWSER_TRACE: ${{ (matrix.browser == 'webkit' || matrix.browser == 'mobile,tablet') && 'navigation' || '0' }}
# Retain native browser crashes alongside traces (QQQ#904, QQQ#960).
DEBUG: pw:browser
run: node scripts/acceptance.mjs --skip-build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: acceptance-${{ matrix.browser == 'mobile,tablet' && 'touch' || matrix.browser }}
path: test-results/acceptance/
retention-days: 30
+36 -103
View File
@@ -1,108 +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, develop]
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
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install dependencies
run: |
corepack enable
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
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install dependencies
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm exec playwright install --with-deps 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
storybook:
name: Storybook build
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install dependencies
run: |
corepack enable
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
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install dependencies
run: |
corepack enable
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@eeb76914ad49fcdb2197529481de55d1e35a008a
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@eeb76914ad49fcdb2197529481de55d1e35a008a
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@eeb76914ad49fcdb2197529481de55d1e35a008a
-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"
+2
View File
@@ -10,6 +10,8 @@
# next.js
/.next/
/.next-collaboration/
/.next-presence-live/
/out/
# production
+3
View File
@@ -0,0 +1,3 @@
profile: frontend-next
orb_version: '0.8'
default_branch: main
+1
View File
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
// polyfills (crypto-browserify -> elliptic, GHSA-848j-6mx2-7j84) that no story needs.
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
staticDirs: ['../public'],
addons: [
'@storybook/addon-docs',
],
+18 -1
View File
@@ -8,7 +8,7 @@ QQQ is a metadata-driven, low-code application framework. The admin UI renders e
## Tech Stack
- **Framework:** Next.js 15 (App Router) with React 19
- **Framework:** Next.js 16 (App Router) with React 19
- **Language:** TypeScript 5.x (`strict: true` — no `any` in component props)
- **Styling:** Tailwind CSS 4.x with CSS custom properties for QQQ theme tokens
- **Components:** shadcn/ui (Radix primitives + Tailwind)
@@ -143,6 +143,13 @@ pnpm lint # ESLint
pnpm format # Prettier
```
## Branching and Releases
Trunk-based: `main` is the only long-lived branch (no `develop`, `release/*` or `hotfix/*`).
Branch from `main`, keep the branch current by merging `main` in (no rebase or force-push under
review), sign every commit, and open the PR into `main`. Releases are signed `v*` tags on `main`.
Required checks and the release flow are in [CONTRIBUTING.md](CONTRIBUTING.md).
## Implementation Plans
Full implementation plans are in `docs/implementation-plans/`. The project is built in 6 work packages:
@@ -182,3 +189,13 @@ the second-brain vault (`R:/Git.Local/KofTwentyTwo/second-brain`):
Consult the dossier before deep-diving the code — it covers the API contract this
frontend targets (including versioned-middleware endpoint gaps), maturity assessment,
known parity gaps, and its relationship to `qqq-frontend-material-dashboard`.
<!-- 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 -->
+51
View File
@@ -0,0 +1,51 @@
# Contributing
## Branching: trunk-based on `main`
`main` is the only long-lived branch. There is no `develop`, `release/*` or `hotfix/*` branch.
1. Branch from the current `main` (`feature/...`, `fix/...`, `docs/...`, `chore/...`).
2. Keep the branch current by merging `main` into it; do not rebase or force-push a branch
that is under review.
3. Open a pull request into `main`. Every commit must be signed.
4. A pull request merges when the required checks pass and the code owner approves.
Head branches are deleted automatically after the merge.
### Required checks
| Check | Source |
|-------|--------|
| `Typecheck, lint, unit tests and coverage` | `.github/workflows/ci.yml` |
| `Mocked e2e (Playwright, chromium)` | `.github/workflows/ci.yml` |
| `Bundle budget (static export)` | `.github/workflows/ci.yml` |
| `Storybook build` | `.github/workflows/ci.yml` |
| `ci/circleci: qqq-orb/node_app_test_only` | `.circleci/config.yml` |
The real-backend acceptance matrix (`.github/workflows/acceptance.yml`) runs on every pull request
and on `main`. Treat a red row as a release blocker and investigate it; never re-run it until it passes.
## Releases
A release is a signed `v<version>` tag on `main` (for example `v1.0.0-RC.14`), where `package.json`
and `pom.xml` carry that version. The tag publishes the dashboard jar to Maven Central (CircleCI
`publish_dashboard_jar`) and the dashboard container image (`.github/workflows/dashboard-image.yml`).
Release notes live in `docs/releases/`.
## Dependencies
Dependabot opens grouped weekly updates for npm, GitHub Actions and Maven (`.github/dependabot.yml`),
plus security updates as they are published.
## UI API calls (merge order)
Next calls the backend only through operations described in the vendored UI API documents:
`src/lib/api/openapi/qqq-v1.json`, plus one JSON file per extension in `extensions/`.
`ui-api-contract.test.ts` fails on any other call.
A feature that needs new backend calls lands in this order:
1. The qbit or module ships the operations as a UI API extension, with spec classes and its
committed `openapi/qqq-v1/<name>.yaml`.
2. That document is vendored here, as `extensions/<name>.json` or a re-vendored `qqq-v1.json`.
3. The UI PR that calls them.
`OFF_SPEC_CALLS` in `ui-api-calls.ts` is a shrinking list of calls that predate the extension point.
`OFF_SPEC_CEILING` caps it: lower the ceiling as entries move onto specs, and never raise it.
+96 -4
View File
@@ -2,7 +2,7 @@
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.11**, with searchable-selector and script-completion input-race corrections. Confirm availability in the [RC11 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) before installing; see the [RC11 notes](docs/releases/1.0.0-RC.11.md) for the preparation checkpoint and verification scope. [RC10](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
**This checkout targets 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,16 +14,76 @@ 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.11</version>
<version>1.0.0-RC.36</version>
</dependency>
```
The explicit version overrides an older BOM-managed frontend. RC11 requires the QQQ 4.1.0-SNAPSHOT development backend including PRs #913 and #908; see the release notes for the verified backend commit. 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>`.
Feature coverage is recorded in the real-backend acceptance matrix in [`docs/acceptance/feature-matrix.md`](docs/acceptance/feature-matrix.md) ([QRun-IO/qqq#649](https://github.com/QRun-IO/qqq/issues/649)). Run it with `pnpm test:acceptance` (see [`tests/acceptance/README.md`](tests/acceptance/README.md)).
## Adopting in another app
How an existing QQQ backend application (for example one still on QQQ 0.x with the Material Dashboard) switches to, or upgrades, the Next dashboard. The frontend is distributed only as a Maven jar; there is no npm package (`package.json` is `private`).
### Coordinates and compatible versions
| Piece | Coordinates | Version |
|---|---|---|
| Next dashboard | `com.kingsrook.qqq:qqq-frontend-next` | `1.0.0-RC.13` (latest; [all versions](https://repo1.maven.org/maven2/com/kingsrook/qqq/qqq-frontend-next/)) |
| QQQ BOM | `com.kingsrook.qqq:qqq-bom-pom` (import scope) | `4.1.0-RC.3` (manages `qqq-frontend-next` at `1.0.0-RC.11`, so pin `1.0.0-RC.13` explicitly) |
| SQL Workbench QBit (optional) | `com.kingsrook.qbits:qbit-sql-workbench` | `1.0.0-RC.1` (built against QQQ `4.1.0-RC.2`) |
| Container image (optional, separate Node server) | `ghcr.io/qrun-io/qqq-frontend-next` | `1.0.0-RC.13` |
| Next feature | Minimum QQQ | Backend work |
|---|---|---|
| Dashboard at `/` | 4.1.0-RC.1 | Add the jar; `QApplicationJavalinServer` (`qqq-middleware-javalin`) serves it. |
| Standalone branding images | 4.1.0-RC.2 | Branding metadata ([qqq#1009](https://github.com/QRun-IO/qqq/issues/1009)). |
| FILE_PREVIEW thumbnails and lightbox | 4.1.0-RC.3 | Adorn each field with `AdornmentType.FilePreviewValues.newFieldAdornment()` ([qqq#1056](https://github.com/QRun-IO/qqq/issues/1056)). The field value, or `thumbnailUrlField` / `fullSizeUrlField`, must be an http(s) URL or a same-origin path; a BLOB field needs a `FILE_DOWNLOAD` adornment as well. Plain URL fields are never auto-detected. |
| Instance home app | 4.1.0-RC.3 | `qInstance.setHomeAppName("<app>")` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)). Users who may not open that app get Next's `/app` overview. |
| SQL Workbench | 4.1.0-RC.2 + QBit | Produce the QBit (`new SqlWorkbenchQBitProducer().withQBitConfig(...).produce(qInstance)`); it adds the `sqlWorkbench` app with the `SQL_WORKBENCH` widget, five tables and `/qqq/v1/sql-workbench/*` routes. Grant `sqlWorkbench.hasAccess` (and `sqlWorkbench.write` only for write mode) to super-users; configure `readOnlyCredentials` in production. See the [QBit README](https://github.com/QRun-IO/qbit-sql-workbench#readme). |
| Responsive layouts, readable child lists, compact dates | 4.1.0-RC.1 | None; frontend only. Child-record widgets honour `onlyIncludeFieldNames` order. |
### Switching from the Material Dashboard
1. Move the application to QQQ 4.1 (`qqq-bom-pom` `4.1.0-RC.3`). This is a major upgrade from QQQ 0.x; follow the QQQ 4.1 upgrade notes for the backend itself.
2. Add the `qqq-frontend-next` dependency above. When the jar is on the classpath, QQQ serves Next at `/` unless the Material Dashboard is requested explicitly.
3. Choose the dashboard in code with `withServeFrontendNext(true)` / `withServeFrontendMaterialDashboard(true)`, or, when neither is set in code, with `-Dqqq.javalin.frontend=next|material|none`. An app that sets `withServeFrontendMaterialDashboard(false)` and serves Material through its own route provider gets Next at `/` as soon as the jar is added; remove that custom route, or set `withServeFrontendNext(false)` to keep Material.
4. Authentication: keep the existing identity-provider client. OAUTH2 redirects to `{origin}/token` and AUTH_0 to `{origin}/`, as Material did; both use PKCE.
5. URLs change from `/<app>/<table>/<id>` (Material) to `/app/<table>/<id>` (Next). Map old bookmarks and links with `QInstance.redirects` (exact or wildcard paths).
6. Optional: set `homeAppName`, add `FILE_PREVIEW` adornments, add the SQL Workbench QBit (table above). No database migrations are needed for the dashboard itself; the SQL Workbench QBit needs its five tables (DDL in its README).
For a full per-feature checklist (backend upgrade, QBits, migrations, permissions) see the QQQ adoption checklist: https://github.com/QRun-IO/qqq/blob/develop/docs/migration/adoption-checklist.adoc
To upgrade between RCs, change only the `qqq-frontend-next` version and check the minimum QQQ version in the table above and in the [release notes](docs/releases/).
### Configuration
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)).
- Served at `/` only: sub-path hosting is not supported ([#732](https://github.com/QRun-IO/qqq/issues/732)).
- Material parity is not complete ([#714](https://github.com/QRun-IO/qqq/issues/714)); check the parity ledger for the screens your app relies on.
- Real Auth0, Google Drive and QuickSight integrations are not yet verified end to end ([#713](https://github.com/QRun-IO/qqq/issues/713)).
- The container image fixes the backend origin at build time (`host.docker.internal:8000`); use the jar, or rebuild the image, for another origin.
- WebKit and tablet acceptance runs show intermittent navigation diagnostics ([#985](https://github.com/QRun-IO/qqq/issues/985), [#904](https://github.com/QRun-IO/qqq/issues/904)).
## Run the local QQQ sample
Use the [QQQ quickstart](https://github.com/QRun-IO/qqq/blob/main/qqq-sample-project/README.md#quickstart-with-next): it clones editable Java sample source, compiles it against published QQQ, and serves this dashboard from the sample on port 8000. It needs JDK 21+, Git, curl and unzip; no Node.js, Maven or Docker installation.
@@ -55,7 +115,7 @@ The dev server starts at [http://localhost:3000](http://localhost:3000).
| `QQQ_NEXT_OUTPUT` | _(unset)_ | `export` produces the static export for QQQ hosting (`pnpm build:export`) |
| `NEXT_PUBLIC_MOCK_API` | _(unset)_ | Set to `true` to activate MSW browser mocks for dev/demo without a live backend |
Copy `.env.example` to `.env.local` and adjust as needed.
Copy `.env.mock` to `.env.local` to run against MSW mocks, or set `NEXT_PUBLIC_MOCK_API=false` and point `NEXT_PUBLIC_API_BASE_URL` at a real backend.
## Available Commands
@@ -130,3 +190,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.
+2
View File
@@ -26,7 +26,9 @@ The ID may be on a layout-neutral wrapper around a native button or menu item. S
- App home and chrome: `app-card-{appName}-icon`, `.banner.info`/`.banner.warning`/`.banner.error`, and `.qqq-branded-header-bar`.
- Query: `button-filter-builder`, `button-views`, `quick-views-container`, `button-query-mode-basic`, `button-query-mode-advanced`, `table-header-{field}`, and `menu-item-{action}`.
- 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**.
+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`
+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.
+38 -2
View File
@@ -2,6 +2,42 @@
> **Current status lives in GitHub:** [Next UI 1.0 roadmap](https://github.com/QRun-IO/qqq/issues/713). This document records scoped evidence and historical checkpoints; older open/closed wording and summary counts do not override current issue status. See [tracking policy](../ISSUE-TRACKING.md). Final 1.0 remains held for owner testing and explicit approval.
## CI layout
`plan` and `build` run concurrently. The build produces one static export, dashboard
JAR and sample backend; the sample and middleware API JARs plus `out/` are shared
with all shards. Each shard has its own backend, one worker and zero retries.
The dependency graph is `plan + build → shards → acceptance gate`, with an
independent, non-blocking `quarantine` job after `plan + build` in full mode.
| Trigger | Chromium | Firefox | WebKit | Mobile | Tablet | Selection |
| --- | --- | --- | --- | --- | --- | --- |
| PR | 2 | — | — | 2 | — | Changed areas plus landing/navigation/CRUD/query/dashboard smoke |
| main, `v*` tag, workflow dispatch | 3 | 3 | 4 | 4 | 4 | Entire suite, respecting touch tags |
| PR labeled `full-acceptance` | 3 | 3 | 4 | 4 | 4 | Same as full |
Counts are independent Playwright shards. Mobile and tablet are separate projects
and jobs. `scripts/acceptance-select.mjs` owns path selection: docs-only runs smoke;
shared infrastructure changes select all areas on the two PR projects. Label changes
trigger replanning. No change narrows full runs on main or tags except the explicit
quarantine below.
Require **`acceptance gate`** in branch protection. It always runs, merges blobs
into JSON and HTML, checks the matrix (`--partial` only for PR mode), and requires
all planned shards to succeed. Its `acceptance` artifact retains the merged report,
gate summary and failure attachments for 30 days. Full browser coverage cannot be
claimed from a partial PR run. The under-30-minute PR target needs hosted timing
validation; local syntax/unit checks do not measure it.
`WID-029` ([#93](https://github.com/QRun-IO/qqq-frontend-next/issues/93)) has
`@quarantine` tags and an entry in `tests/acceptance/quarantine.json`. The default
suite excludes it and reports it in `summary.quarantined`. Full runs execute it
separately on Chromium with `QQQ_ACCEPTANCE_QUARANTINE=only` and
`continue-on-error: true`, retaining `acceptance-quarantine` results. This is a
visible known failure, not evidence of a passing feature. Missing issue links or
unknown quarantine IDs fail the gate. See the [acceptance README](../../tests/acceptance/README.md)
for local commands, artifact contents and cache/retry behavior.
## September 30 — post-RC4 report and menu checkpoint
Local production export at `a8e4d3b`:583 Chromium tests and all391 required feature rows passed, zero failures/skips/flaky results, three approved external-service exclusions. This includes the uniform table backgrounds, record overview spacing and hidePreview compatibility fixes. Retained full report/gate: `report-preview-full-chromium/` in local visual-review evidence.
@@ -11,8 +47,8 @@ The subsequent column-menu height correction passes28 targeted Chromium/WebKit c
The acceptance suite runs against the owned sample backend and the production static
export (javalin mode). The default gate runs Chromium only; the documented matrix is the
five configured Playwright projects below. CI runs Chromium separately from phone/tablet
so the primary workflow gate can finish independently. Prioritize Chromium
five configured Playwright projects below. CI shards each project separately and merges
their evidence into the single gate described above. Prioritize Chromium
workflow depth for RC1 and final 1.0; the complete final browser claim still requires all five projects.
## Verification order
+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 |
+2 -2
View File
@@ -1,10 +1,10 @@
# Next UI agent guide
> **This checkout targets RC11.** Confirm availability in the [RC11 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) before installing. [RC11 notes](../releases/1.0.0-RC.11.md) record the preparation checkpoint and verification scope; RC10 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.
This is an implementation playbook for coding agents working on the QQQ Next UI. Read the [developer guide](./next-ui-developer-guide.md) for user and integrator behavior, the repository [AGENTS.md](../../AGENTS.md) and [CLAUDE.md](../../CLAUDE.md) for scoped conventions, and the [real-server feature matrix](../acceptance/feature-matrix.md) plus [Material parity ledger](../acceptance/material-parity.md) before claiming compatibility. This guide reflects the `feature/next-1.0` release worktree as of 2026-10-01; the previously verified public candidate at the RC11 preparation checkpoint was [1.0.0-RC.10](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10) at `12cf774`, and final `1.0.0` remains on hold. Later branch changes must not be attributed to that immutable release. Some ledger entries predate integrated changes, so reconcile them with source and real-server evidence. A green subset of tests is evidence for that subset, not proof of complete Material parity or a published 1.0 release.
This is an implementation playbook for coding agents working on the QQQ Next UI. Read the [developer guide](./next-ui-developer-guide.md) for user and integrator behavior, the repository [AGENTS.md](../../AGENTS.md) and [CLAUDE.md](../../CLAUDE.md) for scoped conventions, and the [real-server feature matrix](../acceptance/feature-matrix.md) plus [Material parity ledger](../acceptance/material-parity.md) before claiming compatibility. This guide reflects the `feature/next-1.0` release worktree as of 2026-10-01; the previously verified public candidate at the RC13 preparation checkpoint was [1.0.0-RC.12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) at `e333223`, and final `1.0.0` remains on hold. Later branch changes must not be attributed to that immutable release. Some ledger entries predate integrated changes, so reconcile them with source and real-server evidence. A green subset of tests is evidence for that subset, not proof of complete Material parity or a published 1.0 release.
## Operating rule: metadata is the product boundary
+22 -7
View File
@@ -1,12 +1,12 @@
# Next UI developer guide
> **This checkout targets RC11.** Confirm availability in the [RC11 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) before installing. [RC11 notes](../releases/1.0.0-RC.11.md) record the preparation checkpoint and verification scope; RC10 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 RC11 preparation checkpoint was [1.0.0-RC.10](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10) (commit `12cf774`); 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.
@@ -25,7 +25,7 @@ The source of truth for the frontend shape is [`src/types/metadata.ts`](../../sr
}
```
`appTree` controls the sidebar; `apps[name].widgets` controls ordered dashboard widgets; `apps[name].sections` controls the app home cards. The server may omit denied objects or send disabled permission flags; the frontend also checks flags before requesting data. The server must still enforce authorization. A widget can also be placed in a table section through `QTableSection.widgetName`, or embedded in a process step of type `WIDGET`. `QInstance.redirects` can map old exact or wildcard Material paths to new targets. `supplementalInstanceMetaData.materialDashboardTheme` supplies the application theme, and `environmentValues` can configure analytics.
`appTree` controls the sidebar; `apps[name].widgets` controls ordered dashboard widgets; `apps[name].sections` controls the app home cards. The server may omit denied objects or send disabled permission flags; the frontend also checks flags before requesting data. The server must still enforce authorization. A widget can also be placed in a table section through `QTableSection.widgetName`, or embedded in a process step of type `WIDGET`. `QInstance.redirects` can map old exact or wildcard Material paths to new targets. `QInstance.homeAppName` (QQQ [#1059](https://github.com/QRun-IO/qqq/issues/1059)) designates the app whose dashboard is the instance's home page; it is published only to users who may open that app. `supplementalInstanceMetaData.materialDashboardTheme` supplies the application theme, and `environmentValues` can configure analytics.
## 2. Install and run
@@ -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.11</version>
<version>1.0.0-RC.36</version>
</dependency>
```
@@ -48,7 +48,7 @@ pnpm build:export
pnpm build:jar
```
The mock setting is for isolated development. Set `NEXT_PUBLIC_MOCK_API=false` and connect a real QQQ backend for integration. `build:export` writes `out/`; `build:jar` packages it with Maven. The Maven POM enforces the presence of static placeholder pages. The default `pnpm build` instead creates a standalone Node server, whose `QQQ_BACKEND_URL` build setting adds same-origin rewrites. For the container path see [`docker/quickstart/Dockerfile`](../../docker/quickstart/Dockerfile). This checkout targets `1.0.0-RC.11`, overriding an older BOM-managed frontend; confirm its availability in the [RC11 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) before installing. It requires the QQQ 4.1.0-SNAPSHOT development backend including PRs #913 and #908; see the release notes for its verified backend commit. Use a matching local backend/frontend build to test later branch changes.
The mock setting is for isolated development. Set `NEXT_PUBLIC_MOCK_API=false` and connect a real QQQ backend for integration. `build:export` writes `out/`; `build:jar` packages it with Maven. The Maven POM enforces the presence of static placeholder pages. The default `pnpm build` instead creates a standalone Node server, whose `QQQ_BACKEND_URL` build setting adds same-origin rewrites. For the container path see [`docker/quickstart/Dockerfile`](../../docker/quickstart/Dockerfile). This checkout targets `1.0.0-RC.13`, overriding an older BOM-managed frontend; confirm its availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing. It 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; 4.1.0-RC.3 or later for FILE_PREVIEW thumbnails and the instance home app, QQQ #1056/#1059); see the release notes for the verified backend. Use a matching local backend/frontend build to test later branch changes.
| Setting | Behavior in this checkout |
|---|---|
@@ -77,7 +77,7 @@ Permissions apply to metadata and data: check table capabilities and the corresp
|---|---|
| `/` | Opens `/app`, or handles an Auth0 authorization callback. |
| `/login`, `/callback`, `/token` | Sign-in and supported authorization callback routes. |
| `/app` | Instance dashboard / app entry point. |
| `/app` | Instance home page: the `homeAppName` app's dashboard when the instance designates one the user may open (client-side redirect to `/app/{homeAppName}`; the sidebar logo and breadcrumb Home crumb link there and the sidebar has no separate Dashboard entry), otherwise Next's overview. |
| `/app/{appName}` | App home and its ordered widgets/sections. |
| `/app/{tableName}` | Query or key lookup, according to table capability. |
| `/app/{tableName}/create` | Create record. |
@@ -138,7 +138,7 @@ For a fixed choice list, put `inlinePossibleValueSource` on the QQQ field with `
The query page supports basic quick-filter chips and an advanced nested filter builder, quick search, sort, column visibility/order/width/pinning, density, pagination, CSV export, row selection, bulk process launch, column statistics, and table variants where declared. On desktop, Filter opens a panel containing the Basic/Advanced switch, sort controls, quick-filter chips, and advanced builder. On phones, Filter opens a sheet containing the same mode, sort, and quick-filter controls; Advanced shows the nested builder inside that sheet. Opening Filter preserves the selected mode. Advanced mode uses a single editable condition list with per-row removal and one confirmed Clear action in the footer. Complex criteria remain in Advanced mode; the basic chips expose metadata-selected fields and let the user add more. The Columns popup supports keyboard entry, search, reordering and focus return on close; embedded report column editors keep the surrounding editor focus. Query columns start at 150 px unless a `SIZE` adornment or a saved/resized width supplies another value. `TABLE_QUERY`, `TABLE_GET`, `TABLE_COUNT`, `TABLE_INSERT`, `TABLE_UPDATE`, `TABLE_DELETE`, `TABLE_EXPORT`, and `QUERY_STATS` gate operations alongside permissions. For tables with `usesVariants`, choose a backend-provided variant before querying; it is stored under `qqq.tableVariant.<table>` and sent to query, process and record view/edit/copy GET requests. Backend field-download URLs carry the same variant; inline BLOB data comes from the variant-specific record response. [REC-063](../acceptance/feature-matrix.md) verifies view/edit/copy against duplicate record IDs in two variants. URL parameters `page`, `pageSize`, `filter`, and `q` carry shareable query state ([`use-record-query.ts`](../../src/lib/hooks/use-record-query.ts)). Saved views have backend persistence, quick-view shortcuts and counts, modified-state comparison, and their own routes when the backend capabilities exist ([`use-saved-views.ts`](../../src/lib/hooks/use-saved-views.ts)). Browser local storage holds personal display preferences and last view state, not authoritative records or permissions.
Record pages render sections, field values, associated records, widgets, metadata-defined actions, help, and audit history. `QTableSection.alternatives.RECORD_VIEW` replaces the base section for this screen. Visible sections appear in metadata order; fields, sections and widget cards use `gridColumns` as twelfths on large screens and stack on narrow screens. When widths are not configured, Overview uses half-width cards on large screens; a selected section uses a full-width card with two field columns from the small breakpoint, stacking fields on phones. Explicit field, section, or widget widths take precedence over those presentation defaults. A collapsible section uses `collapsible.isCollapsible` and `initiallyOpen`, then remembers the choice under `qqq.recordView.collapsibleSectionOpenStates.<table>.<section>`; a section's rule overrides its widget's rule. The sticky desktop section sidebar is off by default; only `showRecordSidebar: true` in the table dashboard settings enables it. `recordViewActionsPlacement` can be set on the table or `supplementalInstanceMetaData.materialDashboard`, with the instance winning; `INLINE_WITH_PAGE_TITLE` places desktop actions beside the local record title, while the default `IN_IDENTITY_SECTION` keeps them in the identity controls. The identity card omits the primary-key T1 field and T1 values already contained in the record label; display values take precedence over raw values for that comparison. Desktop Copy and Delete buttons are omitted when the Actions menu already contains the corresponding enabled action, including in a submenu. An absent or disabled menu action preserves its available direct button. The Audit action reads through `GetAuditsForRecord` when permitted, otherwise through the `audit` table; its dialog groups entries by the viewer's local date, supports oldest/newest sort saved under `audit.sortDirection`, and reports when the 1,000-detail cap truncates results. Create/edit/copy forms use a centered container capped at `max-w-4xl` and follow field metadata, section layout, adjusters, possible-value choices, and server validation; `FILE_UPLOAD` width `full` spans the form grid, while `half` uses the default half width. Copy defaults to Base Copy, which duplicates editable fields; choose Full Copy to edit and submit loaded named associations with the new record. A widget section can edit association rows or form values through `WidgetFormContext` when its type and defaults allow it. Process launch actions can receive selected record IDs or a filter. `gotoFieldNames` in Material supplemental table metadata enables a Go To dialog for the primary key or configured unique keys. Developer views are capability and permission dependent. Consult [record and query acceptance rows](../acceptance/feature-matrix.md) for exact supported cases; do not infer coverage from the existence of a component alone.
Record pages render sections, field values, associated records, widgets, metadata-defined actions, help, and audit history. `QTableSection.alternatives.RECORD_VIEW` replaces the base section for this screen. Visible sections appear in metadata order; fields, sections and widget cards use `gridColumns` as twelfths on large screens and stack on narrow screens. When widths are not configured, Overview uses half-width cards on large screens; a selected section uses a full-width card with two field columns from the small breakpoint, stacking fields on phones. Explicit field, section, or widget widths take precedence over those presentation defaults. A collapsible section uses `collapsible.isCollapsible` and `initiallyOpen`, then remembers the choice under `qqq.recordView.collapsibleSectionOpenStates.<table>.<section>`; a section's rule overrides its widget's rule. The sticky desktop section sidebar is off by default; only `showRecordSidebar: true` in the table dashboard settings enables it. `recordViewActionsPlacement` can be set on the table or `supplementalInstanceMetaData.materialDashboard`, with the instance winning; `INLINE_WITH_PAGE_TITLE` places desktop actions beside the local record title, while the default `IN_IDENTITY_SECTION` keeps them in the identity controls. The identity card omits the primary-key T1 field and T1 values already contained in the record label; display values take precedence over raw values for that comparison. Desktop Copy and Delete buttons are omitted when the Actions menu already contains the corresponding enabled action, including in a submenu. An absent or disabled menu action preserves its available direct button. The Audit action reads through `GetAuditsForRecord` when permitted, otherwise through the `audit` table; its dialog groups entries by the viewer's local date, supports oldest/newest sort saved under `audit.sortDirection`, and reports when the 1,000-detail cap truncates results. Create/edit/copy forms use a centered container capped at `max-w-4xl` and follow field metadata, section layout, adjusters, possible-value choices, and server validation; `FILE_UPLOAD` width `full` spans the form grid, while `half` uses the default half width. Copy defaults to Base Copy, which duplicates editable fields; choose Full Copy to edit and submit loaded named associations with the new record. A widget section can edit association rows or form values through `WidgetFormContext` when its type and defaults allow it. Process launch actions can receive selected record IDs or a filter. `gotoFieldNames` in Material supplemental table metadata enables a Go To dialog for the primary key or configured unique keys. Developer views are capability and permission dependent. The record developer view (`/app/{table}/{id}/dev`) keeps the record screen's header and actions and adds a sticky section navigation over fields, API access (v1 routes and application APIs from their OpenAPI specs, filtered by the user's table permissions, with curl, fetch and Java examples that only use placeholder credentials), the panels returned by `GET /qqq/v1/table/{table}/{pk}/developer/panels` (rendered generically from their blocks: KEY_VALUES, TEXT, CODE, TABLE, LINKS and ACTIONS; app-relative or http(s) links only), relations, recent audits, associated scripts, raw JSON and a searchable table-metadata explorer. Modules and qbits add panels by registering a `DeveloperPanelProviderInterface` in QQQ; no frontend change is needed. Consult [record and query acceptance rows](../acceptance/feature-matrix.md) for exact supported cases; do not infer coverage from the existence of a component alone.
Query changes replace the current URL immediately, preserving unrelated parameters and the hash. Reload and browser back/forward restore the visible filter, sort, search and pagination state; changing those controls does not add a separate history entry.
@@ -312,6 +312,21 @@ These examples are payload fragments emitted by an application's backend rendere
{"gridColumns":6,"defaultValues":{"gridCols:sizeClass:xs":12,"gridCols:sizeClass:md":6}}
```
**Readable widths (Next UI).** Without per-breakpoint overrides, a widget's span follows the width of the dashboard grid itself (CSS container queries, so an open sidebar counts), not the viewport. Two `defaultValues` hints tune it, and neither needs a QQQ change:
- `gridMinWidth`: the narrowest the widget may be, in rem (a number, or `"28rem"` / `"448px"`). The widget shares a row only when it, and a neighbour of the same minimum, each get at least that much; otherwise it takes the full row. Values snap up to the tiers 16, 20, 24, 28, 32, 40, 48 or 64 rem; anything wider always takes the full row. Two `gridColumns: 6` widgets with `gridMinWidth: 28` pair from a 57.5rem (920 px) grid and go to thirds (`gridColumns: 4`) from 86.5rem.
- `gridProfile`: `widgets` (charts, statistics: half from a 40rem grid), `cards` or `table` (tables: half from 66rem), or `fields` (small tiles: half from 30rem). The default is `cards` for table-type widgets and `widgets` for the rest.
`gridCols:sizeClass:*` wins over `gridMinWidth`, which wins over `gridProfile`.
```json
{"type":"table","gridColumns":6,"defaultValues":{"gridMinWidth":28}}
```
Table widgets (`table`, `multiTable`) stop growing at 96rem (1536 px) so columns stay readable on ultra-wide screens; set `defaultValues.tableMaxWidth` to another width (rem, or a CSS length in `rem`, `px`, `ch`, `em` or `%`) or to `"none"`. Where a table widget is narrower than 30rem, or than its declared column widths need (6rem per `fr` or unsized column plus the fixed widths), each row renders as a stacked card of `label: value` lines instead of a table that scrolls sideways.
To cap a whole dashboard page on ultra-wide screens, give the app a supplemental app metadata of type `dashboardLayout` (a backend `QSupplementalAppMetaData` subclass whose `getType()` returns `dashboardLayout` and that is included in frontend metadata) with `maxContentWidth` (rem, or a CSS length). The page then centres at that width: `{"supplementalAppMetaData":{"dashboardLayout":{"maxContentWidth":160}}}`.
See [`widgetColumnClasses`](../../src/components/widgets/widget-utils.ts). Parent grids use the same sizing. A `parentWidget` payload such as `{"childWidgetNameList":["totals","trend"],"layoutType":"TABS"}` resolves those names in the instance widget registry. Children inherit parent request parameters and dropdown selections and fetch their own data; tab selection is remembered locally. A child omitted from permitted metadata cannot be configured into existence by listing its name.
**Dropdown data.** Metadata and response lists are paired by position; keep their lengths and order aligned. For the `region` dropdown example above, a response fragment is:
+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 RC11 preparation checkpoint: **[1.0.0-RC.10](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC9 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 [RC11](1.0.0-RC.11.md); confirm availability in the [RC11 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) before installing. The candidate adds searchable-selector and script-completion input-race corrections from [PR24](https://github.com/QRun-IO/qqq-frontend-next/pull/24). These changes are absent from immutable RC10. Candidate notes distinguish versioned local checks from public delivery and retain the historical browser failures and remaining RC limits. Local previews may intentionally preserve an earlier version.
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.
+60
View File
@@ -0,0 +1,60 @@
# Next UI 1.0.0-RC.12 — preparation
At this preparation checkpoint, RC12 is being prepared for application testing and **is not yet published**. [RC11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.12` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.12`. 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 `b0b0d469feb0b1bd473fbf189540e712ffc09807` (merge of [PR46](https://github.com/QRun-IO/qqq-frontend-next/pull/46)). Merged since RC11: PR25, PR27, PR28, PR29, PR30, PR32, PR35, PR36, PR38, PR40, PR42, PR43, PR44 and PR46. The SQL Workbench work in draft PR47 is not included.
## Backend requirements
RC12 runs on a published QQQ 4.1 candidate: **4.1.0-RC.1** or later. Standalone branding images (below) need **4.1.0-RC.2** or later, which contains QQQ #1009's `/qqq/branding/logo` and `/qqq/branding/icon` endpoints. RC12 does not publish QQQ 4.1.
## Product fixes
- Always show the record view's Actions menu when it has entries. Tables without processes or a custom `VIEW_SCREEN_ACTIONS` menu, and read-only tables, again reach Developer Mode and Audit (Material parity); Copy and Delete move into that menu instead of also appearing as separate buttons. [#45](https://github.com/QRun-IO/qqq-frontend-next/issues/45)
- Clear remembered query filters and saved-view selections on logout or a detected user identity change. Same-user resumes keep their query state; global appearance, density and legacy column preferences remain intact. This correction awaits its own branch acceptance. [#1006](https://github.com/QRun-IO/qqq/issues/1006)
- In searchable possible-value fields, invalidate old results immediately when the search term changes, so a debounced request cannot remove an option during its selection. [#970](https://github.com/QRun-IO/qqq/issues/970)
- Keep code suggestions open when a delayed textarea scroll event reports the position already used for the popup; dismiss them when the scroll position actually changes. [#954](https://github.com/QRun-IO/qqq/issues/954)
- Preserve focus on the search dialog's Clear control when keyboard interaction occurs before its scheduled initial focus. [#950](https://github.com/QRun-IO/qqq/issues/950)
- Preserve a clicked possible-value option in WebKit when blurring its search input would scroll the form between mousedown and mouseup. The selected value now reaches process submission and generated output. [#978](https://github.com/QRun-IO/qqq/issues/978)
- Keep authentication loading until explicit logout finishes, so a late logout response cannot erase cookies from a subsequent successful sign-in. [#984](https://github.com/QRun-IO/qqq/issues/984)
## Standalone branding and CSP (QQQ #1009, #1017)
The standalone Node server can route the exact root-relative `branding.logo` and
`branding.icon` paths published by authentication metadata through fixed backend
image roles. Local Node `public` files keep precedence; external image URLs and
Java dashboard serving are unchanged. This does not expose the whole application
overlay or add a general proxy.
This requires **a QQQ middleware-javalin build containing QQQ #1009's
`/qqq/branding/logo` and `/qqq/branding/icon` endpoints**. They are absent from
published **4.1.0-RC.1**. The minimum numbered release is not yet assigned; a
source build must record the exact backend commit/JAR hashes. Older deployments
must supply branding through their Node `public` files or an existing external
image URL. No automatic compatibility with old relative backend assets is claimed.
The preserved standalone qualification against public RC1 remains **5 passed,
38 failed** out of 43 cases because of missing branding assets. The original
staging failure is also retained. The corrected cross-repository pairing has
not yet received its browser qualification; neither #1009 nor #734 is closed by
focused unit/HTTP results.
## Browser testing and infrastructure
- Pin the official Playwright runner containing the fixed Linux WebKit network library, and retain native browser diagnostics for navigation failures. The previous bundle's heap corruption was reproduced; migration to a stable runner remains tracked separately. [#904](https://github.com/QRun-IO/qqq/issues/904), [#973](https://github.com/QRun-IO/qqq/issues/973)
- Correlate interrupted Firefox font diagnostics with real request and decode evidence, wait for initial fonts before the deliberate redirect, and observe transient feedback during the action that produces it. [#951](https://github.com/QRun-IO/qqq/issues/951), [#953](https://github.com/QRun-IO/qqq/issues/953), [#967](https://github.com/QRun-IO/qqq/issues/967)
- Wait for nested report metadata and the destination's owned iframe before advancing acceptance navigation. [#961](https://github.com/QRun-IO/qqq/issues/961), [#972](https://github.com/QRun-IO/qqq/issues/972)
- Verify Linux WebKit's downloaded filename normalization while retaining file-content and cleanup assertions. [#979](https://github.com/QRun-IO/qqq/issues/979)
- Verify actual browser refusal of disallowed framing with a control that renders when the protective headers are removed. [#981](https://github.com/QRun-IO/qqq/issues/981)
- Recognize Firefox's network-error diagnostic for the gallery's intentionally missing extension only after that same script request returns HTTP 404; preserve the raw evidence and reject unrelated failures. [#983](https://github.com/QRun-IO/qqq/issues/983)
- Select all ten ESB scenarios for both phone and tablet acceptance, retaining real broker delivery, lifecycle, replay, SQL and permission checks. Matrix references now use the central QQQ tickets. [#986](https://github.com/QRun-IO/qqq/issues/986), [#990](https://github.com/QRun-IO/qqq/issues/990)
## Verification checkpoint
PR checks on the RC12 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the full real-backend acceptance workflow) are recorded on the preparation PR. Tag acceptance and public-artifact verification are recorded in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) after delivery. No RC11 test count or artifact digest is attributed to RC12.
## Remaining RC limits
The RC11 limits carry forward unchanged. The RC11 tag acceptance recorded 2,990 passes and 2 owner-accepted failures (Firefox NAV-014 font diagnostic, tablet NAV-056 access-control diagnostics) under the documented secondary-browser/tablet exception ([#985](https://github.com/QRun-IO/qqq/issues/985), [#798](https://github.com/QRun-IO/qqq/issues/798)); it does not waive new product defects or authorize GA. Historical REC-054/PRC-061 observations remain under [#954](https://github.com/QRun-IO/qqq/issues/954) and [#970](https://github.com/QRun-IO/qqq/issues/970). Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#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). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin.
+43
View File
@@ -0,0 +1,43 @@
# Next UI 1.0.0-RC.13 — preparation
At this preparation checkpoint, RC13 is being prepared for application testing and **is not yet published**. [RC12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.13` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.13`. 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 `84c95e524a93056864e60863f5ab45357969e786` (merge of [PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53)). Merged since RC12: [PR47](https://github.com/QRun-IO/qqq-frontend-next/pull/47) (SQL Workbench), [PR49](https://github.com/QRun-IO/qqq-frontend-next/pull/49) (FILE_PREVIEW thumbnails and lightbox), [PR50](https://github.com/QRun-IO/qqq-frontend-next/pull/50) (responsive layouts, readable child lists, compact dates) and [PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53) (instance home app). The single page scroll container ([PR54](https://github.com/QRun-IO/qqq-frontend-next/pull/54), its WebKit acceptance not yet green) and widget width readability ([PR55](https://github.com/QRun-IO/qqq-frontend-next/pull/55)) are not included; they follow in RC14.
## Backend requirements
RC13 runs on a published QQQ 4.1 candidate: **4.1.0-RC.1** or later. Standalone branding images need **4.1.0-RC.2** or later (QQQ #1009). **FILE_PREVIEW thumbnails and the instance home app need QQQ 4.1.0-RC.3 or later**, which carries the `FILE_PREVIEW` adornment ([qqq#1056](https://github.com/QRun-IO/qqq/issues/1056)) and `QInstance.homeAppName` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)); on older backends neither appears and the rest of RC13 works unchanged. The SQL Workbench page needs the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit, **1.0.0-RC.1** or later, on the backend. RC13 does not publish QQQ 4.1.
## New features
- SQL Workbench: render the `SQL_WORKBENCH` widget of the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit as a super-user SQL notebook.
- Personal and shared notebooks with SQL and markdown cells, `:parameters`, versions and pinned results.
- A CodeMirror 6 editor with dialect highlighting, schema and QQQ-name autocomplete, and formatting.
- A typed, sortable, paged result grid with CSV/XLSX/JSON export, quick charts, and EXPLAIN plan trees.
- Read-only runs by default, and a two-step write confirmation for users with the write permission.
- CodeMirror and the formatter load only on that page (new `lazyChunks` budgets).
## Features
- Support an instance home app. When the backend designates one with `QInstance.homeAppName` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)), and the user may open it, `/app` shows that app's dashboard (a client-side redirect, matching backends that redirect full page loads of `/app/`), the sidebar logo and the breadcrumb Home crumb link there, and the sidebar drops its separate Dashboard entry. Without a home app, or with one the user may not open, `/app` remains Next's overview.
- Show `FILE_PREVIEW`-adorned fields as thumbnails, and open a preview on click or Enter. Thumbnails appear in the query grid and card view, the record view (a larger thumbnail with the file name), associated-record tables, child-record list widgets and record-grid widgets. They load lazily at a fixed size, so loading does not shift the layout, with a loading pulse and a file-type icon fallback. The preview is an accessible dialog: focus is trapped and restored, Escape closes it, and Left/Right/Home/End step through the other previews on the same screen. It offers zoom and fit, open-original and download links, and shows type, size, dimensions and caption. PDFs served by the backend (same-origin `/qqq/` paths) and video/audio play inline; other types, and other or cross-origin PDFs (frames are limited to `'self'`), get a file card or open in a new tab. Thumbnails are shown only for explicitly adorned fields; plain URL values are never auto-detected. URLs are limited to http(s), same-origin paths and inline images. The lightbox loads on demand (about 3.5 KB gzip). Requires the QQQ backend adornment from [qqq#1056](https://github.com/QRun-IO/qqq/issues/1056).
## Product fixes
- Size record-view cards, dashboard widgets and form fields by the width of their own grid (container queries) rather than the viewport: table panels sit side by side only when each is readable and otherwise stack, cards keep their own height, fields share a row only when they fit, and very wide screens use the room. No horizontal page scroll from 360px up.
- Make child-record lists in record-view panels readable: they show the columns that matter most and fit (the record name, status and boolean columns, the most recently changed date; the key and long text last), with `+N more columns` and View All for the rest. Cells align to the top and long text is clamped with the full text on hover. A widget's `onlyIncludeFieldNames` order is respected.
- Show dates compactly everywhere: `6:33 PM` today, `Jul 5, 6:33 PM` this year, `Jul 5, 2025` before, on one line with the full zoned timestamp on hover, in the viewer's zone and locale. DATE values are no longer shifted a day west of UTC. Exports are unchanged.
## Bundle budget
The record developer route `/app/_/_/dev` and the table developer route `/app/_/dev` each have a 360 KB first-load cap (measured 354.9 KB and 350.8 KB on the combined tree); every other route keeps the 350 KB default. The SQL Workbench editor (CodeMirror 6) and formatter load on demand under separate `lazyChunks` budgets; the FILE_PREVIEW lightbox is a lazy chunk.
## Verification checkpoint
PR checks on the RC13 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the full real-backend acceptance workflow) are recorded on the preparation PR. During integration the WebKit/tablet NAV-016, NAV-023 and NAV-056 access-control diagnostics and one Firefox image-decode diagnostic (SEC-026) recurred intermittently and passed on rerun; they belong to the tracked [#985](https://github.com/QRun-IO/qqq/issues/985) / [#951](https://github.com/QRun-IO/qqq/issues/951) investigations and no assertion was relaxed. Tag acceptance and public-artifact verification are recorded in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) after delivery. No RC12 test count or artifact digest is attributed to RC13.
## Remaining RC limits
The RC12 limits carry forward unchanged: Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#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). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin.
+27
View File
@@ -0,0 +1,27 @@
# Next UI 1.0.0-RC.14 — preparation
At this preparation checkpoint, RC14 is being prepared as an urgent fix for application testing and **is not yet published**. [RC13](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.14` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.14`. 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 `b699204d8736f5cfcc61358c38e86d89ab3d5541` (merge of [PR57](https://github.com/QRun-IO/qqq-frontend-next/pull/57)). Merged since RC13: [PR52](https://github.com/QRun-IO/qqq-frontend-next/pull/52) (Next.js 16.3.8 security update and sharp) and [PR57](https://github.com/QRun-IO/qqq-frontend-next/pull/57) (record search tolerates results without a label). RC14 is cut early because global record search is broken in production on RC13. The README adoption guide (PR58), the record developer view (PR61), the single page scroll container (PR54), widget width readability (PR55) and the NAV-056 diagnostic fix are not included; they follow in RC15.
## Backend requirements
Unchanged from RC13: 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); **4.1.0-RC.3** or later for FILE_PREVIEW thumbnails ([qqq#1056](https://github.com/QRun-IO/qqq/issues/1056)) and the instance home app ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)); [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) **1.0.0-RC.1** or later for the SQL Workbench page. The search fix needs no backend change; the server-side companion [qqq#1068](https://github.com/QRun-IO/qqq/pull/1068) stops emitting blank labels. RC14 does not publish QQQ 4.1.
## Product fixes
- Record search no longer fails when one result has no label. QQQ serializes JSON without empty values, so a record whose label is empty arrived without `recordLabel`, and the strict response schema rejected the whole response ("Record search failed. Try again."). The response envelope is still checked strictly, but each result is now validated on its own: `tableName` and `recordId` are required (a numeric `recordId` is accepted), a result without a label is shown as "{table label} {record id}", and a result that cannot link to a record is skipped with a console warning while the others render.
## Dependencies
- Next.js 16.3.6 to 16.3.8 (security fixes, including the image-optimization SSRF advisory GHSA-cjq9-62q9-8jv4) and the matching `sharp` update ([PR52](https://github.com/QRun-IO/qqq-frontend-next/pull/52)).
## Verification checkpoint
PR checks on the RC14 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the full real-backend acceptance workflow) are recorded on the preparation PR. During integration the WebKit/tablet NAV-023 diagnostic recurred intermittently and passed on rerun. **Known failure accepted for this release:** the WebKit NAV-056 diagnostic (appearance preferences explain an application theme override) failed on [PR57](https://github.com/QRun-IO/qqq-frontend-next/pull/57) and also fails on main without RC14's changes; the owner explicitly approved merging and releasing RC14 with it because production record search was broken. It is tracked under [#985](https://github.com/QRun-IO/qqq/issues/985), a fix is in progress for RC15, and no assertion was relaxed. Tag acceptance and public-artifact verification are recorded in the [RC14 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.14) after delivery. No RC13 test count or artifact digest is attributed to RC14.
## Remaining RC limits
The RC13 limits carry forward unchanged: Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#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). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin.
+41
View File
@@ -0,0 +1,41 @@
# Next UI 1.0.0-RC.15 — preparation
At this preparation checkpoint RC15 **is not yet published**. [RC14](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.14) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.15` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.15`. 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 `5de69be`. Merged since RC14: [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).
## Backend requirements
A published QQQ 4.1 candidate, **4.1.0-RC.1** or later; **4.1.0-RC.3** or later for FILE_PREVIEW and the instance home app; **4.1.0-RC.4** or later for record developer panels ([qqq#1076](https://github.com/QRun-IO/qqq/pull/1076)); [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) 1.0.0-RC.1 or later for the SQL Workbench page (1.0.0-RC.4 or later opens developer-panel SQL in the Runner); [qbit-assistant](https://github.com/QRun-IO/qbit-assistant) 0.1.0-RC.3 or later for the assistant panel (without it nothing renders). RC15 does not publish QQQ 4.1.
## Features
- **QQQ Assistant panel (Phase 1: read-only questions and navigation).** When the `qbit-assistant` backend is installed and `GET /qqq/v1/assistant/config` enables it for the user, a header button (with unread badge), `Ctrl/⌘+J` and a command palette "Ask Assistant" item open a chat panel. It docks beside the page from 1024 px (resizable 360-640 px, its own scrolling message list) and is a full-height sheet below that. Replies stream over SSE with resume after a dropped connection and a Stop button; tool steps, record chips, records/record/count/history renders and chart widgets come from structured payloads, and markdown renders without raw HTML or images, with links only to same-origin `/app` paths. `?assistant=open&assistantPrompt=...` opens a new conversation with the prompt pre-filled. Without the backend nothing renders and the shortcut is inert. The panel code loads on first open (about 1.7 KB added to first load).
- **Search** ([PR59](https://github.com/QRun-IO/qqq-frontend-next/pull/59)): search-as-you-type quick search with richer results (highlights, details, per-table status from QQQ 4.1.0-RC.4 record search) and a URL-addressable search page; the search page and previews load on demand.
- **SQL Workbench dark mode and standard controls** ([PR78](https://github.com/QRun-IO/qqq-frontend-next/pull/78)).
## Product fixes
- **Readable dashboard widget widths** ([PR55](https://github.com/QRun-IO/qqq-frontend-next/pull/55)): per-widget minimum widths, ultra-wide caps and a table card layout.
- **Record developer view** (`/app/{table}/{id}/dev`): the record screen's navigation (back to the record, record title, Edit, Actions, Audit, breadcrumbs) and a sticky section bar (a section picker on phones, wrapping links elsewhere) over Fields (display and raw values, types, possible-value sources and adornments, filterable), **API Access** (the v1 routes this screen uses and every application API that exposes the table, read from its OpenAPI spec and limited to what the user may do, with copyable curl, fetch and Java examples built from the record's values, placeholder credentials only, and links to the API docs and specs), **developer panels** from QQQ's provider contract ([qqq#1076](https://github.com/QRun-IO/qqq/pull/1076): record security, ESB events, and any qbit's panel such as quick search, SQL workbench or the assistant, shown only when installed and active for the table; QQQ before 4.1.0-RC.4 has no panels endpoint, so the view shows none and asks for it only once per session), Relations, recent Audit, associated scripts, raw JSON and a searchable Table Metadata explorer. Readable from 360 to 3200px with no sideways scrolling. The view loads on demand, keeping the route's first-load JavaScript at 333.5 KB (budget 360 KB).
- One scroll container per page: on long pages the document no longer grows a second scrollbar beside the content area's (and no longer slides the header off-screen). The app shell and `<main>` are now positioned, so absolutely positioned descendants such as `sr-only` text (the company footer's "opens in a new tab") stay inside the scrolling area instead of stretching the document. Covered by `tests/e2e/single-scroll-container.spec.ts` at 390-3200px.
## Browser testing and infrastructure
- Dependabot keeps at most three open PRs per ecosystem ([PR77](https://github.com/QRun-IO/qqq-frontend-next/pull/77)).
- The WebKit NAV-056 diagnostic race is fixed in the acceptance tests ([PR63](https://github.com/QRun-IO/qqq-frontend-next/pull/63)).
- README: "Adopting in another app" ([PR58](https://github.com/QRun-IO/qqq-frontend-next/pull/58)).
## Bundle budget
Owner decision 2026-10-12: the total JavaScript cap (every on-demand feature) is 1100 KB gzip and grows with features; first-load route caps, the 23 KB shared CSS cap and the lazy-chunk caps stay strict. See `perf-budget.json`.
## Verification checkpoint
PR checks on the RC15 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the real-backend acceptance workflow) are recorded on the preparation PR. Tag acceptance and public-artifact verification are recorded in the RC15 GitHub release after delivery. No RC14 test count or artifact digest is attributed to RC15.
## Remaining RC limits
The RC14 limits carry forward unchanged: Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#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).
+35
View File
@@ -0,0 +1,35 @@
# Next UI 1.0.0-RC.16 — preparation
At this preparation checkpoint RC16 **is not yet published**. [RC15](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.15) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.16` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.16`. 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 `6dea1c3` (merge of [PR80](https://github.com/QRun-IO/qqq-frontend-next/pull/80)). Merged since RC15: [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).
## Backend requirements
A published QQQ 4.1 candidate, **4.1.0-RC.1** or later; **4.1.0-RC.4** or later for record developer panels; **4.1.0-RC.5** or later for the record picker ([qqq#1099](https://github.com/QRun-IO/qqq/pull/1099)), the form validation limits it reads from metadata ([qqq#1100](https://github.com/QRun-IO/qqq/pull/1100)) and the date filter `StartOfPeriod` presets ([qqq#1115](https://github.com/QRun-IO/qqq/pull/1115)); on earlier backends these features fall back to the RC15 behavior. The Alerts bell and record panel render only when the instance metadata includes the `alerts` app ([qbit-alerts](https://github.com/QRun-IO/qbit-alerts) 0.1.0-RC.1 or later); otherwise they send no requests. RC16 does not publish QQQ 4.1.
## Features
- **Record picker** ([PR80](https://github.com/QRun-IO/qqq-frontend-next/pull/80)): possible-value fields over large tables open a searchable, filterable, paged picker dialog (lazy-loaded, with thumbnails where the table has images).
- **Form validation** ([PR81](https://github.com/QRun-IO/qqq-frontend-next/pull/81)): quiet, metadata-driven validation (required fields, too-long behavior, rejecting value ranges) before any request; server messages are still shown once.
- **Date-first filters** ([PR82](https://github.com/QRun-IO/qqq-frontend-next/pull/82)): date and date-time filters with presets, an include-time toggle, and the `X-QQQ-UserTimezone` header.
- **Alerts bell and record panel** ([PR85](https://github.com/QRun-IO/qqq-frontend-next/pull/85)): a header bell and a record panel for qbit-alerts, gated on the `alerts` app in metadata.
- **Resizable navigation** ([PR79](https://github.com/QRun-IO/qqq-frontend-next/pull/79)) and **search preview thumbnails** ([PR84](https://github.com/QRun-IO/qqq-frontend-next/pull/84)).
## Product fixes
- Range errors use the backend wording ([PR87](https://github.com/QRun-IO/qqq-frontend-next/pull/87)).
## Bundle budget
Shared CSS cap 24 KB ([PR86](https://github.com/QRun-IO/qqq-frontend-next/pull/86), measured 23.07 KB after PR79); total JavaScript cap 1100 KB (owner decision 2026-10-12). Route and lazy-chunk caps are unchanged.
## Verification checkpoint
Each merged PR passed real-backend acceptance (Chromium, Firefox, WebKit, mobile/tablet) on its own head against QQQ develop with the 4.1.0-RC.5 batch. PR checks on the RC16 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC16 GitHub release after delivery.
## Remaining RC limits
The RC15 limits carry forward unchanged: Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#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.17 — preparation
At this preparation checkpoint RC17 **is not yet published**. [RC16](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.16) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.17` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.17`. 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 `6ed2575` (merge of [PR89](https://github.com/QRun-IO/qqq-frontend-next/pull/89)). Merged since RC16: [PR89](https://github.com/QRun-IO/qqq-frontend-next/pull/89) (notification center).
## Backend requirements
Unchanged from RC16 (QQQ **4.1.0-RC.5** or later for the record picker, validation limits and date presets). The notification center renders only when the instance metadata includes the `alerts` app, and needs [qbit-alerts](https://github.com/QRun-IO/qbit-alerts) **0.1.0-RC.2** or later (user notifications); without it nothing renders and no requests are sent. RC17 does not publish QQQ 4.1.
## Features
- **Notification center** ([PR89](https://github.com/QRun-IO/qqq-frontend-next/pull/89)): a bell inbox and notification list with read state and preferences for qbit-alerts user notifications. The center loads on demand (lazy chunk `notificationCenter`, 15.2 KB gzip).
## Bundle budget
**Both the total JavaScript and the first-load budgets are within 2 KB of their caps:** total JS 1098.0 / 1100 KB and `/app` first load 349.2 / 350 KB. The next Next feature needs a measured cap raise (under the owner decision of 2026-10-12) or a code-splitting pass first.
## Verification checkpoint
PR89 passed real-backend acceptance on Chromium, Firefox, WebKit and mobile/tablet. PR checks on the RC17 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC17 GitHub release after delivery.
## Remaining RC limits
The RC16 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).
+32
View File
@@ -0,0 +1,32 @@
# Next UI 1.0.0-RC.18 — preparation
At this preparation checkpoint RC18 **is not yet published**. [RC17](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.17) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.18` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.18`. 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 `0282328` (merge of [PR94](https://github.com/QRun-IO/qqq-frontend-next/pull/94)). Merged since RC17: [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).
## Backend requirements
Unchanged from RC17 (QQQ **4.1.0-RC.5** or later). The Authentik policy editor needs [qbit-authentik](https://github.com/QRun-IO/qbit-authentik) **0.1.0-RC.2** or later (cut-over tooling in RC.3). The assistant pending-action card and persona need [qbit-assistant](https://github.com/QRun-IO/qbit-assistant) **0.1.0-RC.6** or later. Each renders only when its backend is installed. RC18 does not publish QQQ 4.1.
## Features
- **Authentik policy editor** ([PR95](https://github.com/QRun-IO/qqq-frontend-next/pull/95)): the `AUTHENTIK_POLICY_EDITOR` widget for qbit-authentik policies.
- **Assistant pending-action card and persona** ([PR94](https://github.com/QRun-IO/qqq-frontend-next/pull/94)): the assistant panel shows proposed actions as a pending-action card, adds a persona, and refreshes the page after a write.
## Browser testing and infrastructure
- Faster sharded real-backend acceptance with a quarantine lane and an acceptance gate ([PR97](https://github.com/QRun-IO/qqq-frontend-next/pull/97)).
## Bundle budget
The bundle budget is **report-only** (owner decision 2026-10-13, [PR91](https://github.com/QRun-IO/qqq-frontend-next/pull/91)): `perf-budget.json` has `"enforce": false`, and [PR98](https://github.com/QRun-IO/qqq-frontend-next/pull/98) makes the script honor it. Size overruns print as warnings with every number still reported, while structural problems (a lazy chunk on a first load, a missing chunk marker) still fail. Total JavaScript is now above the 1100 KB reference value.
## Verification checkpoint
PR94 and PR95 passed real-backend acceptance. PR checks on the RC18 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC18 GitHub release after delivery.
## Remaining RC limits
The RC17 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).
+28
View File
@@ -0,0 +1,28 @@
# Next UI 1.0.0-RC.19 — preparation
At this preparation checkpoint RC19 **is not yet published**. [RC18](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.18) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.19` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.19`. 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 `a2f2318` (merge of [PR101](https://github.com/QRun-IO/qqq-frontend-next/pull/101)). Merged since RC18: [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).
## Backend requirements
Unchanged from RC18 (QQQ **4.1.0-RC.5** or later; the related-record picker builds on the RC.5 picker contract, qqq#1099). The Read-only / Write switch needs qbit-assistant **0.1.0-RC.6** or later. RC19 does not publish QQQ 4.1.
## Features
- **Related-record picker** ([PR96](https://github.com/QRun-IO/qqq-frontend-next/pull/96)): pick related records in single, multi-child and link modes, built on the media picker dialog.
- **Assistant Read-only / Write switch** ([PR101](https://github.com/QRun-IO/qqq-frontend-next/pull/101)): each conversation can be switched between read-only and write.
## Bundle budget
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
## Verification checkpoint
PR96 and PR101 passed real-backend acceptance. PR checks on the RC19 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC19 GitHub release after delivery.
## Remaining RC limits
The RC18 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.20 — preparation
At this preparation checkpoint RC20 **is not yet published**. [RC19](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.19) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.20` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.20`. 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 `438eda3` (merge of [PR111](https://github.com/QRun-IO/qqq-frontend-next/pull/111)). Merged since RC19: [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).
## Backend requirements
QQQ **4.1.0-RC.5** or later for everything in RC19. The new screens use the UI API 1.2 contract and the collaboration and data-analysis cores from QQQ **4.1.0-RC.6**: the profile panels need the data-analysis aggregates (qqq#1139, qqq#1145), and favorites and server recents need the collaboration core (qqq#1138 and follow-ups). Assistant presentation blocks need qbit-assistant **0.1.0-RC.9** or later. RC20 does not publish QQQ 4.1.
## Features
- **Readable assistant answers** ([PR111](https://github.com/QRun-IO/qqq-frontend-next/pull/111)): GFM markdown (tables, task lists) and presentation blocks from qbit-assistant 0.1.0-RC.9.
- **Data-analysis profile panels** ([PR103](https://github.com/QRun-IO/qqq-frontend-next/pull/103)): column and table profiles with drill-down.
- **Favorites and recents** ([PR105](https://github.com/QRun-IO/qqq-frontend-next/pull/105)): favorites, server-side recents and palette favorites.
- **UI API version contract** ([PR92](https://github.com/QRun-IO/qqq-frontend-next/pull/92), [PR104](https://github.com/QRun-IO/qqq-frontend-next/pull/104)).
## Browser testing and infrastructure
- Acceptance ESB extension fix ([PR109](https://github.com/QRun-IO/qqq-frontend-next/pull/109)).
## Bundle budget
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
## Verification checkpoint
The merged PRs passed real-backend acceptance. PR checks on the RC20 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC20 GitHub release after delivery.
## Remaining RC limits
The RC19 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.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
Main at `a6c95b1` (merge of [PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125)). Merged since RC20: [PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125) (attach a file in the assistant panel for bulk load).
## Backend requirements
QQQ **4.1.0-RC.6** or later for everything in RC20. The assistant file attach feeds assisted bulk load: it uses the bulk-load UI API operations from QQQ **4.1.0-RC.7** (qqq#1179) and the bulk-load tools in qbit-assistant **0.1.0-RC.10** (qbit-assistant#38). The upload itself uses `POST /qqq/v1/uploads` from [qqq#1181](https://github.com/QRun-IO/qqq/pull/1181), which is not yet in a QQQ release; until it ships, an attach attempt fails with a toast saying the file could not be attached, and nothing else changes. RC21 does not publish QQQ 4.1.
## Features
- **Assistant file attach** ([PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125)): attach a file in the assistant panel to start an assisted bulk load.
## Bundle budget
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
## Verification checkpoint
PR checks on the RC21 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC21 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).
+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).
+138
View File
@@ -0,0 +1,138 @@
# Next UI 1.0.0-RC.37 — published collaboration candidate
This candidate is published. Artifacts are
`com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.37` and
`ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.37`. RC.36 remains immutable.
Publication does not deploy Kof22 or authorize final 1.0.
## Source and changes
Prepared from collaboration source `dce92cfba1f02f7969c19b59d4442f5488b3821e`.
The RC preparation changes the package and Maven versions together and retains
main's managed orb 0.8 publication configuration. This source
includes the previously tested collaboration source
`8d372142f8af0c41a75a353edc6d150c33ccf613` plus a My Work browser regression test.
- Presence opens server-issued leases and renews them through
`/presence/leases` and `/presence/leases/renew`. Record/table viewers and
editors receive updates through the existing SSE/HTTP transport.
- Authentication retains the Auth0 display profile while using canonical
backend presence identity. Departure, expired leases and reconnect behavior
remain covered by the collaboration test suite.
- Marking a mention read refreshes My Work; the additional browser test checks
that behavior.
- Changes since RC.36 also include assistant conversation navigation, unread
state and attachment recovery, startup branding, shared HTML sanitization,
dependency fixes and centralized CI. This is not a presence-only diff from
RC.36.
- Development dependencies are aligned on Storybook 10.6.2 and Vitest 4.1.12
(including `@vitest/mocker`) for
[GHSA-f2ph-cmg5-wxc8](https://github.com/storybookjs/storybook/security/advisories/GHSA-f2ph-cmg5-wxc8)
and [GHSA-xp26-3454-mjgp](https://github.com/vitest-dev/vitest/security/advisories/GHSA-xp26-3454-mjgp).
These advisories affect development-server tooling; they do not describe a
Storybook server in the static dashboard package. Integration is tracked in
[QQQ #1327](https://github.com/QRun-IO/qqq/issues/1327).
## Backend pairing
Use a backend implementing the issued-lease collaboration API. The verified
earlier pair is QQQ `4.1.0-collab-33464d37e-SNAPSHOT` with collaboration
`0.1.0-collab-8f348da-SNAPSHOT` and Redis `0.1.0-redis-7fe5492-SNAPSHOT`.
The selectable-field follow-up pair is QQQ
`4.1.0-virtual-field-values-1196-cc3981bb3-SNAPSHOT` with collaboration
`0.1.0-field-values-1196-ecff347-SNAPSHOT`; that pair's independent API probe
does not establish packaged frontend acceptance.
Configure authenticated scope, Redis namespaces and lifecycle, SSE delivery and
connection affinity in the host. Presence is advisory; stale-save protection
still requires backend record versions. Kof22 PR338's RC.36 frontend must be
updated only after this artifact is published and independently verified.
## Qualification and release gates
- [x] Local typecheck and all 3,664 unit tests pass. Lint passes with six
warnings and no errors; license headers pass.
- [x] Canonical Circle caller and immutable GitHub worker bridge validate locally.
- [x] Patched toolchain passes TypeScript, all 3,664 unit tests and Storybook
static build (bundle-size warnings remain).
- [x] Exact RC source passes hosted CI and full browser acceptance.
- [x] Local static export builds and packages under `next-dashboard/`; development
mock worker is excluded.
- [x] Fresh immutable RC tag publishes matching Maven and image versions.
- [x] Public JAR/POM checksums and image digest are recorded.
- [x] Browser acceptance runs against the downloaded frontend JAR and the
selected published backend pair.
- [ ] Kof22 integration and isolated staging qualification are complete before
production rollout is proposed.
Earlier source acceptance run
[39683690943](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/39683690943)
is supporting evidence, not qualification of this RC package. The versioned tag
triggers Maven and container publication; the inspected workflows contain no
host deployment step.
Local Java 21 Maven packaging passed on October 26. ZIP inspection confirmed
the home and dynamic record fallback pages, both issued-lease endpoint strings
in the packaged JavaScript, and exclusion of `mockServiceWorker.js`. This is
package-content evidence, not a substitute for browser protocol acceptance.
## Published artifact evidence
Signed tag `v1.0.0-RC.37` points to
`13abaa795d71f0a2213409fb910003920b82f3c3`. Source acceptance
[40333860200](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/40333860200)
passed all 18 real-backend browser shards and the aggregate gate. One shard
required a targeted retry after a 40-minute system-package download timeout;
its initial attempt never reached browser tests. CodeQL, unit/coverage,
collaboration browser profiles and both image architectures passed.
Circle publisher 758 and node build 759 passed. The tag's separate acceptance
run [40342829221](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/40342829221)
published its images successfully, but its tablet shard and one targeted retry
both timed out after 40 minutes downloading Ubuntu browser dependencies, before
browser tests started. The aggregate gate therefore did not pass. Candidate CI
now pins verified orb commit `eeb76914ad49fcdb2197529481de55d1e35a008a`, whose
browser installation steps use bounded APT retries/timeouts and the official
HTTPS mirror fallbacks. This changes candidate CI only; the signed RC tag and
published artifacts remain unchanged. Updated-worker qualification
[40357830356](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/40357830356)
passed on candidate commit `c8f79847f3174425f02538c3c1b345eff86fda53`: all 18
real-backend browser shards and the aggregate gate succeeded, with no failed
jobs. This qualifies the corrected candidate workflow; it does not change the
failed historical tag run or replace the earlier source acceptance evidence.
| Public artifact | SHA-256 / OCI digest |
| --- | --- |
| Maven POM | `4761d2fd07f60da226fb179837b053a30a5a198e8238189d3912fcb3a69849f9` |
| Maven JAR | `c9fa2eb2381a47c8104f82f4e940d2bfa8ba67e2341298dde0de4e21ad3ff1ab` |
| Multi-platform image index | `sha256:0b0fa9906eed60df1d23f28390888708587361a168b20fef92f49e6090723822` |
| Linux AMD64 image | `sha256:a6e5ee0947b5e12cbf821741a2a72fdac7c705d1276f59bb9497807cdd020f75` |
| Linux ARM64 image | `sha256:91999495b5958a2b1d67a43fd768c274690fea3eb3e79eaaab0e5924c8335eb4` |
The POM and JAR were downloaded directly from Maven Central; both match the
repository's SHA-512 checksums. Coordinate and JAR resource checks passed.
Repository checksums establish transfer integrity, not signature identity.
The immutable AMD64 image served `/app/person` with HTTP 200 in a disposable
container; ARM64 has hosted build verification, not a local runtime claim.
The downloaded public JAR passed Chromium acceptance with six mock identities
across two independent JVMs using the published selectable-field backend pair
above: each browser saw the other five users; all four viewers saw exactly two
editors; editor field focus, retained drafts, table projection and departure
passed. Redis connection/subscription metrics, persisted framework UP health
results and standard health-record navigation also passed. There were no browser
page errors, and all test-owned processes and Redis resources were cleaned up.
The hosts use independently seeded memory records and mock authentication;
these checks do not prove production identity-provider or shared-database save
behavior. Kof22 staging adoption remains outstanding.
## Limits and rollback
This branch does not include separate frontend PR194/195 or the independent
backend attachment, My Work and Redis-recovery follow-ups. Those remain tracked
in the QQQ project.
If packaged acceptance fails, withhold adoption and fix the candidate. After
deployment, restore a previously qualified frontend/backend pair together;
RC.36 must not be substituted against a backend requiring issued leases.
No host data migration, production deployment or GA approval is implied.
+14 -50
View File
@@ -1,62 +1,26 @@
# Next UI — unreleased
[RC11 is published and immutable](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11). It includes the searchable-selector and script-completion corrections from [PR24](https://github.com/QRun-IO/qqq-frontend-next/pull/24); its release details remain in [RC11 notes](1.0.0-RC.11.md). Current main also contains the later menu, copy-focus and grid fixes, which are not in that published artifact. 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).
[RC37 collaboration candidate](1.0.0-RC.37.md) records the published issued-lease
frontend, artifact receipts, backend pairing and outstanding host qualification.
## New features
[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).
- SQL Workbench: render the `SQL_WORKBENCH` widget of the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit as a super-user SQL notebook.
- Personal and shared notebooks with SQL and markdown cells, `:parameters`, versions and pinned results.
- A CodeMirror 6 editor with dialect highlighting, schema and QQQ-name autocomplete, and formatting.
- A typed, sortable, paged result grid with CSV/XLSX/JSON export, quick charts, and EXPLAIN plan trees.
- Read-only runs by default, and a two-step write confirmation for users with the write permission.
- CodeMirror and the formatter load only on that page (new `lazyChunks` budgets).
## Features
- **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
- Clear remembered query filters and saved-view selections on logout or a detected user identity change. Same-user resumes keep their query state; global appearance, density and legacy column preferences remain intact. This correction awaits its own branch acceptance. [#1006](https://github.com/QRun-IO/qqq/issues/1006)
- In searchable possible-value fields, invalidate old results immediately when the search term changes, so a debounced request cannot remove an option during its selection. [#970](https://github.com/QRun-IO/qqq/issues/970)
- Keep code suggestions open when a delayed textarea scroll event reports the position already used for the popup; dismiss them when the scroll position actually changes. [#954](https://github.com/QRun-IO/qqq/issues/954)
- Preserve focus on the search dialog's Clear control when keyboard interaction occurs before its scheduled initial focus. [#950](https://github.com/QRun-IO/qqq/issues/950)
- Preserve a clicked possible-value option in WebKit when blurring its search input would scroll the form between mousedown and mouseup. The selected value now reaches process submission and generated output. [#978](https://github.com/QRun-IO/qqq/issues/978)
- Keep authentication loading until explicit logout finishes, so a late logout response cannot erase cookies from a subsequent successful sign-in. [#984](https://github.com/QRun-IO/qqq/issues/984)
- **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).
## Browser testing and infrastructure
- Pin the official Playwright runner containing the fixed Linux WebKit network library, and retain native browser diagnostics for navigation failures. The previous bundle's heap corruption was reproduced; migration to a stable runner remains tracked separately. [#904](https://github.com/QRun-IO/qqq/issues/904), [#973](https://github.com/QRun-IO/qqq/issues/973)
- Correlate interrupted Firefox font diagnostics with real request and decode evidence, wait for initial fonts before the deliberate redirect, and observe transient feedback during the action that produces it. [#951](https://github.com/QRun-IO/qqq/issues/951), [#953](https://github.com/QRun-IO/qqq/issues/953), [#967](https://github.com/QRun-IO/qqq/issues/967)
- Wait for nested report metadata and the destination's owned iframe before advancing acceptance navigation. [#961](https://github.com/QRun-IO/qqq/issues/961), [#972](https://github.com/QRun-IO/qqq/issues/972)
- Verify Linux WebKit's downloaded filename normalization while retaining file-content and cleanup assertions. [#979](https://github.com/QRun-IO/qqq/issues/979)
- Verify actual browser refusal of disallowed framing with a control that renders when the protective headers are removed. [#981](https://github.com/QRun-IO/qqq/issues/981)
- Recognize Firefox's network-error diagnostic for the gallery's intentionally missing extension only after that same script request returns HTTP 404; preserve the raw evidence and reject unrelated failures. [#983](https://github.com/QRun-IO/qqq/issues/983)
- Select all ten ESB scenarios for both phone and tablet acceptance, retaining real broker delivery, lifecycle, replay, SQL and permission checks. Matrix references now use the central QQQ tickets. [#986](https://github.com/QRun-IO/qqq/issues/986), [#990](https://github.com/QRun-IO/qqq/issues/990)
## Verification and remaining work
Current main `5ca3e0b` has a complete [3,010-case first-attempt acceptance pass](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36989591498): 620 per desktop browser and 575 per touch profile. That run qualifies the main tree before the query-state correction above. The earlier checkpoints below are retained separately.
The [combined acceptance run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36942612338) on head `7ff90f1` (the same tree as merge `289eca19`) passed all 2,972 executed cases: 614 each in Chromium, Firefox and WebKit, and 565 each on phone and tablet. All desktop gates covered 401 required rows. The workflow still failed its touch coverage gate because ten ESB rows were not selected; that original failure remains retained under [#990](https://github.com/QRun-IO/qqq/issues/990). After adding the selection markers, a separate native Linux run passed all 20 ESB phone/tablet cases, with zero retries, skips, flaky outcomes or report errors. Its frozen frontend export is unchanged; its freshly rebuilt backend is `55b33e6`. Complete touch collection is 575 cases per profile. The subsequent [complete PR23 outcome](https://github.com/QRun-IO/qqq/issues/990#issuecomment-5944836159) covers all 401 required IDs on each profile with zero missing and all 20 ESB touch cases passing, completing #990. Touch totals are 1,149 passes and one unrelated tablet NAV-056 failure; the full gate and workflow remain failed. [NAV-056 investigation](https://github.com/QRun-IO/qqq/issues/985#issuecomment-5944830383) remains separate, with no shared-cause claim or new waiver.
Each linked issue and PR records the source commit, reproduction and scoped verification. Preserve failed full-run snapshots separately from later focused passes. The broader historical navigation inventory [#904](https://github.com/QRun-IO/qqq/issues/904), same-user session stall [#952](https://github.com/QRun-IO/qqq/issues/952), legacy process-navigation stall [#960](https://github.com/QRun-IO/qqq/issues/960), and original toast-paint uncertainty [#967](https://github.com/QRun-IO/qqq/issues/967) remain open. The [RC10 preparation notes](1.0.0-RC.10.md) retain their historical checkpoint; its [public release entry](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10) records final delivery verification. See [RC11 notes](1.0.0-RC.11.md) for the successor candidate. No final-release approval is claimed by these results.
None yet.
Final 1.0 remains held for real-application soak and explicit owner approval under [#712](https://github.com/QRun-IO/qqq/issues/712). This work does not authorize a QQQ 4.1 release. See the [testing-period policy](1.0-testing.md) and [issue lifecycle](../ISSUE-TRACKING.md).
## Pending standalone branding compatibility (QQQ #1009)
The standalone Node server can route the exact root-relative `branding.logo` and
`branding.icon` paths published by authentication metadata through fixed backend
image roles. Local Node `public` files keep precedence; external image URLs and
Java dashboard serving are unchanged. This does not expose the whole application
overlay or add a general proxy.
This requires **a QQQ middleware-javalin build containing QQQ #1009's
`/qqq/branding/logo` and `/qqq/branding/icon` endpoints**. They are absent from
published **4.1.0-RC.1**. The minimum numbered release is not yet assigned; a
source build must record the exact backend commit/JAR hashes. Older deployments
must supply branding through their Node `public` files or an existing external
image URL. No automatic compatibility with old relative backend assets is claimed.
The preserved standalone qualification against public RC1 remains **5 passed,
38 failed** out of 43 cases because of missing branding assets. The original
staging failure is also retained. The corrected cross-repository pairing has
not yet received its browser qualification; neither #1009 nor #734 is closed by
focused unit/HTTP results.
+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

+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
+25 -12
View File
@@ -1,6 +1,6 @@
{
"name": "qqq-frontend-next",
"version": "1.0.0-RC.11",
"version": "1.0.0-RC.37",
"private": true,
"packageManager": "pnpm@9.15.9",
"scripts": {
@@ -21,6 +21,7 @@
"test:e2e": "playwright test",
"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",
"prepare": "git config core.hooksPath .githooks || true",
@@ -35,6 +36,8 @@
"@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",
@@ -54,12 +57,16 @@
"dompurify": "^3.4.16",
"lodash": "^4.18.1",
"lucide-react": "^0.485.0",
"next": "16.3.6",
"rapidoc": "9.3.8",
"next": "16.3.8",
"rapidoc": "10.1.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.2",
"react-markdown": "10.1.0",
"recharts": "^2.15.4",
"rehype-sanitize": "6.0.0",
"remark-breaks": "4.0.0",
"remark-gfm": "4.0.1",
"sonner": "^2.0.7",
"sql-formatter": "15.6.6",
"tailwind-merge": "^3.5.0",
@@ -69,9 +76,9 @@
"@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",
@@ -82,8 +89,8 @@
"@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",
@@ -94,10 +101,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": [
@@ -108,7 +116,12 @@
"overrides": {
"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.1": "1.16.1"
},
"ignoredOptionalDependencies": [
"electron"
]
}
}
+37 -5
View File
@@ -1,5 +1,6 @@
{
"$comment": "Bundle budget for the static export, checked by scripts/perf-budget.mjs (QRun-IO/qqq#710). Sizes are KB gzip. '_' is the exported placeholder of a dynamic segment. The complete 1.0 UI includes wave-2 shell, record, form, process and widget parity. After lazy screen dispatch, the 2026-09-26 export measures 245.1 KB on login, 340.8 KB on the largest developer route, 371.7 KB on copy, and 1020.9 KB across all 97 emitted chunks (excluding separately budgeted RapiDoc). These caps leave roughly 3-5% headroom; route caps measure HTML first load and total measures all on-demand features. CI's default-builder export measured 19.2 KB gzip of shared CSS on 2026-09-26; the 21 KB cap leaves about 9% headroom. The 100 KB largest-chunk cap remains unchanged. Raise a cap only with a measured reason in the commit. SQL Workbench (2026-10-11): the qbit-sql-workbench SQL_WORKBENCH widget loads CodeMirror 6 and the workbench UI only through import() from WidgetRenderer, and sql-formatter only when Format is clicked; both are budgeted under lazyChunks. Adding the lazy import measured /app/_/_/dev at 353.1 KB (main 350.0 KB, already at its cap; the same tree without the WidgetRenderer hook-in measures 350.1 KB), so that route's cap is 355 KB.",
"$comment": "Bundle budget for the static export, checked by scripts/perf-budget.mjs (QRun-IO/qqq#710). Sizes are KB gzip. '_' is the exported placeholder of a dynamic segment. The complete 1.0 UI includes wave-2 shell, record, form, process and widget parity. After lazy screen dispatch, the 2026-09-26 export measures 245.1 KB on login, 340.8 KB on the largest developer route, 371.7 KB on copy, and 1020.9 KB across all 97 emitted chunks (excluding separately budgeted RapiDoc). These caps leave roughly 3-5% headroom; route caps measure HTML first load and total measures all on-demand features. CI's default-builder export measured 19.2 KB gzip of shared CSS on 2026-09-26; the 21 KB cap leaves about 9% headroom. The 100 KB largest-chunk cap remains unchanged. 2026-10-11 (QRun-IO/qqq#1056): main measured /app/_/_/dev at 350.0 KB, exactly the default cap; FILE_PREVIEW thumbnails add 0.6 KB to the record routes (resolver, thumbnail, gallery context; the lightbox is a lazy chunk), giving 350.6 KB, so that route gets an explicit 360 KB cap (about 2.7% headroom). SQL Workbench (2026-10-11): the qbit-sql-workbench SQL_WORKBENCH widget loads CodeMirror 6 and the workbench UI only through import() from WidgetRenderer, and sql-formatter only when Format is clicked; both are budgeted under lazyChunks. Adding the lazy import measured /app/_/_/dev at 353.1 KB (main 350.0 KB, already at its cap; the same tree without the WidgetRenderer hook-in measures 350.1 KB) (that route's cap was 355 KB on the SQL Workbench branch). Combined with FILE_PREVIEW (+0.6 KB) the expected /app/_/_/dev first load is about 353.7 KB, inside the 360 KB route cap. Responsive layouts (PR #50): the compact date, child-list column and container-grid work measured 351.1 KB on /app/_/_/dev against main's 350.0 KB (+1.1 KB), and the instance home app (PR #53) 350.1 KB (+0.1 KB). With SQL Workbench, FILE_PREVIEW and PR #50 together the route is expected at about 354.8 KB (355 KB with PR #53), inside the shared 360 KB route cap; every other route keeps the 350 KB default. CI on the combined SQL Workbench + FILE_PREVIEW + PR #50 tree (2026-10-11) measured /app/_/_/dev at 354.9 KB and the table developer route /app/_/dev at 350.8 KB (over the 350 KB default by 0.8 KB), so /app/_/dev gets the same 360 KB route cap (about 2.6% headroom). Readable widget widths (feature/widget-width-readability, on PR #50): the gridMinWidth container-query ladder (about 80 @container rules) and the table-widget card layout raise shared CSS from 20.4 KB (PR #50 CI) to 21.1 KB gzip, over the 21 KB cap, so the CSS cap becomes 22 KB (about 4% headroom); JS routes are unchanged. Raise a cap only with a measured reason in the commit. QQQ Assistant panel (2026-10-12): its Tailwind utilities (panel, sheet, markdown typography) add 0.9 KB gzip to the shared CSS (21.3 KB measured against the 21 KB cap); merged with the widget width ladder the shared CSS measured 21.3 KB (2026-10-12 local export), inside the 22 KB cap; the panel's JS is budgeted under lazyChunks.assistant. Assistant panel plus SQL Workbench dark mode (#78, 2026-10-12): main with #78 measured 21.5 KB of shared CSS, and merging the assistant panel measured 22.3 KB (local export), over the 22 KB cap, so the CSS cap becomes 23 KB (about 3% headroom); JS routes and lazy chunks are unchanged. Owner decision 2026-10-12 (\"raise the limits on size as needed, but still keeping things healthy and small as possible\"): totalJsGzipKB is 1100. The total includes every on-demand feature and grows with features; first-load route caps (255/350/360/390), the 23 KB CSS cap and the lazy-chunk caps stay strict because they govern page speed. Any other cap raise still needs a measured reason, and feature PRs keep code-splitting and report their bundle delta. Resizable navigation (#79, 2026-10-12): main measured 23.07 KB of shared CSS after #79 (CI and local export), over the 23 KB cap; the RC.16 candidates (#80 record picker, #81 form validation, #82 date filters, #84 search thumbnails, #85 alerts bell) each add small amounts, so under the owner decision the CSS cap becomes 24 KB (about 4% headroom). Search preview thumbnails (2026-10-12, owner-approved total cap 1100 KB): restoring FILE_PREVIEW thumbnails, CHIP values and dates in the lazy search record preview measured total JS 1052.0 KB against main's 1042.7 KB (+9.3 KB: Turbopack re-bundles the shared file-preview, gallery, adornment and date modules into more on-demand screen chunks), with every route first load and the CSS (23.0 KB) unchanged; the total cap moves to 1100 KB, first-load and CSS caps stay. Owner decision 2026-10-13 (\"we can remove the size limits\"): the caps below are no longer enforced; \"enforce\": false makes scripts/perf-budget.mjs report sizes and over-cap warnings without failing CI. The numbers stay as reference points so growth remains visible in every build log and in test-results/performance/bundle.json.",
"enforce": false,
"firstLoadJsGzipKB": 350,
"routes": {
"/": 255,
@@ -12,13 +13,14 @@
"/app/_": 350,
"/app/_/_": 350,
"/app/_/_/copy": 390,
"/app/_/_/dev": 360,
"/app/_/_/edit": 350,
"/app/_/dev": 360,
"/app/_/create": 350,
"/app/_/savedView/_": 350,
"/app/_/_/dev": 355
"/app/_/savedView/_": 350
},
"cssGzipKB": 21,
"totalJsGzipKB": 1050,
"cssGzipKB": 24,
"totalJsGzipKB": 1100,
"largestChunkGzipKB": 100,
"lazyChunks": {
"rapidoc": {
@@ -31,10 +33,40 @@
"marker": "qqq-sql-workbench-editor",
"gzipKB": 135
},
"recordPicker": {
"$comment": "The record picker dialog (RecordPickerDialog: search, filters, thumbnail grid and list, preview, upload) for fields whose possible value source declares a picker, loaded when Browse/Change is clicked: import() in PossibleValueSelect. Turbopack emits one variant of the async chunk per parent context that bundles PossibleValueSelect (record forms, process forms, bulk-load mapping), so the budget covers all variants; a session loads one (about 5.9 KB gzip, with the dialog-only record-picker-query helpers). Measured 17.0 KB gzip across the three variants at introduction; budget about 6% above. The inline field (thumbnails, Browse/Preview/View) adds 2.5 KB to total JS (main 1005.4 KB, this branch 1007.9 KB on 2026-10-12) and nothing to any route first load or the shared CSS.",
"marker": "qqq-record-picker-grid",
"gzipKB": 18
},
"sqlFormatter": {
"$comment": "sql-formatter with the postgresql, mysql, sqlite and sql dialects only (formatDialect), loaded when a workbench cell's Format button is first used: import() in SqlCell. Measured 60.2 KB gzip at introduction; budget about 8% above.",
"marker": "logicalOperatorNewline",
"gzipKB": 65
},
"assistant": {
"$comment": "The QQQ Assistant panel (qbit-assistant): chat UI, SSE streaming client, react-markdown + rehype-sanitize, loaded on first open through import() in AssistantMount; the shell keeps only the /assistant/config probe, header button and Ctrl/⌘+J (about 1.7 KB first load). Measured 50.8 KB gzip at introduction; budget about 8% above.",
"marker": "assistant-composer-form",
"gzipKB": 55
},
"searchPage": {
"$comment": "The search results page (/app/search: filters, sort, result cards, split preview pane, side sheet), loaded on demand: import() in app/search/page.tsx. Measured 13.4 KB gzip at introduction (QRun-IO/qqq-frontend-next#59); budget about 8% above.",
"marker": "search-result-card",
"gzipKB": 14.5
},
"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",
"gzipKB": 8.1
},
"notificationCenter": {
"$comment": "The notification center (qbit-alerts phase b): the bell popover (import() in AlertsBell on first open), the sticky-notification banners (import() in NotificationBannerSlot when the summary has banners) and the /app/notifications page (next/dynamic in its route). Turbopack bundles the shared parts (severity icons, per-item actions, the optimistic inbox mutation, MarkdownView) into each of the three async chunks, so the budget covers all three; the marker is the per-item read/unread button they share. Measured 15.2 KB gzip across the three (popover 4.3, banners 4.3, page 6.7) on 2026-10-13; budget about 8% above. The shell keeps the badge, poller with backoff, summary normalizer and banner slot: +0.7 KB first load on the /app routes (/app/_/_ 348.5 -> 349.2 KB, inside its 350 KB cap); the new /app/notifications route measures 339.5 KB; total JS 1092.9 -> 1098.0 KB excluding these chunks; CSS unchanged at 23.2 KB.",
"marker": "notification-toggle-read",
"gzipKB": 16.5
},
"recordPreview": {
"$comment": "The search result record preview (first-tier fields with file thumbnails, chips and dates; quick actions), loaded when a preview first shows: import() in SearchPage. It runs its own two queries (the useTableMetaData and useRecord keys) rather than importing those hooks, which measured +12.4 KB more total JS. Measured 7.1 KB gzip with thumbnails and chips (3.7 KB without); budget about 8% above.",
"marker": "search-record-preview",
"gzipKB": 7.7
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import { defineConfig, devices } from '@playwright/test'
// The acceptance sample does not install qbit-collaboration. Exercise the real browser
// MSW handlers instead, retaining all five acceptance device profiles and zero retries.
// The main e2e suite answers API calls with page.route mocks and never starts MSW, so this
// spec needs its own NEXT_PUBLIC_MOCK_API=true server.
// localhost, not 127.0.0.1: `next dev` serves dev resources only to its own host name and
// blocks them for 127.0.0.1, so the client never hydrates and MSW never starts.
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', 'collaboration-presence-share-tasks.spec.ts'],
outputDir: './test-results/collaboration/artifacts',
fullyParallel: false,
workers: 1,
retries: 0,
timeout: 90_000,
expect: { timeout: 15_000 },
reporter: [['list'], ['json', { outputFile: 'test-results/collaboration/report.json' }]],
use: {
baseURL,
serviceWorkers: 'allow',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
{ name: 'tablet', use: { ...devices['iPad (gen 7)'] } },
],
webServer: {
command: `pnpm dev --port ${port}`,
url: baseURL,
env: { NEXT_PUBLIC_MOCK_API: 'true', QQQ_NEXT_DIST_DIR: '.next-collaboration' },
reuseExistingServer: false,
timeout: 120_000,
},
})
+2
View File
@@ -11,6 +11,8 @@ const production = process.env.QQQ_E2E_SERVER === 'production'
export default defineConfig({
testDir: './tests/e2e',
// This feature needs MSW and the full browser matrix; use playwright.collaboration.config.ts.
testIgnore: 'collaboration-*.spec.ts',
fullyParallel: true,
forbidOnly: !!process.env.CI,
// No retries: a test that needs one is flaky and must be fixed, not re-run.
+1584 -1310
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -47,7 +47,7 @@
</developers>
<properties>
<revision>1.0.0-RC.11</revision>
<revision>1.0.0-RC.37</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-09-24T00:00:00Z</project.build.outputTimestamp>
</properties>
+19 -4
View File
@@ -25,6 +25,17 @@ const report = JSON.parse(readFileSync('test-results/acceptance/report.json', 'u
const rows = new Map(matrix.rows.map((row) => [row.id, row]))
const results = new Map()
const problems = []
const quarantine = JSON.parse(readFileSync('tests/acceptance/quarantine.json', 'utf8'))
const quarantined = new Set()
for (const entry of quarantine) {
if (!rows.has(entry.id)) problems.push(`Unknown quarantine ID ${entry.id}`)
if (typeof entry.issue !== 'string' || !/^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/.test(entry.issue)) {
problems.push(`${entry.id} quarantine entry needs an issue link`)
}
if (quarantined.has(entry.id)) problems.push(`Duplicate quarantine ID ${entry.id}`)
quarantined.add(entry.id)
}
for (const error of report.errors ?? []) problems.push(`Playwright error: ${error.message ?? JSON.stringify(error)}`)
const byProject = Object.fromEntries((report.config?.projects ?? []).map((project) => [project.name, { passed: 0, failed: 0, skipped: 0, flaky: 0 }]))
function walk(suite, titles = []) {
@@ -50,8 +61,12 @@ function walk(suite, titles = []) {
}
for (const suite of report.suites ?? []) walk(suite)
const summary = { passed: 0, failed: 0, missing: 0, excluded: 0 }
const summary = { passed: 0, failed: 0, missing: 0, excluded: 0, quarantined: 0 }
for (const row of matrix.rows) {
if (quarantined.has(row.id)) {
summary.quarantined++
continue
}
if (row.required === false) {
summary.excluded++
if (!row.approval) problems.push(`${row.id} is excluded without a recorded approval`)
@@ -76,7 +91,7 @@ for (const row of matrix.rows) {
const touchProjects = ['mobile', 'tablet'].filter((project) => byProject[project])
const phone = { covered: 0, desktopOnly: 0, uncovered: 0 }
if (touchProjects.length) for (const row of matrix.rows) {
if (row.required === false) continue
if (row.required === false || quarantined.has(row.id)) continue
if (row.desktopOnly !== undefined) {
if (typeof row.desktopOnly !== 'string' || !row.desktopOnly.trim()) problems.push(`${row.id} desktopOnly needs the reason it is not a touch scenario`)
phone.desktopOnly++
@@ -97,8 +112,8 @@ if (!partial) for (const [project, counts] of Object.entries(byProject)) {
if (Object.values(counts).every((count) => count === 0)) problems.push(`Project ${project} ran no tests`)
}
const status = { partial, generatedAt: new Date().toISOString(), summary, byProject, problems,
rows: matrix.rows.map((row) => ({ id: row.id, results: results.get(row.id) ?? [] })) }
const status = { partial, generatedAt: new Date().toISOString(), summary, byProject, problems, quarantine,
rows: matrix.rows.map((row) => ({ id: row.id, quarantined: quarantined.has(row.id), results: results.get(row.id) ?? [] })) }
writeFileSync('test-results/acceptance/gate.json', JSON.stringify(status, null, 2) + '\n')
console.log(`Acceptance gate${partial ? ' (partial run, not an acceptance result)' : ''}:`, JSON.stringify(summary), JSON.stringify(byProject))
for (const problem of problems) console.log(` FAIL ${problem}`)
+96
View File
@@ -0,0 +1,96 @@
/*
* 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
*/
import { execFileSync } from 'node:child_process'
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
export const AREAS = ['esb', 'help', 'navigation', 'performance', 'processes', 'query', 'records', 'security', 'widgets']
export const SMOKE = [
'navigation/landing.spec.ts',
'navigation/navigation.spec.ts',
'records/crud.spec.ts',
'query/query-grid.spec.ts',
'widgets/sample-dashboard.spec.ts',
].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'],
[/process|bulk/, 'processes'],
[/navigation|\bnav\b|sidebar|theme|branding|layout/, 'navigation'],
[/security|auth|login|session|permission/, 'security'],
[/esb/, 'esb'],
[/performance/, 'performance'],
]
/** Unknown production/configuration paths expand coverage instead of silently losing it. */
export function selectSpecs(paths) {
const selected = new Set()
for (const raw of paths) {
const file = raw.replaceAll('\\', '/')
if (file.startsWith('docs/') || /\.md$/i.test(file)) continue
const specArea = file.match(/^tests\/acceptance\/specs\/([^/]+)\//)?.[1]
if (specArea && AREAS.includes(specArea)) {
selected.add(specArea)
continue
}
if (/^(package\.json|pnpm-lock\.yaml|next\.config\.[^/]+)$/.test(file)
|| /^tests\/acceptance\/(playwright\.config\.|support\/|fixture\/|matrix\/|quarantine\.json)/.test(file)
|| /^scripts\/acceptance/.test(file)
|| file === '.github/workflows/acceptance.yml'
|| /^src\/lib\/(api\/|core\/|client|query-client|context\/)/.test(file)) return allSpecs()
if (file.startsWith('src/')) {
const matches = sourceAreas.filter(([pattern]) => pattern.test(file.toLowerCase()))
if (!matches.length) return allSpecs()
for (const [, area] of matches) selected.add(area)
continue
}
// Unit/mocked tests don't change the real backend; smoke still runs.
if (/^(tests\/|\.storybook\/)/.test(file)) continue
return allSpecs()
}
return [...AREAS.filter((area) => selected.has(area)).map((area) => `tests/acceptance/specs/${area}`),
...SMOKE.filter((file) => !selected.has(file.split('/')[3]))]
}
/** Directory filters include new specs automatically. */
function allSpecs() {
return AREAS.map((area) => `tests/acceptance/specs/${area}`)
}
/**
* Build a bounded matrix; each cell owns one browser engine and one backend.
* @param {{ eventName: string, labels?: string[], paths?: string[] }} input
*/
export function planAcceptance({ eventName, labels = [], paths = [] }) {
const full = eventName !== 'pull_request' || labels.includes('full-acceptance')
const counts = full ? { chromium: 3, firefox: 3, webkit: 4, mobile: 4, tablet: 4 } : { chromium: 2, mobile: 2 }
const include = Object.entries(counts).flatMap(([project, total]) => Array.from({ length: total }, (_, index) => ({
project, browser: project === 'mobile' ? 'chromium' : project === 'tablet' ? 'webkit' : project,
shard: index + 1, total,
})))
return { mode: full ? 'full' : 'pr', matrix: { include }, specs: full ? [] : selectSpecs(paths) }
}
// Diff both sides of renames, from the PR merge-base (not the current main tip).
// Event data is passed as argv, never interpolated into a shell command.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'))
const eventName = process.env.GITHUB_EVENT_NAME
const labels = (event.pull_request?.labels ?? []).map((label) => label.name)
const paths = eventName === 'pull_request' && !labels.includes('full-acceptance')
? execFileSync('git', ['diff', '--name-only', '--no-renames', '-z', `${event.pull_request.base.sha}...${event.pull_request.head.sha}`, '--'], { encoding: 'utf8' }).split('\0').filter(Boolean)
: []
const plan = planAcceptance({ eventName, labels, paths })
for (const [key, value] of Object.entries(plan)) {
appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}\n`)
}
console.log(JSON.stringify(plan, null, 2))
}
+8 -2
View File
@@ -16,7 +16,11 @@ import { stageStandalone } from './acceptance-standalone.mjs'
const args = process.argv.slice(2)
const skipBuild = args.includes('--skip-build')
const passthrough = args.filter((arg) => arg !== '--skip-build')
const selectedSpecs = JSON.parse(process.env.QQQ_ACCEPTANCE_SPEC_PATHS ?? '[]')
if (!Array.isArray(selectedSpecs) || selectedSpecs.some((spec) => typeof spec !== 'string' || !spec.startsWith('tests/acceptance/specs/'))) {
throw new Error('QQQ_ACCEPTANCE_SPEC_PATHS must be a JSON array of acceptance spec paths')
}
const passthrough = [...selectedSpecs, ...args.filter((arg) => arg !== '--skip-build')]
const backend = `http://127.0.0.1:${process.env.QQQ_ACCEPTANCE_BACKEND_PORT ?? '18765'}`
resolveSampleJar()
@@ -50,6 +54,8 @@ if (!skipBuild && ACCEPTANCE_MODE === 'standalone') {
writeFileSync(BUILD_MARKER, backend + '\n')
}
const tests = run('pnpm', ['exec', 'playwright', 'test', '-c', 'tests/acceptance/playwright.config.ts', ...passthrough])
const filtered = passthrough.some((arg) => !arg.startsWith('--project'))
// CI shards emit blobs only. The single gate evaluates the merged evidence.
if (process.env.QQQ_ACCEPTANCE_BLOB === '1') process.exit(tests)
const filtered = process.env.QQQ_ACCEPTANCE_QUARANTINE === 'only' || passthrough.some((arg) => !arg.startsWith('--project'))
const gate = run('node', ['scripts/acceptance-gate.mjs', ...(filtered ? ['--partial'] : [])])
process.exit(tests || gate)
+114
View File
@@ -0,0 +1,114 @@
/*
* 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
*/
// Checks the UI API document bundled with this app (src/lib/api/openapi/qqq-v1.json) against the
// server's: the committed openapi.yaml of the QQQ checkout being tested, or a running server.
//
// node scripts/check-ui-api-spec.mjs --server-spec qqq-backend/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml
// node scripts/check-ui-api-spec.mjs --server-url http://127.0.0.1:18765
//
// Fails when the majors differ, when the bundled document is newer than the server's, or when the
// bundled document has an operation the server lacks. A server that is newer within the major is
// fine (it only adds), and is reported so the document can be re-vendored.
import { readFileSync } from 'node:fs'
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', '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} */
function semver(version) {
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(version ?? '').trim())
return match ? match.slice(1, 4).map(Number) : null
}
/** @param {number[]} a @param {number[]} b @returns {number} */
function compare(a, b) {
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
}
/** Operations of the bundled document as `METHOD /path`. */
function bundledOperations() {
return Object.entries(bundled.paths).flatMap(([p, ops]) => Object.keys(ops).filter((m) => HTTP_METHODS.includes(m)).map((m) => `${m.toUpperCase()} ${p}`))
}
/**
* Reads version and operations from a committed openapi.yaml (sorted, as PublishAPI writes it)
* without a YAML parser: `info.version`, and the `/qqq/v1/...` keys and their method keys.
* @param {string} yaml
*/
function readServerYaml(yaml) {
const lines = yaml.split(/\r?\n/)
let version = null
const operations = []
let inPaths = false
let currentPath = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (line === 'info:') {
for (let j = i + 1; j < lines.length && lines[j].startsWith(' '); j++) {
const v = /^ {2}version: "?([^"]+)"?$/.exec(lines[j])
if (v) version = v[1]
}
}
if (/^\S/.test(line)) inPaths = line === 'paths:'
if (!inPaths) continue
const pathMatch = /^ {2}"?(\/[^"]+)"?:$/.exec(line)
if (pathMatch) currentPath = pathMatch[1]
const methodMatch = /^ {4}(get|post|put|patch|delete):$/.exec(line)
if (methodMatch && currentPath) operations.push(`${methodMatch[1].toUpperCase()} ${currentPath}`)
}
return { version, operations }
}
const args = process.argv.slice(2)
const option = (name) => {
const index = args.indexOf(name)
return index >= 0 ? args[index + 1] : undefined
}
let server
if (option('--server-spec')) {
server = readServerYaml(readFileSync(option('--server-spec'), 'utf-8'))
} else if (option('--server-url')) {
const base = option('--server-url').replace(/\/+$/, '')
const response = await fetch(`${base}/qqq/v1/openapi.json`)
if (!response.ok) throw new Error(`GET ${base}/qqq/v1/openapi.json: ${response.status}`)
const document = await response.json()
server = {
version: document.info?.version,
operations: Object.entries(document.paths ?? {}).flatMap(([p, ops]) => Object.keys(ops).filter((m) => HTTP_METHODS.includes(m)).map((m) => `${m.toUpperCase()} ${p}`)),
}
} else {
console.error('Usage: check-ui-api-spec.mjs --server-spec <openapi.yaml> | --server-url <base url>')
process.exit(2)
}
// servers before semver (QQQ 4.1.0-RC.5 and earlier) publish "v1": the 1.0.0 surface
const serverVersion = semver(server.version) ?? (server.version === 'v1' ? [1, 0, 0] : null)
const bundledVersion = semver(bundled.info.version)
const problems = []
if (!serverVersion || !bundledVersion) problems.push(`unrecognised versions: bundled ${bundled.info.version}, server ${server.version}`)
else {
if (serverVersion[0] !== bundledVersion[0]) problems.push(`major mismatch: bundled ${bundled.info.version}, server ${server.version}`)
else if (compare(bundledVersion, serverVersion) > 0) {
const missing = bundledOperations().filter((op) => !server.operations.includes(op) && !OPTIONAL_OPERATIONS.has(op))
if (missing.length) problems.push(`bundled document ${bundled.info.version} is newer than the server's ${server.version} and has operations the server lacks: ${missing.join(', ')}`)
else console.log(`Bundled UI API ${bundled.info.version} is newer than the server's ${server.version}; every bundled operation exists on the server.`)
} else if (compare(bundledVersion, serverVersion) < 0) {
console.log(`Server UI API ${server.version} is newer than the bundled ${bundled.info.version}: re-vendor src/lib/api/openapi/qqq-v1.json to use its additions.`)
}
}
if (problems.length) {
for (const problem of problems) console.error(`UI API spec check: ${problem}`)
process.exit(1)
}
console.log(`UI API spec check passed: bundled ${bundled.info.version}, server ${server.version}.`)
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Copyright 2026 QRun.IO, Inc.
# Licensed under the Apache License, Version 2.0.
# https://www.apache.org/licenses/LICENSE-2.0
set -euo pipefail
if [[ $# -lt 2 || ! $1 =~ ^[1-9][0-9]*$ ]]; then
echo 'Usage: retry.sh <attempts> <command...>' >&2
exit 2
fi
attempts=$1
shift
delays=(15 45 120)
for ((attempt = 1; attempt <= attempts; attempt++)); do
# stderr keeps command substitution (e.g. Maven version output) clean.
echo "Attempt $attempt/$attempts" >&2
if "$@"; then
exit 0
else
status=$?
fi
if ((attempt < attempts)); then
index=$((attempt - 1))
if ((index > 2)); then index=2; fi
sleep "${delays[$index]}"
fi
done
exit "$status"
+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'))
+15 -3
View File
@@ -10,6 +10,9 @@
// node scripts/perf-budget.mjs # check out/ against perf-budget.json, exit 1 on a breach
// node scripts/perf-budget.mjs --json # also print the measurement as JSON
//
// With "enforce": false in perf-budget.json the script still measures and reports every size and
// every cap it is over, but exits 0: the caps are reference points, not gates.
//
// First-load JS of a route is every script the route's HTML loads before hydration (script src
// and preload links), gzip-compressed the way the server sends it. Next 16 no longer prints these
// sizes, so they are measured from out/. The report lands in test-results/performance/bundle.json.
@@ -147,9 +150,18 @@ mkdirSync(path.dirname(REPORT), { recursive: true })
writeFileSync(REPORT, JSON.stringify({ budget, result, problems }, null, 2) + '\n')
if (process.argv.includes('--json')) console.log(JSON.stringify(result, null, 2))
if (problems.length) {
console.error(`\nPerformance budget exceeded (${problems.length}):\n- ${problems.join('\n- ')}`)
// Structural problems (a lazy chunk on a first load, or a marker no chunk contains) always fail.
// Size problems fail only while perf-budget.json enforces the caps ("enforce": false makes them warnings).
const structural = problems.filter((p) => p.includes(': no chunk contains its marker') || p.includes('is loaded on first load of a route'))
const sizeProblems = problems.filter((p) => !structural.includes(p))
const enforce = budget.enforce !== false
if (sizeProblems.length && !enforce) {
console.warn(`\nPerformance budget (report-only, "enforce": false): over the reference sizes (${sizeProblems.length}):\n- ${sizeProblems.join('\n- ')}`)
}
const failures = enforce ? problems : structural
if (failures.length) {
console.error(`\nPerformance budget exceeded (${failures.length}):\n- ${failures.join('\n- ')}`)
console.error('Reduce the bundle, or raise the budget in perf-budget.json with the reason in the commit.')
process.exit(1)
}
console.log('\nPerformance budget: OK')
console.log(enforce ? '\nPerformance budget: OK' : '\nPerformance budget: OK (report-only)')
@@ -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>
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
// Tests for the record developer view (raw values and associated scripts)
// Tests for the record developer view (header parity, sections, API access, provider panels, raw values and associated scripts)
import { render, screen, waitFor, within } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
@@ -52,9 +52,19 @@ vi.mock('@/lib/api/developer', () => ({
storeRecordAssociatedScript: vi.fn(),
storeScriptRevision: vi.fn(),
testScript: vi.fn(),
getTableApis: vi.fn(async () => []),
getApiVersions: vi.fn(),
getApiTableSpec: vi.fn(),
apiTableSpecUrl: () => '/qqq/v1/table/person/apis/siteApi/v1/openapi',
apiMiddlewareBaseURL: () => '',
}))
vi.mock('@/lib/api/developer-panels', () => ({
getRecordDeveloperPanels: vi.fn(async () => []),
}))
import { getRecordDeveloperData } from '@/lib/api/developer'
import { getRecordDeveloperPanels, type DeveloperPanel } from '@/lib/api/developer-panels'
import { getRecord } from '@/lib/api/tables'
const record = { tableName: 'person', recordLabel: 'Ada Lovelace', values: { id: 1, firstName: 'Ada', email: null } }
@@ -119,28 +129,28 @@ describe('RecordDeveloperViewPage', () => {
serveInstance(['storeScriptRevision', 'testScript'])
renderPage()
expect(await screen.findByRole('heading', { name: 'Record Raw Values as JSON' })).toBeInTheDocument()
expect(await screen.findByRole('heading', { name: 'Record Raw Values as JSON' }, { timeout: 5000 })).toBeInTheDocument()
const card = await waitFor(() => {
const element = document.querySelector('[data-qqq-id="associated-script-firstName"]')
if (!element) throw new Error('no card')
return element as HTMLElement
})
}, { timeout: 5000 })
expect(within(card).getByRole('heading', { name: 'First Name' })).toBeInTheDocument()
await waitFor(() => expect(document.querySelector('[data-qqq-id="button-edit-script-firstName"]')).not.toBeNull())
await waitFor(() => expect(document.querySelector('[data-qqq-id="button-edit-script-firstName"]')).not.toBeNull(), { timeout: 5000 })
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([])
renderPage()
expect(await screen.findByRole('heading', { name: 'Record Raw Values as JSON' })).toBeInTheDocument()
expect(await screen.findByRole('heading', { name: 'Record Raw Values as JSON' }, { timeout: 5000 })).toBeInTheDocument()
const json = document.querySelector('[data-qqq-id="json-output"]') as HTMLElement
expect(JSON.parse(json.textContent ?? '')).toEqual({ id: 1, firstName: 101, email: null })
const table = document.querySelector('[data-qqq-id="record-dev-field-table"]') as HTMLElement
expect(within(table).getByText('101')).toBeInTheDocument()
const row = document.querySelector('[data-qqq-id="record-dev-field-table"] [data-qqq-id="dev-field-firstName"]') as HTMLElement
expect(row.querySelectorAll('td')[3]).toHaveTextContent('101')
expect(getRecord).not.toHaveBeenCalled()
})
@@ -155,7 +165,7 @@ describe('RecordDeveloperViewPage', () => {
const element = document.querySelector('[data-qqq-id="associated-script-firstName"]')
if (!element) throw new Error('no card')
return element as HTMLElement
})
}, { timeout: 5000 })
await waitFor(() => expect(within(card).getAllByRole('tab').map((tab) => tab.textContent)).toEqual(['Code', 'Logs', 'Docs']))
expect(document.querySelector('[data-qqq-id="button-edit-script-firstName"]')).toBeNull()
})
@@ -165,7 +175,90 @@ describe('RecordDeveloperViewPage', () => {
vi.mocked(getRecordDeveloperData).mockRejectedValue(new Error('Permission denied.'))
renderPage()
expect(await screen.findByText('Failed to load record: Permission denied.')).toHaveAttribute('role', 'alert')
expect(await screen.findByText('Failed to load record: Permission denied.', {}, { timeout: 5000 })).toHaveAttribute('role', 'alert')
expect(screen.queryByRole('heading', { name: 'Record Raw Values as JSON' })).not.toBeInTheDocument()
})
it('has the record screen header: back link, record title, actions and audit (no view-mode toggle)', async () => {
serveInstance([])
renderPage()
const back = await screen.findByRole('link', { name: 'Back to Ada Lovelace' }, { timeout: 5000 })
expect(back).toHaveAttribute('href', '/app/person/1')
expect(screen.getByRole('heading', { level: 1, name: 'Ada Lovelace' })).toBeInTheDocument()
expect(screen.queryByRole('radiogroup', { name: 'View mode' })).not.toBeInTheDocument()
expect(document.querySelector('[data-qqq-id="record-view-desktop-actions"]')).not.toBeNull()
expect(screen.getByText(/Developer view/)).toBeInTheDocument()
})
it('navigates sections from a sticky section nav', async () => {
serveInstance([])
renderPage()
const nav = await screen.findByRole('navigation', { name: 'Developer view sections' }, { timeout: 5000 })
await waitFor(() => expect(within(nav).getAllByRole('link').map((link) => link.textContent)).toEqual(
expect.arrayContaining(['Fields', 'API', 'Scripts', 'JSON', 'Table Metadata'])))
expect(screen.getByRole('heading', { name: 'API Access' })).toBeInTheDocument()
})
it('lists the v1 operations the user may perform, with curl examples using placeholder credentials', async () => {
serveInstance([])
renderPage()
const api = await waitFor(() => {
const element = document.querySelector('[data-qqq-id="dev-api-access"]')
if (!element) throw new Error('no api section')
return element as HTMLElement
})
await waitFor(() => expect(within(api).getByText('Get this record')).toBeInTheDocument())
for (const label of ['Query by primary key', 'Count matching records', 'Insert a copy', 'Update this record', 'Delete this record']) {
expect(within(api).getByText(label)).toBeInTheDocument()
}
const snippet = api.querySelector('[data-qqq-id="dev-api-snippet-get"]') as HTMLElement
expect(snippet.textContent).toContain('curl -X GET')
expect(snippet.textContent).toContain('/qqq/v1/table/person/1')
expect(snippet.textContent).toContain('sessionUUID=<your-session-uuid>')
})
it('renders no provider panels when none are available (qbits absent)', async () => {
serveInstance([])
renderPage()
await screen.findByRole('heading', { name: 'API Access' }, { timeout: 5000 })
await waitFor(() => expect(getRecordDeveloperPanels).toHaveBeenCalledWith('person', '1'))
expect(document.querySelector('[data-qqq-id^="dev-section-panel-"]')).toBeNull()
})
it('renders each provider panel (ESB, quick search, SQL workbench, assistant) with a nav entry', async () => {
const panels: DeveloperPanel[] = [
{ id: 'esb', title: 'ESB Events', order: 300, statusLabel: 'Publishing', statusTone: 'SUCCESS', blocks: [
{ type: 'KEY_VALUES', items: [{ label: 'type', value: 'qqq.table.person.updated', monospace: true }] },
{ type: 'CODE', label: 'Sample event', language: 'json', code: '{"subject":"1"}' },
] },
{ id: 'quickSearch', title: 'Quick Search', order: 400, statusLabel: 'Indexed', blocks: [
{ type: 'ACTIONS', actions: [{ label: 'Reindex this record', processName: 'quickSearchReindexRecords_person' }] },
] },
{ id: 'sqlWorkbench', title: 'SQL Workbench', order: 450, blocks: [
{ type: 'LINKS', links: [{ label: 'Open in SQL Workbench', href: '/app/sqlWorkbench?sql=SELECT+1' }, { label: 'Bad', href: 'javascript:alert(1)' }] },
] },
{ id: 'assistant', title: 'Assistant', order: 600, error: 'This panel could not be built.', blocks: [] },
]
vi.mocked(getRecordDeveloperPanels).mockResolvedValue(panels)
serveInstance([])
renderPage()
const nav = await screen.findByRole('navigation', { name: 'Developer view sections' }, { timeout: 5000 })
await waitFor(() => expect(within(nav).getByRole('link', { name: 'ESB Events' })).toBeInTheDocument())
for (const title of ['Quick Search', 'SQL Workbench', 'Assistant']) {
expect(within(nav).getByRole('link', { name: title })).toBeInTheDocument()
expect(within(nav).getByRole('option', { name: title })).toBeInTheDocument()
}
expect(screen.getByRole('heading', { name: /ESB Events/ })).toBeInTheDocument()
expect(screen.getByText('qqq.table.person.updated')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /Reindex this record/ }).getAttribute('href')).toContain('/app/quickSearchReindexRecords_person')
expect(screen.getByRole('link', { name: 'Open in SQL Workbench' })).toHaveAttribute('href', '/app/sqlWorkbench?sql=SELECT+1')
expect(screen.queryByRole('link', { name: 'Bad' })).not.toBeInTheDocument()
expect(screen.getByText('This panel could not be built.')).toHaveAttribute('role', 'alert')
})
})
@@ -15,232 +15,26 @@
*/
/**
* @file RecordDeveloperView page — raw record values and table metadata as JSON, plus the
* record's associated scripts (versions, code, editing, logs, testing, docs).
* @file RecordDeveloperView page — serves `/app/[slug]/[recordId]/dev`. The view (record header,
* fields, API access, developer panels, relations, audit, scripts, JSON and table metadata) is
* loaded on demand, as the record view is, so the route's first-load JavaScript stays small.
*/
'use client'
import React, { useCallback, useEffect } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Code, ChevronDown, ChevronUp } from 'lucide-react'
import React, { lazy, Suspense } from 'react'
import { useRouteParams } from '@/lib/hooks/use-route-params'
import { useMetaData } from '@/lib/hooks/use-metadata'
import { useQContext } from '@/lib/context/q-context'
import { SCRIPT_LOG_TABLE, getRecordDeveloperData } from '@/lib/api/developer'
import { loadTableMetaData } from '@/lib/api/metadata'
import { HANDLES_OWN_ERRORS, queryKeys } from '@/lib/query-client'
import { getErrorMessage } from '@/lib/utils/error-utils'
import { AssociatedScriptViewer } from '@/components/records/AssociatedScriptViewer'
import { scriptLogsQueryHref } from '@/components/scripts/script-utils'
const RecordDeveloperView = lazy(() => import('@/components/developer/RecordDeveloperView').then((module) => ({ default: module.RecordDeveloperView })))
/**
* Renders a developer debug view for a single record, showing all field values
* in a table and the raw record and table metadata as collapsible JSON blocks.
* Renders the record developer view once its code has loaded.
*
* @returns A composed page that assembles:
* - A field-values summary table (field name, type, raw value for every field in the record)
* - A `<JsonBlock>` for the raw record values (expanded by default)
* - An `<AssociatedScriptViewer>` card per associated script declared on the table
* - A `<JsonBlock>` for the full table metadata (collapsed by default)
* - A loading spinner while either query is in-flight, and an error panel if the record fetch fails
* @returns The view, with a loading status meanwhile.
*/
export default function RecordDeveloperViewPage() {
const params = useRouteParams<{ slug: string; recordId: string }>()
const { setPageHeader } = useQContext()
const { slug, recordId } = params
const queryClient = useQueryClient()
const { data: metaData, isLoading: metaLoading } = useQuery({
queryKey: queryKeys.tableMetadata(slug),
queryFn: () => loadTableMetaData(slug),
staleTime: 1000 * 60 * 30,
})
const { data: instance } = useMetaData()
// Material's RecordDeveloperView reads the record from the developer-mode route: the same
// response carries the raw values and the associated scripts.
const { data: developerData, isLoading: recordLoading, error: recordError } = useQuery({
queryKey: queryKeys.recordDeveloper(slug, recordId),
queryFn: () => getRecordDeveloperData(slug, recordId),
meta: HANDLES_OWN_ERRORS,
retry: false,
})
const record = developerData?.record
// "View All" logs link, when the session can read the script log table
const logsViewAllHref = instance?.tables?.[SCRIPT_LOG_TABLE] ? scriptLogsQueryHref : undefined
const headerLabel = record?.recordLabel || (metaData ? `${metaData.label} ${recordId}` : `${slug} #${recordId}`)
useEffect(() => {
setPageHeader(`${headerLabel} Developer Mode`)
}, [headerLabel, setPageHeader])
const reloadRecord = useCallback(
() => queryClient.invalidateQueries({ queryKey: queryKeys.tableRecord(slug, recordId) }),
[queryClient, slug, recordId]
)
const isLoading = recordLoading || metaLoading
const canEditScripts = Boolean(instance?.processes?.storeScriptRevision)
const canTestScripts = Boolean(instance?.processes?.testScript)
return (
<div className="space-y-6" data-qqq-id={`record-dev-${slug}-${recordId}`}>
<div className="flex items-center gap-3">
<Code className="h-6 w-6 text-muted-foreground" aria-hidden="true" />
<h2 className="text-2xl font-semibold text-foreground">
Record Developer View:{' '}
<span className="font-mono text-primary">{slug}</span>
{' '}
<span className="font-mono text-muted-foreground">#{recordId}</span>
</h2>
</div>
{isLoading && (
<div
className="flex items-center justify-center py-12"
role="status"
aria-live="polite"
aria-busy="true"
>
<div className="h-6 w-6 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
)}
{recordError && (
<div
className="rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive"
role="alert"
>
Failed to load record: {getErrorMessage(recordError)}
</div>
)}
{!isLoading && record && (
<div className="space-y-4">
{/* Field values summary */}
{metaData && (
<div className="overflow-x-auto rounded-xl border border-border">
<table className="min-w-full text-sm" data-qqq-id="record-dev-field-table">
<thead className="bg-muted">
<tr>
<th className="px-4 py-2 text-left font-medium text-foreground">
Field
</th>
<th className="px-4 py-2 text-left font-medium text-foreground">
Type
</th>
<th className="px-4 py-2 text-left font-medium text-foreground">
Value
</th>
</tr>
</thead>
<tbody>
{Object.entries(record.values).map(([fieldName, value]) => {
const field = metaData.fields[fieldName]
return (
<tr
key={fieldName}
className="border-t border-border"
>
<td className="px-4 py-2 font-mono text-xs text-primary">
{fieldName}
</td>
<td className="px-4 py-2 font-mono text-xs text-muted-foreground">
{field?.type ?? '—'}
</td>
<td className="px-4 py-2 font-mono text-xs text-foreground">
{value === null || value === undefined
? <span className="text-muted-foreground italic">null</span>
: String(value)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
<JsonBlock label="Record Raw Values as JSON" value={record.values} />
{metaData && developerData?.associatedScripts.map((associated) => {
const fieldName = associated.associatedScript.fieldName
const scriptId = record.values[fieldName]
return (
<AssociatedScriptViewer
key={fieldName}
tableName={slug}
recordId={recordId}
fieldLabel={metaData.fields[fieldName]?.label ?? fieldName}
data={associated}
scriptId={typeof scriptId === 'string' || typeof scriptId === 'number' ? scriptId : null}
canCreate={Boolean(metaData.editPermission)}
canEdit={canEditScripts}
canTest={canTestScripts}
logsViewAllHref={logsViewAllHref}
onChanged={reloadRecord}
/>
)
})}
{metaData && <JsonBlock label="Table Metadata" value={metaData} defaultOpen={false} />}
</div>
)}
</div>
)
}
/**
* Collapsible card that renders a JSON value as a formatted `<pre>` block.
*
* @param props - Component props.
* @param props.label - Heading displayed in the toggle button.
* @param props.value - The value to serialize and display as JSON.
* @param props.defaultOpen - Whether the panel starts expanded. Defaults to `true`.
* @returns A collapsible JSON inspector card.
*/
function JsonBlock({
label,
value,
defaultOpen = true,
}: {
label: string
value: unknown
defaultOpen?: boolean
}) {
const [open, setOpen] = React.useState(defaultOpen)
return (
<div className="overflow-hidden rounded-xl border border-border">
<h3 className="m-0">
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex w-full items-center justify-between bg-muted px-4 py-3 text-sm font-medium text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-inset focus:ring-ring"
aria-expanded={open}
data-qqq-id="button-json-block-toggle"
>
<span className="flex items-center gap-2">
<Code className="h-4 w-4" aria-hidden="true" />
{label}
</span>
{open ? (
<ChevronUp className="h-4 w-4" aria-hidden="true" />
) : (
<ChevronDown className="h-4 w-4" aria-hidden="true" />
)}
</button>
</h3>
{open && (
<pre
className="overflow-x-auto bg-gray-900 p-4 text-xs text-green-300"
data-qqq-id="json-output"
>
{JSON.stringify(value, null, 2)}
</pre>
)}
</div>
<Suspense fallback={<div role="status" aria-label="Loading developer view" className="py-12 text-center">Loading developer view…</div>}>
<RecordDeveloperView />
</Suspense>
)
}
@@ -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 () => {
@@ -228,9 +234,9 @@ describe('RecordViewPage independent association loading', () => {
expect(within(dialog).queryByLabelText('Person')).not.toBeInTheDocument()
fireEvent.change(within(dialog).getByLabelText(/Name/), { target: { value: 'Created child' } })
fireEvent.click(within(dialog).getByRole('button', { name: 'Create' }))
await waitFor(() => expect(post).toHaveBeenCalled())
expect(post.mock.calls[0][0]).toBe('/table/pet')
const values = post.mock.calls[0][1] as FormData
// the record view also records a recent view (collaboration) through POST; find the insert
await waitFor(() => expect(post.mock.calls.some((call) => call[0] === '/table/pet')).toBe(true))
const values = post.mock.calls.find((call) => call[0] === '/table/pet')![1] as FormData
expect(values.get('personId')).toBe('2')
expect(values.get('birthDate')).toBe('1990-01-15')
expect(values.get('name')).toBe('Created child')
@@ -33,6 +33,7 @@ import React, { lazy, Suspense, useEffect } from 'react'
import { useQueries, useQuery } from '@tanstack/react-query'
import { useRouteParams } from '@/lib/hooks/use-route-params'
import { AlertsRecordPanelSlot } from '@/components/alerts/AlertsRecordPanelSlot'
import { addRecentRecord, removeRecentRecord } from '@/lib/utils/recent-records'
import { recordAnalytics } from '@/lib/analytics'
import { getErrorStatusCode } from '@/lib/utils/error-utils'
@@ -58,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:
@@ -199,7 +200,9 @@ export default function RecordViewPage() {
{record && associations.isError && (
<p role="alert" className="mb-4 text-destructive">Related records could not be loaded.</p>
)}
<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}
@@ -215,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>
}
+1 -1
View File
@@ -46,7 +46,7 @@ import { NotFoundState } from '@/components/layout/NotFoundState'
// Metadata selects one screen. Keep the other three screens out of this route's first load.
const RecordQuery = lazy(() => import('@/components/query/RecordQuery').then((module) => ({ default: module.RecordQuery })))
const ProcessRun = lazy(() => import('@/components/process/ProcessRun').then((module) => ({ default: module.ProcessRun })))
import { ProcessRun } from '@/components/process/ProcessRunLazy'
const AppHome = lazy(() => import('@/components/widgets/AppHome').then((module) => ({ default: module.AppHome })))
const ReportRun = lazy(() => import('@/components/reports/ReportRun').then((module) => ({ default: module.ReportRun })))
@@ -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>
)
}
@@ -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 />
}
@@ -0,0 +1,47 @@
/*
* 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 Notifications page — the notification center's full list (qbit-alerts), with filters
* and paging in the URL (`status`, `category`, `severity`, `offset`). A fixed page under `/app`
* like `/app/search`, so the static export emits it and the dashboard layout wraps it.
*/
'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 NotificationsPage = dynamic(() => import('@/components/alerts/NotificationsPage').then((module) => module.NotificationsPage), {
ssr: false,
loading: () => (
<p className="flex items-center gap-2 p-6 text-sm text-muted-foreground" role="status" data-qqq-id="notifications-page-loading">
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
Loading notifications…
</p>
),
})
/**
* Renders the notifications page at `/app/notifications`.
*
* @returns The page.
*/
export default function NotificationsRoute() {
return <NotificationsPage />
}
+84
View File
@@ -0,0 +1,84 @@
/*
* 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.
*/
// Tests for the /app home page: Next's overview, or the instance's home app (QRun-IO/qqq#1059)
import React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const replace = vi.fn()
vi.mock('next/navigation', () => ({
usePathname: () => '/app',
useRouter: () => ({ push: vi.fn(), replace }),
}))
vi.mock('@/lib/context/q-context', () => ({ useQContext: () => ({ setPageHeader: vi.fn() }) }))
vi.mock('@/lib/auth/use-auth', () => ({ useAuth: () => ({ user: { name: 'Avery Example' } }) }))
const metaData = vi.hoisted(() => ({ value: {} as Record<string, unknown> }))
vi.mock('@/lib/api/metadata', () => ({ loadMetaData: vi.fn(async () => metaData.value) }))
import DashboardPage from './page'
/**
* Instance metadata with two apps, optionally designating a home app.
*
* @param homeAppName - The home app.
* @returns The metadata.
*/
function instance(homeAppName?: string): Record<string, unknown> {
return {
apps: { home: { name: 'home', label: 'Home' }, cms: { name: 'cms', label: 'CMS' } },
appTree: [{ name: 'home', label: 'Home', type: 'APP', children: [] }, { name: 'cms', label: 'CMS', type: 'APP', children: [] }],
tables: {}, processes: {}, reports: {}, widgets: {},
...(homeAppName ? { homeAppName } : {}),
}
}
/** Renders the page with a fresh query client. */
function renderPage() {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(<QueryClientProvider client={client}><DashboardPage /></QueryClientProvider>)
}
beforeEach(() => {
replace.mockReset()
})
describe('/app home page', () => {
it('shows the overview when the instance designates no home app', async () => {
metaData.value = instance()
renderPage()
await waitFor(() => expect(screen.getByText('CMS')).toBeInTheDocument())
expect(document.querySelector('[data-qqq-id="dashboard-home"]')).toBeInTheDocument()
expect(replace).not.toHaveBeenCalled()
})
it('goes to the home app dashboard instead of the overview when the instance designates one', async () => {
metaData.value = instance('home')
renderPage()
await waitFor(() => expect(replace).toHaveBeenCalledWith('/app/home'))
expect(document.querySelector('[data-qqq-id="dashboard-home"]')).not.toBeInTheDocument()
expect(document.querySelector('[data-qqq-id="dashboard-home-redirect"]')).toHaveAttribute('aria-busy', 'true')
})
it('keeps the overview when the designated home app is not one the user may open', async () => {
metaData.value = { ...instance(), homeAppName: 'secret' }
renderPage()
await waitFor(() => expect(screen.getByText('CMS')).toBeInTheDocument())
expect(replace).not.toHaveBeenCalled()
})
})
+16 -1
View File
@@ -22,6 +22,7 @@
import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useQuery } from '@tanstack/react-query'
import {
Workflow,
@@ -44,6 +45,7 @@ import { canInsertRecords, canRunProcess } from '@/lib/auth/permissions'
import { useAppTreeRoutes } from '@/lib/hooks/use-routes'
import type { SidebarRoute } from '@/lib/hooks/use-routes'
import { getRecentRecords } from '@/lib/utils/recent-records'
import { DEFAULT_HOME_PATH } from '@/lib/utils/home-app'
import type { RecentRecord } from '@/lib/utils/recent-records'
import { MetadataIcon, type MetadataIconKind } from '@/components/layout/MetadataIcon'
@@ -102,7 +104,15 @@ export default function DashboardPage() {
staleTime: 1000 * 60 * 30,
})
const { sidebarRoutes, navTargets } = useAppTreeRoutes(metaData)
const { sidebarRoutes, navTargets, homePath } = useAppTreeRoutes(metaData)
const router = useRouter()
const hasHomeApp = homePath !== DEFAULT_HOME_PATH
// An instance with a home app (homeAppName) shows that app's dashboard as its home page
// (the backend redirects full page loads of /app; this covers client-side navigation).
useEffect(() => {
if (hasHomeApp) router.replace(homePath)
}, [hasHomeApp, homePath, router])
const appRoutes = sidebarRoutes.filter((route) => route.nodeType === 'APP')
const appName = metaData?.branding?.appName || 'QQQ Admin'
@@ -145,6 +155,10 @@ export default function DashboardPage() {
{ id: 'widgets', label: 'Widgets', value: widgetNames.size, hint: 'Dashboard components', icon: <TrendingUp className="h-4 w-4 text-muted-foreground" aria-hidden="true" /> },
]
if (hasHomeApp) {
return <div aria-busy="true" data-qqq-id="dashboard-home-redirect" />
}
return (
<div className="space-y-8" data-qqq-id="dashboard-home">
{/* Welcome header */}
@@ -226,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,39 @@
/*
* 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 Preferences page entry point, compatible with the dashboard static export. */
'use client'
import dynamic from 'next/dynamic'
const PreferencesPage = dynamic(
() => import('@/components/preferences/PreferencesPage').then((module) => module.PreferencesPage),
{
ssr: false,
loading: () => (
<p role="status" className="p-6 text-muted-foreground">
Loading preferences…
</p>
),
}
)
/** Renders current-user preferences.
* @returns The dynamically loaded preferences page.
*/
export default function PreferencesRoute() {
return <PreferencesPage />
}
+18 -164
View File
@@ -15,179 +15,33 @@
*/
/**
* @file Search page — every page (app, table, process, report) and recently viewed record matching
* `?q=`, plus the records a backend record search finds when the metadata advertises searchable tables.
* @file Search page — every page (app, table, process, report) and recently viewed record
* matching `?q=`, plus the records a backend record search finds when the metadata
* advertises searchable tables. URL state: `q`, `tables`, `view=pages`, `sort=recent`.
*/
'use client'
import React, { useState, useEffect, useMemo } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { useQuery } from '@tanstack/react-query'
import Link from 'next/link'
import { Search } from 'lucide-react'
import React from 'react'
import dynamic from 'next/dynamic'
import { Loader2 } from 'lucide-react'
import { loadMetaData } from '@/lib/api/metadata'
import { queryKeys } from '@/lib/query-client'
import { useQContext } from '@/lib/context/q-context'
import { useAppTreeRoutes } from '@/lib/hooks/use-routes'
import { useRecordSearch } from '@/lib/hooks/use-record-search'
import { getRecentRecords } from '@/lib/utils/recent-records'
import type { RecentRecord } from '@/lib/utils/recent-records'
import { buildNavigationSearchItems } from '@/lib/utils/navigation-search'
import { searchableTables } from '@/lib/utils/record-search'
import { MetadataIcon, type MetadataIconKind } from '@/components/layout/MetadataIcon'
/** Fallback icon kind for each app-tree node type. */
const ICON_KIND: Record<string, MetadataIconKind> = { APP: 'app', TABLE: 'table', PROCESS: 'process', REPORT: 'report' }
/** Records listed per table on the search page. */
const PAGE_RECORDS_PER_TABLE = 25
/** The results page loads on demand, so its code stays out of every route's first load. */
const SearchPage = dynamic(() => import('@/components/search/SearchPage').then((module) => module.SearchPage), {
ssr: false,
loading: () => (
<p className="flex items-center gap-2 p-6 text-sm text-muted-foreground" role="status" data-qqq-id="search-page-loading">
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
Loading search…
</p>
),
})
/**
* Renders the search page at `/app/search`.
*
* Pages and recently viewed records match locally by label. When the metadata
* lists searchable tables, the records a backend record search finds are listed
* too; otherwise no search request is made. The `q` parameter is kept in the URL
* so results are shareable and survive refresh.
*
* @returns The search input and matching pages and records.
* @returns The search page.
*/
export default function SearchResultsPage() {
const searchParams = useSearchParams()
const router = useRouter()
const { setPageHeader } = useQContext()
const term = searchParams.get('q') ?? ''
const [inputValue, setInputValue] = useState(term)
const [recentRecords, setRecentRecords] = useState<RecentRecord[]>([])
const { data: metaData } = useQuery({
queryKey: queryKeys.metadataAll(),
queryFn: loadMetaData,
staleTime: 1000 * 60 * 30,
})
const { navTargets } = useAppTreeRoutes(metaData)
useEffect(() => {
setInputValue(term)
}, [term])
useEffect(() => {
setRecentRecords(getRecentRecords())
}, [])
useEffect(() => {
setPageHeader('Search')
}, [setPageHeader])
const searchTables = useMemo(() => searchableTables(metaData), [metaData])
const recordSearch = useRecordSearch(term, searchTables, PAGE_RECORDS_PER_TABLE)
const items = useMemo(
() => (term.trim() ? buildNavigationSearchItems(navTargets, recentRecords, term, 50, recordSearch.results) : []),
[navTargets, recentRecords, term, recordSearch.results]
)
const pages = items.filter((item) => item.kind === 'page')
const found = items.filter((item) => item.kind === 'result')
const records = items.filter((item) => item.kind === 'record')
const searching = recordSearch.available && recordSearch.isSearching
/**
* Writes the input value to the `q` URL parameter.
*
* @param event - Form submit event.
*/
function handleSubmit(event: React.FormEvent) {
event.preventDefault()
const next = inputValue.trim()
router.replace(next ? `/app/search?q=${encodeURIComponent(next)}` : '/app/search')
}
return (
<div className="space-y-6" data-qqq-id="search-results-page">
<h1 className="text-2xl font-bold tracking-tight text-foreground">
{term ? <>Search results for &ldquo;{term}&rdquo;</> : 'Search'}
</h1>
<form role="search" onSubmit={handleSubmit} className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
<input
type="search"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder={recordSearch.available ? 'Find an app, table, process or record...' : 'Find an app, table, process or recent record...'}
aria-label={recordSearch.available ? 'Search pages and records' : 'Search pages and recent records'}
className="w-full rounded-xl border border-input bg-card py-3 pl-10 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
data-qqq-id="search-input"
/>
</form>
{!term && (
<p className="rounded-xl border border-border bg-card py-12 text-center text-sm text-muted-foreground" data-qqq-id="search-prompt-state">
Enter text above to find apps, tables, processes and recently viewed records.
</p>
)}
{/* Record search progress and errors, announced politely */}
<p role="status" aria-live="polite" className={searching || (recordSearch.available && recordSearch.isError) ? 'text-sm text-muted-foreground' : 'sr-only'} data-qqq-id="search-records-status">
{searching ? 'Searching records…' : recordSearch.available && recordSearch.isError ? 'Record search failed. Try again.' : ''}
</p>
{term && items.length === 0 && !searching && (
<p className="rounded-xl border border-border bg-card py-12 text-center text-sm text-muted-foreground" data-qqq-id="search-empty-state">
{recordSearch.available ? 'No pages or records match' : 'No pages or recent records match'} &ldquo;{term}&rdquo;.
</p>
)}
{pages.length > 0 && (
<section aria-label="Pages" className="overflow-hidden rounded-xl border border-border bg-card shadow-sm" data-qqq-id="search-group-pages">
<h2 className="border-b border-border px-6 py-3 text-sm font-semibold text-foreground">Pages</h2>
<ul className="divide-y divide-border">
{pages.map((item) => (
<li key={item.path}>
<Link href={item.path} className="flex items-center gap-3 px-6 py-3 text-sm transition-colors hover:bg-accent" data-qqq-id={`search-page-${item.path.replace(/\//g, '-')}`}>
<MetadataIcon icon={item.icon} kind={item.nodeType ? ICON_KIND[item.nodeType] : 'app'} className="text-muted-foreground" />
<span className="font-medium text-foreground">{item.label}</span>
{item.context && <span className="text-xs text-muted-foreground">{item.context}</span>}
</Link>
</li>
))}
</ul>
</section>
)}
{found.length > 0 && (
<section aria-label="Records" className="overflow-hidden rounded-xl border border-border bg-card shadow-sm" data-qqq-id="search-group-found-records">
<h2 className="border-b border-border px-6 py-3 text-sm font-semibold text-foreground">Records</h2>
<ul className="divide-y divide-border">
{found.map((item) => (
<li key={item.path}>
<Link href={item.path} className="flex items-center gap-3 px-6 py-3 text-sm transition-colors hover:bg-accent" data-qqq-id={`search-record-${item.path.replace(/\//g, '-')}`}>
<span className="font-medium text-foreground">{item.label}</span>
{item.context && <span className="text-xs text-muted-foreground">{item.context}</span>}
</Link>
</li>
))}
</ul>
</section>
)}
{records.length > 0 && (
<section aria-label="Recently viewed" className="overflow-hidden rounded-xl border border-border bg-card shadow-sm" data-qqq-id="search-group-records">
<h2 className="border-b border-border px-6 py-3 text-sm font-semibold text-foreground">Recently viewed</h2>
<ul className="divide-y divide-border">
{records.map((item) => (
<li key={item.path}>
<Link href={item.path} className="flex items-center gap-3 px-6 py-3 text-sm transition-colors hover:bg-accent" data-qqq-id={`search-recent-${item.path.replace(/\//g, '-')}`}>
<MetadataIcon icon={item.icon} kind="table" className="text-muted-foreground" />
<span className="font-medium text-foreground">{item.label}</span>
<span className="text-xs text-muted-foreground">{item.context}</span>
</Link>
</li>
))}
</ul>
</section>
)}
</div>
)
return <SearchPage />
}
@@ -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>
)
}
+132 -66
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,14 +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 { Sparkles } from 'lucide-react'
import Sidebar from '@/components/layout/Sidebar'
import Header from '@/components/layout/Header'
import BannerComponent from '@/components/layout/Banner'
@@ -46,8 +51,18 @@ 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, useAlertsInstalled } from '@/components/alerts/AlertsBell'
import { NotificationBannerSlot } from '@/components/alerts/NotificationBannerSlot'
/**
* Inner layout that renders the full dashboard chrome.
@@ -60,7 +75,8 @@ import { KeyboardShortcutsDialog } from '@/components/feedback/KeyboardShortcuts
* - 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.
@@ -75,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
@@ -91,6 +109,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
data: metaData,
isLoading: metaLoading,
error: metaError,
refetch: reloadMetaData,
} = useQuery({
queryKey: queryKeys.metadataAll(),
queryFn: loadMetaData,
@@ -98,16 +117,29 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
enabled: isAuthenticated,
})
// 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 } = 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(() => {
@@ -116,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
@@ -140,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
@@ -148,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(() => {
@@ -179,61 +209,57 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
/**
* Global `keydown` handler that drives the application-level keyboard shortcuts.
*
* - Cmd+K / Ctrl+K — toggles the command palette.
* - 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).
* - 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 (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) {
e.preventDefault()
setCommandOpen(false)
toggleAssistant()
}
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])
}, [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) {
@@ -245,17 +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 (
<div className="flex h-screen flex-col overflow-hidden bg-background">
<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} />
@@ -265,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>
@@ -273,9 +311,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
<Sidebar
routes={sidebarRoutes}
branding={metaData?.branding}
logout={logout}
userName={user?.name}
userEmail={user?.email}
homePath={homePath}
showFavorites={isAuthenticated}
data-qqq-id="sidebar-container"
/>
@@ -283,35 +320,54 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
<Sidebar
routes={sidebarRoutes}
branding={metaData?.branding}
logout={logout}
userName={user?.name}
userEmail={user?.email}
homePath={homePath}
showFavorites={isAuthenticated}
onSearchOpen={() => openSearch()}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
returnFocusRef={menuButtonRef}
data-qqq-id="sidebar-mobile"
/>
{/* Main content area */}
<div className="flex flex-1 flex-col overflow-hidden">
{/* Main content area (min-w-0: the docked assistant panel never pushes it wider than the window) */}
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
{/* Header — breadcrumbs + search in one row */}
<Header
appName={metaData?.branding?.appName}
onMenuOpen={() => setSidebarOpen(true)}
menuOpen={sidebarOpen}
menuButtonRef={menuButtonRef}
onSearchOpen={() => setSearchOpen(true)}
onCommandOpen={() => setCommandOpen(true)}
onHelpOpen={() => setHelpOpen(true)}
helpButtonRef={helpButtonRef}
onSearchOpen={() => openSearch()}
pathToLabelMap={pathToLabelMap}
ancestorAppMap={ancestorAppMap}
navTargets={navTargets}
searchTables={searchTables}
homePath={homePath}
assistantButton={<AssistantHeaderButton />}
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="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"
@@ -324,23 +380,33 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
) : (
<>
<BannerComponent banners={metaData?.branding?.banners} slot="QFMD_TOP_OF_BODY" className="mb-4 rounded-lg" />
<NotificationBannerSlot metaData={metaData} enabled={isAuthenticated} />
{children}
<CompanyFooter branding={metaData?.branding} />
</>
)}
</main>
</div>
{/* Assistant panel: docked beside the page from 1024 px, a sheet below; lazily loaded */}
<AssistantMount />
</div>
{/* Command Palette */}
<CommandMenu open={commandOpen} onClose={() => setCommandOpen(false)} navTargets={navTargets} metaData={metaData} />
<CommandMenu
open={commandOpen}
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} />
{/* Keyboard Shortcuts Sheet */}
<KeyboardShortcutsDialog open={helpOpen} onClose={() => setHelpOpen(false)} assistantEnabled={assistantEnabled} />
</div>
</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>
)
}
Loaded 100 of 874 files, more files were not shown because too many files have changed in this diff. Show more