Polish QA workspace density
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-20 20:20:44 +02:00
parent c571dfe96c
commit c941b2619a
7 changed files with 375 additions and 186 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 86 QA/QC workspace density polish (2026-06-20)
- Grouped QA/QC summary, dataset evidence, refresh/filter controls and result history into focused surfaces.
- Kept existing persisted quality check filters, refresh behavior, metric cards and history rendering unchanged.
- Added compact mobile breakpoint grids for QA/QC summary, handoff evidence, filters and metric/history rows.
- Added static regression coverage for QA/QC workspace hierarchy and density contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 85 Map workspace density polish (2026-06-20) ## Sprint 85 Map workspace density polish (2026-06-20)
- Added a compact Map workspace context summary for selected AOI, active layer and rendered feature state. - Added a compact Map workspace context summary for selected AOI, active layer and rendered feature state.
@@ -0,0 +1,55 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_quality_results_panel_exposes_structured_surfaces() -> None:
panel = (ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx").read_text(
encoding="utf-8"
)
assert 'className="quality-results-panel"' in panel
assert 'className="quality-results-shell"' in panel
assert 'className="quality-summary-surface"' in panel
assert 'aria-label="QA/QC result summary"' in panel
assert 'className="quality-evidence-surface"' in panel
assert 'aria-label="QA/QC dataset evidence"' in panel
assert 'className="quality-control-surface"' in panel
assert 'aria-label="QA/QC refresh and filters"' in panel
assert 'className="quality-result-state-stack"' in panel
assert 'className="quality-history-surface"' in panel
assert 'aria-label="QA/QC result history"' in panel
def test_quality_results_panel_preserves_existing_refresh_filter_and_history_state() -> None:
panel = (ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx").read_text(
encoding="utf-8"
)
assert "onRefresh" in panel
assert "qualityStatusFilter" in panel
assert "qualityTypeFilter" in panel
assert "qualitySearchQuery" in panel
assert "visibleQualityChecks.map" in panel
assert "Show all QA/QC results" in panel
assert "No QA/QC results match the current filters." in panel
def test_quality_workspace_density_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".quality-results-shell" in css
assert ".quality-summary-surface" in css
assert ".quality-evidence-surface" in css
assert ".quality-control-surface" in css
assert ".quality-history-surface" in css
assert ".quality-result-state-stack" in css
assert ".quality-results-shell .quality-summary-grid" in css
assert ".quality-results-shell .quality-handoff-grid" in css
assert ".quality-results-shell .quality-history-controls" in css
assert "grid-template-columns: repeat(auto-fit, minmax(8.25rem, 1fr));" in css
assert "grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));" in css
assert "background: linear-gradient(180deg, #ffffff, #f7fbf8);" in css
+35
View File
@@ -3108,3 +3108,38 @@ Limitations:
Next recommended pass: Next recommended pass:
- Continue with QA/QC workspace result hierarchy and filter density after this pass is deployed and visually checked. - Continue with QA/QC workspace result hierarchy and filter density after this pass is deployed and visually checked.
## Sprint 86 QA/QC workspace density polish (2026-06-20)
Changed:
- Wrapped QA/QC summary metrics in `quality-summary-surface`.
- Wrapped candidate/reference/latest comparison context in `quality-evidence-surface`.
- Wrapped refresh, result states, filters and list limit controls in `quality-control-surface`.
- Wrapped persisted quality check cards in `quality-history-surface`.
- Added compact mobile CSS contracts for QA/QC summary, handoff, filter, score, metric and raw metric grids.
- Added `backend/tests/test_sprint86_quality_workspace_density.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Live browser pre-check against `http://192.168.10.150:1202` showed QA/QC workspace had no console errors or horizontal overflow, but summary, handoff, filters and history were still visually stacked at equal weight. Mobile measured summary at 210px, handoff at 295px and filters at 278px before the result history.
- Red step: `python -m pytest backend/tests/test_sprint86_quality_workspace_density.py -q` failed on missing QA/QC surface and density CSS contracts.
- `python -m pytest backend/tests/test_sprint86_quality_workspace_density.py -q` (`3 passed`)
- `python -m pytest backend/tests/test_sprint86_quality_workspace_density.py backend/tests/test_sprint70_quality_handoff_polish.py backend/tests/test_sprint71_quality_metric_polish.py backend/tests/test_sprint73_quality_result_filtering.py backend/tests/test_sprint81_result_state_polish.py backend/tests/test_sprint47_workbench_interaction_smoke.py -q` (`18 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- First full readiness run caught a compatibility regression in `backend/tests/test_sprint51_quality_export_polish.py`: the root `quality-results-panel` class was no longer exact. Kept the old root class and moved the new shell into a wrapper.
- `python -m pytest backend/tests/test_sprint51_quality_export_polish.py backend/tests/test_sprint86_quality_workspace_density.py backend/tests/test_sprint70_quality_handoff_polish.py backend/tests/test_sprint73_quality_result_filtering.py -q` (`11 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`292 passed`; frontend typecheck/build passed; Alembic single head `202606120900`)
- `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy_tower.ps1` rebuilt and redeployed the Tower all-in-one container on port 1202, then passed live migration smoke and browser runtime verification.
- Final live browser check against `http://192.168.10.150:1202` passed on desktop and mobile viewports: no console warnings/errors, no horizontal document overflow, QA/QC shell/surfaces present and two persisted quality check cards rendered.
Open:
- None for this pass.
Limitations:
- Frontend QA/QC workspace presentation hierarchy only; no quality-check behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with Change Detection panel hierarchy and analysis workspace balance after this pass is deployed and visually checked.
+1
View File
@@ -75,6 +75,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add Overview workspace panel hierarchy polish for readiness and recommended-action regions. - [x] Add Overview workspace panel hierarchy polish for readiness and recommended-action regions.
- [x] Add Data workspace selected-summary and panel density polish. - [x] Add Data workspace selected-summary and panel density polish.
- [x] Add Map workspace panel hierarchy and layer-control density polish. - [x] Add Map workspace panel hierarchy and layer-control density polish.
- [x] Add QA/QC workspace result hierarchy and filter density polish.
## Sprint 8 status ## Sprint 8 status
+2
View File
@@ -8,6 +8,8 @@ Data and Map workspaces include mobile-density CSS for file inputs, dataset acti
Map workspace now surfaces the selected AOI, active layer and rendered feature state before controls, then separates layer controls, provenance, the MapLibre frame and the feature inspector into focused surfaces. The underlying overlay, opacity and feature-selection behavior is unchanged. Map workspace now surfaces the selected AOI, active layer and rendered feature state before controls, then separates layer controls, provenance, the MapLibre frame and the feature inspector into focused surfaces. The underlying overlay, opacity and feature-selection behavior is unchanged.
QA/QC Results now separates persisted check summary, candidate/reference evidence, refresh/filter controls and result history into focused surfaces. Existing filters, refresh behavior, metric cards and stored result rendering are unchanged, with denser mobile grids for the same controls.
AI Labs uses the same mobile-density baseline for Detection and Segmentation model cards, run forms, QA/result summaries and scroll-safe result tables. This keeps long model ids, tile paths and artifact paths from widening the workbench on phones. AI Labs uses the same mobile-density baseline for Detection and Segmentation model cards, run forms, QA/result summaries and scroll-safe result tables. This keeps long model ids, tile paths and artifact paths from widening the workbench on phones.
Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench. Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench.
@@ -106,194 +106,216 @@ export function QualityResultsPanel({
return ( return (
<section className="quality-results-panel" data-testid="quality-results-panel"> <section className="quality-results-panel" data-testid="quality-results-panel">
<div className="panel-title-row"> <div className="quality-results-shell">
<div> <div className="panel-title-row">
<h2>QA/QC Results</h2> <div>
<p className="muted">Persisted quality checks from dataset, detection and segmentation comparisons.</p> <h2>QA/QC Results</h2>
<p className="muted">Persisted quality checks from dataset, detection and segmentation comparisons.</p>
</div>
<span className="count-pill">{qualityChecks.length} checks</span>
</div> </div>
<span className="count-pill">{qualityChecks.length} checks</span>
</div> <div className="quality-summary-surface" aria-label="QA/QC result summary">
<div className="quality-summary-grid"> <div className="quality-summary-grid">
<div> <div>
<span>Completed</span> <span>Completed</span>
<strong>{completedChecks}</strong> <strong>{completedChecks}</strong>
</div>
<div>
<span>Latest score</span>
<strong>{latestCheck?.score ?? 'n/a'}</strong>
</div>
<div>
<span>Latest status</span>
<strong>{latestCheck?.status ?? 'waiting'}</strong>
</div>
</div>
</div> </div>
<div>
<span>Latest score</span> <div className="quality-evidence-surface" aria-label="QA/QC dataset evidence">
<strong>{latestCheck?.score ?? 'n/a'}</strong> <div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context">
<div>
<span>Candidate layers</span>
<strong>{candidateDatasets.length}</strong>
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'No candidate layer selected yet'}</p>
</div>
<div>
<span>Reference layers</span>
<strong>{referenceDatasets.length}</strong>
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'No reference layer available yet'}</p>
</div>
<div>
<span>Latest comparison</span>
<strong>{latestCheck?.status ?? 'waiting'}</strong>
<p className="quality-dataset-name">
Candidate {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n/a'} / Reference {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n/a'}
</p>
</div>
</div>
</div> </div>
<div>
<span>Latest status</span> <div className="quality-control-surface" aria-label="QA/QC refresh and filters">
<strong>{latestCheck?.status ?? 'waiting'}</strong> <div className="button-row">
<button
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId}
data-testid="refresh-quality-results"
>
Refresh QA/QC results
</button>
</div>
<div className="quality-result-state-stack">
{qualityChecksError ? (
<div className="result-state result-state-error">
<strong>QA/QC results could not be loaded.</strong>
<p>{qualityChecksError}</p>
</div>
) : null}
{qualityChecks.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No persisted QA/QC results yet</strong>
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
</div>
) : null}
</div>
{qualityChecks.length > 0 ? (
<div className="quality-history-controls" aria-label="QA/QC result filters">
<label>
Search QA/QC results
<input
type="search"
placeholder="Type, id or dataset"
value={qualitySearchQuery}
onChange={(event) => setQualitySearchQuery(event.target.value)}
/>
</label>
<label>
Status
<select value={qualityStatusFilter} onChange={(event) => setQualityStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{qualityStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<label>
Check type
<select value={qualityTypeFilter} onChange={(event) => setQualityTypeFilter(event.target.value)}>
<option value="all">All check types</option>
{qualityTypes.map((checkType) => (
<option key={checkType} value={checkType}>
{checkType}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setQualitySearchQuery('')
setQualityStatusFilter('all')
setQualityTypeFilter('all')
setShowAllQualityChecks(false)
}}
disabled={!hasActiveQualityFilters && !showAllQualityChecks}
>
Reset view
</button>
</div>
) : null}
{filteredQualityChecks.length > 8 ? (
<div className="list-limit-banner">
<span className="quality-list-count">
Showing {visibleQualityChecks.length} of {filteredQualityChecks.length} matching QA/QC results.
{hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} older checks hidden.` : ' All matching checks shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllQualityChecks((value) => !value)}>
{showAllQualityChecks ? 'Show latest 8' : 'Show all QA/QC results'}
</button>
</div>
) : null}
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No QA/QC results match the current filters.</strong>
<p>Clear the search, status or check type filter to return to the latest quality results.</p>
</div>
) : null}
</div> </div>
</div>
<div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context"> <div className="quality-history-surface" aria-label="QA/QC result history">
<div> <div className="panel-title-row">
<span>Candidate layers</span> <h3>Result history</h3>
<strong>{candidateDatasets.length}</strong> <span className="count-pill">{visibleQualityChecks.length} shown</span>
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'No candidate layer selected yet'}</p> </div>
</div> <ul className="quality-check-list">
<div> {visibleQualityChecks.map((check) => (
<span>Reference layers</span> <li className="quality-check-card" key={check.id}>
<strong>{referenceDatasets.length}</strong> <div className="quality-check-header">
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'No reference layer available yet'}</p> <div>
</div> <strong>{check.check_type}</strong>
<div> <div className="entity-meta">
<span>Latest comparison</span> <span className="quality-check-dataset-link">
<strong>{latestCheck?.status ?? 'waiting'}</strong> candidate: {check.candidate_dataset_id ? datasetNameById.get(check.candidate_dataset_id) ?? check.candidate_dataset_id : 'n/a'}
<p className="quality-dataset-name"> </span>
Candidate {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n/a'} / Reference {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n/a'} <span className="quality-check-dataset-link">
</p> reference: {datasetNameById.get(check.reference_dataset_id) ?? check.reference_dataset_id}
</div> </span>
</div> </div>
<div className="button-row"> </div>
<button <span className={check.status === 'ok' || check.status === 'completed' ? 'status-badge status-badge-ready' : 'status-badge'}>
type="button" {check.status}
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId}
data-testid="refresh-quality-results"
>
Refresh QA/QC results
</button>
</div>
{qualityChecksError ? (
<div className="result-state result-state-error">
<strong>QA/QC results could not be loaded.</strong>
<p>{qualityChecksError}</p>
</div>
) : null}
{qualityChecks.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No persisted QA/QC results yet</strong>
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
</div>
) : null}
{qualityChecks.length > 0 ? (
<div className="quality-history-controls" aria-label="QA/QC result filters">
<label>
Search QA/QC results
<input
type="search"
placeholder="Type, id or dataset"
value={qualitySearchQuery}
onChange={(event) => setQualitySearchQuery(event.target.value)}
/>
</label>
<label>
Status
<select value={qualityStatusFilter} onChange={(event) => setQualityStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{qualityStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<label>
Check type
<select value={qualityTypeFilter} onChange={(event) => setQualityTypeFilter(event.target.value)}>
<option value="all">All check types</option>
{qualityTypes.map((checkType) => (
<option key={checkType} value={checkType}>
{checkType}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setQualitySearchQuery('')
setQualityStatusFilter('all')
setQualityTypeFilter('all')
setShowAllQualityChecks(false)
}}
disabled={!hasActiveQualityFilters && !showAllQualityChecks}
>
Reset view
</button>
</div>
) : null}
{filteredQualityChecks.length > 8 ? (
<div className="list-limit-banner">
<span className="quality-list-count">
Showing {visibleQualityChecks.length} of {filteredQualityChecks.length} matching QA/QC results.
{hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} older checks hidden.` : ' All matching checks shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllQualityChecks((value) => !value)}>
{showAllQualityChecks ? 'Show latest 8' : 'Show all QA/QC results'}
</button>
</div>
) : null}
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No QA/QC results match the current filters.</strong>
<p>Clear the search, status or check type filter to return to the latest quality results.</p>
</div>
) : null}
<ul className="quality-check-list">
{visibleQualityChecks.map((check) => (
<li className="quality-check-card" key={check.id}>
<div className="quality-check-header">
<div>
<strong>{check.check_type}</strong>
<div className="entity-meta">
<span className="quality-check-dataset-link">
candidate: {check.candidate_dataset_id ? datasetNameById.get(check.candidate_dataset_id) ?? check.candidate_dataset_id : 'n/a'}
</span>
<span className="quality-check-dataset-link">
reference: {datasetNameById.get(check.reference_dataset_id) ?? check.reference_dataset_id}
</span> </span>
</div> </div>
</div> <div className="quality-score-row">
<span className={check.status === 'ok' || check.status === 'completed' ? 'status-badge status-badge-ready' : 'status-badge'}> <div>
{check.status} <span>Score</span>
</span> <strong>{check.score ?? 'n/a'}</strong>
</div> </div>
<div className="quality-score-row"> <div>
<div> <span>Metrics</span>
<span>Score</span> <strong>{check.metrics.length}</strong>
<strong>{check.score ?? 'n/a'}</strong> </div>
</div> <div>
<div> <span>Quality check</span>
<span>Metrics</span> <strong>{check.id}</strong>
<strong>{check.metrics.length}</strong> </div>
</div> </div>
<div> <div className="quality-metric-section">
<span>Quality check</span> <span>Metric evidence</span>
<strong>{check.id}</strong> <div className="quality-metric-grid">
</div> {CORE_METRIC_ORDER.map((metricKey) => {
</div> const metric = check.metrics.find((item) => item.metric_key === metricKey)
<div className="quality-metric-section"> const isCriticalCount = metricKey === 'false_positive_count' || metricKey === 'false_negative_count'
<span>Metric evidence</span> return (
<div className="quality-metric-grid"> <div className={isCriticalCount ? 'quality-metric-card quality-metric-card-critical' : 'quality-metric-card'} key={metricKey}>
{CORE_METRIC_ORDER.map((metricKey) => { <span>{qualityMetricLabel(metricKey)}</span>
const metric = check.metrics.find((item) => item.metric_key === metricKey) <strong>{qualityMetricValue(metric)}</strong>
const isCriticalCount = metricKey === 'false_positive_count' || metricKey === 'false_negative_count' </div>
return ( )
<div className={isCriticalCount ? 'quality-metric-card quality-metric-card-critical' : 'quality-metric-card'} key={metricKey}> })}
<span>{qualityMetricLabel(metricKey)}</span> </div>
</div>
<span className="metric-list-label">Raw metrics</span>
<ul className="metric-list">
{check.metrics.map((metric) => (
<li key={metric.id}>
<span>{qualityMetricLabel(metric.metric_key)}</span>
<strong>{qualityMetricValue(metric)}</strong> <strong>{qualityMetricValue(metric)}</strong>
</div> </li>
) ))}
})} </ul>
</div> </li>
</div> ))}
<span className="metric-list-label">Raw metrics</span> </ul>
<ul className="metric-list"> </div>
{check.metrics.map((metric) => ( </div>
<li key={metric.id}>
<span>{qualityMetricLabel(metric.metric_key)}</span>
<strong>{qualityMetricValue(metric)}</strong>
</li>
))}
</ul>
</li>
))}
</ul>
</section> </section>
) )
} }
+71 -5
View File
@@ -1420,18 +1420,63 @@ button.entity-card {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.quality-summary-grid { .quality-results-shell {
display: grid;
gap: 0.75rem;
}
.quality-summary-surface,
.quality-evidence-surface,
.quality-control-surface,
.quality-history-surface {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.72rem;
background: #ffffff;
}
.quality-summary-surface {
background: linear-gradient(180deg, #ffffff, #f7fbf8);
}
.quality-evidence-surface {
background: #f8fbf9;
}
.quality-control-surface,
.quality-history-surface {
display: grid;
gap: 0.68rem;
}
.quality-control-surface .button-row,
.quality-control-surface .quality-history-controls,
.quality-control-surface .list-limit-banner {
margin-top: 0;
}
.quality-result-state-stack {
display: grid;
gap: 0.55rem;
}
.quality-results-shell .quality-check-list {
margin: 0;
}
.quality-results-shell .quality-summary-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.65rem; gap: 0.65rem;
margin: 0.75rem 0; margin: 0;
} }
.quality-handoff-grid { .quality-results-shell .quality-handoff-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: 0.65rem; gap: 0.65rem;
margin: 0 0 0.75rem; margin: 0;
} }
.quality-summary-grid > div { .quality-summary-grid > div {
@@ -1446,7 +1491,7 @@ button.entity-card {
border: 1px solid rgba(15, 118, 110, 0.18); border: 1px solid rgba(15, 118, 110, 0.18);
border-radius: 8px; border-radius: 8px;
padding: 0.72rem; padding: 0.72rem;
background: #f8fbf9; background: #ffffff;
} }
.quality-summary-grid span, .quality-summary-grid span,
@@ -2750,6 +2795,27 @@ button.entity-card {
padding: 0.58rem; padding: 0.58rem;
} }
.quality-summary-surface,
.quality-evidence-surface,
.quality-control-surface,
.quality-history-surface {
padding: 0.58rem;
}
.quality-results-shell .quality-summary-grid,
.quality-results-shell .quality-history-controls {
grid-template-columns: repeat(auto-fit, minmax(8.25rem, 1fr));
gap: 0.5rem;
}
.quality-results-shell .quality-handoff-grid,
.quality-results-shell .quality-score-row,
.quality-results-shell .quality-metric-grid,
.quality-results-shell .metric-list {
grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
gap: 0.5rem;
}
.layer-control-card { .layer-control-card {
padding: 0.52rem 0.58rem; padding: 0.52rem 0.58rem;
} }