Polish GeoIntel workbench and harden Tower deploy
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-28 07:21:48 +02:00
parent 19cf2b5211
commit 438f10fb4b
13 changed files with 1453 additions and 43 deletions
+2
View File
@@ -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
+3
View File
@@ -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/
@@ -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
+48 -24
View File
@@ -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.
+35
View File
@@ -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.
+9
View File
@@ -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`.
+3
View File
@@ -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'
+31 -12
View File
@@ -176,6 +176,28 @@ function GeoMap({
const dataRef = useRef<GeoJSON.FeatureCollection | null>(data)
const fitDataOnChangeRef = useRef(fitDataOnChange)
const imageOverlayIdsRef = useRef<string[]>([])
// 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<string | null>(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])
+7 -4
View File
@@ -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({
<GeoIntelMark className="landing-brand-mark" />
<span className="landing-brand-copy">
<strong>GeoIntel</strong>
<small>Atlas Workbench</small>
</span>
</a>
@@ -374,7 +377,7 @@ export function LandingPage({
<footer className="landing-footer">
<div className="landing-footer-brand">
<GeoIntelMark className="landing-footer-mark" />
<div><strong>GeoIntel Atlas Workbench</strong><p>Operationele GIS-analyse voor België en de Belgische Noordzee.</p></div>
<div><strong>GeoIntel</strong><p>Operationele GIS-analyse voor België en de Belgische Noordzee.</p></div>
</div>
<div className="landing-footer-ownership">
<ItWorxSignature />
@@ -54,7 +54,6 @@ export function WorkbenchNavigation({
<GeoIntelMark className="brand-mark" />
<span className="sidebar-brand-copy">
<strong>GeoIntel</strong>
<small>Atlas Workbench</small>
</span>
</div>
File diff suppressed because it is too large Load Diff
+4
View File
@@ -32,6 +32,10 @@ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$REMOTE_REPO
git fetch origin "$REMOTE_BRANCH"
git reset --hard "origin/$REMOTE_BRANCH"
git branch -M "$REMOTE_BRANCH"
# Remove only untracked source files that can enter the image. Persistent
# runtime roots (.env, storage, models and postgres-data) are deliberately not
# in this scoped clean list.
git clean -fd -- backend frontend deploy scripts fixtures tests contracts demo
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
FRONTEND_URL="$FRONTEND_URL" \
+7 -2
View File
@@ -4,8 +4,8 @@ set -euo pipefail
REMOTE_HOST="${REMOTE_HOST:-root@192.168.10.150}"
REMOTE_PATH="${REMOTE_PATH:-/mnt/user/appdata/geointel}"
REMOTE_BRANCH="${REMOTE_BRANCH:-main}"
REMOTE_REPO="${REMOTE_REPO:-gitea-widefrog:NuklearRabbit/geointel.git}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/widefrog_unraid_deploy}"
REMOTE_REPO="${REMOTE_REPO:-git@gitea.itworx.tech:Jens/geointel.git}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/itworx_unraid_deploy}"
FRONTEND_URL="${FRONTEND_URL:-http://192.168.10.150:1202}"
BOOTSTRAP="${DEPLOY_BOOTSTRAP:-0}"
DEPLOY_GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-}"
@@ -20,6 +20,7 @@ ssh "${ssh_opts[@]}" "$REMOTE_HOST" \
set -euo pipefail
cd "$REMOTE_PATH"
git config --global --add safe.directory "$REMOTE_PATH"
if [[ ! -d .git ]]; then
if [[ "$DEPLOY_BOOTSTRAP" != "1" ]]; then
@@ -33,6 +34,10 @@ fi
git fetch origin "$REMOTE_BRANCH"
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
# Remove only untracked source files that can enter the image. Persistent
# runtime roots (.env, storage, models and postgres-data) are deliberately not
# in this scoped clean list.
git clean -fd -- backend frontend deploy scripts fixtures tests contracts demo
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
FRONTEND_URL="$FRONTEND_URL" \