Skip to content
This repository was archived by the owner on Oct 4, 2026. It is now read-only.

feat(ui): image map live refresh, status footer, hover thumbnails - #41

Merged
lstein merged 6 commits into
mainfrom
feat/image-map-08-live-theme
Aug 16, 2026
Merged

lstein merged 6 commits into
mainfrom
feat/image-map-08-live-theme

Conversation

@lstein

@lstein lstein commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR 8 of the Image Map stack:

  • Socket-driven refresh replaces polling: image_map_projection_ready (routed by the backend to the owning user) or a socket reconnect refetches the points — but only once the widget has loaded the map, so users who never open it pay nothing. A refresh requested mid-flight is queued and runs when the current fetch settles (the projection-ready-during-initial-GET window is exactly when the event matters most). Typed contracts for both new events are added to the socket event map.
  • Footer: point count, an "updating…" hint while the served projection is stale, embedding-index progress (admins are the only recipients of those events, and the text hides itself when the backfill drains), and a manual refresh button.
  • Hover previews: dwelling 150ms on a point shows its thumbnail beside the cursor (PhotoMapAI behavior). A monotonic hover session guards against early/stale-coordinate previews from slow fetches; thumbnail URLs are cached per session (they derive from the immutable image name, unlike full DTOs).
  • No theming work needed: the plot is transparent with hidden axes, so it inherits every workbench theme.

