Skip to content

Image assets (3/3): widget config UI + settings image-cache card (replicates mxtommy/Kip #1082) - #62

Merged
mairas merged 18 commits into
mainfrom
replicate/pr-1082-image-assets-config-ui
Jul 1, 2026
Merged

Image assets (3/3): widget config UI + settings image-cache card (replicates mxtommy/Kip #1082)#62
mairas merged 18 commits into
mainfrom
replicate/pr-1082-image-assets-config-ui

Conversation

@mairas

@mairas mairas commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Replicates upstream PR mxtommy/Kip#1082 ("Image assets (3/3): widget config UI + settings image-cache card") by dillan.

This is the third part of the image-assets series: the Image widget, its image-source-setup config UI (upload, gallery, fit/background), the ImageAssetService + shared plugin-URL helper, and an image-cache card surfaced under a new Media settings tab.

Method: commit-by-commit cherry-pick of the upstream PR range (linear history, no merge commits). One commit (d3b07875, "Use takeUntilDestroyed on image-cache subscriptions") became empty after conflict resolution and was skipped, because its only change targeted the image-cache code that SKip keeps on its own side of config.component (see below).

⚠️ Conflict resolution applied

The upstream PR first adds an image-cache card directly into config.component, then a later commit (adf95687) moves that card out into a brand-new media.component Media tab. SKip's config.component is independently diverged (rewritten around profiles), so the intermediate "add card to config" states conflicted. Because the PR's net effect on config.component is zero (the card lives in the new Media tab), every conflict there was resolved by keeping SKip's version:

  • src/app/core/components/options/configuration/config.component.ts — kept SKip's profiles-based version; the cache-card logic lands in the new media.component instead.
  • src/app/core/components/options/configuration/config.component.html — kept SKip's version (no cache card in config).
  • src/app/core/components/options/configuration/config.component.scss — kept SKip's version (verified byte-identical to base).
  • src/app/core/components/options/configuration/config.component.spec.ts — kept SKip's version.
  • CHANGELOG.md — kept SKip's changelog; the upstream entry was not merged.

The new media.component, the Media tab wiring in tabs.component, the ImageAssetService, the Image widget, and the image-source-setup config UI were all applied unchanged from upstream. The resulting net diff is identical to the upstream PR except for the intentionally-preserved SKip CHANGELOG.md.

Needs a build + review before merge.

Note

This is an experimental replica carried in the fork for evaluation. No build/install/lint was run during replication; package-lock.json was not affected (the PR adds no dependencies).

dillan added 13 commits July 1, 2026 00:08
resolveKipPluginBaseUrl() extracts the <server>/plugins/kip/ resolution (mirroring
kip-series-api-client) and snapImageWidth() snaps a container width (x DPR) to the
server's variant allow-list so the client requests stable, cache-friendly URLs.

ImageAssetService wraps the plugin endpoints: upload (multipart FormData with progress),
list, delete, cacheStats, purgeCache, and urlFor(id, cssWidth, dpr) for the widget.
Requests are auto-authenticated by the JWT interceptor. 9 vitest cases green.
New widget-image renders a selected image (by id) via ImageAssetService, scaled to fit the
widget with object-fit (contain/cover) over a configurable background that defaults to
transparent so the dashboard shows through. It tracks its container width with the existing
kipResizeObserver directive and requests a server variant matched to that width (x DPR) so a
small widget never downloads a full-resolution image. Shows an empty state when nothing is
selected. Registered in WidgetService (component map + Component-category definition); adds the
`image` config block to IWidgetSvcConfig. 3 vitest cases (empty state, render+object-fit,
background default/override).
Lets users upload an image (client-side 10 MB + type guard, progress),
pick or delete from the shared library, and set the scaling, alt text,
and a background color or transparent background for the Image widget.
Wired into the widget config Display tab for widgets that expose an
image config group.
Shows the on-disk size of generated image variants and a Purge button
(with confirmation). Purging keeps originals; variants regenerate on
demand. Refresh re-reads the size after viewing images.
Splits the thumbnail into a select button and a sibling delete button so
both are keyboard-focusable and avoid an invalid nested-button structure.
Make the gallery selection a signal so the highlight updates under
zoneless change detection even when set from async upload/delete
callbacks, instead of relying on an incidental signal write. Add a
sequence guard so a slow image list() response can't overwrite a newer
one.
Add a small horizontal gap between the Refresh and Purge buttons on the
settings image-cache card (the footer is text-align:end, not flex).
The image cache card was placed on the Configurations tab, which is for KIP
configuration management (backup, restore, import, export). Move it to a new
Media tab, placed before Configurations, so the image cache lives with media
concerns rather than config management.

Extracts the image-cache logic into a SettingsMediaComponent (with its own
spec) and removes it from SettingsConfigComponent. Updates the Image widget
help doc to point at Settings → Media → Image Cache.
The Image widget had no icon in the widget picker (icon was empty). Add an
"imageWidget" landscape/image glyph to the shared SVG icon set and reference
it from the widget definition so the widget shows a recognizable icon.
Acts on a UX audit of the Image widget.

Safety / state
- Deleting an image now asks for confirmation, naming the shared, boat-wide
  scope — a single tap previously removed it from every display with no undo.
  The delete control moved to a 44px button in a footer row, out of the select
  tap zone.
- The gallery distinguishes a failed library load from a truly empty one
  (loading spinner, error + Retry), so an unreachable server no longer reads
  as "no images". The live widget shows "Image unavailable" instead of a broken
  glyph when its image disappears.
- Upload and delete share one status-aware error mapper.

Accessibility
- Thumbnails expose selection (aria-pressed) and group semantics, visible
  keyboard focus, a named delete label, and a live region announces
  upload/delete status. "Alt text" is now "Description" with a hint; thumbnails
  are captioned with the image name and sorted newest-first.

Clarity / theming
- Plainer copy throughout (errors, the Fill option, the cache card's
  "Clear cache"). Touch-safe color control. The opaque background seeds from
  the active theme instead of hardcoded black, transparent diagrams sit on a
  neutral checkerboard, and a live preview shows the chosen fit/background.

Updates the component specs (delete confirmation, 401 mapping, gallery error
state, newest-first sort, theme-seeded background).
Relabels the help text to match the new UI ("Alt text" -> "Description",
"Purge" -> "Clear cache") and refreshes the screenshots: the redesigned
config dialog (shared "Boat image library" with thumbnails, captions and
footer delete) and the Media -> Image Cache card showing a real cache size.

Also points the help menu at the renamed image-widget.md and gitignores the
Playwright MCP working dir.
Spins up an open (no-login) Signal K server with the integrated KIP image
plugin + webapp, seeded with realistic marine diagrams, so the Image widget
help screenshots (gallery, thumbnails, real cache size) can be reliably
re-captured as the UI evolves.

The image feature spans three branches (server/client/config-ui); the README
documents merging the server branch and `npm pack` to produce the kip.tgz the
Dockerfile installs (which resolves native deps like sharp for the container).
@mairas mairas added the upstream-replica Replica of an upstream mxtommy/Kip PR label Jun 30, 2026
mairas and others added 2 commits July 1, 2026 15:31
main replaced static widget imports with a lazy _componentTypeMap
resolved by getComponentType(). Re-expressed the Image widget
registration as a lazy import() entry instead of the eager static
import + reference; the _widgetDefinition entry (componentClassName
WidgetImageComponent, selector widget-image) is unchanged.

Verified: lint clean, dev build succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
…etup

Merging the lazy-widget-registry main exposed a latent circular import:
RootModalWidgetConfigComponent (imports ImageSourceSetupComponent) →
ImageSourceSetupComponent (injected DialogService) → DialogService
(imports RootModalWidgetConfigComponent to open it). In the full test
bundle this left ImageSourceSetupComponent undefined when root-modal's
standalone imports compiled (TypeError reading 'ɵcmp'); it passed in
isolation, only surfacing with the whole suite's load order.

image-source-setup used DialogService solely for a delete confirmation.
Open DialogConfirmationComponent through MatDialog directly instead,
dropping the DialogService import and cutting the cycle; sibling config
components already avoid DialogService for the same reason. Spec updated
to mock MatDialog.

Verified: lint clean, dev build succeeds, full vitest suite 576/576.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
@mairas

mairas commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Content review: accept-with-fixes — clean, idiomatic client; one real reactivity bug

Read the widget, config sub-component, ImageAssetService, and URL util across security and correctness lenses.

Genuinely solid (verified):

  • Client-security is clean — I couldn't construct a script-execution path. Every untrusted server field (asset name, error strings, cache stats) lands in an auto-escaped {{ }}/attribute context; no [innerHTML], bypassSecurityTrust*, DomSanitizer, [href], or srcdoc. The server-supplied image URL field is deliberately ignored and ids are re-encoded before use.
  • Subscription hygiene is complete: every subscription (service endpoint, upload, delete dialog+delete, gallery refresh, cache stats, purge, modal statusChanges) uses takeUntilDestroyed; the gallery-refresh race is handled with a monotonic galleryRequestSeq that drops stale responses.

Findings:

[medium · correctness] Image widget can stay blank after cold bootwidget-image.component.ts:47-51 + image-asset.service.ts. imageUrl is a computed() tracking imageConfig()/containerWidth(), but it calls images.urlFor(id,w) which reads the plain, non-signal pluginBaseUrl, populated asynchronously in the service constructor's serverServiceEndpoint$ subscription. If the ResizeObserver fires before the endpoint resolves (cold boot), the computed pins and never re-evaluates when pluginBaseUrl becomes valid → the widget shows empty state indefinitely until the crew resizes/re-opens it. Make pluginBaseUrl a signal so the computed re-runs.

[medium → low · marine-safety] "No image selected" shown when an image IS configured but not yet loadable — the else-branch conflates "unconfigured" with "configured but urlFor returned null because pluginBaseUrl isn't ready." On a safety display, telling the operator nothing is configured when a stowage/through-hull diagram is configured is misleading. Verified real. Distinguish the two states.

Plus low: an initial 0-width container snaps to the 2560 px variant → a full-res fetch then an immediate re-fetch at the real size.

Bottom line: well-built, idiomatic zoneless-Angular with correct subscription and race handling; the reactivity bug is the real one (widget blank after boot until interaction). Adopt together with #54 — this client is inert without the server plugin.

(Resolution applied earlier: Image widget → main's lazy registry, and broke a DialogService import cycle the merge exposed by opening the confirmation dialog via MatDialog directly. Full vitest suite 576/576; CI green.)

… review)

- ImageAssetService.pluginBaseUrl is now a signal, so the Image widget's
  imageUrl computed re-runs once the SK endpoint resolves after first paint,
  instead of staying blank until the widget is resized or reopened.
- The widget distinguishes "no image configured" from "configured but not
  loadable yet": the latter shows "Loading image…" rather than the
  misleading "No image selected" on a safety display.
- snapImageWidth snaps an unknown/zero container width to the SMALLEST
  variant, not the largest, so the initial 0-width render no longer fetches
  the full-resolution image and immediately re-fetches at the real size.

Tests: reactive recompute + loading state (widget), reactive base URL
(service), smallest-for-zero-width (util). Full vitest suite 579/579.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
@mairas

mairas commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Fixes pushed

Addresses the correctness + empty-state findings:

  • Blank-after-boot (reactivity): ImageAssetService.pluginBaseUrl is now a signal, so the widget's imageUrl computed re-runs once the SK endpoint resolves after first paint, instead of staying blank until resize/reopen.
  • Empty-state: the widget distinguishes "no image configured" from "configured but not yet loadable" — the latter shows "Loading image…" rather than the misleading "No image selected".
  • 0-width variant: snapImageWidth snaps an unknown/zero width to the smallest variant, not the largest, so the initial render no longer fetches the full-res image then immediately re-fetches.

Tests: reactive recompute + loading state (widget), reactive base URL (service), smallest-for-zero-width (util). Full vitest suite 579/579; CI green.

mairas and others added 2 commits July 1, 2026 20:53
The Image widget help doc used parent-relative (../../assets/...) image
paths, which the help-docs path guard (#1056, now on main) correctly
rejects — they don't resolve under the app's base href. Make them
base-relative like the other help docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
@mairas
mairas merged commit 49b4b19 into main Jul 1, 2026
13 checks passed
mairas added a commit that referenced this pull request Jul 1, 2026
Resolves the additive conflicts now that #62 (image) is on main: both
features register in widget.service.ts (Image + Video lazy entries), add
their config component to root-modal-widget-config, extend
widgets-interface, and add help-docs menu + gitignore lines — kept both
sides. Deps union (hls.js/piexifjs + sharp/heic/multer/dompurify);
lockfile regenerated. Fixed the video help-doc images to base-relative
paths (the merged #1056 guard rejects ../).

Verified: lint clean, dev build succeeds, full vitest suite 748/748.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

upstream-replica Replica of an upstream mxtommy/Kip PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants