From 15f0e1f522891d645b984daee6dfbc74a3fc7cd7 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 20 Jun 2026 00:48:40 +0200 Subject: [PATCH] Add QA result filtering controls --- CHANGELOG.md | 9 ++ .../test_sprint73_quality_result_filtering.py | 35 ++++++ docs/CODEX_EXECUTION_LOG.md | 30 +++++ docs/TODO.md | 1 + frontend/README.md | 2 + .../quality/QualityResultsPanel.tsx | 111 +++++++++++++++++- frontend/src/styles/app.css | 16 +++ 7 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_sprint73_quality_result_filtering.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6439b950..c61a0bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ # Changelog +## Sprint 73 QA/QC result filtering (2026-06-20) + +- Added client-side QA/QC result search, status and check-type filters. +- Added latest-eight density control with a show-all toggle for long-lived demo projects. +- Added a no-match empty state and reset action for filtered QA/QC result views. +- Kept metric evidence cards and raw persisted metrics unchanged. +- Added static regression coverage for QA/QC filtering and dense history styles. +- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed. + ## Sprint 72 Mobile overflow hardening (2026-06-20) - Clamped page-level horizontal overflow for the workbench shell on mobile. diff --git a/backend/tests/test_sprint73_quality_result_filtering.py b/backend/tests/test_sprint73_quality_result_filtering.py new file mode 100644 index 00000000..ce435fb6 --- /dev/null +++ b/backend/tests/test_sprint73_quality_result_filtering.py @@ -0,0 +1,35 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_quality_panel_adds_result_filters_and_density_limit() -> None: + quality_panel = ( + ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx" + ).read_text(encoding="utf-8") + + assert "useState" in quality_panel + assert "qualityStatusFilter" in quality_panel + assert "qualityTypeFilter" in quality_panel + assert "qualitySearchQuery" in quality_panel + assert "qualityMatchesSearch" in quality_panel + assert "filteredQualityChecks = useMemo" in quality_panel + assert "visibleQualityChecks = showAllQualityChecks ? filteredQualityChecks : filteredQualityChecks.slice(0, 8)" in quality_panel + assert "quality-history-controls" in quality_panel + assert "Search QA/QC results" in quality_panel + assert "All statuses" in quality_panel + assert "All check types" in quality_panel + assert "Reset view" in quality_panel + assert "Show all QA/QC results" in quality_panel + assert "No QA/QC results match the current filters." in quality_panel + + +def test_quality_filter_styles_reuse_dense_history_patterns() -> None: + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert ".quality-history-controls" in css + assert "repeat(auto-fit, minmax(9rem, 1fr))" in css + assert ".quality-history-controls label" in css + assert ".quality-history-controls button" in css + assert ".quality-list-count" in css diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 3b48604f..5f56a59e 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2729,3 +2729,33 @@ Limitations: Next recommended pass: - Continue with QA/QC result filtering/density for long-lived demo projects, or a broader mobile visual pass across Data and Map once the overflow baseline is stable. + +## Sprint 73 QA/QC result filtering (2026-06-20) + +Changed: +- Added client-side QA/QC result search across check id, type, status, candidate/reference dataset ids, analysis/job ids and resolved dataset names. +- Added status and check-type filters derived from the loaded quality-check list. +- Added latest-eight result density control with a show-all toggle. +- Added a no-match empty state and reset action for filtered result views. +- Reused the dense history control styling pattern while allowing the QA/QC controls to wrap inside narrower workspace columns. +- Added `backend/tests/test_sprint73_quality_result_filtering.py`. +- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- Red step: `python -m pytest backend/tests/test_sprint73_quality_result_filtering.py -q` failed on missing filter state, filtered list logic and styles. +- `python -m pytest backend/tests/test_sprint73_quality_result_filtering.py backend/tests/test_sprint72_mobile_overflow_hardening.py backend/tests/test_sprint71_quality_metric_polish.py backend/tests/test_sprint70_quality_handoff_polish.py backend/tests/test_sprint51_quality_export_polish.py -q` (`13 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- Local browser check against `http://127.0.0.1:5177` with live API proxy: QA/QC filters rendered against 2 persisted demo checks; search/status conflict showed the no-match state; reset restored 2 checks and 12 metric cards; no console warnings/errors and no body-level overflow. +- Local mobile browser check at a 390px viewport: filter controls stayed within viewport width, with 2 checks and 12 metric cards visible. +- Screenshots captured under `artifacts/sprint73-quality-result-filtering/`. +- `bash scripts/run_readiness_check.sh` (`257 passed`) + +Open: +- Redeploy Tower and verify live QA/QC filtering after deployment. + +Limitations: +- Frontend filtering/density pass only; filtering is client-side over already-loaded persisted checks and does not change API pagination, persistence, QA calculations, migrations, provider behavior or AI/model behavior. + +Next recommended pass: +- Continue with Data/Map mobile visual polish, especially dataset upload/action forms and map toolbar density on narrow screens. diff --git a/docs/TODO.md b/docs/TODO.md index c214ee7e..610bfc6b 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -344,3 +344,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add QA/QC handoff polish for candidate/reference context and persisted results. - [x] Add QA/QC metric card polish for precision, recall, F1, IoU and error counts. - [x] Add mobile overflow hardening for workbench navigation, inspector and long QA identifiers. +- [x] Add QA/QC result filtering and density controls for long-running demo projects. diff --git a/frontend/README.md b/frontend/README.md index 60b5b4ef..c3eab265 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -287,6 +287,8 @@ The QA/QC workspace promotes core metrics into evidence cards before the raw met The workbench shell clamps page-level horizontal overflow on mobile while keeping the sidebar and workspace shortcut rows intentionally scrollable. Long QA identifiers and inspector values wrap inside their cards instead of widening the viewport. +The QA/QC result list includes client-side search, status and check-type filters plus a latest-results cap so long-lived demo projects remain scan-friendly without changing the API response shape. + ## Useful repository scripts - `bash scripts/frontend_install.sh` diff --git a/frontend/src/components/quality/QualityResultsPanel.tsx b/frontend/src/components/quality/QualityResultsPanel.tsx index b51214f3..8f5b55df 100644 --- a/frontend/src/components/quality/QualityResultsPanel.tsx +++ b/frontend/src/components/quality/QualityResultsPanel.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import type { DatasetCreateResponse, MetricRead, QualityCheckRead } from '../../types' const CORE_METRIC_ORDER = [ @@ -42,6 +42,30 @@ function qualityMetricValue(metric: MetricRead | undefined): string { return Number.isInteger(value) ? String(value) : value.toFixed(3) } +function qualityMatchesSearch(check: QualityCheckRead, query: string, datasetNameById: Map): boolean { + const normalizedQuery = query.trim().toLowerCase() + if (!normalizedQuery) { + return true + } + const candidateName = check.candidate_dataset_id ? datasetNameById.get(check.candidate_dataset_id) : null + const referenceName = datasetNameById.get(check.reference_dataset_id) + const searchable = [ + check.id, + check.check_type, + check.status, + check.candidate_dataset_id, + check.reference_dataset_id, + check.analysis_run_id, + check.job_id, + candidateName, + referenceName, + ] + .filter(Boolean) + .join(' ') + .toLowerCase() + return searchable.includes(normalizedQuery) +} + export function QualityResultsPanel({ selectedProjectId, qualityChecks, @@ -50,6 +74,10 @@ export function QualityResultsPanel({ referenceDatasets, onRefresh, }: QualityResultsPanelProps): JSX.Element { + const [showAllQualityChecks, setShowAllQualityChecks] = useState(false) + const [qualityStatusFilter, setQualityStatusFilter] = useState('all') + const [qualityTypeFilter, setQualityTypeFilter] = useState('all') + const [qualitySearchQuery, setQualitySearchQuery] = useState('') const completedChecks = qualityChecks.filter((check) => check.status === 'ok' || check.status === 'completed').length const latestCheck = qualityChecks[0] ?? null const datasetNameById = useMemo(() => { @@ -61,6 +89,20 @@ export function QualityResultsPanel({ }, [candidateDatasets, referenceDatasets]) const latestCandidateName = latestCheck?.candidate_dataset_id ? datasetNameById.get(latestCheck.candidate_dataset_id) : null const latestReferenceName = latestCheck?.reference_dataset_id ? datasetNameById.get(latestCheck.reference_dataset_id) : null + const qualityStatuses = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.status))).sort(), [qualityChecks]) + const qualityTypes = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.check_type))).sort(), [qualityChecks]) + const filteredQualityChecks = useMemo( + () => + qualityChecks.filter((check) => { + const statusMatches = qualityStatusFilter === 'all' || check.status === qualityStatusFilter + const typeMatches = qualityTypeFilter === 'all' || check.check_type === qualityTypeFilter + return statusMatches && typeMatches && qualityMatchesSearch(check, qualitySearchQuery, datasetNameById) + }), + [qualityChecks, qualityStatusFilter, qualityTypeFilter, qualitySearchQuery, datasetNameById], + ) + const visibleQualityChecks = showAllQualityChecks ? filteredQualityChecks : filteredQualityChecks.slice(0, 8) + const hiddenQualityCheckCount = Math.max(filteredQualityChecks.length - visibleQualityChecks.length, 0) + const hasActiveQualityFilters = qualityStatusFilter !== 'all' || qualityTypeFilter !== 'all' || qualitySearchQuery.trim().length > 0 return (
@@ -122,8 +164,73 @@ export function QualityResultsPanel({

Run dataset, detection or segmentation QA against a reference layer to populate this workspace.

) : null} + {qualityChecks.length > 0 ? ( +
+ + + + +
+ ) : null} + {filteredQualityChecks.length > 8 ? ( +
+ + Showing {visibleQualityChecks.length} of {filteredQualityChecks.length} matching QA/QC results. + {hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} older checks hidden.` : ' All matching checks shown.'} + + +
+ ) : null} + {qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? ( +
+ No QA/QC results match the current filters. +

Clear the search, status or check type filter to return to the latest quality results.

+
+ ) : null}
    - {qualityChecks.map((check) => ( + {visibleQualityChecks.map((check) => (
  • diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 11b0ae0f..226cfdc5 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -1573,6 +1573,7 @@ button.entity-card { background: var(--accent-soft); } +.quality-history-controls, .export-history-controls { display: grid; grid-template-columns: minmax(14rem, 1.2fr) minmax(10rem, 0.9fr) minmax(9rem, 0.8fr) auto; @@ -1585,15 +1586,29 @@ button.entity-card { background: #ffffff; } +.quality-history-controls label, .export-history-controls label { margin-top: 0; } +.quality-history-controls button, .export-history-controls button { min-height: 2.35rem; width: auto; } +.quality-history-controls { + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); +} + +.quality-history-controls button { + width: 100%; +} + +.quality-list-count { + overflow-wrap: anywhere; +} + .list-limit-banner { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -1951,6 +1966,7 @@ button.entity-card { .quality-score-row, .export-action-grid, .handoff-action-grid, + .quality-history-controls, .export-history-controls, .inspector-action-bar, .list-limit-banner {