Deze worden voorgelezen aan gebruikers die met een schermlezer werken. In een
verder Nederlandse werkbank hoorden zij "Export artifact actions" en "Map
selection QA shortcut".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deze verschenen zodra de backend geen eigen melding meegaf: de terugvalteksten
van formatError en de controles vóór een handeling. Een operator kreeg dan
"Failed to load exports" of "Select a project first" te zien, midden in een
verder Nederlandse werkbank.
Dit is geen meertaligheidsvraagstuk maar inconsistentie binnen de taal die het
product al spreekt. De teksten volgen nu de bestaande toon: benoemen wat er
misging of wat er eerst nodig is, in actieve vorm, zonder excuus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De verzoekdeler uit de vorige commit houdt een antwoord driehonderd
milliseconde vast om de opstartcascade op te vangen. Uitloggen herlaadt de
pagina echter niet, dus die tabel overleeft een sessiewissel: binnen dat venster
zou de volgende gebruiker het antwoord van de vorige kunnen krijgen.
Met de hand is dat nauwelijks te bereiken, maar een verzoekcache die een
identiteitswissel overleeft is hoe dan ook fout. De tabel wordt nu gewist bij
uitloggen, bij aanmelden en zodra de sessie verloopt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De inlogpagina haalde 1,46 MB over de lijn, waaronder de volledige kaartmotor
van 213 kB terwijl er geen kaart staat. De werkbank is nu een aparte brok die
pas na aanmelden geladen wordt: de eerste JS-bundel gaat van 476 naar 30 kB, en
met de beeldwerk uit een eerdere commit komt de inlogpagina op 344 kB.
Bij het opstarten gingen 27 verzoeken de deur uit in drie golven. Alles werd
vooraf opgehaald, ook voor werkbladen die de gebruiker misschien nooit opent,
en twee daarvan werden dubbel opgevraagd door effecten die negentig
milliseconde na elkaar liepen. Gelijktijdige en vlak opeenvolgende identieke
GET-verzoeken delen nu één belofte, en elk werkblad haalt zijn eigen gegevens
pas op wanneer het geopend wordt. Nu zestien verzoeken.
Typen in het themazoekveld kostte 53 ms mediaan met uitschieters tot 127 ms,
omdat die filterwaarde in het viewmodel van de hele werkruimte leefde. Het veld
is een eigen component geworden dat zijn invoer zelf bijhoudt. React.memo om de
kaart is geprobeerd en weer verwijderd: van de negentien props worden er te
veel per render opnieuw gemaakt, dus de vergelijking sloeg nooit over.
Verder: donker blijft de standaard maar er is nu een weg terug naar licht, de
kaartondergrond schakelt mee zonder de datalagen te herbouwen, de onderste
hero-afbeelding wordt pas opgehaald als hij in beeld komt, en de werkbank heeft
een h1 zodat schermlezers een ankerpunt hebben.
De tests op de bootstrap legden het oude gedrag vast en toetsen nu het nieuwe,
van drie naar zeven gevallen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De analyseconsole opent in een eigen venster en stond daar in Times New Roman
op wit, met standaard browserknoppen, terwijl de werkbank ernaast donker was.
Niemand had er ooit naar gekeken.
Er was code die de opmaak moest meenemen, maar die kloonde de link naar het
nieuwe venster. Dat venster wordt geopend met window.open('') en staat dus op
about:blank; nagemeten leverde link.sheet === null op. De regels worden nu
ingeschreven in plaats van gelinkt, met de verwijzingen absoluut gemaakt tegen
de bron van het blad.
Daarna bleef document.fonts.status eeuwig op 'loading' staan. De bestanden
kwamen wel binnen met status 200, maar in een about:blank-document voltooit het
lettertypeladen niet. Een FontFace hangt niet aan een document, dus de
gezichten die het hoofdvenster al geladen heeft worden nu overgenomen en gaan
de @font-face-regels niet mee. Meetbaar: status 'loaded', en een schermafdruk
die op de lettertypen wacht is klaar in 45 ms in plaats van af te breken na
dertig seconden.
Dit verklaarde ook de vijftien !important in dit blok: die probeerden iets te
overschrijven dat er nooit aankwam. Alle zestien bleken overbodig en zijn in de
vorige commit verwijderd.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Er was nergens een error boundary: geen componentDidCatch, geen
getDerivedStateFromError. Eén component die gooit nam de hele werkbank mee,
inclusief de kaart en de navigatie. Nu blijft de schil staan, ziet de gebruiker
welk werkblad het liet afweten, en wist wegnavigeren de fout.
Daarnaast een patroon dat op zeven plekken half was toegepast: een optionele
keten die te vroeg stopt. report?.items?.filter(...) stond geguard, twintig
regels verderop stond report.items.map(...) zonder. Half geguard is de
eigenlijke fout, want het wekt zekerheid zonder die te bieden. Waar de lijst op
meerdere plekken gebruikt wordt staat nu één normalisatie aan de kop.
De brondetails hadden geen enkele afhandeling en werden met .catch(() => null)
overgeslagen; die melden nu wel. De overige elf catch-blokken bleken bovenop
hooks te staan die de fout al zelf opvangen en tonen, wat ik heb nagemeten door
een verzoek te laten mislukken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Achttien bevindingen uit de audit, met de metingen erbij in de betreffende
CSS-secties.
Fundament. Dertien tokens werden op 87 plaatsen aangeroepen zonder ooit
gedefinieerd te zijn; in de browser gaf getPropertyValue('--gi-text') een lege
string terug, dus die kleurregels vielen terug op overerving. Vorm en hoogte
zijn terug naar de schaal: radius via token van 17% naar 81%, unieke schaduwen
van 115 naar 71, losse kleurwaarden van 928 naar 352.
Typografie. De stylesheets declareerden negen gewichten terwijl er per familie
twee gezichten geladen werden, en font-synthesis-weight: none verbiedt
namaakvet. Alles van 650 tot 850 kwam dus als 600 op het scherm: 138 van de 148
declaraties rendereden identiek. Nu vier stappen die alle vier ook geladen
worden, en Manrope draagt de koppen zoals op de landingspagina.
Donkere werkstand. De werkbank staat tegen luchtbeelden aan; een lichte schil
daarnaast laat de kaart altijd verliezen. De landingspagina blijft licht.
Kaart. De ondergrond wordt ontkleurd en gedempt zodat alleen de eigen data nog
kleur draagt, en de symbologie staat niet langer als losse hexwaarden in
GeoMap. De zes zwevende vlakken zijn er drie: statuskaart, staprail en
resultaatchip bleken al één component te zijn die de opmaak over drie hoeken
van de kaart uiteentrok.
Schil. Eén kopbalk in plaats van drie (215px chroom naar 100px), het raster van
AI-vragen zonder botsingen, kapitalen op één niveau in plaats van 57
declaraties, en het leverancierslogo uit de navigatiebalk.
Van 47 echte !important naar 1, gemeten door telkens twee productiebuilds op
berekende stijlen te vergelijken. Contrast: nul tekstelementen onder AA op zes
werkbladen, in beide werkstanden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De inlogpagina haalde 1,46 MB over de lijn voordat iemand kon aanmelden.
Daarvan waren 885 kB drie hero-afbeeldingen; achtergronden in CSS kennen geen
loading="lazy", dus ook de twee onder de vouw laadden meteen.
Daarnaast stond er 8,1 MB aan ongebruikte PNG-originelen in public/portfolio/.
Alles in public/ wordt ongewijzigd naar dist/ gekopieerd, dus die gingen bij
elke build mee terwijl geen regel code ernaar verwees. Ze staan nu in
design-assets/, buiten wat de webserver uitserveert; de README daar legt uit
hoe je een webversie opnieuw maakt.
- de drie hero's opnieuw gecomprimeerd op hun werkelijke weergavegrootte
- de Noordzee-textuur ligt onder een vrijwel dekkende gradiënt: 240 -> 35 kB
- het ITWorx-woordmerk toonde op 118px maar was 512px breed: 40 -> 12 kB
- geointel-icon.png en de dark-case-study-cover werden nergens genoemd
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The recommended detection profile showed operators precision 0.614, recall
0.606 and F1 0.607. Those three numbers appear nowhere in this repository
except the file that publishes them and a test that pinned them as literal
strings. The only recorded evaluation of that model at that operating point —
tile 512, overlap 64, threshold 0.15, the exact key its promotion report
recommended — reported 0.590, 0.577 and 0.582. The published figures were about
two and a half points more flattering than anything that was measured, on the
profile labelled "aanbevolen", and the test made sure nobody would correct
them. They now carry the measured values.
Worse in kind: the conservative profile reported "gemeten achtergrondfouten 0".
Its nine-sample hard-negative matrix at threshold 0.35 recorded 198 background
detections with 55 in the worst sample. The one number that tells an operator
whether a high-precision model invents buildings on empty terrain said zero
where the evidence said 55.
Those zeros are not simply wrong everywhere, which is why the fix is not just a
number. The other two profiles genuinely produced zero — against a strict
pure-empty gate of three samples, a different and much weaker test than the
nine-sample hard-negative matrix. Printing 0, 0 and 55 side by side invites a
comparison the evidence does not support, so each profile now states its gate,
its background sample count and the evaluation behind its figures, and the
panel shows them.
A test refuses any published figure that does not appear in the evidence
record, with a negative control so it cannot pass by matching nothing. Pinning
the numbers as literal strings is what let an unsourced precision survive; that
assertion is gone.
Also ignoring .codex-artifacts/ — ~300 MB of the rejected SAM2 and
edge-alignment trials plus a deploy bundle. Kept on disk, out of the repository.
No credentials in it; the two token scripts generate from settings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MapWorkspace.tsx was 3.157 lines: a props interface, 1.200 lines of derived
state and handlers, and two complete render paths — the map-first explorer and
the advanced workbench behind it. It is now five modules, and the container is
nineteen lines that choose between the two.
The obstacle was the props signature. The explorer reads 97 derived values and
the workbench 40, so passing them individually would have produced a 97-field
interface — worse than the file it replaced. Extracting the derived state into
a hook that returns one object solves it: MapWorkspaceViewModel is
ReturnType<typeof useMapWorkspaceViewModel>, so the shape is derived from what
the hook actually produces and cannot drift from it. Each view then names two
typed objects, and the JSX moved unchanged.
The contract tests found the one place where widening a negative assertion is
wrong. "The map workspace performs no transport" was true of the old file and
false of the whole feature, because the hooks call the API by design. It is now
scoped to the presentational modules, which is what it always meant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Detection and segmentation run listings returned every run a project had ever
produced. Runs accumulate with every analysis while the panel only ever draws
the recent ones, so the response grew without bound for no benefit.
Both take limit and offset now and report total, limit, offset and truncated,
matching the result listings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Detection QA reports a precision/recall curve, average precision and a
calibration sweep; segmentation QA reported a single operating point. Both rank
their outputs by confidence, so the same view applies, and the asymmetry meant
the two panels answered different questions about comparable runs — an
inconsistency introduced when detection gained the curve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The review vocabulary already separates a model error from a reference gap,
because the product's position is that official footprints are not
automatically perfect ground truth. Those verdicts were only counted. An
operator who inspected forty false positives and established that twelve are
buildings the reference simply lacks still saw a precision counting all forty
against the model — a number they had personally disproved, on the panel where
they disproved it.
Applying the verdicts gives an adjudicated score reported next to the raw one,
so nothing is quietly improved. Not being able to judge is not evidence in the
model's favour, so uncertain and obscured verdicts keep counting, as does a
decision from a later release that this runtime does not recognise.
Because part of the evidence is usually still unreviewed, the honest form is an
interval rather than a single corrected number: pessimistic assumes every
unreviewed finding is a model error, optimistic assumes none is, and the
headline equals the pessimistic reading so a partly reviewed check never
presents as a settled one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Threshold calibration ran the model over every tile once per threshold — three
GPU passes to compare 0.50, 0.25 and 0.15 on a hundred-tile raster. The answer
is already in a single run at the lowest value: detections above a higher cut
are a subset of it, and duplicate suppression walks candidates in descending
confidence, so a lower-confidence box can never displace a higher-confidence
one. The kept set above any cut is identical whichever threshold the run used,
which is what makes one pass sufficient rather than merely cheaper.
QA now takes calibration_thresholds and reads each operating point off the same
precision/recall walk it already performs, marking the F1-optimal cut. The lab
runs inference once and fills its table from the sweep.
The contract test asserted the per-threshold loop by name, pinning the waste it
was meant to describe. It now states what calibration owes an operator: a row
per requested threshold, from one run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extracted for its error paths, not its size. Every step can fail or return
nothing, and each outcome has to leave the operator with a status that says
where the chain stopped — a workflow reporting "afgerond" after a step produced
nothing tells them a result exists when it does not. Inline in the component,
none of that was exercised; it now has fourteen tests covering each stopping
point, both rejection kinds, the reuse path and the preconditions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Corner tracking for a two-click rectangle lived inline in the component and
was never exercised. The rule worth pinning is that a first corner yields no
rectangle at all: acting on it would analyse a zero-width area.
The hook holds only interaction state — drawing mode, placed corner, the
coordinate fields. What happens with a finished rectangle stays in the
workspace, which owns retiring stale results and starting the analysis.
Injecting that would have made the hook depend on values declared after it,
which is what a first attempt at a wider extraction ran into.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MapWorkspace held five near-identical useMemo blocks deciding which raster
image the map draws under the active theme — terrain, flood depth, thematic
raster, WALOUS land cover and bathymetry. Each filtered partitions by source
name, read bbox_epsg4326 and assembled the same overlay shape, so the parts
that genuinely differ per theme were buried in the repetition.
One builder makes the rule testable and leaves only the source, the label and
the opacity varying. A raster whose bounds are unusable is now skipped rather
than drawn from a partial bbox: an overlay in the wrong place is worse than no
overlay.
The legend asked "are these thematic or WALOUS overlays" by inspecting two of
the five lists. That is a property of the source, so it says so directly.
Two contract tests needed fixing rather than repointing. One asserted
`"api" not in source.lower()`, which the new hook name useMapImageOverlays
matches inside "useM-api-mageOverlays" — as would rapid, capital or therapy.
The contract is that this component talks to no API client, so it now says
that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MapWorkspace.tsx opened with ~590 lines of theme catalogue, dataset matching
and label formatting above a 3.200-line component. None of it is React, all of
it is independently testable, and both render paths read from it, so it belongs
beside the pure helpers that already live in mapWorkspaceUtils.
The contract tests that read MapWorkspace.tsx would have gone red for a move
that changes no behaviour at all — 24 of them. That is the brittleness the
frontend_contract helper exists to remove, so it gains read_map_workspace():
the workspace is one feature spread over several modules, and a contract
belongs to the feature rather than to whichever file currently holds it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/detection/runs/{id}/detections and its GeoJSON sibling returned every
persisted detection, as did the segmentation equivalents. A regional run holds
tens of thousands, and these are the endpoints the results table and the map
overlay call after every run.
They now take limit and offset, default to 2.000, and report total, limit,
offset and truncated so the complete population stays visible while what is
transferred does not. The GeoJSON responses carry the same window in a
geointel_result_window foreign member.
Rows are ordered by confidence, so a capped overlay draws the strongest
detections rather than an arbitrary slice, and the lab says how many of how
many are being shown rather than silently presenting a page as the whole run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
evidence_geojson emitted one feature per false positive, one per false negative
and two per match, with no limit. A regional check of 40k detections against 45k
reference footprints produced well over a hundred thousand features in a single
response, plus one warning string per unresolvable identifier. The endpoint the
entire review workflow depends on therefore failed exactly where review matters
most.
What to draw is now decided before any geometry is fetched, so the query work is
proportional to the result rather than to the size of the check — previously
130k geometries were resolved through an IN clause holding every identifier in
the check, to then discard most of them.
The budget is split between misses and false positives in proportion to their
populations with at least one of each, rather than by strict priority, which
would mean a check with 50.000 misses and three false positives never showed
one. Confirmations fill what remains, and a match is kept or dropped as a pair
because half a match is not reviewable evidence.
limit_evidence and evidence_role_counts are removed: plan_evidence supersedes
them, and helpers kept alive only by their own tests read like a contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comparison hook runs before the selection state is declared in App, so the
selection is read through a getter at the moment the run starts rather than
captured at render. The panel gains the modified count, which until now was
folded into removed plus added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend now says when a result covers a different area than was drawn, or
when a score belongs to one confidence cut only. None of that helps an
operator while it stays in the response body.
- the raster selection adapters carry the model-coverage and widened-cell
warnings into the map panel and mark the result an estimate when either
applies, so an existing warning slot renders them;
- the map workspace shows the selection-edge disclosure next to the object
count;
- the detection panel shows average precision and the F1-optimal threshold
beside the single-threshold figures, and the box-versus-footprint
interpretation when candidates are detector boxes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>