Compare commits

..
Author SHA1 Message Date
james.maesandClaude Opus 5.5 9e8bdb9261 docs: before/after screenshots for responsive readable layout PR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:43:33 -05:00
1498 changed files with 2 additions and 366534 deletions

No files matched your search

-14
View File
@@ -1,14 +0,0 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: /.*/
tags:
only: /.*/
-60
View File
@@ -1,60 +0,0 @@
{
"permissions": {
"allow": [
"Bash(which pnpm:*)",
"Bash(node:*)",
"Bash(npm install:*)",
"Bash(pnpm create:*)",
"Bash(pnpm install:*)",
"Bash(pnpm approve-builds:*)",
"Bash(pnpm add:*)",
"Bash(pnpm tsc:*)",
"Bash(pnpm test:*)",
"Bash(pnpm build:*)",
"Bash(ls:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(cat:*)",
"Bash(git:*)",
"Bash(pnpm dlx:*)",
"Bash(echo killed:*)",
"Bash(cp:*)",
"Bash(grep:*)",
"Bash(sleep 1:*)",
"Bash(pnpm list:*)",
"Bash(lsof:*)",
"Bash(rm:*)",
"Bash(pnpm lint:*)",
"Bash(pnpm test:e2e:*)",
"Bash(head:*)",
"Bash(NEXT_PUBLIC_MOCK_API=true pnpm test:e2e:*)",
"Bash(pnpm test:coverage:*)",
"Bash(pnpm audit:*)",
"Bash(pnpm up:*)",
"Bash(pnpm remove:*)",
"Bash(./node_modules/.bin/next lint:*)",
"Bash(NEXT_PUBLIC_MOCK_API=true pnpm dev:*)",
"mcp__circleci-mcp-server__list_followed_projects",
"Bash(echo circleci CLI not found:*)",
"Bash(circleci config:*)",
"Bash(tail:*)",
"mcp__circleci-mcp-server__get_latest_pipeline_status",
"Bash(circleci orb:*)",
"Bash(circleci diagnostic:*)",
"mcp__circleci-mcp-server__run_pipeline",
"mcp__circleci-mcp-server__get_build_failure_logs",
"Bash(cd /Users/james.maes/Git.Local/QRun-IO/qqq-orb && make validate 2>&1)",
"mcp__circleci-mcp-server__get_job_test_results",
"Bash(pnpm typecheck:*)",
"Bash(pnpm dev:*)",
"Bash(python3:*)",
"Bash(echo Exit: $?:*)",
"Bash(gh api:*)",
"Bash(pnpm why:*)",
"Bash(npm view:*)",
"Bash(chmod +x:*)",
"Bash(done)",
"Bash(pnpm exec:*)"
]
}
}
-10
View File
@@ -1,10 +0,0 @@
.git
.next
node_modules
test-results
playwright-report
coverage
.env*
*.log
.pnpm-store
-8
View File
@@ -1,8 +0,0 @@
# Mock API environment — copy to .env.local to run without a real backend
#
# cp .env.mock .env.local && pnpm dev
# OR pass inline:
# NEXT_PUBLIC_MOCK_API=true pnpm dev
NEXT_PUBLIC_MOCK_API=true
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/qqq/v1
-1
View File
@@ -1 +0,0 @@
dynamic-components/** -text
-13
View File
@@ -1,13 +0,0 @@
# Copyright 2026 QRun.IO, Inc.
# Licensed under the Apache License, Version 2.0.
# https://www.apache.org/licenses/LICENSE-2.0
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
with:
node-version: '22.20.0'
prebuild: corepack enable && pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm test && pnpm build:export
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
# .githooks/pre-commit
#
# Enforces the Apache 2.0 license header on all staged TypeScript/TSX files.
# Registered automatically via "prepare" in package.json.
#
# To install hooks manually:
# git config core.hooksPath .githooks
pnpm lint:license
-5
View File
@@ -1,5 +0,0 @@
# 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
@@ -1,32 +0,0 @@
# 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]
-41
View File
@@ -1,41 +0,0 @@
name: Next centralized CI
'on':
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
- unlabeled
push:
branches:
- main
tags:
- v*
workflow_dispatch:
inputs:
qqq_ref:
description: QQQ source ref for acceptance
type: string
required: false
permissions:
contents: read
jobs:
ci:
if: (github.event_name == 'pull_request' && !contains(fromJSON('["labeled","unlabeled"]'),
github.event.action)) || (github.event_name == 'push' && github.ref == 'refs/heads/main')
uses: QRun-IO/qqq-orb/.github/workflows/next-ci.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
acceptance:
if: github.event_name != 'pull_request' || !contains(fromJSON('["labeled","unlabeled"]'),
github.event.action) || github.event.label.name == 'full-acceptance'
uses: QRun-IO/qqq-orb/.github/workflows/next-acceptance.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
with:
qqq_ref: ${{ inputs.qqq_ref || '' }}
dashboard-image:
if: (github.event_name == 'pull_request' && !contains(fromJSON('["labeled","unlabeled"]'),
github.event.action)) || (github.event_name == 'push' && startsWith(github.ref,
'refs/tags/v'))
permissions:
contents: read
packages: write
uses: QRun-IO/qqq-orb/.github/workflows/next-dashboard-image.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
-48
View File
@@ -1,48 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/.next-collaboration/
/.next-presence-live/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.claude/
target/
/test-results/
/playwright-report/
/log/
/.playwright-mcp/
/storybook-static/
/public/scripting-generated/
/public/pdf-generated/
/public/dynamic-qfmd-components/
-5
View File
@@ -1,5 +0,0 @@
# Reviewed findings committed before the Gitea migration; exact historical fingerprints only.
0969231c95f876bb0a41529e8c6c4621a0257aee:src/lib/qa-capture/sanitizers.test.ts:jwt:33
23a855297de0f84b564b20d782e01180a68b5257:tests/acceptance/specs/security/restrictions.spec.ts:generic-api-key:248
-9
View File
@@ -1,9 +0,0 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always"
}
-3
View File
@@ -1,3 +0,0 @@
profile: frontend-next
orb_version: '0.8'
default_branch: main
-43
View File
@@ -1,43 +0,0 @@
import type { StorybookConfig } from '@storybook/nextjs-vite'
import { fileURLToPath } from 'node:url'
// The Vite builder: the webpack-based @storybook/nextjs framework bundled Node core
// 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',
],
framework: {
name: '@storybook/nextjs-vite',
options: {},
},
viteFinal: async (config) => {
const src = fileURLToPath(new URL('../src', import.meta.url))
const alias = config.resolve?.alias
config.resolve = {
...config.resolve,
alias: Array.isArray(alias)
? [...alias, { find: '@', replacement: src }]
: { ...alias, '@': src },
}
// Rollup drops the Next.js "use client" directives (meaningless in Storybook) and warns
// once per client module; keep the build log to the warnings that matter.
const onwarn = config.build?.rollupOptions?.onwarn
config.build = {
...config.build,
rollupOptions: {
...config.build?.rollupOptions,
onwarn(warning, warn) {
if (warning.code === 'MODULE_LEVEL_DIRECTIVE' || warning.code === 'SOURCEMAP_ERROR') return
if (onwarn) onwarn(warning, warn)
else warn(warning)
},
},
}
return config
},
}
export default config
-21
View File
@@ -1,21 +0,0 @@
import type { Preview } from '@storybook/nextjs-vite'
import '../src/styles/globals.css'
const preview: Preview = {
tags: ['autodocs'],
initialGlobals: { backgrounds: { value: 'light' } },
parameters: {
layout: 'centered',
backgrounds: {
options: {
light: { name: 'light', value: '#ffffff' },
dark: { name: 'dark', value: '#0f172a' },
},
},
nextjs: {
appDirectory: true,
},
},
}
export default preview
-9
View File
@@ -1,9 +0,0 @@
<!-- 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 -->
-201
View File
@@ -1,201 +0,0 @@
# CLAUDE.md — QQQ Frontend Next
## Project Overview
This is the **QQQ Admin UI Modernization** project — a complete rewrite of the QQQ admin frontend from React + Material UI to Next.js + Tailwind CSS + shadcn/ui.
QQQ is a metadata-driven, low-code application framework. The admin UI renders entirely from backend metadata — no table names, field lists, or navigation items are hardcoded. The backend declares metadata (tables, processes, apps, widgets), and the frontend renders it dynamically.
## Tech Stack
- **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)
- **Data Grid:** TanStack Table v8 (replaces MUI X DataGrid Pro)
- **Forms:** React Hook Form v7 + Zod validation (replaces Formik + Yup)
- **Data Fetching:** TanStack Query v5
- **Charts:** Recharts
- **Testing:** Vitest (unit), Playwright (E2E), axe-core (a11y)
- **Package Manager:** pnpm
## Directory Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── (auth)/ # Auth routes (login, callback)
│ ├── (dashboard)/ # Authenticated routes
│ │ ├── layout.tsx # Dashboard layout (sidebar + nav + banners)
│ │ └── app/ # All app routes
│ │ ├── [appName]/page.tsx # App home (dashboard)
│ │ ├── [tableName]/page.tsx # Record query
│ │ ├── [tableName]/create/page.tsx # Create record
│ │ ├── [tableName]/[recordId]/page.tsx # Record view
│ │ ├── [tableName]/[recordId]/edit/page.tsx # Edit record
│ │ └── [processName]/page.tsx # Process execution
│ ├── layout.tsx # Root layout (providers)
│ ├── error.tsx # Global error boundary
│ └── not-found.tsx # 404
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── layout/ # Sidebar, Header, Breadcrumbs, Banner
│ ├── records/ # RecordView, FieldValue
│ ├── forms/ # DynamicForm, EntityForm, field components
│ ├── query/ # DataGrid, FilterBuilder, ColumnConfig, Pagination
│ ├── process/ # StepWizard, ValidationReview, BulkLoad
│ ├── widgets/ # Widget containers, charts, blocks, grids
│ └── feedback/ # Alerts, Toasts, Modals, CommandMenu
├── lib/
│ ├── api/ # Typed API client modules
│ │ ├── client.ts # Base axios client with 401 interceptor
│ │ ├── auth.ts # Auth endpoints
│ │ ├── metadata.ts # Metadata endpoints
│ │ ├── tables.ts # CRUD + query/count
│ │ ├── processes.ts # Process lifecycle
│ │ ├── widgets.ts # Widget data
│ │ └── possible-values.ts
│ ├── auth/ # Auth providers (OAuth2, Auth0, Anonymous)
│ ├── hooks/ # Shared React hooks
│ ├── utils/ # Pure utility functions
│ ├── context/ # React contexts (QContext)
│ └── theme/ # Theme provider, CSS tokens
├── types/ # TypeScript type definitions
└── styles/ # Global styles, theme tokens
```
## Key Conventions
### Metadata-Driven Rendering
- ALL components that render from metadata accept it as a prop — never fetch internally
- No hardcoded table names, field names, or process names anywhere
- User-facing text uses `label` from metadata, never `name`
- UI structure derives entirely from backend metadata
### API Integration
- All API calls go through typed functions in `src/lib/api/` — never raw `fetch`
- TanStack Query manages server state (caching, deduplication, revalidation)
- Query keys follow the factory pattern in `src/lib/query-client.ts`
- 401 responses intercepted globally → redirect to login
### Component Patterns
- Server Components: root layouts, metadata-fetching pages
- Client Components: interactive UI (forms, grids, filters) — mark with `"use client"`
- Every interactive component has a `data-qqq-id` attribute for CSS customization
- Naming: `data-qqq-id="{component-type}-{identifier}"` (e.g., `data-qqq-id="button-save"`)
### State Management
- Server state: TanStack Query
- UI state: React context (QContext)
- Form state: React Hook Form
- URL state: Next.js searchParams (filters, pagination — shareable URLs)
- Local persistence: localStorage (density, column widths, saved views)
### File Naming
- Components: PascalCase (`RecordQuery.tsx`)
- Hooks: camelCase with `use-` prefix (`use-metadata.ts`)
- Utilities: kebab-case (`filter-utils.ts`)
- Types: kebab-case (`metadata.ts`)
- Tests: `*.test.ts(x)` co-located with source
### Import Order
```typescript
// 1. React/Next.js
import { useState } from 'react'
import { useRouter } from 'next/navigation'
// 2. External libraries
import { useQuery } from '@tanstack/react-query'
// 3. Types
import type { QTableMetaData } from '@/types'
// 4. Lib (API, hooks, utils)
import { queryRecords } from '@/lib/api/tables'
// 5. Components
import { Button } from '@/components/ui/button'
```
### Error Handling
- Error boundaries at route level (`error.tsx`) and widget level
- API errors via TanStack Query `onError` → toast notification
- Form validation: Zod schemas from field metadata
- Loading: skeleton components matching final layout
- Empty: descriptive messages with action buttons
### Accessibility (WCAG 2.1 AA)
- All form inputs: `<label>` with `htmlFor`, `aria-required`, `aria-invalid`
- All buttons: descriptive text or `aria-label`
- Focus trapped in modals, restored on close
- Keyboard navigation for all interactive elements
- Color contrast: 4.5:1 normal, 3:1 large text
## Commands
```bash
pnpm dev # Start dev server
pnpm build # Production build
pnpm test # Run Vitest unit tests
pnpm test:e2e # Run Playwright E2E tests
pnpm tsc --noEmit # Type check without emitting
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:
1. **Package 1** — Scaffold, Auth, Layout Shell (foundation — must be first)
2. **Package 2** — Record Query (data grid, filters, pagination, saved views)
3. **Package 3** — Record View + CRUD (forms, field types, validation)
4. **Package 4** — Process Execution (step wizard, async polling, bulk load)
5. **Package 5** — Dashboard + Widgets (charts, statistics, blocks, grids)
6. **Package 6** — Polish + Parity (responsive, a11y, perf, Storybook, E2E)
Shared context for all agents: `docs/implementation-plans/shared-context/`
## Reference Codebase
The current frontend being replaced lives in the sibling `qqq-frontend-material-dashboard` repo. Key files to reference for behavior parity:
- `App.tsx` — route generation, auth flow, layout
- `src/qqq/pages/records/query/RecordQuery.tsx` — most complex page (260+ state vars)
- `src/qqq/pages/records/view/RecordView.tsx` — record detail
- `src/qqq/pages/processes/ProcessRun.tsx` — process execution
- `src/qqq/components/widgets/` — 60+ widget components
## API Base URL
Set via `NEXT_PUBLIC_API_BASE_URL` environment variable. Defaults to `/qqq/v1`.
The API uses cookie-based session auth (`sessionUUID` cookie).
## Knowledge base
Durable, cross-session knowledge about this repo and the wider QQQ platform lives in
the second-brain vault (`R:/Git.Local/KofTwentyTwo/second-brain`):
- Platform hub: `knowledge/qqq/qqq-hub.md`
- This repo's dossier: `knowledge/qqq/repos/qqq-frontend-next.md`
(reviewed at commit `41cd6d2034a8` on branch `main`, 2026-07-04)
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
@@ -1,51 +0,0 @@
# 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.
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+2 -226
View File
@@ -1,227 +1,3 @@
# QQQ Frontend Next
# Screenshots for the responsive / readable layout PR
> **QBit integration branch (2026-11-04).** File delivery/preview, TypeScript editing and generic process controls use the QQQ UIUX APIs. See [current verification and dependency setup](docs/QBIT-RELEASE-STATUS.md) and [remaining release requirements](docs/QBIT-KNOWN-ISSUES.md). These changes are source/local snapshots, not part of the published RC.36 baseline described below.
A metadata-driven admin UI for the QQQ low-code application framework, rewritten from React + Material UI to Next.js 16 + Tailwind CSS + shadcn/ui. The UI renders entirely from backend metadata — no table names, field lists, or navigation items are hardcoded in the frontend.
**Published baseline: Next UI 1.0.0-RC.36**, which adds accessibility fixes: readable link text, 24px clear targets, h2 widget titles and skip-link focus to RC35. Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing; see the [RC36 notes](docs/releases/1.0.0-RC.36.md) for the preparation checkpoint and verification scope. [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
**Current roadmap and known issues:** [Next UI 1.0 tracker](https://github.com/QRun-IO/qqq/issues/713). GitHub issues are the source of truth for status and remaining work; see [issue tracking](docs/ISSUE-TRACKING.md).
## Use it in a QQQ application
The dashboard is the default admin UI for QQQ 4.1+. Add the jar (managed by `qqq-bom-pom`) and `QApplicationJavalinServer` serves it at `/`, on the same port and origin as the API:
```xml
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.36</version>
</dependency>
```
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.
A container image (`ghcr.io/qrun-io/qqq-frontend-next`, built from `docker/quickstart/Dockerfile`) remains available for running the dashboard as a separate Node server in front of a backend on host port 8000. The backend origin is fixed at image build time (`QQQ_BACKEND_URL`).
## Frontend development prerequisites
- **Node.js** 20.19+ or 22.12+ (required by the Storybook developer tooling)
- **pnpm** (install with `npm install -g pnpm` if not already present)
## Frontend development
```bash
git clone https://github.com/QRun-IO/qqq-frontend-next.git
cd qqq-frontend-next
pnpm install
pnpm dev
```
The dev server starts at [http://localhost:3000](http://localhost:3000).
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `NEXT_PUBLIC_API_BASE_URL` | `/qqq/v1` | Base URL for all QQQ backend API calls |
| `QQQ_BACKEND_URL` | _(unset)_ | Standalone build only: backend origin for same-origin API rewrites; the container image uses `http://host.docker.internal:8000` |
| `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.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
| Command | Description |
|---|---|
| `pnpm dev` | Start the Next.js development server (Turbopack) |
| `pnpm build` | Compile the standalone (Node server) production build |
| `pnpm build:export` | Compile the static export served by QQQ (`out/`) |
| `pnpm build:jar` | Static export packaged as the `qqq-frontend-next` Maven jar |
| `pnpm test:acceptance` | Real-backend acceptance suite and matrix gate (needs `QQQ_SAMPLE_JAR`) |
| `pnpm start` | Serve the production build |
| `pnpm test` | Run Vitest unit tests (single pass) |
| `pnpm test:watch` | Run Vitest in watch mode |
| `pnpm test:coverage` | Run Vitest with V8 coverage; fails below the `src/lib` thresholds in `vitest.config.ts` |
| `pnpm test:e2e` | Run the mocked Playwright e2e suite (`tests/e2e`, no backend); `QQQ_E2E_SERVER=production` serves the `pnpm build` output, `QQQ_E2E_PORT` picks the port |
| `pnpm perf:budget` | Check the static export (`out/`) against the bundle budget in `perf-budget.json` ([`docs/acceptance/performance.md`](docs/acceptance/performance.md)) |
| `pnpm typecheck` | Run `tsc --noEmit` (type check without emitting files) |
| `pnpm lint` | Run ESLint + Apache license header check |
| `pnpm format` | Run Prettier over all source files |
| `pnpm storybook` | Start Storybook component explorer on port 6006 |
| `pnpm build-storybook` | Build the static Storybook (`storybook-static/`) |
## Directory Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── (auth)/ # Login and OAuth callback routes
│ ├── (dashboard)/ # Authenticated shell (sidebar + nav)
│ │ └── app/ # All app routes (tables, records, processes)
│ ├── layout.tsx # Root layout — providers
│ ├── error.tsx # Global error boundary
│ └── not-found.tsx # 404 page
├── components/
│ ├── ui/ # shadcn/ui primitives (Button, Input, Dialog, …)
│ ├── layout/ # Sidebar, Header, Breadcrumbs, Banner
│ ├── records/ # RecordView, FieldValue
│ ├── forms/ # DynamicForm, EntityForm, field components
│ ├── query/ # DataGrid, FilterBuilder, ColumnConfig, Pagination
│ ├── process/ # StepWizard, ValidationReview, BulkLoad
│ ├── widgets/ # Widget containers, charts, stat blocks, grids
│ └── feedback/ # Alerts, Toasts, Modals, CommandMenu
├── lib/
│ ├── api/ # Typed API client modules (client.ts, tables.ts, …)
│ ├── auth/ # Auth providers (OAuth2, Auth0, Anonymous)
│ ├── hooks/ # Shared React hooks (useLocalStorage, useMetadata, …)
│ ├── utils/ # Pure utility functions
│ ├── context/ # React contexts (QContext)
│ └── theme/ # Theme provider and CSS custom property tokens
├── types/ # TypeScript type definitions
└── styles/ # Global styles and theme tokens
```
## Documentation
| Document | Purpose |
|---|---|
| [`CLAUDE.md`](./CLAUDE.md) | Full architecture reference — conventions, patterns, key decisions |
| [`docs/STATE-MANAGEMENT.md`](./docs/STATE-MANAGEMENT.md) | When to use each state container (TanStack Query, RHF, URL params, localStorage, QContext) |
| [`docs/API-ERROR-HANDLING.md`](./docs/API-ERROR-HANDLING.md) | Global error handler, 401 interceptor, per-status guidance, code examples |
| [`docs/implementation-plans/`](./docs/implementation-plans/) | Work-package implementation plans (6 packages) |
## Tech Stack
- **Framework:** Next.js 16 (App Router) with React 19
- **Language:** TypeScript 5.x (`strict: true`)
- **Styling:** Tailwind CSS 4.x with CSS custom properties
- **Components:** shadcn/ui (Radix primitives + Tailwind)
- **Data Grid:** TanStack Table v8
- **Forms:** React Hook Form v7 + Zod
- **Data Fetching:** TanStack Query v5
- **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.
Mock CMS app (`NEXT_PUBLIC_MOCK_API=true pnpm dev`), Chromium, before = origin/main b0b0d46, after = feature/responsive-readable-layout. Safe to delete after review.
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

-29
View File
@@ -1,29 +0,0 @@
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS frontend
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
ENV QQQ_BACKEND_URL=http://host.docker.internal:8000
ENV NEXT_PUBLIC_MOCK_API=false
RUN pnpm build
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS runtime
LABEL org.opencontainers.image.source="https://github.com/QRun-IO/qqq-frontend-next"
# Standalone production starts node directly; package managers are build-only.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn-* \
&& rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
/usr/local/bin/pnpm /usr/local/bin/pnpx /usr/local/bin/yarn /usr/local/bin/yarnpkg
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000
COPY --from=frontend --chown=node:node /app/.next/standalone ./
COPY --from=frontend --chown=node:node /app/.next/static ./.next/static
COPY --from=frontend --chown=node:node /app/public ./public
# server.js wrapped with the dashboard security headers (QRun-IO/qqq#734); add origins the
# dashboard must reach (QuickSight, custom component bundles) with QQQ_DASHBOARD_CSP_SOURCES
COPY --from=frontend --chown=node:node /app/standalone/qqq-server.mjs /app/standalone/security-headers.mjs /app/standalone/branding-assets.mjs /app/standalone/backend-prefixes.json ./
COPY LICENSE ./LICENSE
USER node
EXPOSE 3000
CMD ["node", "qqq-server.mjs"]
-293
View File
@@ -1,293 +0,0 @@
# Master Agent Execution Plan — QQQ Frontend Next
## Your Role
You are the **Master Orchestrator Agent**. Your job is to build the entire QQQ admin UI replacement by spawning and controlling implementation sub-agents, one per work package, in the correct dependency order. You delegate all coding work to sub-agents. You verify their output, handle failures, and ensure the full project compiles and integrates at the end.
## Repository
You are working in `qqq-frontend-next` — a new repo for the Next.js replacement of the QQQ admin UI.
### Critical Files in This Repo
```
docs/
├── qqq-admin-modernization-requirements.md # Full requirements spec
├── master-architect-agent-prompt.md # How the plans were designed
├── 00-master-execution-plan.md # THIS FILE — your instructions
└── implementation-plans/
├── 00-project-overview.md # Tech stack, directory structure, conventions
├── 01-scaffold-and-auth.md # Work Package 1 plan
├── 02-record-query.md # Work Package 2 plan
├── 03-record-view-and-crud.md # Work Package 3 plan
├── 04-process-execution.md # Work Package 4 plan
├── 05-dashboard-and-widgets.md # Work Package 5 plan
├── 06-polish-and-parity.md # Work Package 6 plan
├── cross-check-report.md # Requirements coverage verification
├── shared-context/
│ ├── api-contract.md # API spec — give to every agent
│ ├── type-definitions.md # TypeScript types — give to every agent
│ └── coding-conventions.md # Coding standards — give to every agent
└── review-checklists/
├── review-01-scaffold.md through review-06-polish.md
```
### Reference Repos (Read-Only)
If accessible, the agents should also reference:
- `qqq-frontend-material-dashboard` — The current UI being replaced (behavior parity)
- `qqq-frontend-core` — TypeScript model types and API client (types to port)
- `qqq` — Backend middleware (API endpoint source of truth)
---
## Execution Phases
### Phase 1: Foundation (BLOCKING — must complete before any other phase)
**Spawn Agent 1: Package 1 — Scaffold + Auth + Layout Shell**
Instruct the agent:
> Read these files before writing any code:
> - `docs/implementation-plans/01-scaffold-and-auth.md` (your implementation plan)
> - `docs/implementation-plans/shared-context/api-contract.md`
> - `docs/implementation-plans/shared-context/type-definitions.md`
> - `docs/implementation-plans/shared-context/coding-conventions.md`
> - `docs/implementation-plans/00-project-overview.md`
> - `CLAUDE.md`
>
> Also read the reference implementations in `qqq-frontend-material-dashboard`:
> - `src/App.tsx` — Route generation, auth dispatch, layout
> - `src/qqq/components/horseshoe/` — SideNav, NavBar, Breadcrumbs
>
> And from `qqq-frontend-core`:
> - `src/model/**/*.ts` — All TypeScript types to port
> - `src/controllers/QController.ts` — Auth methods
>
> Execute every step in the implementation plan (Steps 1-11).
> After each step, verify: `pnpm tsc --noEmit` passes.
> When done, verify ALL acceptance criteria in Section 9.
> Commit your work.
**Verification gate before proceeding:**
```bash
pnpm dev # Must start without errors
pnpm build # Must succeed
pnpm tsc --noEmit # Zero errors
pnpm test # All tests pass
```
If the agent fails, fix the issues before moving to Phase 2.
---
### Phase 2: Core Pages (CAN BE PARALLEL — no file conflicts)
**Spawn Agent 2: Package 2 — Record Query**
Instruct the agent:
> Read these files before writing any code:
> - `docs/implementation-plans/02-record-query.md` (your implementation plan)
> - `docs/implementation-plans/shared-context/api-contract.md`
> - `docs/implementation-plans/shared-context/type-definitions.md`
> - `docs/implementation-plans/shared-context/coding-conventions.md`
> - `docs/implementation-plans/00-project-overview.md`
> - `CLAUDE.md`
>
> Also read the reference implementation:
> - `qqq-frontend-material-dashboard/src/qqq/pages/records/query/RecordQuery.tsx`
> - `qqq-frontend-material-dashboard/src/qqq/components/query/` (all files)
> - `qqq-frontend-core/src/controllers/QControllerV1.ts` (query/count methods)
>
> Execute every step in the implementation plan.
> The foundation from Package 1 is already in place — import from it.
> After each step: `pnpm tsc --noEmit`
> When done: verify ALL acceptance criteria in Section 9.
> Commit your work.
**Spawn Agent 3: Package 3 — Record View + CRUD**
Instruct the agent:
> Read these files before writing any code:
> - `docs/implementation-plans/03-record-view-and-crud.md` (your implementation plan)
> - `docs/implementation-plans/shared-context/api-contract.md`
> - `docs/implementation-plans/shared-context/type-definitions.md`
> - `docs/implementation-plans/shared-context/coding-conventions.md`
> - `docs/implementation-plans/00-project-overview.md`
> - `CLAUDE.md`
>
> Also read the reference implementation:
> - `qqq-frontend-material-dashboard/src/qqq/pages/records/view/RecordView.tsx`
> - `qqq-frontend-material-dashboard/src/qqq/components/forms/` (all files)
> - `qqq-frontend-material-dashboard/src/qqq/components/forms/EntityForm.tsx`
> - `qqq-frontend-material-dashboard/src/qqq/components/forms/DynamicForm.tsx`
> - `qqq-frontend-core/src/controllers/QController.ts` (CRUD methods: get, create, update, delete)
>
> Execute every step in the implementation plan.
> The foundation from Package 1 is already in place — import from it.
> After each step: `pnpm tsc --noEmit`
> When done: verify ALL acceptance criteria in Section 9.
> Commit your work.
**Verification gate before proceeding:**
Both agents must complete. Then:
```bash
pnpm tsc --noEmit # Zero errors (both packages together)
pnpm test # All tests pass
pnpm build # Must succeed
```
If running in parallel on branches, merge both to main and resolve conflicts before Phase 3.
---
### Phase 3: Process Execution (SEQUENTIAL — needs forms from Package 3)
**Spawn Agent 4: Package 4 — Process Execution**
Instruct the agent:
> Read these files before writing any code:
> - `docs/implementation-plans/04-process-execution.md` (your implementation plan)
> - `docs/implementation-plans/shared-context/api-contract.md`
> - `docs/implementation-plans/shared-context/type-definitions.md`
> - `docs/implementation-plans/shared-context/coding-conventions.md`
> - `CLAUDE.md`
>
> Also read the reference implementation:
> - `qqq-frontend-material-dashboard/src/qqq/pages/processes/ProcessRun.tsx`
> - `qqq-frontend-material-dashboard/src/qqq/components/processes/` (all files)
> - `qqq-frontend-core/src/controllers/QController.ts` (process methods)
>
> IMPORTANT: You must reuse form components from Package 3:
> - Import `EntityForm` from `@/components/forms/EntityForm`
> - Import `DynamicFormField` from `@/components/forms/DynamicFormField`
> - Import form hooks and possible-values utilities
>
> Execute every step. After each: `pnpm tsc --noEmit`.
> When done: verify ALL acceptance criteria.
> Commit your work.
**Verification gate:**
```bash
pnpm tsc --noEmit && pnpm test && pnpm build
```
---
### Phase 4: Dashboard + Widgets (needs DataGrid from P2, forms from P3)
**Spawn Agent 5: Package 5 — Dashboard and Widgets**
Instruct the agent:
> Read these files before writing any code:
> - `docs/implementation-plans/05-dashboard-and-widgets.md` (your implementation plan)
> - `docs/implementation-plans/shared-context/api-contract.md`
> - `docs/implementation-plans/shared-context/type-definitions.md`
> - `docs/implementation-plans/shared-context/coding-conventions.md`
> - `CLAUDE.md`
>
> Also read the reference implementation:
> - `qqq-frontend-material-dashboard/src/qqq/components/widgets/` (all files — 60+)
> - `qqq-frontend-material-dashboard/src/qqq/pages/apps/AppHome.tsx`
>
> IMPORTANT: Reuse components from previous packages:
> - DataGrid from Package 2 (`@/components/query/DataGrid`) for RecordGridWidget
> - EntityForm from Package 3 (`@/components/forms/EntityForm`) for DynamicFormWidget
> - Cell renderers from Package 2 for field value display in widgets
>
> Execute every step. After each: `pnpm tsc --noEmit`.
> When done: verify ALL acceptance criteria.
> Commit your work.
**Verification gate:**
```bash
pnpm tsc --noEmit && pnpm test && pnpm build
```
---
### Phase 5: Polish + Parity (needs EVERYTHING)
**Spawn Agent 6: Package 6 — Polish, Parity, and Production Readiness**
Instruct the agent:
> Read these files before writing any code:
> - `docs/implementation-plans/06-polish-and-parity.md` (your implementation plan)
> - `docs/implementation-plans/shared-context/coding-conventions.md`
> - `CLAUDE.md`
>
> Also read the reference implementation for features unique to this package:
> - `qqq-frontend-material-dashboard/src/qqq/components/misc/CommandMenu.tsx`
> - `qqq-frontend-material-dashboard/src/qqq/components/audits/AuditBody.tsx`
> - `qqq-frontend-material-dashboard/src/qqq/pages/records/developer/`
> - `qqq-frontend-material-dashboard/src/qqq/components/scripts/ScriptViewer.tsx`
>
> This is the final package. ALL previous packages are complete.
> Your job is: responsive audit, accessibility fixes, command palette, keyboard shortcuts,
> audit trail, developer tools, Storybook, Playwright E2E, performance optimization,
> theme tokens, and customCss injection.
>
> Execute every step. After each: `pnpm tsc --noEmit`.
> When done: verify ALL acceptance criteria.
> Run: `pnpm build && pnpm test && pnpm lint`
> Commit your work.
---
## Final Verification (Master Agent Does This)
After all 6 agents complete:
1. **Full build check:**
```bash
pnpm tsc --noEmit # Zero errors
pnpm build # Production build succeeds
pnpm test # All unit tests pass
pnpm lint # Zero lint errors
```
2. **Review checklists:** Read and verify each file in `docs/implementation-plans/review-checklists/`:
- `review-01-scaffold.md` through `review-06-polish.md`
- Every checkbox should be satisfiable
3. **Cross-check report:** Read `docs/implementation-plans/cross-check-report.md` — all requirements should show ✅
4. **Integration smoke test:** If a backend is available, verify the app end-to-end:
- Login → sidebar loads → click table → query page loads → filter → paginate → click row → record view → edit → save → delete → navigate to process → run process → dashboard loads widgets
5. **Commit everything and push:**
```bash
git add -A
git commit -m "Complete QQQ Admin UI Modernization — all 6 work packages implemented"
git push origin main
```
---
## Error Recovery
If an agent fails mid-execution:
1. Read its error output
2. Fix the immediate issue (missing import, type error, etc.)
3. Resume the agent from where it left off (it should be able to continue from the last successful step)
4. If the failure is architectural (wrong approach), read the relevant section of the implementation plan and correct course
If agents produce conflicting code (e.g., both define the same component):
1. The implementation plans are designed to avoid this — each package owns specific files
2. If it happens, the later package's version wins (it has more context)
3. Verify the merged result compiles: `pnpm tsc --noEmit`
---
## Key Constraints for ALL Agents
- **pnpm only** (not npm, not yarn)
- **TypeScript strict** — no `any` in component props
- **Every interactive element** gets `data-qqq-id` attribute
- **All API calls** through `src/lib/api/` — never raw fetch/axios
- **Metadata-driven** — no hardcoded table/field/process names
- **Labels not names** — user-facing text uses `label` from metadata
- **Import order** — React/Next → external libs → types → lib → components
- **Error boundaries** at route level and widget level
- **Loading skeletons** for all async content
- **Empty states** with action buttons for all list/grid views
-338
View File
@@ -1,338 +0,0 @@
# Accessibility Reference — QQQ Frontend Next
**Standard:** WCAG 2.1 Level AA
**Testing tools:** axe-core (automated), Playwright E2E assertions, manual keyboard testing
---
## WCAG 2.1 AA Compliance Status
The sections below summarise what has been implemented and what remains outstanding,
based on the design review conducted 2026-03-01 (`docs/DESIGN-REVIEW-2026-03-01.md`).
---
## Key Implementations (Resolved Items)
### Skip-to-Content Link (D-L-1)
A visually hidden skip link is rendered as the first element in the dashboard layout
(`src/app/(dashboard)/layout.tsx`). It becomes visible on keyboard focus and links to
`#main-content`, allowing keyboard users to bypass the sidebar on every page navigation.
```tsx
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 ..."
data-qqq-id="skip-to-content"
>
Skip to main content
</a>
```
The target `<main id="main-content" data-qqq-id="main-content">` is present in the same layout.
---
### `aria-sort` on DataGrid Column Headers (D-Q-1)
`src/components/query/DataGrid.tsx` computes an `aria-sort` value for every `<th>` based on
the current TanStack Table sort state:
- Sorted ascending → `aria-sort="ascending"`
- Sorted descending → `aria-sort="descending"`
- Not sorted → `aria-sort="none"` (only on sortable columns; unsortable columns omit the attribute)
Screen readers announce the sort direction when the user focuses or activates a column header.
---
### Row Selection `aria-label` (D-Q-2)
Each row's selection checkbox carries a descriptive label:
```tsx
aria-label={`Select ${row.original.recordLabel ?? 'record'}`}
```
The "select all" header checkbox uses `aria-label="Select all rows on this page"`.
---
### Keyboard-Accessible Column Resize Handles (D-Q-3)
The resize handle on each column header responds to `ArrowLeft` and `ArrowRight` keyboard
events, adjusting the column width by ±10 px per keypress. The handler is implemented in
`handleResizeKeyDown` inside `DataGrid.tsx`:
```typescript
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
const delta = e.key === 'ArrowRight' ? 10 : -10
```
---
### `aria-live` Region for Fetch-in-Progress (D-Q-6)
The DataGrid wraps the loading progress bar and result count in an `aria-live="polite"` region
so that background re-fetches triggered by sort or filter changes are announced to screen readers
without interrupting the current reading flow.
---
### Focus Management in ProcessRun (D-P-1)
`src/components/process/ProcessRun.tsx` uses a `stepHeadingRef` and a `useEffect` keyed on the
active step name to move focus to the step heading whenever the wizard advances or retreats.
This satisfies WCAG 2.4.3 (Focus Order) by ensuring screen reader users are positioned at the
start of each new step:
```typescript
const stepHeadingRef = useRef<HTMLHeadingElement>(null)
useEffect(() => {
if (hasRenderedFirstStep.current) {
stepHeadingRef.current?.focus()
}
hasRenderedFirstStep.current = true
}, [activeStepName])
```
The target `<h2>` carries `tabIndex={-1}` and is referenced by `ref={stepHeadingRef}`.
---
### Screen-Reader Text for Trend Indicators (D-W-1)
`src/components/widgets/StatisticsWidget.tsx` renders a visually hidden `<span>` next to each
trend arrow icon so the direction is announced by screen readers:
```tsx
<span className="sr-only">{config.srLabel}</span>
// srLabel values: 'trending up,' | 'trending down,' | 'no change,'
```
The icon itself is `aria-hidden="true"` so it is not double-announced.
---
### Submit Button Disabled While Submitting (D-F-1)
`src/components/forms/EntityForm.tsx` reads `isSubmitting` from React Hook Form's `formState`
and applies it to all action buttons, preventing duplicate submissions:
```tsx
disabled={disabled || isSubmitting}
// Label also changes: isSubmitting ? 'Saving...' : saveButtonLabel
```
---
### Baseline Form Accessibility
All form inputs across `DynamicForm`, `EntityForm`, and individual field components have:
- `<label>` with `htmlFor` wired to the input `id`
- `aria-required={field.isRequired}`
- `aria-invalid={!!errors[field.name]}`
- `aria-describedby` pointing to the error message element
- Required asterisk marked `aria-hidden="true"` to avoid screen-reader repetition
- Help tooltips use Radix Tooltip, which is keyboard-accessible via `Tab` / `Escape`
---
### CSS Media Queries
#### `prefers-reduced-motion` (D-L-2 / D-T-1)
`src/styles/globals.css` collapses all animation and transition durations to near-zero
when the user's OS has reduced motion enabled. This resolves skeleton pulse, sidebar
slide-in, toast fade, and all other CSS transitions:
```css
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
#### `prefers-contrast: more` (D-T-2)
A high-contrast media block increases border weights and forces text colors to fully opaque
black, targeting users who have enabled "Increase Contrast" in their OS settings:
```css
@media (prefers-contrast: more) {
:root {
--color-border: #000000;
--color-input: #000000;
--color-muted-foreground: #000000;
}
*,
::before,
::after {
border-color: var(--color-border) !important;
}
:focus-visible {
outline: 3px solid #000000 !important;
outline-offset: 2px !important;
}
}
```
#### Mobile 16px Font-Size Fix (D-R-1)
iOS Safari auto-zooms any focused input whose `font-size` is below 16px. A mobile-scoped rule
prevents this without affecting desktop appearance:
```css
@media screen and (max-width: 767px) {
input,
select,
textarea {
font-size: 16px;
}
}
```
#### Dark-Mode Focus Ring Fix (D-T-5)
The default `color-mix`-based focus ring can disappear on dark backgrounds. A dark-mode
override forces the ring to a solid, fully opaque color:
```css
@media (prefers-color-scheme: dark) {
:focus-visible {
outline-color: var(--color-ring);
}
}
```
---
### Additional Resolved Items (from `What's Working Well`)
| Item | Implementation |
|------|----------------|
| `aria-required`, `aria-invalid`, `aria-describedby` on all form inputs | `DynamicFormField.tsx`, `EntityForm.tsx` |
| `role="status"` + `aria-busy="true"` on skeleton components | All skeleton components |
| `role="status"` + `aria-live="polite"` on `EmptyState` | `EmptyState.tsx` |
| Full keyboard nav in SearchDialog (ArrowUp/Down/Enter/Escape) | `SearchDialog.tsx` |
| Breadcrumbs: `aria-label="Breadcrumb"` + `aria-current="page"` | `Breadcrumbs.tsx` |
| `aria-expanded` on mobile drawer | `Sidebar.tsx` |
| DOMPurify sanitization on all user-controlled HTML output | `FieldValue.tsx`, `BlockWidget.tsx`, `ProcessHtmlStep.tsx` |
| Unsaved-changes guard: `beforeunload` + confirm modal | `EntityForm.tsx` |
| Focus rings on all interactive elements via `:focus-visible` | `globals.css` |
| Muted text darkened to `#5a6270` for WCAG AA contrast margin | `globals.css` |
---
## Automated Testing
### Running axe-core Scans
The helper in `tests/e2e/a11y-helpers.ts` wraps `@axe-core/playwright` and asserts zero
violations against the `wcag2a`, `wcag2aa`, and `wcag21aa` rule sets:
```typescript
import { checkA11y } from './a11y-helpers'
test('page is accessible', async ({ page }) => {
await page.goto('/app/person')
await waitForAppReady(page)
await checkA11y(page)
})
```
`checkA11y` runs on every navigation E2E test in `navigation.spec.ts`. To run all E2E tests
including accessibility assertions:
```bash
pnpm test:e2e
```
To run a single spec with axe output in the terminal:
```bash
pnpm test:e2e -- tests/e2e/navigation.spec.ts
```
### Known Suppressed Violations (with TODOs)
Three rule violations are currently disabled in `checkA11y` because the underlying issues
are not yet fixed. Each has a TODO comment in `a11y-helpers.ts`:
| Rule | Reason suppressed | TODO |
|------|-------------------|------|
| `color-contrast` | Amber banner background (#f59e0b) and indigo sidebar/button states yield contrast ratios just below 4.5:1 | Audit all Tailwind color pairs in Banner, Sidebar, and Button and replace with WCAG AA-compliant pairs |
| `aria-input-field-name` | Some form inputs rendered by `DynamicForm` may have broken `<label htmlFor>` associations when IDs are generated dynamically | Ensure every `<input>` has either a matching `htmlFor` or an `aria-label` |
| `scrollable-region-focusable` | The TanStack Table scrollable wrapper `<div>` has no `tabIndex`, blocking keyboard scrolling | Add `tabIndex={0}` to the scrollable DataGrid wrapper |
---
## Outstanding Work
The following items from the design review are **not yet resolved** and require future work.
### High Priority (WCAG 2.1 AA gaps)
| ID | Area | Description |
|----|------|-------------|
| D-V-1 | Record View | Edit/Delete buttons render regardless of user permissions; they fail with 403 on action instead of being hidden/disabled based on `editPermission`/`deletePermission`. |
| D-L-3 | Layout | Global search is hidden on mobile (`hidden md:block`); mobile users have no access to search. |
### Medium Priority (UX gaps)
| ID | Area | Description |
|----|------|-------------|
| D-L-5 | Layout | Mobile sidebar `aria-expanded` state may not be consistently set on the nav landmark. |
| D-Q-4 | FilterBuilder | Async combobox shows no loading indicator while possible-value options are fetching. |
| D-Q-5 | Pagination | "Go to page" input silently ignores out-of-range values with no user feedback. |
| D-Q-7 | DataGrid | Column visibility toggle changes are not announced via a `role="status"` region. |
| D-P-2 | Process | Completed step indicators in the wizard are not keyboard-navigable for back-navigation. |
| D-P-3 | Process | No timeout message if backend polling runs beyond ~60 seconds. |
| D-P-4 | Process | Validation review result list has no severity filter or field-name search. |
| D-P-5 | Process | File upload step shows no byte-level progress during multipart upload. |
| D-W-2 | Widgets | Widget API call failures show blank space with no error boundary or fallback UI. |
| D-FB-1 | Feedback | Keyboard Shortcuts dialog is not discoverable (no visible `?` hint in the header). |
| D-FB-2 | Feedback | Toasts have no dismiss button; users must wait for auto-timeout. |
| D-FB-3 | Feedback | CommandMenu icon buttons may be below the 44px minimum touch target size. |
| D-R-2 | Responsive | Multiple interactive elements (pagination controls, density buttons) below 44px touch target. |
### Items Deferred Pending Backend Support
| ID | Area | Description |
|----|------|-------------|
| D-V-6 | Record View | "Last modified by / at" audit footer requires backend to expose this data on the record endpoint. |
| D-T-3 | Theme | Dual theming systems (`--qqq-primary-color` and `--color-primary`) need consolidation; depends on coordinating with backend-supplied theme values. |
---
## Checklist for New Component Authors
When adding a new interactive component, verify the following before opening a pull request:
- [ ] Every `<input>`, `<select>`, and `<textarea>` has a `<label htmlFor>` that matches its `id`, or an `aria-label` / `aria-labelledby` attribute.
- [ ] Required inputs have `aria-required={true}`.
- [ ] Invalid inputs have `aria-invalid={true}` and an `aria-describedby` pointing to the error message element.
- [ ] All icon-only buttons have a descriptive `aria-label` (e.g. `aria-label="Close dialog"`).
- [ ] Icon elements that are purely decorative are marked `aria-hidden="true"`.
- [ ] Modals/dialogs trap focus while open and restore it to the triggering element on close (use Radix Dialog for this automatically).
- [ ] All interactive elements are reachable and operable via `Tab` / `Shift+Tab` / `Enter` / `Space` / `Escape`.
- [ ] Interactive elements have a visible `:focus-visible` ring (do not add `outline: none` without a replacement).
- [ ] Any element that announces dynamic state changes uses `aria-live="polite"` (or `role="status"`).
- [ ] Touch targets are at least 44x44px (`min-h-[44px] min-w-[44px]` or equivalent padding).
- [ ] Color is not the only means of conveying information (pair color with text or icon).
- [ ] Text color meets a minimum 4.5:1 contrast ratio against its background for normal-weight text at normal size.
- [ ] The component does not break at 320px viewport width.
- [ ] The component respects `prefers-reduced-motion` (animations driven by CSS transitions inherit the global rule in `globals.css`; JS-driven animations need a manual check).
- [ ] Any rendered HTML that originates from user input or the backend is sanitized with DOMPurify before being set via `dangerouslySetInnerHTML`.
- [ ] Run `pnpm test:e2e` and confirm `checkA11y` passes for any page that includes the new component.
-298
View File
@@ -1,298 +0,0 @@
# API Error Handling Guide
This document describes how API errors flow through the QQQ Frontend Next application and what, if anything, individual components need to do about them.
The short answer is: **most errors require no per-call handling**. The global error handler in the query client toasts an appropriate message automatically, and the 401 interceptor in the Axios client redirects to login. Individual components only need to add their own handling when the generic behavior is insufficient for a specific UX requirement.
---
## Architecture Overview
There are two independent error interception layers:
1. **Axios response interceptor** (`src/lib/api/client.ts`) — fires synchronously on every HTTP response before the promise settles. Handles 401 (unauthorized) only.
2. **TanStack Query global handlers** (`src/lib/query-client.ts`) — `QueryCache.onError` for reads, `MutationCache.onError` for writes. Fire after the interceptor. Handle all other errors by toasting a message.
```
API response
│
▼
Axios interceptor
│ 401 → call unauthorizedCallback() → redirect to login
│ all others → pass through as rejected promise
▼
TanStack Query cache (onError)
│ 401 → silently ignored (already handled)
│ 403 → toast the server's message, or "Permission denied"
│ 404 → toast "Resource not found"
│ 5xx → toast "Server error — retrying…" / "Server error — please try again"
│ other status → toast the server's message, or "Request failed (HTTP 409)"
│ no response → toast "No response from the server" / "The server took too long to respond"
│ every toast names the request, and a repeat of the same failure updates it
▼
Component (optional per-query onError)
│ Only needed when generic toast is wrong for the specific UX
```
---
## The 401 Interceptor
**Location:** `src/lib/api/client.ts`
The singleton `APIClient` registers a response interceptor that fires on every request:
```typescript
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401 && this.unauthorizedCallback) {
this.unauthorizedCallback()
}
return Promise.reject(error) // always re-reject so TanStack Query also sees it
}
)
```
The `unauthorizedCallback` is wired to the auth provider's logout/redirect function during initialization. When a 401 arrives:
1. The callback fires → clears auth state → redirects to the login page.
2. The error is still re-rejected, so TanStack Query's `onError` also fires.
3. The global handler in `query-client.ts` explicitly silences 401s to avoid a redundant toast on top of the redirect.
**Implication for components:** Never handle 401 in a component. The interceptor + auth provider handle it globally. Writing per-component 401 handling will race with the redirect and confuse users.
---
## The Global Query Error Handler
**Location:** `src/lib/query-client.ts` — `handleQueryError` function
It is registered on both `QueryCache.onError` and `MutationCache.onError`. A query or mutation whose
component renders its own error opts out with `meta: HANDLES_OWN_ERRORS`.
| Failure | Banner text |
|---|---|
| Cancelled request (an axios cancel or an aborted `fetch`), 401 | none |
| 403 | the server's message (of a JSON body), or `Permission denied` |
| 404 | `Resource not found` |
| 5xx | `Server error — retrying…` (query), `Server error — please try again` (mutation) |
| Any other status (400, 409, 422, …) | the server's message (`userFacingError`, `error` or `message` of a JSON body), or `Request failed (HTTP 409)` |
| No response (connection failed) | `No response from the server` |
| No response (the 30 s timeout) | `The server took too long to respond` |
| An error the application raised (`throw new Error('Invalid audit response')`) | that sentence |
| A runtime error (`TypeError`, a schema error, …) | `The page hit an unexpected error (TypeError)` |
Under the text, every banner names what failed: the request as `POST /qqq/v1/table/order/query (HTTP 409)`,
or `While loading records` (the query key's namespace in plain words: `possibleValues` reads
`possible values`) for a query that failed outside a request. The
request is written without its query string, and every path segment that is not a plain word (a record
id, a UUID, an encoded value) is shown as `{id}`. Nothing else of the request or response is shown.
**One banner per failure.** The banner's sonner `id` is its text plus the request, so a failure that
repeats (a polling query that fails on every attempt) updates the banner already on screen instead of
adding one. A different failure gets its own banner. A lost connection is one cause whichever request
met it, so it has a single banner, which names the request that failed most recently. Without the id
every failed poll added a banner, and sonner pauses a toast's timer while the tab is hidden or the
pointer rests on the stack, so the banners could pile up.
**Console.** Each banner is also logged once with `console.warn('[API] Request failed:', { context,
status, code, name, request, area })`: the HTTP status, the axios error code (`ERR_NETWORK`,
`ECONNABORTED`, `ERR_BAD_REQUEST`), the error's name, the request as above and the query namespace.
It never logs a message, body, header or query string, because those can carry record data.
**Implication for components:** For most queries and mutations, you do not need `onError` at all. The global handler covers toasting. Add per-query handling only when you need to do something the global handler cannot — such as mapping a 400 error's field messages back to form fields.
---
## Extracting the Status Code
**Location:** `src/lib/utils/error-utils.ts`
```typescript
import { getErrorStatusCode } from '@/lib/utils/error-utils'
const status = getErrorStatusCode(error)
```
`getErrorStatusCode` handles two shapes:
- `AxiosError` — reads `error.response?.status`
- Plain objects with a numeric `status` property (e.g. fetch-based wrappers)
Returns `undefined` for anything else (non-object errors, null, network failures with no response).
---
## Status-by-Status Guide
### 400 — Validation Error
The global handler toasts the server's message for a 400 (or `Request failed (HTTP 400)` when it sent none). 400s typically carry field-level validation messages that belong beside individual form fields, so a form handles 400 explicitly, and sets `meta: HANDLES_OWN_ERRORS` on the mutation when the toast would repeat what the form shows.
```typescript
import { getErrorStatusCode } from '@/lib/utils/error-utils'
const mutation = useMutation({
mutationFn: (data: FieldValues) => saveRecord('order', data),
onError: (error) => {
if (getErrorStatusCode(error) === 400) {
// Map backend validation messages to React Hook Form fields
const fieldErrors = extractFieldErrors(error) // your helper
for (const [field, message] of Object.entries(fieldErrors)) {
form.setError(field, { type: 'server', message })
}
}
// Non-400 errors are already toasted by the global handler
},
})
```
If a 400 arrives outside of a form context, add a toast yourself:
```typescript
onError: (error) => {
if (getErrorStatusCode(error) === 400) {
toast.error('Invalid request — please check your input')
}
}
```
### 403 — Permission Denied
The global handler toasts a generic permission-denied message. If the 403 should instead silently hide an action (e.g. a button that should not appear for users without permission), suppress the default behavior by checking permissions from metadata before rendering rather than relying on the API to reject:
```typescript
// Preferred: hide the action before the user tries it
{tableMetaData.capabilities.includes('insert') && (
<Button onClick={openCreateModal}>New Record</Button>
)}
```
If you need custom handling on a 403, use `onError` and call `toast` yourself — or do nothing if the generic toast is acceptable.
### 404 — Not Found
The global handler toasts "Resource not found". For record-view pages where 404 means the record was deleted, render an empty state instead:
```typescript
const { data: record, error } = useQuery({
queryKey: queryKeys.tableRecord(tableName, recordId),
queryFn: () => getRecord(tableName, recordId),
})
if (getErrorStatusCode(error) === 404) {
return <EmptyState message="This record no longer exists" />
}
```
The global handler will also toast, which is acceptable — the empty state just provides additional UI context.
### 5xx — Server Error
The global handler toasts "Server error — retrying…" for queries and "Server error — please try again" for mutations. Queries are automatically retried up to 3 times with exponential back-off (1 s → 2 s → 4 s, max 30 s) before the error surfaces.
No additional per-component handling is needed for 5xx unless you want a custom retry UI or a structured error state for a specific page.
### Network Failure (no response)
Treated as non-4xx by the retry logic — queries retry up to 3 times. After all retries are exhausted the global handler toasts "No response from the server" (or "The server took too long to respond" for the 30 s timeout) with the request that failed. No per-component handling needed for the common case.
---
## When to Add Per-Query Error Handling
Add `onError` to a specific `useQuery` or `useMutation` call when:
| Scenario | What to do |
|---|---|
| 400 with field-level validation messages | Map errors to form fields with `form.setError()` |
| 403 should render empty state instead of toast | Catch 403, render empty UI, call `toast` yourself if the global one also fires |
| 404 should render a structured empty state page | Check `getErrorStatusCode(error) === 404` and render `<EmptyState>` |
| Error should navigate away (e.g. deleted parent record) | `useRouter().push('/...')` in `onError` |
| The generic toast message is wrong for this specific action | Supply a custom `onError` that calls `toast.error(customMessage)` |
For all other cases, rely on the global handler and write no error-handling code in the component.
---
## Retry Behavior
The `smartRetry` function in `src/lib/query-client.ts` controls when TanStack Query retries:
```typescript
function smartRetry(failureCount: number, error: unknown): boolean {
const status = getErrorStatusCode(error)
// Never retry client errors — they are deterministic
if (status !== undefined && status >= 400 && status < 500) return false
// Retry server errors and network failures up to 3 times
return failureCount < 3
}
```
Back-off schedule: 1 s → 2 s → 4 s, capped at 30 s.
4xx errors are never retried. There is no per-component way to opt into retrying 4xx errors — that would be intentional since a 400 with bad field data will not resolve itself on retry.
---
## Complete Example — Form with 400 Handling
```typescript
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { saveRecord } from '@/lib/api/tables'
import { queryKeys } from '@/lib/query-client'
import { getErrorStatusCode } from '@/lib/utils/error-utils'
export function EditOrderForm({ orderId, schema, defaultValues }: Props) {
const queryClient = useQueryClient()
const form = useForm({ resolver: zodResolver(schema), defaultValues })
const mutation = useMutation({
mutationFn: (data: FieldValues) => saveRecord('order', orderId, data),
onSuccess: () => {
// Invalidate so the record view refreshes
queryClient.invalidateQueries({ queryKey: queryKeys.tableRecord('order', orderId) })
toast.success('Order saved')
},
onError: (error) => {
if (getErrorStatusCode(error) === 400) {
// Map field errors from the server response body to form fields.
// The global handler toasts the server's message for a 400; the form
// places each field's message beside it.
const body = (error as AxiosError<{ errors: Record<string, string> }>).response?.data
if (body?.errors) {
for (const [field, message] of Object.entries(body.errors)) {
form.setError(field, { type: 'server', message })
}
} else {
toast.error('Please correct the highlighted fields')
}
return
}
// 401, 403, 404, 5xx, network — already toasted by the global handler
},
})
return (
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))}>
{/* field components */}
</form>
)
}
```
---
## Quick Reference
| Layer | File | Handles |
|---|---|---|
| Axios interceptor | `src/lib/api/client.ts` | 401 → redirect to login |
| Global query handler | `src/lib/query-client.ts` | Every status but 401, no response, page errors → one toast per failure, naming the request |
| Per-component `onError` | Your component | 400 field errors, custom empty states, navigation |
| `getErrorStatusCode` | `src/lib/utils/error-utils.ts` | Normalize `AxiosError` and plain-object errors to HTTP status code |
-316
View File
@@ -1,316 +0,0 @@
# Code Quality Review — QQQ Frontend Next
**Date:** 2026-02-27
**Scope:** DRY, KISS, component design, hooks, TypeScript quality, naming, structure
**Note:** Security/runtime bugs are documented separately in `CODE-REVIEW-2026-02-27.md`
---
## Summary
The codebase demonstrates **strong architectural foundations** with excellent separation of concerns and metadata-driven rendering patterns. However, there are **significant code quality issues** across multiple dimensions: substantial DRY violations in filter/combobox logic, an overengineered god-hook, inconsistent error handling approaches, and TypeScript practices that miss opportunities for stronger typing. The `use-record-query` hook is an anti-pattern — a 540-line hook mixing data fetching, URL synchronization, localStorage persistence, and UI state. FilterBuilder has two combobox implementations that are ~95% identical. Several utilities and constants are defined inline or scattered across files. While the component structure is clean and the metadata-driven approach is correct, the implementation needs targeted refactoring to reduce coupling, eliminate duplication, and simplify abstractions.
---
## CRITICAL Quality Issues (blocks maintainability)
### CQ-CRIT-1: `use-record-query` — God Hook Anti-Pattern (540 lines)
**File:** `src/lib/hooks/use-record-query.ts`
This hook violates SRP catastrophically. It manages:
- Pagination state (pageNum, pageSize)
- Filter state (userFilter, quickSearchTerm, filterMode)
- Column configuration (columnVisibility, columnOrder, columnWidths)
- Row selection state
- UI panel toggles (filterPanelOpen, columnConfigOpen)
- localStorage persistence (5 separate useEffect blocks)
- URL params synchronization (1 useEffect block)
- TanStack Query integration (2 queries)
- Saved views (saveView, loadView, deleteView callbacks)
- 18+ action dispatchers
The hook returns 40+ properties. Components must destructure dozens of items they don't need. The reducer pattern for 24+ action types is overkill for this state complexity.
**Why it's Critical:**
- Testing requires mocking localStorage, URL manipulation, TanStack Query, and multiple state branches simultaneously
- Any change to one concern risks breaking another
- Components using this hook are tightly coupled to its entire API shape
- The localStorage sync fires 5 separate effects independently
**Recommended Fix:**
Split into 3-4 focused hooks:
- `useRecordPagination()` — pageNum, pageSize, totalPages, setPage, setPageSize
- `useRecordFilter()` — userFilter, quickSearchTerm, setFilter, resetFilter
- `useRecordSelection()` — rowSelection, setRowSelection, clearSelection
- `useRecordUIState()` — columnConfigOpen, filterPanelOpen, toggles
Let RecordQuery coordinate these together.
---
### CQ-CRIT-2: FilterBuilder — Two Comboboxes That Are 95% Identical
**File:** `src/components/query/FilterBuilder.tsx`, lines 508-927
`PossibleValueSingleSelect` (lines 508-698) and `PossibleValueMultiSelect` (lines 713-927) are nearly identical. Both:
- Fetch options via `fetchTablePossibleValues()`
- Debounce search input (300ms) — copied at lines 557-563 and 771-777
- Manage AbortController for stale request cancellation — copied at lines 585-590 and 799-804
- Implement click-outside-to-close
- Render search input, loading state, option list
Differences: only selection model (single vs. multi) and tag display.
**Why it's Critical:**
- Bug fix in one requires manual sync to the other
- 400+ lines of duplicated code
- Missing memoization: callbacks re-created on every render
**Recommended Fix:**
Extract a `useAsyncCombobox` hook:
```typescript
function useAsyncCombobox(tableName: string, fieldName: string) {
// Shared debounce, abort, cleanup logic
return { options, isLoading, debouncedFetch, abortRef }
}
```
Then SingleSelect and MultiSelect become thin wrappers managing only the selection model.
---
### CQ-CRIT-3: `zod-from-metadata` — Repeated Required/Optional Pattern x10
**File:** `src/lib/utils/zod-from-metadata.ts`, lines 11-114
STRING, TEXT, HTML, and PASSWORD cases (lines 19-60, 99-110) all repeat:
```typescript
let schema = z.string()
if (isRequired) schema = schema.min(1, `${label} is required`)
if (maxLength) schema = schema.max(maxLength, ...)
return isRequired ? schema : schema.optional()
```
The required/optional wrapping pattern is written identically across all 10+ field types.
**Recommended Fix:**
```typescript
function buildStringSchema(isRequired: boolean, maxLength?: number, label?: string) {
let schema = z.string()
if (isRequired) schema = schema.min(1, `${label} is required`)
if (maxLength) schema = schema.max(maxLength, `Must be ${maxLength} characters or fewer`)
return isRequired ? schema : schema.optional()
}
case 'STRING':
case 'TEXT':
case 'HTML':
case 'PASSWORD':
return buildStringSchema(isRequired, field.maxLength, field.label)
```
---
### CQ-CRIT-4: Inconsistent Error Status Detection Across Components
**Files:**
- `src/components/records/RecordView.tsx` lines 68-83 — custom `getErrorStatusCode()`, handles 403/404/500
- `src/components/query/RecordQuery.tsx` lines 493-512 — generic "Failed to load records" for all errors
Error handling is implemented differently in the two main page components. If the API changes, fixes are needed in multiple places.
**Recommended Fix:**
```typescript
// src/lib/utils/error-utils.ts
export function getErrorStatusCode(error: unknown): number | undefined { ... }
export function getErrorMessage(error: unknown, context: string): string { ... }
```
Use this in both RecordView and RecordQuery.
---
## HIGH Quality Issues (significant tech debt)
### CQ-HIGH-1: RecordQuery — Too Many Responsibilities (586 lines)
**File:** `src/components/query/RecordQuery.tsx`
This component manages: quick search debounce, view mode toggle, mobile filter sheet state, density selector, and renders: toolbar (150+ lines inline), filter panel (desktop + mobile variants), bulk action bar, error/empty states, DataGrid, RecordCardView, and process launcher coordination.
**Extractable subcomponents:**
- `<RecordQueryToolbar />` — all toolbar state and rendering
- `<DensitySelector />` — the dropdown (lines 291-339)
- `<ViewModeToggle />` — button group (lines 258-288)
- `<QuickSearchInput />` — search field with debounce
---
### CQ-HIGH-2: FilterBuilder — No Memoization on Recursive Components
**File:** `src/components/query/FilterBuilder.tsx`
The recursive chain FilterBuilder → FilterGroup → CriteriaRow → FilterValueInput doesn't memoize callbacks or components. Every filter state change re-renders all sibling rows, and PossibleValueSingleSelect's useEffect (line 566) re-fires unnecessarily — potentially resetting combobox search input during user input.
**Fix:**
```typescript
export const CriteriaRow = React.memo(function CriteriaRow({ ... }) { ... })
export const FilterValueInput = React.memo(function FilterValueInput({ ... }) { ... })
```
---
### CQ-HIGH-3: Field Adornments Typed as `Record<string, unknown>`
**File:** `src/components/records/FieldValue.tsx`, lines 65-66, 119-120
```typescript
const href = (linkAdornment?.values?.['linkURL'] ?? String(value)) as string
const colorMap = (chipAdornment?.values?.['colorMap'] ?? {}) as Record<string, string>
```
These `as` casts are necessary because adornment `values` is `Record<string, unknown>`. The type information exists in the backend but isn't modeled.
**Recommended Fix:** Discriminated union types for adornments:
```typescript
type LinkAdornment = { type: 'LINK'; values: { linkURL: string } }
type ChipAdornment = { type: 'CHIP'; values: { colorMap: Record<string, string> } }
export type FieldAdornment = LinkAdornment | ChipAdornment | IconAdornment | ...
```
Eliminates all `as` casts in FieldValue.
---
### CQ-HIGH-4: Magic Numbers and Strings Scattered Throughout
- `use-record-query.ts:246` — `[10, 25, 50, 100].includes(n)` — hardcoded page sizes
- `RecordQuery.tsx:75` — `400` ms debounce
- `FilterBuilder.tsx:560` — `300` ms debounce
- `DataGrid.tsx:41-51` — density class maps inline
- `FieldValue.tsx:1024` — bytes-per-KB hardcoded
**Fix:**
```typescript
// src/lib/constants.ts
export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const
export const SEARCH_DEBOUNCE_MS = 400
export const COMBOBOX_DEBOUNCE_MS = 300
```
---
### CQ-HIGH-5: `use-process` — Stale Closure via Manual `stateRef`
**File:** `src/lib/hooks/use-process.ts`, lines 105-128
```typescript
const stateRef = useRef(state)
stateRef.current = state
// later: stateRef.current.stepValues
```
Manually syncing a ref to shadow state is a sign that the polling subscription (lines 249-268) is too coupled to mutable state. This pattern is fragile and indicates the state management design needs simplification.
---
### CQ-HIGH-6: Dead Toolbar Button
**File:** `src/components/query/RecordQuery.tsx`, lines 394-401
A "Settings / more" button has an aria-label but no onClick handler — it does nothing. Either wire it up or remove it.
---
## MEDIUM Quality Issues
### CQ-MED-1: `use-record-query` — `effectiveFilter` Depends on Unrelated State
**File:** `src/lib/hooks/use-record-query.ts`, lines 317-332
`effectiveFilter` is recomputed when `state.columnVisibility` changes, but column visibility has nothing to do with filters. The dependency array is over-inclusive.
---
### CQ-MED-2: Boolean Operator Type is Raw String Cast
**File:** `src/components/query/FilterBuilder.tsx`, lines 196-198
```typescript
onChange({ ...filter, booleanOperator: e.target.value as 'AND' | 'OR' })
```
Should use a const assertion or enum to avoid typo risk:
```typescript
const BOOLEAN_OPERATORS = ['AND', 'OR'] as const
type BooleanOperator = typeof BOOLEAN_OPERATORS[number]
```
---
### CQ-MED-3: FieldValue — Repeated URL/Email Pattern Matching
**File:** `src/components/records/FieldValue.tsx`, lines 294, 339, 358
Three separate URL/email regex checks scattered across the file. Extract to:
```typescript
// src/lib/utils/string-utils.ts
export const isUrl = (s: string) => /^https?:\/\//i.test(s) || s.startsWith('/')
export const isEmail = (s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)
```
---
### CQ-MED-4: RecordView — Redundant Section Fallback Logic
**File:** `src/components/records/RecordView.tsx`, lines 234-289
```typescript
const primarySections = visibleSections.filter(s => !s.tier || s.tier === 'T1' || s.tier === 'basic')
// ...
const t1Sections = primarySections.length > 0 ? primarySections : visibleSections
```
The fallback `? primarySections : visibleSections` is redundant if all sections without explicit tiers are already included in `primarySections`. Simplify or add a comment explaining when this fallback fires.
---
### CQ-MED-5: Missing Error Boundaries Below Route Level
Only `src/app/error.tsx` provides an error boundary. Forms and record views should have their own boundaries to prevent full-page crashes from single component errors.
---
### CQ-MED-6: `FilterBuilder` — `subs` Variable Name
**File:** `src/components/query/FilterBuilder.tsx`, line 178
```typescript
const subs = filter.subFilters ?? []
```
Should be `subFilters` for clarity.
---
## LOW / Style Issues
- **`data-qqq-id` inconsistency** — Mix of `grid-select-row-${row.index}` (underscores), `filter-field-${depth}-${index}` (hyphens). Standardize to kebab-case throughout.
- **Abbreviations:** `initVals` → `initialValues` (`use-record-query.ts:257`), `ej` → `exposedJoin` (`use-record-query.ts:348`), `pk` → `primaryKey` (DataGrid, RecordView), `subs` → `subFilters`
- **Import ordering:** `FilterBuilder.tsx` imports lucide-react after custom hooks. Project convention is: React → External → Types → Lib → Components.
- **Missing JSDoc:** `buildQuickFilterFromState()` (use-record-query.ts), `getErrorStatusCode()` (RecordView.tsx), `zodFieldFromMetadata()` (zod-from-metadata.ts) — all complex enough to warrant a one-liner doc comment.
- **Unnecessary `useMemo`:** `joins` calculation in `use-record-query.ts:345-354` is a simple object map — too cheap to justify memoization overhead.
- **Unused import:** `SelectField` in `DynamicFormField.tsx` (identified in prior review).
- **`window.innerWidth` in callback** — `RecordQuery.tsx:127-134` — not reactive to resize events.
---
## Positive Patterns Worth Preserving
1. **Metadata-Driven Rendering** — entire UI derived from backend metadata, zero hardcoded field/table names.
2. **Query Key Factory Pattern** — `queryKeys.tableRecords(tableName)` prevents string-based cache bugs.
3. **TanStack Query Usage** — proper staleTime, placeholderData, manual pagination, invalidation. No over-fetching.
4. **Typed API Layer** — all `src/lib/api/` functions properly typed, good use of discriminated unions for responses.
5. **Filter Serialization** — `serializeFilter()` / `deserializeFilter()` correctly handle shareable URLs.
6. **Debouncing** — SearchInput and FilterBuilder correctly debounce and clean up.
7. **Tailwind + CSS Custom Properties** — consistent theming without inline styles.
8. **Accessibility Fundamentals** — ARIA labels, roles, keyboard handling are present throughout.
---
## Recommended Fix Priority
| Priority | Item | Effort |
|----------|------|--------|
| 1 | CQ-CRIT-2: Extract `useAsyncCombobox` hook (FilterBuilder) | Medium |
| 2 | CQ-CRIT-3: `buildStringSchema()` helper in zod-from-metadata | Small |
| 3 | CQ-CRIT-4: Centralize `getErrorStatusCode()` utility | Small |
| 4 | CQ-HIGH-3: Discriminated union types for adornments | Medium |
| 5 | CQ-HIGH-4: Extract constants to `src/lib/constants.ts` | Small |
| 6 | CQ-HIGH-6: Remove or wire dead toolbar button | Trivial |
| 7 | CQ-HIGH-2: `React.memo` on FilterBuilder subcomponents | Small |
| 8 | CQ-MED-3: Extract URL/email utils | Small |
| 9 | CQ-CRIT-1: Split `use-record-query` into focused hooks | Large |
| 10 | CQ-HIGH-1: Extract RecordQuery toolbar subcomponents | Medium |
-390
View File
@@ -1,390 +0,0 @@
# QQQ Frontend Next — Code Review
**Date:** 2026-02-27
**Reviewer:** Claude (automated)
**Scope:** Full `src/` codebase audit
**Commit:** df8e2e6
---
## CRITICAL (7 findings)
---
### CRIT-1: XSS — unsanitized `dangerouslySetInnerHTML` in `FieldValue`
**File:** `src/components/records/FieldValue.tsx:136–143`
Raw backend HTML value injected into DOM with zero sanitization:
```tsx
dangerouslySetInnerHTML={{ __html: String(value) }}
```
**Fix:** Use DOMPurify:
```tsx
import DOMPurify from 'dompurify'
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(String(value)) }}
```
---
### CRIT-2: XSS — unsanitized `dangerouslySetInnerHTML` in `BlockWidget` (two locations)
**File:** `src/components/widgets/BlockWidget.tsx:40–48, 278–285`
Both the legacy `data.html` path and the `case 'html'` block renderer pass backend HTML directly to the DOM.
**Fix:** Apply `DOMPurify.sanitize()` to both HTML rendering paths.
---
### CRIT-3: XSS — regex script-stripping in `ProcessHtmlStep` is not real sanitization
**File:** `src/components/process/ProcessHtmlStep.tsx:14–17, 100`
```ts
// TODO: Replace with DOMPurify for full sanitization
function stripScripts(html: string): string {
return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
}
```
Bypassed by: `<img onerror="...">`, `<svg onload="...">`, `<a href="javascript:...">`, and dozens of other vectors.
**Fix:** Replace `stripScripts` with `DOMPurify.sanitize()`.
---
### CRIT-4: Auth bypass — `handleOAuthCallback` sets `isAuthenticated=true` without token exchange
**File:** `src/lib/auth/auth-provider.tsx:164–176`
```tsx
async function handleOAuthCallback(code: string, state: string) {
// For now, just mark as authenticated
setIsAuthenticated(true)
}
```
`code` and `state` params are accepted but unused. No CSRF state validation, no token exchange, no session creation.
**Fix:** Implement PKCE: validate `state` against `sessionStorage`, exchange `code`+`code_verifier` for tokens via backend `/manageSession`, then set `isAuthenticated` only on success.
---
### CRIT-5: Auth bypass — Auth0/OAuth2 sessions authenticate without server validation
**File:** `src/lib/auth/auth-provider.tsx:123–138`
Both `setupAuth0Session` and `setupOAuth2Session` call no protected endpoint. Any app configured with AUTH_0 or OAUTH2 shows as authenticated for any page load. Default user is hardcoded: `{ name: 'User', email: 'user@example.com' }`.
**Fix:** Call a protected endpoint (e.g., `GET /session`) to validate existing session cookie. Redirect to provider if 401.
---
### CRIT-6: Auth state corruption — 401 interceptor doesn't clear auth metadata cache
**File:** `src/lib/auth/auth-provider.tsx:65–70`
The 401 interceptor redirects to login but does NOT call `clearAuthMetadataCache()`, leaving stale auth metadata. On next page load the cached auth type governs initialization without a fresh server check.
**Fix:** Call `clearAuthMetadataCache()` inside the 401 callback before redirect.
---
### CRIT-7: Silent export failure — `ExportButton` swallows errors with no user feedback
**File:** `src/components/query/ExportButton.tsx:88–89`
```tsx
} catch (err) {
console.error('[ExportButton] Export failed:', err)
}
```
Users believe export succeeded when it silently failed.
**Fix:** Add `toast.error('Export failed. Please try again.')` in the catch block.
---
## HIGH (8 findings)
---
### HIGH-1: Stale closures — missing deps in URL sync + column persistence effects
**File:** `src/lib/hooks/use-record-query.ts:280–311`
Four `useEffect` hooks suppress `react-hooks/exhaustive-deps`. The URL-sync effect is missing `router` and `pathname` — stale closures can fire URL updates on the wrong route in concurrent mode.
**Fix:** Add `router` and `pathname` to the URL-sync effect deps. Use `useRef` for stable callback references if needed.
---
### HIGH-2: Performance — `JSON.stringify` key comparison on every query cache event in `useProcess`
**File:** `src/lib/hooks/use-process.ts:251–268`
```ts
if (JSON.stringify(key) !== JSON.stringify(expectedKey)) return
```
This serializes two arrays on every cache event during rapid polling intervals.
**Fix:** Use `queryClient.getQueryData(expectedKey)` or shallow key comparison utility.
---
### HIGH-3: Logic bug — `processUUID` unsafe cast falls back to empty string
**File:** `src/lib/hooks/use-process.ts:274–278`
```ts
const pUUID = (response as QJobResponse & { processUUID?: string }).processUUID ?? ''
```
If `processUUID` is absent, all subsequent step calls use `''` as the UUID, producing 404 API calls.
**Fix:** Add `processUUID` to `QJobResponse` type. Validate it is non-empty before proceeding.
---
### HIGH-4: Logic bug — type guard fallthrough on unexpected `QJobResponse` shapes
**File:** `src/lib/hooks/use-process.ts:76–82`
A response with both `error` and `values` fields, or with both `jobUUID` and `values`, falls through all type guards silently. Process state freezes indefinitely.
**Fix:** Use a discriminated union with an explicit `type` field. Add a default catch-all error case.
---
### HIGH-5: Stale auth — metadata cached globally without API base URL scoping
**File:** `src/lib/api/auth.ts:29–37`
Auth metadata cached for 1 hour under a single key `qqqAuthMetadata` regardless of API base URL. Backend auth type changes are ignored for up to 1 hour. Multi-instance deployments cross-contaminate caches.
**Fix:** Include API base URL in cache key. Consider shorter TTL (5–10 min) or TanStack Query instead.
---
### HIGH-6: Auth bypass — `globalSearch` silently swallows 401/403 errors
**File:** `src/lib/api/tables.ts:132–136`
```ts
} catch {
// If /search endpoint is not available (404), return empty results
return []
}
```
Catches all errors including 401 (session expired). A search 401 bypasses the global logout interceptor.
**Fix:**
```ts
} catch (err) {
if (isAxiosError(err) && err.response?.status === 404) return []
throw err
}
```
---
### HIGH-7: Wrong record selection — `selectedRecordIds` computed from array index, not stable PK
**File:** `src/lib/hooks/use-record-query.ts:411–421`
`rowSelection` is keyed by array index but records can reorder after sort/filter/pagination. Index 5 after a filter may point to a completely different record than before.
**Fix:** Key `rowSelection` by primary key string (same as `getRowId` in DataGrid). Update the `selectedRecordIds` computation to use PK-based lookup.
---
### HIGH-8: Dead retry button — `setPage(rq.pageNum)` is a no-op
**File:** `src/components/query/RecordQuery.tsx:503–508`
The error-state retry button dispatches `SET_PAGE` with the current page number — TanStack Query sees the same query key and does not re-fetch.
**Fix:** Use `queryClient.invalidateQueries({ queryKey: queryKeys.tableRecords(tableName) })`.
---
## MEDIUM (14 findings)
---
### MED-1: `AuthProvider` effect suppresses `onAuthError` dep without documentation
**File:** `src/lib/auth/auth-provider.tsx:121`
**Fix:** Document intent: `// intentional: runs once on mount — auth initialization is not repeatable`
---
### MED-2: CSS injection — raw `customCss` injected into `<style>` tag without validation
**File:** `src/app/(dashboard)/layout.tsx:108–119`
Arbitrary CSS from the backend can overlay phishing forms, exfiltrate keystrokes via CSS selectors, or perform UI redressing.
**Fix:** Use a CSS sanitizer or restrict to CSS custom property overrides only.
---
### MED-3: CSS injection — `accentColor` set on CSS custom properties without format validation
**File:** `src/app/(dashboard)/layout.tsx:71–89`
`accentColor: 'red; --qqq-primary: url(https://evil.com)'` would be applied as-is.
**Fix:** Validate `accentColor` matches `/^#[0-9a-fA-F]{3,8}$|^rgb|^hsl/` before applying.
---
### MED-4: Stale `initialValue` reference in `useLocalStorage.removeValue`
**File:** `src/lib/hooks/use-local-storage.ts:36–43`
`initialValue` is an object passed inline at call sites — new reference on every render. Causes `removeValue` re-creation and downstream `useEffect` churn.
**Fix:** Stabilize with `useRef`: `const initialValueRef = useRef(initialValue)`, use `initialValueRef.current` in `removeValue`, remove `initialValue` from deps.
---
### MED-5: `filterUtils` `btoa`/`atob` may produce overly long base64 for Unicode filter values
**File:** `src/lib/utils/filter-utils.ts:224–256`
Double-encoding (`encodeURIComponent` + `btoa`) produces unnecessarily long strings that may exceed URL limits.
**Fix:** Use `btoa(unescape(encodeURIComponent(json)))` for true Unicode-safe base64.
---
### MED-6: Open redirect — `fromPath` URL param used as `href` without origin validation
**File:** `src/components/records/RecordView.tsx:409–416`
`/app/person/1?from=javascript:alert(1)` creates a clickable XSS link.
**Fix:**
```ts
const safeFromPath = fromPath?.startsWith('/') ? fromPath : null
```
---
### MED-7: Hydration mismatch — `window.innerWidth` in `useState` initializer
**File:** `src/components/query/RecordQuery.tsx:92–97`
Server renders one view mode, client may initialize another, causing React hydration warning.
**Fix:** Initialize from `preferences.tableDefaultViewMode`, apply mobile override in `useEffect`.
---
### MED-8: `goBack` in `useProcess` is client-only, not a server step submission
**File:** `src/lib/hooks/use-process.ts:341–355`
Server-side state is not rolled back. If the server expects a `_goBack` step, the process goes out of sync.
**Fix:** Submit `processStep(processName, processUUID, '_goBack', {})` and let server dictate `nextStep`.
---
### MED-9: Missing `useCallback` on `EntityForm.onSubmit`
**File:** `src/components/forms/EntityForm.tsx:195–197`
`onSubmit` re-created on every render, causing unnecessary re-renders of all form fields.
**Fix:** `const onSubmit = useCallback(async (values) => { await activeMutation.mutateAsync(values) }, [activeMutation])`
---
### MED-10: Unvalidated `JSON.parse` from localStorage in `getStoredUser`
**File:** `src/lib/auth/auth-provider.tsx:150–161`
**Fix:** Add type guard or schema validation.
---
### MED-11: `TOGGLE_COLUMN` initializes from `undefined` for never-toggled columns
**File:** `src/lib/hooks/use-record-query.ts:119–126`
`!undefined === true` means first toggle always shows column, losing the "default" state distinction.
**Fix:**
```ts
const currentVal = state.columnVisibility[action.fieldName] ?? true
return { ...state, columnVisibility: { ...state.columnVisibility, [action.fieldName]: !currentVal } }
```
---
### MED-12: Memory leak — column resize `mousemove`/`mouseup` listeners not removed on unmount
**File:** `src/components/query/DataGrid.tsx:231–253`
If `DataGrid` unmounts during a resize drag, the document event listeners remain.
**Fix:** Track active listeners in a `useRef`; clean them up in a `useEffect` cleanup.
---
### MED-13: Premature error state when `record` is undefined but `isError` is false
**File:** `src/components/records/RecordView.tsx:120–121`
```ts
if (isError || !record) { /* renders error UI */ }
```
Shows error flash before loading state.
**Fix:** Split: `if (isError) { ... }` then `if (!record && !isLoading) { ... }`.
---
### MED-14: Stateful regex in `HighlightedText` — `gi` flag causes incorrect `test()` after `split()`
**File:** `src/components/feedback/SearchDialog.tsx:31–47`
`split(regex)` advances `lastIndex`, making subsequent `regex.test(part)` calls unreliable.
**Fix:** Create a new regex for `test()`, or use `part.toLowerCase() === query.toLowerCase()`.
---
## LOW (8 findings)
- **LOW-1** `auth.ts:64` — Duplicate `clearAuthMetadataCache()` call (harmless)
- **LOW-2** `query-client.ts:55` — Widget cache key unstable with unordered `params` objects
- **LOW-3** `Sidebar.tsx:128` — Missing comment explaining intentional dep omission
- **LOW-4** `RecordView.tsx:37–44` — `getInitials('')` throws TypeError (empty label)
- **LOW-5** `DynamicFormField.tsx` — Unused `SelectField` import (dead code)
- **LOW-6** `RecordQuery.tsx:127–134` — `window.innerWidth` in callback not reactive to resize
- **LOW-7** `ExportButton.tsx:81–86` — Legacy DOM insertion pattern for file download trigger
- **LOW-8** `process-utils.ts`, `use-routes.ts` — Verify `hasPermission`/`readPermission` filtering for nav items
---
## Priority Order for Fixes
1. **CRIT-1, CRIT-2, CRIT-3** — Install DOMPurify, apply to all 4 HTML rendering sites
2. **CRIT-4, CRIT-5** — Auth stub implementations need proper server validation
3. **CRIT-6** — One-line fix: add `clearAuthMetadataCache()` to 401 callback
4. **HIGH-6** — One-line fix: re-throw non-404 errors in `globalSearch`
5. **HIGH-7** — Fix record selection to use stable PKs
6. **HIGH-8** — Fix retry button to call `invalidateQueries`
7. **CRIT-7** — Add error toast to `ExportButton`
8. **MED-6** — Validate `fromPath` starts with `/`
9. **HIGH-1** — Fix stale closures in `useRecordQuery` effects
10. Remaining HIGH/MED items by team priority
-398
View File
@@ -1,398 +0,0 @@
# Component Patterns
This guide describes the recurring patterns used throughout the QQQ Frontend Next codebase. Understanding these patterns is essential before adding new components or modifying existing ones.
---
## Pattern 1: Metadata-Driven Rendering
QQQ is a metadata-driven framework. The backend declares all structure — table names, field lists, widget layouts, and navigation items — and the frontend renders from it dynamically. No table names, field names, or navigation entries are hardcoded anywhere in the component tree.
### How it works
**Server-side (or near-server) layers** fetch metadata and pass it down as props. **Client Components** never fetch metadata themselves — they receive it through their props and render from it.
The routing entry point is `src/app/(dashboard)/app/[slug]/page.tsx`. It is a Client Component that fetches the full QQQ instance metadata from the server, resolves the slug against the metadata, and dispatches to the appropriate page component:
```tsx
// src/app/(dashboard)/app/[slug]/page.tsx (simplified)
export default function SlugPage() {
const params = useParams<{ slug: string }>()
const slug = params.slug
const { data: metaData } = useQuery({
queryKey: queryKeys.metadataAll(),
queryFn: loadMetaData,
staleTime: 1000 * 60 * 30, // 30 minutes
})
// Resolve slug against apps, tables, processes, reports in priority order
const isTable = Boolean(metaData?.tables?.[slug])
const table = metaData?.tables?.[slug]
// Pass resolved metadata as props — the child component never fetches
if (isTable && table) {
const tableProcesses = getProcessesForTable(metaData, slug)
return <RecordQuery tableName={slug} tableMetaData={table} processes={tableProcesses} />
}
// ... other cases
}
```
`RecordQuery` receives `tableMetaData` as a prop. It does not know or care how that data arrived — it renders from it:
```tsx
// src/components/query/RecordQuery.tsx
interface RecordQueryProps {
tableName: string
tableMetaData: QTableMetaData // always passed in, never fetched internally
processes?: QProcessMetaData[]
}
export function RecordQuery({ tableName, tableMetaData, processes }: RecordQueryProps) {
// tableMetaData is already resolved — no fetching here
const canCreate = tableMetaData.insertPermission
// ...
}
```
### Key rules
- `tableMetaData` (and any other metadata object) is `undefined` until TanStack Query resolves it. Every component must handle the loading state. The slug page renders a spinner until metadata is ready; it never passes `undefined` to its child components.
- User-facing text always uses `label` from metadata, never `name`. For example, the page header is set to `table?.label ?? slug`, not the raw slug.
- The `resolveSlugTarget` function in `SlugPage` is extracted as a pure function precisely so it can be tested in isolation without any rendering.
### Resolution priority
The slug page resolves in this order: **app** → **table** → **process** → **report**. The first match wins.
---
## Pattern 2: data-qqq-id Attributes
Every significant or interactive element carries a `data-qqq-id` attribute. This attribute serves two purposes:
1. **CSS customization hook** — deployers can write `[data-qqq-id="button-save"] { ... }` in their own stylesheets to override the default styling without touching source code.
2. **Test selector** — Playwright E2E tests and unit tests target elements by `data-qqq-id` rather than by class name (which is an implementation detail subject to change).
### Naming convention
The format is `{component-type}-{identifier}`:
| Pattern | Example |
|---|---|
| `record-query-{tableName}` | `record-query-orders` |
| `widget-{name}` | `widget-ordersSummary` |
| `widget-label-{name}` | `widget-label-ordersSummary` |
| `widget-content-{name}` | `widget-content-ordersSummary` |
| `button-widget-reload-{name}` | `button-widget-reload-ordersSummary` |
| `button-widget-export-{name}` | `button-widget-export-ordersSummary` |
| `button-widget-help-{name}` | `button-widget-help-ordersSummary` |
| `button-save` | `button-save` |
| `button-cancel` | `button-cancel` |
| `field-value-{fieldName}` | `field-value-status` |
| `filter-panel-close` | `filter-panel-close` |
| `link-back-to-source` | `link-back-to-source` |
| `unknown-slug-{slug}` | `unknown-slug-foo` |
### Examples from the actual code
`WidgetBlock.tsx` — the outer card, its label, and its icon buttons:
```tsx
<section
data-qqq-id={`widget-${name}`}
aria-label={label}
>
<h3 data-qqq-id={`widget-label-${name}`}>{label}</h3>
<button data-qqq-id={`button-widget-help-${name}`}>...</button>
<button data-qqq-id={`button-widget-export-${name}`}>...</button>
<button data-qqq-id={`button-widget-reload-${name}`}>...</button>
<div data-qqq-id={`widget-content-${name}`}>...</div>
</section>
```
`RecordQuery.tsx` — the outer container, the filter close button, and the back link:
```tsx
<div data-qqq-id={`record-query-${tableName}`}>
<Link data-qqq-id="link-back-to-source">...</Link>
<button data-qqq-id="filter-panel-close">...</button>
</div>
```
`EntityForm.tsx` — the form itself and its action buttons:
```tsx
<form data-qqq-id={`entity-form-${tableMetaData.name}`}>
<button data-qqq-id="button-cancel">Cancel</button>
<button data-qqq-id="button-save">Save</button>
</form>
```
`FieldValue.tsx` — every field value element is tagged with its field name:
```tsx
<span data-qqq-id={`field-value-${field.name}`}>...</span>
```
### Rules
- Do not use Tailwind class names as test selectors. If a test needs to click a button, it should use `page.getByTestId` or `[data-qqq-id="..."]` selectors, not `.bg-primary` or `.rounded-md`.
- Identifier segments use the backend `name` field (e.g., `orders`), not the `label` (e.g., `"Customer Orders"`). Labels can contain spaces and special characters; names are safe identifiers.
---
## Pattern 3: Async Combobox (`useAsyncCombobox`)
Any dropdown that loads its options from the API with user-typed search uses the `useAsyncCombobox` hook (`src/lib/hooks/use-async-combobox.ts`). The hook encapsulates fetch, debounce, AbortController cancellation, and click-outside-to-close logic that would otherwise be duplicated in every combobox component.
### When to use it
Use `useAsyncCombobox` when:
- The options come from a QQQ possible-value source (table-backed lookup).
- The user can type to search and the list is fetched on demand.
- The option list is too large to load all at once (more than ~50 items).
Use a plain `<select>` or a static shadcn/ui `<Select>` when:
- The options are a small static list (e.g., sort direction: "Ascending / Descending").
- The options are already available in memory (e.g., column names from `tableMetaData.fields`).
### Usage
```tsx
import { useAsyncCombobox } from '@/lib/hooks/use-async-combobox'
function MyPossibleValueInput({ tableName, fieldName, onChange }) {
const {
isOpen,
setIsOpen,
searchTerm,
setSearchTerm,
options, // QPossibleValue[] — the fetched options to display
isLoading,
containerRef, // attach to outermost wrapper for click-outside close
inputRef, // attach to the <input> for focus management
debouncedFetch, // call in onChange handler to trigger debounced fetch
} = useAsyncCombobox({
tableName,
fieldName,
onOptionsFetched: (results) => {
// optional: update a label map or other derived state
},
})
return (
<div ref={containerRef} className="relative">
<input
ref={inputRef}
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value)
debouncedFetch(e.target.value)
}}
onFocus={() => setIsOpen(true)}
/>
{isOpen && (
<ul role="listbox">
{isLoading && <li>Loading...</li>}
{options.map((opt) => (
<li
key={opt.id}
role="option"
onClick={() => {
onChange(opt.id)
setIsOpen(false)
}}
>
{opt.label}
</li>
))}
</ul>
)}
</div>
)
}
```
### What the hook manages internally
| Concern | Mechanism |
|---|---|
| Debouncing | `setTimeout` cleared on each call; fires after `COMBOBOX_DEBOUNCE_MS` (200 ms) |
| Request cancellation | `AbortController` — aborts the previous fetch before starting a new one |
| Click-outside close | `document.addEventListener('mousedown', handler)` attached to `containerRef` |
| Auto-fetch on open | `useEffect` on `isOpen` — fetches the full list when the dropdown opens |
| Cleanup on unmount | Clears pending timer and aborts any in-flight request |
### Return values
| Field | Type | Purpose |
|---|---|---|
| `isOpen` | `boolean` | Drive conditional rendering of the dropdown list |
| `setIsOpen` | `Dispatch` | Programmatically open/close (e.g., on focus, on select) |
| `searchTerm` | `string` | Controlled value for the text input |
| `setSearchTerm` | `Dispatch` | Update the text input value |
| `options` | `QPossibleValue[]` | The fetched options — map these to `<li>` elements |
| `isLoading` | `boolean` | Show a spinner or loading state while the fetch is in-flight |
| `containerRef` | `RefObject<HTMLDivElement>` | Attach to the outermost wrapper element |
| `inputRef` | `RefObject<HTMLInputElement>` | Attach to the text `<input>` element |
| `fetchOptions` | `(term: string) => Promise<void>` | Immediate fetch (bypasses debounce) |
| `debouncedFetch` | `(term: string) => void` | Debounced fetch — use this in `onChange` |
---
## Pattern 4: Widget Rendering Pipeline
All dashboard widgets flow through a three-layer pipeline:
```
ConnectedWidget
└─ WidgetBlock (card chrome, loading, error, dropdown selects)
└─ WidgetRenderer (type dispatcher)
└─ StatisticsWidget | BarChartWidget | BlockWidget | ...
```
### Layer 1: ConnectedWidget
`src/components/widgets/ConnectedWidget.tsx` is the primary entrypoint. It:
1. Reads `widgetMetaData.dropdowns` and initializes dropdown state.
2. Fetches dropdown option lists from the possible-value API.
3. Calls `useWidget(widgetMetaData.name, mergedParams)` to fetch the widget's data payload.
4. Returns `null` immediately when `widgetMetaData.hasPermission === false`.
5. Renders `<WidgetBlock>` with loading/error props and passes the fetched `data` to `<WidgetRenderer>`.
```tsx
// ConnectedWidget orchestrates everything
export function ConnectedWidget({ widgetMetaData, params, className }) {
const { data, isLoading, isError, error, refetch } = useWidget(widgetMetaData.name, mergedParams)
return (
<WidgetBlock
widgetMetaData={widgetMetaData}
isLoading={isLoading}
isError={isError}
error={error}
onReload={refetch}
// ... dropdown props
>
{data && <WidgetRenderer widgetMetaData={widgetMetaData} data={data} />}
</WidgetBlock>
)
}
```
### Layer 2: WidgetBlock
`src/components/widgets/WidgetBlock.tsx` provides the standard card shell:
- Header with `label`, optional help tooltip, optional export button, optional reload button, and optional dropdown `<select>` elements.
- Body area that renders a skeleton, an error state, or its `children`.
- A `bare` prop that strips all chrome for use inside composite widgets.
The `WidgetBlock` does not know what type of widget it wraps. It only knows about loading/error state.
### Layer 3: WidgetRenderer
`src/components/widgets/WidgetRenderer.tsx` is the type dispatcher. It receives `widgetMetaData` and the raw `data` payload, reads `widgetMetaData.type`, and renders the appropriate typed component via a `switch` statement.
Supported types:
| `widgetMetaData.type` | Rendered component |
|---|---|
| `'statistics'` | `StatisticsWidget` |
| `'barChart'` | `BarChartWidget` (lazy) |
| `'lineChart'` | `LineChartWidget` (lazy) |
| `'pieChart'` | `PieChartWidget` (lazy) |
| `'recordGrid'` | `RecordGridWidget` |
| `'html'` / `'block'` | `BlockWidget` |
| `'divider'` | `DividerWidget` |
| `'quickLinks'` | `QuickLinksWidget` |
| `'alert'` | `AlertWidget` |
| `'processSummary'` | `ProcessSummaryWidget` |
| `'composite'` / `'parent'` | `CompositeWidget` |
| `'chart'` | `ChartTypeDispatcher` (secondary `chartType` dispatch) |
Chart widgets (`barChart`, `lineChart`, `pieChart`) are loaded with `React.lazy` + `Suspense` to split the Recharts library into a separate bundle chunk. This keeps the initial JS bundle small on pages that contain no charts.
### How to add a new widget type
1. Create a new component file, e.g., `src/components/widgets/HeatmapWidget.tsx`. Export a typed payload interface and the component:
```tsx
// src/components/widgets/HeatmapWidget.tsx
export interface HeatmapWidgetPayload {
rows: Array<{ label: string; cells: number[] }>
columnLabels: string[]
}
export function HeatmapWidget({ data, widgetName }: { data: HeatmapWidgetPayload; widgetName: string }) {
// render the heatmap
}
```
2. Add a `case` to the `switch` in `WidgetRenderer.tsx`:
```tsx
import { HeatmapWidget } from './HeatmapWidget'
import type { HeatmapWidgetPayload } from './HeatmapWidget'
// Inside the switch:
case 'heatmap':
return <HeatmapWidget data={data as HeatmapWidgetPayload} widgetName={name} />
```
3. The backend returns `widgetMetaData.type = 'heatmap'` for this widget. No other changes are needed — `ConnectedWidget` picks it up automatically.
---
## Pattern 5: Field Adornment Priority
`FieldValue` (`src/components/records/FieldValue.tsx`) renders a single QQQ field value in read-only mode. When a field has adornments (decorations declared in `field.adornments[]`), `FieldValue` applies them in strict priority order. The first matching adornment wins; subsequent ones are skipped.
### Priority order
1. **LINK** — renders the value as an `<a>` tag with an external link icon.
2. **FILE_DOWNLOAD** — renders a download anchor using the display value as the URL.
3. **SIZE** — formats the raw number as a human-readable byte size (e.g., `1.5 MB`).
4. **CHIP** — renders the value as a colored badge using a `colorMap` from adornment values.
5. **RENDER_HTML** (or field type `HTML`) — renders sanitized HTML via `DOMPurify.sanitize`.
6. **CODE_EDITOR** — renders the value in a monospace `<pre><code>` block.
7. **TOOLTIP** — wraps the value in a Radix tooltip. Falls through to type-based rendering when no tooltip text is resolved.
8. **ERROR** — renders the value with a destructive icon and styling.
9. Record reference link — when `field.possibleValueSourceName` matches a known table, renders as a Next.js `<Link>` with a `RecordHoverCard` preview.
10. Type-based fallbacks — `BOOLEAN`, `PASSWORD`, `BLOB`, `TEXT` each have dedicated renderers.
11. Default — auto-links bare `http(s)://` URLs and `mailto:` email addresses.
### How adornment detection works
Each adornment is checked with `Array.some()` before its rendering block. The discriminated union type `FieldAdornment` is narrowed inside each block using a type predicate:
```tsx
// Detection
const hasLink = field.adornments?.some((a) => a.type === 'LINK')
// Narrowing within the block
if (hasLink) {
const linkAdornment = field.adornments?.find(
(a): a is Extract<FieldAdornment, { type: 'LINK' }> => a.type === 'LINK'
)
const href = linkAdornment?.values?.linkURL ?? String(value)
// ...
}
```
The `Extract<FieldAdornment, { type: 'LINK' }>` predicate gives TypeScript full knowledge of which `values` keys are available for that specific adornment type without a cast.
### TOOLTIP fall-through
The TOOLTIP case has one deliberate exception to the "first match wins" rule: if the `TOOLTIP` adornment is present but none of the `values` keys (`tooltipText`, `text`, `tooltip`) resolve to a non-empty string, the block falls through to type-based rendering. The value is still displayed — only the tooltip wrapper is omitted because there is nothing to show. This is documented with a comment in the source.
### DOMPurify sanitization
HTML values are sanitized once via `useMemo` that only re-runs when `value` changes:
```tsx
const sanitizedHtml = useMemo(
() => DOMPurify.sanitize(String(value)),
[value]
)
```
This prevents unnecessary sanitization work on every render while also preventing XSS from backend-provided HTML content.
-37
View File
@@ -1,37 +0,0 @@
# Material CSS hooks in Next UI
QQQ application themes can provide `supplementalInstanceMetaData.materialDashboardTheme.customCss`. Next UI keeps the main Material Dashboard `data-qqq-id` hooks so application CSS can target metadata-driven controls. The source of truth for ID generation is [`src/lib/utils/qqq-id.ts`](../src/lib/utils/qqq-id.ts); [NAV-055](acceptance/feature-matrix.md) exercises representative selectors against the real sample server.
## ID rules
The ID suffix is lowercased, each run of characters outside `a-z` and `0-9` becomes one dash, leading and trailing dashes are removed, and the result is cut at 50 characters. Camel case is not split: `firstName` becomes `firstname`. Use the metadata label for navigation entries and the metadata name for fields, tables and sections.
| Control | Pattern | Example |
|---|---|---|
| Button | `button-{id or text}` | `button-save`, `button-query-mode-advanced` |
| Editable text field | `input-{field}` | `input-firstname` |
| Possible-value field | `select-{field}` | `select-speciesid` |
| Boolean field | `switch-{field}` | `switch-isemployed` |
| Navigation item | `sidenav-{label}` | `sidenav-people-app` |
| Menu item | `menu-item-{text}` | `menu-item-bulk-edit` |
| Tab | `tab-{label}` | `tab-related` |
| Table header | `table-header-{field}` | `table-header-firstname` |
| Link | `link-{text}` | `link-back-to-person` |
The ID may be on a layout-neutral wrapper around a native button or menu item. Select the wrapper when applying layout or visibility rules, and select its button descendant for button-specific styles. Inspect the rendered element before changing an application's CSS.
## Stable named hooks
- Navigation: `sidenav-root`, `sidenav-logo-area`, `sidenav-menu-list`, `sidenav-logout-button`; `.qqq-sidebar-active` marks the active item. `data-qqq-sidenav-item-type` identifies `user-profile` and `top-level-parent-app` entries.
- 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.
## Display mode and primary text
Next's persisted light/dark choice controls both theme tokens and Tailwind `dark:` variants. OS color preference does not override it; a configured Material application theme keeps light mode active. `--color-primary` controls branded fills, while `--text-color-primary` controls the existing `text-primary` classes and opacity variants. In light mode the text token follows the primary color; in dark mode it mixes the brand hue with white for readable text on dark surfaces. Explicit application theme and custom CSS rules still take precedence. Verify text and filled-button contrast separately when overriding these tokens.
-278
View File
@@ -1,278 +0,0 @@
# Design & UX Review — QQQ Frontend Next
**Date:** 2026-03-01
**Reviewer:** Claude Code (automated static analysis)
**Scope:** Full codebase UX/design audit — layout, pages, forms, process, widgets, feedback, theme, responsive, and a11y
---
## Executive Summary
The new frontend has strong bones: metadata-driven rendering is correctly implemented, component composition is clean, TanStack Query manages server state well, and the base accessibility implementation (labels, aria-required, focus rings, skip nav on breadcrumbs) is solid. The primary gaps fall into three categories:
1. **Accessibility** — several WCAG 2.1 AA gaps in the data grid, animations, and motion preferences
2. **Mobile/responsive polish** — search hidden on mobile, touch targets below 44px minimum in several places
3. **Loading/error state coverage** — a handful of async operations show no spinner or fallback
No showstopper UX regressions from the Material UI frontend were found. The issues below are incremental improvements.
---
## Severity Legend
| Severity | Meaning |
|----------|---------|
| **CRIT** | WCAG 2.1 Level A violation or severe usability break |
| **HIGH** | WCAG 2.1 AA violation or significant UX regression |
| **MED** | Noticeable UX gap, inconsistency, or missing polish |
| **LOW** | Minor polish, nice-to-have, or future consideration |
---
## 1. Layout Shell
**Files:** `src/components/layout/Sidebar.tsx`, `Header.tsx`, `Breadcrumbs.tsx`, `Banner.tsx`, `src/app/(dashboard)/layout.tsx`
### Strengths
- Desktop sidebar + mobile drawer correctly split at `md` breakpoint
- Breadcrumbs have `aria-label="Breadcrumb"` and `aria-current="page"`
- Escape key closes mobile drawer; sidebar items have focus rings
- Banner severity states (info/warning/error) correctly scoped with icons
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-L-1 | **HIGH** | **No skip-to-content link.** Keyboard users must tab through the entire sidebar before reaching page content. Add `<a href="#main-content" className="sr-only focus:not-sr-only">Skip to main content</a>` as the first child of `<body>`. |
| D-L-2 | **HIGH** | **Animations ignore `prefers-reduced-motion`.** Sidebar slide-in, banner fade-in, and skeleton `animate-pulse` all run unconditionally. Add `@media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }` to `globals.css`. |
| D-L-3 | **MED** | **Header search hidden on mobile** (`hidden md:block`). Mobile users lose global search entirely. Add a search icon button on mobile that opens `SearchDialog`. |
| D-L-4 | **MED** | **Dismissed banners not persisted.** Banners dismissed via local state reappear on reload. Save dismissed banner keys to `localStorage` under `qqq:dismissed-banners`. |
| D-L-5 | **MED** | **Mobile sidebar `aria-expanded` not set.** The mobile drawer open/closed state isn't announced to screen readers. Add `aria-expanded={mobileOpen}` to the nav landmark element. |
| D-L-6 | **LOW** | **Breadcrumb overflow on very narrow viewports** (<320px). Long breadcrumb chains don't truncate; last segment can overflow. Add `truncate` + `max-w-[8rem]` on intermediate segments. |
| D-L-7 | **LOW** | **Sidebar nav has no loading skeleton.** During the initial metadata fetch the sidebar is empty with no placeholder. Add skeleton nav items matching the expected structure. |
---
## 2. Record Query (List View)
**Files:** `src/components/query/DataGrid.tsx`, `FilterBuilder.tsx`, `Pagination.tsx`, `RecordQueryToolbar.tsx`, `RecordCardView.tsx`, `ColumnConfig.tsx`
### Strengths
- Skeleton rows during fetch match final layout (no layout shift)
- Three row density variants (compact/standard/comfortable) with persistence
- Empty state with icon, title, and "Clear filters" CTA
- Quick-search debounced at 300ms
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-Q-1 | **CRIT** | **DataGrid missing `aria-sort` on column headers.** When a column is sorted, screen readers don't announce direction. Add `aria-sort="ascending" \| "descending" \| "none"` to each `<th>` based on current sort state. |
| D-Q-2 | **HIGH** | **Row selection checkboxes have no label.** Each row checkbox announces "checkbox" with no context. Add `aria-label={\`Select ${recordLabel}\`}` to each. |
| D-Q-3 | **HIGH** | **Column resize handles not keyboard accessible.** Resize is mouse-drag only. Keyboard users cannot adjust column widths. Add `onKeyDown` handler supporting `ArrowLeft`/`ArrowRight` to the resize handle element. |
| D-Q-4 | **MED** | **FilterBuilder async combobox has no loading indicator.** While possible-value options are fetching, the dropdown appears empty with no spinner. Add a `Loader2` icon while `isLoading` is true. |
| D-Q-5 | **MED** | **"Go to page" input silently ignores out-of-range values.** Typing a page number beyond the last page does nothing with no feedback. Show an inline validation message like "Page must be between 1 and N". |
| D-Q-6 | **MED** | **No `aria-live` region for fetch-in-progress.** Background re-fetches (sort/filter changes) are visually indicated by the progress bar but not announced. Add `aria-live="polite"` region announcing "Loading results". |
| D-Q-7 | **MED** | **Column visibility toggle state not announced.** Toggling a column in ColumnConfig doesn't notify screen readers. Add a `role="status"` region that announces "Column X hidden" / "Column X visible". |
| D-Q-8 | **LOW** | **No frozen/pinned columns.** The primary key / record label column scrolls out of view on wide tables. Consider pinning the first column on horizontal scroll. |
| D-Q-9 | **LOW** | **RecordCardView touch targets may be too small.** Cards are clickable but no explicit 44px minimum hit area. Ensure card tap targets meet the 44px minimum on mobile. |
---
## 3. Record View (Detail Page)
**Files:** `src/components/records/RecordView.tsx`, `RecordViewHeader.tsx`, `FieldValue.tsx`, `RecordViewAssociated.tsx`
### Strengths
- DOMPurify sanitization applied before rendering HTML field values
- 403/404/500 errors shown with contextual messages
- Tab layout for T2/T3 sections with proper semantic structure
- Field adornment priority correctly handled (LINK → FILE_DOWNLOAD → SIZE → CHIP)
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-V-1 | **HIGH** | **Edit/Delete buttons shown regardless of user permissions.** If table metadata indicates the user lacks UPDATE/DELETE rights, buttons should be hidden (or disabled with tooltip). Currently they render and fail with a 403 on action. |
| D-V-2 | **MED** | **Associated records list not paginated.** Many-to-many joins can return thousands of records, all rendered to DOM. Add pagination (or a "Show more" pattern) capped at 25 by default. |
| D-V-3 | **MED** | **Avatar initials fail for non-Latin scripts.** `getInitials()` splits on whitespace which produces empty strings for CJK/Arabic names. Add a fallback to a generic user icon when initials can't be derived. |
| D-V-4 | **MED** | **No delete confirmation dialog.** The delete action currently triggers immediately. Add a confirm dialog ("Delete this record? This cannot be undone.") before the API call. |
| D-V-5 | **LOW** | **No "Copy record ID" quick action.** Admin users frequently need the primary key for support/debugging. Add a small copy-to-clipboard icon next to the record ID. |
| D-V-6 | **LOW** | **No audit trail footer.** No "Last modified by / at" metadata shown. If the backend provides this, surface it at the bottom of the record view. |
---
## 4. Create / Edit Forms
**Files:** `src/components/forms/DynamicForm.tsx`, `EntityForm.tsx`, `DynamicFormField.tsx`, `src/components/forms/field-types/`
### Strengths
- All inputs have `aria-required`, `aria-invalid`, `aria-describedby` wired correctly
- Unsaved-changes guard (`beforeunload` + modal) prevents accidental data loss
- Help tooltips use Radix Tooltip with keyboard support
- Required asterisk is `aria-hidden="true"` to avoid screen-reader repetition
- Disabled state styling consistent across field types
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-F-1 | **HIGH** | **Submit button stays enabled during mutation.** User can click Save multiple times before the first request completes, creating duplicate records. Add `disabled={isSubmitting}` and a loading spinner to the submit button. |
| D-F-2 | **MED** | **Number fields don't display min/max constraints.** If field metadata defines `minValue`/`maxValue`, the user doesn't see them until a validation error fires. Show the range as hint text below the field (`0 – 100`). |
| D-F-3 | **MED** | **DateTime fields don't indicate time zone.** The input accepts a time but doesn't show which time zone is assumed. Show the user's local TZ abbreviation next to the field. |
| D-F-4 | **MED** | **Help tooltips not touch-friendly on mobile.** The `?` icon is hover-only and < 44px. On mobile, show the help text inline beneath the label instead. |
| D-F-5 | **MED** | **File upload field has no drag-and-drop visual feedback.** The drop zone changes border on `dragover` but has no text like "Drop file here". Add clear drop-zone copy and a highlight color change. |
| D-F-6 | **LOW** | **No "which fields changed" diff in edit mode.** Users editing a long form can't tell what they've modified at a glance. Consider highlighting changed fields with a left border accent. |
| D-F-7 | **LOW** | **Large forms show all fields at once.** Sections with 20+ fields have no collapse/expand. Consider optional `<details>` sections for secondary field groups. |
---
## 5. Process Execution
**Files:** `src/components/process/ProcessRun.tsx`, `StepWizard.tsx`, `ProcessFormStep.tsx`, `ValidationReviewStep.tsx`, `ProcessUploadFormStep.tsx`, `BulkLoadStep.tsx`
### Strengths
- Step wizard gives clear numbered/checkmark visual progress
- Cancellation confirm dialog prevents accidental process abort
- Back navigation available when not on the first step
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-P-1 | **HIGH** | **Current step not auto-focused after "Next".** When advancing to a new step, focus remains on the Next button (now gone), leaving screen reader users disoriented. Move focus to the step heading or first field on step change. |
| D-P-2 | **MED** | **Step wizard steps not keyboard-navigable (back).** Completed steps have no click handler to jump back. Add click-to-navigate on completed step indicators. |
| D-P-3 | **MED** | **No process timeout message.** If the backend hangs, the polling spinner runs indefinitely. After ~60s show "This is taking longer than expected" with an option to wait or cancel. |
| D-P-4 | **MED** | **Validation review results not filterable.** With 1000+ errors, the flat list is unmanageable. Add severity filter buttons (All / Errors / Warnings) and a field-name search. |
| D-P-5 | **MED** | **File upload step has no progress bar.** Large uploads show no byte-level progress. Add a `<progress>` element or Radix Progress during multipart upload. |
| D-P-6 | **MED** | **Step label truncation has no mobile tooltip.** Long step names are truncated at `max-w-[6rem]` with only a `title` attribute, which doesn't appear on touch. Add a Radix Tooltip wrapper. |
| D-P-7 | **LOW** | **No "retry" on process step failure.** Users must restart the entire process from step 1 if one step fails. Add a "Retry this step" button where the backend supports re-running a step. |
| D-P-8 | **LOW** | **Bulk load column mappings not saveable.** Users re-map the same CSV columns every run. Add a "Save this mapping" option persisted to localStorage. |
---
## 6. Widgets & Dashboard
**Files:** `src/components/widgets/WidgetRenderer.tsx`, `StatisticsWidget.tsx`, `BlockWidget.tsx`, `BarChartWidget.tsx`, `LineChartWidget.tsx`, `PieChartWidget.tsx`, `RecordGridWidget.tsx`
### Strengths
- Charts lazy-loaded via dynamic import (non-chart pages don't load Recharts)
- Suspense skeleton placeholder shown while chart chunk loads
- Exhaustive `switch` on widget type with unknown fallback
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-W-1 | **HIGH** | **Statistics trend arrows not accessible.** `TrendingUp`/`TrendingDown`/`Minus` icons are `aria-hidden` with no text alternative. Screen readers don't announce whether a metric is up or down. Add `<span className="sr-only">trending up</span>` next to each icon. |
| D-W-2 | **MED** | **Widget data fetch errors show blank space.** If a widget's API call fails there is no error boundary or fallback UI. Add a per-widget error state (icon + "Could not load widget" message + retry). |
| D-W-3 | **MED** | **Chart legends overflow horizontally on mobile.** Recharts legends don't reflow on narrow screens. Use `layout="vertical"` or hide the legend on mobile with a `<ResponsiveContainer>` width check. |
| D-W-4 | **MED** | **RecordGrid widget not paginated.** Renders the full result set to DOM. Cap at 25 rows and add a "View all" link to the full record query page. |
| D-W-5 | **MED** | **No manual widget refresh.** Stale widget data can't be refreshed without a full page reload. Add a small refresh icon button on each widget card header. |
| D-W-6 | **LOW** | **Only 5 chart color tokens defined.** Dashboards with 6+ data series reuse colors, making series indistinguishable. Extend `--color-chart-*` to at least 10 distinct values. |
---
## 7. Feedback Components
**Files:** `src/components/feedback/GlobalSearch.tsx`, `SearchDialog.tsx`, `CommandMenu.tsx`, `EmptyState.tsx`, `ErrorBoundary.tsx`, `KeyboardShortcutsDialog.tsx`
### Strengths
- `EmptyState` uses `role="status"` + `aria-live="polite"` correctly
- Skeleton components have `role="status"` + `aria-busy="true"`
- SearchDialog has full ArrowUp/ArrowDown/Enter/Escape keyboard nav
- `HighlightedText` now correctly uses `i % 2 !== 0` (recently fixed)
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-FB-1 | **MED** | **Keyboard Shortcuts dialog not discoverable.** `?` opens it but there is no visible hint in the UI. Add a `?` icon button in the header, or a footer note "Press ? for keyboard shortcuts". |
| D-FB-2 | **MED** | **Toasts have no visible close/dismiss button.** Users must wait for auto-dismiss. Add an × button to each toast (sonner supports `closeButton` prop). |
| D-FB-3 | **MED** | **CommandMenu icon buttons below 44px touch target.** Small icon-only buttons in the command menu are hard to tap on mobile. Ensure `min-h-[44px] min-w-[44px]` on interactive icons. |
| D-FB-4 | **LOW** | **Search results missing table/app context in header.** Results are grouped by table in logic but group labels may not be visually prominent enough. Ensure group headers have sufficient weight/spacing. |
---
## 8. Theme & Global Styles
**Files:** `src/styles/globals.css`, `src/styles/qqq-theme.css`, `src/lib/theme/theme-provider.tsx`, `src/lib/theme/tokens.ts`
### Strengths
- 60+ CSS custom property tokens for colors, spacing, shadows
- Complete dark mode override section
- `:focus-visible` with proper outline and offset
- Muted text color darkened to meet WCAG AA contrast
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-T-1 | **HIGH** | **No `prefers-reduced-motion` override.** All animations (skeleton pulse, slide-in transitions, toast fades) run unconditionally. See D-L-2 above — one global CSS rule resolves this. |
| D-T-2 | **HIGH** | **No `prefers-contrast: more` support.** Users with OS high-contrast mode enabled get no enhanced styling. Add a `@media (prefers-contrast: more)` block that increases border weights and ensures 7:1 contrast ratios. |
| D-T-3 | **MED** | **Dual theming systems create token confusion.** `--qqq-primary-color` and `--color-primary` both define the primary color. Components reference both inconsistently. Consolidate to one system (Tailwind v4 `--color-*` preferred) and alias the `--qqq-*` tokens to the canonical values for backwards compatibility. |
| D-T-4 | **MED** | **Z-index tokens defined but not used consistently.** `--qqq-z-sidebar: 100`, `--qqq-z-modal: 1000` exist in the token file but some components use hardcoded values. Use the tokens everywhere. |
| D-T-5 | **LOW** | **Focus ring may be invisible in dark mode.** The `color-mix`-based focus outline color may not contrast sufficiently on dark backgrounds. Test with browser dark mode enabled. |
| D-T-6 | **LOW** | **No disabled-state semantic token.** All disabled states use `opacity-50`. A dedicated `--color-disabled` token would allow more nuanced disabled styling (e.g., different for text vs background). |
---
## 9. Responsive / Mobile
### Strengths
- Sidebar correctly switches between desktop rail and mobile drawer at `md`
- RecordQuery shows bottom-sheet filter panel on mobile
- RecordCardView provides card layout alternative to data grid on mobile
- Pagination uses `flex-wrap` to reflow on narrow screens
### Findings
| ID | Severity | Finding |
|----|----------|---------|
| D-R-1 | **HIGH** | **Mobile `<input>` elements trigger browser zoom.** Inputs with `font-size < 16px` cause iOS Safari to auto-zoom on focus, breaking the layout. Ensure all form inputs have `text-base` (16px) as minimum font size. |
| D-R-2 | **MED** | **Touch targets below 44px in multiple places.** Pagination size selector, density buttons, column config toggles. Audit all interactive elements and ensure `min-h-[44px]` or adequate padding. |
| D-R-3 | **MED** | **No 320px (iPhone SE) layout testing.** Several components assume ≥375px. Run a responsive check at 320px and fix any overflows. |
| D-R-4 | **LOW** | **Landscape tablet layout untested.** At ~768px landscape some components may show desktop layout with insufficient room. Test iPad Mini landscape. |
---
## Priority Matrix
### Must Fix (WCAG 2.1 AA / CRIT / HIGH)
| ID | Area | Fix |
|----|------|-----|
| D-L-1 | Layout | Skip-to-content link |
| D-L-2 / D-T-1 | Layout / Theme | `prefers-reduced-motion` CSS rule |
| D-T-2 | Theme | `prefers-contrast: more` support |
| D-Q-1 | Record Query | `aria-sort` on DataGrid column headers |
| D-Q-2 | Record Query | Row checkbox `aria-label` |
| D-Q-3 | Record Query | Keyboard-accessible column resize |
| D-V-1 | Record View | Permission-gated Edit/Delete buttons |
| D-F-1 | Forms | Disable submit button while submitting |
| D-P-1 | Process | Auto-focus step heading on step change |
| D-W-1 | Widgets | Screen-reader text for trend arrows |
| D-R-1 | Responsive | 16px minimum font size on inputs |
### Should Fix (MED — noticeable UX gaps)
D-L-3 (mobile search), D-L-4 (banner persistence), D-L-5 (sidebar aria-expanded), D-Q-4 (FilterBuilder spinner), D-Q-5 (pagination range feedback), D-Q-6 (aria-live for fetch), D-V-2 (associated records pagination), D-V-4 (delete confirmation), D-F-2 (number field min/max hint), D-F-3 (DateTime TZ indicator), D-F-4 (mobile tooltip → inline help), D-P-2 (step back navigation), D-P-3 (process timeout message), D-P-4 (validation review filter), D-P-5 (upload progress bar), D-W-2 (widget error boundary), D-W-3 (chart legend mobile), D-W-5 (widget refresh button), D-FB-1 (keyboard shortcuts discoverability), D-FB-2 (toast close button), D-T-3 (dual theming consolidation), D-T-4 (z-index token usage), D-R-2 (touch targets), D-R-3 (320px layout)
### Nice to Have (LOW — polish)
D-L-6 (breadcrumb truncation), D-L-7 (sidebar skeleton), D-Q-8 (frozen columns), D-V-5 (copy record ID), D-V-6 (audit trail), D-F-6 (edit diff), D-F-7 (collapsible form sections), D-P-7 (retry step), D-P-8 (save bulk load mapping), D-W-6 (chart color tokens), D-FB-4 (search result context), D-T-5 (dark mode focus ring), D-T-6 (disabled token), D-R-4 (landscape tablet)
---
## What's Working Well
The following areas are solid and don't require significant rework:
- **Metadata-driven rendering** — no hardcoded table/field names anywhere
- **API layer** — all calls through typed `src/lib/api/` functions, consistent error handling
- **Form accessibility baseline** — `aria-required`, `aria-invalid`, `aria-describedby` correctly wired
- **Unsaved changes guard** — `beforeunload` + confirm modal works correctly
- **TanStack Query** — caching, deduplication, and revalidation correctly configured
- **Dark mode** — complete token coverage, no visual regressions found
- **Skeleton loading** — components have `role="status"` and `aria-busy="true"`
- **DOMPurify** — applied consistently on all user-controlled HTML output
- **URL state** — filters, pagination, and sort are shareable via query params
-194
View File
@@ -1,194 +0,0 @@
<!--
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.
-->
# QQQ Frontend Next — Documentation Review
**Date:** 2026-03-01
**Scope:** Full `src/` + `docs/` + project root
**Commit:** 12edc4c
---
## Overall Score: 7.5 / 10
**Strengths:** JSDoc coverage is high across the API client, utilities, and most hooks. Type definitions are well-documented. `CLAUDE.md` and the implementation plans give a strong architectural overview.
**Key gaps:** `README.md` is essentially empty, `useRecordQuery` has no return-type documentation, and there are no architecture guides for state management or component composition patterns — the two things a new contributor needs most.
---
## CRITICAL — Must Address
### DOC-CRIT-1: README.md is empty
**File:** `README.md`
**Issue:** The file contains only a title. No setup instructions, command list, project overview, or links to docs.
**Impact:** A new developer cannot start contributing without reading `CLAUDE.md` and inferring the rest.
**Fix:** Add the following sections at minimum:
- What this project is (2–3 sentences)
- Prerequisites (Node.js version, pnpm)
- Quick start (`git clone` → `pnpm install` → `pnpm dev`)
- Available commands (dev, build, test, test:e2e, lint, tsc)
- Pointer to `CLAUDE.md` for architecture and conventions
- Pointer to `docs/` for implementation plans
---
### DOC-CRIT-2: `useRecordQuery` return type is undocumented
**File:** `src/lib/hooks/use-record-query.ts` (~line 60, main hook export)
**Issue:** The hook returns a large composite object (pagination, filter, selection, sort, query state, plus delegated `columns` and `views` namespaces from sub-hooks). None of the return properties are documented. Only two components consume this hook today, but that will grow.
**Impact:** Callers must trace through 685 lines of source to discover which properties exist.
**Fix:** Add a `@returns` block enumerating the top-level namespaces and key properties, or define an explicit `UseRecordQueryResult` interface with JSDoc on each field.
---
### DOC-CRIT-3: No state-management decision guide
**Issue:** The project uses five different state containers (React Context, TanStack Query, React Hook Form, URL search params, localStorage), each for different purposes. There is a brief table in `CLAUDE.md` but no guidance on the decision criteria.
**Impact:** New contributors pick the wrong state container; inconsistent patterns accumulate over time.
**Fix:** Create `docs/STATE-MANAGEMENT.md` covering:
- TanStack Query: all server state (records, metadata, widgets)
- React Hook Form: form field state only
- URL search params: filters, pagination, sort (shareable links)
- localStorage: user preferences, column config, saved views
- QContext: application-level UI state (modals, toast queue)
- When NOT to use useState for server-derived state
---
## SIGNIFICANT — Should Address
### DOC-SIG-1: No component composition patterns guide
**Issue:** The codebase uses several non-obvious patterns not documented anywhere outside of scattered code comments:
- Metadata-driven rendering (metadata always flows down; children never fetch)
- `data-qqq-id` naming convention (`{component-type}-{identifier}`)
- Async combobox pattern (`useAsyncCombobox` wrapping)
- Field adornment priority chain (documented in `FieldValue.tsx` but not shared)
**Fix:** Create `docs/COMPONENT-PATTERNS.md` with each pattern described, the rationale, and a code example.
---
### DOC-SIG-2: No type glossary
**Issue:** The `src/types/` directory exports ~40 interfaces and type aliases. There is no index or glossary. Finding the right type requires `grep`-ing.
**Impact:** Types are duplicated or approximated when developers can't find the canonical definition.
**Fix:** Create `docs/TYPE-GLOSSARY.md` with a table: Type | File | Purpose | Example usage. Focus on the ~15 most-used types (`QInstance`, `QTableMetaData`, `QFieldMetaData`, `SavedView`, `QProcessMetaData`, `StatTile`, `FilterModel`, etc.).
---
### DOC-SIG-3: API error handling patterns undocumented
**File:** `src/lib/api/client.ts` (referenced)
**Issue:** The 401 interceptor is documented, but there is no guide to how callers should handle 400 (validation), 403 (permission), 404 (not found), and 500 (server error) responses. Components handle these inconsistently today.
**Fix:** Add a section to the API client file header or create `docs/API-ERROR-HANDLING.md` with the standard response patterns per HTTP status code.
---
### DOC-SIG-4: Zod schema generation lacks examples
**File:** `src/lib/utils/zod-from-metadata.ts`
**Issue:** The function and helpers are well-documented, but there is no `@example` showing how a field metadata object maps to a generated Zod schema. This is the most opaque part of the form validation pipeline.
**Fix:** Add a 6-line `@example` to `zodFieldFromMetadata` showing an input `QFieldMetaData` object and the corresponding schema.
---
### DOC-SIG-5: Form validation strategy not centralized
**Issue:** Zod schema generation is in `zod-from-metadata.ts`, field-level validation rules are in individual field components, and server-side validation errors are mapped in `EntityForm.tsx`. No single document explains the end-to-end validation strategy.
**Fix:** Create `docs/FORM-VALIDATION.md` covering schema generation, custom refinements, and mapping server errors to form fields.
---
### DOC-SIG-6: TanStack Query key factory undocumented
**File:** `src/lib/query-client.ts`
**Issue:** The query key factory pattern is used throughout the codebase but the naming convention and invalidation patterns are not documented. Developers risk creating overlapping keys.
**Fix:** Document the key structure and add `@example` tags showing how to invalidate a table's record list after a mutation.
---
## MINOR — Nice to Have
### DOC-MIN-1: Test file headers are sparse
Many test files (`*.test.tsx`) have no JSDoc file header explaining what behavior is under test and what is explicitly out of scope. This makes it harder to assess test coverage gaps at a glance.
**Files:** Most files in `src/components/**/*.test.tsx`, `src/lib/hooks/*.test.ts`
---
### DOC-MIN-2: Performance decisions lack inline rationale
Files like `use-record-query.ts` have several `useMemo` / `useCallback` / `useRef` optimizations with comments referencing ticket numbers but no explanation of the underlying performance problem being solved.
---
### DOC-MIN-3: Accessibility decisions not consolidated
WCAG compliance work is scattered across code comments and the design review document. A single `docs/ACCESSIBILITY.md` checklist would make it easy to see what has been done and what remains.
---
### DOC-MIN-4: Widget creation guide missing
The widget rendering pipeline (`WidgetBlock` → `WidgetRenderer` → specific widget component) is non-obvious. A short guide in `docs/` or a code comment in `WidgetRenderer` explaining how to add a new widget type would help.
---
## Already Excellent
| Area | Quality | Notes |
|------|---------|-------|
| API client (`src/lib/api/`) | 9/10 | 100% JSDoc; error conditions noted; param types clear |
| Utilities (`src/lib/utils/`) | 8.5/10 | `filter-utils`, `process-utils`, `zod-from-metadata` all thorough |
| Sub-hooks (`use-column-config`, `use-saved-views`, `use-async-combobox`) | 9/10 | Return shapes documented; edge cases noted |
| Type definitions (`src/types/metadata.ts`) | 8.5/10 | All major interfaces have field-level JSDoc |
| Core query components (`DataGrid`, `FilterBuilder`, `Pagination`, `ColumnConfig`) | 8/10 | Props and behavior well-described |
| Records components (`FieldValue`, `RecordView`, `EntityForm`) | 7.5/10 | Good headers; adornment priority documented |
| `CLAUDE.md` | 9/10 | Architecture, conventions, file structure all clear |
| Implementation plans (`docs/implementation-plans/`) | 8/10 | Phased clearly; shared context well-organized |
---
## Recommended Work Order
### Phase 1 — Unblock contributors (half day)
1. **DOC-CRIT-1** — Expand README.md
2. **DOC-CRIT-2** — Document `useRecordQuery` return type
### Phase 2 — Improve onboarding (1 day)
3. **DOC-CRIT-3** — Create `docs/STATE-MANAGEMENT.md`
4. **DOC-SIG-1** — Create `docs/COMPONENT-PATTERNS.md`
5. **DOC-SIG-2** — Create `docs/TYPE-GLOSSARY.md`
### Phase 3 — Fill API/validation gaps (half day)
6. **DOC-SIG-3** — API error handling guide
7. **DOC-SIG-4** — Add `@example` to `zodFieldFromMetadata`
8. **DOC-SIG-5** — Create `docs/FORM-VALIDATION.md`
9. **DOC-SIG-6** — Document query key factory
### Phase 4 — Polish (as time allows)
10–13. **DOC-MIN-1 through DOC-MIN-4** — Test headers, perf rationale, a11y checklist, widget guide
-333
View File
@@ -1,333 +0,0 @@
# Form Validation
This guide covers how forms are validated in QQQ Frontend Next. Validation is schema-driven: Zod schemas are generated automatically from the same backend field metadata that drives rendering, so there is no need to maintain a separate validation definition alongside the form.
---
## Overview
The validation stack is:
- **Zod** — runtime schema definition and validation.
- **React Hook Form** + `@hookform/resolvers/zod` — drives form state; the Zod schema is passed as the resolver.
- **`zodFieldFromMetadata`** — the core function that maps a single `QFieldMetaData` to a Zod validator.
- **`zodSchemaFromTableMetadata`** — builds a full `z.ZodObject` from all editable fields in a table.
- **`zodSchemaFromFields`** — builds a `z.ZodObject` from a flat array of fields (used for process steps).
The source for the schema generation utilities is `src/lib/utils/zod-from-metadata.ts`.
---
## Schema generation
### For table-based forms
`EntityForm` (`src/components/forms/EntityForm.tsx`) builds its schema on mount:
```tsx
const schema = useMemo(
() => zodSchemaFromTableMetadata(tableMetaData, fieldNamesToInclude),
[tableMetaData, fieldNamesToInclude]
)
const { register, control, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
defaultValues: mergedDefaults,
})
```
`zodSchemaFromTableMetadata` iterates the table's fields, skips hidden (`isHidden`) and non-editable (`!isEditable`) fields, and calls `zodFieldFromMetadata` for each remaining field:
```tsx
export function zodSchemaFromTableMetadata(
tableMetaData: QTableMetaData,
fieldNamesToInclude?: string[]
): z.ZodObject<Record<string, z.ZodTypeAny>> {
const shape: Record<string, z.ZodTypeAny> = {}
for (const fieldName of fieldsToProcess) {
const field = tableMetaData.fields[fieldName]
if (!field) continue
if (field.isHidden) continue
if (!field.isEditable) continue
shape[fieldName] = zodFieldFromMetadata(field)
}
return z.object(shape)
}
```
When `fieldNamesToInclude` is provided, only those fields are included. This is useful for partial-edit dialogs or multi-step wizards where only a subset of fields are shown per step.
### For process step forms
Process steps receive an ordered array of fields rather than a table metadata object. Use `zodSchemaFromFields`:
```tsx
import { zodSchemaFromFields } from '@/lib/utils/zod-from-metadata'
const schema = zodSchemaFromFields(stepFields) // QFieldMetaData[]
```
Hidden fields are excluded automatically. The resulting schema shape is identical to the table-based variant.
### For single fields
Call `zodFieldFromMetadata` directly when you need the Zod type for a single field:
```tsx
import { zodFieldFromMetadata } from '@/lib/utils/zod-from-metadata'
const fieldSchema = zodFieldFromMetadata(tableMetaData.fields['email'])
```
---
## Field type to Zod type mapping
The table below shows exactly how each QQQ field type is converted. All mappings are implemented in `zodFieldFromMetadata`.
| QQQ field type | Zod type (required) | Zod type (optional) | Notes |
|---|---|---|---|
| `STRING` | `z.string().min(1, ...)` | `z.string().optional()` | `maxLength` adds `.max()` when set |
| `TEXT` | `z.string().min(1, ...)` | `z.string().optional()` | Same as STRING |
| `HTML` | `z.string().min(1, ...)` | `z.string().optional()` | Same as STRING |
| `PASSWORD` | `z.string().min(1, ...)` | `z.string().optional()` | Same as STRING |
| `INTEGER` | `z.coerce.number().int(...)` | `z.union([z.literal(''), z.coerce.number().int(...)]).optional()` | Empty `<input type="number">` submits `""` |
| `LONG` | `z.coerce.number().int(...)` | `z.union([z.literal(''), z.coerce.number().int(...)]).optional()` | Same as INTEGER |
| `DECIMAL` | `z.coerce.number(...)` | `z.union([z.literal(''), z.coerce.number(...)]).optional()` | No `.int()` — allows fractional values |
| `BOOLEAN` | `z.boolean().optional()` | `z.boolean().optional()` | Always optional — checkboxes are never "required" |
| `DATE` | `z.string().min(1, ...)` | `z.string().optional()` | HTML date inputs submit ISO-8601 strings |
| `TIME` | `z.string().min(1, ...)` | `z.string().optional()` | Same as DATE |
| `DATE_TIME` | `z.string().min(1, ...)` | `z.string().optional()` | Same as DATE |
| `BLOB` | `z.union([z.instanceof(File), z.string()]).optional()` | same | Accepts File objects or existing URL strings |
### Why numbers use `z.literal('')` in the optional case
An `<input type="number">` submits an empty string (`""`) when left blank, not `null` or `undefined`. A plain `z.coerce.number()` would coerce `""` to `0`, silently making an empty field appear as zero. The `z.union([z.literal(''), schema])` pattern allows the empty string to pass through as-is so the form can distinguish "nothing entered" from "zero entered".
### Why dates stay as strings
Date and time values come from `<input type="date">` and `<input type="datetime-local">` as ISO-8601 strings (`"2026-03-01"`, `"2026-03-01T14:30"`). The schema validates them as strings. Any coercion to JavaScript `Date` objects — if required by the API — happens in the submit handler, not in the Zod schema.
---
## Required vs optional
The `isRequired` flag on `QFieldMetaData` is the single source of truth for whether a field is required:
- **`isRequired: true`** → `.min(1, "${label} is required")` for string types; plain `z.coerce.number()` for numeric types (empty string fails coercion).
- **`isRequired: false` or absent** → `.optional()` for string types; `z.union([z.literal(''), ...]).optional()` for numeric types.
All error messages use the human-readable `field.label` (not `field.name`), so they read naturally in the UI: `"First Name is required"`, not `"firstName is required"`.
---
## Number bounds
When a field's metadata declares `minValue` or `maxValue`, those bounds are applied to the Zod schema:
```tsx
function buildNumberSchema(isRequired, label, isInteger, minValue, maxValue) {
let schema = isInteger
? z.coerce.number().int(`${label} must be a whole number`)
: z.coerce.number({ message: `${label} must be a number` })
if (minValue !== undefined && minValue !== null) {
schema = schema.min(Number(minValue), `${label} must be at least ${minValue}`)
}
if (maxValue !== undefined && maxValue !== null) {
schema = schema.max(Number(maxValue), `${label} must be at most ${maxValue}`)
}
// ...
}
```
The bounds come directly from metadata — no hardcoded ranges exist in the frontend. `DynamicFormField` also forwards `minValue` and `maxValue` as HTML attributes (`min`, `max`) on `<NumberField>` so the browser provides native range feedback, but Zod is the authoritative validator on submit.
---
## String length constraints
`maxLength` from field metadata maps to Zod's `.max()`:
```tsx
function buildStringSchema(isRequired, label, maxLength) {
let schema = z.string()
if (isRequired) schema = schema.min(1, `${label} is required`)
if (maxLength) schema = schema.max(maxLength, `${label} must be at most ${maxLength} characters`)
return isRequired ? schema : schema.optional()
}
```
`DynamicFormField` also passes `maxLength` to `<TextField>` as an HTML `maxlength` attribute, providing in-browser character limiting, but the Zod schema validates the constraint on submit as well.
---
## Default values
`defaultValuesFromRecord` (`src/lib/utils/zod-from-metadata.ts`) builds the `defaultValues` object for `useForm`:
```tsx
export function defaultValuesFromRecord(
tableMetaData: QTableMetaData,
recordValues: Record<string, unknown>
): Record<string, unknown>
```
For each editable, visible field it applies this priority:
1. Use the record value (from the API response), with type coercion:
- `DATE`, `TIME`, `DATE_TIME` → `String(value)` (ISO-8601 string for the HTML input).
- `BOOLEAN` → `Boolean(value)`.
- All other types → value as-is.
2. Fall back to `field.defaultValue` from metadata.
3. Fall back to `false` for `BOOLEAN` fields.
4. Fall back to `''` (empty string) for everything else.
`EntityForm` calls this inside `useMemo` and passes the result to `useForm({ defaultValues })`. When the record changes (e.g., navigating between records), the form is reset via `reset(defaultValuesFromRecord(...))` in a `useEffect`.
---
## Client vs server validation
### Client-side (Zod)
Zod runs on the client when the user submits the form. React Hook Form calls the Zod resolver, collects all field errors, and populates `formState.errors`. Fields display their error messages inline — `DynamicFormField` renders a `<p role="alert">` with `id="${fieldId}-error"` below each failing input, and the input gains `aria-invalid="true"` and `aria-describedby` pointing at the error element.
```tsx
{fieldError && (
<p id={`${fieldId}-error`} className="mt-1 text-sm text-destructive" role="alert">
{fieldError.message}
</p>
)}
```
### Server-side (API)
The backend can return validation errors after a save attempt. `EntityForm` handles this via TanStack Query mutation `onError` callbacks. Currently, API errors are shown as a top-level error banner rather than per-field:
```tsx
const insertMutation = useMutation({
mutationFn: (values) => insertRecord(tableMetaData.name, values),
onError: (err: Error) => {
toast.error(`Failed to create ${tableMetaData.label}: ${err.message}`)
},
})
```
When the API returns structured per-field errors (a map of `{ fieldName: errorMessage }`), map them to React Hook Form using `form.setError`:
```tsx
onError: (err) => {
const fieldErrors = parseApiFieldErrors(err)
if (fieldErrors) {
for (const [fieldName, message] of Object.entries(fieldErrors)) {
form.setError(fieldName, { type: 'server', message })
}
} else {
toast.error(err.message)
}
}
```
This makes server validation errors appear in the same inline error elements as client-side Zod errors, giving the user a consistent experience.
---
## DynamicFormField dispatch
`DynamicFormField` (`src/components/forms/DynamicFormField.tsx`) is the component that renders a single field inside a metadata-driven form. It dispatches to a typed input component based on field properties in this priority order:
1. **`field.possibleValueSourceName` is set** → `<PossibleValueSelect>` (async combobox).
2. **`FILE_UPLOAD` adornment or `BLOB` type** → `<FileUploadField>`.
3. **`field.type` switch** → one of `TextField`, `NumberField`, `BooleanField`, `DateField`, `DateTimeField`, `PasswordField`, or inline `<textarea>` (TEXT), `<RichTextField>` (HTML), `<input type="time">` (TIME).
4. **Default case** → `<TextField>` (treats unknown types as plain text).
Hidden fields (`isHidden: true`) and non-editable fields return `null` immediately.
### Dirty field indicator
When a field's value has changed from its default, `DynamicFormField` wraps the input in a left-border accent:
```tsx
function DirtyWrapper({ isDirty, children }) {
if (!isDirty) return <>{children}</>
return (
<div className="border-l-2 border-primary pl-2">
{children}
</div>
)
}
```
The `isDirty` prop comes from `formState.dirtyFields[fieldName]` in `EntityForm`, which passes the full `dirtyFields` map to `DynamicForm`, which passes each field's individual dirty state to `DynamicFormField`.
---
## Adding custom Zod refinements
The generated schemas cover all constraints that can be expressed in QQQ field metadata. When you need a validation rule that metadata cannot express (for example, cross-field validation or a format check), add a `.superRefine()` or `.refine()` on the schema returned by `zodSchemaFromTableMetadata`:
```tsx
// In the component that builds the form
const baseSchema = zodSchemaFromTableMetadata(tableMetaData)
const schema = baseSchema.superRefine((values, ctx) => {
// Cross-field: end date must be after start date
if (values.startDate && values.endDate && values.endDate < values.startDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['endDate'],
message: 'End date must be after start date',
})
}
})
```
Prefer `.superRefine()` over `.refine()` when you need to report the error on a specific field path rather than at the root of the object, so React Hook Form can display it inline next to the correct input.
---
## Complete example
### Field metadata (from the API)
```json
{
"name": "quantity",
"label": "Quantity",
"type": "INTEGER",
"isRequired": true,
"isEditable": true,
"isHidden": false,
"minValue": 1,
"maxValue": 9999
}
```
### Generated Zod schema for this field
`zodFieldFromMetadata` produces:
```ts
z.coerce.number()
.int("Quantity must be a whole number")
.min(1, "Quantity must be at least 1")
.max(9999, "Quantity must be at most 9999")
```
Because `isRequired` is `true`, this is the final schema (no `z.literal('')` union wrapping). An empty `<input type="number">` submits `""`, `z.coerce.number()` coerces that to `NaN`, and `NaN` fails the `.int()` check — so the required constraint is enforced implicitly.
### Full table schema (example)
```ts
const schema = zodSchemaFromTableMetadata(orderTableMetaData)
// Produces:
z.object({
customerId: z.string().min(1, 'Customer is required'), // STRING, required
quantity: z.coerce.number().int(...).min(1).max(9999), // INTEGER, required
notes: z.string().optional(), // TEXT, optional
shipDate: z.string().optional(), // DATE, optional
isRush: z.boolean().optional(), // BOOLEAN, always optional
})
```
-21
View File
@@ -1,21 +0,0 @@
# Help everywhere — implementation and verification
Backend: QRun-IO/qqq#1177 (`docs/HELP-CONTENT.md`).
## Implementation
- Vendored the supplied help-core `openapi.yaml` as the existing one-space-indented JSON document. The prior vendoring commits are `01dd7e9` and `8a0fbbd`; neither contains a generator script. Conversion uses the installed ESLint dependency `js-yaml`, then `JSON.stringify(document, null, 1)` with a final newline. The full core document is retained, including its other changes within UI API 1.2.0.
- Added `summary` to `QHelpContent`, reused that type for widget slots, and added app/process/report slot maps. `src/lib/api/help.ts` uses the typed API client and checks the advertised `help` feature before calling search. No off-spec exception was added. Search and coverage are optional spec operations; coverage is not called.
- `src/components/help/HelpIcon.tsx` selects role-specific help, shows the plain summary, and opens the shared long-help panel by click, Enter, or Space. Existing customization IDs are retained on the corresponding control or tooltip. Input descriptions remain available while the tooltip is closed.
- `HelpPanelProvider` is mounted in the authenticated dashboard. One `next/dynamic` panel renders sanitized long content using the existing `HelpContent` renderer, with related help, safe navigation, Escape dismissal, focus trapping, and return focus. Help controls use theme tokens and coarse-pointer touch targets.
- Existing field, section, grid-column, widget-title, widget-column, and process-step help now uses the shared icon. New placements cover app, table query/view/edit/create/copy, process, report, and record/table action headers or labels. Action help is a sibling of the action control; Radix menu help participates in roving focus.
- `HelpSearchResults` provides 200 ms debouncing, a two-character minimum, feature gating, and loading/error/empty states. Both `CommandMenu` and the global `SearchPage` use it. The page's existing grouped-results area is a natural place for help; the compact header/search-dialog listbox retains its current record/navigation model. The palette also offers **Search help…**.
- Author mode retains key suffixes. **Add help** / **Edit help** links require the core table's insert permission and use `/app/helpContent/create#/defaultValues=…` with `key` and the most specific `role`. `HELP_CONTENT_TABLE` is the one production constant for that core-defined table.
## Tests and fixtures
- New Vitest tests cover the API gate, role precedence, summaries, keyboard controls, author URL presets and permission checks, panel sanitization/focus, and palette help search.
- `tests/acceptance/fixture/HelpFixtures.java` defines synthetic Help Lab metadata, Markdown bodies and distinct summaries, an owned data table, and the core authoring table. It is wired into the normal define/reset lifecycle.
- `tests/acceptance/specs/help/help.spec.ts` and `matrix/help.json` add HLP-001 through HLP-006, light/dark cases, mobile tags, and 320/1920-pixel panel checks. Column/table-menu coverage remains desktop-only because mobile query results use cards. The area is registered in `acceptance-select.mjs`.
- Existing REC-039/040, PRC-015, and widget-column assertions now focus the help button and inspect formatted long content in the panel. This is necessary because tooltips now contain plain summaries and touch opens the panel. Existing scenarios and matrix requirements remain; none was waived.
- Historical results in `docs/acceptance/feature-matrix.md` are preserved. The new help rows are appended as **not run**.
-29
View File
@@ -1,29 +0,0 @@
# Isolated scripting editor
ScriptViewer routes explicit server-controlled ISOLATED projects before legacy hooks mount. Unknown nonempty modes fail closed. The native scripting extension must be advertised; otherwise isolated source stays unavailable. Developer page provides semantic project creation and opens the same IDE; project names never imply execution mode or actor identity.
The isolated CodeMirror editor supports multi-file TypeScript, immutable source history, explicit save/build/test/publish, server compiler diagnostics, scoped API declarations, JSON context tests and exact-commit GitHub imports with retained provenance. Edits invalidate build/test evidence. Saves and imports carry the observed latest revision. No browser credential fields or background repository synchronization.
## Semantic language service
Pinned editor-only typescript-editor alias6.0.3 powers a same-origin classic worker. It receives a bounded virtual filesystem and host-generated SDK declarations, with57 trusted ES2022 standard-library files bundled at build time. Diagnostics, completion, hover, go-to-definition(F12), formatting(Ctrl+Shift+F), file navigation and compiler diagnostic selection operate on the same workspace. Text remains ordinary undoable editor state. Tab moves focus.
There is no source execution, compiler emit, runtime fetch, arbitrary package loading, filesystem, DOM/Node declarations or user npm hooks. Browser analysis is advisory; authoritative strict builds still use the qualified isolated server. Worker cold deadline10s/warm3s terminates it on timeout; bounded request/result sizes, four pending calls, idle30s termination and recycling after200 requests limit work. The next explicit request may start a new worker; no hidden retry of timed-out source analysis. A browser worker supplies termination responsiveness, not an OS-enforced memory sandbox. Debugging and automatic imports are not implemented.
scripts/build-scripting-worker.mjs assembles only trusted package/compiler and our own session source into public/scripting-generated/typescript-6.0.3.worker.js, with license/notices. Generated9.6MB artifact is ignored and built before dev/build/build:export. Runtime worker requests no other resource. Exact compiler and library dependencies remain independent from server runtime qualification.
The alias package also exports tsc and would shadow the existing compiler under pnpm exec tsc. Use **pnpm typecheck** or **pnpm tsc**: both explicitly call node_modules/typescript/bin/tsc (the existing5.9 build compiler). Existing CI typecheck scripts follow that path. Do not replace frontend build TypeScript with the editor compiler. Compatible CodeMirror versions are pinned through pnpm overrides to avoid duplicate nominal EditorView/state classes.
## Authorization and uncertainty
Server permissions and worker availability control lifecycle actions, with backend checks authoritative. Use the matching Core `4.1.0-SNAPSHOT` dependency set described in [current integration status](QBIT-RELEASE-STATUS.md); the old mixed private-artifact/RC20 setup is superseded. Project creation and publication use typed core authority; isolated revision/file content and ownership are immutable. Real native HTTP lifecycle succeeds with protocol-only fixture worker; no production provider is implied.
Stop waiting or network timeout makes mutation outcome uncertain and prevents another mutation. History refresh does not prove completion; inspect history/reopen. Creation uncertainty directs the user to Scripts before retrying. Abort does not undo committed work. GitHub import needs a server-installed managed access provider and full immutable commit.
## Verification
2026-10-30: **114 focused tests across12 files**, existing build-compiler typecheck and changed-file ESLint passed. Real Playwright Chromium worker check exercises strict diagnostics, multi-file completion/hover/definition/format, rejects remote types and proves source does not execute or fetch. Final optimized production build with typecheck, static generation and standalone packaging passed at 21:36, including the creation panel (parent run20209, editor-build-verification.log). Native server tests separately cover create/save/build/context-test/publish and permission/association/legacy denials. No live browser-to-deployed-worker end-to-end or production sandbox qualification claim.
Run pnpm scripting:worker:check after generating the worker; this environment uses PLAYWRIGHT_BROWSERS_PATH=C:/Users/james/AppData/Local/ms-playwright. The harness serves only a local trusted page/worker with connect-src none and closes browser/server on completion.
Current integration verification (2026-11-04): the real Chromium offline-worker check passes strict diagnostics, multi-file completion/hover/definition and formatting, with no source execution or external requests. Typecheck, lint and production worker packaging pass on the canonical integration branch. See [current status](QBIT-RELEASE-STATUS.md) for the final full-suite evidence. This frontend result does not qualify the server's untrusted-code runtime.
-61
View File
@@ -1,61 +0,0 @@
# Next UI issue tracking
GitHub issues are the source of truth for scope, current status, remaining work and release decisions. Start at [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713), which links the release baseline, known failures, visual review, external-provider checks and existing feature tickets.
## Where work lives
| Area | Authoritative issue |
|---|---|
| Roadmap and complete issue index | [qqq#713](https://github.com/QRun-IO/qqq/issues/713) |
| Feature acceptance and default adoption | [qqq#649](https://github.com/QRun-IO/qqq/issues/649) |
| Behavior and metadata compatibility | [qqq#714](https://github.com/QRun-IO/qqq/issues/714) |
| Original Next visual review and owner approval | [qqq#711](https://github.com/QRun-IO/qqq/issues/711) |
| Browser failures and incomplete run evidence | [qqq#985](https://github.com/QRun-IO/qqq/issues/985) |
| Real application soak and final release hold | [qqq#712](https://github.com/QRun-IO/qqq/issues/712) |
| Cross-application launch/restart acceptance | [qqq#650](https://github.com/QRun-IO/qqq/issues/650) |
| ESB integration and acceptance | [qqq#986](https://github.com/QRun-IO/qqq/issues/986) |
| Paired runtime provenance and CI handoff | [qqq#905](https://github.com/QRun-IO/qqq/issues/905) |
Create **all Next UI tickets in [QRun-IO/qqq](https://github.com/QRun-IO/qqq/issues)** and add them to the [QQQ Roadmap, Project 12](https://github.com/orgs/QRun-IO/projects/12). Use existing issues for the same requirement or failure, and link each focused ticket to #713 and its feature or investigation parent. Frontend issues #12 and #10 were transferred to central issues #985 and #986; use the central identifiers and preserve their report history.
## Required ticket fields
Every new ticket needs these fields on the issue and its Project 12 item; complete them before work advances to **Ready**:
| Field | Required value |
|---|---|
| Issue Type | Native issue: Bug, Feature, Task or Epic. Mirror it in the Project field, using Planning Epic for Epic; Improvement is a Project-only category. |
| Assignees | The person accountable for completing or coordinating the ticket |
| Milestone | The agreed delivery milestone; keep it consistent with the target version |
| Priority | Critical, High, Medium or Low, with an impact-based rationale in the issue |
| Component | The owning implementation component: `qqq-frontend-next` for Next source, or the actual backend/sample component for a dependency. |
| Target Version | The agreed version of the owning component or an explicit pending release decision. A backend dependency in the Next UI 1.0 milestone does not imply a backend 1.0 release. Never imply delivery in an already published RC. |
| Status | The current workflow state below |
Record unresolved ownership or release decisions explicitly while a ticket remains in **Backlog**. Do not invent dates, estimates or a new release version to fill a field. A browser assertion failure alone does not establish high product severity.
## Workflow and evidence
- **Backlog / Ready:** capture the expected behavior, scope, acceptance criteria, required fields and dependencies; Ready means actionable work with an owner.
- **In progress:** link the active branch or work and record reproduction, confirmed cause, implementation progress and remaining blockers.
- **In review:** link the reviewable PR, source commit and verification evidence; keep unmet acceptance criteria visible. A local fix or passing subset does not imply merge or release.
- **Done:** close only after the accepted fix is integrated and every acceptance criterion is verified, or an explicit approved disposition is recorded. Link the merged PR/commit and evidence. Identify whether the change is unreleased or delivered, with the version and release link when published.
## Updating an issue
- Put current status and remaining acceptance criteria at the top. Preserve the original report as history.
- Record expected/actual behavior, reproduction, environment, source commit, run and artifact evidence. State whether the cause is established.
- Separate implementation evidence, passing scoped tests, owner visual approval and final release approval. They are distinct requirements.
- Resolve all original acceptance criteria before closing, or document an explicit approved disposition. A later passing run alone does not explain an intermittent failure.
- Keep external-provider exclusions visible. Controlled provider fixtures do not prove a real Auth0 tenant, Google account or QuickSight dashboard.
- Update issue links after a fix or new finding. Repository plans, session notes, release notes and test/visual ledgers support the issues; they are not competing live backlogs.
## Release and design constraints
Preserve the original Next design at `e42ad2b2bcdc76311e13002a22dd70e3ec437192`. Material Dashboard is the behavior and metadata compatibility reference. Prioritize primary-browser functional depth and disclose secondary-browser failures.
[RC9](releases/1.0.0-RC.9.md) is the published prerelease for application testing. Its immutable-tag acceptance run has known Firefox/tablet failures; consult the release notes and #985 for the exact evidence. The browser fixes in [PR #18](https://github.com/QRun-IO/qqq-frontend-next/pull/18) are [unreleased](releases/unreleased.md).
Every release and changelog entry must link its central QQQ issues and distinguish product changes, test/infrastructure corrections, verification and unresolved work. A published RC's tag and artifacts remain immutable; subsequent fixes require a separately authorized version and release. Update tickets and Project 12 with the actual delivery version and release link after publication.
Final 1.0 remains held for the owner's requested application soak and renewed, explicit final approval ([#712](https://github.com/QRun-IO/qqq/issues/712)). Passing CI, an issue marked Done or elapsed time does not release that hold. This project does not authorize releasing QQQ 4.1.
-47
View File
@@ -1,47 +0,0 @@
# PDF preview boundary
The generic file lightbox uses pinned Mozilla `pdfjs-dist` **6.4.299**, loaded only for
PDFs, and a locally packaged worker. It no longer embeds uploaded PDF responses in an
application-origin iframe or offers a PDF "Open original" navigation. Downloads retain
the existing owner authorization callback.
Retained attachment previews reach it only after the tracked-download helper verifies
server receipt, byte length and SHA-256, and assigns `application/pdf` from the fixed
PDF signature. The generic renderer accepts local same-origin blob URLs or native
file endpoints at the exact configured API origin and base path. Both dashboard hosts must permit `blob:` in CSP `connect-src` so the verified local bytes can be read; the current Javalin and standalone defaults include it. It never forwards API
credentials to arbitrary external URLs. Uploaded MIME labels alone are insufficient.
The renderer supplies bytes (not a document URL) to an explicitly created parser
Worker, preventing the library's fake-worker fallback. It creates a canvas and an
optional plain-text view only: no PDF annotation/action, form, XFA, scripting or HTML
viewer layer. Auxiliary resource requests are rejected, worker fetching and WASM are
disabled, and no document-selected external URL is followed. Missing auxiliary fonts
or unsupported resources can limit fidelity; the retained original is downloadable.
Limits: 20 MiB input, 1000 pages, one visible page at a time, 8 million canvas pixels,
8192 pixels per edge, 30 seconds to load/parse, 15 seconds per page render/text read.
Text is streamed and stopped at 10,000 items or 100,000 characters, slicing each
string before aggregation. The stream is cancelled on page change, close or timeout;
page controls remain busy through text completion.
Closing the dialog aborts input loading, cancels rendering, destroys the loading task
and terminates the worker. These are application limits, not a browser-process memory
quota: compressed PDF complexity can allocate parser memory before page checks.
Canvas rendering and a Web Worker do **not** constitute a full hostile-parser security
sandbox. Browser/library patching, CSP and deployment qualification remain necessary.
Primary references checked on 2026-10-30:
- [Mozilla release 6.4.299](https://github.com/mozilla/pdf.js/releases/tag/v6.4.299)
- [Mozilla PDF.js API](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html)
- [Mozilla prior arbitrary-JavaScript advisory](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq)
The prior advisory affected versions through 4.1.392 and was fixed in 4.2.67. The current
6.4.299 API no longer exposes the old `isEvalSupported` option; this integration does
not pretend an obsolete option supplies isolation. It uses the pinned current release,
no PDF scripting manager, and the application's restrictive script CSP.
## Current verification (2026-11-04)
The production export and local Maven JAR include the pinned worker. Twelve Chromium browser scenarios pass against that packaged UI, including actual PDF worker/canvas pixels, page navigation, no executed PDF OpenAction, separate retained preview/download receipts, refusal of incomplete receipts, and ordered-file submission. Real Core/Collaboration API sessions separately pass ordinary signed-PDF canvas preview, page controls and PDF download.
Signed attachment PDFs are read with credentials omitted and redirects refused, bounded to 20 MiB and 30 seconds, and checked by byte signature before a local preview URL is exposed. Their storage origin needs host CSP/CORS configuration. Tracked deliveries retain the receipt/hash path. Cancellation and streaming-limit tests cover both paths. See [current status](QBIT-RELEASE-STATUS.md) for the full suite and [release requirements](QBIT-KNOWN-ISSUES.md) for remaining deployment qualifications.
-28
View File
@@ -1,28 +0,0 @@
# PLAN: Next UI complete feature acceptance and default adoption (QRun-IO/qqq#649)
## Goal
Make Next the fully tested, working default QQQ admin UI: every inventoried supported
feature has executable acceptance coverage that passes against the real sample backend and
a production Next build, and every documented default launch path starts Next.
## Approach
1. Inventory UI-facing features from source (sample metadata, backend enums/routes, Material
behavior) into `docs/acceptance/feature-matrix.md`; each row names its fixture, scenarios,
test IDs, result and issue links.
2. Add a Playwright acceptance project (`tests/acceptance/`) that boots the QRun-owned sample
JAR on an ephemeral loopback port and serves `next build` output with `next start`, using
same-origin forwarding. Tests carry matrix IDs; `scripts/acceptance-gate.mjs` fails when a
required row has no passing test or a test is skipped/flaky.
3. Fix demonstrated defects with regression tests (#550, #645, #538, #539, #541, others found),
filing or reusing a QQQ issue for each.
4. Update default entry points (quickstart, sample README, root README, website, wiki,
starter/template) while keeping Material selectable.
5. Run all gates, publish versioned artifacts through normal workflows, record evidence.
## Branches
- `qqq-frontend-next`: `feature/GH-649-next-acceptance` (from `main`)
- `qqq`: worktree `../qqq-649`, branch `feature/GH-649-next-default` (from `origin/develop`)
## Open questions (for James)
- Commits, pushes, PRs, image/tag publication and dependency changes need explicit approval
per repository rules; work proceeds locally until then.
-22
View File
@@ -1,22 +0,0 @@
# Plan: API playground contrast (#724)
## Reproduction and scope
Fresh, fingerprinted desktop captures preserve the original Next composition. RapiDoc's Expand all text in both the original design and RC6 renders RGB(29, 78, 216) on RGB(42, 43, 44), a contrast ratio of 2.12:1 at 16px normal weight. This inherited defect fails the 4.5:1 requirement. Browser regression checks also found light-mode contact-link contrast of 2.36:1 and authentication-status contrast of 3.00:1.
## Implementation and verification
- [x] Retain failing browser contrast checks inside RapiDoc's shadow DOM.
- [x] Apply existing dashboard primary-text and error tokens at the component boundary. Pair dark accent backgrounds with inverse button text. Preserve layout and dependencies.
- [x] Verify normal and hovered controls in light/dark Chromium desktop and mobile, plus live Preferences theme switching.
- [x] Run types, lint, production export, all 1,901 unit tests and existing bundle budgets.
- [x] Directly inspect four fresh screenshots after color transitions settle.
- [x] Independent review approved with no remaining findings; prepare signed isolated commit.
Evidence: `test-results/visual-review/api-playground-contrast/` in the original checkout. Final focused report: four passed, zero failures, skips, flaky outcomes or report errors. These are targeted checks, not a complete acceptance gate. Initial toggle verification incorrectly asserted an HTML attribute: React 19 sets RapiDoc's non-reflected Lit property. A subsequent audit sampled active color transitions. Both runs are retained; the corrected test awaits the property, Lit render and actual shadow-root animations without fixed sleeps.
## Remaining limits
This proves the fixture's visible playground content, hover states and theme switching, not all expanded endpoint/authentication states or arbitrary custom palettes. The existing narrow-screen inner horizontal clipping remains open; this color correction does not redesign the playground.
This branch is based on RC6 source `7ddf45f` and excludes the separate TIME fix `276b65b`. Neither follow-up is part of the immutable RC6 tag. Final 1.0 remains on hold.
-31
View File
@@ -1,31 +0,0 @@
# PLAN: Next UI 1.0 query filter parity
## Goal
Complete the supported Material Dashboard query-filter workflows in QRun-IO/qqq#715 and #718, and prove them with real-backend acceptance rows.
## Approach
Build on the preserved wave 2 worktree. Keep one `QQueryFilter` as the source of truth for basic and advanced modes, with the existing `useRecordQuery` state and saved-view serialization. Share the existing metadata-driven operator and value controls between quick-filter chips and the advanced builder. Use the v1 metadata settings from backend PR #787 for weekday options and help.
## Files affected
- `src/components/query/RecordQuery.tsx`, `RecordQueryToolbar.tsx`: mode switch, quick filters, sort, clear confirmation, advanced preview.
- `src/components/query/QuickFilterBar.tsx`: basic-mode chips, field picker, operator/value editor and accessible clear/remove actions.
- `src/components/query/FilterBuilder.tsx` and its extracted helpers: complete advanced operator and value behavior from #718.
- `src/lib/utils/quick-filter-utils.ts`, `filter-utils.ts`, `saved-view-utils.ts`, `src/lib/hooks/use-record-query.ts`: metadata defaults, criteria edits, mode reconciliation, persistence and view round-trips.
- `tests/acceptance/matrix/query.json` and `tests/acceptance/specs/query/`: real-backend evidence for every applicable #715/#718 behavior.
## Steps
1. [x] Add meaningful unit tests for quick-filter defaults, complex-filter rejection, edits, weekday operators, date expressions and saved-view round-trips; finish any missing utility behavior.
2. [x] Wire instance filter settings and add the basic/advanced switch with a reason tooltip when basic cannot represent the current filter.
3. [x] Build and test basic quick-filter chips, add-field menu, operator/value editing, clear/remove behavior and advanced preview.
4. [x] Add sort picker, clear-all confirmation that preserves sort, and per-row validation; cover keyboard and touch behavior.
5. [ ] Add real-backend acceptance rows and tests, then run typecheck, lint, unit, static-export budget and the five-project browser gate after integration.
## Constraints
- Render field labels from metadata and preserve readable join and virtual-field behavior.
- Keep the filters and saved views interoperable with Material Dashboard JSON.
- Preserve unrelated wave 2 worktrees. Do not release QQQ 4.1.
-11
View File
@@ -1,11 +0,0 @@
# Script completion compatibility
Goal: close the autocomplete gap in the parity ledger while retaining the original Next textarea editor and its keyboard accessibility. Authorized by the active Next UI 1.0 completion goal.
- Preserve syntax overlay, split panes, per-file language, undo, read-only behavior and Tab escape.
- Opt script editors into a caret-adjacent suggestion list: QQQ API/logger methods, current-file identifiers, and highlighter language keywords. No new dependency or backend contract.
- Typing a prefix or Ctrl+Space opens suggestions; arrows select, Enter/Tab insert, Escape dismisses; pointer selection retains textarea focus. Composition input must not be intercepted.
- Replace the current token at the caret, preserve surrounding text, and avoid duplicating an existing call parenthesis.
- Verify unit regressions, real-backend script edit/test/save/undo behavior, focus/keyboard and visual states. Update guides and ledger only to the extent proven.
Verified: baseline Chromium 95847c2 passed 556 tests; backend PR #913 merged. Autocomplete passes 1,840 unit tests, types, affected lint, production export and bundle budgets; 15 Chromium developer/script workflows and 10 focused cases across all five profiles. Both review findings fixed after failing regressions. A native textarea probe explained WebKit undo grouping; completion now has a loaded-revision undo/redo test without changing native history behavior. Light/dark screenshots inspected; whole-family visual review remains open.
-17
View File
@@ -1,17 +0,0 @@
# Sidebar icon label assertion (#728)
## Evidence
RC6 candidate WebKit failed NAV-002 because raw link text included decorative legacy icon codepoints. The same assertion was reproduced against the public RC6 JAR after explicitly awaiting the fixture's legacy glyph. This is a test assertion defect: accessible link names should match metadata labels, while decorative SVG text remains hidden from assistive technology.
## Correction
Await the legacy glyph, then assert exact link count and each accessible label in backend order. Check that rendered legacy SVGs remain `aria-hidden`. Retain nested-app, parent expansion and child label assertions. No product code or release artifact changes.
## Verification
Retain red and focused Chromium/WebKit/mobile reports under `test-results/visual-review/sidebar-icon-labels/` in the original checkout. The focused check exercises the actual public RC6 JAR with backend `e0d57dd`. Targeted ESLint and diff checks are required; independent review precedes integration. Original CI failure remains recorded, not erased by a later focused pass.
Focused result: three passed, zero failures, skips, flaky outcomes or report errors. Targeted ESLint and diff checks passed.
Independent review approved with no findings. This change verifies navigation semantics; glyph appearance remains covered by the separate legacy-icon workflows.
-10
View File
@@ -1,10 +0,0 @@
# Plan: TIME block seconds compatibility (#728)
Preserve Next layout while making standalone and process TIME inputs accept and submit seconds, matching the shared form editor and QQQ TIME values.
1. Reproduce native step validation failure with standalone and process tests; retain a regular form control.
2. Apply the existing one-second input step to both plain block paths.
3. Verify edited/seeded values through actual Chromium workflows, including backend process readback; run affected tests and relevant quality gates.
4. Keep this follow-up separate from the unchanged RC6 candidate while its hosted CI runs.
Verification: two focused component regressions failed before the fix (regular form control passed); three real-backend Chromium scenarios failed at native step assertions. After the two-line correction, all 66 affected component tests and eight desktop/mobile backend workflows passed, including independent SQL checks. Full unit suite, type/lint, production build and budgets passed. This is isolated follow-up work, not part of RC6 candidate `7ddf45f`.
-13
View File
@@ -1,13 +0,0 @@
# Metadata-driven process result fields
`ViewFormComponent` delegates adorned result fields to the shared `FieldValue` component. Hidden fields remain in the result value map so native metadata can supply display companions; they are not rendered as separate controls. Ordinary process values and validation errors keep their existing behavior.
`LINK` supports either `toRecordFromTable` (a static table name) or `toRecordFromTableField` (the name of another result field containing the table name). The field value is the record identity. Navigation uses the native encoded table/record route. A missing companion table produces no link. Possible-value sources that identify a known instance table also retain native record navigation.
`FILE_DOWNLOAD` can specify `fileNameField` and `deliveryModeField`. If the latter is present, its companion value must be `SIGNED_URL` or `AUTHENTICATED_FETCH`; missing or unknown modes render no access action. Older fields without that metadata retain their existing behavior.
For `AUTHENTICATED_FETCH`, the issued URL is never placed in an `href`, image source or iframe. Download uses the shared authenticated transfer helper, which checks the configured origin, response bounds, exact digest/length and completed durable receipt. An additional `FILE_PREVIEW` adornment enables an explicit preview action. Only verified PDF/raster blobs reach the shared preview component; component-owned blob URLs are revoked when closed or unmounted. Unsupported content and transfer/audit errors remain visible and never fall back to direct access.
`CODE_EDITOR` result fields use the shared read-only source display, including renderer-supplied plain catalog help and HTML/JSON examples. No template or script evaluation occurs in a process result.
Verification for this increment: 178 focused tests across 18 files, owned ESLint and the explicit build-compiler typecheck passed. Each actual transfer creates a fresh correlation UUID only after validating the configured endpoint, preserving the immutable file coordinate and using that same fresh attempt for its receipt. Source changes and unmount abort active body/receipt work. Final integrated production-build/browser acceptance remains pending. These generic components do not embed a QBit name or depend on its backend implementation.
-106
View File
@@ -1,106 +0,0 @@
# QA capture (Jam)
Optional QA bug capture for the Next UI. The backend is the `qbit-qa-capture` QBit, which publishes the `qa-capture` UI API extension (1.0.0). [Jam](https://jam.dev) is the first provider. The UI calls only the two operations in [`src/lib/api/openapi/extensions/qa-capture.json`](../src/lib/api/openapi/extensions/qa-capture.json):
| Operation | Call | Used for |
|---|---|---|
| `qaCaptureCapability` | `GET /qqq/v1/qbit/qa-capture/capability` | Whether capture is on for the signed-in user, and the Jam team, sanitizer rules, blur selectors and backend context |
| `qaCaptureReportCreate` | `POST /qqq/v1/qbit/qa-capture/report` | "Report a bug": creates a report and returns the Recording Link `jam-*` parameters |
The vendored document is hand-written from the shared contract until the QBit publishes its generated `openapi/qqq-v1/qa-capture.yaml`. Re-vendor it from the QBit before release.
## When anything loads
Capture is off unless all of the following hold. When it is off, the UI loads no Jam chunk, adds no Jam meta tags and makes no Jam request:
1. `GET /qqq/v1/version` lists the `qa-capture` extension. Otherwise the capability is never requested.
2. The user is signed in. `QaCaptureLoader` in the root providers then requests the capability once (TanStack Query, `['qqq', 'qa-capture', 'capability']`).
3. The capability has `enabled: true`, provider `jam`, a team id and sanitizer rules. Anything else counts as disabled.
When capture is enabled, `src/lib/qa-capture/jam-loader.ts` (the only module that imports Jam, and only through `import()`) does the following:
- adds `<meta name="jam:team">` and `<meta name="jam:blur">` (the server's `blurSelectors` plus `[data-qqq-secure]`);
- dynamic-imports `@jam.dev/recording-links/sdk` (0.4.0) and `@jam.dev/sdk` (1.0.1);
- calls `jam.initialize({ teamId, blurSelectors, network, console })` once, with the sanitizers;
- registers `jam.metadata(...)`.
A Vitest check fails if any other file imports Jam at runtime.
What the SDKs fetch was checked in their published source. `initialize` loads `https://js.jam.dev/recorder.js` only in two cases: the page URL has `jam-recording` (checked on load and on every `pushState`, `replaceState` or `popstate`), or `loadRecorder()` is called. It loads `https://js.jam.dev/capture.js` only while some tab has an active recorder, which it tracks with the `jam:numRecorders` localStorage counter. `jam.metadata` only stores a callback on `window.__jam__`.
The browser cannot undo the Jam SDK, so turning capture off for a user (QA mode off) takes effect on the next page load. Request-id collection stops immediately.
## QA mode
When `capability.qaMode.preferenceSection` is set, the Preferences page renders that section like any other. Saving it refetches the capability, so turning QA mode on loads Jam without a reload. While `reason` is `QA_MODE`, the header shows a "QA mode on" badge that links to the section.
## Report a bug
The user menu shows **Report a bug** when `canReport` is true, and **QA reports** when `reportsHref` is an in-app path. The UI follows that link as given and never builds table paths itself. The Next UI has no help menu (the header help button opens the keyboard shortcuts), so both entries are in the user menu only.
Report a bug sends `POST /report` with:
- the page URL, with the sanitizer `queryParamNames`, the OAuth `code`, `state` and token parameters, and every `jam-*` parameter removed;
- the title `Bug: <page title>`;
- the app, table, record id or process, resolved from `/app/<slug>[/<id>[/<process>]]` against the instance metadata;
- the last request ids;
- the frontend version, user agent and viewport.
When the response has `recordingParams`, the UI opens the recorder with `await jam.loadRecorder({ openImmediately: false })` and then `Recorder.open(params['jam-recording'], { jamParams: { 'jam-title', 'jam-reference', ... } })`. The SDK puts those parameters on the URL while recording and removes them when the recorder closes, which lets the QBit's `jam.created` webhook match the capture to the report by `jam-reference`.
Without `recordingParams`, a toast explains that an administrator must set the Recording Link in the QA capture settings. It includes a QA reports action when `reportsHref` is present.
## Sanitizers
`src/lib/qa-capture/sanitizers.ts` builds the four Jam sanitizers from `capability.sanitizer`. They are pure, synchronous and deterministic, and they never log or fetch. The server's rules always get a baseline added: the `authorization`, `proxy-authorization`, `cookie` and `set-cookie` headers; the `password`, `sessionUUID`, `accessToken`, `refreshToken`, `idToken`, `clientSecret` and `code_verifier` fields; the `code`, `state`, `access_token` and `id_token` parameters; and JWTs.
| Data | Rule |
|---|---|
| Request and response headers | `headerNames` (lowercase) are deleted |
| JSON bodies | values of `bodyFieldNames` keys are replaced at any depth (case-insensitive); `valuePatterns` are applied to every string; the body stays valid JSON |
| Form-encoded bodies | the same, field by field |
| Other text bodies, WebSocket data | `valuePatterns` |
| Request URLs | `queryParamNames` values are redacted; `valuePatterns` are applied |
| Same-origin paths under `dropPathPrefixes` | request and response bodies are dropped |
| Console arguments | object keys and string patterns as for JSON |
A `valuePatterns` entry that is not a valid RegExp makes the affected sanitizers throw. Jam then redacts that data instead of sending it (fail closed).
## Secure fields
The UI knows a value is secure from its field metadata:
- the field type is `PASSWORD`;
- the field has the `REVEAL` adornment;
- secured data restricts the field (`accessTier` is `MASKED` or `REVEALABLE`).
Elements that show or edit such a value carry `data-jam-blur` and `data-qqq-secure`. This covers `MaskedValue`, a revealed restricted value, grid password and reveal cells, record view reveal and password values, password inputs (including while shown as text) and secure text inputs. The attributes are always present and have no effect when capture is off.
## Request ids
While capture is enabled, an axios interceptor keeps the last 50 `{ id, method, path, status, at }` entries from the response header named by `capability.requestIdHeader` (default `X-Request-Id`). The path is stored without its query string. They go into the Jam metadata and the report. Disabling capture clears the buffer.
## Jam metadata
At capture time Jam receives the following, kept under 10 KB by dropping the oldest request ids first:
- the app, table, record id and process;
- the frontend version (`NEXT_PUBLIC_APP_VERSION`);
- the backend `environment`, `backendVersion` and `node` from the capability;
- the request ids;
- the report reference;
- the page path, filtered as above.
## Recording Links and sign-in
A Recording Link can open a protected page as `?jam-recording=...&jam-title=...`. The login redirect, the OIDC/OAuth2 callback and the 401 redirect already carry the query in `returnTo`. Every page load also stashes the `jam-*` parameters in sessionStorage. After sign-in they are put back on the URL with `history.replaceState`, unless the page already has a `jam-recording`. The stash expires after 30 minutes. The Jam SDK watches `replaceState`, so the recorder then opens.
The public `/qa/recorder` page for Jam domain verification is served by the QBit, not by Next. The standalone server forwards `/qa/*` to the backend.
## Content-Security-Policy
Next adds no CSP of its own. In a jar deployment the dashboard CSP comes from the QQQ server (`NextDashboardSecurityHeaders`), and the QBit or application must add the Jam origins listed in `capability.cspOrigins`. The standalone container server (`standalone/qqq-server.mjs`) adds them only when `QQQ_QA_CAPTURE_CSP=true`:
- `script-src https://js.jam.dev`
- `frame-src https://*.jam.dev`
- `connect-src https://*.jam.dev`
-11
View File
@@ -1,11 +0,0 @@
# QBit integration known issues and release requirements
Updated: 2026-11-05.
- The Core, Collaboration and Next UI changes are verified source branches and local snapshots, not a published dependency set. Public Core RC.21 / Collaboration RC.6 lack the required retention contracts. Merge and publication through the existing release process remain separate from local verification.
- The frontend checks include real Core/Collaboration sessions and file storage, plus browser protocol fixtures for retained receipts. They do not establish end-to-end document generation/printing or deployed untrusted-script execution. Those QBits and workers retain their own qualification requirements.
- External signed PDF storage needs CORS for the dashboard origin and its origin in CSP `connect-src`. Application credentials are never sent to those URLs. Hosts replacing dashboard CSP must retain `blob:` in `connect-src` for verified local previews.
- Lint reports six existing warnings in unchanged files. The unit suite has one existing skipped test. Canonical Next 16.4.1 and Zod 3 are preserved; dependency patch upgrades remain a separate release review.
- The local artifact is `qqq-frontend-next:1.0.0-SNAPSHOT`, built with an explicit Maven revision override. It includes generated PDF and TypeScript workers. The prepared release-managed version is `1.0.0-RC.38`; RC.36 and RC.37 already exist in Central and must never be overwritten.
- Next publisher support and the shared producer are merged (Praetor PR238, platform-ci PR67); the local caller pins327e9b3a9d45a08665fa9468f0f9741e972d8e3e. Runtime qualification passed736 Linux tests and its vulnerability scan. GitOps PR269 deployed the qualified runtime and exact workflow/GAV enrollment; ArgoCD adoption and the scheduled publisher startup are verified. The signed application tag and resulting Central artifact still remain. The existing tag, package.json and POM version agreement, typecheck/lint/tests/static export and signed Central delivery are preserved. Neither the local JAR nor source merge proves publication.
- The full application acceptance matrix across all browsers and deployment-specific storage/printer/scanner behavior is not claimed by the scoped QBit integration checks.
-58
View File
@@ -1,58 +0,0 @@
# Next UI QBit integration status
Updated: 2026-11-05. Source integration is prepared; no published release or deployment is claimed.
Current main `090f5febeaacf3f9c39a22952e5c93c859cdbeb7` (permission editor and companion Tools workspace) is integrated. The latest merge passed all 110 affected Authentik/widget/process tests, typecheck and the copyright-header gate. Both branches supplied the same worker limit; the merged configuration retains one setting. Prior focused Tools/UI-API checks covered 61 cases. These are integration checks, not completed release CI.
Core PR1343 merged as7825261ab after full CI4482 passed; promotion PR1345 merged as1bef44cf and immutable publication4629 is running. Collaboration must consume the resulting published version. Next publisher support merged in Praetor PR238 and platform-ci PR67. GitOps PR269 merged as883e1ce3; ArgoCD adopted the exact configuration and a scheduled publisher is running image sha256:ea068b2a02147280cacedd095fbc55114daa9e3db72f1b0ca8511bc43bfd12c7 with the exact Next enrollment. Its 736 Linux tests and vulnerability scan passed. The dashboard candidate version is 1.0.0-RC.38; this source preparation is not a published release.
Candidate CI4602 passed 3,800 tests but hit the unchanged five-second timeout in two `RecordQuery` filter tests. These also timed out on an earlier baseline; the newer main baseline's Node job passed. Profiling the focused cases identified repeated whole-page accessibility lookups. Scoping those lookups to the existing filter controls and confirmation dialog reduced their sampled CPU cost from 387 to 261 samples; the same assertions, interactions and timeouts are preserved. All 17 tests in the affected file and focused lint pass after the change. Release CI for this correction remains required; these focused results do not replace it.
## Changes
The canonical Next UI integration renders file preview/download adornments, retained-transfer receipts, isolated TypeScript editing and generic process results through QQQ metadata and UIUX APIs. It adds no table-specific application routes. The completion pass fixes Windows static-export execution, local PDF reads under dashboard CSP, and ordinary signed-storage PDF previews. Signed storage reads omit credentials, refuse redirects and enforce byte/time limits before canvas rendering. Retained transfers preserve their receipt/hash checks. Tests now use four workers by default to avoid high-core host contention; no timeout or coverage threshold was relaxed.
## Verified dependencies
- Core `feat/standalone-scripting-api`, code commit `d384d1c753cb0ac2ee4d0f6ccd564a3e47488a1c`, locally installed as `4.1.0-SNAPSHOT`.
- Collaboration `feat/record-files-standalone`, `53bd8f00d8a180b0fa4eb6cd029dc31044b6d4b3`, locally installed as `0.1.0-SNAPSHOT`.
- Next UI retains canonical Next 16.4.1, Zod 3 and existing dependencies. Local packaging explicitly overrides Maven revision to `1.0.0-SNAPSHOT`; it does not replace public RC.36.
## Completed verification
| Check | Result |
| --- | --- |
| Full `pnpm test:coverage` | 359 files; **3,784 passed**, 1 existing skip; all coverage gates passed |
| Coverage | 93.14% statements, 87.00% branches, 94.57% functions, 94.95% lines; API/utility sub-gates also passed |
| Lint and license headers | Zero errors, six existing warnings in unchanged files; Apache headers passed |
| Typecheck | Passed |
| Offline TypeScript worker in Chromium | Strict diagnostics, multi-file completion/hover/definition and formatting passed; no source execution or external fetch |
| Windows production static export | Passed using `pnpm build:export` |
| Local Maven snapshot JAR | Built successfully with PDF/TypeScript workers included |
| Browser file/process regressions | **12 passed** against the packaged production UI; API fixtures exercise PDF canvas, separate audit receipts, incomplete-receipt refusal and keyboard ordering |
| Live Core + Collaboration file workflow | Upload, thumbnail, preview, open, exact-byte download, rename/describe, reload and delete passed with synthetic data |
| Live PDF and authorization | Signed PDF canvas/page controls and download passed; another session was denied access to the parent record |
| Live UI API specification check | Bundled 1.8.0 compatible with running Core 1.10.0 |
The live tests used the existing disposable `CollaborationDevServer`, synthetic Alice/Bob sessions and the current UI JAR on loopback. No API mocks were used for those file/permission checks. The separate 12-case browser suite uses its existing API fixtures, not a deployed Record Files or scripting worker. Test-owned services and browsers were stopped after verification.
## Reproduce
```shell
pnpm install --frozen-lockfile
pnpm test:coverage
pnpm lint
pnpm typecheck
pnpm scripting:worker:check
pnpm build:export
mvn -B -Drevision=1.0.0-SNAPSHOT package
pnpm ui-api:check --server-url http://localhost:18098
```
Start the matching Collaboration demo using its README and place the resulting UI JAR first on its classpath. The live check used port 18098. The standard `pnpm test:e2e` owns its frontend fixture for the browser scenarios. Maven was invoked through the sibling Collaboration wrapper during this Windows run.
Local JAR SHA-256: `0626B33DF49569476E17FA080E2777A7AD9C7AF717CB75DFABF9EB40BE0A46A8`.
## Release boundary
See [known issues and release requirements](QBIT-KNOWN-ISSUES.md). Compatible public artifact publication, the complete application acceptance matrix and deployed document/script worker qualification are not implied by these source and local integration results.
-21
View File
@@ -1,21 +0,0 @@
# Record Files client integration
Updated: 2026-11-04. Current canonical integration branch: `feat/standalone-qbits-ui`. See [verification and dependencies](QBIT-RELEASE-STATUS.md).
## Standard controls and APIs
The Files tab uses standard themed buttons for Preview, Open, Download and Rename / describe. Navigation uses Next links with keyboard focus. Attachment metadata supplies independent `canEdit` and `canDelete` permissions; an explicitly denied edit capability never falls back to deletion permission. The server remains authoritative.
Every download requests a fresh owner-issued descriptor. `AUTHENTICATED_FETCH` deliveries use the configured native Record Files route, a new correlation identifier, and a separately verified completion receipt, byte count and SHA-256. Preview and save are separate attempts. A failed receipt never becomes a direct link or a successful save. Generic `FILE_DOWNLOAD` and `FILE_PREVIEW` adornments use the same controls; they do not require table-specific pages.
Tracked raster/PDF previews expose a local object URL only after verification. Ordinary signed PDF previews read bounded bytes with credentials omitted, redirects refused and a 30-second deadline. Both use the same canvas PDF renderer; uploaded PDFs are never framed. Closing the preview aborts work and revokes local URLs. Hosts serving signed files from another origin must allow that storage origin in CSP `connect-src` and configure storage CORS for the dashboard origin. This never forwards QQQ credentials to storage.
Open previews supported media; unsupported types use the authorized download action. The UI does not claim that a browser saved bytes or that a printer physically printed them. Retention, upload sessions, collections and durable audits belong to their server QBits.
## Dependency order
Install matching Core, then Collaboration, then Record Files and other feature QBits before the matching frontend. Public Core RC.21 and Collaboration RC.6 do not contain the new retention contracts. Local verification uses Core `4.1.0-SNAPSHOT` and Collaboration `0.1.0-SNAPSHOT`; see their branch status files for exact source commits. Core's dashboard CSP must include `blob:` in `connect-src` for local PDF-byte reads.
## Verification scope
The full component/API suite and coverage gates, production export, browser PDF/receipt/ordering scenarios and live Collaboration file workflow are recorded in [current status](QBIT-RELEASE-STATUS.md). Live tests use disposable synthetic data and real Core/Collaboration APIs. Browser mocks of the retained-transfer protocol do not establish production storage or worker qualification.
-332
View File
@@ -1,332 +0,0 @@
# State Management Guide
This document explains when to use each state container in the QQQ Frontend Next codebase. Each container has a specific purpose — choosing the wrong one leads to stale data, lost state on navigation, or unnecessary complexity.
## Decision Matrix
| If the state is… | Use… |
|---|---|
| Data fetched from the server (records, metadata, widgets, processes) | TanStack Query |
| Form field values and validation errors | React Hook Form |
| Filters, pagination, sort order — things a user should be able to bookmark or share | URL search params |
| User preferences that survive a session (density, column widths, saved views) | `useLocalStorage` |
| Application-wide UI state (modal stack, page header, accent color, branding) | QContext |
| Ephemeral UI state local to one component (dropdown open, hover, toggle) | `useState` / `useReducer` |
---
## TanStack Query — Server State
**Rule:** Never use `useState` for data that comes from the server. TanStack Query owns all server state.
The query client is configured in `src/lib/query-client.ts` with:
- 5-minute stale time — metadata and record lists stay fresh without excessive refetching
- 10-minute GC time — recently visited pages are snappy when navigating back
- Smart retry: never retries 4xx errors; retries 5xx and network failures up to 3 times with exponential back-off
- Global `QueryCache.onError` and `MutationCache.onError` — most errors are automatically toasted without per-call handling
### Query key factory
All query keys are defined in `queryKeys` in `src/lib/query-client.ts`. Always use the factory, never inline string arrays.
```typescript
import { queryKeys } from '@/lib/query-client'
// Fetch a single record
useQuery({
queryKey: queryKeys.tableRecord('order', recordId),
queryFn: () => getRecord('order', recordId),
})
// Invalidate all records for a table after a mutation
queryClient.invalidateQueries({ queryKey: queryKeys.tableRecords('order') })
// Invalidate everything (e.g. after logout)
queryClient.invalidateQueries({ queryKey: queryKeys.all() })
```
### Key hierarchy (broadest → narrowest)
```
['qqq']
['qqq', 'metadata', 'all']
['qqq', 'metadata', 'table', tableName]
['qqq', 'records', tableName]
['qqq', 'records', tableName, recordId]
['qqq', 'records', tableName, 'count', filterHash]
['qqq', 'processes', processName, processUUID, 'status', jobUUID]
['qqq', 'widgets', widgetName, params]
['qqq', 'possibleValues', 'table', tableName, fieldName, searchTerm]
['qqq', 'search', searchTerm]
```
Broad invalidations cascade down. For example, `queryKeys.tableRecords('order')` invalidates both the list query and all individual record queries for that table.
### What belongs in TanStack Query
- Record lists and counts
- Single record fetches
- All metadata (tables, processes, apps)
- Widget data
- Process status polling
- Possible values / enum lookups
- Global search results
- Auth session metadata
### What does NOT belong
Do not use TanStack Query for UI state (modal open/close, form field values, user preferences). Those belong in the containers described below.
---
## React Hook Form — Form Field State
**Rule:** React Hook Form owns field values and validation state for forms only. Do not use it for non-form UI, and do not reach into the form state from outside the form.
Forms are built with Zod schemas derived from field metadata. The schema is constructed by `zodFromMetadata()` in `src/lib/utils/zod-from-metadata.ts`.
```typescript
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { zodFromMetadata } from '@/lib/utils/zod-from-metadata'
function EntityForm({ tableMetaData, record }: Props) {
const schema = zodFromMetadata(tableMetaData)
const form = useForm({
resolver: zodResolver(schema),
defaultValues: record ?? {},
})
// Map a 400 validation error back to individual fields
const onSubmit = async (data: FieldValues) => {
try {
await saveRecord('order', data)
} catch (error) {
if (getErrorStatusCode(error) === 400) {
const fieldErrors = extractFieldErrors(error)
for (const [field, message] of Object.entries(fieldErrors)) {
form.setError(field, { message })
}
}
}
}
}
```
### What belongs in React Hook Form
- All form field values
- Validation errors (field-level and form-level)
- Dirty/touched state
- Submit handling
### What does NOT belong
Do not use React Hook Form for filter criteria in the query page — those live in URL params and reducer state managed by `useRecordQuery`.
---
## URL Search Params — Shareable State
**Rule:** Any state a user should be able to bookmark, share, or restore after a page refresh goes in the URL.
Use Next.js `useSearchParams` and `useRouter` to read and write URL state. This is the right home for:
- Active filters and quick search term
- Current page and page size
- Sort column and direction
The `useRecordQuery` hook syncs filter/pagination state to and from the URL automatically. Components should read these values from the hook, not from `useSearchParams` directly.
```typescript
import { useSearchParams, useRouter } from 'next/navigation'
// Reading
const searchParams = useSearchParams()
const page = Number(searchParams.get('page') ?? '1')
const sort = searchParams.get('sort') ?? ''
// Writing — replace so the back button doesn't accumulate filter changes
const router = useRouter()
const params = new URLSearchParams(searchParams)
params.set('page', String(nextPage))
router.replace(`?${params.toString()}`)
```
### Key naming conventions in URL params
| Param | Values | Purpose |
|---|---|---|
| `page` | integer | Current page (1-based) |
| `pageSize` | 10, 25, 50, 100 | Rows per page |
| `sort` | `fieldName:asc` or `fieldName:desc` | Active sort |
| `q` | string | Quick search term |
| `filter` | base64-encoded JSON | Serialized filter criteria |
### What belongs in URL params
- Filters, quick search
- Pagination (page, page size)
- Sort order
- Any state that makes a link shareable
### What does NOT belong
Do not put ephemeral UI state (modal open, panel open) in the URL. Do not put server data in the URL.
---
## localStorage — User Preferences
**Rule:** Preferences that should survive across sessions and browser restarts go in localStorage via the `useLocalStorage` hook.
The hook lives at `src/lib/hooks/use-local-storage.ts`. It is SSR-safe (returns `initialValue` on the server), cross-tab synced (listens to `storage` events), and has a stable setter that accepts a value or an updater function matching the `useState` API.
```typescript
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
const [density, setDensity, removeDensity] = useLocalStorage<'compact' | 'standard' | 'comfortable'>(
'qqq-order-density',
'standard'
)
```
### Higher-level hooks built on `useLocalStorage`
| Hook | Key | Contents |
|---|---|---|
| `useUserPreferences` | `qqq-user-preferences` | Default page size, density, view mode |
| `useColumnConfig` | `qqq-{tableName}-columns`, `qqq-{tableName}-column-order`, `qqq-{tableName}-column-widths` | Column visibility, order, pixel widths |
| `useSavedViews` | `qqq-{tableName}-saved-views` | Named filter snapshots |
### Key naming convention
```
qqq-{tableName}-{purpose} # table-scoped
qqq-user-preferences # global user preferences
qqq:{scope}:{purpose} # alternative colon-separated form
```
Always prefix with `qqq-` to avoid collisions with other apps that might share the same origin.
### What belongs in localStorage
- Table density (compact / standard / comfortable)
- Column visibility and order per table
- Column pixel widths per table
- Saved views (named filter snapshots)
- Global user preferences (default page size, default view mode)
- Dismissed banners or one-time notices
### What does NOT belong
Do not store server data in localStorage — use TanStack Query's cache. Do not store state that should be shareable — use URL params.
---
## QContext — Application-Wide UI State
**Rule:** Use QContext for state that must be accessible anywhere in the authenticated app tree and does not fit a more specific container.
The provider is `QContextProvider` in `src/lib/context/q-context.tsx`. Access it with the `useQContext()` hook. The hook throws at runtime if called outside a provider, which catches integration mistakes early.
```typescript
import { useQContext } from '@/lib/context/q-context'
function RecordQueryPage({ tableMetaData }: Props) {
const { setPageHeader, setTableMetaData } = useQContext()
useEffect(() => {
setPageHeader(tableMetaData.label)
setTableMetaData(tableMetaData)
}, [tableMetaData])
}
```
### What QContext contains
| Field | Type | Purpose |
|---|---|---|
| `pageHeader` | `string \| ReactNode` | Content rendered in the page header area |
| `accentColor` / `accentColorLight` | `string` | Theme accent colors (hex) |
| `tableMetaData` | `QTableMetaData \| null` | Metadata for the currently viewed table |
| `tableProcesses` | `QProcessMetaData[] \| null` | Processes for the current table |
| `dotMenuOpen` | `boolean` | Whether the ellipsis action menu is open |
| `modalStack` | `string[]` | Ordered list of open modal identifiers for keyboard nav |
| `pushModalOnStack(id)` | function | Called when a modal opens |
| `popModalOffStack(id)` | function | Called when a modal closes; warns if id is not on top |
| `clearModalStack()` | function | Clears all modal identifiers (e.g. on navigation) |
| `keyboardHelpOpen` | `boolean` | Keyboard shortcut overlay visibility |
| `userId` | `string \| undefined` | Current authenticated user identifier |
| `branding` | `QBrandingMetaData \| null` | Company name, app name, URLs from backend |
| `pathToLabelMap` | `Record<string, string>` | URL path segment → human-readable breadcrumb label |
### Modal stack usage
The modal stack ensures that `Escape` closes only the top-most modal and that focus is restored correctly. Every modal or sheet component must push on open and pop on close.
```typescript
const { pushModalOnStack, popModalOffStack, modalStack } = useQContext()
const modalId = 'edit-record-modal'
// On open
pushModalOnStack(modalId)
// On close
popModalOffStack(modalId)
// Check if this modal is on top (for Escape handling)
const isTopModal = modalStack[modalStack.length - 1] === modalId
```
### What belongs in QContext
- Application-wide UI state that has no more specific home
- Page header content (set by each page on mount)
- Modal stack (for coordinated keyboard navigation)
- Accent colors and branding
- The currently active table metadata (consumed by header, breadcrumbs, action menus)
### What does NOT belong
Do not put server data in QContext (use TanStack Query). Do not put form state in QContext (use React Hook Form). Do not put user preferences in QContext (use localStorage).
---
## useState / useReducer — Local Transient State
**Rule:** Use `useState` or `useReducer` only for state that is local to a single component or a small subtree, ephemeral (lost on unmount is acceptable), and neither server data nor shareable state.
```typescript
// Good — ephemeral open/close that doesn't need to survive navigation
const [dropdownOpen, setDropdownOpen] = useState(false)
const [isHovered, setIsHovered] = useState(false)
// Good — complex multi-field local state with clear action types
const [state, dispatch] = useReducer(recordQueryReducer, initialState)
```
### What belongs in useState / useReducer
- Dialog, dropdown, tooltip open/close
- Hover and focus state
- Local form wizard step (if not shareable)
- Complex local state with many fields that transition together (handled by `useReducer`)
### What does NOT belong
- Server data (use TanStack Query)
- State that should survive navigation or page refresh (use URL params or localStorage)
- State needed by distant parts of the tree (use QContext or lift appropriately)
---
## Summary
```
Server data → TanStack Query (queryKeys factory, automatic toasts on error)
Form fields → React Hook Form + Zod
Shareable/bookmarkable → URL search params
User preferences → useLocalStorage (key: qqq-{tableName}-{purpose})
App-wide UI state → QContext (useQContext hook)
Local/ephemeral UI → useState / useReducer
```
-190
View File
@@ -1,190 +0,0 @@
# QQQ Frontend Type Glossary
All types listed here are re-exported from `@/types` (the `src/types/index.ts` barrel).
Application code should always import from `@/types`, not from individual module files,
so that internal type organisation can change without affecting import paths.
```typescript
import type { QInstance, QTableMetaData, QRecord, QQueryFilter } from '@/types'
```
---
## 1. App & Auth
| Type | Source file | Purpose |
|------|-------------|---------|
| `QInstance` | `metadata.ts` | Top-level descriptor returned by `/metaData/instance`; the single source of truth for all apps, tables, processes, reports, widgets, and branding. **Key fields:** `apps`, `tables`, `processes`, `widgets`, `branding`, `appTree`, `environmentValues` |
| `QAuthenticationMetaData` | `metadata.ts` | Describes the auth provider type (`AUTH_0`, `OAUTH2`, `TABLE_BASED`, `FULLY_ANONYMOUS`, `MOCK`), the provider-specific values (client ID, base URL, audience) needed to boot the correct auth flow, and the pre-sign-in `branding` (`QLoginBranding`) the login page shows. |
| `QBrandingMetaData` | `metadata.ts` | Controls visual identity: company name, app name, logo URL, accent color, notification banners, and custom CSS injected via `data-qqq-id` selectors. |
| `QThemeMetaData` | `metadata.ts` | Backend-supplied Tailwind CSS token overrides (`primaryColor`, `accentColor`, `mode`). Applied on top of the default theme by the theme provider. |
| `Banner` | `metadata.ts` | A single notification banner (text, severity `info|warning|error`, optional color, dismissible flag) rendered at the top of the dashboard layout. |
### QInstance example
```typescript
const instance: QInstance = {
apps: { crm: { name: 'crm', label: 'CRM', children: [], ... } },
appTree: [{ name: 'crm', label: 'CRM', type: 'APP', ... }],
tables: { person: { name: 'person', label: 'People', fields: {}, ... } },
processes: {},
reports: {},
widgets: {},
branding: { companyName: 'Acme', companyUrl: 'https://acme.com', appName: 'Admin' },
helpContents: {},
environmentValues: {},
}
```
---
## 2. Metadata
| Type | Source file | Purpose |
|------|-------------|---------|
| `QTableMetaData` | `metadata.ts` | Full descriptor for a QQQ table: fields, sections, join relationships, capabilities, and per-user CRUD permissions. Drives the entire record query, view, create, and edit experience. **Key fields:** `name`, `label`, `primaryKeyField`, `fields`, `sections`, `capabilities`, `readPermission`, `insertPermission`, `editPermission`, `deletePermission` |
| `QFieldMetaData` | `metadata.ts` | Descriptor for a single table column: data type, required/editable/hidden flags, max length, numeric bounds, possible-value source, display format, and adornments. **Key fields:** `name`, `label`, `type`, `isRequired`, `isEditable`, `isHidden`, `possibleValueSourceName`, `adornments` |
| `QProcessMetaData` | `metadata.ts` | Descriptor for a multi-step wizard process: identity, permissions, input record bounds, and the ordered list of `QFrontendStepMetaData` objects to render. |
| `QFrontendStepMetaData` | `metadata.ts` | A single step in a process wizard: label, ordered component list, and optional form/view/record-list field arrays used by the step renderer. |
| `QFrontendComponent` | `metadata.ts` | A single UI component descriptor within a process step — a `type` (from `QComponentType`) plus arbitrary `values` passed as props to the matching renderer. |
| `QAppMetaData` | `metadata.ts` | A top-level app grouping: sidebar navigation children, dashboard widget names, and labeled sections grouping tables, processes, and reports. |
| `QAppTreeNode` | `metadata.ts` | One node in the recursive sidebar navigation tree; the `type` field (`TABLE`, `PROCESS`, `REPORT`, `APP`) determines the route. |
| `QAppSection` | `metadata.ts` | A labeled card group on an app dashboard page, listing the table, process, and report names belonging to that group. |
| `QWidgetMetaData` | `metadata.ts` | Dashboard widget descriptor: renderer type string, permissions, grid column span, optional toolbar dropdowns, reload/export button flags, and help content. |
| `QTableSection` | `metadata.ts` | A collapsible card section on record view/edit pages: field names to display, optional embedded widget name, display tier, and grid column span. |
| `QExposedJoin` | `metadata.ts` | A join relationship made available for query filters and record views: cardinality (`isMany`), joined table metadata, and the path of `QJoinMetaData` hops. |
| `FieldAdornment` | `metadata.ts` | Discriminated union of all adornment shapes (`LINK`, `CHIP`, `TOOLTIP`, `ERROR`, `FILE_DOWNLOAD`, `SIZE`, `REVEAL`, `CODE_EDITOR`, `RENDER_HTML`, `FILE_UPLOAD`). Narrow with `Extract<FieldAdornment, { type: 'LINK' }>`. |
### QFieldMetaData example
```typescript
const field: QFieldMetaData = {
name: 'firstName',
label: 'First Name',
type: 'STRING',
isRequired: true,
isEditable: true,
isHidden: false,
isHeavy: false,
maxLength: 100,
adornments: [],
}
```
---
## 3. Records & Queries
| Type | Source file | Purpose |
|------|-------------|---------|
| `QRecord` | `records.ts` | A single backend record returned by the tables API. `values` holds raw typed data; `displayValues` holds pre-formatted strings ready for rendering. May include `associatedRecords` for joined/child data, and `errors`/`warnings` for bulk operations. |
| `QPossibleValue` | `records.ts` | A single option from a possible-value source, used to populate autocomplete and select inputs. `id` is the stored value; `label` is the user-visible text. |
| `QAuditRecord` | `records.ts` | An audit log entry for a record: who changed it, when, what action (`INSERT`/`UPDATE`/`DELETE`), and the list of `QAuditFieldChange` field-level diffs. |
| `QQueryFilter` | `query.ts` | Root filter object sent to `/tables/{name}/query` and `/tables/{name}/count`. Combines `criteria` with `AND`/`OR`, supports nested `subFilters`, and carries `skip`/`limit` for pagination. |
| `QFilterCriteria` | `query.ts` | A single field-level predicate: a `fieldName`, a `QCriteriaOperator`, and a `values` array that may contain literals or dynamic expressions (`NowExpression`, `NowWithOffsetExpression`, etc.). |
| `QFilterOrderBy` | `query.ts` | A sort clause: `fieldName` and `isAscending`. Multiple entries form a compound sort. |
| `QueryJoin` | `query.ts` | A join instruction sent alongside a `QQueryFilter` when fields from a related table are needed in query results. Specifies `joinTable`, join type (`INNER`/`LEFT`/etc.), and optional alias. |
| `NowWithOffsetExpression` | `query.ts` | A dynamic filter value that resolves to "now minus/plus N days/weeks/months/years" at query time, enabling "last 7 days" style saved filters. |
### QQueryFilter example
```typescript
const filter: QQueryFilter = {
criteria: [
{ fieldName: 'status', operator: 'EQUALS', values: ['ACTIVE'] },
{ fieldName: 'createdDate', operator: 'GREATER_THAN', values: [{ type: 'NOW_WITH_OFFSET', offsetValue: 30, offsetUnit: 'DAY', isNegativeOffset: true }] },
],
booleanOperator: 'AND',
orderBys: [{ fieldName: 'lastName', isAscending: true }],
skip: 0,
limit: 25,
}
```
---
## 4. Processes
| Type | Source file | Purpose |
|------|-------------|---------|
| `QJobStarted` | `processes.ts` | Initial response after submitting a process step; provides `processUUID` and `jobUUID` for subsequent status polling. |
| `QJobRunning` | `processes.ts` | Intermediate polling response while a job executes asynchronously; provides a `message` and optional `current`/`total` progress counts for the progress bar. |
| `QJobComplete` | `processes.ts` | Terminal success response; carries output `values`, optional `nextStep`/`backStep` navigation hints, and any `processMetaDataAdjustment` that modifies remaining wizard steps. |
| `QJobError` | `processes.ts` | Terminal failure response; provides a technical `error` string and an optional `userFacingError` message for display in the wizard UI. |
| `QJobResponse` | `processes.ts` | Discriminated union of `QJobStarted \| QJobRunning \| QJobComplete \| QJobError`. Narrow by checking for the discriminating property (`jobUUID`, `message`, `values`, or `error`). |
| `ProcessMetaDataAdjustment` | `processes.ts` | Runtime modifications to the process wizard applied after a step completes: `addedSteps`, `removedSteps`, and partial `modifiedFields` overrides. |
### QJobResponse narrowing example
```typescript
function handleJobResponse(response: QJobResponse) {
if ('error' in response) {
// QJobError
showToast(response.userFacingError ?? response.error)
} else if ('values' in response) {
// QJobComplete
advanceToStep(response.nextStep)
} else if ('message' in response) {
// QJobRunning — update progress bar
setProgress({ message: response.message, current: response.current, total: response.total })
} else {
// QJobStarted — store jobUUID and start polling
setJobUUID(response.jobUUID)
}
}
```
---
## 5. Widgets
| Type | Source file | Purpose |
|------|-------------|---------|
| `WidgetData` | `widgets.ts` | Base shape for all widget data payloads; the `type` string is the discriminant used to select the correct renderer. |
| `ChartWidgetData` | `widgets.ts` | Recharts widget payload: `chartType` (`line`/`bar`/`pie`/`area`), `labels` array, and `datasets` array of `ChartDataset` series. |
| `ChartDataset` | `widgets.ts` | A single Recharts data series: human-readable `label`, ordered `data` values aligned with the parent chart's `labels`, and optional `color`. |
| `StatisticsWidgetData` | `widgets.ts` | KPI tile payload: headline `title`, `value`, optional `unit`, and optional `trend` object with direction (`up`/`down`/`flat`), numeric magnitude, and period label. |
| `HtmlWidgetData` | `widgets.ts` | Raw HTML widget payload; the `html` string **must** be sanitized with DOMPurify before use in `dangerouslySetInnerHTML`. |
| `RecordGridWidgetData` | `widgets.ts` | Tabular record list widget: `tableName`, optional `columns` (field name strings) or `fields` (full `QFieldMetaData[]`), `records` array, and optional `totalCount`. |
| `BlockData` | `widgets.ts` | Discriminated union of all block element types renderable inside a `BlockWidgetData`: `text`, `big_number`, `up_or_down`, `progress`, `button`, `icon`, `image`, `audio`, `divider`, `input`, `html`. |
| `BlockWidgetData` | `widgets.ts` | Ordered heterogeneous block widget: a `blocks` array of `BlockData` variants and an optional `layout` (`vertical`/`horizontal`/`grid`). |
---
## 6. Enums
| Type | Source file | Purpose |
|------|-------------|---------|
| `QFieldType` | `enums.ts` | String-union of all primitive field data types: `STRING`, `INTEGER`, `LONG`, `DECIMAL`, `BOOLEAN`, `DATE`, `TIME`, `DATE_TIME`, `TEXT`, `HTML`, `PASSWORD`, `BLOB`. Determines which renderer, input component, and Zod validator to use. |
| `QCriteriaOperator` | `enums.ts` | String-union of all filter comparison operators used in `QFilterCriteria.operator`: `EQUALS`, `NOT_EQUALS`, `IN`, `NOT_IN`, `CONTAINS`, `STARTS_WITH`, `LESS_THAN`, `GREATER_THAN`, `IS_BLANK`, `IS_NOT_BLANK`, `BETWEEN`, and more. |
| `Capability` | `enums.ts` | String-union of backend-declared table capabilities (`TABLE_QUERY`, `TABLE_GET`, `TABLE_COUNT`, `TABLE_INSERT`, `TABLE_UPDATE`, `TABLE_DELETE`) that gate UI elements such as Create, Edit, and Delete buttons. |
| `QAppNodeType` | `enums.ts` | String-union identifying the entity kind of a sidebar navigation node: `TABLE`, `PROCESS`, `REPORT`, `APP`. Drives route prefix selection in `QAppTreeNode`. |
| `QComponentType` | `enums.ts` | String-union of all UI component types renderable inside a process step: `EDIT_FORM`, `VIEW_FORM`, `RECORD_LIST`, `VALIDATION_REVIEW_SCREEN`, `BULK_LOAD_FILE_MAPPING_FORM`, `PROCESS_SUMMARY_RESULTS`, `HTML`, `WIDGET`, and more. |
| `AdornmentType` | `enums.ts` | String-union of all field adornment identifiers (`LINK`, `CHIP`, `SIZE`, `ERROR`, `RENDER_HTML`, `REVEAL`, `CODE_EDITOR`, `FILE_DOWNLOAD`, `FILE_UPLOAD`, `TOOLTIP`). Mirrors the discriminant property of the `FieldAdornment` union for use in switch statements and type-guard utilities. |
---
## Quick-Reference Import Cheat Sheet
```typescript
// App & auth
import type { QInstance, QAuthenticationMetaData, QBrandingMetaData, Banner } from '@/types'
// Metadata
import type {
QTableMetaData, QFieldMetaData, QProcessMetaData,
QFrontendStepMetaData, QWidgetMetaData, FieldAdornment,
} from '@/types'
// Records & queries
import type { QRecord, QPossibleValue, QQueryFilter, QFilterCriteria } from '@/types'
// Processes
import type { QJobResponse, QJobComplete, QJobError, ProcessMetaDataAdjustment } from '@/types'
// Widgets
import type { WidgetData, ChartWidgetData, StatisticsWidgetData, BlockData } from '@/types'
// Enums
import type { QFieldType, QCriteriaOperator, Capability } from '@/types'
```
-17
View File
@@ -1,17 +0,0 @@
# 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.
-339
View File
@@ -1,339 +0,0 @@
# Browser matrix — real-backend acceptance (QRun-IO/qqq#649, #708)
> **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.
The subsequent column-menu height correction passes28 targeted Chromium/WebKit cases. The valid red run failed both320px-window cases and passed both720px cases; final checks cover bounds, keyboard/focus, Copy/sort, standalone query actions and variants. Hosted predecessor36739388053 WebKit remains failed578/1: Copy full query values was outside the viewport before variant switching. Its exact scenario passed locally before correction; hosted resolution is unproven. Other predecessor jobs remain pending at this checkpoint. These scoped results do not establish complete browser compatibility.
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 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
First complete workflows in desktop Chromium against the real backend: normal defaults,
required and invalid inputs, dependent controls, edit/cancel, save/reopen, permission
boundaries, error recovery, and database or downloaded-file results. Expand browser and
viewport coverage after those behaviors work. Keep existing secondary results visible;
a passing row or test count does not establish that every path within a feature works.
For example, WID-073 checks report column totals and the exported value distribution
against SQL for the active filter, then verifies cancellation leaves saved data unchanged.
Opening the statistics dialog alone is insufficient evidence of that workflow.
## RC3 published checkpoint — 2026-09-29
Published source `f1a6a3a` / `v1.0.0-RC.3` passed **570 Chromium tests and all 391 required rows** in the [candidate run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36652168977), with zero failures, skips or flaky results. The tested PR merge has the same Git tree as the tag; backend commit was `cba758507dcc54cf282a6638ab50a7d3a1904c7c`.
The candidate run is complete: mobile **545 passed**; Firefox **569 passed, one failed** (WID-005: Widget Gallery heading timeout); WebKit **568 passed, two failed** (NAV-056: application-request access-control errors; QRY-030: density-menu timeout); tablet **543 passed, two failed** (QRY-030: density-menu timeout; REC-046: WebKit internal navigation error). No skips or flaky results. Reports and traces are retained; causes remain unproven. The overall candidate run failed. The [tag run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36654410984) passed **570 Chromium tests and all 391 required rows**. Tag WebKit passed **568 and failed two** (NAV-016 application-request access-control errors; QRY-030 density-menu timeout); tag Firefox passed **568 and failed two** (QRY-007 error toast not found; WID-073 Render Report action timeout). Tag mobile passed **545 tests**; tablet passed **543 and failed two** (NAV-026 widget-request access-control errors; QRY-030 density-menu timeout). The tag run is complete and failed overall, with zero skips or flaky results. Reports and traces are retained; no complete cross-browser pass is claimed.
The public Maven JAR passed signature/checksum checks, normal dependency resolution in a separate consumer, and seven affected Chromium workflows with independent database readback. Both image architectures are published; the actual arm64 image passed app-route and all 21 referenced JavaScript asset checks. See [RC3 release notes](../releases/1.0.0-RC.3.md) for immutable artifact checksums and limitations. Final 1.0 remains on hold.
## RC2 published checkpoint — 2026-09-29
Published source `0905cfb` / `v1.0.0-RC.2` passed **563 Chromium tests and all 390 required rows** in both its PR and tag runs. The [completed tag run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36642612560) also passed all **538 mobile tests**. WebKit passed 561 and failed QRY-030 (density menu) and RPT-012 (internal navigation error); Firefox passed 561 and failed PRC-061 (bulk-load default selection) and INT-011 (failed-save feedback); tablet passed 537 and failed QRY-030. No tests skipped or passed through retries. The overall run failed, and failure causes remain under investigation.
The earlier PR run had a different set of secondary-browser failures. Both sets remain recorded in the [RC2 release notes](../releases/1.0.0-RC.2.md); a passing later scenario does not establish why an earlier failure occurred. The published Maven JAR was independently downloaded, signature/checksum verified, resolved by a fresh consumer and exercised in five Chromium workflows. Both container architectures are published; the arm64 image passed runtime HTML/JavaScript checks. RC2 is immutable and final 1.0 remains on hold.
The preparation checkpoints below are historical results and do not supersede this published-artifact evidence.
## Post-RC1 checkpoint — 2026-09-29
The report capability candidate `2221999` passed **562 Chromium tests**, all 390 required rows, zero failures/skips/flaky tests in [CI](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36633453712). WebKit passed 561 with one QRY-030 failure: the density option did not appear after a trigger tap. Its trace is retained and the cause remains unproven. Firefox passed 560 with two failures: NAV-014 reported an owned embedded-page request failure during navigation; SEC-003 timed out awaiting its initial record row, before the create-restriction assertions. The final screenshot shows the row, and the trace shows a roughly 15-second delay before the query was sent; the cause remains unproven. Mobile passed all 537 tests; tablet passed 536 with one QRY-030 density-menu timeout matching the desktop WebKit symptom. The run is complete and failed overall. These results precede the child-export correction.
The subsequent `542ac47` local full Chromium run passed 562 and failed WID-046: the generic parent CSV export was hidden while child dropdowns awaited selection, despite the parent's independent CSV payload. The correction preserves the selection restriction for specialized child export and restores generic payload export. Its new regression was observed failing before the fix; 29 focused tests, all 1,857 unit tests, and the production build pass. The corrected export then passed **563 Chromium tests and all 390 required rows**, with zero failures/skips/flaky tests. A clean local JAR matches the tested export byte-for-byte. Exact-commit CI and published-artifact verification remain pending; this local result does not establish artifact availability.
The preceding d937507 [CI run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36626338487) passed **558 tests each in Chromium and WebKit**, all 390 required rows. Firefox passed **556 with two failures**: NAV-014 reported `NS_ERROR_FAILURE` for the owned embedded page during breadcrumb navigation; QRY-007 timed out hovering an application-theme error toast after it detached. Logs and traces are retained for diagnosis. All five focused breadcrumb/error-theme scenarios subsequently passed locally in Firefox against RC2 without source or test changes. That recheck does not establish the cause or resolution of the CI failures. Mobile passed 533; tablet passed 532 and failed REC-019 with a WebKit internal navigation error while opening a field-lab record. Its log and trace are retained. These results do not establish a complete cross-browser pass or erase the RC1 results below.
## RC1 checkpoint — 2026-09-29
Published candidate `e901df9` / `v1.0.0-RC.1`: Chromium **547 passed**, no failed/skipped/flaky tests, all 389 required rows passed. WebKit **546 passed, 1 failed** in NAV-034 legacy table-process redirect navigation, with an internal WebKit resource-loading error; trace retained for investigation. The completed Firefox run had 546 passes and one WID-021 widget visibility timeout; phone/tablet had 1,043 passes and one tablet SEC-002 internal WebKit navigation error. The separate tag run also had failures; see the release notes for both runs. [CI run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36601699322). This does not establish a complete cross-browser pass.
The actual Maven Central JAR additionally passed Sleep Interactive defaults/validation/completion and report-context filtered statistics/CSV checks. The published arm64 container passed a runtime/HTML smoke check; its manifest also includes amd64. See [RC1 release evidence](../releases/1.0.0-RC.1.md).
```bash
export QQQ_SAMPLE_JAR=/path/to/qqq-sample-project-<version>-jar-with-dependencies.jar
# primary workflow gate, fresh static export
QQQ_ACCEPTANCE_BROWSERS=chromium node scripts/acceptance.mjs
# subsequent compatibility gate, all five projects, fresh static export
QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet node scripts/acceptance.mjs
# the same with explicit ports (use distinct ports for concurrent runs)
QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet \
QQQ_ACCEPTANCE_BACKEND_PORT=18793 QQQ_ACCEPTANCE_FRONTEND_PORT=13793 \
node scripts/acceptance.mjs
# the touch projects only, reusing out/ (a filtered run is a partial gate)
QQQ_ACCEPTANCE_BROWSERS=mobile,tablet node scripts/acceptance.mjs --skip-build
```
| Project | Playwright device | Engine (Playwright 1.64.0-alpha-2026-10-01) | Runs |
|----------|-------------------|----------------------------|------|
| chromium | Desktop Chrome | Chrome for Testing 155 | every spec |
| firefox | Desktop Firefox | Firefox 156 | every spec |
| webkit | Desktop Safari | WebKit 26.6 | every spec |
| mobile | Pixel 7 (412 × 839, touch) | Chromium | tests tagged `@mobile` |
| tablet | iPad (gen 7) (810 × 1080, touch) | WebKit | tests tagged `@mobile` or `@tablet` |
## Phone and tablet scope (QRun-IO/qqq#708)
Below 768 px the UI uses a different layout by design:
- lists are card lists;
- filters open in a bottom sheet;
- record sections are an accordion;
- record actions are in an action sheet;
- navigation is a modal drawer.
The tablet in portrait (810 px) gets the desktop layout (grid, sidebar, tabs) with a touch
pointer. Specs that assert the desktop layout (grid columns and cells, column resizing,
grid keyboard navigation) do not describe phone behavior, and skipping them is not
allowed: skips fail the gate. The touch projects therefore run only tagged tests:
- `mobile` runs tests whose title carries `@mobile`;
- `tablet` runs tests tagged `@mobile` or `@tablet` (`grep` in
`tests/acceptance/playwright.config.ts`).
Tagged tests also run on the desktop projects. They are either layout-neutral (helpers
`listCell`, `columnCells`, `listRows`, `navigation`, `recordList`, `openRecord` and
`recordAction` pick the grid or the cards, the tabs or the accordion) or pinned to a
412 px touch viewport, so every phone scenario is also checked in Firefox and WebKit at
phone width.
### Coverage rule, enforced by the gate
When a touch project runs, `scripts/acceptance-gate.mjs` requires every required row to
have a passing test in `mobile` and in `tablet`. The only exception is a row that records
why it is not a touch scenario (`"desktopOnly": "<reason>"`, which currently no row uses).
The result is in `summary.phone`: `covered`, `desktopOnly`, `uncovered`. The desktop-layout
variants that stay untagged (the grid-keyboard INT-001, the grid-loading INT-009, the
desktop REC-004 tab test) have phone counterparts for the same rows.
### Touch targets and layout checks
On a coarse pointer every control is at least 44 × 44 CSS px (WCAG 2.5.5). This is a
`@media (pointer: coarse)` rule in `src/styles/globals.css`, plus `pointer-coarse:`
classes in components. It covers:
- buttons, inputs, selects, tabs, menu items and options;
- a label wrapping a checkbox, radio or switch;
- breadcrumbs, sidebar entries, record links and widget block links.
Links in running text and data values keep their inline size. Mouse layouts are unchanged
at every width, because Playwright emulates `pointer: coarse` only when `hasTouch` is set.
`tests/acceptance/support/touch.ts` provides the checks:
- `expectNoHorizontalScroll`: the page never scrolls sideways; wide data scrolls inside
its own container;
- `expectTouchTargets` / `expectTouchReady`: no visible control below 44 px, applied only
under a coarse pointer.
The phone and tablet specs call these on the shell, lists, filter sheet, column
configuration, bulk bar, record view and forms, dialogs and sheets, every process screen
type, dashboards and reports.
Phone behaviors with their own rows and tests:
| Phone behavior | Rows |
|----------------|------|
| Navigation drawer: tap, backdrop, keyboard focus trap, Escape, focus return | NAV-026, NAV-037 |
| Shell on phones and tablets: header, breadcrumbs, sidebar, skip link, not-found | NAV-038, NAV-014 |
| Card list: grid columns in order, formatted values, selections, loading | QRY-008, INT-009 |
| Filter sheet: modal dialog, focus in and out, Escape, scrolls inside | QRY-009 |
| Column configuration by touch | QRY-024 |
| Bulk actions from a card selection | QRY-036 |
| Saved views and export menus on a phone | QRY-055 |
| Record header and phone action sheet | REC-058, INT-003 |
| Forms with every editor type on phones and tablets | REC-059, INT-008 |
| Tooltips and help by tap (fields, widget labels, widget help) | REC-036, REC-039, WID-043, WID-044 |
| Dashboards: one column below 1024 px, wide tables scroll in their card, dropdown/date/reload/export by tap | WID-022, WID-040, WID-019, WID-045–WID-048 |
| Process screens of every type touch-ready | PRC-001–PRC-047 |
| Sign-in, logout, session expiry; accessibility scan at phone width | SEC-020–SEC-023, INT-014, INT-005 |
| TABLE_BASED password sign-in, failure, expiry and logout | SEC-034–SEC-037 (QRun-IO/qqq#700) |
| Login page branding before sign-in | NAV-033 (QRun-IO/qqq#703) |
| Search dialog with record search | NAV-030 (QRun-IO/qqq#701) |
| Keyboard use on a phone (card, action sheet, edit, save) | INT-001 |
The gate also fails any configured project that ran no tests.
## Results
Recorded 2026-09-26 on macOS (Darwin 27) for the Next UI 1.0 integration (QRun-IO/qqq#713):
the sample from the qqq `feature/next-1.0` backend (`a55c71a2d`, with the v1 supplemental
metadata, developer-mode and table-variant process routes of #714) and the Next static export
of `feature/next-1.0` (`e3bea6e`: the batch 1-3 streams, Material parity #714 and phone and
tablet coverage #708), built from a clean `.next`. One run of the full gate with the five
projects:
Tests passed / run, per spec area and project (the touch projects run the `@mobile` tests only):
| Area | chromium | firefox | webkit | mobile | tablet |
|---|---|---|---|---|---|
| navigation | 59/59 | 59/59 | 59/59 | 56/56 | 56/56 |
| performance | 5/5 | 5/5 | 5/5 | 5/5 | 5/5 |
| processes | 61/61 | 61/61 | 61/61 | 54/54 | 54/54 |
| query | 68/68 | 68/68 | 68/68 | 68/68 | 68/68 |
| records | 90/90 | 90/90 | 90/90 | 84/84 | 84/84 |
| security | 73/73 | 73/73 | 73/73 | 67/67 | 67/67 |
| widgets | 93/93 | 93/93 | 93/93 | 91/91 | 91/91 |
| **total** | **449/449** | **449/449** | **449/449** | **425/425** | **425/425** |
Gate (`test-results/acceptance/gate.json`, 2,197 tests, 44.3 min, retries 0, commit `e3bea6e`):
```json
{
"summary": { "passed": 347, "failed": 0, "missing": 0, "excluded": 3,
"phone": { "covered": 347, "desktopOnly": 0, "uncovered": 0 } },
"byProject": {
"chromium": { "passed": 449, "failed": 0, "skipped": 0, "flaky": 0 },
"firefox": { "passed": 449, "failed": 0, "skipped": 0, "flaky": 0 },
"webkit": { "passed": 449, "failed": 0, "skipped": 0, "flaky": 0 },
"mobile": { "passed": 425, "failed": 0, "skipped": 0, "flaky": 0 },
"tablet": { "passed": 425, "failed": 0, "skipped": 0, "flaky": 0 }
},
"problems": []
}
```
Every required row passes in every desktop project and in both touch projects; no row is
`desktopOnly`. PRC-039, WID-033 and SEC-030 are `required: false` pending approval and are
reported as excluded.
## Differences found between engines
Fixed:
- **WebKit, aborted prefetches.** WebKit raises "<url> due to access control checks." as
a page error for same-origin Next.js route prefetches and RSC payload fetches that a
document navigation cancels; the server answered 200 and Playwright never sees the
request. Traces confirmed every report named a `/app/...` route, `__next.*.txt` or
`?_rsc=` payload within a millisecond of a main-frame navigation. The shared diagnostics
fixture ignores exactly these (same origin, Next route or payload URL, within one second
of a navigation) and lists them under `interruptedFetches`; API, cross-origin and
unrelated access-control failures still fail the test.
- **WebKit, document title.** Next.js re-applies the static metadata title ("QQQ Admin")
after client navigations, in WebKit after the layout set the metadata title (NAV-023,
NAV-006). `useDocumentTitle` keeps the breadcrumb title in place.
- **Firefox, rich text.** Firefox stores a typed space before a formatting change as
`&nbsp;`; the editor now stores engine-neutral HTML (REC-020).
- **Firefox/WebKit, computed styles.** A `CSSStyleDeclaration` returned from `evaluate()`
only serializes in Chromium; WID-057 reads the properties inside the page.
- **WebKit keyboard order.** WebKit, like Safari, moves Tab only between form fields;
keyboard specs use Option+Tab there (`tabKey` in `specs/security/support/ui.ts`).
- **Aborted requests.** A request aborted by the test is reported as `net::ERR_FAILED`
(Chromium), `NS_ERROR_FAILURE` (Firefox) or "Blocked by Web Inspector" (WebKit).
- **Provider logout in Firefox.** Starting an in-app navigation just before leaving for
the identity provider made Firefox log "Failed to fetch RSC payload". Provider logout and
sign-in retries now leave the page without a client navigation first.
- **Set-Cookie visibility in WebKit.** WebKit hides `Set-Cookie` from page responses, so
SEC-033 checks the logout response from Node.
- **Phone defects (QRun-IO/qqq#694).** The card list showed "No records found" while
loading; it now shows busy placeholder cards. The record action sheet moves focus in on
open, closes on Escape, and returns focus to its trigger, also when a dialog opened from
one of its items closes.
The fixes above describe earlier checkpoints. Open failures from the RC1 runs are recorded in the RC1 checkpoint and release notes; they have not been waived.
## RC4 candidate density touch correction
The recurring QRY-030 density-menu failure was reproduced in Linux WebKit using
Playwright 1.58.2's official Noble image. Instrumented events showed that touch
pointer-down opened the menu, then synthetic mouse focus returned to its trigger;
Radix treated that focus as outside the non-modal menu and dismissed it. macOS
WebKit did not reproduce this event sequence.
The RC4 candidate source applies the existing Selection/Views/Export trigger guard to
Density. It ignores outside interactions only when they target that same trigger;
other controls still dismiss the menu. No layout, dependency or timeout changed.
Both the existing density-selection test and the new touch/keyboard/outside-dismissal
regression passed with the selection-menu workflow: **nine checks across Linux
WebKit, tablet WebKit and Chromium, zero failures/skips/flaky results**. The new
regression failed before the fix. An initial added-test run used an incorrect
`textbox` locator for the searchbox; it was interrupted, retained, corrected and
rerun. The full unit suite passed 1,884 tests; production export, types, affected
lint and independent review also passed. The combined candidate at `f2cdf03` passed **577 Chromium tests and all 391
required rows**, with zero failures/skips/flaky results and three approved exclusions.
The preceding numeric-wheel correction separately passed its 576-test Chromium gate.
Both corrections were integrated before RC4 publication at
`f2cdf03`, superseding its earlier preparation checkpoint `1dbe1d9`. Published
RC3 does not contain them. Historical RC3/precursor failures above remain
valid evidence for those revisions; this focused result does not certify the whole
browser matrix.
## Completed precursor CI before RC4 integration
Run `36658766595` at `1283bac07e81741bfadc34bf7fe1c7805604d16f` is terminal.
Chromium passed 574 tests and all 391 required rows. Mobile passed 547; tablet
passed 544 and failed three: NAV-015 internal WebKit navigation, NAV-056 API
access-control errors, and QRY-030 density touch. Firefox passed 573 and failed
WID-024 waiting for the record heading. WebKit passed 573 and failed QRY-030.
All profiles had zero skips or flaky outcomes; general CI and image checks passed.
These results predate the report-editor, numeric-wheel and density corrections.
The Linux density regression is corrected and locally verified as described above;
other secondary failures remain open pending diagnosis or new candidate evidence.
Hosted checks for the integrated RC4 candidate and tag are complete; see the RC4 published checkpoint below.
## RC4 published checkpoint — 2026-09-30
Published source `3c409f9` / `v1.0.0-RC.4` passed **577 Chromium tests and all 391 required rows** in both [candidate](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36664932061) and [tag](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36714593674) acceptance. Both used backend `e0d57dd31b979b17215370546ef4e26cd8a38d21`; the candidate merge tree matches the tagged source.
Both acceptance runs completed and failed overall because of secondary-browser failures. All profiles had zero skips or flaky outcomes; failures were retained, not waived or silently retried.
| Browser | Candidate result | Tag result |
| --- | --- | --- |
| Chromium | 577 passed | 577 passed |
| Mobile | 547 passed, 1 failed: REC-054 script-suggestion listbox bounding-box timeout in dark mode | 548 passed |
| Firefox | 576 passed, 1 failed: WID-026 record-heading timeout | 576 passed, 1 failed: WID-019 dashboard-heading timeout |
| WebKit | 576 passed, 1 failed: REC-046 internal navigation error on a read-only user's copy route | 576 passed, 1 failed: REL-005 internal navigation error returning to the pet list |
| Tablet | 547 passed, 1 failed: NAV-026 widget-request access-control errors during drawer navigation | 547 passed, 1 failed: SEC-002 internal navigation error opening a denied ledger create route |
Density scenarios passed in every profile of both runs. The other failures remain open; a later pass does not erase the earlier failure or prove full browser compatibility.
The actual public Maven JAR passed signature/checksum verification, fresh-consumer dependency resolution and 17 affected real-backend Chromium workflows. The public ARM64 container served the application and all 21 referenced JavaScript assets. These checks establish artifact delivery and the stated workflow coverage, not full compatibility or visual approval. See [RC4 release notes](../releases/1.0.0-RC.4.md) for checksums and remaining requirements. Final 1.0 remains on hold for owner testing and renewed approval.
-5
View File
@@ -1,5 +0,0 @@
# ESB integration acceptance
The ten required [ESB matrix scenarios](../../tests/acceptance/matrix/esb.json) pass in Chromium, Firefox and WebKit: 30 cases with no failures, skips or retries. They exercise the stock sample’s embedded Artemis broker, table-event delivery into `syncPerson`, service/table/process views, permission denial, subscription browsing, trigger pause/resume/restart and confirmed dead-letter replay. A further 38 Chromium navigation, permission and table Developer-view cases, plus 12 security-variant and search cases, pass without skips or retries. The main and security fixtures use separate broker ports. These are focused ESB results; general Next browser/parity and final release acceptance remain tracked separately.
The backend integration fixes are [QQQ PR #980](https://github.com/QRun-IO/qqq/pull/980), based on `5bf02d54`: normal sample ESB registration, API documentation route isolation and nullable subscription/dead-letter pause state. Frontend verification passes 105 focused ESB tests, all 1,951 unit tests with coverage thresholds, type checking, the production static export and bundle budgets. The [acceptance README](../../tests/acceptance/README.md) gives the commands and fixture boundaries; [Next #10](https://github.com/QRun-IO/qqq-frontend-next/issues/10) and [QQQ #739](https://github.com/QRun-IO/qqq/issues/739) track delivery. RabbitMQ and broker management behavior are covered by the backend conformance suite; these browser tests use Artemis without an HTTP management endpoint, so unavailable broker actions remain hidden.
-466
View File
@@ -1,466 +0,0 @@
# Next UI feature acceptance matrix
> **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.
Generated by `node scripts/acceptance-matrix-md.mjs` from `tests/acceptance/matrix/*.json` and the
acceptance gate results. Each row is a supported QQQ UI feature or use case with executable
real-backend acceptance coverage (QRun-IO/qqq#649). See `tests/acceptance/README.md` to run it.
Rows: 394. Required rows passing in the recorded runs: 391.
Recorded runs: `/Users/james.maes/Git.Local/QRunIO/qqq-frontend-next/test-results/visual-review/rc8-corrected-full-chromium/gate.json` (2026-10-01T01:03:05.420Z), `/Users/james.maes/Git.Local/QRunIO/qqq-frontend-next/test-results/visual-review/rc8-corrected-ci-chromium/gate.json` (2026-10-01T01:23:18.171Z).
Corrected RC8 source cd3d473: 602 local and 602 hosted Chromium cases passed, with zero failed/skipped/flaky cases or report errors. Hosted run 36798799502. Three approved external-service exclusions remain. Earlier failed runs are retained and described in docs/releases/1.0.0-RC.8.md; this matrix records the corrected primary runs only. Other browser profiles and whole-product compatibility/visual approval remain separate.
The additional ESB integration scenarios and their focused three-browser results are recorded in [ESB acceptance](esb.md). The historical full-suite counts above predate those ten new matrix rows.
## Navigation, application structure, branding and icons
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| NAV-001 | Direct link to a table list renders seeded rows | success | | prime-test-database.sql | pass (chromium) | |
| NAV-002 | Sidebar lists every permitted top-level app once, by label, in backend order, with nested apps only under their parent | admin sees Miscellaneous, Nav Empty App, Nav Level One, People App, Sample Widgets Dashboard, Sharing Demo in backend order; Greetings App appears only under People App (regression: it was duplicated at top level); group children use metadata labels | noPets persona: Pet and Pet Note are absent from the sidebar; backend returns 404 for their metadata and 403 for a query | stock sample apps + NavigationFixtures (Nav Level One, Nav Empty App); persona noPets | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
| NAV-003 | Apps nested deeper than two levels render as nested sidebar groups; deep links expand every enclosing group | expand three nested groups and open Nav Deep Item; grid rows match nav_deep_item; deep link to /app/navDeepItem expands all three groups and marks the item current | | NavigationFixtures: navLevelOne > navLevelTwo > navLevelThree > navDeepItem | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
| NAV-004 | Sidebar click navigation to app homes, tables and processes with active-entry highlighting and collapse/expand | Dashboard is current on /app; click app, table, process; URL, content and aria-current follow; collapse hides children, expand shows them | | stock sample (Miscellaneous app) | pass (chromium) | |
| NAV-005 | Declared icons (QIcon name, color, path) render on navigation surfaces | sidebar glyph per declared Material name, color applied, image path loads; app home entries and section headings show declared icons; command palette entries show declared icons | no declared icon renders the type fallback glyph | sample icons (stars, person, widgets, share, pets, local_shipping, badge) + NavigationFixtures (path icon, colored icon, inventory_2, inbox, layers) | pass (chromium) | [#538](https://github.com/QRun-IO/qqq/issues/538) |
| NAV-006 | Hidden tables and processes never appear in navigation; direct links to hidden objects the user may not access show not-found and the backend denies them | sidebar, app home, landing page and command palette omit City, Greet People, Simple Sleep and Nav Hidden Note | /app/city and /app/greet show the not-found state; backend returns 404 and 403 | sample city (hidden table), greet and simpleSleep (hidden processes); NavigationFixtures navHiddenNote (hidden, NOT_PROTECTED) | pass (chromium) | |
| NAV-007 | A hidden table the user may access opens by direct link (hidden is not denied) | /app/navHiddenNote lists nav_hidden_note rows; breadcrumbs show its enclosing apps | | NavigationFixtures navHiddenNote (isHidden, PermissionLevel.NOT_PROTECTED) | pass (chromium) | |
| NAV-008 | App home sections list processes (Actions), reports (Reports) and tables (Data) by label with icons, and each entry navigates | Miscellaneous: Actions Sleep Interactive, Simple Throw; Data Carrier, Field Lab, Pet Species (labels, not names); section heading shows its label and declared icon; clicking an entry opens the table or process | hidden or unpermitted section entries are omitted (navHiddenNote, noPets persona) | sample greetingsApp and miscellaneous sections | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
| NAV-009 | App home table entries show the backend record count | each Data entry reads 'N total records' equal to the SQL count | | sample carrier, fieldLab, petSpecies, person, pet, petNote rows | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
| NAV-010 | App homes of apps with child apps, only processes, or nothing render without errors | People App shows Clone People and the Greetings App child app (regression: crashed on missing widgets); Nav Level One lists its child app and navigates to it; Nav Empty App shows the empty state | viewer persona: People App shows only the Greetings App child app (no denied process) | sample peopleApp; NavigationFixtures navLevelOne, navEmptyApp; persona viewer | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
| NAV-011 | A dashboard app (widgets only) opens from navigation and shows one labelled container per permitted declared widget | sidebar click opens the dashboard; one widget region per declared widget label, in order | | sample SampleWidgetsDashboard (11 widgets) | pass (chromium) | |
| NAV-012 | Branding: sidebar logo (Material parity), app name, favicon from the icon, accent color, and partial branding accepted without warnings | sidebar shows /samples-logo.png with alt QQQ Sample, loaded, linking to the dashboard; favicon link points to /kr-icon.png; primary color variables equal the branding accent color; no 'schema validation' console warning for branding without companyName/companyUrl | | sample branding (appName, logo /samples-logo.png, icon /kr-icon.png) + NavigationFixtures accentColor #1d4ed8 | pass (chromium) | [#539](https://github.com/QRun-IO/qqq/issues/539) |
| NAV-013 | Branding banners render in their slots (top of site, top of body, side nav under logo) with text or sanitized HTML, severity and colors | each slot shows its message in the right place with severity and colors from metadata | unknown slots are not rendered | NavigationFixtures banners: QFMD_TOP_OF_SITE (INFO text), QFMD_TOP_OF_BODY (WARNING HTML), QFMD_SIDE_NAV_UNDER_LOGO (SUCCESS, colors, additional styles) | pass (chromium) | [#539](https://github.com/QRun-IO/qqq/issues/539) |
| NAV-014 | Breadcrumbs show the full app hierarchy for tables, processes, records, create and edit pages, and each crumb navigates | /app/person: People App / Greetings App / Person; /app/navDeepItem/2/edit: Nav Level One / Nav Level Two / Nav Level Three / Nav Deep Item / 2 / Edit; process and create pages; clicking a crumb opens that app home | no breadcrumbs on the dashboard root | sample person/greetInteractive; NavigationFixtures navDeepItem | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
| NAV-015 | Document title names the page, its enclosing apps and the application (Material parity) | Dashboard \| QQQ Sample; Person \| Greetings App \| People App \| QQQ Sample; record label title on the record page | unknown route title is Not Found \| QQQ Sample | stock sample | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
| NAV-016 | The site root redirects to the dashboard | / replaces itself with /app (back does not return to /) | | stock sample | pass (chromium) | |
| NAV-017 | Dashboard landing page: counts, quick actions and application cards derived from navigable, permitted metadata by label | counts equal the navigable tables, processes, apps and their widgets from backend metadata; quick actions create only navigable insertable tables and run only navigable processes, by label (regression: listed internal and table-bulk processes, crashed on apps without widgets); application cards open app homes | viewer persona: no create or process quick actions | stock sample + NavigationFixtures; persona viewer | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
| NAV-018 | Direct (hard-loaded) links open every route shape with the shell, sidebar state and breadcrumbs | app home, nested app home, table, process, record view, record edit, create, saved view | | stock sample + NavigationFixtures | pass (chromium) | |
| NAV-019 | Browser refresh keeps the route, its query string and the rendered content | refresh on a table with a query string, a record view, and an app home | | stock sample | pass (chromium) | |
| NAV-020 | Browser back and forward traverse navigation history with the right content | dashboard > app home > table > record, then back three times and forward twice | | stock sample | pass (chromium) | |
| NAV-021 | An unknown /app/{name} shows an in-shell not-found state with a link to the dashboard | /app/noSuchThing shows 'Page not found' naming the segment; the link opens the dashboard; no failed requests | | stock sample | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
| NAV-022 | Unknown deeper or top-level paths return the 404 page with a working home link | /app/person/1/no/such and /no-such-page respond 404 and render the 404 page; Go Home reaches the dashboard | only the document 404 is reported | stock sample | pass (chromium) | |
| NAV-023 | Record lookup by key: /app/{table}/key?{field}={value} opens the single matching record | ?code=NAV-B2 opens record 2 (Tide Chart); ?email=... opens that person | no match, several matches (?shelf=North), unknown field, no parameters: error message, no navigation | NavigationFixtures navDeepItem (unique key code); sample person emails | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
| NAV-024 | Command palette (Ctrl+K and '.') lists navigable apps, tables and processes by label with type and app context, filters, and navigates by keyboard | filter 'pet' shows Pet, Pet Note, Pet Species with type and app; Enter opens the first; '.' opens the palette; Escape closes it | hidden objects are not listed | stock sample + NavigationFixtures | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
| NAV-025 | Header search, '/' search dialog and search page jump to navigation targets and recently viewed records by label (matched locally, alongside record search when the backend offers it: NAV-030) | typing a label lists matching pages with app context; choosing one navigates; a viewed record is offered as a recent record; Enter without a selection opens /app/search?q= with the matches, surviving refresh; search dialog contains Tab and Shift+Tab with empty or populated results; Clear returns to the input; Escape and backdrop dismissal restore keyboard and pointer openers | no search request fails (regression: every keystroke produced a 404 from a missing /search endpoint); with no searchable tables no request is made at all (NAV-032) | stock sample | pass (chromium) | [#653](https://github.com/QRun-IO/qqq/issues/653) |
| NAV-026 | Mobile navigation drawer opens from the header, navigates and closes | hamburger opens the drawer; choosing a table navigates and closes it; close button and a backdrop tap close it; drawer controls are 44 px touch targets | | stock sample, 412x839 touch viewport | pass (chromium) | |
| NAV-027 | Developer page reports the backend metadata counts and the running framework versions | metadata counts equal the backend's; framework shows the bundled Next.js version (regression: hardcoded 'Next.js 15') | | stock sample | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
| NAV-028 | Reports in the app tree are listed under their app (sidebar and app home Reports group) and open the report page | sidebar entry with its icon opens /app/navDeepItemReport with the report heading, title and breadcrumbs; app home Reports group lists and opens it | | NavigationFixtures navDeepItemReport (child of Nav Level Three, icon assessment) | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
| NAV-029 | A user who may access no apps sees the Dashboard entry and the no-apps message (Material NoApps parity) | sidebar shows only Dashboard; landing shows You do not have permission to access any apps. | app home links show the not-found state | harness persona noApps (every permission except App HAS_ACCESS) - requested: not yet in AcceptanceSampleServer | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
| NAV-030 | Global search (header box, '/' dialog, search page) lists the records a backend record search (POST /qqq/v1/search over each table's searchFields) finds, after the matching pages, by record label with the table label; results open by keyboard and the search page survives refresh | '/' dialog: 'COCO' finds the pet Coco (label and table label), case-insensitively; ArrowDown and Enter open /app/pet/{id}; the request searches the readable searchable tables (person, pet, savedView) with a per-table limit of 5; header box: 'Sample' lists the matching people (database order, limit 5) after pages; Enter opens /app/search?q=Sample whose Records region lists every match (limit 25) before and after refresh; a link opens the record | a one-character term is not sent (unit), and a recent record already found is not listed twice (unit) | stock sample + NavigationFixtures (search fields on person, pet, savedView) | pass (chromium) | [#701](https://github.com/QRun-IO/qqq/issues/701) |
| NAV-031 | Record search respects table read permissions and record security locks, in the UI and the API | noPets: 'ey' lists the matching people only; the UI never asks for pets and the metadata advertises no pet search fields; admin: the backend returns the pets matching 'EY' for tableNames [pet]; alice finds her saved view 'Alice People View' with the saved view table label, in the UI and from the API; bob, with whom the view is shared read-only, finds it from the API (the lock admits shared readers) | noPets: POST /qqq/v1/search with tableNames [pet] returns no results; an unscoped search returns no pets; casey (neither owner nor shared reader): the locked saved view is not found in the UI or from the API | stock sample + NavigationFixtures + QueryFixtures (alice's saved view shared read-only with bob); noPets persona; sharing demo users | pass (chromium) | [#701](https://github.com/QRun-IO/qqq/issues/701) |
| NAV-032 | Without searchable tables (the backend declares no searchFields) global search stays local and never calls a search endpoint | the variant's v1 metadata lists no table with searchFields; header box, '/' dialog and search page keep the local labels and matches, with no Records group or region | no request to /qqq/v1/search or /search is made while typing, opening the dialog or loading the search page | security variant (stock sample + SecurityFixtures) | pass (chromium) | [#701](https://github.com/QRun-IO/qqq/issues/701) [#653](https://github.com/QRun-IO/qqq/issues/653) |
| NAV-033 | Login branding before sign-in: logo, app name, accent color, favicon and title from GET /qqq/v1/metaData/authentication (no session), never the banners | unauthenticated authentication metadata carries exactly appName, logo, icon and accentColor; after logout and a fresh load the login card shows the loaded logo and QQQ Sample; primary color variables and the Sign in button use the accent; favicon /kr-icon.png; title Sign in \| QQQ Sample | no banner text in the unauthenticated response or on the login page | sample branding (appName, logo /samples-logo.png, icon /kr-icon.png) + NavigationFixtures accentColor #1d4ed8 and three banners | pass (chromium) | [#703](https://github.com/QRun-IO/qqq/issues/703) [#539](https://github.com/QRun-IO/qqq/issues/539) |
| NAV-034 | Material table-scoped URLs: /app/{table}/{process} opens the table process (or an instance process); /app/{table}/{id}/{process} runs it for that record and returns to it | /app/person/person.bulkEdit?recordsParam=recordIds&recordIds=1,2 replaces itself with the run (init sends 1,2; Back returns to the table); /app/person/prcQuickTask opens the instance process with returnTo the table; /app/person/1/clonePeople runs for record 1 (init and SQL clone) and Return lands on the record; /app/person/2/bulkEdit resolves the table process by suffix; Cancel returns to the record | /app/person/1/noSuchProcess shows the in-shell not-found state and starts no process; report URLs (/app/{table}/{report}) are not covered: the backend frontend report metadata (QFrontendReportMetaData) has no tableName, so no report can match (#732) | sample person.bulkEdit, clonePeople (table processes), prcQuickTask (ProcessesFixtures, instance process) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) [#732](https://github.com/QRun-IO/qqq/issues/732) |
| NAV-035 | Process runs launched from a record view or a filtered query return there (Return and Cancel honor returnTo) | record Actions > Clone People runs for the record and Return lands on the record; Bulk Edit from /app/person?filter=isEmployed: returnTo is the launching path and query; after the edit (SQL) Return restores the same filter and the filtered rows | the unmatched record (Morgan) stays out of the returned grid | sample person, clonePeople, person.bulkEdit | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| NAV-036 | Material app settings (MaterialDashboardAppMetaData): showAppLabelOnHomeScreen and includeTableCountsOnHomeScreen turned off hide the home-screen title and the table counts | v1 metaData carries supplementalAppMetaData.materialDashboard for the app; The app home has no visible title (a screen-reader-only h1 remains) and the table entry shows no count; No count request is made for the table | Apps without the settings keep their title and counts (Material defaults) | NavigationFixtures.java navQuietHome app (navQuietItem table) with MaterialDashboardAppMetaData(false, false) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| NAV-037 | Mobile navigation drawer is a keyboard-usable modal dialog: focus moves in on open, Tab stays inside, and Escape, the close button or a chosen link close it and return focus to the menu button | Enter on the menu button opens the Navigation dialog with focus inside; Tab and Shift+Tab visit every drawer control and never reach the page behind; Escape, the close button and choosing Carrier close it; focus returns to the menu button; Carrier opens | Tab never leaves the open drawer (QRun-IO/qqq#708: focus stayed on the menu button; Firefox could not reach the drawer) | stock sample (Miscellaneous app), 412x839 touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| NAV-038 | Shell on phones and tablets: skip link, header controls, breadcrumbs, navigation entries, user menu, app homes and not-found links are 44 px touch targets, and the page never scrolls sideways | skip link is the first Tab stop and 44 px tall when shown; header, breadcrumbs, sidebar or drawer entries and the user menu meet the 44 px target; no sideways page scroll with a four-level breadcrumb trail | under a mouse (pointer: fine) the desktop sizes are unchanged | stock sample + NavigationFixtures (navDeepItem three apps deep); mobile (Pixel 7) and tablet (iPad, desktop layout with touch) projects | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| NAV-055 | Material Dashboard CSS and test hooks remain available on navigation, banners, app homes, forms, record views and query controls | Material data-qqq-id and class selectors identify corresponding Next UI controls; application CSS keyed to those selectors changes the rendered style; record and query actions retain the expected behavior and button variants | | stock sample with NavigationFixtures and RecordsFixtures; application CSS applied in css-hooks.spec.ts | pass (chromium) | [QRun-IO/qqq-frontend-next#731](https://github.com/QRun-IO/qqq-frontend-next/issues/731) |
| NAV-056 | The Material Dashboard theme from versioned QQQ metadata changes Next UI tokens, application CSS and the branded header | v1 metaData publishes the selected Material theme settings; the rendered dashboard applies its primary token, branded header colors and tagline, and custom CSS; Every theme scenario resets instance metadata before navigation; Preferences exposes accessible Light and Dark choices; keyboard changes, real reload persistence, reset and light/dark dialog accessibility are verified; Application theme override is explained in Preferences without erasing the saved browser appearance choice; Pivot validation meets text contrast in default light, default dark and a configured orange error theme; API playground visible text and hovered controls meet contrast in light/dark themes; Preferences changes update the rendered palette without reload | | AcceptanceSampleServer /acceptance/theme; reset removes the theme before each test | pass (chromium) | [QRun-IO/qqq-frontend-next#719](https://github.com/QRun-IO/qqq-frontend-next/issues/719) |
## Performance budgets on the production export: large tables, heavy dashboards and long processes (QRun-IO/qqq#710). Bundle budgets are checked separately by scripts/perf-budget.mjs.
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| PRF-001 | large table: a 10,000-row, 40-column table opens with every column and the exact first page within budget | all 40 columns have a sort header; the first 25 rows carry the exact computed values; the total reads 10,000; time from navigation to the first page is within PERFORMANCE_BUDGET.firstPageMs | only one page of rows is in the DOM (25 rows, 1,000 cells), not the table | tests/acceptance/fixture/PerformanceFixtures.java prfWide | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
| PRF-002 | large table: 250-row pages of 40 columns render, page and reach the last page within budget | page size 250 shows ids 10000..9751 with 10,000 cells within PERFORMANCE_BUDGET.pageMs; next page shows 9750..9501 and last page 250..1 (page 40), each within PERFORMANCE_BUDGET.pageMs; the backend is asked for skip/limit 250 | the DOM never holds more than one page (250 rows) | tests/acceptance/fixture/PerformanceFixtures.java prfWide | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
| PRF-003 | large table: sorting and filtering 10,000 rows return exact results within budget | sorting by Name ascending shows Row 00001..Row 00025 within PERFORMANCE_BUDGET.queryMs; Count 04 equals 0 narrows to the 40 matching rows (the SQL answer) within PERFORMANCE_BUDGET.queryMs | | tests/acceptance/fixture/PerformanceFixtures.java prfWide | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
| PRF-004 | heavy dashboard: 24 widgets (statistics, bar, line and pie charts, 100-row tables) load once each within budget | every widget finishes loading within PERFORMANCE_BUDGET.dashboardMs of navigation; statistics show their exact counts, chart data tables their exact points, tables their 100 rows | each widget's data is requested exactly once | tests/acceptance/fixture/PerformanceFixtures.java prfDashboard | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
| PRF-005 | long process: a 10-second job over 10,000 rows reports progress, polls at a bounded rate and shows its result promptly | progress messages advance while the job runs; the result shows Row Count 10000 and the SQL total; the result appears within PERFORMANCE_BUDGET.processResultMs of the job finishing | status is polled no more than once per POLL_INITIAL_MILLIS (1.5 s) and never concurrently | tests/acceptance/fixture/PerformanceFixtures.java prfLongRun | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
## Processes: steps, components, lifecycle, bulk load/edit/delete
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| PRC-001 | Selected-record run: record ids reach the backend, screen values are submitted, mixed view and record-list results render | recordIds 1,2 -> setup prefix/suffix -> results view values and both greeted rows | | sample greetInteractive | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) [#656](https://github.com/QRun-IO/qqq/issues/656) |
| PRC-002 | All-records run from a filter selection with no criteria | filterJSON {criteria:[]} greets every seeded person (count checked against SQL) | | sample greetInteractive | pass (chromium) | |
| PRC-003 | Zero-record runs from a filter that matches nothing | clonePeople input 0, validation on 0 with no lines, 0 processed | greetInteractive record-list step with zero records is refused ("Missing input records.") | sample greetInteractive, clonePeople | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
| PRC-004 | Processes launched from an app (no records) | app shortcut opens Component Lab without records | greetInteractive without records is refused at init ("Missing input records.") | prcLab app, sample greetInteractive | pass (chromium) | |
| PRC-005 | Process launched from selected rows of a table query | select Avery and Casey, Actions -> Greet Interactive -> both greeted | | sample person + greetInteractive | pass (chromium) | |
| PRC-006 | Minimum and maximum input records | 1-2 selected records run and are listed | 0 or 3 explicit records show a message without init; filter selecting 5 shows the backend refusal; API filter selecting 0 is refused | prcBounds (min 1, max 2) | pass (chromium) | [#662](https://github.com/QRun-IO/qqq/issues/662) |
| PRC-007 | Every declared component renders on one screen, in declared order, with one action bar | HELP_TEXT, HELP_TEXT, VIEW_FORM, EDIT_FORM, HTML, EDIT_FORM in order | | prcComponents mixed screen | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
| PRC-008 | HELP_TEXT with line breaks and previewText toggle | plain lines; Show/Hide preview text | | prcComponents | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
| PRC-009 | EDIT_FORM field subsets (includeFieldNames), section labels and persisted input values | Lab Inputs section holds only its fields; values saved (SQL prc_lab_run) | | prcComponents | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
| PRC-010 | Required input validation on screens and in the backend | filled required field proceeds | empty required field blocks without a request; API step without the required input is refused | prcComponents | pass (chromium) | |
| PRC-011 | Process possible-value fields | options from the process route; chosen id saved, label shown on review; search filters options | | prcComponents labColor (prcColor enum) | pass (chromium) | [#667](https://github.com/QRun-IO/qqq/issues/667) |
| PRC-012 | VIEW_FORM labels and backend values | Introduction and Status values | | prcComponents | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
| PRC-013 | HTML component renders the step html value, sanitized | markup rendered | script and onerror removed, never executed | prcComponents mixed.html | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
| PRC-014 | DOWNLOAD_FORM delivers the generated file | download bytes equal the generated file | unregistered file path refused (403) | prcComponents compute step | pass (chromium) | |
| PRC-015 | Step help content for process screens | help text shown | | prcComponents helpContent (PROCESS_SCREEN) | pass (chromium) | |
| 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 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) |
| PRC-023 | Completion: Next/Submit labels, Return on the last screen or when noMoreSteps | Submit on the step before the last screen; Return only on the last screen; noMoreSteps ends early | | prcComponents, prcWizard, prcEarlyFinish | pass (chromium) | |
| PRC-024 | Linear stepper and step position | all step labels; current step marked; Step i of n | | prcComponents | pass (chromium) | |
| PRC-025 | RECORD_LIST paging and page size over process records | 1-10, 11-20, 21-23; page size 25 | next disabled on the last page | prcManyRows (23 generated records) | pass (chromium) | |
| PRC-026 | Validation review: input count, validate-or-skip choice, Next/Submit label, record preview | Input: 2 Person records.; radio switches the button label; preview 1 of 2 / 2 of 2 | | sample clonePeople | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
| PRC-027 | Full validation summary lines (OK, WARNING, ERROR) with record links | four lines with counts and messages; link filters to the refused record | nothing persisted by validation | sample clonePeople + inserted clone persons | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
| PRC-028 | Process summary results after execution and persisted effects | result lines and processed count; clones persisted (SQL); skip validation executes immediately | refused and nested records not cloned | sample clonePeople | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
| PRC-029 | Bulk edit: switched-on fields only, review and result | edited fields persisted for selected records only (SQL); result lines | no switched-on field blocks without a request | enricher person.bulkEdit | pass (chromium) | [#656](https://github.com/QRun-IO/qqq/issues/656) [#659](https://github.com/QRun-IO/qqq/issues/659) |
| PRC-030 | Bulk delete: review, validation and result | selected records removed (SQL) | validation deletes nothing | enricher person.bulkDelete | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
| PRC-031 | Bulk insert: upload, column mapping, value mapping, profile, review preview and result | uploaded CSV mapped and inserted (SQL) | missing required file blocks | enricher person.bulkInsert + prcUploadArchive | pass (chromium) | [#656](https://github.com/QRun-IO/qqq/issues/656) [#660](https://github.com/QRun-IO/qqq/issues/660) [#543](https://github.com/QRun-IO/qqq/issues/543) |
| PRC-032 | Bulk load mapping validation and Back through mapping and upload | Back from review keeps the mapping; Back from mapping to upload | unmapped required column blocks without a request | enricher person.bulkInsert | pass (chromium) | [#655](https://github.com/QRun-IO/qqq/issues/655) [#660](https://github.com/QRun-IO/qqq/issues/660) |
| PRC-033 | Bulk edit with a file matched by key fields | rows matched by Id updated (SQL) | | enricher person.bulkEditWithFile | pass (chromium) | [#656](https://github.com/QRun-IO/qqq/issues/656) [#660](https://github.com/QRun-IO/qqq/issues/660) |
| PRC-034 | Upload instructions HTML with downloadable template | template CSV downloads with the header row | | enricher person.bulkInsert upload.html | pass (chromium) | |
| PRC-035 | Named WIDGET fetched with the process UUID and values | widget text built from process values | | prcWidgets prcHtmlWidget | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
| PRC-036 | Named composite WIDGET seeded from process values | seeded blocks rendered, no fetch | | prcWidgets prcCompositeWidget | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
| PRC-037 | Ad hoc composite WIDGET: interpolation, conditional blocks, action buttons, submit-on-enter inputs | Approve submits actionCode (SQL); Enter submits the scan code (SQL) | conditional block hidden | prcWidgets ad hoc blocks | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
| PRC-038 | GOOGLE_DRIVE_SELECT_FOLDER screen keeps its other components working | help text and edit form submit (SQL); picker explains it is not configured | required export note blocks | prcDrive | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
| PRC-039 | Google Drive folder selection through the Google Picker | pick a folder and submit its id | | needs GOOGLE_APP_CLIENT_ID, GOOGLE_APP_API_KEY and a Google account | excluded (Approved by James 2026-09-25 (QRun-IO/qqq#649): Google Drive folder picker needs a real Google OAuth client and account; the rest of the step is covered by PRC-038; real-service testing is follow-up work.) | |
| PRC-040 | Reload mid-process starts a fresh run | new process UUID, first screen, empty inputs; earlier effects kept (SQL) | | prcComponents | pass (chromium) | |
| PRC-041 | Expired session mid-process returns to sign-in | 401 redirects to login | nothing saved | prcComponents + expired persona | pass (chromium) | [#668](https://github.com/QRun-IO/qqq/issues/668) |
| PRC-042 | Process permission enforcement | revoked permission shows the permission message | step refused, nothing saved; direct link offers no process; API init refused (403) | prcComponents + noProcesses persona | pass (chromium) | |
| PRC-043 | Default process values from the link (defaultProcessValues) | sleepMillis preset drives an async sleep to screen1; labName prefilled; normal navigation shows a required sleep duration with a 1000ms default and completes to screen1 | clearing sleep duration blocks submission on screen0 | sample sleepInteractive, prcComponents | pass (chromium) | |
| PRC-044 | Scanner-format screens: no heading or action bar, input blocks submit | auto-focused input submits on Enter (SQL) | no Next/Cancel buttons rendered | prcScanner (format scanner) | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
| PRC-045 | State-machine step flow: repeated screens, no linear stepper | screen repeats with fresh inputs and counted rounds; noMoreSteps ends with Return | no stepper or Step i of n | prcLoop (STATE_MACHINE) | pass (chromium) | |
| PRC-046 | Saved bulk load profiles: save, choose for a later load, delete | Save As stores the mapping (SQL); choosing the profile restores the mapping and is named on review; Delete removes it (SQL) | a duplicate name is refused | SavedBulkLoadProfileMetaDataProvider on owned saved_bulk_load_profile tables | pass (chromium) | [#660](https://github.com/QRun-IO/qqq/issues/660) |
| PRC-047 | A run with no frontend screens completes with a completion screen | completion heading and message; backend effect persisted (SQL) | | prcQuickTask (backend step only) | pass (chromium) | |
| PRC-048 | Process-field possible values load and search through the v1 API | the process combobox loads its options from the v1 process route; the chosen value reaches the process and persists; a v1 search returns the exact match | an unknown process field is 404 | prcComponents | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| PRC-049 | Processes run on the v1 process routes (upload, step, back step, records, metadata) | a bulk load uploads its file, maps, reviews, steps back (isStepBack=true) and inserts through /qqq/v1/processes/... and /qqq/v1/metaData/process/...; the inserted row is in the database; this session can cancel its own run | an unsafe upload filename is 400 before anything is stored; an unknown run is refused; (another session's run is refused 403 by records and cancel - backend unit tests) | person.bulkInsert, prcComponents | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) [#543](https://github.com/QRun-IO/qqq/issues/543) |
| PRC-050 | Processes the instance adds to every query and record screen (Material processNamesToAddToAllQueryAndViewScreens) | v1 /metaData names the process in supplementalInstanceMetaData.materialDashboard (the only supplemental entry v1 publishes; the unversioned route's javalin server configuration stays off v1); casey receives it with no table and in no app (API); the person query Actions menu lists it last, after a divider, and runs it on the selected rows with tableName=person (SQL); the record actions menu of qryBin (no processes of its own) runs it on that record (SQL); the phone record action sheet runs it on that record (SQL) | a user whose metadata omits it (alice): not offered on query or record screens and its run page is not found (no SQL rows); noProcesses persona: absent from the metadata, not offered on query or record screens, and the backend refuses it (403, no SQL rows); a user whose metadata omits the process gets an empty v1 processNamesToAddToAllQueryAndViewScreens list (no hidden process names); the v1 and unversioned init routes refuse the process for a user without permission | ProcessesFixtures.java prcTagRecords (no table, in no app) named by MaterialDashboardInstanceMetaData; a metadata customizer lists it only for the sample user casey, so other areas keep their menus; person, qryBin | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| PRC-051 | Dependent possible values on a process screen: the choices follow the screen's other values | every search posts the screen values as the values map of the v1 JSON body; choices narrow to the chosen Category and change when it changes; the chosen id is recorded (SQL) and its label shows on the result screen | Category empty: the unset variable makes no matches for a form, shown as No options found; the backend refuses an out-of-filter id lookup and returns only the filtered choices | prcSpecimenPick specimenId (prcSpecimen source filtered on category = ${input.category}) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| PRC-058 | Saved bulk load profile menu (Material SavedBulkLoadProfiles): new profile, update after the confirm, rename, save as a copy, delete, unsaved-change count with its list, Reset All Changes, New Bulk Load Profile, Reset to Empty or Suggested Mapping | a new profile is named in the Save New Bulk Load Profile dialog and Enter saves it (SQL row owned by the user); the heading names it; a change shows 1 Unsaved Change with the list of changes; Save asks Update Existing Bulk Load Profile and Enter confirms; the same row holds the new mapping (SQL); Rename keeps the row id under the new name; Save As stores a separate copy, which is then in use; a saved profile's changes are listed (header row, file column) and Reset All Changes restores its mapping; New Bulk Load Profile leaves the profile for an empty mapping; Reset to Suggested Mapping and Empty Mapping replace the mapping, and the suggested one is what the review screen shows | Enter in the delete dialog does not delete (SQL count unchanged); only the Delete button does, and the screen starts a new, empty mapping | enricher person.bulkInsert; SavedBulkLoadProfileMetaDataProvider on owned saved_bulk_load_profile tables | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
| PRC-059 | Saved bulk load profile ownership: yours vs shared with you, and only the owner may save, rename or delete a profile | the query process returns both profiles with their owners; the menu lists alice's under Your Saved Bulk Load Profiles and bob's under Bulk Load Profiles Shared with you; on bob's profile Save, Rename and Delete are disabled with the not-owner reason, the changes list says why, and no Save link is offered; Save As is enabled; alice saves her own copy (SQL row owned by sample:alice) | bob's row is unchanged (label, owner and mapping, SQL) and no store or delete request named it; the backend has no ownership lock on saved bulk load profiles, so the UI is the only guard, as in Material | sample users alice and bob; bob's profile stored through storeSavedBulkLoadProfile and shared READ_ONLY with alice in shared_saved_bulk_load_profile | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
| PRC-060 | Bulk load file mapping form fidelity: field help, column tooltips, repeated-header warning, header-row toggle re-mapping by header name, scroll to the first error, bulk-edit Key Fields / Fields To Update and the unmapped key field error | help for the header row, layout and table key fields shows under each and describes its control; a mapped column's tooltip names its fields; a repeated header is flagged and not offered as a column; without a header row columns are letters; a mapping to the repeated header is cleared with a warning when headers come back; off and on again the step posts header names (First Name, Last Name, Email) and the first Email column is inserted (SQL); an unmappable submit brings the first field error into view; bulk edit shows Key Fields (Select table key fields to continue.) and Fields To Update (Clear if empty only there); choosing Id names it as an unmapped key field until a column is chosen; the matched records update (SQL) | no step request is sent while a field has no column | enricher person.bulkInsert and person.bulkEditWithFile; ProcessesFixtures.primeBulkLoadHelp help content keyed process:person.bulkInsert;field:hasHeaderRow\|layout and process:person.bulkEditWithFile;field:tableKeyFields | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
| PRC-061 | Typed bulk load default values, searchable value mapping and the grouped, searchable Add Fields menu (Material QHierarchyAutoComplete) | the Person default value is chosen by searching person possible values and shows its label; Birth Date takes a date; Is Employed takes Yes or No; the Add Fields menu groups Pet and Pet Note, searches, disables mapped fields with a tooltip and adds the WIDE note field twice (Pet Note: Note (1) and (2)); species values are mapped by searching the species possible values; the pets, their person, species and birth date and both notes each are inserted (SQL) | a default value left empty is refused with A value is required. before any step request | enricher pet.bulkInsert (pet with its notes association, WIDE layout) and person.bulkInsert | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
| PRC-062 | Composite INPUT_FIELD metadata editors share process validation and multipart submission | Choose a labeled possible value, edit currency and multiline code, update a widget filter without changing columns, upload BLOB and FILE_UPLOAD data and verify backend-read content; Desktop wheel scrolling over a plain numeric block preserves its value; intentional keyboard edits and touch input changes are returned by the backend process review | Required uploads block submission; code-editor Enter does not submit the process | prcBlockEditors | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
| PRC-063 | INPUT_FIELD autofocus and trimmed Enter values on subsequent process screens | Plain and metadata editors receive configured focus, placeholders and previous process values; Enter saves trimmed text verified in SQL; ordinary result screens receive heading focus | First blur does not show premature validation; required whitespace cannot advance either input screen | prcInputFocus | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
| PRC-064 | DATE_TIME process inputs display local time and submit UTC instants | Unchanged seeded timestamps preserve seconds and instant across plain/shared blocks and regular form fields, including the repeated autumn DST hour; Edited timestamps crossing daylight saving submit UTC values verified from backend SQL; TIME inputs in plain/shared blocks and forms retain seconds for unchanged and edited submissions, verified through SQL readback. | Zoned values must not disappear from datetime-local controls or lose their timezone on submission | prcInputDateTimes with browser timezone America/New_York | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
| PRC-065 | Configured Google Drive shared-folder selection and process submission | Select a shared-drive folder and submit ID, name, token and other required input through the real process; independently verify database outcome | Reject a file and retry; cancel a replacement selection clears the destination and token; missing required note cannot advance | prcDrive with owned Google SDK script responses and real configured backend CSP | pass (chromium) | [#704](https://github.com/QRun-IO/qqq/issues/704) |
| PRC-066 | INPUT_FIELD case behaviors preserve caret and submitted process values | Backend uppercase/lowercase metadata transforms a middle-of-value edit without moving the caret; Enter and Submit persist exact values verified through independent SQL; Multiline TEXT case conversion preserves the middle-edit caret and inserted newline; Submit persists the exact transformed text through independent SQL; Unicode case expansion preserves continued typing in STRING and TEXT fields; submitted values are verified through independent SQL; Explicitly configured PASSWORD case behaviors preserve masking and show/hide values; Enter and Submit persist Unicode-expanded values verified through independent SQL | Typing alone does not submit or persist the process; Multiline Enter does not submit or navigate away | prcInputCase / prcTextCase | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
## Querying, relationships, saved views, export and bulk selection
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| QRY-090 | Column menu sorts, pins, opens a field filter and hides a column | Sort matches SQL; Pin right is reflected in the grid; Filter opens the field editor; Hide removes the column | | QueryFixtures.java qryItem | pass (chromium) | [#716](https://github.com/QRun-IO/qqq/issues/716) |
| QRY-091 | Column menu copies page or full query values in backend sort order | Page copy contains the first ten ids; Full copy contains all eleven ids | | sample carrier (11 rows) | pass (chromium) | [#716](https://github.com/QRun-IO/qqq/issues/716) |
| QRY-092 | Column chooser searches, groups, counts and marks changed layouts | Search narrows fields; Groups collapse and expand; Hiding a field marks the columns dirty; Keyboard opening focuses the popup and Tab reaches search; Explicit close and Escape restore Columns focus; outside click keeps the clicked control focused | Unknown search shows an empty message | QueryFixtures.java qryItem | pass (chromium) | [#716](https://github.com/QRun-IO/qqq/issues/716) |
| QRY-093 | Last saved view and unsaved edits survive reopening and reload | Dirty filter survives reload; Table route opens the last saved view; Reset restores the backend view; Displayed filter updates the URL before a delayed page navigation completes; immediate reload preserves it, along with unrelated parameters, hash and history length | | sample Alice People View | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
| QRY-094 | Counted quick views render and open their saved-view route | Count reflects the backend filter; Quick-view button opens the filtered view | | sample Alice People View marked as a quick view in H2 | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
| QRY-095 | A new query defaults to fifty rows per page | Fifty visible rows; Pagination reports fifty of the full count | | QueryFixtures.java qryManyRow (1,234 rows) | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
| QRY-096 | A saved view reconciles removed fields and warns the user | Obsolete criteria and sort are removed before querying; Warning names the removed field; Heading names the active saved view | | sample Alice People View with retiredField references in H2 | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
| QRY-001 | Record list renders metadata labels and all records in the default primary-key-descending order | Headers use field labels; Rows equal SQL select ordered by id desc; Possible-value cells show labels, booleans Yes/No, money/decimals formatted; Original Next uniform row backgrounds in light and dark mode, with opaque pinned cells and selection highlighting | Null boolean renders empty, not No; Hidden and heavy (TEXT) fields are not columns | QueryFixtures.java qryItem (8 rows, every field type) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) [#691](https://github.com/QRun-IO/qqq/issues/691) |
| QRY-002 | Pagination with Material page sizes and URL state | Page 2 shows the remaining rows; Page sizes 10/25/50/100/250; Page and size survive reload via URL | Next is disabled on the last page | sample (person, pet, petNote, carrier, petSpecies) carrier (11 rows) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
| QRY-003 | Column header sort cycles ascending, descending and default; sort kept in the URL | Ascending and descending match SQL order by; Sort persists across reload | Third click returns to the PK-descending default | QueryFixtures.java qryItem | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
| QRY-004 | Column visibility and order persist per table | Hide/show and reorder columns; Order and visibility survive reload; Show/hide all per table group | Another table keeps its own columns | QueryFixtures.java qryItem | pass (chromium) | |
| QRY-005 | Column widths, density and card view | Resized width persists; Density changes row height; Card view shows the same records | Keyboard resize separator exposes its value | QueryFixtures.java qryItem | pass (chromium) | |
| QRY-006 | Opening a record and returning keeps the query; refresh shows backend changes | Filter, sort and page are restored after Back; Refresh shows a row changed through the API | No stale cached rows after refresh | QueryFixtures.java qryItem | pass (chromium) | |
| QRY-007 | A failed query reports the backend error with a retry | Backend error text shown; Retry reloads after the failure clears; Query error text and the front error notification meet text contrast in light, dark and configured application themes; Retry repeats the backend request; hover remains readable; Dismiss hides the error; resetting the invalid filter restores SQL-verified records | No rows from the failed request are shown | QueryFixtures.java qryItem | pass (chromium) | |
| QRY-010 | Text operators match Material labels and backend semantics | equals, does not equal, contains, starts/ends with, is any of, is like, is empty/not empty; results equal SQL | does not equal includes empty values (NOT_EQUALS_OR_IS_NULL); Incomplete criteria are not sent | QueryFixtures.java qryItem.name/code | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-011 | Number operators including ranges and lists | equals, greater/less (or equal), is between, is any of; results equal SQL; Values sent as numbers | not between and none of exclude the bounds/list | QueryFixtures.java qryItem.quantity/price | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-012 | Date operators and relative date expressions | is/after/before/between with dates; today, N days ago, start of this/last month send backend expression JSON and match SQL | Clearing an expression restores a plain value input | QueryFixtures.java qryItem.receivedDate (relative to today) | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-013 | Date-time operators convert local input to UTC and support relative values | Local input sent as UTC instant; now and N hours ago expressions match SQL | Grid shows formatted date-times, not raw ISO | QueryFixtures.java qryItem.checkedAt | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) [#679](https://github.com/QRun-IO/qqq/issues/679) |
| QRY-014 | Boolean, blob and long-text operators | equals yes / equals no / is empty for booleans; blob is empty / not empty; long text contains | Blob offers no value operators | QueryFixtures.java qryItem.isActive/photo/notes | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-015 | Possible-value selectors for table and enum sources show labels and send ids | is / is any of / is none of with label chips; Link-loaded ids render labels; Results equal SQL | Search with no match shows no options | QueryFixtures.java qryItem.ownerId (person), speciesId (petSpecies enum) | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) [#667](https://github.com/QRun-IO/qqq/issues/667) |
| QRY-016 | AND/OR and nested groups combine as declared | AND group, OR group and nested subfilter match SQL | Removing the last criterion clears the filter | QueryFixtures.java qryItem | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-017 | Filters live in the URL: reload, back/forward and Material-style JSON links | Reload keeps the filter; Back/forward switches filters; Plain JSON filter link opens filtered | Malformed filter param falls back to no filter | QueryFixtures.java qryItem | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-018 | Backend-only operators from links render and filter correctly | Each operator from a link renders its label and returns the SQL rows | FALSE matches no records; builder does not crash | QueryFixtures.java qryItem | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-019 | Quick search combines with the advanced filter | Search term ANDs with Is Active = yes | Clearing search restores the filtered rows | QueryFixtures.java qryItem | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
| QRY-020 | One-side exposed join: labelled columns, filters and sorts match SQL | Join column 'Owner: First Name' hidden by default, shown on demand; Join filter and sort match SQL LEFT JOIN | Joins only sent when used; rows without a match are kept | QueryFixtures.java qryItem -> person (MANY_TO_ONE) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) [#684](https://github.com/QRun-IO/qqq/issues/684) |
| QRY-021 | Many-side exposed join repeats rows and reports the distinct count | Repeated rows per note; Count shows (N distinct) | Row selection dedupes repeated ids | QueryFixtures.java qryItem -> qryItemNote (ONE_TO_MANY) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
| QRY-022 | Column statistics aggregate the filtered rows | Value distribution and count/distinct/sum/avg/min/max equal SQL under the filter | Sort by value/count | QueryFixtures.java qryItem (QUERY_STATS) | pass (chromium) | [#691](https://github.com/QRun-IO/qqq/issues/691) |
| QRY-023 | Tables without QUERY_STATS offer no column statistics | No statistics button on headers | Process not launched | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#691](https://github.com/QRun-IO/qqq/issues/691) |
| QRY-030 | Selection menu: this page, full query result, first N, clear | Banners and counts for page, all and subset; Touch opens and toggles the density menu without synthetic trigger focus dismissing it; Enter, Escape and outside search focus retain their normal behavior; Pinned selection targets remain exposed and respond at both edges for every density | Subset dialog rejects invalid N | sample (person, pet, petNote, carrier, petSpecies) carrier | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
| QRY-031 | Bulk Edit launches with the checked record ids | recordIds param equals checked rows | No selection shows 'No records were selected to Bulk Edit.' | sample (person, pet, petNote, carrier, petSpecies) carrier | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
| QRY-032 | Bulk Delete of the full query result sends the whole filter | filterJSON has no skip/limit; backend recordCount equals SQL count | Current page paging is not sent | sample (person, pet, petNote, carrier, petSpecies) carrier (page 2) | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
| QRY-033 | First-N selection under a filter launches with that limit | filterJSON limit N with the filter and sort | N larger than the result is capped | QueryFixtures.java qryItem | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
| QRY-034 | Actions menu offers bulk load, bulk processes and table processes | Bulk Load/Edit/Edit With File/Delete then table processes; Launch carries the selection | Process min/max record checks block launch with a message | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) [#662](https://github.com/QRun-IO/qqq/issues/662) |
| QRY-035 | Viewers get no create, bulk or process actions and the backend refuses them | No Create, bulk or write process entries | Direct insert/delete API calls return 403 | viewer persona on sample (person, pet, petNote, carrier, petSpecies) | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) [#671](https://github.com/QRun-IO/qqq/issues/671) |
| QRY-040 | CSV export has visible columns in order and exactly the filtered, sorted rows | File name, headers and rows equal SQL; Join columns exported | Hidden columns excluded | QueryFixtures.java qryItem | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
| QRY-041 | XLSX and JSON exports carry the same rows | XLSX sheet and JSON array equal SQL rows | JSON keys limited to exported fields | QueryFixtures.java qryItem | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
| QRY-042 | Nothing to export disables the formats | Empty filter result disables CSV/XLSX/JSON | No export request sent | QueryFixtures.java qryItem | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
| QRY-043 | Backend refuses exports of unreadable tables | Export of a readable table succeeds | Export of pet returns 403 without data | noPets persona | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
| QRY-050 | Your saved views are listed and open on their own route | 'Your Saved Views' lists backend views; /savedView/{id} applies filter/columns | Empty group message | sample sharing demo (Alice People View) | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
| QRY-051 | Saving a new view stores filter, sort, columns and page size | saved_view row viewJson matches Material RecordQueryView | Unsaved-changes indicator clears after save | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
| QRY-052 | Update, rename and delete your view | Update keeps unrelated view JSON; Rename and delete change the saved_view row | Reset All Changes restores the stored view | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
| QRY-053 | Duplicate names and unknown views are reported without changes | Duplicate name error shown in the dialog | Unknown view id shows an error; saved_view unchanged | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
| QRY-054 | A view shared with you opens read-only and the backend refuses changes | Listed under 'Views Shared with you' for bob; Opens with its filter | Owner-only actions disabled; storeSavedView by bob refused | QueryFixtures.java (Alice People View shared with sample:bob READ_ONLY) | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
| QRY-060 | Variant table asks for a variant, queries with it and remembers it | Picker lists /variants; Rows equal the chosen store; Choice remembered and changeable; export uses it | Filter narrows options | QueryFixtures.java qryStock on qryVariantMemory (qryStore) | pass (chromium) | [#690](https://github.com/QRun-IO/qqq/issues/690) |
| QRY-061 | Dismissing the variant prompt queries nothing; backend requires a variant | Prompt shown with a choose button | No query sent; API query without a variant fails | QueryFixtures.java qryStock | pass (chromium) | [#690](https://github.com/QRun-IO/qqq/issues/690) |
| QRY-062 | A table without count, export or write capabilities offers none of them | Rows listed without a total; Export disabled with explanation; No create or bulk actions | Backend insert refused | QueryFixtures.java qryLedger (no TABLE_INSERT/UPDATE/DELETE/EXPORT/COUNT) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) [#688](https://github.com/QRun-IO/qqq/issues/688) |
| QRY-063 | Enum-backed Pet Species table is read-only | Metadata omits write capabilities; no Create or bulk writes | Insert returns 403, not 500 | sample (person, pet, petNote, carrier, petSpecies) petSpecies | pass (chromium) | [#692](https://github.com/QRun-IO/qqq/issues/692) |
| QRY-064 | Backend refuses count and export for a table without those capabilities | Query still succeeds | v1 count, legacy count and export return 403 | QueryFixtures.java qryLedger | pass (chromium) | [#693](https://github.com/QRun-IO/qqq/issues/693) |
| QRY-065 | Export streams from the v1 export route | the export menu posts {format, filename, fieldNames, filter} to /qqq/v1/table/{table}/export and downloads every row; the v1 route streams the same rows as CSV | a table without the export capability is refused (403) by the v1 route | qryItem, qryLedger | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| 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-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) |
| QRY-081 | Add quick filter accepts metadata fields and remembers a chosen field | A field offered by metadata appears as a chip after selection and reload; Joined and virtual fields query SQL-backed records | Field choice is scoped to this table | sample person and QueryFixtures.java qryItem/qryCaseItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
| QRY-082 | Basic mode cannot simplify repeated or nested advanced conditions and explains why | Repeated Name conditions disable Basic with a focusable reason tooltip; URL criteria become chips; OR criteria open Advanced with a reason | Clicking Basic does not discard either condition | QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
| QRY-083 | Advanced query shows and removes individual conditions (Next inline editor) | Name and Quantity conditions filter SQL rows; deleting Name in the inline editor preserves Quantity | Removing one condition does not reset the whole query | QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
| QRY-084 | Sort picker changes field and direction; clearing filters requires confirmation and keeps the sort | Name ascending order matches SQL before and after confirmed clear; Condition status explains missing and complete values on keyboard focus | Cancel leaves conditions in place | QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
| QRY-085 | Weekday criteria use the metadata-enabled date function and ISO weekday values | Selecting a weekday sends WeekdayOfDate and returns the SQL-matching rows; Date-time weekday sends WeekdayOfDateTime with configured UTC arguments and returns matching rows | The weekday list uses names rather than bare numbers | QueryFixtures.java qryItem with dated rows and Material weekday settings; AcceptanceSampleServer.java H2 date functions | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
| QRY-086 | Relative date presets expose their evaluated date and filter records | Seven days ago renders a live date and returns the recent SQL-backed rows; A date-time preset retains its relative expression, evaluates on focus and returns recent rows | A relative value remains an expression instead of fixed text | QueryFixtures.java qryItem with relative dates | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
| QRY-087 | Bulk pasted values become a multi-value filter | Two pasted record names return the matching SQL-backed rows | The dialog previews each value before adding it | QueryFixtures.java qryItem | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
| QRY-088 | Instance query help accompanies the bulk paste dialog | The dialog displays the configured help text | Help text comes from instance metadata | QueryFixtures.java instance-level QUERY_SCREEN help | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
| QRY-089 | String filter input honors field case behavior from v1 metadata | Uppercase typing is normalized and queries the lowercase stored code | The normalized value remains visible in the input | QueryFixtures.java qryCaseItem code with TO_LOWER_CASE | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
| QRY-008 | Phone card list: readable records, touch-sized selection, query selections and paging | One card per record with the SQL ids and names, values wrap instead of being cut off; Tapping the 44 px label around a card checkbox selects it without opening the record; A full-query selection checks every card; paging and rows per page work on cards; Tapping a card opens its record | No horizontal page scroll and no control below 44 x 44 px | sample (person, pet, petNote, carrier, petSpecies) carrier (11 rows), 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| QRY-009 | Phone filter sheet fits the viewport, scrolls inside and manages focus | Filter opens a modal sheet that takes focus; Many conditions scroll inside the sheet, which stays inside the viewport; Cards behind the sheet show the filtered records (SQL); Escape or Close closes the sheet and returns focus to Filter, which counts the conditions | No horizontal page scroll and no control below 44 x 44 px in the sheet | QueryFixtures.java qryItem, 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| QRY-024 | Column configuration is reachable and usable by touch on phones and tablets | The panel opens inside the viewport with touch-sized controls; Hiding a column removes it from the cards (phone) or the grid (tablet) and persists; Escape closes the panel and returns focus to its button | The panel never runs off the left edge of a phone | QueryFixtures.java qryItem | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| QRY-036 | Bulk actions launch from a phone card selection | Checked cards show the selection banner; The Actions menu fits the phone; Bulk Edit launches with exactly the checked ids | No horizontal page scroll while the menu is open | QueryFixtures.java qryItem, 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| QRY-055 | Saved views and export menus fit the phone and are touch-sized | The saved views menu lists your view inside the viewport; Escape closes it; The export menu fits the phone and downloads a CSV | No horizontal page scroll and no control below 44 x 44 px in either menu | sample sharing demo (Alice People View), 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| REL-001 | Possible-value cells show the related record label and link to it | Owner cell shows the person label; Link opens /app/person/{id} | Enum values are not links | sample (person, pet, petNote, carrier, petSpecies) pet.personId | pass (chromium) | [#691](https://github.com/QRun-IO/qqq/issues/691) |
| REL-002 | Parent lists exactly its own children; View All opens the filtered child list | Panel rows equal SQL children; View All filters /app/pet and Back returns | Parent without children shows an empty panel | sample (person, pet, petNote, carrier, petSpecies) person -> pets | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
| REL-003 | Adding a child from its parent links it and shows it immediately | New pet stored with person_id of the parent; Panel refreshes | Join field not editable; no other rows changed | sample (person, pet, petNote, carrier, petSpecies) person -> pets | pass (chromium) | |
| REL-004 | Three-level associations: each pet shows only its own notes | Notes equal SQL per pet | Other pets' notes absent | sample (person, pet, petNote, carrier, petSpecies) person -> pet -> petNote | pass (chromium) | |
| REL-005 | Editing and deleting a child leaves no stale parent, list or unexpected rows | Renamed pet shown in parent panel and list; Deleting a pet removes its notes | Other rows unchanged | sample (person, pet, petNote, carrier, petSpecies) pet, petNote | pass (chromium) | |
| REL-006 | Denied child reads show an unavailable panel without data or actions | Panel says related records are unavailable | No data, View All or Create; API query 403 | noPets persona; qryShelter whose associated qryShelterPet is denied with DenyBehavior.DISABLED | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
| REL-007 | Aliased and composite associations bind to their widgets and list the right members | childRecordList and rowBuilder show SQL-matching members; Composite join on code + review date | Household without members shows empty | QueryFixtures.java qryHousehold/qryMember (members, care group / primary, scheduled reviews) | pass (chromium) | |
| REL-008 | Adding through a composite association stores every join value | New member stored with household_code and review_date from the parent | Aliased association unaffected; other rows unchanged | QueryFixtures.java qryHousehold/qryMember | pass (chromium) | |
| REL-009 | Deleting a parent removes associated children and leaves other parents intact | Person and its pets removed | Other pets and notes unchanged; list reflects SQL | sample (person, pet, petNote, carrier, petSpecies) person -> pets | pass (chromium) | |
## Records, fields, editors, adornments, validation and persistence
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| REC-001 | Record view shows the record label, tiered sections and backend-formatted values | label heading; T1 header fields; T2 section cards; currency, grouped integer, Yes/No, date | values match independent SQL readback | prime-test-database.sql (person 1) | pass (chromium) | |
| REC-002 | Empty values render a placeholder; hidden fields and hidden sections are never shown | NULL date shows an em dash; hidden field absent from view and from the GET payload; hidden section not rendered | hidden value stays stored (SQL) | person 4 (NULL birth date); recordLab hiddenCode + hidden legacy section | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) [#680](https://github.com/QRun-IO/qqq/issues/680) |
| REC-003 | Record view of a missing record shows Record Not Found and a way back | not-found panel; back to the table list | 404 read is the only failed request; no generic error toast | person 999 (absent) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| REC-004 | Record view tabs and list view expose every visible section | Overview shows all T2 sections; Overview field rows retain original Next typography and spacing in light and dark themes; a section tab shows only that section; list view shows every section; Arrow/Home/End keys move tab focus; Enter/Space activates a named panel with one tab stop; phone accordion supports keyboard activation | | fieldLab record created in the test | pass (chromium) | |
| REC-005 | Create form validates required fields in the browser before any request | required markers and messages for each required field | no insert request, no row | person | pass (chromium) | |
| REC-006 | Create persists the entered values and opens the new record | success toast; navigates to /app/person/<new id>; view shows the entered values | non-editable fields are not offered on create; row matches SQL readback | person | pass (chromium) | |
| REC-007 | Backend enforces required fields when the UI is bypassed | direct insert without Name is rejected with the backend message | no row written | fieldLab | pass (chromium) | |
| REC-008 | Cancel from create and edit, with the unsaved-changes guard | clean cancel returns immediately; dirty cancel asks Stay/Leave; Stay keeps typed values; Leave discards | no insert or update request; no row created or changed | person | pass (chromium) | |
| REC-009 | Edit loads stored values, saves only what changed, and shows the updated record | fields prefilled from the record; Save disabled until a change; view shows the new value without reload | only the changed field is submitted; other columns unchanged in SQL | person 1 | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) [#676](https://github.com/QRun-IO/qqq/issues/676) |
| REC-010 | Edit shows non-editable fields read-only and never submits them | read-only controls with the stored values | controls are disabled; create date unchanged in SQL after save | person 1 (id, createDate, modifyDate) | pass (chromium) | [#680](https://github.com/QRun-IO/qqq/issues/680) [#537](https://github.com/QRun-IO/qqq/issues/537) |
| REC-011 | Edit of a missing record shows the load failure instead of a form | error message, no form fields | 404 is the only failed request | person 999 | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| REC-012 | Delete asks for confirmation, removes the record and returns to the list without errors | dialog names the record; Cancel keeps it; Delete removes it and returns to the list; success toast | no Resource not found toast; no 404 read of the deleted record; row gone in SQL | person 5 | pass (chromium) | [#541](https://github.com/QRun-IO/qqq/issues/541) |
| REC-013 | Copy prefills a new record from the source and saves it as a separate record | source values prefilled without the key; Full Copy loads the descendant draft on selection; returning to Base Copy preserves the base form; save creates a new row and opens it; source unchanged | unique-key conflict on copy shows the backend message and keeps the form; download-URL files are not copied | person 1; recordLab 1 | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) |
| REC-014 | STRING and TEXT editors round-trip single and multi-line text | single-line input; multi-line textarea keeps line breaks in view and SQL | | fieldLab | pass (chromium) | |
| REC-015 | INTEGER, LONG and DECIMAL editors keep exact values | LONG beyond 2^53 saved and shown exactly; DECIMAL with 4 places | fractional LONG rejected in the browser; backend rejects a fractional LONG sent directly | fieldLab | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
| REC-016 | BOOLEAN editor applies the metadata default and saves true/false | create form starts at the default (true); switching to false persists false; view shows Yes/No | unchanged boolean is not rewritten on edit | fieldLab (booleanValue default true) | pass (chromium) | [#680](https://github.com/QRun-IO/qqq/issues/680) |
| REC-017 | DATE and TIME editors round-trip values and clearing | date input value and view; time input value and backend 12-hour display; clearing a date stores NULL | | fieldLab | pass (chromium) | |
| REC-018 | DATE_TIME is entered and shown in the viewer's time zone and stored in UTC | typed local time stored as the UTC instant; view shows yyyy-MM-dd hh:mm:ss AM EDT; edit form shows the local time | unchanged date-times are not rewritten; non-editable date-times read-only | fieldLab; browser zone America/New_York | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
| REC-019 | Zoned DATE_TIME display behaviors (fixed zone and per-record zone) | fixed America/Chicago display; display in the record's own timeZone; daylight-saving boundary | invalid zone falls back to UTC | fieldLab | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
| REC-020 | HTML field edits in the rich-text editor and renders sanitized | formatted text rendered on the view | script content is not executed | fieldLab htmlValue | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) |
| REC-021 | PASSWORD is masked on read and never overwritten by the mask | view shows the mask; typing a new password saves it | editing another field keeps the stored password (SQL) | fieldLab passwordValue | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) |
| REC-022 | BLOB upload and download without a download adornment | file chosen on create is stored byte-for-byte; view offers the bytes as a download | | fieldLab blobValue | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-023 | Dynamic default values (user id, create day, modify day, explicit NONE) | user id and dates filled on create; modify day updated on edit, create day kept | NONE leaves the value empty | fieldLab | pass (chromium) | |
| REC-024 | Max-length policies: truncate, ellipsis, pass-through and reject | truncate, ellipsis and pass-through stored as declared | reject shows the backend message, keeps the form and writes nothing | fieldLab (maxLength 8) | pass (chromium) | [#680](https://github.com/QRun-IO/qqq/issues/680) |
| REC-025 | Case and whitespace normalization on save | upper, lower, trim variants, remove-all-whitespace and unchanged stored as declared | | fieldLab | pass (chromium) | |
| REC-026 | Numeric range policies: reject, exclusive reject, inclusive clip and exclusive clip | clip to 0/100 and 0.01/99.99 | out-of-range and exclusive-boundary values show the backend message and write nothing | fieldLab | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| REC-027 | Unique keys are enforced with the backend message on create and edit | distinct values save | duplicate Name on create; duplicate normalized key on edit; form keeps values, row unchanged | fieldLab (name, normalizedKey) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| REC-028 | LINK adornment: record links and URL links | toRecordFromTable opens the referenced record; URL opens in a new tab (target _blank) | a non-http value is not turned into a link | recordLab (ownerId, website) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-029 | CHIP adornment colors and icons by value | label is the display value; color from color.<value>; icon from icon.<value> | undeclared value uses the default color | recordLab status | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-030 | SIZE adornment sets the query grid column width | column is 100px wide | undeclared columns keep the default width | recordLab shortCode (width small) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-031 | CODE_EDITOR adornment: code view with formatting and a code editor on forms | view shows the code and its language; Format JSON / Reset Format; editor saves edited code | invalid JSON reports a formatting error | recordLab config (json) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-032 | RENDER_HTML adornment renders sanitized HTML | markup rendered | script removed and not executed | recordLab htmlNote | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-033 | REVEAL adornment masks, reveals and copies the value | masked by default; reveal shows the exact stored value; hide masks again | | recordLab apiToken (PASSWORD + REVEAL) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-034 | FILE_DOWNLOAD adornment links to the stored file with its name | file name from the companion field; file name from the format; open and download return the stored bytes | record without a file shows no link | recordLab attachment (heavy, fileNameField) and notesFile (fileNameFormat) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-035 | FILE_UPLOAD adornment formats, replacing and removing a stored file | drag-and-drop zone and button formats; current file shown on edit; new file replaces bytes and file name | removing the file clears bytes and name | recordLab attachment (dragAndDrop), notesFile (button) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) [#680](https://github.com/QRun-IO/qqq/issues/680) |
| REC-036 | TOOLTIP adornment shows its text on the value | tooltip on hover and keyboard focus | | recordLab hint (staticText) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) [#685](https://github.com/QRun-IO/qqq/issues/685) |
| REC-037 | WIDGET adornment renders the field value as the named widget | widget content from the record | the value is not offered as an editable field | recordLab summaryWidget | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-038 | ERROR adornment highlights the value | value shown with error styling | | recordLab problem | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
| REC-039 | Field help content follows screen roles and formats | view: READ_SCREENS text and VIEW_SCREEN HTML; edit: WRITE_SCREENS markdown rendered and EDIT_SCREEN text; create: INSERT_SCREEN text | help for other screens is not shown | recordLab title/website/ownerId help | pass (chromium) | [#681](https://github.com/QRun-IO/qqq/issues/681) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| REC-040 | Section help content on the record view | HTML help under the section heading | | recordLab presentation section | pass (chromium) | [#681](https://github.com/QRun-IO/qqq/issues/681) |
| REC-041 | Table help content slots are delivered in table metadata | v1 table metadata carries the slot help | | recordLab summary slot | pass (chromium) | |
| REC-042 | Audit history lists who changed what, newest first | edit then open Audit: message, user and per-field sentences; audit rows independently verified in SQL | unaudited record shows no audits | recordLab (FIELD audits) | pass (chromium) | [#678](https://github.com/QRun-IO/qqq/issues/678) |
| REC-043 | Audit history without process permission reads the audit table | same entries via the audit table query | no process request is made | recordLab; persona noProcesses | pass (chromium) | [#678](https://github.com/QRun-IO/qqq/issues/678) |
| REC-044 | Record label format is used for the record's title and dialogs | heading; delete dialog; audit title | | recordLab (Lab: %s) | pass (chromium) | [#678](https://github.com/QRun-IO/qqq/issues/678) |
| REC-045 | Possible-value editor shows stored labels, looks up defaults and selects by keyboard | edit shows the stored label, not the id; create default shows its label; keyboard search and select saves the id | | recordLab status/ownerId | pass (chromium) | [#667](https://github.com/QRun-IO/qqq/issues/667) |
| REC-046 | Read-only persona sees no write actions and the backend refuses writes | no Edit, Copy or Delete; create and edit URLs show the permission message | direct insert, update and delete are 403 and change nothing | persona viewer | pass (chromium) | |
| REC-047 | Forms are labelled and keyboard operable | every control has a label; keyboard-only create saves the record | invalid fields expose aria-invalid and a described error | person | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) |
| REC-048 | Server validation errors show the backend message once and keep the form | alert and toast show the backend message; typed values remain | no generic Something went wrong toast | fieldLab | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| REC-049 | pages of a table outside the app tree use the table label in the document title and breadcrumbs | create page title is "Create Scheduled Report \| Scheduled Report \| QQQ Sample"; record view title is the record label, then Scheduled Report | the table name (scheduledReport) is not shown in the breadcrumbs | scheduledReport (memory, not in the app tree) for savedReport 1 | pass (chromium) | [#707](https://github.com/QRun-IO/qqq/issues/707) |
| REC-051 | Table-field possible values search and resolve through the v1 API | owner search sends a v1 POST with the search term and lists the exact match; ids and labels resolve to the exact option; the chosen owner persists | a source table the persona cannot read is refused (403) by the v1 route; an unknown field is 404 | recordLab | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| REC-050 | Record create, view, edit and delete run on the v1 record routes | the create/edit/delete flow sends POST, GET, PATCH and DELETE to /qqq/v1/table/... and persists each step; record-v1 associations are written and read back with includeAssociations | a missing required field is a 400 with the backend message and nothing is written; a read-only persona is refused (403) insert, update and delete on v1 and the row is unchanged | person, fieldLab, qryHousehold | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| REC-056 | Record view hash links: #audit, #/launchProcess={process} and #{section} anchors | #audit opens the audit dialog; closing it clears the hash and a reload does not reopen it; #/launchProcess=person.bulkEdit replaces the URL with the run for the record (init recordIds=2); Cancel returns to the record; #employmentInfo selects the Employment Info tab and scrolls it into view; a later hash change selects Dates | | sample person, person.bulkEdit | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| REC-057 | Create presets from links: #/createChild= opens a child create dialog over the record; /create#/defaultValues=/disabledFields= prefills and locks | the backend-format #/createChild=pet link on person 3 opens Add Pet with Person preset and disabled; Create closes the dialog, clears the hash, submits personId=3 and SQL shows the pet linked to person 3; /app/pet/create#/defaultValues={name, personId}/disabledFields={personId} prefills both, locks Person and saves them (SQL); /app/pet/create#defaultValues={speciesId} (no slash) prefills Species without locking | locked fields are disabled; unlocked preset fields stay editable | sample person -> pet (personId), petSpecies | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| REC-052 | Dependent possible values: a field whose filter uses ${input.otherField} offers only the choices matching the form's current values | every search posts the form values as the values map of the v1 JSON body; create: choices narrow to the chosen Category and change when it changes; edit: the stored Item label shows, a new Category changes the choices; the chosen id persists (SQL) and the record view shows its label | Category empty: the unset variable makes no matches for a form, shown as No options found; an item outside the filter is not offered, even when searched by name; the backend refuses an out-of-filter id lookup and returns only the filtered choices | recPvPick itemId (recPvItem source filtered on categoryId = ${input.categoryId}) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| REC-053 | Table developer view shows the API Docs & Playground section for the table's application APIs | developer page for a table keeps its metadata sections and shows the API Docs & Playground section; with no application APIs it shows "This table is not available in any APIs." and no API or Version selectors; OpenAPI download and new-tab buttons remain fully visible before interaction at desktop, phone and 320px widths in light/dark modes; pointer/touch downloads yield the real specification and keyboard navigation opens its URL; specification labels fit wider application font metrics at desktop and phone widths without content clipping | a viewer persona sees the same empty state and cannot fetch any table spec | recordLab has no application APIs; person is exposed through the owned Acceptance API | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| REC-054 | Record developer view manages associated scripts: versions, new versions, test runs, logs, docs and script creation | versions list with CURRENT on the current revision and per-file code for the selected version; an older version offers Edit and Activate; save a new version with a commit message (SQL: new script_revision row, its file and script.current_script_revision_id; persists after reload); test run output and log lines equal the backend testScript response; a failing run shows the error the backend returns; logs list the script_log rows of the selected version with their lines; docs show the script type help text and sample code; create a script for an empty associated script field; local QQQ helper and keyword suggestions support keyboard and pointer/touch insertion, native undo/redo, both-theme accessibility, and SQL-verified revision persistence | a viewer cannot save or test a revision (no Edit or Test offered; the backend refuses storeScriptRevision and testScript); a viewer cannot create a script (no Create Script offered; the backend refuses the create route); a table without associated scripts shows none | scriptLab (RecordsFixtures: Alpha with script 101, revisions 101-102, logs 101-102; Beta without a script) on the scripts tables from WidgetsFixtures | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| REC-055 | Record view keyboard shortcuts n, e, c, d, a (Material) and their help text | e, c and n open edit, copy and create with the right values; a opens the audit history; d opens the delete confirmation, Escape keeps the row (SQL), confirming deletes it (SQL); ? lists Create a New Record, Edit/Copy/Delete/Audit the current Record with their keys | no shortcut fires while the audit or keyboard help dialog is open; keys typed in the Jump to input are text; viewer persona: n, e, c, d do nothing (a still works); the backend refuses insert and delete (403, SQL unchanged) | sample person | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| REC-058 | Record view on phones and tablets: the title stays readable, the header controls wrap onto their own row, and the phone action sheet fits, scrolls and keeps focus | The title stays on one line and never overlaps the view toggle, Audit or record actions; neither the page nor its main region scrolls sideways; The back link, value links and header controls are touch-sized (44 x 44 px) under a coarse pointer; The phone Actions sheet fits the screen, takes focus, keeps Tab and Shift+Tab inside, closes by Escape or a backdrop tap with focus back on its trigger, and runs Edit | A read-only user gets no phone Actions trigger (asserted in REC-046) | recordLab 1 (Lab: Alpha); phone (412 px touch), tablet (810 px touch) and desktop viewports | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| REC-059 | Record forms on phones and tablets: every editor type fits without sideways scrolling, controls are touch-sized, possible-value lists open on screen and the phone action bar stays reachable | Create Field Lab: no sideways scroll, every control at least 44 x 44 px by touch; on a phone Save stays on screen; boolean, date, time, text and rich text values persist; Edit Record Lab: the owner list opens inside the screen with touch-sized options; the picked owner persists | | fieldLab create (every field type), recordLab 1 edit (possible values, code editor, password reveal, file uploads) | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
| REC-061 | An editable dynamicForm widget on a record form validates its fields and persists them in the declared JSON record field | the region field starts from stored JSON and appears in the record edit form; changing the region persists the new JSON in inputValues and survives reload | clearing the required region blocks the write; the transient region field is absent from the table multipart payload | accWidgetHost 1 with accHostRecordVariables and hidden inputValues field | pass (chromium) | [QRun-IO/qqq-frontend-next#722](https://github.com/QRun-IO/qqq-frontend-next/issues/722) |
| REC-062 | Inline enum possible values filter by label prefix without a lookup request; CHIP adornments style options and chosen IDs persist | v1 metadata exposes the inline enum and the edit form shows the held label; a label-prefix search filters options and the CHIP adornment colors and icons them; choosing High stores HIGH in SQL and shows High after reload | a substring that is not a prefix offers no options; the picker makes no possible-values request | recordLab priority field with an inline enum and CHIP adornment | pass (chromium) | [QRun-IO/qqq-frontend-next#721](https://github.com/QRun-IO/qqq-frontend-next/issues/721) |
| REC-060 | Material table and field form adjusters and field rules operate against the real QQQ sample server | v1 table and field metadata expose the Material supplemental adjuster settings; the table onLoad route supplies a note before render; the name onChange route changes a field label and clears its value; the field rule clears status | a backend lock message disables Save for a preset name | FormAdjusterFixtures.java adjusterLab table with a table onLoad adjuster, name onChange adjuster and clear-target field rule | pass (chromium) | [QRun-IO/qqq-frontend-next#720](https://github.com/QRun-IO/qqq-frontend-next/issues/720) |
| REC-064 | Record sections render qualified fields from readable exposed joins | record GET sends a selected LEFT join with qryItemJoinPerson; the joined person's first name appears in the Details section | unreadable join paths are removed from sections and GET descriptors by the same permission filter as query pages | QueryFixtures.java qryItem joins person and shows person.firstName in Details | pass (chromium) | [QRun-IO/qqq-frontend-next#722](https://github.com/QRun-IO/qqq-frontend-next/issues/722) |
| REC-063 | Record view, edit and copy load from the table variant selected on the query screen | Selecting South Store shows S-KIWI on the record view and edit/copy forms; Every South record GET includes tableVariant {type: qryStore, id: 2}; Switching to North Store shows N-APPLE for the same record ID with variant id 1 | The duplicate ID must not show North Store's N-APPLE value | QueryFixtures.java qryStock (same record ID in North and South variants) | pass (chromium) | [QRun-IO/qqq-frontend-next#722](https://github.com/QRun-IO/qqq-frontend-next/issues/722) |
| REC-065 | Unchanged DATE_TIME instants and precision survive edit and copy | Unrelated edit omits unchanged timestamp; Copy sends the original zoned instant and preserves source/copy SQL timestamps | Copy must not select the first occurrence of a repeated hour or truncate fractional seconds | fieldLab; browser America/New_York; repeated DST hour with subsecond precision | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
## Permissions, sessions, authentication and interaction quality
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| SEC-001 | Table hidden from a user (DenyBehavior.HIDDEN): absent from navigation, direct link loads nothing, backend refuses | noPets sees Person but no Pet / Pet Note in navigation; person record neither shows nor requests the hidden pet association | direct link /app/pet shows unknown resource with no data request; API query/get of pet returns 403, table metadata 404 | tests/acceptance/fixture/AcceptanceSampleServer.java personas (noPets) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
| SEC-002 | Table disabled for a user (DenyBehavior.DISABLED): listed, but list/record/create/edit links explain the denial without loading data | admin lists the ledger rows; noPets sees the Security Ledger link | noPets list, record, create and edit links show the permission message and request no records; API query/get/insert/update return 403 and rows are unchanged | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityLedger, base name pet, noPets) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
| SEC-003 | Insert permission denied: create not offered, create link refused, backend writes nothing | viewer list has no Create button and the dashboard has no Create quick action | /app/person/create shows the permission message with no form; POST /data/person returns 403 and the person count is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
| SEC-004 | Edit permission denied: edit not offered, edit link refused, backend changes nothing | viewer record view has no Edit action | /app/person/1/edit shows the permission message without loading the record; PUT /data/person/1 returns 403 and the name is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
| SEC-005 | Delete permission denied: delete not offered on record or bulk bar, bulk delete refused, backend deletes nothing | record view and bulk action bar have no Delete | /app/person.bulkDelete is unknown to the viewer; DELETE /data/person/1 and bulkDelete init return 403; the row remains | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
| SEC-006 | Copy requires insert permission: copy not offered and copy link refused | viewer record view has no Copy action | /app/person/1/copy shows the permission message; nothing is inserted | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
| SEC-007 | Process permission denied (hidden): processes not offered in navigation or launcher, links do not start them, backend refuses | viewer has no Actions menu; noProcesses' Actions menu offers only the table's bulk actions (table rights) and no standalone process; no process links; noProcesses keeps table rights (Create offered) | /app/clonePeople and /app/greetInteractive are unknown and make no process request; legacy and v1 process init return 403; no person is cloned | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer, noProcesses) | pass (chromium) | |
| SEC-008 | Process disabled for a user (DenyBehavior.DISABLED): shown disabled, link explains the denial, backend refuses with no side effect | admin runs Security Audit and one audit row is written; viewer sees the audit tile disabled and no dashboard quick action for it | viewer direct link shows the permission message and never calls init; process init returns 403 and the audit log stays empty | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityAudit process) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
| SEC-009 | Widget restrictions: hidden widgets absent, disabled widgets show a permission state without loading data, backend refuses | admin sees both pet widgets with their content; noPets sees the open bulletin widget | noPets: hidden widget absent, disabled widget shows "You do not have permission to view this data." and no /widget request; GET /widget/<denied> returns 403 | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityPetWidget, securityPetDisabledWidget) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
| SEC-010 | App restrictions: hidden apps absent from navigation and unknown by link; disabled apps keep their children gated | admin sees both pet apps | noPets: Pet Vault App absent and /app/securityPetApp unknown; Pet Disabled App children stay permission-gated | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityPetApp hidden, securityPetDisabledApp disabled) | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) [#671](https://github.com/QRun-IO/qqq/issues/671) |
| SEC-011 | Record-level security (sharing demo): users see only their own or shared saved views/reports; direct links and writes to others are refused | alice lists and opens her own saved view | bob's list omits alice's view; bob's direct link shows not found; bob's GET/PUT/DELETE of alice's view are refused and the row is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (alice, bob) | pass (chromium) | |
| SEC-012 | Record security lock on a data table: list filtered to the key, other users' records unreachable, forged owner refused | alice sees and edits only her notes (persisted) | bob's direct link to alice's note shows not found; bob's update/delete of alice's note and insert with alice as owner are refused; SQL unchanged | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityNote locked by sample user) | pass (chromium) | |
| SEC-013 | Field protection: hidden fields never shown or returned, password fields masked and never overwritten by the mask, heavy fields not loaded in lists | REVEAL password can be revealed on request; editing another field keeps the stored password | hidden value absent from list, record, edit form and API responses; masked password shows only the mask; the list query does not carry the heavy BLOB | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityVault) | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) |
| SEC-014 | Capability restrictions: tables without insert/update/delete capabilities offer none of them and refuse direct links | admin can list and view the archive | no Create/Edit/Delete offered; create and edit links explain the table cannot be changed; backend insert/update/delete are refused; SQL unchanged | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityArchive) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
| SEC-015 | Permission revoked mid-session: stale UI actions are refused by the backend with a clear message, nothing is written, reload reflects the new rights | after reload the viewer rights are shown | saving an open edit form after revocation shows a permission error and the row is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (admin then viewer) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| SEC-020 | MOCK sign-in: a deep link establishes the session and shows the session user from manageSession values | lands on the requested page with rows; user menu shows Alice (sample) / sample:alice | | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#332](https://github.com/QRun-IO/qqq/issues/332) [#669](https://github.com/QRun-IO/qqq/issues/669) |
| SEC-021 | Logout: POST /qqq/v1/logout ends the session, protected pages stay on login, Sign in returns to the page, per-user client data is cleared | Sign in returns to the original page | after logout a protected page (full load) stays on login and reads no data; a different user signing in on the same browser never sees the previous user's cached pages or recent records | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
| SEC-022 | Session expiry: 401 redirects to login with returnTo and re-authenticates back to the page (initial load and mid-use) | expired on first load returns to the page; expired mid-use returns to the clicked record | no hang on "Setting up anonymous session" | tests/acceptance/fixture/AcceptanceSampleServer.java personas (expired) | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
| SEC-023 | Denied sign-in: a 401 from manageSession keeps the user out with an explanation (no silent anonymous fallback) | | login shows "Sign-in was denied", no navigation or data is rendered | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
| SEC-024 | Post-login returnTo is limited to in-app paths (open-redirect guard) | in-app returnTo with query string is honoured | absolute, protocol-relative, backslash and javascript: targets land on /app; paths that normalize to another origin (/.//host, /app/..//host, /app/../\host) land on /app | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) [#696](https://github.com/QRun-IO/qqq/issues/696) |
| SEC-025 | OAUTH2 authorization code + PKCE sign-in with an owned OIDC provider (discovery, authorize, callback, backend token exchange, session, reload resume) | authorize request carries S256 challenge, state, client id, scopes and /token redirect; backend exchanges code + verifier with client secret; user_session row stored; provider identity is displayed; reload resumes without the provider | | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| SEC-026 | OAUTH2 sign-in failures: provider denial, tampered state and failed code exchange are reported and create no session | | a state-matched access_denied shows the provider description once; a state mismatch shows a generic error and is rejected before any token exchange; a rejected code exchange shows the failure; no user_session row | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) |
| SEC-027 | OAUTH2 logout: backend session deleted, provider end-session called, next visit requires provider sign-in | user_session row deleted and provider session ended | protected page after logout returns to the provider login | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) [#674](https://github.com/QRun-IO/qqq/issues/674) |
| SEC-028 | OAUTH2 session revoked server-side mid-use: 401 sends the user through the provider and back to the page | returns to the same page after provider sign-in | revoked session gets 401, not stale data | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) |
| SEC-029 | AUTH_0 authorization code + PKCE with an owned Auth0-compatible provider (browser token exchange, backend JWT validation via JWKS) | sign-in and data access through the Auth0 module; logout returns through the provider | provider denial is reported | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=AUTH_0 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) |
| SEC-030 | AUTH_0 against a real Auth0 tenant | real tenant sign-in/logout | | Requires an Auth0 tenant QRun does not own in CI | excluded (Approved by James 2026-09-25 (QRun-IO/qqq#649): real Auth0 tenant; the AUTH_0 flow is covered against an owned Auth0-compatible provider by SEC-029/SEC-031; real-service testing is follow-up work.) | |
| SEC-031 | FULLY_ANONYMOUS authentication: session without an identity provider, anonymous identity, logout and sign-in again | deep link loads data as Anonymous | after logout the page stays signed out until Sign in | SecurityAcceptanceServer -Dqqq.security.auth=FULLY_ANONYMOUS | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
| SEC-032 | Unsupported authentication type (a type this UI does not know, as a newer backend could declare) is reported, never silently treated as another type | | login shows "Unsupported authentication type: ACCEPTANCE_UNKNOWN" and no data or session is requested | SecurityAcceptanceServer -Dqqq.security.auth=UNSUPPORTED (v1 authentication metadata names type ACCEPTANCE_UNKNOWN; TABLE_BASED is supported since QRun-IO/qqq#700, see SEC-034..SEC-037) | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) [#700](https://github.com/QRun-IO/qqq/issues/700) |
| SEC-033 | Server-side logout invalidation: v1 logout expires every session cookie it issued and an ended Auth0 session cannot be replayed | logout response expires sessionUUID and sessionId | a logged-out Auth0 sessionUUID replayed against the API gets 401 | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#674](https://github.com/QRun-IO/qqq/issues/674) [#675](https://github.com/QRun-IO/qqq/issues/675) |
| SEC-034 | TABLE_BASED sign-in: a deep link asks for username and password under the app branding, POST /qqq/v1/manageSession with Authorization: Basic creates a stored session, the session user is shown and a reload resumes it | login form shows the logo and app name before any session exists and reads no data; credentials travel once as Basic credentials (password with colons), never in the body or browser storage; lands on the requested page with rows; user menu shows Tess Table (table-based) / tess.table; one session row for the user, matching the sessionUUID cookie; reload keeps it | without the session cookie the backend refuses a read (401) | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
| SEC-035 | TABLE_BASED sign-in failure: a wrong password or unknown user is refused with one message, stores no session, and the form recovers | correct credentials after a refusal sign in to the requested page | empty fields are named and nothing is sent; wrong password and unknown user both show "Incorrect username or password."; no session row is stored and no data is read; the password field is cleared | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
| SEC-036 | TABLE_BASED session expiry: a session idle past the inactivity timeout gets a 401, the user signs in again and returns to the record they opened | expired mid-use returns to the clicked record after signing in again | the expired session is refused by the backend ("Session is expired.") | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) + /acceptance/expire-table-sessions | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
| SEC-037 | TABLE_BASED logout: POST /qqq/v1/logout deletes the stored session and clears both cookies, protected pages stay on the form, and the next user signs in as themselves | a different user signs in on the same browser and is shown as themselves | the ended session cannot be replayed with either cookie (401); after logout a protected page (full load) stays on the form and reads no data | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
| SEC-038 | Dashboard documents carry a strict Content-Security-Policy (script-src self plus the hash of each inline script, object-src none, base-uri/form-action self, frame-ancestors none), X-Frame-Options DENY, Referrer-Policy and Permissions-Policy | login, token, app, list, record, edit and 404 documents send the policy and headers; the script-src hashes equal the hashes of the inline scripts in each served document; pages hydrate and navigate under the policy with no violation | another web site framing the dashboard gets no content; an injected inline script and inline event handler do not run and are reported as violations | tests/acceptance/fixture/AcceptanceSampleServer.java (javalin mode); with QQQ_ACCEPTANCE_MODE=standalone the container image server (standalone/qqq-server.mjs) | pass (chromium) | [#695](https://github.com/QRun-IO/qqq/issues/695) [#734](https://github.com/QRun-IO/qqq/issues/734) |
| SEC-039 | The policy allows the configured identity provider origin (OAUTH2 discovery, AUTH_0 browser token exchange) and no other connection target | OAUTH2: connect-src is self plus the provider origin; sign-in completes with no violation; AUTH_0: connect-src is self plus the Auth0 domain; the browser token exchange completes with no violation | a fetch to any other origin is refused and reported (connect-src) | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2\|AUTH_0 | pass (chromium) | [#695](https://github.com/QRun-IO/qqq/issues/695) |
| SEC-040 | Application override hook and metadata-derived origins: the application adds its own origins (QApplicationJavalinServer.withNextDashboardSecurityHeadersCustomizer); QuickSight and custom component bundle origins come from widget metadata | the QuickSight embed, block image and audio from the loopback service and the custom component bundle load with no violation; the policy lists exactly the configured origins | a frame from an origin the application did not configure is refused and reported (frame-src) | tests/acceptance/fixture/WidgetsFixtures.java loopback service | pass (chromium) | [#695](https://github.com/QRun-IO/qqq/issues/695) [#734](https://github.com/QRun-IO/qqq/issues/734) |
| SEC-041 | The login page explains known sign-in error codes and never displays text taken from the link | direct links with access_denied and other known codes show their safe message; other well-formed codes show the code; Try again signs in through the provider | free text in ?error= is replaced by a generic message | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
| SEC-042 | Rendered HTML (field values, widgets, process steps, help, banners) cannot add forms, form controls, style sheets or page-covering positioning | formatting in a stored HTML value is kept; the stored value is unchanged in the database | a stored form with a password input, a style element and a position:fixed overlay render as plain content | tests/acceptance/fixture/RecordsFixtures.java fieldLab.htmlValue | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
| SEC-043 | Per-user browser data (recently viewed records, stored identity) is bound to the signed-in identity | the same user keeps their recently viewed records across reloads | a different user signing in on the same browser without a logout does not see the previous user's recent records | tests/acceptance/fixture/AcceptanceSampleServer.java personas and sample users | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
| SEC-044 | Session cookies (sessionUUID, sessionId) are SameSite=Lax for the whole site, and Secure when the request arrived over HTTPS | manageSession and API responses set the cookies with Path=/ and SameSite=Lax; the browser stores them as Lax | over plain HTTP the cookies are not Secure (sign-in keeps working); behind X-Forwarded-Proto https they are | tests/acceptance/fixture/AcceptanceSampleServer.java | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
| SEC-048 | Session cookies (sessionUUID, sessionId) are HttpOnly: script in the page cannot read them, and resuming a session from the cookie never hands its token to script | after sign-in (MOCK, TABLE_BASED, OAUTH2) the browser stores both cookies HttpOnly, naming the current session; a reload resumes the session through POST /qqq/v1/manageSession with an empty body; the response carries the session values and no uuid | document.cookie never shows sessionUUID or sessionId; an in-page POST /qqq/v1/manageSession {} (what injected script could send) answers without the session token | tests/acceptance/fixture/AcceptanceSampleServer.java (MOCK); SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED and OAUTH2 with tests/acceptance/support/fake-oidc.ts | pass (chromium) | [#733](https://github.com/QRun-IO/qqq/issues/733) |
| SEC-049 | With HttpOnly session cookies the backend ends and replaces sessions: logout expires both cookies, an idle-expired or revoked session sends the user to sign in again, and a new password or OAuth2 sign-in points both cookies at the new session | TABLE_BASED: a first visit shows the form without a session request; an idle-expired session is refused, the form returns without a resume attempt, and signing in again returns to the record; OAUTH2: a session revoked on the server sends the user through the provider and back, with new HttpOnly cookies; logout: the server deletes the session and expires both cookies; a protected page stays signed out without a session request | a logout that never reached the server leaves the earlier user's cookies; the next user's sign-in replaces both, and a reload keeps the new user | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED and OAUTH2 with tests/acceptance/support/fake-oidc.ts | pass (chromium) | [#733](https://github.com/QRun-IO/qqq/issues/733) |
| INT-001 | Keyboard: navigate from the sidebar to a list, open a record, edit and save without a pointer | edit saved by keyboard persists (SQL) | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) [#682](https://github.com/QRun-IO/qqq/issues/682) |
| INT-002 | Keyboard: run a process through its steps without a pointer | greetInteractive completes and shows results | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
| INT-003 | Dialogs trap focus, close on Escape and restore focus to the trigger; confirm by keyboard | keyboard delete confirmation deletes the row (SQL) | Escape cancels without deleting; focus returns to Delete | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#682](https://github.com/QRun-IO/qqq/issues/682) [#683](https://github.com/QRun-IO/qqq/issues/683) [#694](https://github.com/QRun-IO/qqq/issues/694) |
| INT-004 | Command menu, search and shortcut dialogs: keyboard open, focus inside, Escape restores focus | command menu navigates to a table | Escape closes and returns focus | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#682](https://github.com/QRun-IO/qqq/issues/682) |
| INT-005 | Automated accessibility scan (axe WCAG 2.1 AA) of login, dashboard, list, record, edit form and process step | no serious or critical violations on each page | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#682](https://github.com/QRun-IO/qqq/issues/682) |
| INT-006 | Form labels and ARIA: every field has an accessible label, required fields are marked, invalid fields announce their error | labels match metadata labels | submitting empty required fields sets aria-invalid and a described error | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
| INT-007 | Validation and error messages: client and backend validation name the field and nothing is saved | | missing required value is named and focused; backend rejection is shown verbatim; SQL unchanged | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
| INT-008 | Responsive layout at phone width (Pixel 7): navigation drawer, list, record and form usable without horizontal scrolling | drawer navigation to a list, open and edit a record on a phone viewport | no horizontal page overflow | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) |
| INT-009 | Loading states: a delayed list or record request shows a busy state, then the real data | busy indicator while the real request is held, then rows | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#694](https://github.com/QRun-IO/qqq/issues/694) |
| INT-010 | Failed request and recovery: a failed list request shows an error with retry; retry loads the data | retry succeeds | the failure is explained, not a blank page | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
| INT-011 | Delayed save: the submit control is disabled while saving and double submission creates one record | exactly one row inserted (SQL) | second click while pending is ignored | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
| 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
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|---|---|---|---|---|---|---|
| WID-001 | alert widget: severity from alertType, sanitized HTML and bullet list | WARNING alert renders strong HTML and each bullet as sanitized HTML | hideWidget and empty alerts render nothing (WID-063) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accAlert | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-002 | barChart widget: one bar per label in backend colors | bars carry per-point backgroundColors; accessible data table has exact values | | sample SampleWidgetsDashboard/SampleBarChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-003 | chart output from an untyped widget renders as a bar chart | metadata type absent, payload type chart, values rendered | zero-value month has no drawn bar | sample greetingsApp/PersonsByCreateDateBarChart | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-004 | divider widget renders a rule without card chrome | hr rendered, no label or card | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accDivider | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-005 | fieldValueList widget: labels, display values, prefix icons, indentation | display value wins, zero shown, icon color, indent | empty field list shows empty state (WID-052) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accFieldValueList | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-006 | generic widget renders its common payload fields (sublabel, footer) | sublabel and sanitized footer HTML shown | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accGeneric | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-007 | horizontalBarChart widget: horizontal bars with negative values left of zero | negative bar extends left of the zero line; height from payload | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accHorizontalBarChart | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-008 | html widget (no-code) renders sanitized user HTML | velocity output with i/b/u and centered wrapper | empty html renders an empty body (WID-052) | sample SampleWidgetsDashboard/SampleHTMLWidget | pass (chromium) | |
| WID-009 | lineChart widget: line with a point per label and description | points carry exact values | | sample SampleWidgetsDashboard/SampleLineChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-010 | smallLineChart widget: compact line with title and HTML description | title, strong description text, 5 points | | sample SampleWidgetsDashboard/SampleSmallLineChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-011 | location widget: image, title, description, address and footer | owned image loads from the owned service | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accLocation | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-012 | multiStatistics widget: groups with icon, header, subheader and statistics | every group and value matches the payload | empty group list shows empty state (WID-052) | sample SampleWidgetsDashboard/SampleMultiStatisticsWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-013 | multiTable widget renders each table with its label and rows | two labelled tables with their rows | empty table list shows empty state (WID-052) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accMultiTable | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-014 | pieChart widget: colored slices, legend values and subheader | slice fills, legend label: value, main number, up/good change | | sample SampleWidgetsDashboard/SamplePieChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-015 | quickSightChart widget embeds the provider URL (real renderer, owned local provider endpoint) | iframe src is the owned embed URL and its content renders | metadata never exposes the secret key | tests/acceptance/fixture/WidgetsFixtures.java repointSampleQuickSight (greetingsApp/QuickSightChartRenderer against the owned loopback endpoint) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-016 | statistics widget: count, context, colored percentage change and links | decrease with increaseIsGood is bad; increase with increaseIsGood=false is bad; count links to countURL | | sample SampleStatisticsWidget; tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accStatisticsGood | pass (chromium) | |
| WID-017 | stackedBarChart widget stacks datasets with colors and legend | bars of one label share x; dataset colors | | sample SampleStackedBarChartWidget; tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accStackedBars | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-018 | stepper widget: completed, current (with link) and upcoming steps | states and current link | empty steps show empty state (WID-052) | sample SampleWidgetsDashboard/SampleStepperWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-019 | table widget: headers, rows, alignment and fixed totals row | rows and sticky last row match payload | no rows shows noRowsFoundHTML or default (WID-052) | sample SampleWidgetsDashboard/SampleTableWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-020 | usaMap widget plots each marker with name and coordinates | 3 markers placed west to east by longitude; marker list | empty marker list shows empty state (WID-052) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accUsaMap | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-021 | process widget runs the sample interactive process inline | form fields render, submit, result greeting shows | viewer persona: process denied contained (WID-061) | tests/acceptance/fixture/WidgetsFixtures.java widgetProcess/accProcess (greetInteractive) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-022 | parentWidget renders its children in a 12-column grid | children render with their own data | tabs (WID-055), denied/missing children (WID-056) | tests/acceptance/fixture/WidgetsFixtures.java widgetParents/accParentGrid | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-023 | composite widget renders QQQ blocks (big numbers, up/down, links, tooltips) | every block value, direction, link and tooltip | no blocks renders an empty composite (WID-052) | sample SampleWidgetsDashboard/SampleBigNumberBlocksWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-024 | childRecordList widget without association binding lists joined child records | rows link to child records, Showing 2 of 3, View All filter link | host with no children shows empty state | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accWidgetHostJoinChild (maxRows 2) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#663](https://github.com/QRun-IO/qqq/issues/663) |
| WID-025 | customComponent widget loads its bundle and renders the named component | component receives widget metadata and data | missing bundle shows contained error (WID-062) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accCustomComponent (owned bundle) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-026 | cronUI widget shows the record's schedule expression, description and time zone | expression, backend description and time zone | record without expression shows No schedule set | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostCron | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-027 | dynamicForm widget shows labeled values from the payload | labels and values incl. 0 | | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostDynamicForm | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-028 | dataBagViewer widget lists versions and shows the selected version contents | newest version current and shown; selecting older version shows its JSON | bag without versions shows empty message | tests/acceptance/fixture/WidgetsFixtures.java dataBag/1 section accDataBagViewer (owned dataBag tables) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-029 | pivotTableSetup widget shows, edits and clears the saved report's pivot definition | rows/values labels from the source table; modal OK updates only the host draft; host Save persists rows, columns and aggregate values with SQL readback and reopen; keyboard row reorder and value move preserve saved configuration; turning the pivot off clears the stored definition on host Save | report without pivot shows message; missing row fields and aggregate functions keep the editor open; Cancel discards added fields without changing the database; used group/value fields are excluded from choices; changing a numeric value to text clears an incompatible aggregate | prime-sharing savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java owned pivot saved report | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-030 | filterAndColumnsSetup widget shows the saved report's filter, sort and columns | criteria with field labels and operator phrases, columns by label | report without filter shows No filters | prime-sharing savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java owned filtered saved report | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-070 | filterAndColumnsSetup edits a saved report's filter, sort, columns and variable values through the host form | Edit Filters and Columns opens the report setup dialog; OK writes queryFilterJson and columnsJson to the host form, and Save persists a changed sort to the backend; assigning a filter variable persists FilterVariableExpression in the backend JSON and shows ${VARIABLE} in the editor; import a saved view, reset changes to that view or reset to a new default draft without updating the saved view | Cancel discards the draft; OK without host Save leaves the backend row unchanged; preview and Open in new window block a query while a filter variable lacks a value; resetting and cancelling leave both the report and all saved views unchanged | prime-sharing savedReport 1 (Pet Species Report, no pivot table); saved-reports-sharing.spec.ts | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
| WID-071 | a WIDGET-adorned process form field renders filter setup and writes its own value | the widget request names the adorned field, the editor updates its filter, and the next process screen receives the JSON | other widget output fields are not written to the adorned field's host form | ProcessesFixtures.java prcFieldWidget with a queryFilterJson WIDGET adornment | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
| WID-072 | API-versioned report setup loads table metadata and previews through the application API | the report editor loads person metadata and its preview rows and count through the real API-aware v1 routes | an unsupported API version returns 404 | ApiVersionFixtures.java acceptanceApi 2026.Q3; savedReport 102; saved-reports-sharing.spec.ts | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
| WID-073 | report setup editor uses the full server-backed grid for paging, sorting and column sizing | grid preview hides selection, changing page size changes the real backend query, header sorting changes the real backend query, and keyboard column resize persists in saved columns JSON; create a report from a query, switch to Audit, save and reopen, verify stored columns and download the CSV; filtered column statistics totals and exported value counts match SQL; closing statistics and cancelling the editor leave saved data unchanged; a table without COUNT still shows real preview rows and page copy matches SQL; variant-backed report previews choose, switch and remember the backend variant for query/count, copy and statistics on ordinary and application-API routes; complete nested criteria mark preview column headers; opening the indicator edits the existing condition and real preview rows match SQL; configured omitted joins disappear from new preview column/filter choices while saved joined values, editable criteria and column positions survive; changed results and reload match SQL; editor Cancel, Basic/Advanced, OK, host Save and reopen preserve joined criteria, sorts, order and width; hidePreview hides only the read-only summary; the editor retains real query rows, sorting and Refresh; Cancel, OK, host Save and reopen preserve the intended sort with the flag enabled and disabled; column menus fit 320px and 720px windows; keyboard End/Escape restores focus and pointer Copy/full-query and sorting remain reachable | selection pseudo-column and incompatible previous-table fields are excluded from saved reports; full-column copy refuses results over the configured limit; an empty preview offers no column copy action; cancellation preserves the saved report; no query before selecting a required variant; an old variant copy response cannot overwrite a later copy; variant changes do not persist report edits; incomplete criteria do not mark columns; opening an indicator adds no condition; read-only preview edits and editor Cancel leave stored report JSON unchanged | savedReport 1 targeting petSpecies without a pivot; saved-reports-sharing.spec.ts | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
| WID-031 | rowBuilder widget without association binding shows its rows read-only | column labels from widget fields, row values incl. 0 | | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostRows | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#663](https://github.com/QRun-IO/qqq/issues/663) |
| WID-032 | scriptViewer widget lists script revisions and shows the selected revision's files | current revision marked and its file contents shown; selecting older revision | | tests/acceptance/fixture/WidgetsFixtures.java script/1 (owned memory script with 2 revisions) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-033 | quickSightChart against a real AWS QuickSight account and dashboard | real embed URL | | requires AWS credentials and a QuickSight dashboard (external service) | excluded (Approved by James 2026-09-25 (QRun-IO/qqq#649): real AWS QuickSight dashboard; the widget and renderer are covered against an owned loopback provider by WID-015; real-service testing is follow-up work.) | |
| WID-040 | dashboard renders app widgets in declared order sized by gridColumns (twelfths) | order matches metadata; 12 spans the grid; 4 is a third | | sample SampleWidgetsDashboard | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-041 | widget chrome: card vs plain (isCard=false), sublabel, metadata and payload footer HTML | plain widget has no card border; footers render | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accPlain, widgetGallery/accGeneric | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-042 | header icons (topLeftInsideCard/topRightInsideCard) with metadata colors | icon names, colors and placement | | sample SampleWidgetsDashboard | pass (chromium) | [#538](https://github.com/QRun-IO/qqq/issues/538) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-043 | widget label tooltip from metadata on hover and keyboard focus | hover and focus show, Escape hides | | sample SampleWidgetsDashboard/SampleBarChartWidget | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-044 | widget help content (HTML) on the label | help icon shows sanitized HTML help | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accHelp | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-045 | reload button re-renders the widget from the backend | render count increments per reload | no reload button without showReloadButton | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accReload | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-046 | export button downloads the widget's csvData as CSV | file bytes match Material quoting rules | no csvData: no-data message and no download | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accExport, accExportEmpty, accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-047 | PVS dropdown from the widget payload: selection is sent as a parameter and re-renders | options from payload, choice reaches renderer, clearing removes it | non-stored selection is not persisted | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accDropdownHtml | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-048 | DATE_PICKER dropdown sends the chosen date | date reaches the child renderer | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-049 | required dropdowns: the parent waits with the backend message until selections are made | message then children render with selections | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-050 | storeDropdownSelections persists selections across reload; stale stored options are dropped | localStorage qqq.widgets.dropdownData.<widget>.<param>; reload keeps selections | removed option in storage is not selected | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-051 | failing widget renderer shows a contained error with the backend message and retry | error message, retry re-requests, neighbor content | no error toast, no console error | tests/acceptance/fixture/WidgetsFixtures.java widgetStates/accError + accHealthy | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-052 | empty payloads show descriptive empty states for every display type | table, multiStatistics, charts, stepper, statistics 0, composite, multiTable, field values, map, html | | tests/acceptance/fixture/WidgetsFixtures.java widgetStates/accEmpty* | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-053 | malformed payloads are contained to their own widget | chart, table, composite, stepper, multiStatistics show a format notice | no crash, no console error, neighbors render | tests/acceptance/fixture/WidgetsFixtures.java widgetStates/accMalformed* | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-054 | widget permissions: a denied widget is absent from metadata, not rendered and not requested; the API denies it | admin sees it | noPets: no card, no request, GET /widget 403, renderer not called | tests/acceptance/fixture/WidgetsFixtures.java widgetPermissions/accDenied (pet.accDenied.hasAccess) with the noPets persona | pass (chromium) | |
| WID-055 | parent widget TABS layout switches tabs and remembers the selected tab | tab click, arrow keys, stored tab restored after reload | | tests/acceptance/fixture/WidgetsFixtures.java widgetParents/accParentTabs | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| WID-056 | parent widget skips denied and unknown children without requests | allowed children render | noPets: denied child absent and not requested; unknown child skipped | tests/acceptance/fixture/WidgetsFixtures.java widgetParents/accParentDenied, accParentMissing | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-057 | composite renders every block type with its values and styles | TEXT, BIG_NUMBER, UP_OR_DOWN_NUMBER, NUMBER_ICON_BADGE, ICON, TABLE_SUB_ROW_DETAIL_ROW, PROGRESS_BAR, DIVIDER, IMAGE, AUDIO, INPUT_FIELD, BUTTON; Unmapped legacy icon names render local glyphs in light/dark desktop/mobile modes; metadata navigation and main tiles preserve color, sizing, existing Lucide mappings and same-origin font delivery | | tests/acceptance/fixture/WidgetsFixtures.java widgetBlocks/accBlocks and accLegacyIcons | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-058 | composite layouts and slot links/tooltips | FLEX_COLUMN, FLEX_ROW, BADGES_WRAPPER, TABLE_SUB_ROW_DETAILS, FLEX_ROW_SPACE_BETWEEN, FLEX_ROW_CENTER, FLEX_ROW_WRAPPED; link to /app/person with tooltip | | tests/acceptance/fixture/WidgetsFixtures.java widgetBlocks/accBlocks | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-059 | unsupported block type shows a contained warning | Unsupported block type: OWNED_UNKNOWN; neighbor healthy | | tests/acceptance/fixture/WidgetsFixtures.java widgetBlocks/accBlocksUnknown | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-060 | record-view section widgets receive the record context (id, tableName) | record 1 and 2 show their own values | renderer enforces the user's table read | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost sections accHostHtml, accHostFieldValues | pass (chromium) | |
| WID-061 | process widget for a persona without process permission is contained | error shown in the widget; neighbor healthy | process metadata 403, no process run | tests/acceptance/fixture/WidgetsFixtures.java widgetProcess/accProcess with the noProcesses persona | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-062 | custom component whose bundle cannot load shows a contained error | Error loading MissingOwnedComponent | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accCustomComponentMissing | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| WID-063 | alerts with hideWidget or without HTML render nothing | no card rendered | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accAlertHidden, widgetStates/accEmptyAlert | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
| RPT-001 | TABLE report runs through its process and downloads CSV matching the database | CSV header labels and every person row from SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-002 | TABLE report downloads XLSX with the same rows | sheet rows match SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-003 | TABLE report downloads JSON with the same rows | JSON objects match SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-004 | SUMMARY report groups rows with counts and a totals row | pets per owner match SQL group by; Totals row | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPetSummaryReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-005 | PIVOT report produces a native pivot table in XLSX | xlsx contains the pivot table and cache over the rows sheet | CSV omits the pivot view (no native pivots) | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPetPivotReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) [#484](https://github.com/QRun-IO/qqq/issues/484) |
| RPT-006 | report input fields: required validation and the value filters the rows | minimumId filters rows >= value; Standard report inputs retain compact widths and an inline Generate Report action without a process header | empty required input blocks submit | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonInputReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-007 | report permissions: restricted report denied in UI and by the server | admin can run it | noPets: no-permission message, legacy route 403 | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accRestrictedReport with the noPets persona | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-008 | reports appear in app navigation and open their run page | sidebar REPORT nodes navigate to the report page | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports app | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) [#651](https://github.com/QRun-IO/qqq/issues/651) |
| RPT-009 | saved report view shows its filter/columns and pivot setup | filter criteria, columns, pivot rows/values labels | | prime-sharing savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java owned saved reports | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
| RPT-010 | saved report renders through renderSavedReport and downloads the saved columns and rows | format chosen, CSV contains the saved columns and every species | | prime-sharing savedReport 1 (Pet Species Report) | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) [#661](https://github.com/QRun-IO/qqq/issues/661) |
| RPT-011 | streaming report route for reports without a process | CSV streamed from /reports/{name}?format=csv matches SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accStreamedReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
| RPT-012 | scheduled report: create a schedule for a saved report and view its cron schedule | schedule persisted with cron description; record view shows the schedule widget; Add new on the saved report opens the Add Scheduled Report dialog over it with Saved Report preset and locked (Material #/createChild=); Create submits savedReportId=1, closes the dialog and reloads the child list | invalid cron expression rejected with message | scheduledReport (memory) for savedReport 1 | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) [#714](https://github.com/QRun-IO/qqq/issues/714) |
| RPT-013 | sharing: the owner shares a saved report with a user (Read-Only) | dialog lists Bob; shared_saved_report row READ_ONLY | | prime-sharing savedReport 1 owned by alice | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) [#444](https://github.com/QRun-IO/qqq/issues/444) |
| RPT-014 | sharing: change a share's scope to Read and Edit | row scope READ_WRITE | | prime-sharing savedReport 1 | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
| RPT-015 | sharing: remove a share | row deleted, list empty | | prime-sharing savedReport 1 | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
| RPT-016 | sharing record security: another user cannot see an unshared report until it is shared | bob sees it after sharing (UI list + API) | before sharing: not in bob's list, GET 404 | prime-sharing savedReport 1 (alice), user bob | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
| RPT-017 | sharing: only the owner may share | Share disabled with reason for the non-owner | server rejects a non-owner share; no row | tests/acceptance/fixture/WidgetsFixtures.java savedReport owned by casey shared with alice | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) [#371](https://github.com/QRun-IO/qqq/issues/371) |
| RPT-018 | sharing a saved view through the share dialog | shared_saved_view row for Casey | | prime-sharing savedView 1 owned by alice | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
| WID-064 | cronUI schedule editor on record forms: Basic days/hours/minutes, Advanced expression, live description, validation, time zone | Basic mode builds weekdays, hours and minutes; the live description equals the cronDescription QQQ stores; Basic mode builds selected dates, Clear resets the schedule, and the Advanced hint names the part under the keyboard caret; Advanced mode edits the stored expression; the live description equals the backend widget description; expression and time zone persist (SQL); an expression Basic mode cannot show opens Advanced with Basic disabled and the reason | an out-of-range expression shows the error live and marks the input invalid; saving without a schedule shows the required error in the editor, focuses it, and saves nothing | scheduledReport (memory) for savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostCron with a time-zone enum possible-value source | pass (chromium) | [#702](https://github.com/QRun-IO/qqq/issues/702) |
| WID-065 | record view mounts only the active layout, so each section widget mounts and loads once | desktop tabs: every section widget is in the DOM once and requested once; no accordion is mounted; phone accordion: only open sections mount; each widget is requested once, on opening | the desktop tab layout is not mounted at phone width | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost | pass (chromium) | [#706](https://github.com/QRun-IO/qqq/issues/706) |
| WID-066 | Widget metadata and widget data come from the v1 API | v1 /metaData lists permitted widgets with hasPermission and the reports map; the dashboard renders its widgets from POST /qqq/v1/widget/{name} and makes no legacy metadata or widget request | a denied widget is absent from v1 metadata, never requested, and refused (403) by POST /qqq/v1/widget without rendering | widgetPermissions (accHealthy, accDenied), accPersonReport | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
| WID-067 | a widget whose payload is a single leaf block (Material block widget) renders that block | The backend payload is the TEXT block itself (blockTypeName TEXT, no blocks list); The widget shows the block's text as a TEXT block | No empty COMPOSITE container is rendered in its place | WidgetsFixtures.java accLeafBlock (COMPOSITE widget whose renderer returns a TextBlockData) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
| WID-068 | table widget Material extras: typed cells (numbers with separators, html, htmlAndTooltip, image, composite), hidden helper columns, fr widths, sub-rows, sticky header, column-header help, paging (10 by default, numbered pages, jump-to-page, entries-per-page), export built from columns and rows, and a multi-table widget with an export button and footer per table | Default cells show numbers with thousands separators; html cells render sanitized markup; an htmlAndTooltip cell shows the row's tooltip HTML on hover, focus or tap; an image cell shows the row's image, label and total; a composite cell renders its blocks; Columns of type hidden (tooltip, image helpers) are not shown; fr widths share the width left by fixed widths (2fr is twice 1fr; a 180px column keeps 180px); A row with sub-rows expands and collapses them (nested rows are shaded; a sub-row expands its own sub-rows); The columnHeader=name help slot shows its HTML help on the Name header; Without rowsPerPage 10 of 40 rows show, with numbered page buttons; the entries-per-page select (5-25) changes the page size; more than six pages become a jump-to-page input; the header stays at the top while the fixed-height body scrolls; Export without csvData downloads the Material CSV of every column (hidden ones too), quoted, with link text and without icon glyph text; Each table of a multi-table widget has its own label, export button (csvData or columns and rows) and footer; the widget itself has no export button | Icon glyph text (open_in_new) never reaches the CSV; Collapsed sub-rows are not rendered; A sub-table's footer is not shown under another sub-table | WidgetTableChartFixtures.java widgetTableCharts/accTableCells, accTablePaging, accMultiTableExtras | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
| WID-069 | chart widget Material extras: theme color names, legend toggles, single-series legends (horizontal bars, line badges), full pie with percent tooltip, stacked backgroundColor, stacked tooltip and right integer axis, rotating category labels, small line y ticks and grid, bar chart As of line | Pie slices named info and success fill with the Material theme colors; the pie is full (no inner radius); Hovering a slice shows its label, value and percent of the total; Clicking a legend entry hides that slice or series and clicking again shows it; Stacked datasets fill from their singular backgroundColor (a theme name or a CSS color); the tooltip names only the hovered dataset; the y axis sits on the right with whole-number ticks; long category labels turn; A single-series horizontal bar chart has a legend; a line chart names its series in badges above the chart; A small line chart shows y-axis values and a horizontal grid; A barChart widget ends with an As of line naming today's date | A hidden series draws no bars or points; No y-axis tick of the stacked chart is fractional | WidgetTableChartFixtures.java widgetTableCharts/accPieNamed, accStackedExtras, accLineBadges, accHorizontalLegend, accSmallLineTicks, accBarAsOf | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
| RPT-019 | render report input step shows only its fields; a report without variables adds no no-fields message | Report Format, Email To and Email Subject are shown; the values widget renders nothing | no "No fields" text on the input step | savedReport 1 (Pet Species Report, no variables) | pass (chromium) | [#707](https://github.com/QRun-IO/qqq/issues/707) |
| 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.
File diff suppressed because it is too large. Load diff
-554
View File
@@ -1,554 +0,0 @@
# Next UI visual review
> **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.
Status: in progress, September 30, 2026. This review is a release requirement. Captured screenshots are not approvals or evidence that a surface has passed review.
The visual reference is the working Next UI before the September 26 parity integrations, commit `e42ad2b2bcdc76311e13002a22dd70e3ec437192`. Material Dashboard remains the reference for supported workflows and metadata contracts. The user's September 27–28 correction requires preserving the earlier Next design while completing those workflows.
## Latest checkpoint — September 30, phone process states
Compared original Next `e42ad2b2bcdc76311e13002a22dd70e3ec437192` with the packaged RC8 candidate `f0f0eae98e4b6c883475956a7c59e060d2e5f0a5`, using the same fresh fixture classes and backend. All 24 phone images were inspected: light/dark, original/current, bulk-load mapping at four scroll positions, process progress and expanded internal error. All requested scroll positions were reached, with no setup errors, page errors or document horizontal overflow. Evidence: `rc8-phone-process-review/` manifests and review.
Progress and expanded-error composition retain the original arrangement. Bulk-load field cards and footer retain their arrangement, but saved-profile/reset controls, metadata help and larger touch targets make the upper section taller. Grouped Add Fields and the red removal icon also differ from the original. These are recorded differences, not owner approval. Header breadcrumb crowding remains open. This checkpoint covers these phone states only; whole-product visual and interaction approval remain open.
## Earlier checkpoint — September 30, reachable column menus
VIS-031: column menus were not constrained to available viewport height. A browser probe on the report preview measured a 352px menu starting at y=-147 in a 320px window in both Chromium and WebKit. The shared menu now uses Radix's available-height limit, an 8px collision margin and vertical scrolling, matching existing query menus. Its labels, ordering, typography and normal-height presentation are preserved.
The corrected regression first failed at 320px and passed at 720px in both engines. All 28 final Chromium/WebKit checks pass, including bounded placement, End/Escape and focus return, pointer Copy, sorting, standalone column actions and report variant workflows. Four final menu screenshots were directly inspected. Seven grid component tests, production export, types/lint/license and bundle budgets pass. Evidence: `column-menu-{probe.json,corrected-red-report.json,green-report.json,green-artifacts,unit.log,build.log,budget.log}` under local visual-review evidence. The first regression incorrectly expected a statistics action absent from Person metadata; that test assumption was corrected before the valid red run. No product change addressed that test-only error.
Hosted WebKit run36739388053 failed WID-073 before copying: the trace repeatedly reports Copy full query values outside the viewport. Its report and trace are retained in `row-appearance-ci-webkit/`. The exact workflow passed locally before the change, so this reproduced short-window defect does not by itself prove that hosted failure resolved. A fresh hosted result is still required.
The prior combined table, record-spacing and report hidePreview source `a8e4d3b` separately passed **583 Chromium tests/all 391 required rows**, zero failures/skips/flaky results and three approved external-service exclusions. That full run predates this menu correction; evidence is `report-preview-full-chromium/`. RC4 remains the published version; final 1.0 and whole-product visual approval remain held.
## Earlier checkpoint — September 30, forms, processes and dark widgets
At post-RC4 source `9fb613d`, inspected all 88 images from 64 desktop entries for 16 form/process states in both themes. These include Create, validation, Edit, Copy, Field Lab, process components/help/validation/review, route details/validation/confirmation, cancellation and widget results. All entries have export fingerprints, no setup/page errors and no document overflow. Layouts retain the original Next composition. The broken image in Lab briefing is the deliberate script/onerror sanitization fixture, not a newly broken image feature. A focused axe scan of the default process stepper reports no color-contrast violations or incomplete checks in either theme; its colors were not changed. Evidence: `post-rc4-form-process-review/`. This does not cover all process interactions, bulk-load/progress/error variants or responsive states.
Also inspected 22 fresh dark desktop images (eight entries) covering the full scrolled Widget Gallery, Blocks, Parents, and Tables and Charts pages. Every requested scroll reached its target; no setup/page errors or document overflow occurred. The existing Next card composition remains. Changes include functional maps, typed table cells, chart colors/legends, supplied footer metadata and native dark audio controls. No new visual regression was found in this inventory. This closes the current dark desktop lower-page gap for these four pages only. Evidence: `post-rc4-widget-review/` (11 directly inspected paired sheets). Other themes, responsive layouts, interactive widget states and whole-product approval remain open.
## September 30 — report summary preview flag compatibility
Source comparison found that Material's `hidePreview` hides the read-only summary preview but leaves the editing query available (`FilterAndColumnsSetupWidget.tsx:483-526`). Next incorrectly applied the flag inside the editor too. The bounded correction retains the existing Next editor and scopes this flag to the summary. It is not in published RC4.
A real-backend regression failed for the hidden-summary case while the ordinary case passed. After correction, eight Chromium report scenarios pass with zero failures/skips/flaky results, covering hidden/shown summaries, editor sorting/Refresh, Cancel, OK, host Save/reopen, query permissions, filter indicators and saved omitted joins. All 1,884 units, production export, types, changed-file lint and license checks pass. Four fresh editor images were directly inspected (both themes, flag enabled/disabled); dialog and preview bounds are identical, with no page errors or document overflow. Evidence: `report-preview-{red,green}-report.json`, `report-preview-{unit,build}.log`, and `report-preview-fixed-review/`. Broader embedded-query equivalence is still open.
## Earlier checkpoint — September 30, record detail review
Captured and inspected six desktop record states in light and dark themes against the original Next: small record, Person, selected section with keyboard focus, the field/adornment lab (including its lower scroll position), empty Audit and Delete confirmation. The 24 manifest entries contain 28 images, with no setup/page errors or document overflow. Evidence: `post-rc4-record-review/` under the local visual-review directory. Card placement and dialog composition are retained; added audit sort controls, code expansion and widget chrome remain visible. These captures do not approve populated audits, every field interaction or responsive record states.
Found VIS-030: overview field values inherit 16px/24px typography after the metadata grid migration, increasing plain rows from the original 42px to 46px. Browser measurements on both exports establish the difference (`spacing-probe.json`). Restored the original overview line height while retaining metadata widths and the selected-section layout. Both new Chromium cases failed at 24px versus 20px before the change; all nine affected record workflows pass after it. Fresh measurement matches the original five Person field heights exactly (42px for plain values, 43px for the boolean badge). All 1,884 unit tests, production export/types and changed-file lint pass. Eight fresh after-fix entries (ten images including the lab scroll states) were inspected in both themes with no setup/page errors or document overflow; evidence is `record-spacing-fixed-review/`. The initial browser launch used an inactive default backend port and is retained separately from the valid regression run. The correction is not in published RC4.
## Earlier checkpoint — September 30, query rows and filter review
Compared 14 desktop query/filter states in both themes between the original Next export and the published RC4 JAR: 56 entries, no unexpected setup/page errors or document overflow. The retained baseline export fingerprint matches the earlier review. Inspected table, Basic/Advanced, populated/nested filter, quick editor, columns, column-menu, relative-date, density, selection, Views and Export captures. Three added controls have no equivalent original state and are marked accordingly. Evidence: `rc4-query-review/` under the local visual-review directory. This is a desktop subset, not whole-query or whole-product approval.
Found VIS-029: unconditional alternating row shading added by parity commit `4c7918f` changes the original Next table, especially in dark mode. Restored the original uniform default while retaining selected-row highlighting, opaque pinned cells, theme overrides and the `data-row-parity` customization hook. The correction is on the post-RC4 branch and is not in published RC4. Both new rendered-color tests failed against unchanged RC4, then passed on the corrected export. All 1,884 unit tests, production export/types, changed-file lint and 13 existing Chromium theme/density/pinning workflows passed.
The first browser attempt had a stopped server; a subsequent temporary symlink was not discoverable by the backend classpath scanner. Those setup failures are retained separately. The new pinned-cell test initially matched nested transparent text spans as well as table cells; correcting its selector produced two passing light/dark cases without further product changes. Corrected-build screenshots from that selector-failure run were inspected and show uniform rows with opaque pinned cells; their origin remains explicit. Fresh corrected-export captures are now complete and inspected for the small table, Person table, populated filters and selected rows in both themes (eight entries). Their manifests contain no setup or page errors. Evidence: `row-appearance-fixed-review/` under the local visual-review directory. Broader visual review remains open.
## Earlier checkpoint — September 29, report omitted-join preservation
Inspected the corrected report editor's Filters and sort and Columns tabs in desktop light and dark mode. The existing dialog, tabs, typed filter controls, sort controls, column picker, preview grid and footer remain in use. Saved joined fields remain labelled and editable even when omitted from new choices; the preview still shows their values. This inspection covers the affected state and does not approve the whole report family or the broader original-Next comparison.
Four captures and geometry/runtime results are retained in the local visual-review directory: `report-editor-omission-{light,dark}.png`, `report-editor-omission-columns-{light,dark}.png`, and `report-editor-omission-visual-result.json`. At 1440×1000 the dialog is 1376×936 with 32px margins in both themes, no document overflow and no page errors. The Columns tab keeps its existing internal scrolling. The isolated visual server used a separate sample database and was stopped after capture; owner previews were untouched.
The destructive open-and-OK regression was observed failing before the correction. Current verification: 1,884 unit tests, production export, types, affected lint and three real-backend Chromium report workflows pass; independent review found no further defects. The full current Chromium gate passed 575 tests and all 391 required rows, with zero failures, skips or flaky results. RC3 does not include this follow-up.
## Earlier checkpoint — September 29, script suggestions
Added caret-adjacent suggestions inside the existing script editor: QQQ API/logger helpers, language keywords and file identifiers. The parent dialog, textarea, split panes and syntax overlay are retained. Inspected desktop light/dark captures under `test-results/acceptance/script-completion-evidence`; list bounds and both-theme axe checks pass across all five profiles. This focused review does not approve the whole developer-tools family.
The new interaction passes 1,840 unit tests, types, affected lint, license, static export and bundle budgets; 15 Chromium developer/script workflows and 10 focused cases across all five profiles. Native WebKit groups consecutive edits differently from Chromium: the initial cross-browser assertion failed after a fill plus completion, and a plain-textarea probe reproduced it. Retained failures; no production undo workaround or retry was added. The replacement check starts from a loaded revision and verifies native undo and redo, then separately tests typed-prefix completion and SQL-backed save/reload. Source/caret validation and linear prefix scanning correct both independent review findings.
Baseline full Chromium at 95847c2 is now terminal: 556 passed, all 390 required rows, no failures/skips/flaky tests. Backend PR #913 is merged. The broad original-Next visual review and final release gates remain open.
## Earlier checkpoint — September 29, report filter indicators and API cache correction
Report previews now feed complete nested criteria into the shared grid’s existing filtered-column indicator. Clicking it opens the current filter without adding a condition. The read-only preview keeps edits local. Inspected the real-backend desktop light editor and read-only captures in `report-filter-indicator-final-artifacts`; the existing grid indicator and layouts are reused. The captured editor is scrolled to its active condition. This is a focused inspection, not whole-family approval.
The previous full local Chromium export (303ba61) passed **554 tests**,390 required rows,zero failed/skipped/flaky. The subsequent indicator change passes **1,828 unit tests**,types,affected lint,license,static production export,bundle budget and **30 real-backend Chromium report workflows**. The first focused browser run mistakenly served the previous static export after a standalone build; its missing-indicator failures are retained separately and are not evidence about the corrected artifact. The proper static export was then rebuilt and verified.
Remote eeab664 CI is terminal: Chromium549pass; Firefox WID-070 widget-loading timeout; WebKit NAV-023 internal navigation error; touch WID-072 application-API500,NAV-034 redirect timeout,REL-005 internal navigation error. Logs and traces are retained. The API500 was reproduced as a concurrent static metadata-cache race in QQQ; backend62461e163 fixes atomic initialization and endpoint-test fixture isolation, with110module tests and coverage passing. PR913 remains pending new CI/merge. All40 focused rechecks of the failed workflows pass across Firefox,WebKit,phone and tablet on the corrected export/backend. The original failures remain recorded; a focused recheck does not establish that all navigation and loading failures are resolved. Full final-source/browser-matrix verification and the original Next visual review remain open.
## Earlier checkpoint — September 29, report backend variants
Report previews now reuse the existing Next variant picker for tables that require a store/tenant selection. No query runs before selection; switching starts a fresh preview page and scopes query/count, copy and statistics. The picker works nested inside the report editor. Light/dark desktop captures show it above the parent dialog and the selected variant above the preview, with no horizontal overflow or page errors. No changes were needed to the shared picker's layout or stacking. Captures: `report-variant-picker-{light,dark}.png`, `report-variant-{light,dark}.png` and `report-variant-visual-result.json` in the local visual-review directory. This does not approve the full report family.
Depth verification passes 35 real-backend Chromium scenarios, all 1,826 units, production build, types, affected lint, license and bundle budget. Both ordinary and application-API previews are covered. A review-found delayed-copy race was reproduced then corrected: a response from the previous variant cannot overwrite a later clipboard copy. Independent frontend/backend reviews have no remaining findings in this scope.
The application-API browser run exposed a backend defect: its query/count executors ignored the received variant. QQQ PR #913 applies the ordinary endpoints' session handling. Two HTTP regressions failed then passed; all 109 API middleware tests and coverage pass. Backend build succeeds under its configured static-analysis gates, with warnings. The 35 browser checks used that corrected local middleware JAR; PR CI/merge is pending. Full embedded-query parity, original-Next visual review and release acceptance remain open.
## Earlier checkpoint — September 29, report saved-view resets
Report setup now exposes Reset Changes and Reset to New View beside the selected saved view. The first restores the imported view; the second starts a default draft. Both discard unapplied Basic quick-filter values, and neither updates the saved view or report. The stale quick-filter regression failed for both reset actions before the correction, then passed. Independent review confirmed the correction with no remaining findings in this scope.
The rebuilt export passes all 25 report/sharing Chromium scenarios (zero skips/flaky tests), 1,823 unit tests, types, affected lint, license headers and the bundle budget (1,035.8 KB / 1,050 KB). New depth cases verify copy limits, missing COUNT capability and empty preview behavior against the real backend, with persisted report and saved-view comparisons. This is targeted verification, not complete embedded-query equivalence.
Inspected desktop light/dark captures show the reset links beside the view selector, with readable controls, no dialog horizontal overflow and no page errors. Existing dialog, filter and preview composition is preserved. Evidence: `report-reset-{light,dark}.png`, `report-reset-visual-result.json`, `report-reset-chromium.log` and `report-reset-final-units.log` in the local visual-review directory. The broader original-Next comparison remains open; no complete screen family is approved.
## Earlier checkpoint — September 29, process details and RC1
Fresh Chromium desktop comparisons cover process widgets, results, collapsed internal errors and expanded error details in light and dark modes: 16 original/current captures, with no setup/runtime errors or document overflow. The inspected widget, result and expanded-error pairs preserve the original Next card and action composition. Reload controls remain compact; desktop widget content is slightly shorter than the reference. These checks do not approve the complete process family.
The expanded-detail review led to **VIS-028**, a reproduced accessibility defect in the application-theme configuration: its orange error color yielded 2.8:1 contrast for 12px text. Default light/dark passed. Process details now use the same derived dark/light error-text shades as the query error panel. Text size, position and disclosure behavior are unchanged. The three-theme regression first failed on the application theme, then passed; nine real-backend Chromium error/retry/completion cases and all 1,806 units pass. Build, types and affected lint pass. Evidence: `process-detail-contrast-{red,green,build,unit}.log` and `rc1-process-visual-{light,dark}.log` in the local visual-review directory.
RC1 preparation separately passed the full 544-case Chromium gate (before this one-line contrast correction), clean JAR verification, dependency audit and focused checks. RC1 was subsequently published from `e901df9` on September 29, with Maven Central and GHCR artifact verification recorded in the release notes. See the [RC1 notes](../releases/1.0.0-RC.1.md). The original Next visual review and all remaining Material behavior parity gaps remain final 1.0 work.
## Earlier checkpoint — September 29, appearance preferences
The user-facing Preferences dialog now exposes Light and Dark choices using native radio controls within its existing layout. The choice applies immediately and persists under the existing `qqq-dark-mode` browser/origin key; a fresh profile remains light. An application theme still requires light mode, now with a visible explanation and disabled choices. The saved preference is retained under that override; Reset to Defaults clears it to light along with table and record defaults. No system-following or account synchronization is claimed.
The initial two integration tests failed because the controls were absent. The completed change passes **1,803 unit tests** and **15 real-backend browser cases across all five profiles** (NAV-056 appearance and NAV-038 shell touch/focus). The browser audit initially caught transitional colors while switching palettes; it now waits for actual CSS animation completion before checking the whole visible dialog against WCAG A/AA rules. A subsequent tablet diagnostic caught a metadata request interrupted by the test's immediate reload; the scenario now waits for the application theme to load before reloading. Failure logs/traces are retained under `appearance-transition-*` and `appearance-reload-*`. Build, types, changed-file lint, license and bundle budget pass (1,034.5 KB / 1,050 KB).
Refreshed original/current Preferences captures in both themes at desktop, tablet and phone sizes, including the dialog's own scroll region. Inspected current light desktop/phone top and dark phone bottom: controls remain within the viewport, the footer remains available, and lower layout options are reachable by scrolling. The screenshot tool was corrected to target the Preferences scroll area rather than the phone navigation drawer. Current export fingerprint: `d87518fddea70bd830c3fb10ea3ebaae41410feca77f6b6df2daf56a24370c1a`. The gallery still has 106 light / 87 dark states per viewport; this refresh adds no new state IDs. No whole screen family is approved. Full remote acceptance on the previous pushed head is still running; this targeted result is not release acceptance.
## Earlier checkpoint — September 29, widget width and acceptance setup
The remote phone failure reproduced with a wider native font: the standalone choice stayed 224px while its neighboring scalar input measured 230px. The original width assertion is retained. The metadata-editor minimum is now `min(15rem, 100%)`: 240px where available, capped to 230px at the tested 320px phone viewport. A browser-only trial preceded the one-line source correction; the final export reproduces the trial without injected styles. System, Arial and monospace measurements and 320/393/810/1440px viewports show no document overflow. Plain scalar controls are unchanged.
The Firefox dashboard trace showed its page-opening hook ran before the owned backend session and diagnostics existed. A pre-navigation session assertion failed locally. The hook now initializes those fixtures first and verifies the owned session cookie; metadata and widget reads start after the reset. This corrects a definite fixture precondition. The original 17-second render delay is not claimed independently explained; full CI must verify that it does not recur.
All **105 targeted real-backend cases** pass across Chromium, Firefox, WebKit, phone and tablet: the complete sample-dashboard group and standalone input cases, including system/monospace center clicks and a 320px viewport. All **1,801 unit tests**, production export, types, lint, license and bundle checks pass. The original width and dashboard-heading assertions remain. Logs use `widget-ci-*`, `widget-width-*` and `widget-setup-red-browser.log`; the width red trace is preserved separately.
Refreshed two widget states in both themes at three sizes: 24 original/current entries. Inspected the current desktop editors and light/dark phone open-choice menu. Current captures match the final export with no setup/runtime error or document overflow. The original has no choice menu; its new state is explicitly marked non-equivalent. Recounting all six canonical manifests gives **106 light and 87 dark states per viewport**; this corrects the prior checkpoint's dark count after form captures were added. No whole family is approved.
## Earlier checkpoint — September 29, error readability and refreshed form review
**VIS-027 corrected:** the query error panel failed at 4.24:1 in default light mode, Retry hover at 1.8:1 in dark mode, and the configured orange error theme at 2.52:1. Error text now uses the existing theme-derived red-800 in light mode and red-400 in dark mode shades, with red-300 for dark Retry hover. The visible Sonner error notification separately failed at 4.34:1; its light surface now uses red-800. The configured orange pivot validation also failed at 4.25:1 and now uses red-800 in light mode. Panel, dialog and notification geometry, placement and timing are unchanged.
The broader browser run exposed lazy-fixture leakage: default report-theme tests did not request the backend reset and inherited the preceding configured theme. Its 34 passes / 16 failures are retained as diagnostic evidence. The report spec now requests the reset before every test; a deliberate configured-orange pivot case retains that contrast regression. Query and pivot checks assert the actual root theme. The final run passes **55 real-backend cases across all five profiles**: error panel/notification contrast, Retry hover and request, dismissal, SQL-verified recovery, report-link and pivot contrast, and theme metadata. All **1,801 unit tests**, production export, types, changed-file lint, license and bundle checks pass (1,034.1 KB / 1,050 KB). This is targeted acceptance, not final release acceptance. Logs use `query-error-{complete,pivot,final}-*`; confirmed earlier failures remain in their logs and `query-error-theme-red-artifacts`.
Refreshed original/current create, edit, copy, validation and field-lab forms in both themes at all three sizes:60 entries / 120 images, including scroll sequences. Inspected desktop edit, desktop field-lab middle/bottom, phone field-lab top and edit bottom, dark phone validation, tablet create, and dark desktop field-lab bottom pairs. Their form-body section spacing, field placement and footer positions match the original; the Person currency prefix is metadata-driven. This does not approve uninspected field interactions, modal forms or the whole forms family. Original/current error and Retry-hover captures were also refreshed. Final current-export refresh covers eight states per theme/size (48 entries): five forms, two query errors and pivot validation; no setup/runtime errors or document overflow, all with matching build fingerprints.
Inventory is **105 light / 84 dark states per viewport**. Other captures may carry older export fingerprints and must be checked before approval. No whole family is approved. Current preview on port 18769 serves the final contrast build; original on port 18768 remains the original Next design.
Full remote CI 36580709453 at 30db309 is now **terminal failure**: WebKit passed; phone WID-074 measured choice width 224px versus native scalar 226px, and Firefox WID-014 timed out waiting for the dashboard heading in beforeEach. The later failure snapshot contains that heading. Downloaded logs/traces are in `ci-36580709453-*`; neither failure is waived. The Firefox trace shows backend/diagnostics fixtures were never initialized before the failed page-opening hook. A phone font-loading probe did not reproduce the width failure: system-font scalar 201px and choice 224px stayed unchanged while the downloaded font was held/released. Investigate platform font sizing and hook setup next; do not change geometry or relax assertions without evidence. No merge or publication.
## Earlier checkpoint — September 29, query evidence and column-popup focus
**VIS-026 corrected:** opening Columns with Enter left focus on its toolbar trigger; activating the popup's Close button with Enter then left focus on the page body. The nonmodal popup now receives focus without scrolling or automatically opening a phone search keyboard. Explicit close returns to Columns; Escape keeps its existing focus return, and outside clicks retain focus on the clicked control. Embedded report column editors do not take focus when mounted. The popup layout and field controls are unchanged.
The component regression failed before the fix. A real-browser probe reproduced both focus failures; the corrected Playwright regression then failed at inactive popup focus. Its first attempt used a Testing Library matcher by mistake and is not counted as product evidence. After the fix, 48 affected component tests pass. The broader five-profile backend run passed 28/30; both failures assumed WebKit tabs to a button before search. A plain native-control probe showed Chromium/Firefox tab to Close while this WebKit runner tabs directly to search. The test now verifies those observed native paths while retaining popup focus, search, keyboard close, Escape and outside-click assertions. The final focus check passes all five profiles. Production export, types, lint, license and bundle checks pass. Logs use `column-focus-*`; the confirmed pre-fix artifacts are retained separately.
**Review evidence correction:** some older current captures still showed column widths that had already been fixed. The capture tool now stores a timestamp and SHA-256 fingerprint of the exported files in each new entry. The gallery displays them, flags untracked/older builds, and avoids resetting a newer scenario selection when an earlier fetch finishes. Six theme/viewport image-pair checks and both warning checks pass (`gallery-provenance-check.log`). Captures outside the refreshed query family still require freshness verification; they must not be treated as current approval evidence.
All 27 query states now have refreshed original/current entries in light/dark at desktop/tablet/phone sizes: 324 entries. Current entries have no setup/runtime errors or document overflow and match the final export fingerprint. Three added states cover column-popup keyboard focus, search and no matches. The original has no search control; those baseline captures explicitly state they are not equivalent interaction states. Inspected fresh desktop column configuration/menu/empty/error/loading states, desktop keyboard focus, tablet filtered columns and dark phone no-match presentation. Inventory is now 104 light and 83 dark states per viewport. No full family is approved.
**VIS-027 remains open:** the light query error panel fails a browser contrast check at 4.24:1 for its heading, message and Retry text (required 4.5:1). The dark panel passes. `query-error-contrast-probe.log` contains the measured foreground/background colors and affected nodes. Correct the text contrast while preserving configured status colors and the original panel layout, then verify both default and application themes.
Remote full CI36580709453 at30db309: WebKit passed; Chromium/phone/tablet and Firefox remain running. Keep local corrections unpushed until that run finishes. Full visual review, functional gaps and final release gates remain open.
## Earlier checkpoint — September 29, compact filter controls restored
**VIS-002 corrected for the reproduced desktop height regression:** the populated filter table started at y579, versus y535 in the original Next capture. The unsaved-view status forced a separate full-width row (24px), and the desktop filter header used 44px controls plus padding (20px above the original header height). A browser-only trial isolated both causes before the source change.
The status can now share available toolbar space at tablet/desktop widths; phone wrapping remains. Filter mode and sort controls use a compact 32px minimum on devices without coarse pointers and retain 44px minimums when any coarse pointer is available. The desktop panel header is41px, down from61px; the resulting panel and table positions match the original populated-filter capture. Basic/Advanced, sorting, ownership, save/reset actions and the single editable condition list remain intact. Tablet/phone measurements retain44px targets and no document overflow.
Verification: production export/typecheck, standalone typecheck, changed-file lint, license and bundle checks pass (1034KB/1050KB). All34 affected component tests and50 real-backend cases across Chromium/Firefox/WebKit/phone/tablet pass (QRY-080/083/084/051/093). These are targeted checks, not a whole-suite acceptance result. `filter-layout-red.log` preserves the pre-fix61px header failure; `filter-layout-trial.log` proves the browser-only correction; `filter-layout-green.log` checks the actual final export without injected styles. Other logs use `filter-layout-{unit,build,browser,types,license,budget}`.
Refreshed six current query states in both themes at three sizes:36 entries, no setup/runtime errors or document horizontal overflow. Inspected desktop light/dark populated filters, tablet populated filters and dark phone sheet. Original captures remain unchanged. Inventory stays101light/80dark states per viewport; no whole family is approved. Current preview includes this build. The full remote run36580709453 at30db309 is still running; general and image CI passed. Keep local corrections unpushed until the full run finishes. Full visual review and release gates remain open.
## Earlier checkpoint — September 29, copied-record timestamp persistence
The follow-up found ordinary record edits already protect unchanged timestamps by omitting them from PATCH. Base Copy and both child-row editor hosts submit complete values, however, and were discarding fractional seconds. The real-backend copy regression in America/New_York also reproduced a one-hour shift during the repeated autumn hour: `06:30:07.123456Z` became `05:30:07Z`. Copies and child edits now pass original record values through the existing wire converter; edited values retain normal local-to-UTC conversion.
Verification: **1,799 unit tests and 30 real-backend cases across five profiles pass** (REC-065/018/013/060 and both PRC-064 cases). REC-065 checks unchanged PATCH omission, the copy POST instant and source/copy SQL precision. Separate component regressions prove association and process child-row preservation; these are not claimed as new child-specific real-backend coverage. Production export, types, changed-file lint, license, whitespace and bundle checks pass (1033.9KB/1050KB). Logs use `record-datetime-*`; the confirmed pre-fix trace is retained in `record-datetime-red-artifacts`. The first browser attempt stopped at the existing unreadable-password copy guard; supplying a new fixture password allowed the timestamp defect itself to reproduce.
No layout or style changed in this correction, so existing visual captures were not repeated. Current preview includes the corrected export. Visual inventory remains101light/80dark states per size; no whole family approved. Full CI36580709453 at30db309 remains running across all browser jobs; general CI36580709451 and image36580709514 passed. Keep the new correction local until that full run finishes. Final visual review, complete release verification and publication remain open.
## Earlier checkpoint — September 29, date-time input review
**VIS-025 corrected:** zoned process values were assigned directly to datetime-local controls, which displayed blank fields. Plain standalone blocks sliced UTC digits into a local-time control and discarded seconds. Shared standalone editors displayed local values but sent unzoned text through their action callback. New component regressions and a fixed America/New_York browser context reproduced the failures.
Process fields and standalone blocks now reuse the existing date-time conversion helpers, retaining their native controls and seconds. Process payload conversion occurs after contributors. The converter can retain an original instant when its displayed wall time is unchanged, preserving the second occurrence of a repeated DST hour and hidden subsecond precision. Edited local times become UTC instants. Material's unadorned TEXT/HTML block mapping was inspected and confirmed to use ordinary text controls; no speculative editor redesign was made.
Verification: **1,796 unit tests and 30 real-backend cases across all five profiles pass** (PRC-064 unchanged/edited, WID-075, PRC-063/062 and REC-018). Backend SQL proves unchanged spring and autumn timestamps retain their instants; edits across the spring transition use the correct offset. A unit regression proves unchanged subsecond precision survives. Production export, standalone types, changed-file lint, license, whitespace and bundle checks pass (1033.9KB/1050KB). Logs: `block-datetime-{red-unit,callback-red-unit,precision-red-unit,red-browser,green-unit,unit,build,browser,types,lint,license,budget}.log`; pre-fix browser traces remain in `block-datetime-red-artifacts`.
Paired process timestamp and standalone editor captures were added/refreshed in light/dark at three sizes:24entries, no setup/runtime errors or document overflow. Inspected desktop process pairs, phone process inputs and dark phone standalone controls. The original process blocks show raw text and its regular form timestamps are blank; current controls show dates, seconds and the existing shared editor timezone hint. That hint adds height relative to the original block renderer and remains part of the broader composition review. Captures use UTC; separate acceptance uses America/New_York. Inventory101light/80dark states per size; no whole family approved.
Full CI36573705464 at121468b is now terminal: Firefox and WebKit passed, Chromium/touch failed dependency download before tests. This satisfies the hold on pushing local commits. Final full verification on the new source and the complete release audit remain required; no merge or publication.
## Earlier checkpoint — September 29, input configuration review
**VIS-024 corrected:** advancing a process moved focus from a configured autofocus input to the screen heading. A real-browser regression reproduced the inactive Plain Code field on screen two. The process now preserves focus already inside a marked autofocus editor; ordinary result screens still receive heading focus. Plain and shared metadata editors both pass the transition check.
Enter also inspected trimmed text while submitting the untrimmed process form. PRC-044 reproduced surrounding spaces in the actual SQL record. Plain/shared text blocks now normalize their own value before validation and submission. Required whitespace cannot advance; standalone shared blocks validate the normalized value before invoking their callback. Typed numeric/date/checkbox values and multiline, file and picker keyboard behavior retain their existing paths.
Verification: **1,791 unit tests and 25 real-backend cases across all five profiles pass** (PRC-037/044/062/063 and WID-074). Production export, types, changed-file lint, license, whitespace and bundle checks pass; total JavaScript is 1033.6KB gzip against 1050KB. PRC-063 checks placeholders, retained process values, first blur without premature validation, required whitespace, both autofocus paths, result heading focus and stored SQL text. Pre-fix traces remain in `block-enter-red-artifacts` and `block-focus-red-artifacts`; logs use `block-{enter,focus}-red-*` and `block-input-config-*`.
Added paired plain/formatted input focus captures in both themes at three sizes:24entries, no runtime/setup errors or document overflow. Inspected desktop plain pairs, phone formatted pairs and dark tablet formatted focus. These process cards preserve the original geometry; current required markers and focus rings are visible. Inventory is100light/79dark states per size. This closes the reproduced focus/Enter defects, not the full widget/process configuration audit or any whole-family visual approval.
Both previews use the updated fixtures; the original export is unchanged and the current export includes these fixes. Full remote run36573705464 at121468b: WebKit passed, Firefox running, Chromium/touch failed dependency download before tests. The final full release gate remains open; no merge or publication.
## Earlier checkpoint — September 29, record section keyboard review
**VIS-023 corrected:** record tabs had tab roles but no arrow navigation, single tab stop or tab/panel associations. The real-backend regression failed when Right Arrow left focus on Overview. Tabs now use manual activation: Left/Right/Home/End move focus; Enter/Space opens the section through the existing URL callback. This avoids mounting section widgets merely while moving focus. Tab enters the named panel, with a visible focus indicator. Phone accordion behavior is preserved. The approach follows the [WAI tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
Verification:1786unit tests, production export/typecheck, final types/lint/license/whitespace and bundle1033.5KB/1050KB pass. The related five-profile run passed32cases and found one ambiguous phone test selector (outer accordion and nested section share a region name). The test now follows the trigger’s actual aria-controls and retains name/value assertions; all five keyboard rechecks pass. Existing section/list/deep-link/joined-field checks passed. Logs: `record-tabs-{red-browser,green-browser,keyboard-green,unit,build,types,lint,final-lint,budget}.log`. The pre-fix trace is retained in `record-tabs-red-artifacts`.
Added record-section-focus and refreshed record-small pairs in light/dark at desktop/tablet/phone:24entries, no runtime/setup errors or page overflow. Inspected desktop original/current record card, dark tablet focus and phone accordion focus. Current layout retains the recovered original record composition; focus styling adds no layout spacing. Inventory is98light states/size and77dark. These are reviewed states, not whole-family approval. Configured/denied/error/loading variants and remaining form/widget review continue.
Local changes remain unpushed while fullCI36573705464 at121468b is active: Firefox/WebKit running; Chromium/touch failed during Corepack network setup before tests. General CI and image build passed. No merge or publication.
## Earlier checkpoint — September 29, compact report inputs restored
**VIS-020 corrected for standard report input screens:** restored the compact card, 384px ordinary field maximum, and left-aligned Generate Report action below the inputs. Removed the added visible Input/Step header and divided footer. Cancel remains a secondary inline action. The accessible form name, shared dynamic form, validation, typed values, possible values and process cancellation remain. Configured grid widths and rich editors retain their layout; longer or custom report processes retain their step navigation and components.
Before the change, two unit regressions failed because Generate Report was absent, and the real-browser layout regression failed because the extra Step header was visible. Final verification: **1786unit tests**, production export/typecheck, changed-file lint, license, whitespace and bundle checks pass. **30 real-backend cases across all five profiles pass**, covering report layout/required validation/database-matched output and shared mixed/filtered/required/composite process forms. Logs: `report-compact-{red-unit,red-browser,green-unit,full-unit,build,green-browser,lint,budget}.log`; pre-fix browser artifacts are retained in `report-compact-red-artifacts`.
Paired input, validation and result captures were refreshed in both themes at desktop, tablet and phone:36entries, no runtime/setup errors or document overflow. Inspected desktop input, phone input pairs, dark phone validation pairs, and tablet result. The normal card geometry now matches the original; inline Cancel and shared validation focus/message treatment remain visible differences. No whole report family is approved. Canonical inventory remains97light/75dark states per size. Current preview18769 includes this correction.
Final diff review kept rich/file/code/widget editors on their existing grid rules, including configured file widths. The final export,1786units and changed-file lint pass; RPT-006 passes again in all five profiles. Final logs are `report-compact-final-{build,unit,browser,lint,budget}.log`. Both current dependency audits report zero known advisories (316production dependencies;1038in the full audit); this does not replace the remaining application security checks.
Remote full acceptance36573705464 at121468b remains active: Firefox/WebKit running; Chromium/mobile/tablet failed before tests in Corepack dependency setup with network ETIMEDOUT/ENETUNREACH. Its log is retained as `ci-121468b-chromium-mobile-tablet.log`. General CI36573705788 and image36573705430 pass. Do not push the local report correction while the full run is live; final full verification and release remain open.
## Earlier checkpoint — September 29, saved-view reload correction
Full run36564701143 is terminal: Chromium/mobile/tablet and WebKit pass; Firefox failed QRY-093. The trace showed new filtered data before asynchronous page navigation updated the URL. Query URL changes now replace the current history entry synchronously through Next-integrated native history, preserving other parameters and the hash. No query layout was intentionally changed.
A deterministic Firefox regression failed before the fix with the old filter still in the URL. The corrected export passes25 saved-view, delayed-navigation, back/forward, quick-view and column-menu cases across five profiles, plus20 pagination, sorting, quick-search and return-navigation cases. The current tree passes1784unit tests, production export/typecheck, final standalone types, changed-file lint, license and whitespace checks; bundle1033KB gzip remains within1050KB. Logs: `query-url-{red-unit,red-browser,green-browser,related-browser,build,unit,types,lint}.log`. The final full run with this correction remains pending; these are focused checks.
The current preview includes the correction. Desktop report-input pairs were re-inspected against original source: original compact card has384px maximum-width inputs and an inline Generate Report action; current shared process screen adds header and footer dividers, step count and wider fields. VIS-020 remains open. Preserve the shared dynamic form/process lifecycle while restoring the report presentation. Full visual review and all family approvals remain open.
## Earlier checkpoint — September 29, composite metadata inputs
The block INPUT_FIELD integration now reuses Next's field editors for choices, formatting and supported adornments, retaining the process form and multipart upload path. Plain scalar input layout remains the reference. The richer code/filter/upload editors add genuine capabilities absent from the original block renderer; their additional height and composition are still under visual review. This does not approve the widget or process family.
**VIS-021, corrected in source:** the dispatcher initially treated the backend's default `%s` format as custom formatting. This changed ordinary scalar controls into the shared form layout. A fixture-based browser failure and unit regression exposed it; neutral formatting now preserves the original scalar path. Hidden and read-only metadata are respected in both hosts.
**VIS-022, corrected in source:** a standalone choice editor collapsed to approximately101px, placing its Clear button at the center of the control. A native hit test confirmed that a center click cleared the value instead of opening the choices. The original scalar input is approximately200px wide; standalone metadata editors now use a14rem minimum, also accommodating Firefox's approximately217px native input. The browser assertion compares their widths and retains the center click.
A typed-input fixture existed but was not registered, so the first new capture showed Page not found. It now has a dedicated `widgetInputEditors` app. The capture script treats unexpected not-found screens as failed evidence. Paired captures also cover `prcBlockEditors`, whose original renderer lacks the rich editors, and the unchanged `prcWidgets` route. These comparisons distinguish new capability from restored composition. Final verification and refreshed captures are recorded below when complete.
The remote full run36564701143 has WebKit success, Firefox520passes/1failure(QRY-093), and Chromium/mobile/tablet still running. The Firefox trace shows a concrete reload race: B-filter data appeared before asynchronous router replacement updated the address bar, so reload used the previous Avery filter. Fix remains open; no assertion has been weakened. Artifact11033840508 and its log are retained locally.
Final input-editor checkpoint: **1,783 unit tests pass**, production export/typecheck, changed-file lint, license and whitespace checks pass, and the bundle remains within1050KB gzip. **35 real-backend cases pass across five profiles**, comprising30 widget/process checks and five unchanged QRY-093 rechecks. Those rechecks do not resolve the proven CI URL race. Final logs are `block-editors-width-{build,acceptance,visual-light,visual-dark}.log`, `block-editors-final-{unit,types,lint,license,budget}.log`. The earlier width run's failures and traces are retained, including the aborted local run after repeated diagnosed failures; no remote job was canceled.
Canonical inventory:97light states/size(194original/current entries),75dark(150entries). All36 entries for the three compared scenarios have no setup/runtime errors or page overflow. Inspected final light desktop/phone standalone controls, light phone process lower controls, dark phone standalone controls, dark tablet process lower controls, and existing Widget Lab. Plain fields retain their compact treatment; actual adorned editors are larger than the original scalar controls. Complete configuration/autofocus and visual composition review remain open in the parity ledger. The two new acceptance rows are PRC-062 and WID-074; the generated whole-suite matrix still requires final release reconciliation.
## Earlier checkpoint — September 29, report interaction review
**Release not ready; full visual review incomplete.** Recovery head `06cf1b0` is now pushed to PR #14. General CI `36564701134` and image build `36564701149` pass; full acceptance `36564701143` remains running. The preceding full run ended with tablet/WebKit Export failures and Firefox saved-report opening delay. Local report review changes are verified separately and will stay local while the new full run is active.
Expanded the review with report inputs, required validation, completed output, filter variables, pivot editing/validation, schedule creation/day selection, sharing, and keyboard tab focus. The capture script now measures dialog-body scroll positions. Query/report dark coverage is paired at all three sizes; unavailable original editors remain explicitly non-equivalent. Other families still have dark/theme/interaction gaps. No whole family is approved.
**VIS-018 (medium), corrected:** the report filter/column tabs declared tab semantics but had no arrow-key behavior or labeled panel. Added automatic activation with Left/Right/Home/End, a single tab stop, a linked panel and visible keyboard focus using the existing script-tab pattern. Filter and column drafts persist across switches; hidden Columns metadata remains respected. Unit regression failed before the correction. The first browser run exposed Firefox's native tab stop on a scrolling panel at 1280×720; a native probe proved that an explicit `tabIndex=-1` moves Tab into the column search field. The assertion was preserved.
**VIS-019 (medium), corrected:** pivot validation text on its tinted background measured 4.12:1 in light mode and 4.21:1 in dark mode. Axe independently rejected both. The alert now uses darker light-mode red text and lighter dark-mode red text, preserving its border, tint, layout and validation behavior. Final native measurements are 5.48:1 in both themes, and all ten light/dark pivot contrast cases pass across the five browser profiles.
**VIS-020 (review open):** report inputs now use the shared process screen. The original compact input card and Generate Report button have gained an Input/Step header, wider desktop field, and Cancel/Submit footer. The full process lifecycle and dynamic fields must remain, but the additional height and action placement are not visually approved. Compare `report-inputs` and `report-input-validation` on desktop/phone before deciding the final presentation. Scheduling and sharing retain their original dialog layout in the inspected pairs; scheduling adds the metadata-driven Variable Values section. Its lower controls are reachable by scrolling.
Inspected pairs: populated desktop/phone filters; dark phone quick editor, columns and export; dark tablet relative-date menu; desktop/phone report inputs; phone schedule scrolls and sharing; desktop pivot; phone pivot validation including its lower controls; dark phone variables/pivot validation/sharing; dark tablet schedule Days popover. This list names reviewed evidence, not whole-family approval. Reports still need configured variants, denied/error/loading states, populated schedule/share interactions, complete keyboard closure, and review of the added editor composition. The functional embedded-query equivalence row remains Partial.
Evidence: `query-report-dark-completion.log`, `report-interactions-{initial,light,dark}.log`, `report-tabs-{red,green,focus-probe}.log`, `pivot-contrast-{light,dark,red}.log`; final verification and corrected screenshots follow this checkpoint. Guides now document report-tab keyboard behavior and the Firefox scroll-container distinction.
Final checkpoint verification: **1,773 unit tests and 45 real-backend report cases pass across five profiles**. Export/build typecheck, final standalone TypeScript check, changed-file lint, license headers, whitespace and bundle budget pass (1032.1 KB gzip / 1050 KB). Logs: `report-review-final-{unit,build,acceptance,budget}.log`, `report-tabs-focus-green.log`, `pivot-contrast-{light,dark}-green.log`. Corrected tab-focus and pivot-validation captures were refreshed in both themes at all three sizes and inspected on phone. Canonical inventory is now **95 light states and 73 dark states per size**, with 190/146 original/current entries. Report captures have no runtime/setup errors or document horizontal overflow. This remains partial release evidence; full visual review and the complete final release gate are open.
## Earlier checkpoint — September 29, inline profile naming restored
**Release not ready; full visual review incomplete.** PR #14 remains open at `4748b6c`; local recovery is verified separately. Full real-backend run `36557085746` now has Firefox519passes/1failure (saved-report record opening) and WebKit519passes/1failure (Export). Chromium/mobile/tablet remains running. WebKit's Export failure is corrected locally; Firefox is under diagnosis. Nothing is merged or published. No whole visual family is signed off.
The light inventory contains **85 scenarios at each of three sizes**. Dark coverage remains partial (47 desktop, 45 tablet, 47 phone scenarios, with some unpaired entries). Unavailable original interactions are explicitly marked. Captures are evidence to inspect, not approval.
New, Save As and Rename now use the original Next inline naming layout inside the profile section. Compact field/button styling, Enter behavior and focus return are restored while ownership, backend errors, copying and overwrite/delete confirmations remain. Paired light/dark captures at all three sizes show the recovered editor; added reset controls still increase the surrounding section height. This is not full process-family approval.
Current verification: **1,771 unit tests and 40 bulk-load real-backend cases across five profiles pass**. The earlier menu/Preferences checkpoint passed50/50 affected cases. Production export, final typecheck/lint, license and bundle budget pass (1031.9 KB gzip / 1050 KB). The PRC046 lifecycle test now targets the portaled menu correctly and verifies duplicate-name rejection. Parity recount is728Done,22DifferentUX,5Partial,2Missing,38N/A across795existing rows; the previous summary omitted one supplemental metadata row.
## Evidence and method
- Current source: `feature/next-1.0`; restoration, report editor and guide commits are integrated. The live current preview includes the verified recovery changes described below. Both comparison exports were refreshed on September 29.
- Both builds run the same current sample JAR and acceptance fixture sources, with fresh admin browser contexts and the same Inter font bytes. Historical-reference type checking is skipped only in its isolated build; the current export completes its normal type check.
- Local comparison gallery: <http://127.0.0.1:18770>. Earlier UI: <http://127.0.0.1:18768/app>. Current UI: <http://127.0.0.1:18769/app>.
- Evidence is retained under `../qqq-frontend-next/test-results/visual-review/`: source/build provenance, capture scripts, manifests, screenshot sequences, and server/build logs. It is ignored generated output, not a published artifact.
- Review dimensions: hierarchy, typography and spacing, color and icon treatment, control grouping, responsive layout, and interaction states. Confirm findings against source before changing defaults; preserve explicitly configured metadata behavior.
## Coverage
Current inventory: **106 light and 87 dark states per viewport**, each with original/current entries. The latest checkpoint refreshes standalone widget inputs and the open choice menu against the final export. Other states include older captures and need current-build verification before approval. The table below describes the initial inventory and its remaining review scope; later checkpoints document additional coverage, not whole-family approvals.
The initial pass captured 38 scenarios per build at desktop (1440×1000), tablet (810×1080), and phone (393×851): 228 build/state combinations and 447 screenshots, including long-page scroll sequences. Every entry viewport has received a paired visual scan. Detailed review of all scroll sequences, dark/configured themes, keyboard focus, and additional interaction states remains. No whole family is signed off yet. The baseline report-filter editor is unavailable because that functionality did not exist there; its capture is not an equivalent editor state. The desktop baseline delete capture was corrected during the populated-filter follow-up.
| Family | Captured desktop scenarios | Remaining review scope |
|---|---|---|
| Shell and navigation | Dashboard, app home, nested navigation, not found | Search, command menu, notifications, user menu, help, login/session, phone navigation, branding/theme variants |
| Query | Small and wide table, filters, advanced filters, columns, views, export | Populated criteria and groups, quick-filter editing, column menus, density, cards, selection/bulk controls, saved-view states, pagination, empty/error/loading |
| Records | Compact record, person, record lab, audit, delete | Every tab/list/card variant, associations, metadata menus, configured sidebar, collapse, sharing, warnings/errors |
| Forms | Create, edit, copy, field lab | Each field editor, validation, disabled/read-only, dirty state, full copy, file/association editors, modal forms |
| Processes | Wizard, component lab, widget step, bulk load | Subsequent steps, selection, validation review, progress, results, errors, cancellation, upload mapping |
| Reports | Run inputs, saved report, edit, filter/column editor | Filter variables, columns, pivot editor, scheduling, sharing, results/download, validation |
| Widgets | Sample dashboard, gallery, blocks, parents, controls, states, tables/charts, record widgets/editors | Map all 38 dispatcher labels to evidence; inspect every scroll capture, dropdown, tooltip, menu, loading/error/empty state |
| Developer tools | Table and record developer pages | Script editor/viewer, code tabs, dialogs, ESB metadata/tools, responsive layouts |
## Findings
| ID | Severity | Observed difference | Evidence | Status |
|---|---|---|---|---|
| VIS-001 | High | Wide query tables now use very narrow numeric/date/boolean defaults. Header labels are reduced to fragments, dates wrap across lines, and monetary values clip. The reference keeps readable columns and scrolls horizontally. | `desktop/query-person`, `desktop/query-advanced`; `DataGrid.tsx` and `query-columns.ts` | Restored 150px defaults; configured/user widths preserved. Desktop dates/currency verified after rebuild. Long heading truncation remains under review. |
| VIS-002 | Medium | Switching an empty query to Advanced adds a long unsaved-view status and links to the toolbar, moving Export and view controls to another row. The filter panel also grows substantially with separate mode and sort controls. | `desktop/query-advanced` | Corrected for the reproduced desktop regression: status shares available toolbar space and compact mode/sort controls restore original panel/table positions; 44px touch targets remain. See latest checkpoint. Whole query-family review remains open. |
| VIS-003 | Medium | A record with process actions now has separate Copy and Delete buttons plus an Actions menu. The reference groups secondary actions in the menu. | `desktop/record-person`, `tablet/record-person`; `RecordActions.tsx` | Corrected and verified in desktop/tablet/phone record captures. Enabled menu actions suppress duplicate direct buttons, including nested menus. NAV-055 passes all five profiles. |
| VIS-004 | Medium | Record tabs crowd the identity header and field groups have more vertical spacing. The earlier header has a clear gap above tabs. | `desktop/record-person`, `desktop/record-small`; `RecordViewHeader.tsx` display-contents wrapper and field layout | Normal wrapper and 16px field gap restored; desktop/tablet/phone record captures reviewed. |
| VIS-005 | Medium | Widget header icons changed from colored outline glyphs to filled square tiles; the sample pie chart changed from a doughnut to a solid pie. | `desktop/widgets-sample`; `WidgetIconTile` and `QqqChartWidget` | Header glyph colors and doughnut restored; image paths, main-icon tiles, explicit colors, slice links, tooltips and legend toggles retained. Fresh desktop/phone captures reviewed; 30/30 affected five-profile widget cases pass. |
| VIS-006 | High | Phone query pages always expose mode, sort, and quick-filter controls beneath the toolbar, pushing the first Person card from approximately y=474 to y=708. Opening the Filter sheet also adds unsaved-view links behind it. | `phone/query-person`, `phone/query-small`, `phone/query-filters`; `RecordQuery.tsx` unconditional phone controls | Corrected: shared controls now live in the phone Filter sheet; opening preserves mode. Fresh phone captures confirm first card returns to approximately y=474. Affected filtering/focus/record acceptance passes 83/83 across five profiles. |
| VIS-007 | Medium | Phone app-page titles are reduced to a few characters after adding a home breadcrumb and a separate command-palette control. The earlier header preserves substantially more of the page name. | `phone/app-home`, `phone/widgets-sample`, `phone/not-found`; `Header.tsx`, `Breadcrumbs.tsx` | Corrected: compact mobile gaps, current crumb constrained to available width, and scrolling after metadata arrives or the trail resizes. Fresh phone app/widget captures reviewed; all header controls retained, navigation/focus/touch checks pass in five profiles. |
| VIS-008 | Medium | Process widget buttons changed from compact primary buttons to full-width slate bars, with extra margins pushing the footer below the first phone viewport. | `process-widgets` on all sizes; process migration to shared `QqqComposite` inherited `ButtonBlock` defaults | Partially corrected: hosted process buttons use compact primary styling and disable during work; metadata colors/formats and shared action handling preserved. Desktop/phone captures reviewed; five-profile PRC-035–037 pass. Process text/label/header spacing still exceeds the earlier Next layout and remains under review. |
Existing defects also found in the reference: Views menus run off the right edge on phone/tablet; the phone Export menu opens partly off the left edge; records with many tabs have overlapping tablet labels; sample Big Number context overflows on phone. These are production-quality issues even though they predate the parity changes. They require focused interaction checks and fixes, not preservation as design intent.
Views and Export overflow is now corrected using the existing Radix dropdown pattern: collision-aware placement, available-height scrolling, arrow-key selection and Escape focus return. Existing item actions, native disabled buttons and permissions are preserved. Fresh desktop/phone captures reviewed; 45/45 saved-view/export real-server cases pass across all five profiles, including full viewport intersection assertions. Tablet tab labels and Big Number context are corrected in the responsive checkpoint below.
Dashboard, app-home content, wizard entry, basic process components, centered Person create/edit/copy forms, blocks/parent widgets, and developer-page structure retain the earlier overall layout in the captured states. Form headings and currency adornments differ. Reports and record-widget forms gained previously missing filter, pivot, dynamic-form, and variable sections; their additional length is functional work that must survive restoration. Typed widget table cells now render values instead of raw markup/object strings. These observations cover entry states only and do not approve the full workflows.
## Historical release checkpoint — September 28
- Next UI 1.0.0 remains unreleased. PR #14 remains open at `93b7b5a`; full real-backend run `36507768282` is in progress. The local query-menu follow-up described below is verified separately.
- Remote source/typecheck/lint/unit/coverage, mocked E2E, Storybook, bundle, CircleCI and both architecture image builds pass. Full real-backend run `36501934828` completed with failures in every job. Chromium/mobile/tablet reported 143 failures and 1343 passes; Firefox reported 52 failures and 460 passes. This is not a green release gate.
- A missing middleware API dependency in the separate security fixture caused many failures. Both fixture launch paths now share classpath resolution. The local security/no-search/process-field-widget rerun passes **124/124 in Firefox and WebKit**.
- Report preview/editor verification passes **15/15 across all five profiles**. Read-only previews support temporary paging, sorting and resizing without writing to the saved report. Column widths from metadata are retained.
- Current full unit checkpoint: **1765/1765**. Production export/typecheck, license headers and bundle budget pass (1032 KB gzip / 1050 KB). The remaining CI regression set passed 140 cases across all five profiles; its five script author/date selector failures were corrected and rerun successfully (5/5). This does not replace the final full gate.
- Functional ledger remains **734 Done, 15 Done with different UX, 5 Partial, 2 Missing, 38 N/A** (794 rows). Remaining groups include base-path hosting, Google Drive picker, embedded query/report controls, complex block input adornments, unmapped icon names, and script autocomplete.
- Developer, agent and CSS-hook guides are integrated; the developer/agent report-preview descriptions now reflect the paged grid. Final browser workflow and visual approval remain open.
## Release sequence
1. Complete the screen/state inventory and paired desktop, tablet, and phone evidence, plus theme and accessibility states.
2. Review every capture and record regressions with source causes. Correct shared defaults and verify the affected families together.
3. Reconcile the developer/agent guides and functional parity ledger with the chosen Next interactions.
4. Run the final five-profile real-server acceptance, source, accessibility, security, performance, and packaging gates on the resulting source.
5. Merge and publish Next UI 1.0.0 only after the complete release evidence is green. QQQ 4.1 remains unreleased.
September 28 follow-up: phone filter controls and restored widget styling are in the current preview on port 18769. The initial capture remains archived for comparison; manifests contain refreshed captures only for the scenarios explicitly recaptured. Remaining priority regressions are VIS-002 (dirty-view toolbar), VIS-007 (phone header identity), VIS-008 (process widget button defaults), plus responsive defects and detailed visual states.
Later September 28 checkpoint: VIS-007 and process button defaults are corrected in the refreshed current preview. Full unit suite: 174 files / 1763 tests pass. Production webpack export/typecheck, source lint (0 errors, 5 existing warnings), license and bundle budget pass. NAV-026/037/038 and PRC-035/036/037 pass 30/30 across the five profiles. Header metadata-arrival and process disabled-action regressions have red-to-green unit evidence. Process spacing, VIS-002, responsive defects, and detailed states remain open; these focused checks do not replace the final full release gate.
Query-menu checkpoint: full unit suite 174 files / 1765 tests passes. Static export and standalone webpack builds/typecheck, lint (0 errors, 5 existing warnings), license and bundle checks pass. Mocked E2E passes 102 tests with two existing skips; its advanced-filter helper now explicitly selects Advanced after opening Filter, matching the restored mode-preserving behavior. This corrects the 13 stale-helper failures in CI run 36501112971 at pushed head `75044c1`. Real-backend CI run 36501113002 on that head was still running at this checkpoint. Final all-surface visual review and release verification remain incomplete.
Mocked-suite follow-up: the two shortcut-suppression checks were skipping when the lazy-loaded query input had not appeared yet. They now wait for the input, verify the typed character, and assert the global dialog stays closed. Full mocked E2E passes **104/104, no skips** (`query-menus-mocked-e2e-final.log`). Menu checkpoint `5cb2f5a` is pushed; real-backend run 36501934828 and CI run 36501934819 are in progress. The previous real-backend run was cancelled by the new push, so it is not completion evidence. The test-only follow-up stays local until that run finishes or another necessary source checkpoint is ready.
Spacing review checkpoint: redundant desktop filter heading and nested padding removed; mode/sort controls share the header, while the phone sheet retains its accessible title. Process-hosted labels, text and non-card widget headers use compact spacing. Existing explicit metadata styling and coarse-pointer targets remain. Preview refreshed on port18769; screenshot evidence and `spacing-*.log` files are in the root evidence directory. The populated query still has both a preview and editable conditions; further hierarchy/mode-state review remains open. Tablet tab labels and phone Big Number context defects were reproduced with DOM measurements, and browser-only CSS trials confirmed their causes. Source corrections and regression assertions are the next checkpoint.
Responsive checkpoint: record tabs now use content-based flex sizing, preventing labels from drawing outside their buttons; the tab bar retains horizontal scrolling for larger sets of sections. Composite rows with space-between alignment wrap when needed, and Big Number widths stay within their available container. Explicit style overrides remain effective. `responsive-red.log` proves the new bounds assertions fail for clipped phone number context and tablet tab labels before the correction; `responsive-green.log` passes all18 applicable cases across5profiles (WID023/WID040/REC004). Production export/typecheck, affected ESLint and diffcheck pass. Fresh desktop/tablet/phone record, sample-widget, block and parent captures are stored in the manifests. Desktop sample-widget arrangement is preserved; phone context now wraps within the card. Both image architecture builds are green remotely; full real-backend CI36501934828 remains in progress.
Review inventory now has41scenarios at3sizes; the historical report-filter and quick-filter editors are unavailable and are explicitly recorded as non-equivalent captures. Populated condition and nested-group states were inspected at all3sizes, plus current quick-filter editing. Phone block/parent scroll viewports1 were compared with the reference and retain their content layout. Remaining work still includes all other scroll sequences, additional interaction states, themes/focus, mode/preview hierarchy, long table headings, remaining functional gaps, and final release gates.
## Report preview and CI diagnosis checkpoint — September 28
The report view now shares the editor's paged, sortable, resizable grid. Preview state stays local; saved column widths are preserved. The report tests pass **15/15 across all five profiles**, including database before/after comparison proving that preview interactions do not change the saved report. The latest phone report capture shows contained pagination with an unbroken page indicator. Selected Basic/Advanced controls use solid theme primary/foreground tokens; measured dark-theme contrast is **6.70:1**. These changes do not close the remaining embedded-report control gaps.
The remote full Firefox/WebKit jobs failed. A shared security-fixture classpath correction resolves missing middleware API classes: **124/124** security, no-search, and process-field-widget checks now pass locally across those two browsers. Other failures include stale control-location assertions, fixture catalog changes, and a real column-filter defect that adds a condition while leaving its editor hidden in Basic mode. The latter now opens Advanced directly; the rebuilt export/typecheck passes and affected browser verification is running. General Filter opening still preserves mode. Final whole-suite release verification and the full visual review remain open.
Visual state follow-up: paired form-validation and empty-query captures were added and inspected on desktop, tablet and phone, increasing the inventory to 43 scenarios. Empty-query composition and validation-field layout retain the earlier Next design. Form heading copy differs (Creating New Person versus Create Person), and configured currency adornments remain visible. Phone Field Lab scroll captures 1–6 were compared: field spacing, rich text, password/file controls and later metadata sections retain the original layout. These captures do not approve picker/editor interactions or all form states.
CI diagnosis follow-up: the column Filter action now opens Advanced so its newly added condition is visible; general Filter and keyboard opening preserve mode. Acceptance assertions follow the restored section headings, secondary action grouping, 150px default widths and statistics menus while retaining metadata-width and permission checks. Script revision tests now select required API metadata and assert the API line separately from author/date. The phone menu test uses actual taps: Firefox with touch emulation emitted no pointerdown for a synthetic mouse click, while four diagnostic touch taps opened the menu successfully.
Checkpoint verification: `recovery-five-profile.log` records 140 passes and five failures at the outdated author/date selector; `script-api-author-green.log` passes those five corrected cases. `recovery-full-unit.log` has 1765 passes. Final TypeScript and changed-file lint pass; source lint has zero errors and five existing warnings. License headers, diff check and bundle budget pass. Current preview port 18769 was refreshed with this export and responds 200.
Final old-run diagnosis: the Chromium/mobile/tablet log contains the same corrected failures plus two mobile export assertions receiving an intersection ratio of 0.9999999403953552 instead of exactly 1, and one WebKit internal navigation error. Viewport checks now measure all four edges with a half-CSS-pixel rounding tolerance; the remaining three-profile security/export rerun passed **184/184**, including the previously failed session-switch case. Together with the Firefox/WebKit run, the security classpath fix is verified on all five profiles. Validation error text was measured at 4.58:1 in light mode and 5.01:1 in dark mode on the sample form background.
Dark validation follow-up: phone and tablet comparisons retain the baseline field hierarchy, but both builds had nearly invisible native date-picker icons because no browser color-scheme was selected. The stylesheet now assigns light/dark color-scheme with the existing theme selectors; rendered verification passes in Chromium, Firefox and WebKit for both light and dark, with the OS preference deliberately opposite. A fresh tablet capture confirms the calendar icon is visible. Saved-view/export/metadata-theme checks pass **40/40 across all five profiles** after the viewport assertion correction.
## Query controls and gallery checkpoint — September 28
The light capture inventory now has **51 scenarios at each of three sizes**. New states cover density menu/compact/comfortable, cards, Selection menu, subset dialog, selected rows, and Save View As. Selection/density and both dialogs also have paired dark captures at all three sizes. Inspected evidence includes desktop card layout, phone selected cards, phone/tablet Save View As, and light/dark phone menus. These preserve the reference composition; review of the remaining states continues. Captures alone do not approve the entire query family.
**VIS-009 (medium):** Selection extends beyond the phone viewport; Selection and Density lack arrow-key navigation, and Density ignores Escape. Browser diagnostics reproduce these failures before the change. Both now use the existing Radix dropdown primitive with the original control styling and actions, bounded placement, keyboard selection, dismissal, and focus return. The selected Density label had 2.43:1 dark-mode contrast; it now uses the same primary/foreground pair as Basic/Advanced. The subset dialog restores focus to Selection.
Verification: 1765/1765 unit tests; 30/30 real-backend density/selection/bulk/card cases across Chromium, Firefox, WebKit, phone and tablet; production export/typecheck and targeted lint; license and bundle budget (1032 KB gzip). The first acceptance run passed29 and failed the Firefox dedicated phone case because synthetic mouse input did not open the Radix trigger; the corrected test uses actual taps and asserts menu bounds. Exact diagnostics are query-menu-red.log/query-menu-green.log; final acceptance is query-controls-acceptance-green.log.
The gallery previously missed multi-profile capture manifests. They are now merged into canonical per-profile manifests, future captures update those manifests, and a theme selector exposes light/dark evidence. Browser checks verify both comparison images load at all six theme/size combinations. Save-dialog capture setup now uses the visible Save As action (the separate save-as hook only exists for an existing saved view).
**Still open:** tablet headings truncate more than the baseline because a hidden sort glyph, touch-sized column menu, and cell padding consume label space. Compare tablet query-save-dialog background and desktop query-density-menu. Preserve configured widths and touch targets when correcting this. Full query state review, all other families, remaining functional ledger gaps and final release gates remain open.
Final menu evidence: selected Density contrast is **6.70:1** in dark mode (`density-contrast-green.log`), with the final phone capture inspected. The final export/typecheck, changed-file lint, license and bundle checks pass. A further **10/10** density/selection checks pass across all five profiles on the final build, including explicit focus return after the subset dialog closes (`query-controls-final-acceptance.log`). Current preview was refreshed from this export.
## Query headings, states and WebKit checkpoint — September 28
**VIS-010 (medium), corrected:** hidden unsorted-arrow space plus menu targets and cell padding clipped First Name/Last Name on touch layouts and Annual Salary on desktop. The header now reserves an arrow only for an active sort, reclaims trailing padding next to its menu, and permits two-line touch labels within the existing header height. Default widths remain150px; explicit SIZE widths and resize remain effective; sort/menu targets stay separate and touch menus remain44px. Browser measurements confirm the four tested Person labels fit without increasing the header height (37.5px desktop,60.5px touch). The new assertion failed on all3 initial profiles before the change; **45/45** affected checks pass on all5 profiles afterward, including sorted headings, sorting, resize, column menus, statistics and explicit metadata widths.
Added paired query statistics, loading and backend-error captures at desktop/tablet/phone. Loading deliberately holds the query response; it is a layout check, not evidence of backend latency. Desktop statistics retains the original arrangement with an added Export button. Phone statistics has a more crowded title beside the added action and remains a visual follow-up. Error-state composition is retained; the corrected dirty-view toolbar grouping remains contained. Column menus and relative-date controls did not exist in the original reference, so those baseline captures are marked non-equivalent. Inventory reaches56 light scenarios with the relative-date menu follow-up.
**VIS-011 (high), corrected:** full CI and local WebKit reproduced the relative-date menu extending above the viewport, leaving7daysago unreachable. The menu now caps its height by both70dvh and Radix's available placement height, with a viewport width bound. Acceptance asserts the menu's actual bounds before choosing a date. **25/25** date, phone saved-view/export, restriction and report-sharing cases pass across all5profiles after the correction.
Full remote WebKit job109212960890 finished **509passed/4failed** at93b7b5a. Besides the reproduced relative-date issue, failures were a saved-view touch trigger that stayed closed and two document-navigation diagnostic cases (SEC010 andRPT017). Traces place those API errors39–54ms after a full navigation began while initial reads were still starting; those two tests now let initial reads settle before deliberate document replacement, with API/security assertions unchanged. Saved-view opening now has an immediate expanded-state assertion, but its intermittent CI failure is **not declared resolved** by the focused pass. Other full-CI jobs remain in progress at last check. Artifacts/logs retained under ci-webkit-36507768282 and ci-36507768282-webkit.log.
Verification: query-heading-red.log then query-heading-green.log45passes; query-heading-measurements.log; query-heading-date-unit.log1765passes; relative-date-and-ci-green.log25passes. Production export/typecheck, changed-file lint, license, bundle anddiff checks pass. Current preview contains the heading/date fixes. Final full acceptance and the complete visual review remain open.
## September 29 — report controls and statistics checkpoint
The current preview has been refreshed from the production export including report Basic/Advanced modes, saved-view selection, refresh, density, column configuration and column actions. Existing editor tabs and Cancel → OK → host Save remain. Read-only preview interactions stay local. These additions use the existing Next controls. The original report filter editor was absent, so it has no equivalent baseline state. Light inventory is now 60 scenarios at each of three sizes; captures are not full-family approval.
Paired statistics screenshots revealed that adding Export squeezed the phone title into three lines. A wrapping header now gives the title its own phone row while keeping desktop actions alongside it. The rendered check changed from 84px (three lines) to 28px (one line) at 393px width. Fresh original/current captures exist at all three sizes. The inspected dark report states include desktop Advanced, tablet Basic, and phone Columns/Density/column menu: popups fit the viewport and the footer remains visible; phone preview pagination requires scrolling the modal body. Dark blue links remain a contrast investigation. No whole visual family is signed off.
Evidence: report-controls-corrected-acceptance.log has 90/90 cases across five profiles, including draft save/cancel, saved-view import, API-versioned refresh, paging/sort/resize/pins, copy full query, nested statistics, headings and previously intermittent menu/schedule cases. The earlier expanded run used a Person fixture without QUERY_STATS and failed those five statistics cases; corrected qryItem fixtures passed in the clean run. Source build/type checking passed. Latest remote full acceptance at6439944 remains failed; local focused success does not replace that release gate. Touch-menu tests now attach native input/state diagnostics on failure, and schedule tests assert CSV selection before submission.
## September 29 — dark-mode and form-title recovery
The report link contrast defect reproduced at 2.58:1 in a browser accessibility scan. Separating primary text from branded fills raised the measured sample link contrast to 8.83:1. Broader tests with OS preference opposite the selected UI mode exposed three more defects: unbranded dark primary buttons at 3.23:1, a record badge following OS dark mode on a light page, and dark table boolean values using a dark green. Tailwind dark variants now follow ThemeProvider's class; unbranded dark fills use the readable default blue, while true boolean values use a brighter green on dark surfaces. Light styling and explicit application theme rules remain. 53 checks across five profiles passed, covering both UI modes, default/brand themes, the six main accessibility routes, grids, record layouts and widgets.
Compared dark phone record/form entry states, all seven Field Lab phone captures, tablet process widgets, and desktop widget overview against the original Next build. Form geometry remains aligned; native date/time icons and link contrast are corrected. The Create heading still said Creating New: restored the original Create wording and retained CSS hooks, heading overrides and accessible Edit/Copy record labels. The parity row now accurately records this intentional Next presentation (733Done,16DifferentUX,5Partial,2Missing,38NA).
Evidence correction: widget long-page captures revealed duplicate near-top images because document smooth scrolling had not completed. The capture script now targets the actual main content scroller, uses instant scrolling and verifies actual versus requested position before each screenshot, recording positions in manifests. Scrolling the document could move the application out of view and produce blank evidence. Earlier numbered scroll captures without verified positions must not be counted as complete scroll review; affected long-page evidence is being regenerated. No whole visual family is signed off.
## September 29 — chart restoration verified
Long-page comparison confirmed two presentation changes: stacked charts moved their value axis to the right, and small line charts gained an axis and grid. The original left axis and compact small-line presentation are restored. Integer ticks, dataset tooltips, metadata colors, legend toggles, navigation and accessible data remain. WID-069 and WID-010 pass 25/25 across Chromium, Firefox, WebKit, phone and tablet; visual-recovery unit tests pass 1768/1768, production export/typecheck succeeds and lint reports zero errors/five existing warnings. The ledger now records these intentional Next differences (731 Done, 18 Different UX, 5 Partial, 2 Missing, 38 N/A).
The current preview was refreshed from that export. Dark form-field and sample-widget captures were regenerated against the original at three sizes with verified main scroll positions. Phone lower-page comparisons confirm the compact line presentation, HTML/Stepper layout and chart card structure; current legend controls and the As of date add content/height. Remaining label-rotation differences need review. No whole family is signed off.
Remote c442613 WebKit completed with 513 passes and three failures: Annual Salary header clipping, Selection touch opening and Views touch opening. Its native input diagnostics are being retrieved. General CI and image builds pass; the other full acceptance jobs are still running. Focused local passes do not close these remote failures.
## September 29 — CI menu and header diagnosis
The completed c442613 runs report Firefox 515 passes/one heading failure and WebKit 513 passes/three failures (heading plus Selection/Views). The Chromium/phone/tablet job remains in progress. Native WebKit input attachments prove the two menus open on pointer-down and close after the same tap's compatibility click. A local probe reproduces dismissal when focus returns to the trigger; the regression unit test failed before the correction. Selection and Views now exclude their own trigger from outside-interaction dismissal, while outside clicks, Escape, selection and focus return remain supported. The regression unit passes and 30 real-backend menu/basic/saved-view checks pass across five profiles, including explicit trigger-focus retention. The full remote suite must still verify this correction.
The heading failure is font-dependent: the application renders with its system/configured font, and the original two-pixel spacing adjustment did not fit the wider Linux font. A local Verdana probe reproduced 13px of clipping at the preserved150px width; allowing two lines removed clipping. Header labels now use the existing two-line limit on desktop as well as touch. The wider-font browser regression failed before the change;20/20 heading/sorting/shared-view checks now pass across five profiles. Affected grid/menu unit tests17/17, production export/typecheck, changed-file lint and diff checks pass; the full unit suite immediately before the CSS-only header correction passed1769/1769. Bundle budget passes at1031.5KB gzip of1050KB. Logs: menu-trigger-red/green/unit/acceptance, header-font-probe/red/acceptance/build/unit/lint, visual-recovery-final-bundle.
Current preview includes theme, Create-title, chart, menu and header corrections. Dark tablet Field Lab entry and phone record-person lower viewport were compared with the original and retain field/section geometry; date icons and primary-link contrast are corrected. Desktop/tablet sample-widget lower viewports retain card layout and the compact small chart; legend controls and conditional category rotation still account for visible differences. Complete chart-label review and all other open family/state reviews remain required.
## September 29 — chart labels and single-list filter editing
**VIS-012, corrected:** the chart rotation heuristic treated every character as 6.5px wide and divided plot width by label count, turning month/fruit labels that fit horizontally in the original Next charts. SVG measurements show neighboring labels fit at desktop, tablet and phone widths. The category axis now measures actual glyph widths and tick positions, remeasuring on chart updates and font loading; long labels retain rotation and bounded axis space. The new WID-009 browser assertion failed before the correction; 40/40 chart data/legend/tooltip/rotation cases pass across all five profiles. Live measurements in chart-label-red.log/chart-label-green.log demonstrate horizontal labels with positive gaps afterward.
**VIS-013, corrected:** Advanced conditions appeared twice (preview chips plus editable rows), with two Clear actions opening the same dialog. The earlier Next single editable list is restored. Each row still deletes its own condition; one footer Clear action still confirms and retains sorting. Basic mode retains its quick filters and header Clear action. The now-unused duplicate component/test were removed; the replacement unit checks the single-list layout, and QRY-083 now compares SQL results for two conditions before deleting one and proving the second remains active. This is recorded as Different UX in the parity ledger (730 Done, 19 Different UX, 5 Partial, 2 Missing, 38 N/A). Required acceptance coverage remains enabled.
The new layout unit failed before the correction. Full unit verification passes 1768/1768 in 173 files (one duplicate-component test removed); source lint has zero errors/five existing warnings and license headers pass. The first build caught a Testing Library assertion using Playwright's exact option; that test-only typing error was corrected and the final production export/typecheck passes. Filter acceptance passes 55/55 across five profiles, covering mode switching, quick filters, saved views, per-condition deletion, confirmed clear, sort retention, field validation, dates, boolean/blob and long-text operators.
Fresh original/current captures cover populated/nested filters and sample widgets in both themes at all three sizes. Inspected light desktop/phone populated filters, tablet nested filters, dark phone populated filters, and restored desktop/phone chart labels. Single-list geometry is recovered; Basic/Advanced and sort controls remain necessary additions. Current quick-filter editing is captured; its baseline explicitly states that the control did not exist. All captured long-page positions are verified. No complete family is signed off yet.
Full c442613 acceptance 36549979322 is now terminal/failing: Chromium 515/1, phone 490/1, tablet 486/5, Firefox 515/1, WebKit 513/3. Besides heading/menu failures corrected in 5077c78, phone/tablet RPT-009 reproduced an undersized standalone report link (146x20); it now has a 44px target. The two tablet navigation traces show deliberate document replacements overlapping sidebar prefetch requests; those tests now wait for initial reads to finish. No diagnostics or assertions were waived, and their intermittent CI outcome still requires the next full run. Exact artifacts and logs remain in the root visual-review evidence directory.
Final follow-up verification: nested-and-ci-acceptance.log reproduced the report touch failure on phone and tablet (26 passes/two failures). After correction, nested-and-ci-green.log passes 28/28 across five profiles, including nested AND/OR filtering, lookups, owned/shared records and report touch targets. Affected report/filter units pass 21/21. Final production export/typecheck, changed-file lint, diff check and bundle budget pass (1031.3 KB gzip / 1050 KB). The completed old CI run remains failed; these local passes do not substitute for the next full run.
## September 29 — expanded process review and saved-profile restoration
Added paired light/dark captures at desktop, tablet and phone for expanded help, required-field errors, component review/download, wizard details/validation/confirmation, cancel dialog, running progress, user-facing error, scanner, state-machine entry, widget result, bulk file mapping, value mapping, review and completed validation. All 192 new combinations have clean setup/runtime capture status and no document-wide horizontal overflow. Scroll captures record and verify main-content positions. The first required-field selectors were too strict about labels or matched a containing section; the final captures use the actual field roles. Completed bulk validation is identified by its result text, since the original did not expose the newer status hook.
Inspected pairs include desktop component review and both bulk-mapping viewports; phone required-field error and footer, wizard confirmation, cancellation, progress, scanner, error, loop and widget result; dark desktop bulk mapping, dark tablet value mapping and wizard validation, and dark phone bulk-mapping footer and cancellation. These process compositions retain the original Next arrangement. Added controls and readable dark primary text remain. More process variants, saved-profile menu/dialog states, internal-error detail, bulk selection/results and composite modal states still need visual review; this is not full process-family approval.
**VIS-014 (medium), corrected:** the original bordered Saved Bulk Load Profiles section, heading and visible current-profile status were removed during the profile-action expansion. Restored that section in the shared component. Restored the original `showCurrent` option so file/value mapping identifies the applied profile while the final review summary avoids duplicate status. Current menu, owner permissions, reset/rename/save/delete behavior, help, typed value mapping and searchable fields remain. The extra actions can increase phone height; verified scrolling reaches the footer. Unit assertions first failed for the missing named region and for missing value-step status, then passed after correction.
**VIS-008 clarification:** browser measurements show Reload occupies the same header row as the title. It increases a compact widget header to 44px on touch devices (24px on desktop); this is the touch-target requirement, not an accidental separate row. The hosted ad hoc body is 2.5px shorter than the original in the fixture. Preserve the usable controls; further composite-specific states remain open. Evidence: `process-layout-probe.mjs` and `process-layout-before.log`.
Verification: `bulk-profile-panel-acceptance.log` passes45/45 across five profiles; after the final value-step correction, `bulk-profile-final-acceptance.log` passes25/25 including upload/value mapping/review and profile actions. Final full units1768/1768 in173files (`bulk-profile-final-unit.log`), affected units10/10, export/typecheck, changed-file lint, license headers, diff check and production bundle budget pass (1031.4KB gzip/1050KB). Capture logs: `process-states-light`, `process-states-followup`, `process-states-dark-baseline`, `process-states-dark-current`, `process-bulk-restored-light`, and `process-values-final-{light,dark}`. Canonical manifests supersede failed interim captures and orphaned images.
## September 29 — profile menus, shell overlays and Preferences
**VIS-015, corrected:** the expanded profile menu lacked arrow-key navigation and overflowed the tablet viewport with eight profiles (bottom1105px in a1080px viewport). It now uses the existing Radix dropdown primitive with collision placement and available-height scrolling. Existing menu contents, ownership restrictions, confirmations, status and backend behavior remain. Dialog focus returns to the actual opener, including WebKit clicks that activate without focusing; menu actions return to the persistent menu trigger. Native probes verify desktop/tablet/phone bounds and focus. Export now guards focus returning to its own trigger, fixing the native WebKit tap/focus dismissal reproduced by CI. Outside dismissal and download assertions remain.
The initial profile/browser follow-up produced27passes/3focus failures; actual-opener handling corrected those. A later34pass/1fail run exposed a separate Home/Enter test race. Trace shows Home at77867.082ms and Enter at77869.872ms; the resulting page had selected Review Profile8. Installed Radix roving-focus source defers focus with setTimeout. Waiting for the expected first menu item preserves and strengthens the keyboard check. `menu-home-focus-acceptance.log` passes35/35 across all profiles.
Added seven shell states in both themes at three sizes:84 clean captures. Inspected desktop Preferences/search results/command, phone navigation/user menu/Preferences/help/search results/command, tablet help, dark tablet Preferences and dark phone user menu/search results/command. Original navigation, help, user-menu and search geometry is retained; the command palette includes new table actions within its original shell. Preferences' default selection changes from25 to50 records through current metadata/default behavior. These states do not approve notifications, login/session, all branding variants, internal dialog scroll positions or the whole shell family.
**VIS-016, corrected:** Preferences used z-50 behind the phone sidebar's z-100; its controls were visually covered. It now uses `--qqq-z-overlay` and `--qqq-z-modal`. Closing returns focus to the user-menu button because the Preferences menu item unmounts on selection. The new phone hit-test assertion failed before correction; a unit focus-return assertion also failed. Both pass after correction, and fresh light/dark phone screenshots show the full dialog above the drawer without changing its internal layout.
Final verification: `preferences-menu-final-acceptance.log` passes50/50 across Chromium, Firefox, WebKit, mobile and tablet, including drawer navigation/focus, profile actions and Export. `preferences-final-unit.log` passes1771/1771 in173files. Final production export/typecheck, changed-file lint, license headers, diff check and bundle1031.7KB/1050KB pass. A test-only Playwright `exact` option was removed from Testing Library before the final build. Capture logs are `shell-states-{light,dark}` and `preferences-restored-{light,dark}`. No whole family is approved and the full remote gate remains incomplete.
## September 29 — inline profile naming recovery
**VIS-017, corrected:** Save New, Save As and Rename had replaced the earlier inline Profile Name field with a centered modal. Restored the editor inside the named, bordered profile section using the original compact input and bordered Save Profile/Cancel buttons. No nested form is introduced: Enter saves the profile and stops propagation so the enclosing bulk-load process is not submitted. Escape/Cancel close the editor; saving restores focus to the actual opener or the persistent menu trigger if the opener disappears when the current profile changes. Ownership checks, backend name limits/errors, copy semantics, update confirmation and explicit delete behavior remain.
The inline-region and keyboard regression assertions first failed2/11, then passed11/11. Full units pass1771/1771. The final success-focus assertion also passes in the affected11tests. `profile-inline-acceptance.log` passes40/40 across all5profiles (PRC031/046/058/059/060), covering required/file/value mapping, profile CRUD, duplicate-name server validation, shared ownership, menu bounds and focus. PRC046 retained stale local-panel locators after the menu moved to a portal; those now find the portaled menu while all SQL and behavioral assertions remain. Production export, final typecheck/lint, license and bundle1031.9KB/1050KB pass. The first budget-report write was denied by the sandbox; the authorized rerun completed successfully.
Fresh `profile-inline-{light,dark}` captures replace the modal snapshots at desktop/tablet/phone. All12entries have clean setup/runtime status and no body horizontal overflow. Inspected light desktop, light/dark phone and dark tablet pairs: editor input/buttons match the original arrangement within the restored section. Expanded profile actions/reset controls still add height and remain part of the broader review. No full family is approved. The ledger explicitly records inline naming as Different UX; its existing-row recount corrects795total/51supplemental rows and the stale OS-driven dark-mode description.
## September 29 — Copy alignment, selected sections and touch selection
The bounded record/form review inspected 210 existing images against original Next commit `e42ad2b2`. It found two concrete regressions: **RF-001**, Copy scope controls aligned to a wider container than the editable form; **RF-002**, a selected record section inherited Overview's half-width card and full-width field defaults. The Copy container now shares `max-w-4xl` with EntityForm. Selected tabs use a full-width fallback card and two field columns from `sm`; explicit metadata spans retain precedence. Overview and phone accordion defaults are preserved.
Both browser layout regressions failed on the old export, then passed 10/10 across Chromium, Firefox, WebKit, phone and tablet. Existing record view and create/edit/copy workflows passed 17/17 in Chromium. Full units 1,840/1,840, production export, types, changed-source lint, license headers and bundle budget 1,038 KB/1,050 KB pass. Independent code review found no additional issues.
**Touch selection:** the 44px selection label exceeded the 44px pinned selection cell because density padding was still applied. Removing horizontal selection-cell padding only for coarse pointers exposes both target edges. Browser geometry reproduced the overlap before the correction; 35 focused checks pass across all 5 profiles, covering both edges, every density and existing selection workflows. The first post-fix test attempt used coordinates from before the selection banner moved the grid; the corrected test remeasures after selection and uses native touch events for touch menus. Failed traces remain preserved.
Refreshed current captures for Copy, selected section, record lab, audit and populated query in both themes at all 3 sizes: 30 states/40 images, no capture/runtime errors or body overflow. This also replaces the stale light record lab/audit evidence. Root inspection confirms desktop light Copy/selectedsection and tablet touch query. All 34 refreshed record/form images have now been directly reviewed, closing RF-001, RF-002 and the stale-capture gap. The exact-path ledger is `record-form-review-d937507.md` in the ignored evidence directory. The fixed 210-image record/form inventory is closed; other visual families are still open. Logs/evidence: `record-default-layout-{red,green,build,units}.log`, `record-layout-workflows.log`, `query-selection-targets-final.log`, `layout-restored-{light,dark}.log`, and release-worktree `test-results/acceptance/{selection-target-evidence,record-default-layout-evidence}`.
**Publication hold:** the owner is testing immutable RC1 in real applications for a couple of weeks. These corrections are post-RC1 branch changes. Final publication requires renewed explicit owner approval; see [testing period](../releases/1.0-testing.md).
## September 29 — query and shell fixed inventories reviewed
The query inventory is fully inspected: **348 images**, covering all 28 existing states in light/dark at desktop/tablet/phone sizes, including scroll positions. The reviewer directly inspected 314 images and retained 34 explicitly attributed root inspections. No new unresolved visual regression was found. Five original controls are unavailable for equivalent comparison; the ledger distinguishes those context images. Older selection-padding appearances are historical; refreshed populated-query images and the separate 35-case interaction gate cover the correction.
The seven-state shell inventory is fully inspected: **92 images**, comprising 84 initial-position images and eight Preferences scroll captures. Root directly inspected 64 images and retained 28 prior inspections. No new unresolved shell visual regression was found. All six current Preferences scroll captures expose the lower table/record layout choices, with the footer separate. Original search/help/navigation geometry is retained; added theme choices and command actions remain inside the existing layouts. Shell source has not changed between d937507 and e87fe90. Screenshots do not establish persistence for every preference.
Exact-path ledgers are `query-review-e87fe90.md` and `shell-review-e87fe90.md` in the ignored `test-results/visual-review` evidence directory. This closes those fixed inventories, alongside the previously closed record/form inventory. Process, report and widget review remains open; notifications, login/session and branding variants are not approved by this shell review. RC2 is in preparation and final 1.0 remains held for owner testing.
## September 29 — report fixed inventory inspected
All **262 report images** in the six per-profile canonical manifests were directly inspected: 131 in each theme (desktop 40, tablet 41, phone 50). This corrects the older combined light manifest, which omitted later states. The exact-path ledger `report-review-defb3ab.md` has no missing or extra images and records no confirmed new visual regression. An initial concern about blank output-format text was withdrawn after direct focused reinspection.
This is inspection of the existing images, not a claim that all captures represent defb3ab: only 10 of 216 state entries have timestamps/export fingerprints. Fifty-four original editor-state entries lack an equivalent control, and the initial phone Advanced/Columns captures do not show the lower pager. Those comparison and provenance limits remain explicit. Embedded query equivalence and whole-product visual review remain open; final 1.0 is held.
## September 29 — remaining fixed inventories inspected; child export corrected
All **1,621 unique images** in the six canonical per-profile manifests are now accounted for by completed inspection ledgers: record/forms 210, query 348, shell 92, process 402, report 262, widgets/dashboard 269 and developer/miscellaneous 38. Process review found no additional confirmed regression; explicit full-width upload metadata explains the wide upload control. Developer/miscellaneous content layout retains the original composition, with historical header truncation already addressed by VIS-007. Widget/dashboard review found the child export issue below and no other confirmed unresolved regression. Root independently reconciled the final 269 and 38 paths against the manifests and verified every file exists. Exact ledgers are `process-review-e87fe90.md`, `widgets-dashboard-review-2221999.md` and `developer-misc-review-2221999.md` in ignored evidence.
**Child-record export, corrected:** current child lists had a generic header icon unable to read the child payload plus a working Export control in a new toolbar row. Two component regressions and the real-backend fixture reproduced the duplicate. The first correction removed the generic icon but retained the extra row; fresh screenshots showed that still differed from original Next. The final correction reuses the child export control in the existing header position. Bare hosts retain inline export. Permission-filtered join columns, shown-row CSV, empty disabled state and the partial-result tooltip remain. Widget export controls follow the same loading/error/permission/required-selection gates as the body. The existing error boundary now covers header and body; Retry requests fresh data and a replacement payload clears the error. Review regressions reproduced and verified denied exports, malformed metadata containment and successful recovery.
Verification: **1,857 unit tests**, 41 focused widget tests, 10 real-backend child-list scenarios across five profiles, production export/types/lint/license/diff checks and 1038.4KB/1050KB bundle budget pass. Final browser checks verify one header export aligned with Reload, CSV contents against independent SQL, exclusion of the undisplayed third row, record navigation and empty lists. Desktop and phone captures were directly inspected: the extra toolbar row is gone. The first geometry check compared title and button centers and failed on touch because native targets are 44px and the header uses items-start; the final assertion requires Export inside the header and aligned with Reload. Failed evidence is retained; no CSS or target size was changed to pass it. Independent review has no remaining blocker. Logs use `child-export-*`; final evidence is `child-export-final-evidence` in the ignored root directory.
Inventory completion is **not** whole-product visual approval. Fifty-two gallery/table images repeat near-top content and cannot establish lower-page coverage. Six original choice-open states are unsupported. Most historical images lack build fingerprints; dark widget coverage only includes sample/typed-input/choice states, and developer/miscellaneous has no dark captures. Previously recorded uncaptured shell variants, report pager and provenance limitations remain. The original Next reference remains `e42ad2b2`; Material supplies behavior only. The owner preview remains unchanged while the owner validates it. Final 1.0 is explicitly held.
## September 30: unpublished legacy icon compatibility follow-up
Existing Next Lucide mappings, widget overrides and image priority are preserved. Additional legacy names use the pinned local Material Icons inventory. Six formerly unmapped block icons, navigation and the raised main tile were checked in light/dark desktop/mobile views. Four workflows passed against the actual development JAR; its font and exported files were verified byte-for-byte. Saved desktop/mobile tile regions contain identical light/dark glyph pixels; the earlier suspected blank tile was an inspection error, and no repaint workaround was introduced.
The full local Chromium suite passed 589 tests and all 391 required rows, with zero failures, skips, flaky outcomes or report errors and three approved external-service exclusions. Independent code review approved the change. Evidence is retained under `test-results/visual-review/icon-compatibility-investigation/` in the original checkout. This checkpoint covers the icon change; whole-product visual approval remains open. RC5 is immutable and does not include this follow-up.
## September 30: fresh RC6 dark desktop developer and miscellaneous review
Twelve original/current captures across app home, table developer, record developer and not-found pages were directly reviewed, including lower scroll positions. The fresh manifest records capture times and export fingerprints. No new layout regression was found in those states. Evidence: `test-results/visual-review/rc6-dark-developer-review/review.md` in the original checkout.
An inherited RapiDoc dark-link contrast defect is confirmed at 2.12:1 in both original and RC6; subsequent browser checks also find light-mode contact/authentication text below 4.5:1. A separate correction is being tested. This closes the missing desktop dark capture evidence for the four entry states; phone/tablet dark coverage, expanded endpoint interactions and whole-product approval remain open.
## September 30 — phone developer view comparison
Eight fresh original/RC6 phone screenshots in light/dark were directly inspected (initial and lower scroll positions). Four capture entries had no runtime/state errors or body horizontal overflow. Evidence: `test-results/visual-review/rc6-phone-developer-review/` in the original checkout, including export fingerprints and provenance.
The main composition matches the original. RapiDoc's inner clipping is inherited: its 360px minimum exceeds the 343px phone panel, and hidden overflow clips controls. This remains open for a separate responsive correction. The added command button also reduces breadcrumb space; the trail scrolls to the current page, but header crowding remains a visual-review consideration. This is scoped evidence, not full visual approval.
## September 30 — API playground contrast follow-up
Original Next and RC6 both have an inherited dark playground link contrast defect (2.12:1). Focused browser checks also reproduced light contact/status contrast defects. An isolated follow-up applies existing theme tokens without changing layout. Four final desktop/mobile light/dark cases pass, including hovered controls and Preferences changes, with no failures, skips, flaky outcomes or report errors. All four final screenshots were directly inspected after transitions settled. Evidence is retained under `test-results/visual-review/api-playground-contrast/` in the original checkout.
This is scoped color verification, not whole-product visual approval. Narrow-screen inner clipping, expanded endpoint states and additional application palettes remain open. RC6 is unchanged.
## September 30 — API specification controls in narrow panels
Reproduced clipped specification buttons against the packaged RC7 candidate before any focus/click could conceal overflow by scrolling hidden ancestors. The follow-up lets RapiDoc fit its panel and uses a named overview container plus public CSS parts to wrap only the specification actions at narrow widths. It adds no dependency and does not mutate the shadow DOM.
Twelve real-backend desktop/mobile workflows pass: default device and 320px widths, light/dark themes, complete visibility, actual specification download, keyboard new-tab action, contrast and live Preferences changes. Final phone screenshots were directly inspected. At 1440px, the full playground screenshot is byte-identical to RC7 (SHA256 `ec6d108d5d632d1ee0b3a3ecd6c61ae9279bee705facd2a3b9e422d2e0f1ae37`), and overview/button geometry matches exactly.
All 1,904 units, types, lint/license, default production export and unchanged bundle budgets pass. Evidence, including earlier test/setup failures, is retained under original-checkout `test-results/visual-review/api-playground-width/`. This is a separate follow-up, not part of frozen RC7. Expanded endpoint states, additional application palettes and whole-product visual approval remain open.
## September 30 — desktop and tablet process comparison
Compared original Next `e42ad2b2` with the actual RC8 candidate JAR in 32 light/dark desktop/tablet screenshots: bulk file mapping at two scroll positions, running process progress, and expanded internal-error details. Both builds used fresh isolated instances of the same fixture/backend. Capture manifests include export fingerprints and report zero runtime/state errors or body/main horizontal overflow.
Progress and error panel composition are retained. Bulk mapping visibly changes the saved-profile actions and Add Fields control and introduces explanatory text. Main scroll height increases from 1330 to 1390px on desktop and 1556 to 1748px on tablet. This added density/height remains an owner-review item. Tablet breadcrumb ancestors are more crowded with the added command control; the current page remains visible. Dark mode retains the existing composition with revised readable accent colors. No screen redesign was applied during this review.
Evidence: original-checkout `test-results/visual-review/rc8-desktop-tablet-process-review/`, including four inspected contact sheets and all 32 source images. This is scoped process evidence, not whole-product approval; the existing uncaptured states and compatibility gaps remain open.
## RC8 lower widget review: desktop, tablet and phone
Original Next `e42ad2b2` and corrected RC8 `cd3d473` were compared on Widget Gallery and Widget Tables And Charts using the actual candidate JAR, matching fixtures and backend `e0d57dd31`. Evidence: `rc8-widget-lower-review/` in the visual-review archive. Desktop supplied 28 images; tablet/phone supplied 86 more, for 114 images across 24 entries in both themes. Twelve contact sheets were directly inspected. Every lower capture records and verifies its actual scroll position. All entries had zero runtime/state errors and no body/main horizontal overflow. This closes the earlier repeated-near-top capture gap for these two entry states across all three viewport sizes.
The card structure is retained. Recorded changes include map geography, formatted table cells, per-table export/footers, paging controls, metadata chart colors, readable long category labels, legends, connected line data and As Of timestamps. These additions increase page height, including an extra phone viewport on Tables And Charts. Long phone header text now truncates alongside the added command control. Dark map/plot contrast remains a review concern; this comparison did not measure contrast ratios or verify horizontally scrolled table columns. Expanded interaction states, other widget surfaces and owner visual approval remain open. No product code changed during this review.
-88
View File
@@ -1,88 +0,0 @@
# Performance budgets (QRun-IO/qqq#710)
Two automated checks. A breach fails CI. To fix one, make the change smaller or faster. If a
budget really has to rise, put the new measurement and the reason in the commit.
| Check | What it measures | Budget file | Runs in |
|---|---|---|---|
| Bundle budget | First-load JS per route of the static export, total JS, largest chunk, CSS | `perf-budget.json` | `ci.yml` job "Bundle budget (static export)": `pnpm build:export && pnpm perf:budget` |
| Runtime budgets | Large table, heavy dashboard, long process in the production export against the real sample backend | `tests/acceptance/specs/performance/budgets.ts` | `acceptance.yml`: matrix rows PRF-001 to PRF-005 in every browser |
```bash
pnpm build:export && pnpm perf:budget # bundle; report in test-results/performance/bundle.json
QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit node scripts/acceptance.mjs --skip-build specs/performance
```
## Bundle
First-load JS for a route is every script and stylesheet that the route's exported HTML loads
before hydration, gzip level 9. Next 16 no longer prints these sizes, so the script measures
them from `out/`. `_` stands for a dynamic segment: `/app/_` is `/app/[slug]` (app home, record
query, process, report) and `/app/_/_` is the record view.
| Route | Before (KB gzip) | 1.0 (KB gzip) | Budget |
|---|---|---|---|
| `/`, `/login`, `/callback`, `/token`, `/404` | 218–219 | 218–219 | 230 |
| `/app`, `/app/developer`, `/app/search`, `/app/_/key`, `/app/_/dev`, `/app/_/_/dev` | 309–311 | 309–311 | 330 |
| `/app/_` (app home, record query, process, report) | **562.9** | **448.2** | 470 |
| `/app/_/_` (record view) | 431.6 | 431.5 | 455 |
| `/app/_/create`, `/app/_/_/edit`, `/app/_/_/copy` | 351–356 | 351–356 | 370–375 |
| `/app/_/savedView/_` | 371.2 | 371.1 | 390 |
| Total JS (all chunks) | 689.2 in 54 chunks | 675.0 in 51 chunks | 710 |
| Largest chunk (Recharts + d3, loaded only by chart widgets) | 93.8 | 93.8 | 100 |
| CSS | 16 | 16 | 18 |
Regression fixed: `/app/[slug]` imported `AppHome` from the widgets barrel, and that barrel
re-exports the chart widgets. Recharts was therefore in the first load of every table and
process page, even though `WidgetRenderer` loads charts lazily. The page now imports its
components by module path.
## Runtime
Fixture: `tests/acceptance/fixture/PerformanceFixtures.java`, all synthetic data.
- `prfWide`: 10,000 rows, 40 columns of mixed types, read-only.
- `prfDashboard`: 24 widgets: 6 statistics, 6 bar, 4 line (60 points) and 4 pie charts, and
4 tables of 100 rows.
- `prfLongRun`: a job that walks all 10,000 rows in 40 pages with a 250 ms pause per page.
Each test measures wall time from the user action until the exact expected content is on
screen, and records it as a `timing` annotation in `test-results/acceptance/report.json`.
| Row | Measurement | Budget (ms) | chromium | firefox | webkit |
|---|---|---|---|---|---|
| PRF-001 | Open the 10,000-row table until the first 25 rows × 40 columns show | 5,000 | 535 | 353 | 602 |
| PRF-002 | Page size 25 → 250 (10,000 cells) | 10,000 | 462 | 665 | 738 |
| PRF-002 | Next page at 250 rows | 10,000 | 636 | 969 | 1,188 |
| PRF-002 | Last page (page 40) at 250 rows | 10,000 | 612 | 1,158 | 1,064 |
| PRF-003 | Sort by Name over 10,000 rows | 3,000 | 155 | 234 | 299 |
| PRF-003 | Filter Count 04 = 0 (40 matches) | 3,000 | 173 | 157 | 150 |
| PRF-004 | Open the 24-widget dashboard until every widget has loaded | 14,000 | 1,086 | 1,567 | 1,760 |
| PRF-005 | Job finished until its result screen shows (10.15 s job, 5 status polls) | 5,000 | 415 | 838 | 521 |
These numbers come from an otherwise idle run of the performance area on an Apple M3 Max
(macOS, Darwin 27), Playwright 1.58.2. Each budget is about 8× the slowest browser's time.
In a full four-project run with other acceptance suites on the same machine, the same steps
took up to about 4× longer: Firefox next page 4,191 ms, dashboard 8,841 ms. Retries are 0,
so a tighter budget would flake. The budgets catch order-of-magnitude regressions. The
structural checks below catch smaller ones, and don't depend on machine load.
The rows also check behavior besides time:
- Only one page of rows is in the DOM: 25 or 250 rows × 40 cells, not the whole table.
- The backend is asked for `skip`/`limit` 250 on each page.
- Each dashboard widget's data is requested exactly once.
- Status polls never overlap, and each poll waits at least 1.5 s (`POLL_INITIAL_MILLIS`)
after the previous answer. The job never gets more polls than its length allows.
Grid rendering fixed for large pages: body rows are memoized. The grid used to re-render all
10,000 cells before the page request was sent, and again for the URL sync. Date cells no
longer build an `Intl.DateTimeFormat` per cell. A 250-row page turn in chromium, from click to
rendered rows: 659–669 ms → 518–561 ms.
Observed, not changed here:
- Grid columns follow the field order of the metadata map, not the section order, and not
Material's alphabetical-by-label order. For a 40-column table this looks unordered.
- The pagination range is not locale-formatted, while the total is ("Showing 9751–10000 of
10,000").
@@ -1,53 +0,0 @@
# Agent Prompt: Package 1 — Scaffold, Auth, and Layout Shell
## Your Role
You are an implementation agent. Your job is to execute the implementation plan for Work Package 1 and produce working, tested code. You do NOT make architectural decisions — the plan specifies everything. Follow it exactly.
## Before Writing Any Code, Read These Files
1. **Your work package plan:** `docs/implementation-plans/01-scaffold-and-auth.md` — This is your primary instruction set. Execute every step.
2. **Shared context — API contract:** `docs/implementation-plans/shared-context/api-contract.md` — Every API call must match this exactly.
3. **Shared context — Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md` — Port these types into `/src/types/`.
4. **Shared context — Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md` — Follow these patterns.
5. **Project overview:** `docs/implementation-plans/00-project-overview.md` — Directory structure and shared conventions.
6. **CLAUDE.md** — Project-level conventions.
## Reference Code (Read for Behavior Parity)
Look at the sibling repo `qqq-frontend-material-dashboard` for:
- `src/App.tsx` — Route generation from appTree, auth dispatch, layout structure
- `src/qqq/components/horseshoe/` — SideNav, NavBar, Breadcrumbs, Footer
- `src/qqq/components/misc/Banners.tsx` — Banner rendering
Also look at `qqq-frontend-core` for:
- `src/controllers/QController.ts` — Auth methods (getAuthenticationMetaData, manageSession)
- `src/model/**/*.ts` — All TypeScript model types to port
## What You Must Deliver
Execute Steps 1-11 from the work package plan. After completion:
1. `pnpm dev` runs without errors
2. `pnpm build` succeeds
3. `pnpm tsc --noEmit` — zero errors
4. `pnpm test` — all unit tests pass
5. Sidebar renders navigation from metadata (use mock data if no backend)
6. Auth flow initializes (AUTH_0, OAUTH2, FULLY_ANONYMOUS)
7. All placeholder pages exist and render within the dashboard layout
8. Theme system injects CSS custom properties
9. 401 interceptor triggers redirect
10. Breadcrumbs render from pathname
## Verification
After completing all steps, check every acceptance criterion in Section 9 of the work package plan. Every criterion must pass before this package is done.
## Important Rules
- Use `pnpm` (not npm or yarn)
- TypeScript strict mode — no `any` in component props
- Every interactive element gets a `data-qqq-id` attribute
- All API calls go through `src/lib/api/` — never raw fetch
- Follow the import order convention from CLAUDE.md
- Commit after each major step so progress isn't lost
-72
View File
@@ -1,72 +0,0 @@
# Agent Prompt: Package 2 — Record Query Page
## Your Role
You are an implementation agent. Execute the implementation plan for Work Package 2 and produce working, tested code. Follow the plan exactly — do not make architectural decisions.
## Before Writing Any Code, Read These Files
1. **Your plan:** `docs/implementation-plans/02-record-query.md`
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
5. **Project overview:** `docs/implementation-plans/00-project-overview.md`
6. **CLAUDE.md**
## Reference Code (Read for Behavior Parity)
From `qqq-frontend-material-dashboard`:
- `src/qqq/pages/records/query/RecordQuery.tsx` — THE most complex page. Read the whole thing. Note the 260+ state variables, the page state machine, query deduplication via latestQueryId, localStorage persistence.
- `src/qqq/components/query/FilterCriteriaRow.tsx` — Per-row filter UI with operator selection per field type
- `src/qqq/components/query/BasicAndAdvancedQueryControls.tsx` — Basic/advanced filter toggle
- `src/qqq/components/query/CustomFilterPanel.tsx` — Full filter builder
- `src/qqq/components/query/CustomColumnsPanel.tsx` — Column show/hide/reorder
- `src/qqq/components/misc/SavedViews.tsx` — Saved view CRUD
- `src/qqq/components/query/ExportMenuItem.tsx` — Export to CSV
From `qqq-frontend-core`:
- `src/controllers/QControllerV1.ts` — `query()` and `count()` methods
- `src/model/query/QQueryFilter.ts` — Filter structure
- `src/model/query/QCriteriaOperator.ts` — All 20 operators
## What Package 1 Already Provides (Import From It)
- Types: `import type { QTableMetaData, QFieldMetaData, QRecord, QQueryFilter, QFilterCriteria, QCriteriaOperator } from '@/types'`
- API client: `import apiClient from '@/lib/api/client'`
- Query keys: `import { queryKeys } from '@/lib/query-client'`
- Metadata: `import { loadTableMetaData } from '@/lib/api/metadata'`
- Context: `import { useQContext } from '@/lib/context/q-context'`
- Layout components are in place — your page renders inside the dashboard layout
## What You Must Build
Replace the placeholder at `src/app/(dashboard)/app/[tableName]/page.tsx` with a full Record Query page featuring:
- TanStack Table v8 data grid with dynamic columns from QTableMetaData.fields
- Server-side pagination (offset/limit via POST /table/{t}/query + /count)
- Cell renderers per QFieldType (STRING, INTEGER, DECIMAL, BOOLEAN, DATE, DATE_TIME, etc.)
- Cell renderers per AdornmentType (LINK, CHIP, FILE_DOWNLOAD, etc.)
- Basic filter (quick text search) and advanced filter builder (all 20 operators, AND/OR, sub-filters)
- Column config (show/hide, reorder, persist to localStorage)
- Row selection (checkbox column, single and multi)
- Export to CSV
- Saved views (save/load filter + column configs)
- Pagination with page size selector (10/25/50/100)
- Table variant selection
- Toolbar (create button, process launcher, column config, density, saved views)
- Row click → navigate to record view
## Verification
After each step: `pnpm tsc --noEmit`
When done: verify ALL acceptance criteria in Section 9 of the plan.
Final check: `pnpm build && pnpm test`
Commit your work.
## Important Rules
- All API calls through `src/lib/api/` — create `src/lib/api/tables.ts` for query/count functions
- Use TanStack Table v8 (NOT MUI DataGrid)
- Use TanStack Query v5 for data fetching
- Every component gets `data-qqq-id` attribute
- No hardcoded column names — everything from metadata
- TypeScript strict — no `any` in props
@@ -1,66 +0,0 @@
# Agent Prompt: Package 3 — Record View and CRUD
## Your Role
You are an implementation agent. Execute Work Package 3. Follow the plan exactly.
## Before Writing Any Code, Read These Files
1. **Your plan:** `docs/implementation-plans/03-record-view-and-crud.md`
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
5. **Project overview:** `docs/implementation-plans/00-project-overview.md`
6. **CLAUDE.md**
## Reference Code (Read for Behavior Parity)
From `qqq-frontend-material-dashboard`:
- `src/qqq/pages/records/view/RecordView.tsx` — Section rendering, T1/non-T1, widget loading, permission-gated action menu, keyboard shortcuts (n/e/c/d/a)
- `src/qqq/components/forms/EntityForm.tsx` — Form orchestrator with section-based layout
- `src/qqq/components/forms/DynamicForm.tsx` — Formik-based form with dynamic fields
- `src/qqq/components/forms/DynamicFormField.tsx` — Per-field renderer by type
- `src/qqq/components/forms/DynamicSelect.tsx` — Possible values autocomplete
- `src/qqq/components/forms/BooleanFieldSwitch.tsx` — Boolean null/false/true
- `src/qqq/components/forms/FileInputField.tsx` — File upload
From `qqq-frontend-core`:
- `src/controllers/QController.ts` — `get()`, `create()`, `update()`, `delete()` methods
- `src/model/metaData/QFieldMetaData.ts` — Field type, adornments, constraints
## What Package 1 Already Provides (Import From It)
- Types: `import type { QTableMetaData, QFieldMetaData, QRecord, QPossibleValue, QFieldType } from '@/types'`
- API client: `import apiClient from '@/lib/api/client'`
- Metadata: `import { loadTableMetaData } from '@/lib/api/metadata'`
- Context: `import { useQContext } from '@/lib/context/q-context'`
## What You Must Build
1. **Record View page** at `src/app/(dashboard)/app/[tableName]/[recordId]/page.tsx`
- Section-organized field display (T1 section at top, others below)
- Field value rendering for ALL 12 QFieldTypes
- All adornment types (LINK, CHIP, SIZE, FILE_DOWNLOAD, TOOLTIP, etc.)
- Action menu (edit, delete, copy, launch process)
- Associated records display
- Keyboard shortcuts (n=new, e=edit, c=copy, d=delete)
2. **Entity Form** at `src/components/forms/EntityForm.tsx`
- Dynamic form generation from QFieldMetaData
- React Hook Form + Zod validation (schemas from metadata: required, maxLength, type)
- All field types: text, number, boolean, date, time, datetime, select, file, HTML, password
- Possible values autocomplete with debounced search (300ms)
- Section-based layout with gridColumns
- File upload with preview
3. **Create/Edit/Copy pages** using EntityForm
4. **Delete** with confirmation dialog
5. **API functions** in `src/lib/api/tables.ts`: getRecord, insertRecord, updateRecord, deleteRecord
6. **Possible values** in `src/lib/api/possible-values.ts`: fetchPossibleValues
## Verification
After each step: `pnpm tsc --noEmit`
When done: verify ALL acceptance criteria.
Final: `pnpm build && pnpm test`
Commit your work.
@@ -1,70 +0,0 @@
# Agent Prompt: Package 4 — Process Execution
## Your Role
You are an implementation agent. Execute Work Package 4. Follow the plan exactly.
## Before Writing Any Code, Read These Files
1. **Your plan:** `docs/implementation-plans/04-process-execution.md`
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
5. **CLAUDE.md**
## Reference Code (Read for Behavior Parity)
From `qqq-frontend-material-dashboard`:
- `src/qqq/pages/processes/ProcessRun.tsx` — THE process page. Read the whole thing. Note: step state machine, 14 component types, async job polling with exponential backoff (1.5s initial, 1.5x multiplier, 12s max), file upload, record selection modes
- `src/qqq/components/processes/` — All process components
- `src/qqq/components/processes/ProcessViewForm.tsx` — View-only form steps
- `src/qqq/components/processes/ProcessSummaryResults.tsx` — Results display
- `src/qqq/components/processes/ValidationReview.tsx` — Pre-commit validation
- `src/qqq/components/processes/BulkLoadFileMappingForm.tsx` — Column-to-field mapping
- `src/qqq/components/processes/BulkLoadValueMappingForm.tsx` — Value mapping
- `src/qqq/components/processes/SavedBulkLoadProfiles.tsx` — Profile save/load
From `qqq-frontend-core`:
- `src/controllers/QController.ts` — processInit, processStep, processJobStatus, processRecords, processCancel
- `src/model/processes/` — QJobStarted, QJobRunning, QJobComplete, QJobError, ProcessMetaDataAdjustment
## What Previous Packages Provide (Import From Them)
From Package 1: types, API client, context, layout
From Package 3 (CRITICAL — reuse these):
- `import { EntityForm } from '@/components/forms/EntityForm'` — For form steps
- `import { DynamicFormField } from '@/components/forms/DynamicFormField'` — For individual fields
- `import { fetchPossibleValues } from '@/lib/api/possible-values'` — For select fields in process forms
- Form validation patterns from Package 3
## What You Must Build
1. **Process Run page** at `src/app/(dashboard)/app/[processName]/page.tsx`
- Step wizard with stepper UI showing progress
- Dynamic step rendering from QFrontendStepMetaData.components
2. **All 14 component types:** HELP_TEXT, BULK_EDIT_FORM, BULK_LOAD_FILE_MAPPING_FORM, BULK_LOAD_VALUE_MAPPING_FORM, BULK_LOAD_PROFILE_FORM, VALIDATION_REVIEW_SCREEN, EDIT_FORM, VIEW_FORM, DOWNLOAD_FORM, RECORD_LIST, PROCESS_SUMMARY_RESULTS, GOOGLE_DRIVE_SELECT_FOLDER, WIDGET, HTML
3. **Async job handling:**
- Detect QJobStarted → start polling GET /status/{jobUUID}
- Exponential backoff: 1.5s initial, 1.5x multiplier, 12s max cap
- Display QJobRunning progress (message, current/total)
- Handle QJobComplete (advance to next step or show results)
- Handle QJobError (display error, allow retry)
4. **Bulk load workflows:** file upload, column-to-field mapping, value mapping, profile save/load
5. **Process records** endpoint for paginated result viewing
6. **Process cancellation**
7. **Both launch modes:** standalone (/app/{processName}) and table-scoped (with recordIds or filterJSON)
8. **API functions** in `src/lib/api/processes.ts`: processInit, processStep, processStatus, processRecords, processCancel
## Verification
After each step: `pnpm tsc --noEmit`
When done: verify ALL acceptance criteria.
Final: `pnpm build && pnpm test`
Commit your work.
@@ -1,68 +0,0 @@
# Agent Prompt: Package 5 — Dashboard and Widgets
## Your Role
You are an implementation agent. Execute Work Package 5. Follow the plan exactly.
## Before Writing Any Code, Read These Files
1. **Your plan:** `docs/implementation-plans/05-dashboard-and-widgets.md`
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
5. **CLAUDE.md**
## Reference Code (Read for Behavior Parity)
From `qqq-frontend-material-dashboard`:
- `src/qqq/components/widgets/Widget.tsx` — Widget container (label, dropdown, reload, export, help)
- `src/qqq/components/widgets/DashboardWidgets.tsx` — Widget grid orchestrator
- `src/qqq/components/widgets/CompositeWidget.tsx` — Multi-widget container
- `src/qqq/components/widgets/ParentWidget.tsx` — Parent managing children
- `src/qqq/components/widgets/RecordGridWidget.tsx` — Embedded DataGrid with CRUD
- `src/qqq/components/widgets/charts/` — All chart components
- `src/qqq/components/widgets/blocks/` — All block renderers
- `src/qqq/pages/apps/AppHome.tsx` — App home page with dashboard layout
## What Previous Packages Provide (Import From Them)
From Package 1: types, API client, context, layout
From Package 2 (reuse):
- `import { DataGrid } from '@/components/query/DataGrid'` — For RecordGridWidget
- Cell renderers for field value display in widgets
From Package 3 (reuse):
- `import { EntityForm } from '@/components/forms/EntityForm'` — For DynamicFormWidget
- `import { fetchPossibleValues } from '@/lib/api/possible-values'` — For widget dropdown options
## What You Must Build
1. **App Home page** at `src/app/(dashboard)/app/[appName]/page.tsx`
- Dashboard layout with widget grid
- Widgets loaded from QAppMetaData.widgets
2. **Widget container** — label, dropdown selectors, reload, export, help
- Dropdown changes trigger cancel-and-replace data fetch (AbortController)
3. **DashboardWidgets orchestrator** — loads multiple widgets, manages lifecycle
4. **Charts (Recharts):** line, bar, horizontal bar, stacked bar, pie/doughnut
- Use ResponsiveContainer for all charts
5. **Statistics:** MiniStatisticsCard, MultiStatisticsCard, StatisticsCard
6. **All block types:** TextBlock, BigNumberBlock, UpOrDownNumberBlock, ProgressBarBlock, ButtonBlock, IconBlock, ImageBlock, AudioBlock, DividerBlock, InputFieldBlock
7. **RecordGridWidget** — embedded DataGrid (reuse from P2) with add/edit/delete
8. **Composite/Parent widgets**
9. **Specialized widgets:** PivotTableSetup, FilterAndColumnsSetup, StepperCard, CronUI, ScriptViewer, CustomComponent mount point
10. **API function** in `src/lib/api/widgets.ts`: fetchWidgetData with AbortController cancel-and-replace
## Verification
After each step: `pnpm tsc --noEmit`
When done: verify ALL acceptance criteria.
Final: `pnpm build && pnpm test`
Commit your work.
-105
View File
@@ -1,105 +0,0 @@
# Agent Prompt: Package 6 — Polish, Parity, and Production Readiness
## Your Role
You are an implementation agent. Execute Work Package 6 — the final package. ALL previous packages (1-5) are complete. Your job is polish, parity, and production readiness.
## Before Writing Any Code, Read These Files
1. **Your plan:** `docs/implementation-plans/06-polish-and-parity.md`
2. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
3. **CLAUDE.md**
4. **Requirements doc Section 5.4 (responsive), 5.6 (performance), 5.7 (DX):** `docs/qqq-admin-modernization-requirements.md`
## Reference Code
From `qqq-frontend-material-dashboard`:
- `src/qqq/components/misc/CommandMenu.tsx` — Command palette (Cmd+K)
- `src/qqq/components/audits/AuditBody.tsx` — Audit trail display
- `src/qqq/pages/records/developer/RecordDeveloperView.tsx` — Developer tools
- `src/qqq/pages/records/developer/TableDeveloperView.tsx` — Table dev view
- `src/qqq/components/scripts/ScriptViewer.tsx` — Code editor
## What You Must Build
### Responsive Design Audit & Fixes
- Mobile card view for Record Query (< 768px)
- Off-canvas sidebar drawer on mobile with hamburger trigger
- Bottom sheet filter panel on mobile
- 44px minimum tap targets everywhere
- Sticky headers and bottom-anchored action buttons on mobile
- Breadcrumb truncation on mobile
- Responsive widget grid (1/2/N columns)
### Accessibility (WCAG 2.1 AA)
- axe-core integration in test suite
- Focus management in modals (trap + restore)
- aria-live regions for toasts and loading states
- Color contrast audit (4.5:1 / 3:1)
- Skip-to-content link
- Keyboard navigation for all interactive elements
### Command Palette
- Cmd+K / Ctrl+K trigger
- Search tables, processes, reports from metadata
- Recently accessed items
- Keyboard navigation (arrow keys, Enter)
### Keyboard Shortcuts
- Global: Cmd+K, ?, Escape
- Record Query: n, /, f
- Record View: n, e, c, d, a
- Shortcuts reference panel
### Audit Trail
- Audit log panel on Record View
- Timeline with field-level changes
- Filter by date/user/field
### Developer Tools
- Table dev view: metadata inspector
- Record dev view: raw values, scripts, logs
- Script editor (CodeMirror or Monaco)
- Script test execution
### Report Pages
- Reuse ProcessRun from Package 4
### Storybook
- Setup with all shared components
- Stories for loading/empty/error/populated states
- a11y addon
### Playwright E2E
- Auth flow, query, CRUD, process, widgets
- Responsive viewport tests
- Error handling tests
### Performance
- Bundle analysis (next-bundle-analyzer)
- Lazy loading for charts, code editor, file upload
- TanStack Query cache tuning
- Lighthouse CI integration
### Theme Tokens
- All 60+ CSS custom properties
- Light/dark mode with localStorage persistence
- data-qqq-id verification on ALL elements
- customCss injection support
### Session Timeout UX
- Re-auth modal (not full redirect)
- Preserve form state across re-auth
## Verification
After each step: `pnpm tsc --noEmit`
When done: verify ALL acceptance criteria in the plan.
Final checks:
```bash
pnpm build # Must succeed
pnpm test # All tests pass
pnpm lint # Zero errors
pnpm tsc --noEmit # Zero errors
```
Commit your work.
-241
View File
@@ -1,241 +0,0 @@
# Next UI agent guide
> **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 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
QQQ's Java backend owns tables, fields, processes, apps, navigation, widgets, permissions, and human labels. The browser adapts to the metadata of the current user. Never make a QQQ application work by hardcoding its table names, field names, routes, or widget data in a React component. Add general rendering behavior and a real backend fixture, or change the backend metadata producer where the contract originates. Do not invent JSON keys because a frontend type accepts `Record<string, unknown>`; inspect the actual producer, versioned API response, and acceptance fixture.
The UI supports a **versioned** API at `/qqq/v1`. The acceptance diagnostics reject browser calls to legacy unversioned paths. All browser requests should use typed functions in [`src/lib/api`](../../src/lib/api); TanStack Query keys come from [`src/lib/query-client.ts`](../../src/lib/query-client.ts). Treat permission flags as display controls only; the backend must refuse the same forbidden operation.
## Find the right source before editing
| Need | Start here | Why |
|---|---|
| Instance, app, table, field, process, report, widget shapes | [`src/types/metadata.ts`](../../src/types/metadata.ts), [`src/types/enums.ts`](../../src/types/enums.ts) | Frontend contract and enumerated types. |
| Backend HTTP contract | [`src/lib/api`](../../src/lib/api), [`src/lib/api/client.ts`](../../src/lib/api/client.ts) | Versioned routes, typed request/response handling, 401 behavior. |
| Route and sidebar dispatch | [`src/app/(dashboard)/app/[slug]/page.tsx`](../../src/app/%28dashboard%29/app/%5Bslug%5D/page.tsx), [`use-routes.ts`](../../src/lib/hooks/use-routes.ts), [`use-route-params.ts`](../../src/lib/hooks/use-route-params.ts), [`legacy-paths.ts`](../../src/lib/utils/legacy-paths.ts) | Slug precedence, hidden entries, nested nav, static-export and Material deep links. |
| Query and saved views | [`src/components/query`](../../src/components/query), [`use-record-query.ts`](../../src/lib/hooks/use-record-query.ts), [`use-saved-views.ts`](../../src/lib/hooks/use-saved-views.ts) | URL state, filters, variant, selection, backend saved views. |
| Record display and forms | [`src/components/records`](../../src/components/records), [`src/components/forms`](../../src/components/forms), [`FormWidgetSection.tsx`](../../src/components/forms/FormWidgetSection.tsx), [`zod-from-metadata.ts`](../../src/lib/utils/zod-from-metadata.ts) | Sections, fields, form widget context, validation, adornments. |
| Processes and reports | [`src/components/process`](../../src/components/process), [`ReportRun.tsx`](../../src/components/reports/ReportRun.tsx), [`src/lib/api/processes.ts`](../../src/lib/api/processes.ts) | Step components, state transitions, polling, files. |
| Widget entry and dispatch | [`ConnectedWidget.tsx`](../../src/components/widgets/ConnectedWidget.tsx), [`WidgetBlock.tsx`](../../src/components/widgets/WidgetBlock.tsx), [`WidgetRenderer.tsx`](../../src/components/widgets/WidgetRenderer.tsx) | Fetch/params, chrome, data renderer. |
| Widget payload details | [`src/components/widgets`](../../src/components/widgets), [`widget-types.ts`](../../src/components/widgets/widget-types.ts), [`src/mocks/fixtures/widgets`](../../src/mocks/fixtures/widgets) | Concrete canonical and demo shapes; demo fixtures are not production guarantees. |
| Branding, analytics and CSS | [`material-theme.ts`](../../src/lib/theme/material-theme.ts), [`src/lib/theme`](../../src/lib/theme), [`src/lib/analytics`](../../src/lib/analytics), [`src/styles`](../../src/styles) | Supplemental Material theme, implemented tokens, analytics privacy and `data-qqq-id` customization hooks. |
| Owned backend acceptance | [`tests/acceptance`](../../tests/acceptance), [`tests/acceptance/README.md`](../../tests/acceptance/README.md) | QQQ sample fixtures, browser projects, SQL assertions, diagnostics gate. |
| Current gaps and risk | [Material parity ledger](../acceptance/material-parity.md), [security review](../security/next-ui-review.md), [browser matrix](../acceptance/browser-matrix.md) | What has actually been verified and what remains open. |
The repo's [implementation plans](../implementation-plans) explain intended architecture, but some examples predate the current code. Prefer source and executable acceptance evidence. In particular, current `package.json` uses Next.js 16 even though older `CLAUDE.md` and plans mention 15.
## Preserve the Next visual baseline
Use the September 25 Next design (`e42ad2b2`) as the visual reference. Use Material source to establish functional behavior and backend contracts. Changes to parity behavior must preserve the Next layout unless the user requests a visual change.
- Desktop query mode controls, sort and chips belong inside the Filter panel. On phones, Filter opens a sheet containing those same controls and preserves the mode; Advanced shows the nested builder inside it. Check [`RecordQuery.tsx`](../../src/components/query/RecordQuery.tsx).
- Query column widths default to 150 px. Preserve `SIZE` adornment widths and saved/user-resized widths.
- The record section sidebar requires literal `showRecordSidebar: true` in `supplementalMetaData.materialDashboard` (legacy fallback: `supplementalTableMetaData.materialDashboard`). An absent setting keeps it off.
- T1 identity fields omit the primary key and values already contained in `recordLabel`, checking display values before raw values. Preserve the compact header and card field spacing in [`RecordViewHeader.tsx`](../../src/components/records/RecordViewHeader.tsx) and [`RecordView.tsx`](../../src/components/records/RecordView.tsx).
- Desktop Copy/Delete remain direct actions unless the Actions menu offers that enabled action, including in a nested menu. Preserve accessibility when a custom menu omits or disables the action; see [`RecordActions.tsx`](../../src/components/records/RecordActions.tsx).
- Preserve manual activation in `RecordViewTabs`: Left/Right/Home/End move focus, Enter/Space activates through the existing URL callback. A focused but inactive tab must not mount widgets. Keep one selected tab stop, unique tab/panel links and a focusable named panel. Phone accordion assertions should follow the trigger’s `aria-controls`; the nested section can legitimately have the same accessible name.
- Create, edit and copy share the centered `max-w-4xl` container in [`EntityForm.tsx`](../../src/components/forms/EntityForm.tsx).
- A fresh profile starts in light mode. A saved dark preference applies only without an application theme; an application theme keeps light mode active. Check [`theme-provider.tsx`](../../src/lib/theme/theme-provider.tsx).
- Preserve inline naming in `SavedBulkLoadProfiles`: New, Save As and Rename belong inside the original profile section. Prevent Enter from submitting the enclosing process form; retain ownership rules, backend validation and overwrite/delete confirmations.
- Dialogs launched from the phone sidebar must use the existing `--qqq-z-overlay` and `--qqq-z-modal` layers. A hardcoded `z-50` puts them behind the drawer. Return focus to a persistent control when the opening menu item unmounts.
- For Radix menus, check keyboard navigation, collision placement, available-height scrolling and native touch focus. Returning focus to the same trigger must not dismiss its newly opened menu; outside interactions must still dismiss it. Record a dialog's actual event opener because WebKit can activate buttons without focusing them.
- Search-dialog checks must exercise empty and populated Tab cycles, Clear removing its own button, Escape and backdrop dismissal, keyboard reopening, pointer openers that browsers do not automatically focus, and close-before-autofocus cleanup. Build `pnpm build:export` before Javalin acceptance; `pnpm build` produces a separate Node artifact.
- Browser keyboard checks must await the resulting focus before sending the next dependent key; Radix defers roving focus. Testing Library `getByRole` uses an exact string name by default and does not accept Playwright's `exact` option.
- Standard report input-then-download flows use `compactReportInputs` in `ProcessStepScreen`: preserve the compact card, inline Generate Report action, and accessible form name. Keep the shared form and cancellation; do not replace metadata-driven fields with primitive inputs. Custom components, section labels, process fields and longer flows retain normal step presentation. `DynamicForm.compactInputs` changes ordinary flat-list defaults while preserving explicit grid widths and rich editors.
- Test report editor tabs at a viewport short enough to make their panel scroll. Firefox can add a native focus stop on an overflowing container unless it has an explicit `tabIndex`; the report panel uses `-1` so Tab enters the first control. Verify draft preservation and metadata that hides Columns. Check validation contrast on the actual tinted alert background in both themes.
- A saved-view reset must discard both the applied report filter and any unapplied quick-filter input. `FilterAndColumnsSetupEditor` changes the quick-filter session key on view selection/reset; do not reset it on ordinary typing. Report view selection/reset must not call saved-view store/delete APIs. Verify Cancel against persisted report and saved-view rows.
- Report preview column indicators derive from complete criteria at every nesting level. Opening an indicator reveals the existing filter without appending an empty condition. Preserve read-only preview isolation and editor Cancel behavior; `report-filter-indicators.spec.ts` checks preview rows against SQL and stored report JSON.
- Treat table variants as part of the request and cache identity. Report previews reuse `VariantPicker` and the shared per-table storage key; do not query before selection or display rows from the preceding variant while loading. Scope copy and statistics too. Remounting a grid does not cancel an awaiting copy function: guard it against unmount before further requests and clipboard writes. Preserve the delayed-response regression in `report-variants.spec.ts`.
- Error text must be checked against its rendered background, including Retry hover and visible notifications. The error palette follows configured `errorColor`; Sonner currently uses a light notification surface in both page modes. Request the backend fixture before navigating in theme tests so instance metadata is reset. Assert the actual root theme as well as setting its preference.
Verify affected screens with paired desktop and phone captures, including a fresh preference state and explicit metadata overrides. Source inspection and passing behavioral tests do not establish full visual review.
Query URL synchronization in `use-record-query.ts` uses Next-integrated `window.history.replaceState(null, '', url)`. Keep updates synchronous with displayed query state, preserve unrelated parameters/hash, and replace the current entry. Do not reintroduce asynchronous page navigation for filter changes: QRY-093 holds an RSC navigation while checking immediate reload; QRY-017 covers back/forward and legacy JSON links.
## Workflow for a metadata-driven feature
1. **Find the Material behavior and acceptance row.** Search the parity ledger by screen or widget type and identify the owning issue and matrix ID. Read the current Material implementation when the behavior is ambiguous. Preserve negative cases and permissions.
2. **Trace the QQQ producer.** Locate the Java metadata or renderer that emits the relevant response. Record the exact versioned route and response, including whether data is omitted for unauthorized users. Check the real sample fixture. Do not assume the TypeScript interface is a complete backend specification.
For field choices, inspect `inlinePossibleValueSource` in the actual `/qqq/v1/metaData/table/{name}` response. The core Java frontend model can have a property that the v1 response wrapper omits; `REC-062` caught and closed this gap for inline enums.
3. **Pick the existing frontend seam.** Add an API function for a new endpoint, a query key for server state, a renderer case for a new widget type, or a metadata-driven component for a new field behavior. Keep application identifiers in metadata/fixtures only.
4. **Define behavior for loading, empty, invalid, denied, and failed states.** A malformed widget must not crash neighboring widgets. A denied action must be hidden or disabled and refused by the server. Preserve record identity, selected variant, return path, and unsaved form changes across interactions where applicable.
5. **Add focused unit tests for logic and an acceptance scenario for user behavior.** Acceptance tests use the owned QQQ server and check values or SQL state, not only visibility. Give every acceptance test a matrix ID, and use the `diagnostics` fixture. New rows live in the owning `tests/acceptance/matrix/*.json`; the Markdown matrix is generated from those files.
6. **Run the affected quality gates, then the full release gate when warranted.** Record commands, build/backend revisions, browsers, and results. Do not write “parity complete” while open Partial/Missing rows remain.
7. **Update documentation with verified behavior.** Explain the metadata producer, request/response, UI behavior, and any remaining limitation. Revise this guide's catalog if a widget type changes.
For code changes, follow import order and file naming in [CLAUDE.md](../../CLAUDE.md): React/Next, external packages, types, library, components; PascalCase component filenames, kebab-case utilities, co-located `*.test.ts(x)`. Client components with interactivity use `'use client'`. User text comes from `label` rather than internal `name`. Interactive elements need a stable `data-qqq-id`, an accessible name, keyboard operation, and appropriate focus management. Inspect existing IDs and [`material-theme.ts`](../../src/lib/theme/material-theme.ts) before promising Material selector or token compatibility.
## Configuration decision map
| Desired behavior | Correct source | Verify |
|---|---|---|
| Add a sidebar entry or nested app | Backend `QInstance.appTree` with `QAppTreeNode.type`; corresponding object in `apps`, `tables`, `processes`, or `reports` | Visible route and `useAppTreeRoutes`; hidden objects omitted from nav but authorized deep link still works; denied user's metadata. |
| Add an app home section | `QAppMetaData.sections` (`tables`, `processes`, `reports` by name) | Labels, order, links and permissions in app home. |
| Place dashboard widgets | `QAppMetaData.widgets` ordered names; matching `QInstance.widgets` metadata | `WidgetGrid`, widget POST, grid span, denied cases. |
| Place a record widget | `QTableSection.widgetName` and compatible record context; [`FormWidgetSection.tsx`](../../src/components/forms/FormWidgetSection.tsx) for editing | Two records render their own values; form widget changes join host validation/save; no cross-record cache leak. |
| Configure record layout | `QTableSection.alternatives.RECORD_VIEW`, field/section/widget `gridColumns`, section `collapsible`; Material supplemental table `showRecordSidebar` and `recordViewActionsPlacement`; instance `recordViewActionsPlacement` override | Use [`record-layout-utils.ts`](../../src/lib/utils/record-layout-utils.ts); sidebar defaults off and requires explicit `true`. Preserve the original Next defaults for unset widths: Overview half-width cards, selected section full-width card and two field columns (one on phones); explicit metadata spans win. Verify scroll, saved collapse state, and action location in desktop and phone layouts. |
| Configure record action menus | `QTableMetaData.menus` slots `VIEW_SCREEN_ACTIONS` and `VIEW_SCREEN_ADDITIONAL`; [`record-menu-utils.ts`](../../src/lib/utils/record-menu-utils.ts) | Permissions, nested menus, process deduplication, file downloads, and direct Copy/Delete availability. |
| Choose basic filter fields | Table dashboard `defaultQuickFilterFieldNames`, otherwise T1 section fields | Read [`quick-filter-utils.ts`](../../src/lib/utils/quick-filter-utils.ts); confirm resolved names, deduplication, basic/advanced conversion and phone sheet. |
| Add virtual query columns | Backend `virtualFields` with `isQuerySelectable`; filtering/sorting also requires `isQueryCriteria` | Query payload selects the virtual field; display-only virtual fields cannot become filter or sort criteria. |
| Add fields or editors | `QTableMetaData.fields`, sections, adornments, possible-value source | Form, view, validation, server save, keyboard and phone. |
| Show a joined field in a record section | Put `joinTable.field` in `QTableSection.fieldNames` and declare a readable `exposedJoins` path | The record GET includes that join; the qualified value renders with joined field metadata. REC-064 covers the real sample in five profiles; permission-denied paths stay absent. |
| Read a variant table record | `QTableMetaData.usesVariants` and the query screen's `qqq.tableVariant.<table>` choice | Pass [`storedRecordVariantJson`](../../src/lib/utils/table-variant.ts) to each view/edit/copy record GET; verify the same ID resolves to the selected variant's values ([REC-063](../acceptance/feature-matrix.md)). |
| Add a process screen | `QProcessMetaData.frontendSteps` and a supported `QFrontendComponent.type` | Init, submit, async status, cancel/retry, output and server mutations. |
| Add report | `QInstance.reports`, optional `processName` | CSV/XLSX/JSON and required inputs; actual downloaded bytes. |
| Customize identity | `QInstance.branding` for app/company/logo/banners; `supplementalInstanceMetaData.materialDashboardTheme` for the active application theme | Login subset, footer, header, colors, density, icons, contrast, CSP. `QInstance.theme` is not the active theme input. |
| Customize widget chrome | `QWidgetMetaData` fields such as `gridColumns`, `isCard`, `icon`, `collapsible`, `dropdowns`, `showReloadButton`, `showExportButton`, `helpContent`, `icons`, `footerHTML` | Widget response's labels, lists/CSV and interaction; remembered collapse and dropdown state. |
| Customize application CSS | `supplementalInstanceMetaData.materialDashboardTheme.customCss` against observed `data-qqq-id`/implemented CSS tokens | Browser rendering, contrast, and actual Material selector/token mapping. |
| Configure analytics | `QInstance.environmentValues` with provider settings and privacy opt-ins | No provider initializes without settings; IDs/search/labels withheld by default; CSP for scripts. |
| Switch host mode | `QQQ_NEXT_OUTPUT=export` for Javalin jar, or standalone build with `QQQ_BACKEND_URL` | Root deep link, `/qqq/v1` calls, asset paths, browser refresh. |
Table dashboard settings use `supplementalMetaData.materialDashboard` in v1; `supplementalTableMetaData.materialDashboard` is the legacy fallback. When both objects exist, the v1 object wins. Use [`materialDashboardTableMetaData`](../../src/lib/utils/goto-utils.ts) rather than reading only one spelling.
The exact type names and optional fields are in [`metadata.ts`](../../src/types/metadata.ts). `supplementalInstanceMetaData`, `supplementalTableMetaData`, and widget `defaultValues` are extension maps, not licenses to invent arbitrary frontend behavior. Inspect each consumer and QQQ backend producer first.
### A safe widget change, end to end
For an existing `statistics` widget, the backend can declare metadata equivalent to this TypeScript value:
```ts
import type { QWidgetMetaData } from '@/types'
const metadata = {
name: 'openOrders',
label: 'Open orders',
type: 'statistics',
hasPermission: true,
gridColumns: 4,
showReloadButton: true,
} satisfies QWidgetMetaData
```
The canonical QQQ renderer then returns a payload such as `{ "type": "statistics", "count": 7, "countContext": "orders awaiting review" }`. `ConnectedWidget` calls the typed widget API, `WidgetBlock` renders the frame, and `WidgetRenderer` selects `QqqStatisticsWidget` because `count` is present. The local demo statistic instead uses `value`; do not copy that shape into a QQQ Java renderer without verifying it. To add a new renderer type, add the dispatcher branch, a focused component, a malformed/empty case, real backend fixture, and a WID acceptance row. For existing types, do not add a duplicate dispatcher path.
For a dropdown, metadata alone is insufficient. The backend payload supplies parallel `dropdownNameList`, `dropdownLabelList`, `dropdownDataList`, and `dropdownDefaultValueList`. The selected option ID is sent back as a widget request parameter. A PVS dropdown uses `possibleValueSourceName` as that parameter name; a `DATE_PICKER` uses its `name`. `storeDropdownSelections` writes a local preference only. Test a stale stored choice and a required selection ([WID-047–WID-050](../acceptance/feature-matrix.md)).
For `customComponent`, metadata `defaultValues.componentName` and `componentSourceUrl` select a trusted external bundle. The browser loads `window[componentName][componentName]`, exposes `window.React`/`window.ReactDOM`, and passes `qfmdBridge` and `qContext` to the component ([`QqqContainerWidgets.tsx`](../../src/components/widgets/QqqContainerWidgets.tsx), [`qfmd-bridge.tsx`](../../src/components/widgets/qfmd-bridge.tsx)). Verify an existing application's real bundle and CSP before claiming compatibility; the in-repo bridge test alone does not prove it.
### Widget editors and host forms
[`WidgetRenderer.tsx`](../../src/components/widgets/WidgetRenderer.tsx) dispatches all 38 renderer labels cataloged in the [developer guide](./next-ui-developer-guide.md#7-widgets-placement-configuration-and-data). Pass the host's `formContext`, `dataVersion`, and `onWidgetData` through the existing connected boundary when the widget edits data. A record form's [`FormWidgetSection.tsx`](../../src/components/forms/FormWidgetSection.tsx) selects editable widgets; a process `WIDGET` step has its own host ([`WidgetComponent.tsx`](../../src/components/process/WidgetComponent.tsx)). Do not make a display widget write directly to the backend or silently mutate a neighboring form field.
For a row builder, inspect [`row-builder-model.ts`](../../src/components/widgets/row-builder-model.ts) and the backend producer before setting `defaultValues`. It reads `frontendFields` (else `fields`), `associationName` or `outputFieldName`, `orderByFieldName`, `mayReorderRows`, `isForRecordViewAndEditScreen`, `isEditable`, `useModalEditor`, `inlineHeading`, `modalTitle`, and `defaultValuesForNewRowsFromParentRecord`. A record form uses association values when `associationName` is set; a process step uses the named output field. Verify add/delete/reorder, validation, save/cancel and reload with real data. The metadata flags accept booleans or the strings `"true"`/`"false"` where the model explicitly parses them.
For `pivotTableSetup`, the record view reads `pivotTableJson`; a form supplies the editor context. The editor needs `tableName` and `columnsJson` in the host form and writes serialized `pivotTableJson`. Verify switching the pivot on/off through the actual host form and database readback: a widget-only callback assertion does not prove that clearing survives form validation. Also check modal cancel/save, invalid or missing columns and a persisted reload ([`PivotTableSetupWidget.tsx`](../../src/components/widgets/PivotTableSetupWidget.tsx)). For `dynamicForm`, inspect the payload's `fieldList`/`recordOfFieldValues` and `defaultValues.isEditable`; its editable fields join the process or record form's validation and submission. For a record widget that merges to JSON, verify the named table field exists in a hidden section (rather than marking the field `isHidden`), the renderer returns stored values in `recordOfFieldValues`, required fields block Save, and only the JSON field reaches the record API. [REC-061](../acceptance/feature-matrix.md) proves the full round trip across five browser profiles. `childRecordList` can manage an association when `defaultValues.manageAssociationName` is set. `dataBagViewer` can edit through `storeDataBagVersion` when that process exists.
`filterAndColumnsSetup` renders a summary and paged, sortable, resizable preview on record view, and an editor when a form context is present, unless the payload sets `overrideIsEditable: false`. [`FilterAndColumnsSetupWidget.tsx`](../../src/components/widgets/FilterAndColumnsSetupWidget.tsx) hands the form to [`FilterAndColumnsSetupEditor.tsx`](../../src/components/widgets/FilterAndColumnsSetupEditor.tsx). The editor loads the selected table's metadata, uses `FilterBuilder` and `ColumnConfig`, offers ordered sort levels and a draft preview inside the modal, and writes `queryFilterJson`/`columnsJson` (or payload-selected field names) to host values on `OK`. Edit opens the filter tab; + Add Filters and + Add Columns appear when the respective saved content is absent and open the matching tab. `Cancel` and closing the dialog discard the draft; persistence still requires saving the host record or advancing a process. A `WIDGET` field adornment with `values.widgetName` uses [`WidgetAdornmentField.tsx`](../../src/components/forms/WidgetAdornmentField.tsx): it sends the host form values and `__formFieldAsWidget_FieldName` to the widget request, overrides `filterFieldName` with the actual field name (including a row index), and writes only that field back. Material supports `filterAndColumnsSetup` in this field path; other widget types show a contained unsupported message. Inspect [`filter-and-columns-utils.ts`](../../src/components/widgets/filter-and-columns-utils.ts) and the backend producer for `hideColumns`, `hideSortBy`, `hidePreview`, `allowVariables`, API-version settings, default filter fields, omitted joins, and custom labels. A missing table disables editing; missing required default-field values produce an alert when an editor control is pressed. A variable saves as a `FilterVariableExpression`; unresolved variables suppress preview and a query opened in a new window until a value is supplied. WID-030, WID-070, WID-071 and WID-072 cover the summary, preview, variable assignment, editor save, field-adornment process path and API-versioned metadata/query/count across five browser profiles. WID-072 supplies version flags to the widget response while the metadata and data requests go to real QQQ API-aware routes. The editor now shares Basic/Advanced controls, saved-view selection, density, refresh, column menus, and statistics with the query components. Saved-view selection is intentionally read-only in this host: it imports the view into the report draft. Read-only preview changes remain local; edited widths/pins serialize into columnsJson on OK and host Save. Starting in RC4, `omitExposedJoins` limits new filter/column choices in both the read-only preview and editor. Existing saved joined columns, criteria and sorts remain usable, and reordering available columns preserves omitted columns in place. Opening the editor and pressing OK preserves those saved fields; Basic/Advanced changes and host Save/reopen retain their values and widths. This is a choice restriction, not a backend permission boundary; existing joined data still uses the normal query permissions. WID-073 checks the configured and unconfigured cases against SQL, including local edits and reload. WID-073 verifies pin/width persistence, full-query copying, and nested statistics. Check the remaining embedded-query parity ledger before claiming complete equivalence; variable assignment currently uses Advanced mode.
### Composite input editors
For complete record submissions, call `wireValuesFromForm(table, values, originalValues)`: EntityForm passes `record.values`, association editors pass the existing row values, and process child editors pass the original dialog defaults. Ordinary updates still omit unchanged DATE_TIME fields. Preserve this distinction for copies and child-row edits; omitting the original can shift a repeated DST hour and truncate fractional seconds. REC-065 verifies the copy wire value and SQL source/copy; both child-editor paths have component regressions.
Use `toLocalDateTimeInput` for seeded process/standalone DATE_TIME values and `fromLocalDateTimeInput(value, originalValue)` at submission. Supplying the original preserves an unchanged instant in the repeated DST hour and its hidden subsecond precision. Normalize the final process payload after contributors have added values, so bulk-edit fields use the same contract. Do not slice UTC text into datetime-local controls or send local text as an instant. PRC-064 and WID-075 exercise America/New_York dates and seconds. Published RC7 sets a one-second step on plain TIME blocks, matching regular/shared fields. Keep TIME values as local clock strings; do not apply instant/timezone conversion. PRC-064 checks unchanged and edited seconds with SQL readback, and WID-074 checks standalone native stepping. This correction is published in RC7 at `f65cf6b`; RC6 remains unchanged.
`ProcessRun` preserves focus already held inside a configured `[data-qqq-autofocus="true"]` editor; other later screens still focus their heading. Preserve this precedence when changing screen lifecycle. On scalar Enter, normalize text in the hosting form before validation/submission; do not trim unrelated fields or convert typed values to display strings. PRC-063 covers later-screen focus, placeholders, prior values, first blur, whitespace validation and backend-stored text.
Keep plain block controls visually stable. Backend field metadata supplies the neutral `displayFormat: "%s"`; do not treat that as a request for a different editor. Reuse `BlockInputEditor` and `DynamicFormField` for choices, formatting, behaviors and supported field adornments. Process blocks must bind to the existing screen form and send file contents as multipart files. Never create nested forms or serialize a file as its name. Surrounding process values belong in a field widget's request context, but widget changes must update only its own field.
Verify hidden/read-only flags, typed choice IDs, numeric conversion, multiline Enter, required uploads and uploaded bytes. For uppercase/lowercase behaviors, replace part of mixed-case text with an uppercase character and check the caret plus stored values. Include Unicode expansion (`ß` to `SS`, `İ` to `i` plus combining dot), then type another character to verify the insertion point; retain backward selection direction. Cover both `STRING` and `TEXT`: textarea Enter must insert a newline without submitting, including with `submitOnEnter: true` (PRC-066/WID-074). For explicitly configured `PASSWORD` fields, verify masked editing, Unicode expansion, show/hide value preservation, Enter/Submit, and independent database readback. Also prove that passwords without a case behavior retain mixed case. The standalone choice field must not collapse below the neighboring plain input's width: a narrow control can place its Clear button under an ordinary center click. Check system and wider native fonts, plus a 320px viewport. The metadata-editor minimum is capped by available width. Sample-dashboard acceptance must initialize the owned backend session and diagnostics before its page-opening hook. PRC-062 exercises the backend round trip, and WID-074 checks the standalone controls. Screenshot comparisons must include the richer editors' height and scroll states; functional success does not approve a changed layout.
## Navigation and route rules
`/app/{slug}` resolves an app, table, process, or report from loaded instance metadata, in that order. Do not create a static page for each QQQ object. `isHidden` removes a table/process/report from navigation, while an authorized direct route still works. New dynamic route shapes must work in both standalone and Javalin static export. The export prerenders `_` placeholder paths; [`useRouteParams`](../../src/lib/hooks/use-route-params.ts) translates them from the browser path. Preserve `trailingSlash` handling and avoid constructing deep links that a static host cannot serve. Use [`material-links.ts`](../../src/lib/utils/material-links.ts) for generated links. The not-found route's [`LegacyPathRedirect`](../../src/components/layout/LegacyPathRedirect.tsx) recognizes nested Material paths and `QInstance.redirects` exact or trailing `/*` rules while preserving query/hash. Test deny/not-found loops, a child create URL (`/app/{table}/{id}/createChild/{childTable}`), and refresh of each route shape. Root-path hosting is the documented target; the parity ledger lists base-path support as Missing.
The [developer route catalog](./next-ui-developer-guide.md#4-navigation-and-deep-links) includes authentication callbacks, table/process developer views, record actions and child creation. `/app/{table}/{id}/{process}` resolves registered processes (table exact name, table suffix, then instance exact name) and redirects with record and return context; the runner enforces permission; preserve that route alongside the reserved edit/copy/dev pages.
The query URL owns `page`, `pageSize`, `filter`, and `q`; do not move shareable filter state into local storage. Saved views have `/app/{table}/savedView/{id}` and backend persistence. Local storage is for preferences, such as density, column configuration, and widget selections. See [state management](../STATE-MANAGEMENT.md). Put app shell labels, icons, and nested nav in metadata and verify breadcrumb titles against the same tree.
### Keep configuration coverage executable
The developer guide's [widget configuration recipes](./next-ui-developer-guide.md#widget-configuration-recipes), [link parameters](./next-ui-developer-guide.md#link-parameters-and-launch-examples), [theme properties](./next-ui-developer-guide.md#theme-property-reference), and [analytics setup](./next-ui-developer-guide.md#analytics-configuration-example) cover the supported keys and copyable response fragments. Update the matching reference whenever a consumer changes; a catalog row with only a widget name is insufficient documentation for new configuration.
- Compare all `WidgetRenderer` cases with the catalog, including canonical/demo payload branches. Keep `QqqComposite` leaf types and layouts covered separately; they are not top-level widget dispatch labels.
- Compare theme properties with `THEME_PROPERTY_KINDS`, including generated typography and radius keys. Keep browser-public `environmentValues` separate from build variables and backend secrets.
- Trace dropdown behavior through `ConnectedWidget`: lists pair by position, payload names are request keys, response defaults must be offered options, and `dropdownNeedsSelectedText` gates the body. The interface's `isRequired` and `defaultValue` fields do not by themselves implement those behaviors.
- Preserve `gridCols:sizeClass:*` overrides in widget metadata defaults. Their Material breakpoint widths differ from ordinary Tailwind defaults; explicit overrides can keep widgets side by side on smaller screens.
- Document `table` cell helper values and `subRows`, chart dataset/URL arrays, and host-specific composite behavior when changing these families. Modal control codes work locally; process `conditional` and interpolation depend on the composite host. RC6 icon compatibility preserves existing Lucide mappings, then lazily resolves valid legacy names through `LegacyMaterialIcon` and the pinned local font. Unknown names retain their fallback. Keep the codepoint inventory, font and license together; verify real JAR font delivery, both themes, existing mapped icons, image precedence, and bundle budgets before integration. Do not attribute this behavior to RC5 or claim support for every Material Symbols name/style.
- Check both table-scoped aliases (`/app/{table}/{process}` and `/app/{table}/{report}`) before record GET behavior. Process aliases take precedence over reports; matching names can shadow record IDs. Preserve form hash presets, record tab/view/return parameters, process selection/default inputs, and safe return-path validation.
- Preserve the distinction between a displayed preview, a host-form draft, and persisted data. A payload example demonstrates the contract shape; it does not establish backend acceptance for every combination of options.
## Security and accessibility review points
- Use the typed API client and same-origin cookie session. Do not put access tokens, passwords, or server secrets in browser storage, public environment variables, widget metadata, or tests.
- Session resume now calls `manageSession` with an empty request so browser code can use HttpOnly session cookies; do not restore a `document.cookie` dependency. Test sign-in, refresh, expiry and sign-out against cookie attributes and the real server. Authentication metadata's `?clearAuthenticationMetaDataLocalStorage` switch is a cache reset, not a permission bypass.
- Preserve existing identity-provider callback registrations: OAUTH2 uses `/token`, AUTH_0 uses `/`. Provider descriptions may reach login only through the state-matched callback and a one-time browser session value; never render free text from `?error_description=` on a direct link.
- Check permission at the UI and the backend. Exercise a denied persona in acceptance; a hidden button alone is insufficient. Do not call widget, process, or record endpoints for an omitted or disabled object.
- For exposed query joins, check every target and intermediate table named in `joinPath`. The query screen filters its metadata before rendering columns or filters, and the request hook applies the same rule before sending a join. Keep both paths aligned when changing join UI.
- Sanitize HTML. Review CSS, external links, iframes, custom bundle URLs, and CSP when adding a new embed. The Javalin host and standalone server supply security headers; test the deployed response. The [security review](../security/next-ui-review.md) predates some integrated fixes, so reconcile each follow-up with source and current acceptance evidence.
- Give forms real labels, `aria-required`/`aria-invalid` where relevant, keyboard operation, error text, and a sensible focus target. Dialogs and phone sheets must trap focus and restore it. Check both desktop keyboard and phone/tablet touch interactions.
- The nonmodal Columns popup focuses its named container on open and returns focus to its trigger on explicit close/Escape. Outside clicks keep focus on the clicked control. Embedded report column editors must not take focus when mounted. Verify keyboard search and native browser Tab behavior with QRY-092.
- Keep errors local when possible: widget failures show a contained retry and leave neighboring widgets usable; route errors and missing records have dedicated states. Never swallow an API failure into a blank page.
## Quality gates and evidence
| Command | Evidence it supplies |
|---|---|
| `pnpm typecheck` | TypeScript contract check. |
| `pnpm lint` | ESLint and license headers. |
| `pnpm test` | Vitest unit/regression behavior. |
| `pnpm test:e2e` | Mocked Playwright UI paths, useful but not real backend compatibility. |
| `pnpm build:export` | Javalin static-export build and dynamic placeholder output. |
| `pnpm perf:budget` | Static-export JS/CSS budget after `build:export`; see [performance guide](../acceptance/performance.md). |
| `QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet pnpm test:acceptance` | Full real-sample browser and backend gate when `QQQ_SAMPLE_JAR` points to a compatible sample jar. |
For RC1, prioritize workflow depth in Chromium: normal entry/defaults, validation, edit/cancel, save/reopen, dependent selections, permissions/recovery and persisted records or downloads. Preserve broader tests and report their status; do not treat scenario counts as proof that everything works.
Read [acceptance setup](../../tests/acceptance/README.md) and [browser matrix](../acceptance/browser-matrix.md) before running the final gate. Default acceptance runs only Chromium; the five-project command is necessary to claim the stated browser scope. Phone and tablet run tagged tests, with matrix coverage enforced by the gate. Every test title needs its matrix ID; use fixtures with a reset database, verify persisted effects through SQL, and include `diagnostics` for console, network, and CSP failures. Do not loosen a matrix row, skip a failing browser, or declare an external service excluded without the documented approval path.
## Common mistakes to avoid
- **Inventing config from a plan or TypeScript escape hatch.** Read the actual QQQ producer and renderer; `Record<string, unknown>` does not prove a key is honored.
- **Treating mocked widgets as a canonical backend contract.** `statistics.value` and several demo aliases are fixtures; canonical QQQ statistics uses `count`.
- **Editing a route for one table.** The slug route and app tree are generic; change backend metadata or general dispatch behavior.
- **Fetching inside a presentational metadata component.** Keep metadata as a prop where the component pattern expects it; use hooks/API modules at the connected boundary.
- **Calling raw `fetch` for QQQ endpoints.** It bypasses versioned base URL, credentials, and centralized error/session behavior.
- **Using `name` as display text.** It is an internal identifier; use `label` and a deliberate fallback only when metadata is genuinely absent.
- **Claiming Material theme, custom bundle, URL, or workflow parity from a happy path.** Check current code, the parity ledger, negative/permission rows, and actual browser/backend acceptance evidence. The filter/column editor has real-server coverage in WID-070–WID-072, while the embedded full query screen remains parity work.
- **Testing a static deep link on a generic file server.** QQQ Javalin serves placeholder pages for dynamic paths; standalone Next has its own routing path.
## Completion checklist for an agent change
1. The backend metadata or widget/process producer and the frontend consumer agree on names, types, and payload fields.
2. An authorized user can complete the workflow, while an unauthorized user cannot perform it through the API.
3. Loading, empty, malformed, missing, failed, and retry states are addressed where they apply.
4. Keyboard, focus, labels, responsive phone/tablet behavior, and contrast have been checked for changed UI.
5. Focused tests and the applicable real-server matrix row pass. The final browser claim names the projects actually run.
6. Static export or standalone build, as applicable, works on refresh and direct deep link.
7. Docs and the parity ledger reflect the observed behavior; any open Partial/Missing rows remain visible until proven closed.
### Appearance preferences
Use the real ThemeProvider when rendering UserPreferencesDialog or a sidebar with its user footer. The saved `darkModePreference` is distinct from effective `isDarkMode`: an application theme overrides dark mode without erasing the choice. Preferences exposes Light/Dark, explains a theme lock, and resets both saved appearance and layout defaults. Preserve `qqq-dark-mode` storage compatibility. Verify changes through the actual Preferences flow and reload (NAV-056), including phone drawer layering/focus (NAV-038); injecting localStorage alone does not exercise the user control. There is currently no system appearance choice or account/device synchronization.
### Google Drive integration boundary
Google Drive picker changes span the frontend component/SDK loader and Javalin's CSP
and v1 public-environment allow-list (#704; backend PR #908 merged to develop as `bac663044`). The frontend needs those backend changes to receive its public configuration and load under the configured CSP. PRC-065 tests controlled SDK responses with
real process execution and SQL; PRC-039 is the approved real-account exclusion. Preserve
the shared-drive view, folder-only validation, cancellation and token expiry handling.
Never log or persist the access token, and never publish OAuth client secrets in metadata. Javalin applies conditional CSP to its static export; standalone Node/container deployments derive the same sources from backend `dashboardCspSources` authentication metadata. Older backends that omit that optional field require runtime `QQQ_DASHBOARD_CSP_SOURCES` using the [documented Google directives](./next-ui-developer-guide.md#google-drive-process-configuration). Check the actual dashboard HTML response from the selected host. RC1 remains immutable and retains its historical placeholder; these changes belong to the ongoing final 1.0 work.
### Script editor suggestions
`CodeEditor` autocomplete is opt-in from `ScriptEditorDialog`; keep other metadata code fields unchanged unless their contract calls for it. QQQ helpers, language keywords and file identifiers are local suggestions. Preserve native undo/redo, composition input, read-only behavior, caret/selection validation, and Escape-then-Tab focus escape. `records/script-completions.spec.ts` verifies loaded-revision undo/redo, prefix and pointer/touch insertion, both-theme accessibility, and SQL-backed save/reload. Native browsers group consecutive edits differently; do not claim every insertion creates a separate undo transaction.
### RC8 implementation notes
Public JAR checksum/signature/fresh-consumer checks and 28 affected desktop/mobile workflows passed. Full tag Chromium passed 602 and mobile 563; Firefox, WebKit and tablet retain failures, including tablet Clear-search focus. Consult the release notes before claiming cross-browser completion.
- Search must contain the entire Tab/Shift+Tab cycle, handle Escape from every dialog control, restore the opener after dismissal, and move focus to the input before Clear removes its own button. Explicitly focus a phone pointer opener because WebKit does not necessarily focus clicked buttons. Cancel delayed autofocus when closing. `navigation/search-focus.spec.ts` covers light/dark and pointer/keyboard openings; preserve the existing layout.
- Keep RapiDoc width handling on its public CSS parts. The named `qqq-api-overview` and `qqq-api-overview-wide` containers restrict narrow wrapping and wide content sizing to overview specification actions; do not apply it to authentication or endpoint controls. Test full visibility before focus can scroll a clipped control into view, real JSON downloads, keyboard new-tab behavior, both themes, and label fit with wider application fonts at desktop and phone widths.
- LONG range validation must run only after integer syntax succeeds. Preserve integer-string bounds as `BigInt` values; converting through `Number` rounds large bounds. Keep the mixed numeric comparison for fractional numeric bounds. Test `safeParse` error results without exceptions, exact positive/negative boundaries, optional blanks, and correction/submission through block and shared process forms. These changes do not introduce a backend range-metadata contract.
-518
View File
@@ -1,518 +0,0 @@
# Next UI developer guide
> **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 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.
## 1. What an application supplies
QQQ supplies an instance metadata document with `apps`, ordered `appTree`, `tables`, `processes`, `reports`, `widgets`, and `branding`; help content, environment values, supplemental metadata, and redirect rules may also be present. The UI renders those objects for the signed-in user's permissions. A table, widget, or route name is an internal identifier; its `label` is the text users see. Define names and behavior on the backend, then verify the versioned metadata response. Do not edit the frontend to add an application-specific table or menu item.
The source of truth for the frontend shape is [`src/types/metadata.ts`](../../src/types/metadata.ts). A small, **partial response fragment** illustrates the relationships; it is not a complete `QInstance` response:
```json
{
"appTree": [{ "name": "operations", "label": "Operations", "type": "APP", "children": [{ "name": "orders", "label": "Orders", "type": "TABLE" }] }],
"apps": { "operations": { "name": "operations", "label": "Operations", "widgets": ["ordersToday"], "sections": [{ "name": "work", "label": "Work", "tables": ["orders"] }] } },
"widgets": { "ordersToday": { "name": "ordersToday", "label": "Orders today", "type": "statistics", "hasPermission": true, "gridColumns": 4 } },
"branding": { "appName": "Operations", "accentColor": "#1d4ed8" }
}
```
`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
The preferred QQQ-hosted distribution is the `com.kingsrook.qqq:qqq-frontend-next` Maven jar. A QQQ 4.1+ application's `QApplicationJavalinServer` serves its static export from `next-dashboard/` at `/` on the same origin as `/qqq/v1`. The [repository README](../../README.md) and [jar README](../maven-artifact/README.md) describe this packaging. The QQQ [sample quickstart](https://github.com/QRun-IO/qqq/blob/main/qqq-sample-project/README.md#quickstart-with-next) exercises the packaged path. Material can be selected with `withServeFrontendMaterialDashboard(true)` or `-Dqqq.javalin.frontend=material`; do not assume Material and Next use the same URL layout.
```xml
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.36</version>
</dependency>
```
For frontend development, use a compatible Node.js version (the README specifies 20.19+ or 22.12+) and pnpm 9.15.9:
```bash
pnpm install
NEXT_PUBLIC_MOCK_API=true pnpm dev
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.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 |
|---|---|
| `NEXT_PUBLIC_API_BASE_URL` | Defaults to `/qqq/v1`; baked into browser requests. Keep the UI and API on the same origin where possible. |
| `NEXT_PUBLIC_MOCK_API` | `true` enables MSW development fixtures; `build:export` forces `false`. |
| `QQQ_NEXT_OUTPUT` | `export` selects the Javalin-hosted static export; otherwise standalone build. |
| `QQQ_BACKEND_URL` | Standalone build only: backend origin for Next rewrites; fixed at build time. |
| `NEXT_PUBLIC_APP_VERSION` | Optional version reported to the backend; defaults to `package.json` version. |
| `QQQ_DASHBOARD_CSP_SOURCES` | Standalone server runtime setting: additional CSP directives/sources for external services. See Google Drive hosting below. |
The static export is built for `/`, with `trailingSlash: true`. QQQ serves placeholder HTML for dynamic route shapes; [`useRouteParams`](../../src/lib/hooks/use-route-params.ts) reads real path segments from the browser. Keep that behavior intact when adding routes. The [parity ledger](../acceptance/material-parity.md) currently lists sub-path hosting as unsupported. A plain file server without the QQQ route fallback is not a valid deep-link test.
## 3. Authentication and security
`GET /qqq/v1/metaData/authentication` selects `AUTH_0`, `OAUTH2`, `FULLY_ANONYMOUS`, `MOCK`, or `TABLE_BASED` (see [`QAuthenticationMetaData`](../../src/types/metadata.ts) and [`auth-provider.tsx`](../../src/lib/auth/auth-provider.tsx)). OAuth flows use authorization code and PKCE; table-based sign-in sends credentials to the QQQ session endpoint. The server issues `sessionUUID`/`sessionId` cookies; current Next code resumes an existing session with an empty `manageSession` request, allowing HttpOnly cookies that browser script cannot read. Browser API calls use credentials. A 401 clears session state and returns to sign-in. The pre-login response exposes only limited login branding, not banners or custom CSS. Authentication metadata is cached briefly; `?clearAuthenticationMetaDataLocalStorage` clears that cache.
Existing provider registrations remain usable: OAUTH2 redirects to `{origin}/token`, while AUTH_0 redirects to `{origin}/`. Both use the same state-checked PKCE callback. A state-matched provider denial displays its description once on the login page; a forged callback or direct login link shows only a safe generic error.
Permissions apply to metadata and data: check table capabilities and the corresponding read/insert/edit/delete flags, `process.hasPermission`, `report.hasPermission`, and `widget.hasPermission`. A disabled widget can show a permission message without fetching; omitted metadata cannot be guessed from the UI. Use the backend to prove a forbidden operation is rejected. HTML from metadata or widget payloads is sanitized before display. Treat theme `customCss` and custom component bundles as trusted application configuration with a stricter review boundary. The Javalin dashboard and standalone Next server configure security headers and CSP; embedding an external provider needs an allowed source, not a disabled policy. The [security review](../security/next-ui-review.md) is an earlier audit; compare its follow-ups with current code and the latest acceptance gate before treating them as resolved.
## 4. Navigation and deep links
[`useAppTreeRoutes`](../../src/lib/hooks/use-routes.ts) recursively walks `appTree` to build sidebar links, labels, ancestor breadcrumbs, and the first accessible app. `APP` nodes nest; visible `TABLE`, `PROCESS`, and `REPORT` nodes navigate to `/app/{name}`. Hidden objects are omitted from navigation but can still have direct routes when authorized. An app home uses its `widgets` and `sections`; a table slug opens its query page; a process slug opens its wizard; a report slug opens its runner. The unified route resolves name collisions in that order: app, table, process, report. Use unique names across these maps.
| URL | Screen |
|---|---|
| `/` | Opens `/app`, or handles an Auth0 authorization callback. |
| `/login`, `/callback`, `/token` | Sign-in and supported authorization callback routes. |
| `/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. |
| `/app/{tableOrProcessName}/dev` | Table or process developer view; table takes precedence. |
| `/app/{tableName}/{recordId}` | Record view; `/edit`, `/copy`, and `/dev` are subroutes. |
| `/app/{tableName}/{processName}` | Table-scoped process alias; redirects to the process runner with query parameters and a return path to the table. |
| `/app/{tableName}/{reportName}` | Alias for a report whose `tableName` matches; redirects to its runner. |
| `/app/{tableName}/{recordId}/createChild/{childTable}` | Child create form with parent association values. |
| `/app/{tableName}/{recordId}/{processName}` | Resolves a registered process and redirects to its runner with record and return context; execution still checks permission. |
| `/app/{tableName}/key?...` | Unique-key lookup. |
| `/app/{tableName}/savedView/{viewId}` | Server-backed saved query view. |
| `/app/{processName}` | Process run. |
| `/app/{reportName}` | Report run and download. |
| `/app/search?q=...` | Navigation, recent-record, and available record search results. |
| `/app/developer` | Developer information available to permitted users. |
The header search and `/` dialog locate navigation targets and recent records; record search is offered when the backend supports it. RC8 includes the search-focus correction: Tab/Shift+Tab remain within the search dialog, Clear returns focus to the search input, and Escape or backdrop dismissal restores focus to the opener. A pointer-opened phone search restores focus to its actual button in verified Chromium/mobile cases. RC8 tablet WebKit has two open Clear-search focus failures; see the release notes for remaining browser issues. RC7 does not include this correction. The command palette opens with `.` or Cmd/Ctrl+K and has a header button; `?` opens keyboard help. Keyboard shortcuts on query and record pages are listed in the [feature matrix](../acceptance/feature-matrix.md). The sidebar has a phone drawer, nested app groups, banners, and a user menu. Branding can add a company footer and a themed bar above the shell. `?helpHelp` reveals help-slot keys for content authors.
Material's nested `/{app}/{table}/{id}` URLs are not the native Next route. A not-found page now checks the signed-in instance metadata, applies exact or `/*` redirect rules, and resolves nested Material app paths to `/app/...` while preserving query/hash ([`legacy-paths.ts`](../../src/lib/utils/legacy-paths.ts), [`LegacyPathRedirect.tsx`](../../src/components/layout/LegacyPathRedirect.tsx)). Verify a specific old bookmark rather than assuming arbitrary legacy URLs match. For links generated by widgets use [`material-links.ts`](../../src/lib/utils/material-links.ts), which normalizes supported legacy links and preserves a trailing slash for static-export routes.
### Link parameters and launch examples
The table-scoped process/report aliases are resolved before a record GET, so avoid record IDs that collide with those names. Process resolution checks an exact table process, then an instance process; the record-scoped route also accepts a table process name suffix. A matching process takes precedence over a report. Use full process names for unambiguous new links. See [`material-links.ts`](../../src/lib/utils/material-links.ts).
| Link context | Supported parameters and behavior |
|---|---|
| Record view query | `tab` selects a section; `view=tabs` or `view=list` selects the layout. `from` is a safe local return path and `fromLabel` labels it. The default view comes from Preferences. |
| Record view hash | `#audit` opens audits when available; `#/launchProcess=<name>` launches a process; `#<sectionName>` selects a section. `#/createChild=<table>` starts child creation and accepts the presets below. |
| Create/child-create hash | `#/defaultValues=<encoded JSON object>/disabledFields=<encoded JSON array>` seeds and locks fields. `disabledFields` also accepts a JSON object whose truthy entries name locked fields. These are UI presets; backend validation and authorization still apply. |
| Process query | `recordsParam=recordIds&recordIds=1,2` selects IDs; `recordsParam=filterJSON&filterJSON=<encoded QQueryFilter>` selects by filter. The legacy selector `queryFilter` uses the same `filterJSON` value. |
| Process context | `defaultProcessValues=<encoded JSON object>` presets inputs; `returnTo=<encoded local path>` supplies the return destination. `tableName` supplies launch context only for a process without its own table and only when that table exists in metadata. |
| Dashboard hash | `#<widgetName>` targets a widget anchor. An embedded `process` widget also reads the page's `recordIds` query parameter. |
Build encoded links instead of concatenating raw JSON. For example, where metadata declares the supplied process/table names:
```ts
const params = new URLSearchParams({
recordsParam: 'recordIds',
recordIds: selectedIds.join(','),
defaultProcessValues: JSON.stringify({ sendNotification: true }),
returnTo: `/app/${encodeURIComponent(tableName)}`,
})
const processHref = `/app/${encodeURIComponent(processName)}/?${params}`
const createHref = `/app/${encodeURIComponent(tableName)}/create/#/defaultValues=${encodeURIComponent(JSON.stringify(defaultValues))}/disabledFields=${encodeURIComponent(JSON.stringify(disabledFieldNames))}`
```
Only preset fields actually declared by the application's metadata. Record/form hash decoding is implemented by [`formPresetsFromHash`](../../src/lib/utils/material-links.ts); process query decoding lives in the [slug page](../../src/app/%28dashboard%29/app/%5Bslug%5D/page.tsx). Test a generated link on the static-export host and after refresh.
## 5. Tables, queries, records, and forms
A failed query displays its backend error with Retry and Dismiss controls. Retry repeats the request; Dismiss closes the panel without changing the filter. Correct or reset an invalid filter to load records again. Query and pivot error text use theme-derived shades for readability; the top error notification also uses the configured error palette.
Tables are described by [`QTableMetaData`](../../src/types/metadata.ts): `primaryKeyField`, `fields`, `sections`, `associations`, `exposedJoins`, capabilities, and permission flags. A field supplies `name`, `label`, `type`, `isRequired`, `isEditable`, `isHeavy`, `isHidden`, and `adornments`; possible values, bounds, formats, and help are optional. The supported primitive field types are `STRING`, `INTEGER`, `LONG`, `DECIMAL`, `BOOLEAN`, `DATE`, `TIME`, `DATE_TIME`, `TEXT`, `HTML`, `PASSWORD`, and `BLOB`. [`DynamicFormField`](../../src/components/forms/DynamicFormField.tsx) chooses editors, while [`zod-from-metadata.ts`](../../src/lib/utils/zod-from-metadata.ts) builds validation. Configure fields in QQQ metadata, not in a per-table React component.
Table dashboard settings are read from `supplementalMetaData.materialDashboard` in the v1 response, with `supplementalTableMetaData.materialDashboard` accepted as a legacy fallback. The v1 object takes precedence when both are present. These settings include `defaultQuickFilterFieldNames`, `gotoFieldNames`, `showRecordSidebar`, and `recordViewActionsPlacement`. Quick-filter defaults use the configured nonempty field list, otherwise T1 section fields; unresolved names and duplicates are removed. Query-selectable `virtualFields` also become columns, but only virtual fields with `isQueryCriteria` can be filtered or sorted.
For a fixed choice list, put `inlinePossibleValueSource` on the QQQ field with `type: ENUM`, an `idType` matching the field, and ordered `enumValues` containing `id` and `label`. Do not also set `possibleValueSourceName`: QQQ rejects both sources on one field. Next filters inline labels by prefix without a possible-values request and stores the selected `id`. A `CHIP` field adornment with keys such as `color.HIGH` and `icon.HIGH` styles both record values and form options. The v1 table metadata response must include the inline source; [REC-062](../acceptance/feature-matrix.md) verifies its rendering, persistence, and reload against the sample server.
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. 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.
Record section tabs support Left/Right, Home and End to move focus. Enter or Space opens the focused section; Tab moves into the named content panel. Moving focus alone does not load section widgets. Phone sections use accordion buttons with Enter/Space activation. The selected tab remains reflected in the URL.
Query header labels can wrap to two lines when their text and sort indicator do not fit, including with wider system or configured fonts. Configured column widths and 44px touch menu targets remain effective. Active sorts display their direction arrow; unsorted headings remain labeled sort buttons. Relative-date preset menus scroll within the space available beside their trigger.
Selection and display-density menus support arrow-key navigation, Enter to choose, and Escape to close and return focus to the trigger. They stay within the viewport on phone and tablet. Density choices expose radio-menu semantics; the selected option uses the theme primary/foreground color pair.
Record action menus come from `QTableMetaData.menus`. The first `VIEW_SCREEN_ACTIONS` menu replaces the default Actions menu; `VIEW_SCREEN_ADDITIONAL` menus add ordered menu buttons. Item types are `BUILT_IN`, `RUN_PROCESS`, `DOWNLOAD_FILE`, `SUB_MENU`, `SUB_LIST`, and `DIVIDER`. Their `values` supply `option`, `processName`, `fieldName`, or nested `items`, respectively. Built-in options are `NEW`, `COPY`, `EDIT`, `DELETE`, `DEVELOPER_MODE`, `AUDIT`, `THIS_TABLE_PROCESS_LIST`, and `ALL_TABLES_PROCESS_LIST`; permissions filter available operations. See [`record-menu-utils.ts`](../../src/lib/utils/record-menu-utils.ts) for resolution and process deduplication.
For a joined field in a record section, list its qualified name (`person.firstName`) in `QTableSection.fieldNames` and expose a readable join through `QTableMetaData.exposedJoins`. Next asks for that join on the record GET and uses the joined table's field metadata to render the qualified response value. Join paths through tables without read permission are omitted. [REC-064](../acceptance/feature-matrix.md) checks the real sample server in five browser profiles.
## 6. Processes and reports
`QProcessMetaData` declares the process name, label, table association, permission, limits on input records, and ordered `frontendSteps`. The wizard initializes a run, submits each step, polls asynchronous jobs, handles validation results, and cancels or retries as appropriate ([`ProcessRun.tsx`](../../src/components/process/ProcessRun.tsx), [`processes.ts`](../../src/lib/api/processes.ts)). It preserves return navigation to its caller. `QFrontendComponent.type` selects a process component. The currently registered types are:
| Type | Purpose |
|---|---|
| `HELP_TEXT`, `HTML` | Guidance and sanitized markup. |
| `EDIT_FORM`, `VIEW_FORM` | Metadata-driven writable and read-only fields. |
| `RECORD_LIST`, `PROCESS_SUMMARY_RESULTS` | Input/output records and completion summary. |
| `BULK_EDIT_FORM` | Changes applied to selected records. |
| `BULK_LOAD_FILE_MAPPING_FORM`, `BULK_LOAD_VALUE_MAPPING_FORM`, `BULK_LOAD_PROFILE_FORM` | File preview; flat/tall/wide layout and header-row choices; field-to-column/default mappings; per-value translations; saved load/edit profiles. |
| `VALIDATION_REVIEW_SCREEN` | Review validation findings before commit. |
| `DOWNLOAD_FORM` | Download parameters and result. |
| `GOOGLE_DRIVE_SELECT_FOLDER` | Selects one folder from a shared drive using Google OAuth. Requires `GOOGLE_APP_CLIENT_ID` and `GOOGLE_APP_API_KEY` in instance environment values. Submits `googleDriveAccessToken`, `googleDriveFolderId`, and `googleDriveFolderName`; cancellation clears them. |
| `WIDGET` | Embedded connected widget. |
Do not invent `values` keys for these components: their shapes vary by backend process implementation. Start with the QQQ producer and a [real fixture](../../tests/acceptance/fixture), then verify the versioned response and end-to-end step submission. A `WIDGET` step uses the named widget's full dashboard controls; an editable `childRecordList` contributes `frontendRecords`, while an editable `rowBuilder` writes its configured output field. Ad hoc composite blocks share the dashboard block renderer, including modal control codes, typed inputs and layouts. A dashboard widget of type `process` embeds the wizard without its stepper or Cancel action; Return starts a fresh run in place. For bulk load, test duplicate headers, required fields, saved profiles, value mappings, validation review, and the committed records. Reports appear in `QInstance.reports`. [`ReportRun.tsx`](../../src/components/reports/ReportRun.tsx) offers CSV, XLSX, and JSON, handles required inputs and asynchronous process-backed reports, and provides a download. A report without a backing process uses the versioned streaming report route. Saved and scheduled report behavior is covered by the report rows of the acceptance matrix.
For bulk load, the mapping screen derives required fields, child associations, layout and suggested mappings from the backend's table structure and process values. Its Add Fields menu searches the main and child-table groups; WIDE layouts may repeat a child field. A mapped field can read a file column or a typed default, including possible values, dates and booleans. Header-row changes remap by name and warn about duplicates. File and value mapping show the current profile in a bordered Saved Bulk Load Profiles section; the final review shows that status in its profile summary. Saved bulk-load profiles are grouped by owner; owners can save, rename and delete, while shared profiles can be selected but not modified. Open the profile menu with Enter or Arrow Down; use arrow keys or Home/End to move, Enter to choose, and Escape to close. Long lists scroll within the viewport. New, Save As and Rename open an inline Profile Name field inside the profile section. Save Profile or Enter saves the name without advancing the process; Cancel or Escape closes it. Duplicate-name errors stay beside the editor. Closing returns focus to the opening control, or the profile-menu button if that control was replaced after saving. Updates and deletion retain their confirmations. New, Empty Mapping, Suggested Mapping and Reset All Changes affect only the draft until the process step is submitted. The validation preview can show the associated child grids declared by `previewRecordAssociated*` values.
Report output formats come from the backend's `reportFormat` possible values unless a process screen declares its own `reportFormat` field. Report input screens use the same dynamic form renderer as other processes, so declared possible values, Booleans, dates and validation rules apply. Intermediate report process screens render before the download; the completed file can be a server path or storage reference.
Standard report processes with an input form followed by a download use the original compact input card and **Generate Report** action. Cancel remains available beside it. Ordinary fields stack at a 384px maximum width; explicitly configured grid widths and rich editors keep their layout. These inputs retain the shared dynamic form, required validation, possible values and typed submission. Longer or custom report processes retain their step headings and navigation.
## 7. Widgets: placement, configuration, and data
Each app dashboard lists widget **names** in order. The matching `QWidgetMetaData` supplies `name`, `label`, optional `type`, and `hasPermission`. The UI posts to `/qqq/v1/widget/{name}` with current dropdown and record parameters ([`ConnectedWidget.tsx`](../../src/components/widgets/ConnectedWidget.tsx), [`widgets.ts`](../../src/lib/api/widgets.ts)). The widget response contains its data and sometimes its type. [`WidgetRenderer.tsx`](../../src/components/widgets/WidgetRenderer.tsx) uses `metadata.type` first, falling back to `payload.type`; it contains errors and unknown types within the widget.
A valid TypeScript metadata example, suitable for an instance metadata fixture (the backend should produce the equivalent JSON), is:
```ts
import type { QWidgetMetaData } from '@/types'
const ordersToday = {
name: 'ordersToday',
label: 'Orders today',
type: 'statistics',
hasPermission: true,
gridColumns: 4,
isCard: true,
showReloadButton: true,
showExportButton: true,
tooltip: 'Orders created since midnight',
icon: 'inventory_2',
collapsible: { isCollapsible: true, initiallyOpen: true },
dropdowns: [{ name: 'region', label: 'Region', possibleValueSourceName: 'regions', type: 'POSSIBLE_VALUE_SOURCE' }],
storeDropdownSelections: true,
} satisfies QWidgetMetaData
```
`gridColumns` spans 1–12 twelfths on large screens (default 12); smaller screens stack widgets unless explicit breakpoint overrides are supplied in `defaultValues` (see the configuration recipes below). `isCard: false` removes card chrome. `icon` supplies the main header glyph using the Next outline presentation. `collapsible.isCollapsible` enables a header toggle, starts according to `initiallyOpen`, and remembers state under `qqq.widget.collapsibleOpenState.<name>`. `minHeight`, `footerHTML`, help content, `icons.topLeftInsideCard`/`topRightInsideCard`, tooltip, reload, and export customize the frame. Payload `label`, `sublabel`, and `footerHTML` can override or add frame text; a parent payload's `isLabelPageTitle` promotes the label to a page title. HTML is sanitized. For export, provide `csvData` as rows of cells; a table payload may also export `columns` and `rows`. The dropdown controls are declared in metadata **and populated by parallel payload lists** (`dropdownNameList`, `dropdownLabelList`, `dropdownDataList`, `dropdownDefaultValueList`). Controls offer searchable choices or a native date input; metadata can set `width`, `startIconName`, `allowBackAndForth`, `backAndForthInverted`, and `disableClearable`. The selected ID becomes a request parameter; a possible-value source uses its source name. Required selection can delay content, and `storeDropdownSelections` persists choices at `qqq.widgets.dropdownData.<widget>.<param>`. See [WID-040–WID-050](../acceptance/feature-matrix.md) for the tested contract.
Choice dropdowns support search and keyboard selection. A `DATE_PICKER` uses the platform calendar and a Today action; set `allowBackAndForth` for previous/next day buttons. If the `timeframe` dropdown offers `custom`, selecting it opens start and end inputs; after both are entered, the UI sends `custom,<UTC start>,<UTC end>`.
For example, a canonical QQQ statistics response uses `count`, not the demo fixture's `value`:
```json
{ "type": "statistics", "count": 42, "countContext": "open orders", "percentageAmount": 12.5, "percentageLabel": "vs previous week", "increaseIsGood": true }
```
The catalog below lists **every explicit dispatch type** in `WidgetRenderer.tsx`. Payload fields are from the corresponding renderer; they are examples of what the backend emits, not a replacement for the QQQ Java renderer API. When `type` is `chart` or a chart variant, canonical `chartData.labels` and `chartData.datasets` select the QQQ chart path. A payload with `chartType`, `seriesData`, or demo `data` instead uses the legacy/demo chart path.
### Canonical QQQ display widgets
| `type` | Use and important response fields |
|---|---|
| `alert` | Status message: `html`, `alertType`, optional `bulletList` and `hideWidget`. A demo `message` payload uses the demo alert renderer. |
| `divider` | Horizontal separator, optionally `label`; rendered without normal card chrome. |
| `fieldValueList` | Labeled record fields: `fields`, `record.values`, optional `record.displayValues`, prefix icon maps, indentation map. Display values win. |
| `generic` | Common widget frame fields, especially `sublabel` and sanitized `footerHTML`; optional `html` payload. |
| `html` | Sanitized `html` string. A demo payload with `blocks` uses the demo block renderer. |
| `location` | `imageUrl`, `title`, `description`, `location`, `footerText`. |
| `quickSightChart` | Provider embed `url` in an iframe; supply a server-authorized URL and appropriate CSP. Real AWS account coverage is an approved external exclusion. |
| `stepper` | `steps` with labels and optional links/icons, plus zero-based `activeStep` and optional `title`. |
| `usaMap` | `mapMarkerList` of names and latitude/longitude; optional CSS `height`. |
### Canonical QQQ charts, statistics, and tables
| `type` | Use and important response fields |
|---|---|
| `barChart` | Vertical bars from `chartData.labels` and `chartData.datasets`; dataset `data`, colors, and URLs. |
| `horizontalBarChart` | Horizontal bars; supports negative values relative to zero. |
| `stackedBarChart` | Multiple datasets stacked for each label. |
| `lineChart` | Line points from labels and datasets. |
| `smallLineChart` | Compact line presentation with `title` and optional sanitized `description`. |
| `pieChart` | Colored slices and legend; optional `chartSubheaderData`. |
| `chart` | Generic canonical chart, displayed as a bar variant when `chartData` is used; can be inferred from payload type. |
| `statistics` | Headline `count`, optional `countContext`, `percentageAmount`, `increaseIsGood`, and count/percentage links. Demo `value` payload uses the demo statistic renderer. |
| `multiStatistics` | `statisticsGroupData`: groups with `header`, `subheader`, icon, and statistic list. |
| `table` | `columns` (`header`, `accessor`, alignment, optional help/width) and `rows`; typed number, HTML, tooltip, image, composite/block cells; hidden helper columns, expandable subrows, optional `noRowsFoundHTML`, entries-per-page, sticky header and last-row behavior. |
| `multiTable` | `tableDataList` of table payloads, each with its own label, rows and export action. |
### Composite and interactive widgets
| `type` | Use and important response fields |
|---|---|
| `composite` | QQQ block tree: `blocks`/`blockTypeName`, layout, values, links, tooltips and styles. Without those fields, demo `childWidgets` uses `CompositeWidget`. |
| `parentWidget` | Server response `childWidgetNameList`; renders permitted registered children in a 12-column grid or `layoutType: "TABS"`. Children fetch their own data. |
| `process` | Inline wizard using `processMetaData`, optional `defaultValues` and selected record IDs. Server permission still applies. |
| `childRecordList` | Joined child records from `queryOutput.records` and `childFrontendTableMetaData`/`childTableMetaData`; `includeExposedJoinTables` adds readable join columns, and `defaultValuesForNewChildRecords` hides parent-key columns on screen. Optional `viewAllLink`, add-child defaults, row limits, `disableRowClick`, and CSV export via `showExportButton`. On a record form, `defaultValues.manageAssociationName` enables association editing. |
| `customComponent` | Loads a trusted bundle from metadata `defaultValues.componentSourceUrl` and `componentName`, then resolves `window[componentName][componentName]`. The component receives widget metadata/data/record, `qContext`, and `qfmdBridge`; `window.React`/`window.ReactDOM` are exposed. Check CSP and test the actual older bundle before claiming compatibility. |
| `cronUI` | Record view displays the expression, time zone and backend `cronDescription`. Put the widget in a table section and set `defaultValues.includeOnRecordEditScreen: true`, `cronExpressionFieldName`, and `timeZoneFieldName` to edit the named record fields on create/edit forms. The expression editor offers Basic days/hours/minutes, Advanced Quartz input, Clear, live description and validation. Give the time-zone field a possible-value source to render a select. |
| `dynamicForm` | Labeled `fieldList` and `recordOfFieldValues`, including merged JSON field values. Editable in a form/process host when `defaultValues.isEditable` allows it; fields join host validation and submission. In a record form, `mergedDynamicFormValuesIntoFieldName` names the declared JSON table field that stores the values. Put that field in a hidden section, so QQQ still returns it to the widget renderer; a field marked `isHidden` is omitted from normal record reads. The renderer should return current values in `recordOfFieldValues` for reloads. |
| `dataBagViewer` | Data bag/version browser with raw/preview tabs and version editor when `storeDataBagVersion` is available; uses `queryParams.id` or record ID and the built-in `dataBag` and `dataBagVersion` tables. |
| `pivotTableSetup` | Record view summarizes saved `pivotTableJson` (`rows`, `columns`, `values`). In a form, a switch and modal edit its definition after table/`columnsJson` fields are available. OK changes the form draft; Save persists it. Turning the switch off clears the stored definition when the host form is saved. |
| `filterAndColumnsSetup` | Record view summarizes saved filter, sort and columns JSON and shows a paged, sortable, resizable preview unless `hidePreview` is set. A host form offers Edit, + Add Filters and + Add Columns controls, then opens an editor for nested filters, multiple sort levels, visible/ordered columns and a draft preview; `OK` writes JSON into the host form, `Cancel` discards the draft. When payload `allowVariables` is true, a criterion value can become a `FilterVariableExpression`; its `${VARIABLE}` chip is saved in `queryFilterJson`, and preview and the opened query page wait for a value. Saving the host record or advancing a process persists the JSON. A `WIDGET` field adornment can host the setup in a process or row-builder field; its `widgetName` names the widget and only that field receives its output. `overrideIsEditable: false` keeps it read-only. |
| `rowBuilder` | Payload `records`, with columns from `defaultValues.frontendFields` or `fields`. Record view/dashboard is read-only; a form/process host can add, edit, delete and reorder rows when the metadata's edit flags allow it. |
| `scriptViewer` | Script revisions, files, logs, test runner, docs and editor; uses `queryParams.id` or record ID and the built-in script tables. |
| `ESB_OVERVIEW` | ESB destination/trigger overview; fetches its own ESB API data and does not use the ordinary widget payload for content. Requires the ESB backend module. |
For `scriptViewer`, pass a script ID through `queryParams.id` or host it on that script's record. The built-in script and script-type tables supply revisions, schema-ordered files, docs and logs. Register `storeScriptRevision` to expose editing and `testScript` to expose the test panel. The editor supports per-file syntax coloring, split panes, API name/version selection when those fields exist, and a guard for unsaved changes. Local suggestions include QQQ API/logger helpers, language keywords and identifiers in the current file. Type a prefix or press Ctrl+Space, use arrows to choose and Enter/Tab to insert, or click/tap a suggestion. Escape dismisses the list; Escape followed by Tab leaves the editor. Completion supports native undo/redo; the browser controls how consecutive edits are grouped. Suggestions do not validate or execute code.
QQQ composite block types rendered by [`QqqBlocks.tsx`](../../src/components/widgets/blocks/QqqBlocks.tsx) include `TEXT`, `BIG_NUMBER`, `UP_OR_DOWN_NUMBER`, `NUMBER_ICON_BADGE`, `ICON`, `TABLE_SUB_ROW_DETAIL_ROW`, `PROGRESS_BAR`, `DIVIDER`, `IMAGE`, `AUDIO`, `INPUT_FIELD`, and `BUTTON`. Supported layouts include `FLEX_COLUMN`, `FLEX_ROW`, `FLEX_ROW_WRAPPED`, `FLEX_ROW_SPACE_BETWEEN`, `FLEX_ROW_CENTER`, `TABLE_SUB_ROW_DETAILS`, and `BADGES_WRAPPER`. Buttons that submit process actions and standalone input submissions need a host callback. Modal control codes also work locally on dashboard composites. Unknown block types show a contained warning. See [WID-057–WID-059](../acceptance/feature-matrix.md).
`INPUT_FIELD` blocks read `values.fieldMetaData`, optional `value`, `placeholder`, `autoFocus`, and `submitOnEnter`. Ordinary scalar inputs retain the compact block controls; the backend's neutral `%s` display format does not change their presentation. Possible-value sources, explicit formatting/behaviors, and `CODE_EDITOR`, `FILE_UPLOAD`, or `WIDGET` adornments use the shared Next field editors. Hidden fields are omitted and read-only fields cannot be changed. Standalone metadata editors retain a 14rem minimum width so short choice labels do not collapse the control. For `STRING`, `TEXT`, and explicitly configured `PASSWORD` blocks, `fieldMetaData.behaviors` accepts `TO_UPPER_CASE` or `TO_LOWER_CASE`: text changes case while typing and the caret remains at the edit position, including conversions that expand characters such as `ß` to `SS`. Selection boundaries and direction are preserved. For single-line inputs, process Enter and Submit both send the transformed value; standalone Enter requires `submitOnEnter` and a host callback. Multiline `TEXT` fields preserve newlines: Enter edits the text even when `submitOnEnter` is true, and the process Submit button sends the transformed value. Password inputs remain masked by default; showing or hiding the value does not change it. Passwords without a case behavior retain their entered case. The behavior names come from backend metadata, so do not apply a second conversion in an action callback.
Numeric block controls in RC4 blur on mouse-wheel scrolling to preserve the entered value, matching shared numeric form fields. Clicking back into the control allows normal typing and arrow-key edits. Text controls keep focus when scrolling. This correction is included in published RC4 (`3c409f9`); RC3 does not contain it.
In a process, block editors use the screen's form, validation and submission. A BLOB or FILE_UPLOAD field sends the selected `File` as multipart content, and required files block submission until chosen. Named choices on block-only fields search their standalone possible-value source; declared process fields use the process source. WIDGET fields receive surrounding process values (for example `tableName`) merged with current form values; editing writes only the adorned field. Scalar Enter submits only when `submitOnEnter` is true; `->code` submits an action code. Multiline code, choice searches and file controls own their keyboard interactions. Standalone blocks keep their draft locally and send scalar Enter values through their action callback. PRC-062 and WID-074 cover the process and standalone fixture paths. Enter trims text before form validation and submission; required whitespace cannot advance. Process initial values come from the process state, while standalone values come from the block. Configured autofocus takes precedence over heading focus on subsequent process screens, and the first blur does not display a premature required error. PRC-063 verifies this configuration across screen transitions and checks the actual stored text.
Ordinary record edits omit unchanged timestamps. Base Copy and both child-row editor hosts resubmit complete values, so they retain each unchanged timestamp's source instant and fractional seconds. REC-065 verifies copy persistence during the repeated daylight-saving hour; component regressions cover association and in-process child rows.
`DATE_TIME` process fields and input blocks display instants in the browser's local timezone, retain seconds, and send edited times as zoned UTC values. Unchanged values retain their original instant and subsecond precision, including a repeated daylight-saving hour. `DATE` and `TIME` remain calendar and wall-clock values. PRC-064 checks seeded and edited values across plain blocks, metadata editors and regular process forms against backend SQL; WID-075 checks standalone display.
The filter/column editor reads the payload's `tableName` or the form's `tableName`, then loads that table's metadata. It writes to `queryFilterJson` and `columnsJson` by default; `filterFieldName` and `columnFieldName`/`columnsFieldName` override those keys. `hidePreview` controls the read-only summary preview. Starting in RC5, the editing dialog retains its query grid even when that summary is hidden, matching Material behavior; RC4 incorrectly hides both. Payload flags `hideColumns`, `hideSortBy`, `allowVariables`, `isApiVersioned`, `apiName`/`apiPath`/`apiVersion`, `filterDefaultFieldNames`, `filterDefaultFieldNameSourceFieldNames`, `omitExposedJoins`, `editButtonLabel`, and `modalHeader` affect the editor. It waits for a selected table and required default-field values. The preview supports server-side paging, sorting, column resizing, Refresh, column configuration, and column-menu actions (filter, hide, pin, copy page/full-query values, and statistics when permitted). The editor also offers density, Basic quick filters, Advanced conditions, and selection of an existing saved view. Selecting a view imports its filter and columns into the draft; it does not modify that saved view. Column widths and pins survive host Save. Unresolved variables suppress the preview; assign variables in Advanced mode. Interactions in a read-only record preview are temporary and never update the saved report. Starting in RC4, `omitExposedJoins` limits new filter/column choices in both the read-only preview and editor. Existing saved joined columns, criteria and sorts remain usable, and reordering available columns preserves omitted columns in place. Opening the editor and pressing OK preserves those saved fields; Basic/Advanced changes and host Save/reopen retain their values and widths. This is a choice restriction, not a backend permission boundary; existing joined data still uses the normal query permissions. WID-073 checks the configured and unconfigured cases against SQL, including local edits and reload. `OK` updates the host form's values, so the record must still be saved to persist. [WID-030 and WID-070](../acceptance/feature-matrix.md) cover the summary, preview, Cancel, OK, and persisted host save across five browser profiles.
In the report setup dialog, Left/Right Arrow switches between Filters and sort and Columns; Home/End selects the first/last available tab. Tab enters the selected panel's controls. Switching tabs preserves the draft, and metadata that hides Columns leaves only the filter tab. The tab controls expose `filter-editor-tab-<widget>-filters` and `filter-editor-tab-<widget>-columns` CSS hooks.
Selecting a saved view imports it into the report draft. **Reset Changes** reapplies that view; **Reset to New View** clears filters and custom quick fields, restores default columns, and sorts by the primary key descending when present. Both discard an open quick-filter edit. These actions leave the saved view unchanged. OK applies the draft to the host form; saving the form persists it. Cancel discards the dialog draft.
Complete conditions, including nested groups, mark the corresponding preview column header. Use that indicator to open the existing filter. It adds no condition; editing a read-only preview stays local, and editor changes remain a draft until OK and host Save. Very narrow columns retain the shared grid’s compact header treatment.
When a preview table declares `usesVariants`, select its backend variant before querying. The choice uses the same per-table browser storage as Record Query (`qqq.tableVariant.<table>`). Switching variants starts a fresh preview page; query/count, full-column copy and statistics use the selection. It does not alter the saved report definition. Application-API query/count require the backend fix in [QQQ PR #913](https://github.com/QRun-IO/qqq/pull/913), merged to QQQ develop as `cba758507dcc54cf282a6638ab50a7d3a1904c7c`; verification used the middleware JAR built from its reviewed source. This does not require or announce a QQQ 4.1 release.
### Demo and compatibility dispatch labels
The remaining dispatch labels mainly support local mock fixtures. They are implemented UI paths, but are not a promise that the production QQQ backend emits those shapes. Check [`src/mocks/fixtures/widgets`](../../src/mocks/fixtures/widgets) for demo payloads and the real widget fixtures and matrix for QQQ-backed examples.
| `type` | Use and important response fields |
|---|---|
| `recordGrid` | Demo record grid with `tableName`, `records`, and `columns` or full `fields`; optionally `totalCount`. |
| `quickLinks` | Demo list of labeled navigation links; inspect the fixture's `links` payload before using it. |
| `processSummary` | Demo process status/summary cards; inspect the fixture's status and counts before using it. |
| `block` | Chooses QQQ composite blocks when the payload has `blockTypeName` or `blocks`; otherwise falls back to `BlockWidget`. A demo `blocks` list therefore enters the QQQ composite branch. |
| `parent` | Demo `CompositeWidget` alias using `childWidgets`; canonical QQQ parent widgets use `parentWidget` and `childWidgetNameList`. |
Canonical chart types can also fall through to the demo chart components when the response is not a QQQ `chartData` payload.
### Widget configuration recipes
These examples are payload fragments emitted by an application's backend renderer, with illustrative names and values. Pair them with matching `QWidgetMetaData`; they are not standalone Java producer definitions. Every dispatch label is listed above; the details below cover the shared and specialized configuration that is easy to miss when copying a basic example.
**Responsive placement.** Metadata `defaultValues` accepts `gridCols:sizeClass:xs`, `sm`, `md`, `lg`, `xl`, and `xxl`. Their minimum viewport widths are 0, 576, 768, 992, 1200, and 1400 px. Values are column counts from 1 to 12; numeric strings are accepted. Once any override is set, `xs` defaults to 12 and `xxl` defaults to `gridColumns` or 12. Other declared spans apply until the next declared breakpoint. This example gives a half-width widget from 768 px upward, including 1400 px:
```json
{"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:
```json
{"dropdownNameList":["regions"],"dropdownLabelList":["Region"],"dropdownDataList":[[{"id":"east","label":"East"},{"id":"west","label":"West"}]],"dropdownDefaultValueList":["east"]}
```
The request parameter is the payload's `dropdownNameList` entry (`regions` here). The backend convention uses the possible-value source name for choice controls and the declared name for date controls. A valid stored choice wins over the response default; unavailable stored choices are cleared. `labelForNullValue` adds an explicit empty option only when the payload has no default. `disableClearable` removes the clear button. To gate the body, return `dropdownNeedsSelectedText` until the required choice is supplied. Metadata `isRequired` and `defaultValue` alone are not consumed as client-side gating/default rules by [`ConnectedWidget`](../../src/components/widgets/ConnectedWidget.tsx). A date control sends local `YYYY-MM-DD` text; the special custom timeframe sends the UTC range described above.
**Charts and statistics.** All seven chart dispatch labels use the canonical shape below. `backgroundColors` and `urls` are arrays aligned with data points; `color` supplies the series color and `backgroundColor` is the stacked-series fallback. Top-level `chartData.urls` supplies category links. `height` is numeric; `isCurrency` or `isYAxisCurrency` enables the renderer's currency formatting.
```json
{"type":"barChart","height":280,"isCurrency":true,"chartData":{"labels":["Open","Closed"],"datasets":[{"label":"Value","data":[1200,800],"backgroundColors":["info","success"],"urls":["/app/orders/","/app/archive/"]}]}}
```
`chartSubheaderData` accepts `mainNumber`, `vsPreviousPercent`, `vsPreviousNumber`, `isUpVsPrevious`, `isGoodVsPrevious`, `vsDescription`, `mainNumberUrl`, and `previousNumberUrl`; use it for the optional comparison summary. See [`QqqChartPayload`](../../src/components/widgets/widget-types.ts). Statistics adds `countFontSize`, `countURL`, `percentageURL`, and `isCurrency` to the basic example; numeric currency counts format as USD, while string counts pass through. A zero or absent percentage hides the change row. Multi-statistics uses this nested shape:
```json
{"type":"multiStatistics","statisticsGroupData":[{"header":"Orders","subheader":"Today","icon":"inventory_2","iconColor":"#0062ff","statisticList":[{"label":"Open","value":7,"url":"/app/orders/"}]}]}
```
**Tables.** Each `table` payload (and each member of `multiTable.tableDataList`) uses the same configuration. `rowsPerPage` defaults to 10; `hidePaginationDropdown` hides its size selector, `fixedHeight` constrains the scroll area, and `fixedStickyLastRow` fixes the final row. `linkText`/`linkURL` add the table link. Columns accept `type`, `header`, `accessor`, `width`, `align`, and `verticalAlign`; widths can be fixed CSS lengths or `fr` shares.
```json
{"type":"table","rowsPerPage":5,"columns":[{"header":"Name","accessor":"name","width":"2fr"},{"header":"Count","accessor":"count","width":"1fr"},{"accessor":"tooltip","type":"hidden"}],"rows":[{"name":"Open","count":7,"tooltip":"Current backlog","subRows":[{"name":"Priority","count":2}]}]}
```
Cell `type` is `default`, `html`, `htmlAndTooltip`, `composite`, `block`, `image`, or `hidden`. An `htmlAndTooltip` cell reads the row's `tooltip`; `image` reads `imageUrl`, `imageLabel`, `imageTotal`, and `imageTotalType`. Composite/block cells read a block payload at the column accessor. Hidden helper columns do not display but remain available to cells and export. `subRows` provides expandable rows. See [`QqqTablePayload`](../../src/components/widgets/QqqTableWidget.tsx) and [`TableWidgetColumn`](../../src/components/widgets/table-widget-utils.ts).
**Display and editor details.** These keys supplement each catalog row; they are read by the linked implementations.
| Widget | Additional configuration |
|---|---|
| `stepper` | Each step accepts `label`, `linkText`, `linkURL`, `iconOverride`, and `colorOverride`; only the active step shows its action link. |
| `fieldValueList` | Use `fieldLabelPrefixIconNames`, `fieldLabelPrefixIconColors`, and `fieldIndentLevels`, keyed by field name. Fields use the shared record formatter, including declared formats/adornments and possible-value record links. |
| `alert` | `alertType` supports ERROR, WARNING, SUCCESS and INFO; absent/unknown values use INFO. `bulletList` contains sanitized HTML strings. Empty `html` renders no alert body. |
| `usaMap` | `mapMarkerList` entries use `name`, `latitude`, and `longitude`; the UI includes the US basemap and an accessible location list. |
| `html`, `generic`, `divider`, `location`, `quickSightChart` | Use the fields in the catalog and shared chrome; these types do not expose host-form editing. QuickSight's optional payload `label` names the iframe. |
| `childRecordList` | `omitFieldNames` and `onlyIncludeFieldNames` control columns. `canAddChildRecord` enables Add; `disabledFieldsForNewChildRecords` locks named defaults, and `defaultValuesForNewChildRecordsFromParentFields` maps child field names to parent field names. `totalRows` supplies the total. A process host uses `isInProcess`, `allowRecordEdit`, and `allowRecordDelete` to control in-memory rows. |
| `rowBuilder` | Metadata defaults include `frontendFields` (fallback `fields`), `associationName`/`outputFieldName`, `orderByFieldName`, `mayReorderRows`, `isForRecordViewAndEditScreen`, `isEditable`, `useModalEditor`, `inlineHeading`, `modalTitle`, and `defaultValuesForNewRowsFromParentRecord`; see the [agent editor guide](./next-ui-agent-guide.md#widget-editors-and-host-forms). |
| `process` | `processMetaData.name` selects the process; payload `defaultValues` presets its inputs. Reloading the widget reinitializes the embedded run. |
| `cronUI`, `dynamicForm`, `pivotTableSetup`, `filterAndColumnsSetup` | These edit host fields through their documented bindings above; changing the widget draft alone does not save a record. Use the shared form context and inspect each editor's validation before introducing a new field binding. |
| `dataBagViewer`, `scriptViewer`, `ESB_OVERVIEW`, `customComponent` | These require their respective backend module/process or trusted bundle described above; ordinary display payloads cannot replace those dependencies. |
| `recordGrid`, `quickLinks`, `processSummary`, `parent`, demo `block` | Use the [demo fixtures](../../src/mocks/fixtures/widgets) and matching renderer for the demo contract. These labels do not imply an equivalent canonical Java renderer. |
**Composite blocks.** `link` and `tooltip` apply to the block; `linkMap` and `tooltipMap` apply to named slots, such as `NUMBER`. Links carry `href` and optional `target`; tooltips carry `title`, `placement`, or nested `blockData`. Help slots can provide fallback tooltip text. `styles` configures the leaf block, while composites also accept `styleOverrides`, sanitized `overlayHtml`, and `overlayStyleOverrides`.
| `blockTypeName` | Values and styles to supply |
|---|---|
| `TEXT` | `values.text`, optional `startIcon`/`endIcon`; `styles.color`, `format`, `size`, `weight`. |
| `NUMBER_ICON_BADGE` | `values.number`, `iconName`. |
| `UP_OR_DOWN_NUMBER` | `values.number`, `context`, `isUp`, `isGood`; `styles.colorOverride`, `isStacked`. |
| `TABLE_SUB_ROW_DETAIL_ROW` | `values.label`, `value`; `styles.labelColor`, `valueColor`. |
| `PROGRESS_BAR` | `values.percent`, `heading`, `value`; `styles.barColor`. |
| `BIG_NUMBER` | `values.heading`, `number`, `context`; `styles.width`, `numberColor`. |
| `DIVIDER` | No values required. |
| `IMAGE` | `values.path`, `alt`; `styles.width`, `height`, `bordered`. |
| `AUDIO` | `values.path`, `showControls`, `autoPlay`; playback still follows browser policy. |
| `ICON` | `values.name`; `styles.color`, `fontSize`. |
| `BUTTON` | `values.label`, `actionCode` or `controlCode`, optional `startIcon`/`endIcon`; `styles.format` is filled, outlined or text. |
| `INPUT_FIELD` | `values.fieldMetaData`, `value`, `placeholder`, `autoFocus`, `submitOnEnter`; host behavior is detailed above. |
| `COMPOSITE` | Nested `blocks`, one of the documented `layout` values, and optional `blockId`/`modalMode`. |
A host evaluates `conditional` as the truthiness of the named host value and interpolates `${name}` in text; it is not a JavaScript expression. Dashboard composites without a process host do not evaluate those conditions. Modal buttons use `showModal:<blockId>`, `hideModal:<blockId>`, or `toggleModal:<blockId>` control codes. A nonempty `modalMode` marks the target composite; a host value with that block ID set to literal `true` opens it initially. This local dashboard example needs no process submission callback:
```json
{"type":"composite","blocks":[{"blockTypeName":"BUTTON","values":{"label":"Details","controlCode":"showModal:details"}},{"blockTypeName":"COMPOSITE","blockId":"details","modalMode":"dialog","blocks":[{"blockTypeName":"TEXT","values":{"text":"Details appear here."}}]}]}
```
See [`QqqComposite`](../../src/components/widgets/blocks/QqqComposite.tsx), [`QqqBlocks`](../../src/components/widgets/blocks/QqqBlocks.tsx), and [`BlockSlot`](../../src/components/widgets/blocks/BlockSlot.tsx). RC6 icon compatibility preserves existing Lucide mappings and adds a locally served glyph for valid unmapped names from the pinned 2,234-name legacy Material Icons inventory. Examples include `3d_rotation`, `account_balance_wallet`, `battery_6_bar`, and `60fps`. Unknown names retain their fallback; this does not include every Material Symbols name or distinct outlined/rounded font variants. Metadata image paths still take precedence. The font and Apache license ship with the static export/JAR; no Google Fonts request is needed at runtime. See the [font provenance](../../public/fonts/material-icons/README.md). This behavior first ships in RC6. The parity ledger also retains open INPUT_FIELD configuration and visual review work.
## 8. Theme, branding, and customization
`QInstance.branding` supplies app/company name, logo/icon, accent colors, banners and optional `gravatarDefault`. A company footer appears when both `companyName` and a safe HTTP(S) `companyUrl` are present. The active application theme is `QInstance.supplementalInstanceMetaData.materialDashboardTheme`, read by [`material-theme.ts`](../../src/lib/theme/material-theme.ts); do not configure `QInstance.theme` and expect these styles to apply. Its optional fields cover palette, fonts, typography, radii, density, logo/icon/favicon, icon style, branded header, app bar, sidebar, table colors, and `customCss`. Invalid theme values are ignored. For example:
```json
{"supplementalInstanceMetaData":{"materialDashboardTheme":{"primaryColor":"#245ca6","fontFamily":"Inter, sans-serif","borderRadiusCard":"12px","brandedHeaderEnabled":true,"brandedHeaderTagline":"Operations","customCss":"[data-qqq-id='sidenav-root'] { border-right: 1px solid #ccc; }"}}}
```
[`apply-branding.ts`](../../src/lib/theme/apply-branding.ts), [`material-theme.ts`](../../src/lib/theme/material-theme.ts), [`tokens.ts`](../../src/lib/theme/tokens.ts), and [`qqq-theme.css`](../../src/styles/qqq-theme.css) define the CSS mapping, including supported Material `--qqq-*` variables. Scope custom CSS to observed `data-qqq-id` attributes, and inspect actual rendering and contrast before migrating a Material stylesheet. A declared application theme sets the app look and keeps light mode active. Without an application theme, the local display-mode preference applies; a fresh profile starts in light mode regardless of the operating system preference. Density is available through theme metadata and local preferences. Open the sidebar user menu → Preferences → Appearance to choose Light or Dark. Changes apply immediately and persist in `localStorage` as `qqq-dark-mode` for this browser profile and origin (scheme, host and port), not the signed-in account; there is no cross-device sync or operating-system mode. Clearing site data removes the preference. When an application theme is active, the choices are disabled with an explanation and the saved preference is retained. Reset to Defaults resets appearance to light along with table and record defaults.
See the [Material CSS hook guide](../CSS-HOOKS.md) for the ID sanitizer, selector patterns, named hooks, and phone/tablet visibility rules.
Analytics is configured by the backend's `QInstance.environmentValues` ([`src/lib/analytics`](../../src/lib/analytics)). It stays off when no analytics values are published. `ANALYTICS_PROVIDERS` can name Google, PostHog, or registered plugins; provider-specific IDs and enable flags are also required. The default privacy behavior omits record IDs, labels and query strings and does not identify users. Only publish `ANALYTICS_INCLUDE_RECORD_DATA=true` or `ANALYTICS_IDENTIFY_USERS=true` after deciding those disclosures are appropriate for the application.
### Theme property reference
All keys below belong to `supplementalInstanceMetaData.materialDashboardTheme`. Unset properties retain the Next defaults. The executable allow-list and value validation are [`THEME_PROPERTY_KINDS`](../../src/lib/theme/material-theme.ts); [`QThemeMetaData`](../../src/types/metadata.ts) defines their types.
| Area | Keys or key pattern |
|---|---|
| Palette | `primaryColor`, `secondaryColor`, `backgroundColor`, `surfaceColor`, `textPrimary`, `textSecondary`, `errorColor`, `warningColor`, `successColor`, `infoColor`. |
| Action color | `preferInfoColorToPrimaryColor` chooses the info color for the controls that honor that option. |
| Fonts | `fontFamily`, `headerFontFamily`, `monoFontFamily`, `fontSizeBase`, `fontWeightLight`, `fontWeightRegular`, `fontWeightMedium`, `fontWeightBold`. |
| Typography | `typography{variant}{property}`, where variant is `H1`–`H6`, `Body1`, `Body2`, `Button`, or `Caption`, and property is `FontSize`, `FontWeight`, `LineHeight`, `LetterSpacing`, or `TextTransform`. Example: `typographyH2FontSize: "1.5rem"`. |
| Radii | `borderRadiusGlobal`, `borderRadiusScale`, and `borderRadius{component}` for `Button`, `Card`, `Chip`, `Dialog`, `OutlinedInput`, `LinearProgress`, `MenuPaper`, `PaperRounded`, `PopoverPaper`, and `Tooltip`. A component radius overrides the global radius; the scale applies when no global radius is set. |
| Density and icons | `density`: `compact`, `normal`, `comfortable`; `iconStyle`: `filled`, `outlined`, `rounded`, `sharp`, `two-tone`. Unsupported icon names can still fall back to the default glyph. |
| Images and CSS | `logoPath`, `iconPath`, `faviconPath`, `customCss`. |
| Branded header | `brandedHeaderEnabled`, `brandedHeaderBackgroundColor`, `brandedHeaderTextColor`, `brandedHeaderLogoPath`, `brandedHeaderLogoAltText`, `brandedHeaderHeight`, `brandedHeaderTagline`. |
| App bar | `appBarBackgroundColor`, `appBarTextColor`. |
| Sidebar | `sidebarBackgroundColor`, `sidebarTextColor`, `sidebarIconColor`, `sidebarSelectedBackgroundColor`, `sidebarSelectedTextColor`, `sidebarHoverBackgroundColor`, `sidebarDividerColor`. |
| Tables | `tableHeaderBackgroundColor`, `tableHeaderTextColor`, `tableRowHoverColor`, `tableRowSelectedColor`, `tableBorderColor`. |
| Borders | `dividerColor`, `borderColor`, `cardBorderColor`. |
Lengths use CSS length strings; colors use supported named, hex, RGB or HSL values. Font weights must be integers from 1 through 1000; radius scale is 0 through 10. Text transforms are `none`, `uppercase`, `lowercase`, or `capitalize`. Invalid values are ignored rather than inserted into CSS. Application CSS and external assets remain trusted configuration, and their actual appearance/CSP must be checked on the host.
### Help content configuration
Fields and sections expose `helpContents`; widgets expose a slot map in `helpContent`. Entries supply `content`, `format` (`TEXT`, `HTML`, or `MARKDOWN`), and optional `roles`. The shared selector prefers the first entry matching the most specific screen role, then an entry without roles. View roles are `VIEW_SCREEN`, `READ_SCREENS`, `ALL_SCREENS`; query substitutes `QUERY_SCREEN`; edit uses `EDIT_SCREEN`, `WRITE_SCREENS`, `ALL_SCREENS`; create/copy substitutes `INSERT_SCREEN`; processes use `PROCESS_SCREEN`, `ALL_SCREENS`.
For example, widget help can use `{"helpContent":{"label":[{"content":"Current totals","format":"TEXT"}],"columnHeader=count":[{"content":"Matching records","format":"TEXT"}]}}`. Table widget headers use `columnHeader=<accessor>` slots; composite slots use `<blockId>,<slot>` keys. Turn on `?helpHelp` to inspect the exact key at the target control before defining content. See [`help-utils.ts`](../../src/lib/utils/help-utils.ts) and [`widgetSlotHelp`](../../src/components/widgets/widget-utils.ts).
### Analytics configuration example
Publish these values through `QInstance.environmentValues`, not frontend build variables. Google uses `GOOGLE_ANALYTICS_ENABLED="true"` and `GOOGLE_ANALYTICS_TRACKING_ID` (a valid GA4 tracking ID). PostHog uses `POSTHOG_ENABLED="true"`, `POSTHOG_API_KEY` (alias `POSTHOG_PROJECT_API_KEY`), and optional `POSTHOG_HOST`, which defaults to `https://us.i.posthog.com`.
```json
{"environmentValues":{"ANALYTICS_PROVIDERS":"google,posthog","GOOGLE_ANALYTICS_ENABLED":"true","GOOGLE_ANALYTICS_TRACKING_ID":"G-EXAMPLE123","POSTHOG_ENABLED":"true","POSTHOG_PROJECT_API_KEY":"<public-project-key>","ANALYTICS_INCLUDE_RECORD_DATA":"false","ANALYTICS_IDENTIFY_USERS":"false"}}
```
Replace the illustrative IDs with the application's public provider identifiers. `ANALYTICS_PROVIDERS` accepts comma-, semicolon-, or newline-separated names; names are lowercased. If omitted, Google and PostHog are considered, but each still requires its own enable/configuration values. `ANALYTICS_PLUGIN_SCRIPTS` (alias `ANALYTICS_PLUGIN_SCRIPT_URLS`) loads configured provider scripts; those register through `window.QQQAnalytics`. Use the [provider registry](../../src/lib/analytics/registry.ts) and [provider interface](../../src/lib/analytics/types.ts), and configure host CSP for each script/endpoint. Privacy defaults and opt-ins are described above; never place server-side provider secrets in environment values exposed to browsers.
## 9. Accessibility, testing, and known boundaries
Components use labeled fields, keyboard navigation, focus-managed dialogs and drawers, inline errors, contained widget failures, and touch layouts. Below 768 px, queries become record cards and filters/actions use sheets; tablet testing uses a touch WebKit project. Test with keyboard and assistive technology in addition to axe. See [browser matrix](../acceptance/browser-matrix.md) and [accessibility rows](../acceptance/feature-matrix.md).
Run `pnpm typecheck`, `pnpm lint`, `pnpm test`, `pnpm test:e2e`, `pnpm build:export`, and `pnpm perf:budget` for the relevant change. For RC1, prioritize complete Chromium workflows and the full Chromium gate before expanding browser/viewport coverage; verify defaults, validation, save/reopen and persisted results. The final release acceptance gate is `QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet pnpm test:acceptance` with `QQQ_SAMPLE_JAR` pointing at a compatible real sample build. It verifies SQL/backend behavior as well as browser behavior; mocked tests are not a substitute. See [test setup](../../tests/acceptance/README.md), [performance budget](../acceptance/performance.md), and [API error handling](../API-ERROR-HANDLING.md).
Current limits to account for: the Material parity ledger has open Partial/Missing rows beyond the narrower feature matrix; the report filter/column editor supports variables and API-versioned metadata but still needs complete embedded-query equivalence review; a built-in QFMD bridge does not prove every third-party Material bundle works; root-path static hosting is the documented deployment; and Google Picker requires a backend with the #704 metadata/CSP changes (backend PR #908 merged to develop as `bac663044`). Real Auth0 tenant, Google account, and AWS QuickSight service tests have approved external-service exclusions in the feature matrix. Validate any additional feature against the current branch, backend contract, and matrix before describing it as production-compatible.
### Google Drive process configuration
This implementation is part of work after the immutable RC1 release. RC1 itself retains the disabled picker placeholder; the final 1.0 implementation requires the matching backend changes below.
Set the public browser OAuth client ID and browser API key as `GOOGLE_APP_CLIENT_ID`
and `GOOGLE_APP_API_KEY` in the QInstance environment (normally `QQQ_ENV_` environment
variables). Enable the Google Picker and Drive APIs and configure the application's
JavaScript origin in Google Cloud. Restrict the API key to the required APIs and website
origins, including `https://docs.google.com/*` as required by the
[Google Picker setup guide](https://developers.google.com/workspace/drive/picker/guides/web-picker).
Client secrets are not browser configuration and are not published in v1 metadata.
The Next UI middleware must include the #704 changes (backend PR #908, merged to develop as `bac663044`) that publish these two settings
and permit Google's SDK and picker origins in CSP only when both are configured.
Those conditional headers apply to the Javalin-hosted static export. The standalone
Node/container host also derives them when the backend publishes `dashboardCspSources`
in authentication metadata. With an older backend that omits that optional field, supply
`QQQ_DASHBOARD_CSP_SOURCES` at runtime. Merge these sources with any other deployment
additions and start through [`standalone/qqq-server.mjs`](../../standalone/qqq-server.mjs):
```bash
export QQQ_DASHBOARD_CSP_SOURCES='script-src https://accounts.google.com/gsi/client https://apis.google.com; connect-src https://accounts.google.com/gsi/; frame-src https://accounts.google.com/gsi/ https://docs.google.com; style-src https://accounts.google.com/gsi/style'
```
This matches the #704 backend directive additions. The standalone parser adds these to
its base policy; custom reverse proxies/hosts must supply equivalent allowances. See
[`security-headers.mjs`](../../standalone/security-headers.mjs). Keep the metadata client
ID/API key configured too: CSP permissions alone do not enable the picker.
The component preserves Material's
shared-drive folder view and `drive` OAuth scope. It loads the official SDKs before the
button becomes ready, supports retry after loading or authorization errors, rejects files,
and requires selection again if authorization expires before submission. Tokens remain
in screen memory and are submitted to the backend process; they are not persisted in
localStorage or sessionStorage. Without configuration the picker stays disabled while
other inputs remain usable. PRC-065 verifies the browser/backend contract with controlled
Google SDK responses; PRC-039 retains the approved real-account testing exclusion.
### Post-RC4 table appearance correction
RC5 restores the original Next uniform row background; RC4 has alternating row shading. Selection highlighting and opaque pinned cells are retained. Applications can target `data-row-parity="odd"` or `"even"` through their configured CSS if they explicitly want alternating colors.
### API playground theme integration (RC7)
The embedded RapiDoc viewer uses its own shadow DOM. Next applies existing dashboard color tokens to its dark accent/inverse pair and light contact/status colors, preserving the surrounding layout. Preferences theme changes apply without reloading the page. Focused Chromium desktop/mobile checks cover the fixture's visible text, hovered controls and live theme changes; expanded endpoint states, arbitrary custom palettes and narrow-screen inner layout still require review. This correction is not included in RC6.
### TIME block correction (RC7)
Plain process and standalone TIME blocks now use a one-second native input step, matching regular/shared form fields. This permits second-level clock values without a minute-step validation error and makes native stepping advance one second. TIME values remain local clock strings, with no timezone conversion. This correction is published in RC7 at `f65cf6b`; earlier RCs remain unchanged.
### Narrow API controls and LONG validation (RC8)
The API specification Download and View actions fit narrow panels, including 320px phones, and wrap their labels when necessary. Their download/new-tab behavior is preserved. On wide panels, buttons grow to fit the rendered font instead of clipping labels at a fixed width. This correction is not included in RC7.
When the frontend receives LONG range bounds, invalid integer text produces a field error before range comparisons run. Integer-string bounds retain exact precision beyond JavaScript's safe-number range; optional blanks and required-field messages retain their existing behavior. This describes the frontend schema's supported inputs, not a new backend metadata contract. RC8 includes this correction; RC7 does not.
@@ -1,237 +0,0 @@
# Project Overview: QQQ Admin UI Modernization
## Technology Stack
- **Framework:** Next.js 15+ (App Router) with React 19
- **Language:** TypeScript 5.x (strict mode)
- **Styling:** Tailwind CSS 4.x with CSS custom properties for QQQ theme tokens
- **Component Library:** shadcn/ui (Radix primitives + Tailwind styling)
- **Data Grid:** TanStack Table v8 with virtual scrolling (replaces MUI X DataGrid Pro)
- **Forms:** React Hook Form v7 + Zod validation (replaces Formik + Yup)
- **Data Fetching:** TanStack Query v5 (React Query) for server state
- **Charts:** Recharts (replaces Chart.js) — React-native, composable, SSR-compatible
- **Testing:** Vitest (unit/component), Playwright (E2E), axe-core (accessibility)
- **Build:** Turbopack (dev), Webpack (production via Next.js)
- **Linting:** ESLint + Prettier with project-standard config
- **Package Manager:** pnpm
## Project Structure
```
qqq-frontend-next/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # Auth route group (no layout)
│ │ │ ├── login/page.tsx # Login page
│ │ │ └── callback/page.tsx # OAuth callback handler
│ │ ├── (dashboard)/ # Authenticated route group
│ │ │ ├── layout.tsx # Dashboard layout (sidebar + nav + banners)
│ │ │ └── app/ # All app routes
│ │ │ ├── [appName]/
│ │ │ │ └── page.tsx # AppHome (dashboard/widgets)
│ │ │ ├── [tableName]/
│ │ │ │ ├── page.tsx # RecordQuery (list)
│ │ │ │ ├── savedView/[viewId]/page.tsx # Saved view query
│ │ │ │ ├── create/page.tsx # Create record
│ │ │ │ ├── key/page.tsx # View by unique key
│ │ │ │ ├── dev/page.tsx # Table developer view
│ │ │ │ └── [recordId]/
│ │ │ │ ├── page.tsx # RecordView
│ │ │ │ ├── edit/page.tsx # Edit record
│ │ │ │ ├── copy/page.tsx # Copy record
│ │ │ │ ├── dev/page.tsx # Record developer view
│ │ │ │ └── createChild/[childTable]/page.tsx
│ │ │ └── [processName]/
│ │ │ └── page.tsx # ProcessRun (standalone)
│ │ ├── layout.tsx # Root layout (providers, global styles)
│ │ ├── not-found.tsx # 404 page
│ │ └── error.tsx # Global error boundary
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives (Button, Input, Dialog, etc.)
│ │ ├── layout/ # Sidebar, Navbar, Breadcrumbs, BrandedHeader, Footer
│ │ ├── records/ # RecordView, FieldValue, RecordSidebar
│ │ ├── forms/ # DynamicForm, EntityForm, field-type components
│ │ ├── query/ # DataGrid, FilterBuilder, ColumnConfig, Pagination, Export
│ │ ├── process/ # StepWizard, ValidationReview, BulkLoad, ProcessResults
│ │ ├── widgets/ # Widget, DashboardWidgets, all chart/block/grid widgets
│ │ └── feedback/ # Alerts, Toasts, Modals, HelpContent, CommandMenu
│ ├── lib/
│ │ ├── api/ # Typed API client modules
│ │ │ ├── client.ts # Base axios/fetch client with auth interceptor
│ │ │ ├── auth.ts # getAuthenticationMetaData, manageSession, logout
│ │ │ ├── metadata.ts # loadMetaData, loadTableMetaData, loadProcessMetaData
│ │ │ ├── tables.ts # queryRecords, countRecords, getRecord, insertRecord, updateRecord, deleteRecord
│ │ │ ├── processes.ts # processInit, processStep, processStatus, processRecords, processCancel
│ │ │ ├── widgets.ts # fetchWidgetData
│ │ │ ├── possible-values.ts # fetchPossibleValues (table, process, standalone contexts)
│ │ │ └── developer.ts # developer mode, scripts, logs
│ │ ├── auth/ # Auth providers (OAuth2, Auth0, Anonymous)
│ │ │ ├── auth-provider.tsx # AuthContext + provider component
│ │ │ ├── use-auth.ts # useAuth() hook
│ │ │ ├── oauth2.ts # OAuth2/OIDC flow
│ │ │ ├── auth0.ts # Auth0 integration
│ │ │ └── anonymous.ts # Anonymous/mock auth
│ │ ├── hooks/ # Shared React hooks
│ │ │ ├── use-metadata.ts # useMetaData(), useTableMetaData(), useProcessMetaData()
│ │ │ ├── use-query-records.ts # useQueryRecords() with TanStack Query
│ │ │ ├── use-record.ts # useRecord() single record fetch
│ │ │ ├── use-process.ts # useProcess() step machine
│ │ │ ├── use-widget.ts # useWidget() with abort/cancel
│ │ │ ├── use-possible-values.ts # usePossibleValues() with debounce
│ │ │ ├── use-saved-views.ts # useSavedViews() CRUD
│ │ │ └── use-local-storage.ts # useLocalStorage() typed wrapper
│ │ ├── utils/ # Pure utility functions
│ │ │ ├── filter-utils.ts # QQueryFilter builders and validators
│ │ │ ├── value-utils.ts # Display value formatting per field type
│ │ │ ├── table-utils.ts # Table section/field helpers
│ │ │ ├── process-utils.ts # Process navigation helpers
│ │ │ ├── export-utils.ts # CSV/Excel export
│ │ │ └── responsive-utils.ts # Breakpoint helpers
│ │ └── theme/ # Theming system
│ │ ├── theme-provider.tsx # ThemeContext (light/dark, accent colors)
│ │ ├── tokens.ts # CSS custom property definitions (60+ tokens)
│ │ └── tailwind-plugin.ts # Tailwind plugin for QQQ theme tokens
│ ├── types/ # TypeScript type definitions
│ │ ├── index.ts # Re-exports all types
│ │ ├── metadata.ts # QInstance, QTableMetaData, QFieldMetaData, QProcessMetaData, etc.
│ │ ├── records.ts # QRecord, QPossibleValue
│ │ ├── query.ts # QQueryFilter, QFilterCriteria, QCriteriaOperator, etc.
│ │ ├── processes.ts # QJobStarted, QJobRunning, QJobComplete, QJobError
│ │ ├── widgets.ts # Widget data types
│ │ └── enums.ts # QFieldType, QComponentType, AdornmentType, Capability, QAppNodeType
│ └── styles/
│ ├── globals.css # Tailwind directives + CSS custom properties
│ └── qqq-theme.css # QQQ-specific theme tokens
├── public/ # Static assets
├── .storybook/ # Storybook configuration
├── tests/
│ ├── e2e/ # Playwright E2E tests
│ └── setup.ts # Vitest setup
├── next.config.ts # Next.js configuration
├── tailwind.config.ts # Tailwind configuration
├── tsconfig.json # TypeScript config (strict: true)
├── vitest.config.ts # Vitest configuration
├── playwright.config.ts # Playwright configuration
└── package.json
```
## Dependency Graph
```
Package 1 (Scaffold + Auth + Layout Shell)
│
├──→ Package 2 (Record Query)
│ │
│ ├──→ Package 4 (Process Execution) ←── Package 3 (Record View + CRUD)
│ │ │
│ └──→ Package 5 (Dashboard + Widgets) ←──────┘
│
└──→ Package 3 (Record View + CRUD)
│
└──→ Package 6 (Polish + Parity) [depends on ALL above]
```
Packages 2 and 3 can execute in parallel after Package 1 completes.
Package 4 requires form components from Package 3.
Package 5 requires the data grid from Package 2 and form components from Package 3.
Package 6 requires everything.
## Work Package Summary
| Package | Name | Est. Duration | Prerequisites | Key Deliverables |
|---------|------|---------------|---------------|------------------|
| 1 | Scaffold + Auth + Layout Shell | 4 weeks | None | Project setup, types, API client, auth flow, sidebar/nav, theming, placeholder routes |
| 2 | Record Query | 6 weeks | Package 1 | Data grid, filters (basic/advanced), column config, pagination, saved views, export |
| 3 | Record View + CRUD | 6 weeks | Package 1 | Record detail view, entity form (all 12 field types), create/edit/copy/delete |
| 4 | Process Execution | 5 weeks | Packages 1, 3 | Step wizard, async job polling, bulk load, validation review, process results |
| 5 | Dashboard + Widgets | 4 weeks | Packages 1, 2, 3 | App home, widget system, charts, statistics, record grid widget, block types |
| 6 | Polish + Parity | 5 weeks | All above | Responsive audit, a11y, performance, Storybook, E2E tests, dev tools, command palette |
## Shared Conventions
### Metadata-Driven Rendering
- ALL components that render from metadata MUST accept the metadata type as a prop — never fetch it internally
- No hardcoded table names, field names, or process names anywhere in the UI code
- All user-facing text from metadata uses the `label` property, never `name`
- UI structure (navigation, pages, forms, columns) derives entirely from backend metadata
### API Integration
- All API calls go through typed client functions in `src/lib/api/` — never raw `fetch` or `axios`
- TanStack Query manages all server state caching, deduplication, and revalidation
- Query keys follow convention: `['metadata']`, `['tableMetadata', tableName]`, `['records', tableName, filterHash]`
- Mutations use `useMutation` with `onSuccess` invalidation of affected queries
- 401 responses intercepted globally → redirect to login preserving current URL
### Component Patterns
- Server Components used for: root layouts, metadata-fetching route pages
- Client Components used for: interactive UI (forms, grids, filters, modals)
- Mark client components with `"use client"` directive at file top
- Every interactive component must have a `data-qqq-id` attribute for CSS customization hooks
- Naming convention: `data-qqq-id="{component-type}-{identifier}"` (e.g., `data-qqq-id="button-save"`, `data-qqq-id="field-firstName"`)
### State Management
- Server state: TanStack Query (data from API)
- UI state: React context (theme, sidebar open/closed, modal stack, current user)
- Form state: React Hook Form (form values, validation, dirty tracking)
- URL state: Next.js searchParams (filters, pagination, sort — shareable URLs)
- Local persistence: localStorage (density, column widths, saved view selections, widget dropdown choices)
### Error Handling
- Error boundaries at: route level (Next.js error.tsx), widget level (per-widget ErrorBoundary)
- API errors: caught in TanStack Query `onError` → displayed via toast notification
- Form validation: Zod schemas generated from field metadata (required, maxLength, type)
- Loading states: Skeleton components matching final layout shape
- Empty states: Descriptive messages with action buttons ("No records found. Create one?")
### Accessibility (WCAG 2.1 AA)
- All form inputs: `<label>` element with `htmlFor` or wrapping input, `aria-required`, `aria-invalid`
- All buttons: descriptive text or `aria-label`
- Focus management: trap focus in modals, restore on close
- Keyboard navigation: all interactive elements reachable via Tab, actions via Enter/Space
- Color contrast: minimum 4.5:1 for normal text, 3:1 for large text
- Screen reader: `aria-live` regions for dynamic content updates (toasts, loading states)
### CSS Customization
- 60+ CSS custom properties (tokens) for theming — defined in `src/styles/qqq-theme.css`
- `data-qqq-id` attributes on all interactive elements enable `customCss` injection
- Light/dark mode via `prefers-color-scheme` media query + manual toggle stored in localStorage
- Tailwind classes for all layout/spacing; CSS custom properties for brand colors only
### File Naming Conventions
- Components: PascalCase (`RecordQuery.tsx`, `FilterBuilder.tsx`)
- Hooks: camelCase with `use-` prefix (`use-metadata.ts`, `use-query-records.ts`)
- Utilities: kebab-case (`filter-utils.ts`, `value-utils.ts`)
- Types: kebab-case (`metadata.ts`, `records.ts`)
- API modules: kebab-case (`possible-values.ts`, `auth.ts`)
- Tests: `*.test.ts(x)` co-located with source or in `tests/` directory
### Import Organization
```typescript
// 1. React/Next.js
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
// 2. External libraries
import { useQuery } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
// 3. Internal: types
import type { QTableMetaData, QRecord } from '@/types'
// 4. Internal: lib (API, hooks, utils)
import { queryRecords } from '@/lib/api/tables'
import { useMetaData } from '@/lib/hooks/use-metadata'
// 5. Internal: components
import { Button } from '@/components/ui/button'
import { DataGrid } from '@/components/query/DataGrid'
```
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,565 +0,0 @@
# Requirements Coverage Cross-Check Report
**Document Version:** 1.0
**Date:** 2026-02-25
**Status:** COMPLETE COVERAGE VERIFIED
**Reviewed Sections:** Section 3 (API Contract) and Section 5 (New Admin UI Requirements)
---
## Executive Summary
This report validates that **EVERY subsection** of Section 5 (New Admin UI Requirements) and Section 3 (API Contract) from the QQQ Admin UI Modernization Requirements document appears in at least one of the six work packages.
**Result:** ✅ **100% COVERAGE** - All requirements are mapped to work packages with clear ownership and no gaps.
---
## Section 3: Semantic API Contract v1.0 Coverage
### Authentication Endpoints
| Requirement | Section | Details | Covered By | Status |
|---|---|---|---|---|
| Get Authentication Metadata | 3.2.1 | `GET /qqq/v1/metaData/authentication` (unsecured) | Package 1 (Auth Provider) | ✅ |
| Create/Manage Session | 3.2.2 | `POST /qqq/v1/manageSession` with accessToken | Package 1 (Auth Provider) | ✅ |
| Logout | 3.2.3 | `POST /qqq/v1/logout` clears session cookie | Package 1 (Auth Provider) | ✅ |
| OIDC Back-Channel Logout | 3.2.4 | `POST /qqq/v1/oidc/backchannel-logout` | Package 1 (Auth Provider) | ✅ |
**Implementation Status:** Package 1 Step 3-4 provides complete auth interceptor, session management, and logout handling.
---
### Metadata Endpoints
| Requirement | Section | Details | Covered By | Status |
|---|---|---|---|---|
| Application Metadata | 3.3.1 | `GET /qqq/v1/metaData` returns QInstance | Package 1 (API Client) | ✅ |
| Table Metadata | 3.3.2 | `GET /qqq/v1/metaData/table/{tableName}` returns QTableMetaData | Package 1 (API Client), Package 2 | ✅ |
| Process Metadata | 3.3.3 | `GET /qqq/v1/metaData/process/{processName}` returns QProcessMetaData | Package 1 (API Client), Package 4 | ✅ |
**Implementation Status:** Package 1 Step 3 and later steps in Packages 2/4 handle all metadata loading.
---
### Table Data Endpoints
| Requirement | Section | Details | Covered By | Status |
|---|---|---|---|---|
| Query Records | 3.4.1 | `POST /qqq/v1/table/{tableName}/query` with QQueryFilter | Package 2 (Step 2) | ✅ |
| Count Records | 3.4.2 | `POST /qqq/v1/table/{tableName}/count` with optional includeDistinct | Package 2 (Step 2) | ✅ |
| Get Single Record | 3.4.3 | `GET /qqq/v1/table/{tableName}/{primaryKey}` with tableVariant, includeAssociations | Package 3 (Step 1) | ✅ |
| Insert Record | 3.4.4 | `POST /qqq/v1/table/{tableName}` with multipart/form-data | Package 3 (Step 1) | ✅ |
| Update Record | 3.4.5 | `PUT /qqq/v1/table/{tableName}/{primaryKey}` with multipart/form-data | Package 3 (Step 1) | ✅ |
| Delete Record | 3.4.6 | `DELETE /qqq/v1/table/{tableName}/{primaryKey}` | Package 3 (Step 1) | ✅ |
**Implementation Status:** Package 2 handles query/count, Package 3 handles get/create/update/delete with full form integration.
---
### Process Endpoints
| Requirement | Section | Details | Covered By | Status |
|---|---|---|---|---|
| Initialize Process | 3.5.1 | `POST /qqq/v1/processes/{processName}/init` with values, recordsParam, file | Package 4 (Step 1) | ✅ |
| Process Step | 3.5.2 | `POST /qqq/v1/processes/{processName}/{processUUID}/step/{stepName}` | Package 4 (Step 1) | ✅ |
| Process Status (Poll) | 3.5.3 | `GET /qqq/v1/processes/{processName}/{processUUID}/status/{jobUUID}` | Package 4 (Step 1) | ✅ |
| Process Records | 3.5.4 | `GET /qqq/v1/processes/{processName}/{processUUID}/records` with skip/limit | Package 4 (Step 1) | ✅ |
| Cancel Process | 3.5.5 | `GET /qqq/v1/processes/{processName}/{processUUID}/cancel` | Package 4 (Step 1) | ✅ |
**Implementation Status:** Package 4 Step 1 implements complete process API client with all endpoints and job polling.
---
### Additional Endpoints
| Requirement | Section | Details | Covered By | Status |
|---|---|---|---|---|
| Widget Endpoint | 3.6 | `GET /qqq/v1/widget/{widgetName}` with dynamic query params | Package 5 (Step 2) | ✅ |
| Possible Values Endpoints | 3.7 | `POST /qqq/v1/table/{t}/possibleValues/{f}`, `/processes/{p}/possibleValues/{f}`, `/possibleValues/{f}` | Package 2 (Step 5), Package 3 (Step 1), Package 4 | ✅ |
| Error Response Shape | 3.8 | Consistent error format with HTTP status codes | All packages (API client) | ✅ |
| Pagination Contract | 3.9 | offset/limit based with skip and limit | Package 2 (Step 6) | ✅ |
| Filtering Contract | 3.10 | Tree-structured QQueryFilter with boolean operators, subFilters, expressions | Package 2 (Step 4-5) | ✅ |
| Backwards Compatibility Policy | 3.11 | SemVer with PATCH/MINOR/MAJOR deprecation cycles | Package 6 (documentation) | ✅ |
**Implementation Status:** Complete coverage across all packages.
---
## Section 5: New Admin UI Requirements Coverage
### 5.1: Core Principles
| Requirement | Details | Covered By | Status |
|---|---|---|---|---|
| 100% feature parity | Every page, component, interaction in qfmd | All Packages 1-6 | ✅ |
| Responsive design | Fully functional at mobile/tablet/desktop | Packages 1, 2, 3, 4, 5, 6 | ✅ |
| Metadata-driven rendering | No hardcoded tables, fields, pages | All Packages | ✅ |
| WCAG 2.1 AA accessible | Keyboard navigation, ARIA, color contrast | Package 6 (Steps 7-13) | ✅ |
| Themeable | Light/dark mode, custom brand colors, CSS variables | Package 1 (Step 9), Package 6 (Step 26) | ✅ |
---
### 5.2: Page Inventory
#### 5.2.1 Authentication Pages
| Page | Route | Covered By | Status |
|---|---|---|---|
| Login | `/login` | Package 1 (Step 4, Step 11) | ✅ |
| No-Auth Screen | `/no-auth-screen` | Package 1 (Step 11) | ✅ |
**Implementation Status:** Package 1 Step 11 creates login page with error handling.
---
#### 5.2.2 App Home Pages
| Page | Route | Components | Covered By | Status |
|---|---|---|---|---|
| App Dashboard/Home | `/app/{appName}` | Dashboard widgets, record counts, navigation | Package 5 (Step 12) | ✅ |
**Implementation Status:** Package 5 Step 12 implements App Home page with full widget orchestration.
---
#### 5.2.3 Record Pages
| Page | Route | Key Elements | Covered By | Status |
|---|---|---|---|---|
| Record Query/List | `/app/{table}` | DataGrid, filters, pagination, toolbar | Package 2 (Steps 1-11) | ✅ |
| Saved View | `/app/{table}/savedView/:id` | Pre-filtered query with saved config | Package 2 (Step 10) | ✅ |
| Create Record | `/app/{table}/create` | Multi-section form, file upload, validation | Package 3 (Steps 5-6) | ✅ |
| View Single Record | `/app/{table}/:id` | Record header, sections, widgets, audit trail | Package 3 (Step 6) | ✅ |
| Edit Record | `/app/{table}/:id/edit` | Form with pre-populated values | Package 3 (Steps 5-6) | ✅ |
| Copy Record | `/app/{table}/:id/copy` | Form with copied values (no ID) | Package 3 (Steps 5-6) | ✅ |
| View by Key | `/app/{table}/key` | Record lookup by unique key | Package 3 (Step 6) | ✅ |
| Table Developer View | `/app/{table}/dev` | Metadata inspector | Package 6 (Step 17) | ✅ |
| Record Developer View | `/app/{table}/:id/dev` | Script editor, logs, test execution | Package 6 (Step 18) | ✅ |
**Implementation Status:** Package 2 handles query/list (Steps 1-11), Package 3 handles create/view/edit/copy (Steps 5-6), Package 6 handles developer views (Steps 17-18).
---
#### 5.2.4 Process Pages
| Page | Route | Components | Covered By | Status |
|---|---|---|---|---|
| Standalone Process | `/app/{processName}` | Step wizard, forms, validation, results | Package 4 (Steps 2-8) | ✅ |
| Table-Scoped Process | `/app/{table}/{processName}` | Same as standalone with record context | Package 4 (Steps 2-8) | ✅ |
**Implementation Status:** Package 4 Steps 2-8 implement ProcessRun component for both standalone and embedded contexts.
---
#### 5.2.5 Report Pages
| Page | Route | Components | Covered By | Status |
|---|---|---|---|---|
| Report Execution | `/app/{reportName}` | Report config form, output display | Package 6 (Step 19) | ✅ |
**Implementation Status:** Package 6 Step 19 reuses ProcessRun for report execution.
---
### 5.3: Component Inventory
#### 5.3.1 Layout Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Side Navigation | Hierarchical from appTree, collapsible, mini-mode, keyboard nav | Package 1 (Steps 7-8) | ✅ |
| Top Navigation | Breadcrumbs, user menu, sidebar toggle, search | Package 1 (Steps 7-8), Package 6 (Step 14) | ✅ |
| Header Banner | Custom branding, environment banners, dismissible | Package 1 (Step 11) | ✅ |
| Footer | Minimal, optional | Package 1 (Step 7) | ✅ |
**Implementation Status:** Package 1 Steps 7-8 implement Sidebar, Header, Breadcrumbs. Package 6 Step 14 adds Command Palette.
---
#### 5.3.2 Data Display Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Data Grid | Server-side pagination, sorting, filtering, column reordering, row selection, density control, export | Package 2 (Steps 1-11) | ✅ |
| Record Detail View | Section-organized fields, field adornments, audit trail, associated records | Package 3 (Step 6) | ✅ |
| Statistics Cards | Number cards with icons, color coding, click-through | Package 5 (Step 8) | ✅ |
| Charts | Line, bar, pie, stacked, doughnut with interactivity | Package 5 (Step 7) | ✅ |
| Data Bags | JSON/structured data with syntax highlighting | Package 5 (Step 6) | ✅ |
**Implementation Status:** Package 2 handles DataGrid (Steps 1-11), Package 3 handles RecordView (Step 6), Package 5 handles all widgets (Steps 6-8).
---
#### 5.3.3 Form Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Entity Form | Multi-section, Zod validation, all 12 field types, conditional visibility | Package 3 (Steps 2-6) | ✅ |
| Autocomplete/Select | Possible-values dropdown with server search, hierarchical | Package 3 (Step 4) | ✅ |
| Boolean Fields | Toggle switch, tri-state | Package 3 (Step 4) | ✅ |
| Chip Input | Multi-value with tags | Package 3 (Step 4) | ✅ |
| File Upload | Single/multi with preview, drag-drop | Package 3 (Step 4) | ✅ |
| Date/Time Fields | Date picker, time picker, range selection | Package 3 (Step 4) | ✅ |
| Code Editor | Syntax-highlighted editing | Package 3 (Step 4), Package 6 (Step 18) | ✅ |
**Implementation Status:** Package 3 Steps 2-6 implement all form components. Package 6 Step 18 adds developer code editor.
---
#### 5.3.4 Process Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Step Wizard | Linear stepper, forward/back navigation, progress | Package 4 (Steps 2-3) | ✅ |
| Validation Review | Pre-commit warnings/errors, accept/reject/modify | Package 4 (Step 5) | ✅ |
| Bulk Load | CSV mapping, value mapping, profile save/load | Package 4 (Step 6) | ✅ |
| Process Results | Summary, record counts, error lists, downloads | Package 4 (Step 8) | ✅ |
| Progress Display | Progress bar, message, current/total, cancel | Package 4 (Step 3) | ✅ |
| Google Drive | Folder picker wrapper | Package 4 (Step 9) | ✅ |
**Implementation Status:** Package 4 Steps 2-9 implement all process components.
---
#### 5.3.5 Query/Filter Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Filter Builder | Basic + advanced modes, 20 operators, AND/OR logic, sub-filters | Package 2 (Steps 4-5) | ✅ |
| Quick Filter | Text search across columns | Package 2 (Step 4) | ✅ |
| Column Configuration | Show/hide, reorder, persist to localStorage | Package 2 (Step 8) | ✅ |
| Pagination | Page size selector, page navigation, total count | Package 2 (Step 6) | ✅ |
| Saved Views | Save/restore filter + column + sort config | Package 2 (Step 10) | ✅ |
| Export | CSV, PDF, Excel (extensible) | Package 2 (Step 11) | ✅ |
| Table Variants | Select alternate views | Package 2 (Step 7) | ✅ |
**Implementation Status:** Package 2 Steps 4-11 implement all query/filter components.
---
#### 5.3.6 Widget Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Widget Container | Label, dropdown, reload, export, help | Package 5 (Step 4) | ✅ |
| Block Types (11 types) | Text, BigNumber, UpOrDown, IconBadge, Progress, Button, Icon, Image, Audio, Divider, InputField | Package 5 (Step 6) | ✅ |
| Record Grid Widget | Embedded grid, add/edit/delete | Package 5 (Step 9) | ✅ |
| Specialized Widgets (10 types) | Composite, Parent, DynamicForm, PivotTable, FilterColumns, Stepper, Cron, USMap, ScriptViewer, Custom | Package 5 (Step 10) | ✅ |
**Implementation Status:** Package 5 Steps 4-11 implement all widget components.
---
#### 5.3.7 Feedback Components
| Component | Required Behavior | Covered By | Status |
|---|---|---|---|
| Alerts | Inline success/error/warning/info | Package 1 (Step 11), All packages | ✅ |
| Modals/Dialogs | Modal stack, confirmation, focus trap | Package 1 (Step 6), Package 6 (Step 8) | ✅ |
| Tooltips/Help | Contextual help, rich HTML content | Package 3 (Step 3), All packages | ✅ |
| Command Menu | Cmd+K quick navigation | Package 6 (Step 14) | ✅ |
**Implementation Status:** All feedback components covered across packages, with full keyboard/focus support in Package 6.
---
### 5.4: Responsive Design Requirements
#### 5.4.1 Breakpoints
| Breakpoint | Width | Layout | Covered By | Status |
|---|---|---|---|---|
| Mobile | < 768px | Single-column, off-canvas nav, bottom navigation | Packages 1, 2, 3, 4, 5, 6 | ✅ |
| Tablet | 768px–1024px | Adaptive layout, mini-sidebar, two-column | Packages 1, 2, 3, 4, 5, 6 | ✅ |
| Desktop | > 1024px | Full layout, persistent sidebar, multi-panel | Packages 1, 2, 3, 4, 5, 6 | ✅ |
**Implementation Status:** Package 6 Steps 1-6 comprehensively audit and implement responsive design across all pages.
---
#### 5.4.2 Page-Specific Responsive Treatment
| Page | Mobile Treatment | Covered By | Status |
|---|---|---|---|
| Record Query | Card-based list, simplified filter, 2-3 columns | Package 6 (Step 1) | ✅ |
| Record View | Single-column stacked, collapsible sections | Package 6 (Step 2) | ✅ |
| Entity Form | Single-column, sticky buttons | Package 6 (Step 2) | ✅ |
| Process Execution | Full-screen steps, bottom navigation | Package 6 (Step 5) | ✅ |
| App Home/Dashboard | Stacked widgets, horizontal scroll stats | Package 6 (Step 5) | ✅ |
**Implementation Status:** Package 6 Steps 1-6 implement comprehensive responsive treatment for all page types.
---
### 5.5: Authentication and Authorization
#### 5.5.1 Authentication Flows
| Flow | Implementation | Covered By | Status |
|---|---|---|---|
| OAuth2/OIDC | Redirect, token exchange, session creation | Package 1 (Steps 3-4) | ✅ |
| Auth0 | Auth0-specific configuration | Package 1 (Step 3) | ✅ |
| Anonymous/Mock | For development and test instances | Package 1 (Steps 3-4) | ✅ |
| Session Management | sessionUUID cookie-based | Package 1 (Step 3) | ✅ |
| Back-Channel Logout | Server-side session invalidation | Package 1 (Step 4) | ✅ |
| Explicit Logout | POST /logout clears session | Package 1 (Step 4) | ✅ |
**Implementation Status:** Package 1 Steps 3-4 implement complete auth system.
---
#### 5.5.2 Authorization (Role-Based UI)
| Control | Implementation | Covered By | Status |
|---|---|---|---|
| Table-level permissions | readPermission, insertPermission, editPermission, deletePermission control visibility | Packages 2, 3, 6 | ✅ |
| Process-level permissions | hasPermission controls launch buttons | Package 4 | ✅ |
| Capability-based toggling | TABLE_QUERY, TABLE_GET, TABLE_COUNT, etc. hide unavailable operations | Packages 2, 3 | ✅ |
| Widget-level permissions | hasPermission controls visibility | Package 5 | ✅ |
| Help content role filtering | Roles control help visibility | All packages | ✅ |
**Implementation Status:** Authorization checks integrated throughout all packages.
---
#### 5.5.3 Session Timeout
| Feature | Implementation | Covered By | Status |
|---|---|---|---|
| 401 Detection | Global axios interceptor | Package 1 (Step 3) | ✅ |
| Re-auth Modal | Non-disruptive modal prompt | Package 6 (Step 29) | ✅ |
| State Preservation | Unsaved form data, scroll, filter state | Package 6 (Step 29) | ✅ |
| Timeout Warning | Configurable warning (5 min before expiry) | Package 6 (Step 29) | ✅ |
**Implementation Status:** Package 1 Step 3 handles 401 globally, Package 6 Step 29 implements full session timeout UX.
---
### 5.6: Performance Requirements
#### 5.6.1 Load Time Targets
| Metric | Target | Implementation | Covered By | Status |
|---|---|---|---|
| First Contentful Paint | < 1.5s | Server Components, code splitting | Package 1, Package 6 (Step 23) | ✅ |
| Time to Interactive | < 3.0s | Lazy loading, optimizations | Package 6 (Steps 22-25) | ✅ |
| Largest Contentful Paint | < 2.5s | Image optimization | Package 6 (Step 23) | ✅ |
| Cumulative Layout Shift | < 0.1 | Skeleton loaders, reserved space | All packages | ✅ |
| First Input Delay | < 100ms | Efficient event handlers | All packages | ✅ |
**Implementation Status:** Package 6 Steps 22-25 implement comprehensive performance optimization.
---
#### 5.6.2 Caching Strategy
| Endpoint | Strategy | TTL | Covered By | Status |
|---|---|---|---|
| GET /metaData | Memoize in memory | Session | Package 1 (Step 3) | ✅ |
| GET /metaData/table/{t} | Memoize per table | Session | Package 2 (Step 2) | ✅ |
| GET /metaData/process/{p} | Memoize per process | Session | Package 4 (Step 1) | ✅ |
| GET /metaData/authentication | localStorage TTL | 1 hour | Package 1 (Step 3) | ✅ |
| POST /table/{t}/query | No cache | N/A | Package 2 (Step 2) | ✅ |
| POST /table/{t}/count | Short-lived cache | 30 seconds | Package 2 (Step 2) | ✅ |
| GET /widget/{w} | Request dedup | Cancel-replace | Package 5 (Step 2) | ✅ |
| POST /possibleValues/{f} | Debounced | 300ms | Packages 3, 4 | ✅ |
**Implementation Status:** Caching strategy implemented across all packages with TanStack Query.
---
#### 5.6.3 Optimization Strategies
| Strategy | Implementation | Covered By | Status |
|---|---|---|---|
| Route-based code splitting | Next.js automatic | Package 1 (Step 1) | ✅ |
| Lazy loading | Charts, code editor, file upload, rich text | Package 6 (Step 24) | ✅ |
| Optimistic UI | Create/update/delete with immediate feedback | Packages 3, 4 | ✅ |
| Virtual scrolling | DataGrid >100 rows, dropdowns >50 items | Package 6 (Step 24) | ✅ |
| Server-side metadata | Server Components processing | Package 1 (Step 1) | ✅ |
**Implementation Status:** All optimization strategies covered across packages.
---
### 5.7: Developer Experience Requirements
#### 5.7.1 Component Documentation
| Requirement | Implementation | Covered By | Status |
|---|---|---|---|
| Storybook setup | All shared components documented | Package 6 (Step 20) | ✅ |
| Component stories | Loading, empty, error, populated, disabled states | Package 6 (Step 20) | ✅ |
| Interactive controls | Prop exploration in Storybook | Package 6 (Step 20) | ✅ |
| Accessibility audit | axe-core integration in stories | Package 6 (Step 20) | ✅ |
**Implementation Status:** Package 6 Step 20 implements comprehensive Storybook documentation.
---
#### 5.7.2 Testing
| Test Type | Implementation | Covered By | Status |
|---|---|---|---|
| E2E Testing | Playwright for critical flows | Package 6 (Step 21) | ✅ |
| Component Testing | Vitest + Testing Library | All packages (unit tests) | ✅ |
| Visual Regression | Screenshot comparison | Package 6 (Step 21) | ✅ |
| Accessibility Testing | axe-core in CI | Package 6 (Step 21) | ✅ |
| API Contract Testing | Type definitions validation | Package 1 (Step 2) | ✅ |
**Implementation Status:** Comprehensive testing covered across packages and Package 6.
---
#### 5.7.3 Build and CI/CD
| Requirement | Implementation | Covered By | Status |
|---|---|---|---|
| Gradle/Maven integration | Part of QQQ build pipeline | Package 1 (Step 1) | ✅ |
| HMR | Vite-based or Turbopack | Package 1 (Step 1) | ✅ |
| Production build | Minification, tree-shaking, source maps | Package 1 (Step 1) | ✅ |
| Bundle size monitoring | CI threshold check | Package 6 (Step 22) | ✅ |
| Lint + Format enforcement | ESLint + Prettier in CI | Package 1 (Step 1) | ✅ |
**Implementation Status:** Package 1 Step 1 establishes build pipeline, Package 6 Step 22 adds monitoring.
---
#### 5.7.4 Type Safety
| Requirement | Implementation | Covered By | Status |
|---|---|---|---|
| Generated API client | From OpenAPI spec or shared types | Package 1 (Step 2) | ✅ |
| Strict TypeScript | strict: true in tsconfig | Package 1 (Step 1) | ✅ |
| Runtime validation | Zod for API responses | Package 3 (Step 2) | ✅ |
**Implementation Status:** Type safety implemented from Package 1 foundations across all packages.
---
### 5.8: Migration Strategy
#### 5.8.1 Phased Approach
| Phase | Duration | Coverage | Covered By | Status |
|---|---|---|---|
| Phase 0 — Foundation | Weeks 1–4 | Project setup, auth, layout, routing | Package 1 | ✅ |
| Phase 1 — Record Query | Weeks 5–10 | DataGrid with filtering, sorting, pagination | Package 2 | ✅ |
| Phase 2 — Record CRUD | Weeks 11–16 | View, create, edit, delete forms | Package 3 | ✅ |
| Phase 3 — Processes | Weeks 17–22 | Step wizard, bulk load, async execution | Package 4 | ✅ |
| Phase 4 — Dashboard | Weeks 23–26 | Widgets, charts, statistics | Package 5 | ✅ |
| Phase 5 — Polish | Weeks 27–32 | Responsive, a11y, testing, E2E | Package 6 | ✅ |
**Implementation Status:** All phases mapped to work packages 1-6.
---
#### 5.8.2 Coexistence Strategy
| Strategy | Implementation | Covered By | Status |
|---|---|---|---|
| Dual UI support | New UI at `/v2/`, legacy at `/` | Package 1 (Step 1) | ✅ |
| Shared auth | Both UIs use same sessionUUID | Package 1 (Step 3) | ✅ |
| Feature flag | User preference for default UI | Package 1 (Step 6) | ✅ |
**Implementation Status:** Coexistence architecture built into Package 1.
---
#### 5.8.3 First Milestone Deliverable
| Deliverable | Scope | Covered By | Status |
|---|---|---|---|
| Record Query page | Single table fully functional | Package 2 (Complete) | ✅ |
| Validates | Metadata-driven rendering, API integration, grid performance, filters, auth, responsive, keyboard nav | Package 2 + Package 1 | ✅ |
**Implementation Status:** Package 2 complete record query implementation is production-ready first milestone.
---
## Gaps Found
**Result: ZERO GAPS**
All subsections of Section 5 and Section 3 are covered by at least one work package. No requirements are left unmapped.
---
## Overlap Analysis
### Intentional Overlaps (No Conflicts)
| Requirement | Package A | Package B | Nature | Resolution |
|---|---|---|---|---|
| API Client setup | Package 1 | All packages | Foundational | Package 1 provides base, others extend with specialized functions |
| TanStack Query | Package 1 | Packages 2-5 | Foundational | Package 1 sets up QueryClient, others define query keys and hooks |
| Form components | Package 3 | Package 4 | Reuse | Package 4 reuses EntityForm, DynamicFormField from Package 3 |
| Metadata loading | Package 1 | Packages 2-5 | Foundational | Package 1 provides loadMetaData, others load specific table/process metadata |
| Authorization checks | Package 1 | Packages 2-6 | Distributed | Package 1 establishes context, others gate features based on permissions |
| Responsive design | Packages 1-6 | Package 6 | Progressive | Packages 1-5 implement responsive layouts, Package 6 audits and polishes |
| Testing | All packages | Package 6 | Progressive | Packages 1-5 include unit/component tests, Package 6 adds E2E/Storybook |
| Accessibility | All packages | Package 6 | Progressive | Packages 1-5 build accessible components, Package 6 audits and adds advanced features |
**Conclusion:** All overlaps are intentional, complementary, and properly sequenced. No ownership conflicts.
---
## Coverage Summary Table
### By Work Package
| Package | Section 5 Coverage | Section 3 Coverage | Total Subsections |
|---|---|---|---|
| Package 1: Scaffold & Auth | 5.1, 5.2.1, 5.3.1, 5.3.7, 5.4, 5.5.1, 5.5.2, 5.6.1-3, 5.7.3-4, 5.8 | 3.2 (all), 3.3.1, 3.8, 3.11 | 17 |
| Package 2: Record Query | 5.2.3 (query), 5.3.2 (grid), 5.3.5 | 3.4.1, 3.4.2, 3.7, 3.9, 3.10 | 8 |
| Package 3: Record CRUD | 5.2.3 (create/edit/view), 5.3.2 (detail), 5.3.3 | 3.4.3, 3.4.4, 3.4.5, 3.4.6, 3.7 | 8 |
| Package 4: Processes | 5.2.4, 5.3.4, 5.3.6 (process widgets) | 3.5 (all) | 8 |
| Package 5: Dashboard | 5.2.2, 5.3.2 (stats/charts), 5.3.6 (all widgets) | 3.6 | 5 |
| Package 6: Polish | 5.3.7 (advanced), 5.4.2, 5.5.3, 5.6.1-3, 5.7.1-2, 5.8.3 | N/A | 7 |
**Total Unique Subsections:** 53/53 = **100% Coverage**
---
## Verification Methodology
This cross-check was performed by:
1. **Extracting all section headers** from Section 3 (API Contract) and Section 5 (New Admin UI)
2. **Reading each work package** and identifying implemented subsections
3. **Mapping each subsection** to responsible package(s) and specific implementation step(s)
4. **Verifying coverage** — every subsection appears in at least one package
5. **Checking for overlaps** — ensuring no conflicts, all overlaps are intentional
6. **Validating completeness** — no gaps, no floating requirements
---
## Recommendations
1. **For Implementation Teams:**
- Use this matrix as a checklist during development
- Cross-reference work package steps with requirement subsections
- Update this report when new requirements are added
2. **For Project Management:**
- Track progress against this coverage map
- Use as acceptance criteria for package completion
- Monitor for any new requirements that might appear during implementation
3. **For Architecture Reviews:**
- Reference this document when evaluating design decisions
- Ensure API client changes don't break type contracts defined in Section 3
- Validate responsive design completeness per Section 5.4
---
## Sign-Off
**Report Created By:** Cross-Check Analysis
**Date:** 2026-02-25
**Status:** COMPLETE AND VERIFIED
**Conclusion:** ✅ All requirements from Section 3 (Semantic API Contract v1.0) and Section 5 (New Admin UI Requirements) are fully covered by the six work packages with clear ownership, no gaps, and intentional complementary overlaps.
The implementation plan is ready for execution.
@@ -1,54 +0,0 @@
# Review Checklist: Package 1 — Scaffold, Auth, and Layout Shell
### Requirements Coverage
- [ ] Section 3.2: Auth endpoints (getAuthenticationMetaData, manageSession, logout) implemented
- [ ] Section 5.1: Metadata-driven rendering foundation established
- [ ] Section 5.5: Authentication flows (AUTH_0, OAUTH2, FULLY_ANONYMOUS) working
- [ ] Section 5.3.1: Layout components (sidebar, navbar, breadcrumbs, banner) rendering
- [ ] Section 5.5.2: Permission fields present on metadata types (readPermission, insertPermission, etc.)
### Integration Check
- [ ] `pnpm dev` starts without errors
- [ ] `pnpm build` completes successfully
- [ ] `pnpm tsc --noEmit` passes with no type errors
- [ ] All type definitions from shared-context/type-definitions.md present in /src/types/
- [ ] API client base URL configurable via NEXT_PUBLIC_API_BASE_URL
- [ ] TanStack Query provider wraps the app
- [ ] Auth provider wraps the app
- [ ] Theme provider wraps the app
### API Contract Compliance
- [ ] GET /qqq/v1/metaData/authentication — correct path, no auth required
- [ ] POST /qqq/v1/manageSession — multipart/form-data, accessToken field
- [ ] POST /qqq/v1/logout — session cookie sent
- [ ] GET /qqq/v1/metaData — correct path, returns QInstance shape
- [ ] GET /qqq/v1/metaData/table/{tableName} — returns QTableMetaData
- [ ] GET /qqq/v1/metaData/process/{processName} — returns QProcessMetaData
- [ ] 401 responses trigger unauthorized callback
### Metadata-Driven Check
- [ ] Sidebar navigation built from appTree metadata (no hardcoded nav items)
- [ ] Routes generated dynamically from app tree traversal
- [ ] Breadcrumbs use pathToLabelMap from metadata
- [ ] Branding (logo, appName, accentColor) from QBrandingMetaData
- [ ] Banners from branding.banners metadata
### Responsive Check
- [ ] Sidebar collapses to mini-mode on tablet
- [ ] Sidebar becomes off-canvas drawer on mobile (to be finalized in Package 6)
- [ ] Header adapts to screen width
- [ ] Layout doesn't overflow horizontally at any breakpoint
### Accessibility Check
- [ ] Sidebar has role="navigation"
- [ ] All buttons have aria-label or text content
- [ ] Breadcrumbs have aria-current="page" on last item
- [ ] Banner has role="alert"
- [ ] Focus visible on all interactive elements
- [ ] Tab order is logical through sidebar → header → content
### Type Safety Check
- [ ] No `any` types in component props (except values: Record<string, any> from API)
- [ ] All API functions have typed return values
- [ ] QueryClient configured with proper generic types
- [ ] Auth context type is fully defined (no partial types)
@@ -1,66 +0,0 @@
# Review Checklist: Package 2 — Record Query
### Requirements Coverage
- [ ] Section 5.2.3: Record query page at /app/{tableName} renders
- [ ] Section 5.3.2: Data grid with server-side pagination, sorting, filtering
- [ ] Section 5.3.5: Basic filter (quick search) and advanced filter builder
- [ ] Section 5.3.5: All 20 QCriteriaOperator values supported in filter builder
- [ ] Section 5.3.5: AND/OR boolean logic and sub-filters
- [ ] Section 5.3.5: Column configuration (show/hide, reorder)
- [ ] Section 5.3.5: Saved views (save/load/delete)
- [ ] Section 5.3.5: Export to CSV
- [ ] Section 5.3.5: Pagination with page size selector (10/25/50/100)
- [ ] Section 5.3.5: Table variant selection
- [ ] Section 5.4.2: Mobile card view alternative
- [ ] Section 3.4.1: POST /table/{t}/query with correct filter shape
- [ ] Section 3.4.2: POST /table/{t}/count for total count
- [ ] Section 3.7: Possible values for filter field dropdowns
- [ ] Section 3.9: Offset/limit pagination (skip = (page-1) * pageSize)
- [ ] Section 3.10: Tree-structured filters with subFilters
### Integration Check
- [ ] Imports from Package 1 resolve: types, API client, queryKeys, QContext, layout
- [ ] Page renders within dashboard layout with sidebar and breadcrumbs
- [ ] Row click navigates to /app/{tableName}/{recordId} (Package 3 route)
- [ ] Create button navigates to /app/{tableName}/create (Package 3 route)
- [ ] Process launcher invokes process routes (Package 4)
- [ ] Toolbar integrates with QContext (setPageHeader, setTableMetaData)
### API Contract Compliance
- [ ] POST /qqq/v1/table/{tableName}/query — JSON body with filter object
- [ ] POST /qqq/v1/table/{tableName}/count — same filter structure
- [ ] POST /qqq/v1/table/{tableName}/possibleValues/{fieldName} — correct form data
- [ ] Response shapes match: { records: QRecord[] } and { count, distinctCount? }
- [ ] Query deduplication prevents stale results from overwriting fresh
- [ ] Joins passed when exposedJoins present on table metadata
### Metadata-Driven Check
- [ ] Columns generated from QTableMetaData.fields (no hardcoded columns)
- [ ] Cell renderers chosen by QFieldType and adornment type
- [ ] Filter operators filtered per field type (e.g., no CONTAINS on INTEGER)
- [ ] Create button shown only if insertPermission is true
- [ ] Table label used in page header (not table name)
- [ ] Hidden fields (isHidden) excluded from default column set
### Responsive Check
- [ ] Desktop: full data grid with toolbar, filters, pagination
- [ ] Tablet: horizontal scroll data grid, sticky first column
- [ ] Mobile: card-based list view as alternative to grid
- [ ] Filter panel adapts to screen size (bottom sheet on mobile)
- [ ] Pagination controls usable at all breakpoints
### Accessibility Check
- [ ] Data grid has role="grid" or role="table"
- [ ] Column headers have scope="col"
- [ ] Sort buttons have aria-sort attribute
- [ ] Filter inputs have labels
- [ ] Row selection checkboxes have aria-label
- [ ] Keyboard navigation within grid (arrow keys, Tab)
- [ ] Page size selector is a labeled select
### Type Safety Check
- [ ] QQueryFilter type used for all filter construction
- [ ] QRecord[] typed for query results
- [ ] Column definitions typed as ColumnDef<QRecord>[]
- [ ] No `any` in filter builder state
- [ ] Export function parameter typed
@@ -1,73 +0,0 @@
# Review Checklist: Package 3 — Record View and CRUD
### Requirements Coverage
- [ ] Section 5.2.3: Record view at /app/{tableName}/{recordId}
- [ ] Section 5.2.3: Create at /app/{tableName}/create
- [ ] Section 5.2.3: Edit at /app/{tableName}/{recordId}/edit
- [ ] Section 5.2.3: Copy at /app/{tableName}/{recordId}/copy
- [ ] Section 5.3.2: Section-organized field display (T1 and non-T1)
- [ ] Section 5.3.2: Field value rendering for all 12 QFieldTypes
- [ ] Section 5.3.2: All adornment types rendered (LINK, CHIP, SIZE, FILE_DOWNLOAD, etc.)
- [ ] Section 5.3.3: Dynamic form generation from QFieldMetaData
- [ ] Section 5.3.3: Possible values autocomplete with debounced search
- [ ] Section 5.3.3: File upload fields
- [ ] Section 5.3.3: Boolean switch (null/false/true)
- [ ] Section 5.3.3: Date/time pickers
- [ ] Section 5.3.3: Rich text editor for HTML fields
- [ ] Section 5.4.2: Responsive view (single-column mobile, multi-column desktop)
- [ ] Section 5.4.2: Responsive form (single-column mobile, gridColumns tablet+)
- [ ] Section 3.4.3: GET /table/{t}/{id} for single record
- [ ] Section 3.4.4: POST /table/{t} for insert
- [ ] Section 3.4.5: PUT /table/{t}/{id} for update
- [ ] Section 3.4.6: DELETE /table/{t}/{id} for delete
- [ ] Section 3.7: Possible values endpoint integration
### Integration Check
- [ ] Imports from Package 1: types, API client, queryKeys, QContext
- [ ] Record View navigates back to Record Query (Package 2)
- [ ] Action menu can launch processes (Package 4 route)
- [ ] Widget sections delegate to Package 5 components (or stubs)
- [ ] Delete navigates back to query page
- [ ] Unsaved changes prompt before navigation
### API Contract Compliance
- [ ] GET /qqq/v1/table/{tableName}/{primaryKey} — includes associations param
- [ ] POST /qqq/v1/table/{tableName} — multipart/form-data with field values
- [ ] PUT /qqq/v1/table/{tableName}/{primaryKey} — multipart/form-data
- [ ] DELETE /qqq/v1/table/{tableName}/{primaryKey} — returns deletedCount
- [ ] POST .../possibleValues/{fieldName} — searchTerm, debounced 300ms
- [ ] Error responses (403, 404, 500) handled gracefully
### Metadata-Driven Check
- [ ] Form fields generated from QTableMetaData.sections → fieldNames → QFieldMetaData
- [ ] Section layout follows sections metadata (T1 at top)
- [ ] gridColumns from section metadata controls form layout
- [ ] Field labels from metadata (never field names)
- [ ] isEditable respected (read-only fields in edit mode)
- [ ] isRequired reflected in Zod schema and visual indicator
- [ ] isHidden fields excluded from view and form
- [ ] isHeavy fields lazy-loaded or excluded from initial fetch
- [ ] Possible values sourced from possibleValueSourceName metadata
### Responsive Check
- [ ] Record View: single-column stacked sections on mobile
- [ ] Record View: collapsible sections on mobile
- [ ] Entity Form: single-column on mobile, gridColumns on desktop
- [ ] Action menu: bottom sheet on mobile, dropdown on desktop
- [ ] Save/Cancel buttons: sticky at bottom on mobile
### Accessibility Check
- [ ] All form inputs have associated <label> elements
- [ ] Required fields marked with aria-required="true"
- [ ] Invalid fields marked with aria-invalid="true" and aria-describedby
- [ ] Delete confirmation dialog traps focus
- [ ] Keyboard shortcuts documented and displayed (n, e, c, d, a)
- [ ] Help tooltips accessible via keyboard (not just hover)
- [ ] File upload has accessible label and drag-drop instructions
### Type Safety Check
- [ ] Form state typed with Zod schema inference (z.infer<typeof schema>)
- [ ] QRecord type used for all record data
- [ ] Field rendering switches on QFieldType enum (exhaustive)
- [ ] Adornment rendering switches on AdornmentType enum
- [ ] No `any` in form submission handlers
@@ -1,63 +0,0 @@
# Review Checklist: Package 4 — Process Execution
### Requirements Coverage
- [ ] Section 5.2.4: Standalone process at /app/{processName}
- [ ] Section 5.2.4: Table-scoped process with recordIds
- [ ] Section 5.2.4: Table-scoped process with filterJSON
- [ ] Section 5.3.4: Step wizard with stepper UI
- [ ] Section 5.3.4: All 14 component types rendered
- [ ] Section 5.3.4: Validation review screen
- [ ] Section 5.3.4: Bulk load workflows (file mapping, value mapping, profiles)
- [ ] Section 5.3.4: Process results summary
- [ ] Section 5.3.4: Async job progress display
- [ ] Section 3.5.1: POST /processes/{p}/init
- [ ] Section 3.5.2: POST /processes/{p}/{uuid}/step/{stepName}
- [ ] Section 3.5.3: GET /processes/{p}/{uuid}/status/{jobUUID}
- [ ] Section 3.5.4: GET /processes/{p}/{uuid}/records
- [ ] Section 3.5.5: GET /processes/{p}/{uuid}/cancel
### Integration Check
- [ ] Form components reused from Package 3 (EntityForm, DynamicFormField)
- [ ] Possible values autocomplete from Package 3
- [ ] Process can be launched from Record Query toolbar (Package 2)
- [ ] Process can be launched from Record View action menu (Package 3)
- [ ] Process results link back to affected records (Package 3 routes)
- [ ] Widget component type delegates to Package 5
### API Contract Compliance
- [ ] POST /processes/{p}/init — multipart/form-data with values, recordsParam, file
- [ ] POST /processes/{p}/{uuid}/step/{s} — multipart/form-data with values, file
- [ ] GET /processes/{p}/{uuid}/status/{j} — returns QJobRunning | QJobComplete | QJobError
- [ ] GET /processes/{p}/{uuid}/records — query params skip, limit
- [ ] GET /processes/{p}/{uuid}/cancel — returns boolean
- [ ] Exponential backoff polling: 1.5s initial, 1.5x multiplier, 12s max
### Metadata-Driven Check
- [ ] Steps rendered from QFrontendStepMetaData.components
- [ ] Form fields from step.formFields metadata
- [ ] View fields from step.viewFields metadata
- [ ] Process label from metadata (not process name)
- [ ] ProcessMetaDataAdjustment handled (step list and field updates)
- [ ] minInputRecords/maxInputRecords enforced
### Responsive Check
- [ ] Full-screen steps on mobile
- [ ] Stepper horizontal scrollable on mobile
- [ ] Bottom-anchored navigation buttons on mobile
- [ ] File upload drop zone works on touch devices
- [ ] Progress display readable at all breakpoints
### Accessibility Check
- [ ] Stepper has aria-label and step indicators
- [ ] Current step has aria-current="step"
- [ ] Progress bar has role="progressbar" with aria-valuenow/min/max
- [ ] Cancel button has confirmation dialog
- [ ] Form steps use accessible form patterns from Package 3
- [ ] Error messages linked to relevant fields via aria-describedby
### Type Safety Check
- [ ] Process response union type properly discriminated (QJobStarted | QJobComplete | QJobError)
- [ ] ProcessMetaDataAdjustment typed
- [ ] Step component type discriminated on QComponentType enum
- [ ] File upload typed with File/FormData
- [ ] No `any` in process state management
Loaded 100 of 1498 files, more files were not shown because too many files have changed in this diff. Show more