From 438f10fb4baa79b513c55f48caad1a831d84398f Mon Sep 17 00:00:00 2001 From: Jens Date: Tue, 28 Jul 2026 07:21:48 +0200 Subject: [PATCH] Polish GeoIntel workbench and harden Tower deploy --- .dockerignore | 2 + .gitignore | 3 + .../tests/test_sprint31_unraid_template.py | 1 + deploy/unraid/deploy-release.sh | 72 +- docs/CODEX_EXECUTION_LOG.md | 35 + docs/TODO.md | 9 + frontend/src/App.tsx | 3 + frontend/src/components/GeoMap.tsx | 43 +- frontend/src/components/auth/LandingPage.tsx | 11 +- .../components/shell/WorkbenchNavigation.tsx | 1 - frontend/src/styles/geointel-system.css | 1303 +++++++++++++++++ scripts/deploy_tower.ps1 | 4 + scripts/deploy_tower.sh | 9 +- 13 files changed, 1453 insertions(+), 43 deletions(-) create mode 100644 frontend/src/styles/geointel-system.css diff --git a/.dockerignore b/.dockerignore index 086577c8..07cf1ecb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,9 +17,11 @@ __pycache__ # Node dependencies at every level (root test harness + frontend). node_modules **/node_modules +frontend/node_modules frontend/dist frontend/*.tsbuildinfo **/*.tsbuildinfo +frontend-src.tar.gz backend/.pytest_cache backend/**/*.pyc diff --git a/.gitignore b/.gitignore index 6a412359..62ee7448 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ dist/ build/ *.tsbuildinfo +# Local source-transfer archives; preserve them outside version control. +/frontend-src.tar.gz + # Large local data /artifacts/ /.cache/ diff --git a/backend/tests/test_sprint31_unraid_template.py b/backend/tests/test_sprint31_unraid_template.py index 0deb701e..cd524a48 100644 --- a/backend/tests/test_sprint31_unraid_template.py +++ b/backend/tests/test_sprint31_unraid_template.py @@ -136,6 +136,7 @@ def test_tower_deploy_uses_single_container_unraid_compose() -> None: for script in (powershell, bash): assert "bash deploy/unraid/deploy-release.sh" in script + assert "git clean -fd -- backend frontend deploy scripts fixtures tests contracts demo" in script assert "docker compose -f docker-compose.unraid.yml config" in release_script assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script diff --git a/deploy/unraid/deploy-release.sh b/deploy/unraid/deploy-release.sh index 568b1f2e..e606fd7d 100644 --- a/deploy/unraid/deploy-release.sh +++ b/deploy/unraid/deploy-release.sh @@ -32,17 +32,57 @@ if ! [[ "$GEOINTEL_APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] echo "Invalid semantic version in VERSION: ${GEOINTEL_APP_VERSION}" >&2 exit 2 fi +# Hash of everything that actually lands in the image. This is the honest +# answer to "does this image need rebuilding?" — unlike a git SHA, it changes +# when working-tree files change without a commit. +source_tree_hash() { + local hash="" + command -v sha1sum >/dev/null 2>&1 || return 1 + hash="$( + find backend frontend deploy scripts fixtures VERSION \ + -type f \ + ! -path '*/node_modules/*' \ + ! -path '*/dist/*' \ + ! -path '*/__pycache__/*' \ + ! -path '*/.pytest_cache/*' \ + ! -name '*.pyc' \ + -print0 2>/dev/null \ + | sort -z \ + | xargs -0 sha1sum 2>/dev/null \ + | sha1sum \ + | cut -c1-40 + )" || return 1 + [ -n "$hash" ] || return 1 + printf '%s' "$hash" +} + resolve_build_sha() { + local head="" content="" + # 1. Explicit override wins. if [ -n "${GEOINTEL_BUILD_SHA:-}" ]; then printf '%s' "$GEOINTEL_BUILD_SHA" return 0 fi - # 2. Real git checkout. + # 2. Git checkout, but only when the working tree matches the commit. + # A manually copied tree often carries .git along while the files on disk + # have moved on. Trusting HEAD there produces an unchanged image tag, and + # the deploy silently reuses the previous image instead of rebuilding. if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then - git rev-parse HEAD - return 0 + head="$(git rev-parse HEAD 2>/dev/null || true)" + if [ -n "$head" ]; then + if [ -z "$(git status --porcelain 2>/dev/null)" ]; then + printf '%s' "$head" + return 0 + fi + echo "Working tree differs from HEAD; tagging this build by content." >&2 + content="$(source_tree_hash || true)" + if [ -n "$content" ]; then + printf '%s-wip%s' "${head:0:12}" "${content:0:12}" + return 0 + fi + fi fi # 3. Manually copied tree with a RELEASE_SHA marker file. @@ -51,27 +91,11 @@ resolve_build_sha() { return 0 fi - # 4. Manually copied tree without git: derive a stable content hash so - # an unchanged redeploy reuses the existing immutable image tag. - if command -v sha1sum >/dev/null 2>&1; then - local tree_hash - tree_hash="$( - find backend frontend deploy scripts fixtures VERSION \ - -type f \ - ! -path '*/node_modules/*' \ - ! -path '*/__pycache__/*' \ - ! -path '*/.pytest_cache/*' \ - ! -name '*.pyc' \ - -print0 2>/dev/null \ - | sort -z \ - | xargs -0 sha1sum 2>/dev/null \ - | sha1sum \ - | cut -c1-40 - )" || tree_hash="" - if [ -n "$tree_hash" ]; then - printf '%s' "$tree_hash" - return 0 - fi + # 4. No git: content hash, so an unchanged redeploy still reuses its image. + content="$(source_tree_hash || true)" + if [ -n "$content" ]; then + printf '%s' "$content" + return 0 fi # 5. Last resort: unique per deploy. diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index b8efd3d3..ac9205ce 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -11633,3 +11633,38 @@ Next gate: The v6 train/calibration/test/background exports are ready for the next loop checkpoint; the running v5 iteration remains evidence but cannot supersede the cleaner v6 corpus. + +## 2026-07-28 - Visual release correction (pre-deploy) + +- Reviewed the user's consolidated GeoIntel design pass in the running guest + workflow against the live API at desktop, 1024 px laptop and tablet widths. +- Confirmed the revised landing page, guest authentication, map explorer, + full-work-area analysis and quality workspace with browser-rendered evidence. +- Corrected Chromium's internal `details` content-grid behavior so quality + evidence, history and the inspector occupy their intended columns instead of + collapsing into one narrow rail with per-letter filename wrapping. +- Moved the insights panel below the map below 1240 px, kept that combined + workspace internally scrollable and widened the desktop drawer enough to + show complete values such as `5,1% dekking`. +- Guarded optional `scrollIntoView` use so guest login remains functional in + browsers and test environments that do not implement it. +- Preserved the local `frontend-src.tar.gz` transfer archive while excluding it + from Git and Docker build contexts. Restored the explicit + `frontend/node_modules` exclusion required by the Unraid release contract. +- Hardened both Tower wrappers with a scoped untracked-source cleanup after the + remote reset. It removes stale files only from image source directories and + deliberately leaves `.env`, storage, models and PostGIS data untouched. + +Verified before deployment: + +- `npm run typecheck` +- `npm run build` +- `npm run test:unit` (`43 passed`) +- `py -3 -m pytest -q backend/tests/test_sprint31_unraid_template.py backend/tests/test_docker_runtime_config.py backend/tests/test_sprint193_end_user_workbench.py backend/tests/test_sprint194_regional_timeseries.py` (`56 passed`) +- Browser checks found no horizontal body overflow in the inspected desktop, + laptop and tablet layouts. + +Open at this checkpoint: + +- Push the verified commit and run the Dockerman-native Tower deployment from + the canonical `/mnt/user/appdata/geointel` checkout. diff --git a/docs/TODO.md b/docs/TODO.md index 91fa5adf..8e09e4af 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -971,3 +971,12 @@ This file now starts with the current implementation status. Older preparation/b - [x] Replace rolling-mosaic training inputs with governed dated 2025 Flanders/Brussels and complete 2023 SPW imagery; retain exact flight-day limitations. - [x] Reject positive labels over blank/no-data imagery and replace partial SPW 2024 coverage with the complete dated SPW 2023 campaign. - [x] Exclude GRB/PICC features created after the corresponding dated imagery period while retaining auditable rejection evidence. + +# Sprint 229 - Visual release correction and Tower redeploy + +- [x] Audit the revised landing page, guest map flow and quality workspace at desktop, laptop and tablet widths. +- [x] Keep the new consolidated GeoIntel design system while correcting the quality-details grid and narrow-laptop result layout. +- [x] Preserve complete insight labels and readable dataset/evidence cards without weakening the map-first workflow. +- [x] Make guest authentication independent of optional browser scrolling support. +- [x] Restore the explicit frontend dependency exclusion required by the Unraid build-context gate. +- [ ] Push the verified release and redeploy the Dockerman-native all-in-one runtime from `/mnt/user/appdata/geointel`. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 25217b04..b18bfa70 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,9 @@ import './styles/premium.css' import './styles/atlas-workbench.css' import './styles/atlas-premium-v2.css' import './styles/professionalization.css' +// Laatst geladen en leidend: het geconsolideerde designsysteem. +// Zie de kop van geointel-system.css voor de reden. +import './styles/geointel-system.css' import { LandingPage } from './components/auth/LandingPage' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel' diff --git a/frontend/src/components/GeoMap.tsx b/frontend/src/components/GeoMap.tsx index a0c43dd9..867064ef 100644 --- a/frontend/src/components/GeoMap.tsx +++ b/frontend/src/components/GeoMap.tsx @@ -176,6 +176,28 @@ function GeoMap({ const dataRef = useRef(data) const fitDataOnChangeRef = useRef(fitDataOnChange) const imageOverlayIdsRef = useRef([]) + // Onthoudt op welk kader al is ingezoomd. De fit-effecten draaien ook wanneer + // alleen `data` verandert; zonder deze bewaking sprong de kaart na elke + // analyse terug naar het volledige werkgebied en verloor de gebruiker zijn + // ingezoomde beeld direct na het tekenen van een selectie. + const lastFittedBoundsRef = useRef(null) + + // Zoomt alleen wanneer het kader echt anders is dan waarop we al pasten. + // Herhaalde aanroepen met dezelfde grenzen laten het beeld met rust. + const fitBoundsIfChanged = ( + map: maplibregl.Map, + bounds: maplibregl.LngLatBoundsLike | null, + ) => { + if (!bounds) { + return + } + const key = JSON.stringify(bounds) + if (lastFittedBoundsRef.current === key) { + return + } + lastFittedBoundsRef.current = key + map.fitBounds(bounds, { padding: 40, duration: 0 }) + } const [mapStyleReady, setMapStyleReady] = useState(false) areaDataRef.current = areaData @@ -230,10 +252,9 @@ function GeoMap({ map.resize() const fitCollection = areaDataRef.current ?? (fitDataOnChangeRef.current ? dataRef.current : null) - const bounds = fitCollection ? collectCoordinates(fitCollection) : null - if (bounds) { - map.fitBounds(bounds, { padding: 40, duration: 0 }) - } + // Bij het aanpassen van de venstergrootte het beeld behouden in plaats + // van terugspringen naar het volledige werkgebied. + fitBoundsIfChanged(map, fitCollection ? collectCoordinates(fitCollection) : null) }) resizeObserver.observe(containerRef.current) map.addControl(new maplibregl.NavigationControl(), 'top-right') @@ -425,10 +446,7 @@ function GeoMap({ if (data && fitDataOnChange && !areaData) { const collection = data if (collection.type === 'FeatureCollection' && collection.features.length > 0) { - const bounds = collectCoordinates(collection) - if (bounds) { - map.fitBounds(bounds, { padding: 40, duration: 0 }) - } + fitBoundsIfChanged(map, collectCoordinates(collection)) } } }, [areaData, data, dataFillColor, dataLineColor, fitDataOnChange, mapStyleReady]) @@ -494,12 +512,13 @@ function GeoMap({ ) } + // Dit effect draait ook wanneer alleen `data` verandert, bijvoorbeeld zodra + // een analyse resultaten oplevert. Ongewaakt zoomde de kaart dan terug naar + // het volledige werkgebied, meteen nadat de gebruiker een rechthoek had + // getekend. De bewaking laat het beeld staan zolang het gebied gelijk blijft. const activeCollection = areaData ?? (fitDataOnChange ? data : null) if (activeCollection) { - const bounds = collectCoordinates(activeCollection) - if (bounds) { - map.fitBounds(bounds, { padding: 40, duration: 0 }) - } + fitBoundsIfChanged(map, collectCoordinates(activeCollection)) } }, [areaData, data, fitDataOnChange, mapStyleReady]) diff --git a/frontend/src/components/auth/LandingPage.tsx b/frontend/src/components/auth/LandingPage.tsx index e9455f31..0aa5fd68 100644 --- a/frontend/src/components/auth/LandingPage.tsx +++ b/frontend/src/components/auth/LandingPage.tsx @@ -98,7 +98,9 @@ export function LandingPage({ setMenuOpen(false) setPendingAction('guest') setAuthError(null) - accessPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + if (typeof accessPanelRef.current?.scrollIntoView === 'function') { + accessPanelRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } try { const session = await loginAsGuest() onAuthenticated(session) @@ -111,7 +113,9 @@ export function LandingPage({ const focusLogin = () => { setMenuOpen(false) - accessPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + if (typeof accessPanelRef.current?.scrollIntoView === 'function') { + accessPanelRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } window.requestAnimationFrame(() => usernameRef.current?.focus()) } @@ -124,7 +128,6 @@ export function LandingPage({ GeoIntel - Atlas Workbench @@ -374,7 +377,7 @@ export function LandingPage({