Add image upload, thumbnail and full-image viewer to the WebUI (task 249, UI) #194
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/task-249-place-images-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
UI half of task 249, now targeting
maindirectly — #192 (the backend PR) merged as969442bf, and this branch is rebased onto that merge commit.What's here
PlaceMarkApiClient:UploadPlaceImageAsync/DeletePlaceImageAsync/GetPlaceImageAsync/GetPlaceThumbnailAsync. Images are fetched as bytes and shown asdata:URIs, not<img src>pointed at the API directly — the image endpoints need a bearer token a plainsrcattribute has no way to attach.PlaceDetailPanel: a thumbnail under the name (only whileImageVersionis set), fetched once per mount and shown as adata:URI; clicking it opensPlaceImageViewer. A failed fetch is silent — the thumbnail simply doesn't appear, the same progressive-enhancement shape the share fallback link already uses. The asymmetry with the viewer's own loud failure is now explained in the component's own remarks (review finding).PlaceImageViewer(new): the full-image popup. A genuine modal task under ADR-0099 unlike the rest ofPlaceDetailPanel, so it hosts its ownModalOverlay— sharing the existing--z-overlayrung rather than inventing one, per the ticket's own instruction. On a failed fetch it renders arole="alert"with the API's own message — loud, deliberately, unlike the thumbnail.PlaceFormPanel: an "Image" section, edit mode only (CallerMayEditImage, same threshold as the form fields themselves) — upload/replace via<InputFile>, remove with no confirmation (unlike deleting the place itself: an image is one upload away from being back). PatchesMapPlacesState.UpsertPlaceimmediately on a successful upload, decoupled from the best-effort thumbnail-preview fetch that follows it (review finding — see below).PlaceImageJourneyTests— upload → thumbnail in the edit panel → close → thumbnail in the detail panel → click → full image in its own popup, through a real browser, API, Postgres and SkiaSharp pipeline. The uploaded file is a real JPEG, SkiaSharp-encoded at test time rather than a checked-in binary fixture.Review fixes
HandleImageFileSelectedAsyncused to patchMapPlacesStateonly after both the uploadPUTand the follow-up thumbnail-previewGETsucceeded, so a successful upload followed by a failed preview fetch reported an error that misleadingly implied the upload itself had failed, while leaving the map/list state stale until a full reload. Fixed: the state patch now happens the moment the upload succeeds; the preview fetch is a separate, silent best-effort afterwards, matching the same conventionOnInitializedAsync's own preview fetch already uses. Covered by a new test,PlaceFormPanel_ImageUploadSucceedsButThumbnailPreviewFetchFails_PatchesMapPlacesStateAndDoesNotClaimTheUploadFailed, written and watched red against the unfixed code before the fix, then green after.coverage-baseline.json's WebUI entry was left at 91.3%, 0.2pp below the 91.5% CI's own run had just measured. Set to 91.5% exactly, matching this branch's own measured coverage with no slack.Judgement calls
Tests
859 in
PlaceMark.WebUI.Tests(855 passing + 4 pre-existing skips), 131 inPlaceMark.Architecture.Tests, plus the one E2E journey — all green locally and in CI. Full solution build clean with zero warnings,dotnet format --verify-no-changesclean, coverage ratchet holds with the corrected baseline (91.5% measured against a 91.5% floor). CI green at the current head (run #804): build/e2e/container-images all succeeded, includingPlaceImageJourneyTests' real execution through a live browser, API, Postgres and the SkiaSharp pipeline.ce918c27f0to83e2131c8883e2131c88to1ac61901881ac6190188to0354766f8b0354766f8bto1dc244517b1dc244517bto379723686dVerdict: changes needed
State-sync bug in
PlaceFormPanel.HandleImageFileSelectedAsync(PlaceFormPanel.razor.cs):PlacesState.UpsertPlaceis only called after both the upload PUT and the follow-upGetPlaceThumbnailAsyncpreview fetch succeed. If the upload succeeds but the preview fetch then fails, the catch sets_imageErrorto that fetch's message — misleadingly implying the upload itself failed — andPlacesStateis never patched, so the map/list state keeps claiming the place has no image until a full reload, even though the server now has one. PatchPlacesState.UpsertPlaceimmediately after the upload response, decoupled from the best-effort preview fetch.PlaceFormPanelTestsonly covers the PUT-itself-fails case (PlaceFormPanel_ImageUploadRefused_...); nothing covers PUT-succeeds-then-GET-fails.coverage-baseline.json's WebUI entry moves 91.4 → 91.3 (commit "Give the thumbnail button its own weight class"), while CI's own logged run shows actual coverage at 91.5%. The ratchet script does allow a deliberate manual lower with justification in the PR, and "blending in new-but-not-100%-covered code dilutes the average" is plausible, but there's no reason to leave the floor 0.2pp below what CI just measured — set it to the actual 91.5% so the ratchet keeps the ground this PR gained instead of leaving slack a future PR can erode unnoticed.Everything else holds:
PlaceImageViewercorrectly hosts its ownModalOverlayon the existing--z-overlayrung (no new layer invented); focus returns viaModalOverlay's ownOverlayFocus.ExitAsync, Escape works through the defaultHandleKeyDownAsyncpath.PlaceDetailPanel_ThumbnailFetchFails_RendersNoThumbnailAndNoError). The full-image viewer is not silent on failure, contrary to how the review brief characterised it — it rendersrole="alert"with the API's own message (PlaceImageViewer_FetchFails_ShowsAnAlertRatherThanAnImage) — no defect there.CallerMayEditImagecorrectly gates upload/remove at Owner/Editor, matching the backend'sMayEditPlaces; the GET routes areMember-only, andPlaceDetailPanelshows the thumbnail with no role gate at all, so a Viewer can see but not edit.data:URI re-fetch-per-mount cost is real but already stated plainly in ADR-0164's own "Consequences" section (landed with #192) as an accepted cost at this project's scale, not glossed over — nothing further needed here.PLACEMARK_RUN_E2E_TESTS=trueis set only in thee2ejob, and that job is reachable onpull_requestperci.yml; the one CI run for this head SHA (run #803,pull_requestevent) succeeded, consistent withPlaceImageJourneyTestsgenuinely running rather than being skipped. Couldn't pull raw step logs through the available tooling to confirm the literal 46/46 figure.Verdict: mergeable
Both findings from the previous round are fixed, confirmed against the diff at
532c780:HandleImageFileSelectedAsyncnow callsPlacesState.UpsertPlaceimmediately after the upload PUT succeeds, before the thumbnail-preview GET even starts; the preview fetch has its own nested try/catch that's silent onApiException, matchingOnInitializedAsync's existing convention. Only one call site patches state, unconditional on the preview outcome — decoupling is complete, and nothing that was a useful error before is now swallowed (the preview fetch was never the place a real error would need reporting).ImageVersionon the patchedMapPlacesStateplace and asserts norole="alert"in the image section and no trace of the preview's error text in the markup. That combination is exactly what the unfixed code would have failed — old code skipped the state patch and set_imageErrorto the preview's message.PlaceDetailPanel's remarks is accurate: quiet thumbnail because it's incidental to a panel showing plenty else, loud viewer because opening it is the reader's whole reason for being there and an unexplained empty popup would be worse.Everything else from the first review stands unchanged.