An adversarial review covering this diff was run; its findings (swallowed mid-flight refresh, hover race) are fixed above. Accepted trade-offs it confirmed: after first load, projection-ready events refetch for the session even with the widget closed (host runtimes are always mounted; admin scope makes any user's activity relevant); the thumbnail-URL cache grows by name→string entries only.

Testing

Runtime attach/detach with mocked socketHub (gating on load state, reconnect refresh, index-count storage), store chaining semantics. Full suite green at the stack top.

🤖 Generated with Claude Code

@lstein
lstein requested a review from blessedcoolant as a code owner August 2, 2026 21:49
@github-actions github-actions Bot added the python label Aug 2, 2026
@lstein
lstein marked this pull request as draft August 2, 2026 22:24
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from aa21512 to 32045e9 Compare August 2, 2026 22:38
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch 2 times, most recently from 3a5662d to 84be196 Compare August 3, 2026 01:38
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 32045e9 to bf26409 Compare August 3, 2026 01:38
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 84be196 to 7960b81 Compare August 3, 2026 02:26
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from bf26409 to 0015963 Compare August 3, 2026 02:26
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 7960b81 to f15c9c5 Compare August 3, 2026 18:19
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 0015963 to 32b12f4 Compare August 3, 2026 18:20
@lstein
lstein marked this pull request as ready for review August 3, 2026 18:28
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from f15c9c5 to e55f4db Compare August 4, 2026 01:43
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 32b12f4 to 2d68eb8 Compare August 4, 2026 01:43
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from e55f4db to 3545b1f Compare August 4, 2026 05:37
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch 2 times, most recently from 312156e to 8a2e9a2 Compare August 4, 2026 14:58
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch 2 times, most recently from 343998e to cc30d46 Compare August 4, 2026 22:32
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch 2 times, most recently from c47f5ae to e239eca Compare August 5, 2026 00:31
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from cc30d46 to 16273bb Compare August 5, 2026 00:31
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from e239eca to 78b62a5 Compare August 5, 2026 00:38
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 16273bb to aae9670 Compare August 5, 2026 00:38
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 78b62a5 to a43f464 Compare August 5, 2026 01:18
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch 2 times, most recently from 640ce5a to d36dbbb Compare August 5, 2026 01:21
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from a43f464 to 311ecaa Compare August 5, 2026 01:21
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from d36dbbb to a400674 Compare August 7, 2026 20:41
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 311ecaa to 4bf36a2 Compare August 7, 2026 20:41
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from a400674 to 229ff97 Compare August 9, 2026 18:27
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 4bf36a2 to 29de7d4 Compare August 9, 2026 18:27
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 229ff97 to df50a10 Compare August 13, 2026 03:21
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 29de7d4 to 7646455 Compare August 13, 2026 03:21
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from df50a10 to 7ff185d Compare August 13, 2026 03:26
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 7646455 to ef0eb5c Compare August 13, 2026 03:26
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 7ff185d to 7115d12 Compare August 13, 2026 04:37
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from ef0eb5c to 353ef22 Compare August 13, 2026 04:37
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 7115d12 to d857e04 Compare August 13, 2026 23:17
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 353ef22 to 6ad08aa Compare August 13, 2026 23:17
@lstein
lstein force-pushed the feat/image-map-07-marker-zoom branch from 57c86da to 328d8db Compare August 15, 2026 19:37
lstein added 2 commits August 15, 2026 17:14
- Socket-driven refresh replaces polling: an `image_map_projection_ready`
  event (routed by the backend to the owning user) or a socket reconnect
  refetches the point set — but only once the widget has loaded the map,
  so users who never open it pay nothing. Contracts for the two new
  backend events are added to the typed socket event map.
- New widget footer: point count, an "updating…" hint while the served
  projection is stale, embedding-index progress (admins are the only
  recipients of those events), and a manual refresh button.
- Hover previews: dwelling on a point for 150ms shows its thumbnail beside
  the cursor (PhotoMapAI behavior). Thumbnail URLs are cached per session —
  unlike full DTOs they derive from the immutable image name.

The plot needs no explicit theming work: its background is transparent and
axes are hidden, so it inherits every workbench theme for free.

Amended during the rebase onto the re-architected webv2:

- The socket event contracts moved out of the deleted shared event map
  (`workbench/backend/events.ts`): the two image-map events are domain
  contracts, so they now live in `workbench/image-map/events.ts` as plain
  interfaces (the queue feature's `data/events.ts` is not a public queue
  module, and `socketHub.on` is stringly-typed, so no shared map is
  needed). The hub itself is imported from `@platform/transport/socketHub`
  (same on/onConnectionChange surface as the old workbench hub).
- The data runtime moved to main's host slot: `hasHost: true` on the
  eager manifest with `host: ImageMapDataRuntime` returned from the
  deferred implementation (queue/workflow pattern); the component wraps
  `attachImageMapDataRuntime` in `useMountEffect`. The footer likewise
  ships through the implementation's `footer` slot instead of the old
  single-manifest module.
- The thumbnail cache resolves names through the gallery feature's public
  interface (`galleryImages.resolveMany`) instead of the deleted
  `workbench/gallery/api`; the footer's IconButton/Tooltip come from
  `@platform/ui`, and `WidgetViewProps` from `@workbench/widgetContracts`.
- The runtime test mocks `@platform/transport/socketHub` at its new path;
  the store and its tests were otherwise untouched by the move.
- Performance baselines recaptured (build + browser, browser verified
  passing twice): mounting the singleton host at editor boot fetches the
  image-map implementation chunk on every editor route (+2 script
  requests, editor owned bytes +34 for the eager `hasHost` flag). Plotly
  itself stays behind the lazy plot import and no map data is fetched at
  boot — the runtime only refreshes a map the user has already opened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Review fix: the thumbnail cache and index-count state are account-owned
resources — cleared on account invalidation, with scope-guarded caching.
…at/image-map-07-marker-zoom

The base was rebased, so this branch was replayed onto its new tip and the two
stale baseline-regeneration commits from the previous catch-up merges were
dropped. These numbers come from a fresh build of the replayed tree.
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 79f59b2 to cc3029f Compare August 15, 2026 21:16
Base automatically changed from feat/image-map-07-marker-zoom to main August 15, 2026 21:36
lstein added 3 commits August 15, 2026 17:46
…d, and other users

Six findings from an adversarial review of the live-refresh work:

- A failed WebGL canvas is left alone by socket-driven refreshes. Every
  successful refresh clears `renderError`, which was designed as the user's
  deliberate retry; now that events refresh on their own, that cleared the
  error, remounted the plot into the same failure, and flickered once per
  event for the length of a backfill.
- The "has the map loaded" guard tests `loaded`/`error` rather than
  `!== 'idle'`. `loading` passed, so an event during the first fetch queued a
  rerun and forced a second full point set the moment the first settled.
- A hover preview survives a refresh. Its teardown lived in the cleanup of an
  effect keyed on `points`, so on a busy server the thumbnail vanished
  mid-hover and never returned — `Plotly.react` resets hover state and no new
  `plotly_hover` fires until the pointer moves. Staleness is now derived from
  whether the hovered image is still on the map.
- The preview flips to the other side of the cursor when it would not fit.
  It is `position: fixed`, so near the right or bottom edge it was simply
  off-screen with no way to scroll to it.
- `image_map_projection_ready` is ignored when it names another user. Admins
  receive every user's projection events, and an admin refetch usually finds
  its own all-images projection stale and enqueues another full recompute, so
  one user's activity drove UMAP fits on every admin's client. Unknown on
  either side still refreshes: single-user mode has no session user.
- The status event carries `failed`, and the footer reports it. It is the
  only thing that explains an index that settles below `total`, so a backfill
  that gave up on some images used to just stop short with no explanation.

Browser baselines regenerated: `hasHost: true` puts the widget's host runtime
in the editor's initial bundle by design (+2 requests, +6 modules), which is
the same budget change the original branch recorded before the rebase.
… editor entry

Follow-ups from a second review of the same work:

- The hover resolution no longer checks `disposed`. That flag belongs to the
  scene effect, which re-runs on every socket-driven refresh rather than only
  on unmount, so a refresh landing mid-dwell still dropped the thumbnail — the
  exact failure the previous round set out to remove, reached by another path.
  Unmount is already covered by the session bump in the cleanup.
- The connection handler refreshes only on a real disconnected -> connected
  transition. `onConnectionChange` replays the current status synchronously on
  subscribe, and this runtime is remounted by every Launchpad -> Editor
  navigation, so an already-connected socket was refetching the whole point
  set on each entry into the editor.
- The footer renders only for the `ready` state. `disabled`, `model_missing`
  and `empty` all carry data too, so "Image indexing is off" got a `0 points`
  line and a Refresh button under it, and `computing` showed a second refresh
  control beside its own "Check again".

Not fixed here: a preview hidden because its image left the map can reappear
if a later refresh restores that image while the pointer has not moved. Both
mechanisms for retiring it — clearing state from an effect, or reading a ref
during render — are rejected by the project's lint rules, and the case is too
narrow to justify a suppression in production code. Filed instead.
@lstein
lstein enabled auto-merge August 16, 2026 14:44
@lstein
lstein merged commit ebc4c27 into main Aug 16, 2026
19 checks passed
@lstein
lstein deleted the feat/image-map-08-live-theme branch August 16, 2026 15:04
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant