Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
be81b52
Add ImageAssetService + shared plugin-URL helper (image assets client)
dillan Jun 24, 2026
991d060
Add Image widget that displays a server-stored image asset
dillan Jun 24, 2026
366da38
Add image-source-setup widget config (upload, gallery, fit/background)
dillan Jun 24, 2026
5e9be23
Add image cache card to settings (size + purge)
dillan Jun 24, 2026
f84fd43
Add Image widget help doc and changelog entry
dillan Jun 24, 2026
fe7be6f
Make gallery thumbnail select/delete proper buttons (a11y)
dillan Jun 24, 2026
72aeeca
Harden image config reactivity (signal highlight + refresh race guard)
dillan Jun 24, 2026
8f7d816
Space out the image-cache option buttons
dillan Jun 24, 2026
1200a73
Move the image cache card to a new Media settings tab
dillan Jun 27, 2026
1bbcbeb
Add an icon for the Image widget
dillan Jun 27, 2026
9a224f0
Image widget UX: confirm shared-library deletes, a11y, plainer copy
dillan Jun 27, 2026
20b37b0
Refresh image widget help doc + screenshots for the redesigned UI
dillan Jun 27, 2026
f20cfef
Add a reproducible Docker harness for image-widget screenshots
dillan Jun 27, 2026
6f8f521
Merge main into image-assets-config-ui; adapt to lazy widget registry
mairas Jul 1, 2026
e9598a6
fix(image-widget): break DialogService import cycle in image-source-s…
mairas Jul 1, 2026
713ae61
fix(image-widget): reactive plugin URL + clearer empty state (content…
mairas Jul 1, 2026
a63a82a
Merge branch 'main' into replicate/pr-1082-image-assets-config-ui
mairas Jul 1, 2026
9ae35a3
fix(help-docs): base-relative image paths in image-widget.md
mairas Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ yarn-error.log
/e2e/*.js
/e2e/*.map

# Playwright MCP working dir
.playwright-mcp/

# System Files
.DS_Store
Thumbs.db
Expand Down
1 change: 1 addition & 0 deletions e2e/image-screenshots/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kip.tgz
10 changes: 10 additions & 0 deletions e2e/image-screenshots/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Reproducible Signal K harness for capturing KIP image-widget screenshots.
# Bakes the (integrated) KIP plugin + webapp and a security-OFF config into the image so the
# image library, thumbnails and on-disk cache all work without a login. No VOLUME on the official
# image's config dir, so the bake survives at runtime.
FROM signalk/signalk-server:latest
USER node
COPY --chown=node:node signalk-config/ /home/node/.signalk/
COPY --chown=node:node kip.tgz /tmp/kip.tgz
# Installs @mxtommy/kip (webapp + plugin) and its native deps (sharp) for THIS platform/arch.
RUN cd /home/node/.signalk && npm install --no-audit --no-fund --omit=dev /tmp/kip.tgz && rm /tmp/kip.tgz
37 changes: 37 additions & 0 deletions e2e/image-screenshots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# KIP image-widget screenshot harness

Reproducible, **open** (no-login) Signal K + KIP stack with the `kip` image plugin, seeded with
realistic marine diagrams, for capturing the Image widget help screenshots.

## Why it exists
The image widget is split across three PRs/branches:
`feat/image-assets-server` (1080, the plugin), `feat/image-assets-client` (1081),
`feat/image-assets-config-ui` (1082, the webapp/config UI). No single branch runs end to end, so this
harness builds the **integrated** package and runs it in a throwaway container.

## Build the package (kip.tgz)
From a clone of KIP, integrate the server branch with the UI branch and pack:
```
git checkout feat/image-assets-config-ui
git checkout -b tmp/image-harness
git merge --no-ff feat/image-assets-server
npm install && npm run build:all # build:plugin + build:prod
npm pack # -> mxtommy-kip-<v>.tgz
cp mxtommy-kip-*.tgz <this-dir>/kip.tgz
```
`npm pack` ships `plugin/**` + `public/**`; the Dockerfile's `npm install` then resolves native deps
(`sharp`) for the container's platform.

## Run
```
./run.sh # build image, start on :3015, seed sample images + warm the cache
./run.sh --down # stop + remove
```
- KIP webapp: http://localhost:3015/@mxtommy/kip
- Image cache: http://localhost:3015/plugins/kip/images/cache

Security is OFF (open) so the gallery, thumbnails and cache work without a login. Do not expose it.

## Updating screenshots as the UI evolves
Rebuild `kip.tgz` (above) and `./run.sh` again, then re-capture. Sample images live in
`sample-images/` (SVG diagrams + PNG rasters; the PNGs populate the raster cache).
10 changes: 10 additions & 0 deletions e2e/image-screenshots/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: kip-image-shots
services:
kip:
build: .
container_name: kip-image-shots
# The base image's startup forces --securityenabled; run signalk-server directly for an OPEN
# local server (do not expose this). With no `security` in settings.json, the server is open.
entrypoint: ['node', '/home/node/signalk/node_modules/signalk-server/bin/signalk-server']
ports:
- '3015:3000'
22 changes: 22 additions & 0 deletions e2e/image-screenshots/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Brings up the KIP image-widget screenshot harness and seeds it.
# Prereq: kip.tgz built from the INTEGRATED image feature (see README) sitting next to this script.
# ./run.sh build + start + seed
# ./run.sh --down stop and remove
set -euo pipefail
cd "$(dirname "$0")"
if [[ "${1:-}" == "--down" ]]; then docker compose down -v; exit 0; fi
[[ -f kip.tgz ]] || { echo "!! kip.tgz missing — see README to build it from the integrated branches."; exit 1; }
echo "==> Building + starting (open Signal K on :3015 with KIP + the kip image plugin)"
docker compose up -d --build
echo "==> Waiting for Signal K"
for i in $(seq 1 60); do curl -fsS http://localhost:3015/signalk >/dev/null 2>&1 && break; sleep 2; done
echo "==> Seeding sample images"
./seed.sh
cat <<EOF

Ready.
KIP webapp: http://localhost:3015/@mxtommy/kip
Image cache: http://localhost:3015/plugins/kip/images/cache
Stop: ./run.sh --down
EOF
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions e2e/image-screenshots/sample-images/dc-electrical-panel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions e2e/image-screenshots/sample-images/safety-equipment-plan.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions e2e/image-screenshots/sample-images/seacock-through-hull-plan.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions e2e/image-screenshots/seed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Uploads the realistic sample marine diagrams into the shared image library, then warms the cache
# (raster variants) so the Media → Image Cache card shows a real on-disk size.
set -euo pipefail
cd "$(dirname "$0")"
BASE="${KIP_SK_URL:-http://localhost:3015}"
upload() { # file mime
curl -fsS -X POST "$BASE/plugins/kip/images" -F "file=@${1};type=${2};filename=$(basename "$1")" >/dev/null \
&& echo " uploaded: $(basename "$1")" || echo " FAILED: $(basename "$1")"
}
for f in sample-images/*.svg; do [ -e "$f" ] && upload "$f" "image/svg+xml"; done
for f in sample-images/*.png; do [ -e "$f" ] && upload "$f" "image/png"; done
echo "Warming the cache (raster variants)…"
curl -fsS "$BASE/plugins/kip/images" | python3 -c "import sys,json;[print(i['id'],i['format']) for i in json.load(sys.stdin)]" | while read -r id fmt; do
[ "$fmt" = "svg" ] && continue
for w in 160 320 640; do curl -fsS -o /dev/null "$BASE/plugins/kip/images/${id}?w=${w}" || true; done
done
echo "Done. Cache: $(curl -fsS "$BASE/plugins/kip/images/cache")"
1 change: 1 addition & 0 deletions e2e/image-screenshots/signalk-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "name": "kip-screenshot-config", "version": "1.0.0", "private": true, "description": "Throwaway Signal K config for KIP image-widget screenshots" }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "enabled": true, "configuration": {} }
5 changes: 5 additions & 0 deletions e2e/image-screenshots/signalk-config/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"interfaces": {},
"pipedProviders": [],
"vessel": { "name": "Test Vessel" }
}
35 changes: 35 additions & 0 deletions src/app/core/components/options/media/media.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<div class="page-content">
<div class="flex-container">
<div class="flex-item-rounded-card rounded-card-color">
<h3>Image Cache</h3>
<p style="margin-bottom: 16px">
KIP keeps smaller copies of your images so they load quickly. Clearing frees
space on the Signal K server — your originals are kept and the smaller copies
are recreated when needed.
</p>
<div class="image-cache-size">
<span class="image-cache-label">Cache size:</span>
<span class="image-cache-value">{{ imageCacheDisplay() }}</span>
</div>
<div class="formActionFooter image-cache-actions">
<mat-divider class="formActionDivider"></mat-divider>
<button
mat-stroked-button
type="button"
(click)="refreshImageCache()"
>
Refresh
</button>
<button
mat-flat-button
type="button"
color="accent"
[disabled]="imageCachePurging() || imageCacheStats() === null"
(click)="purgeImageCache()"
>
Clear cache
</button>
</div>
</div>
</div>
</div>
36 changes: 36 additions & 0 deletions src/app/core/components/options/media/media.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
:host {
display: block;
height: 100%;
width: 100%;
}

.page-content {
width: 100%;
overflow-y: auto;
scroll-behavior: smooth;
padding: 0px 0px 10px 0px;
}

h3 {
margin-top: 5px;
}

.image-cache-size {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 8px;
}

.image-cache-label {
opacity: 0.7;
}

.image-cache-value {
font-weight: 600;
}

// Horizontal gap between the Refresh and Purge buttons (footer is text-align:end, not flex).
.image-cache-actions button + button {
margin-left: 8px;
}
87 changes: 87 additions & 0 deletions src/app/core/components/options/media/media.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SettingsMediaComponent } from './media.component';
import { ToastService } from '../../../services/toast.service';
import { ImageAssetService } from '../../../services/image-asset.service';
import { DialogService } from '../../../services/dialog.service';

describe('SettingsMediaComponent', () => {
let component: SettingsMediaComponent;
let fixture: ComponentFixture<SettingsMediaComponent>;
let toastMock: {
show: ReturnType<typeof vi.fn>;
};
let imagesMock: {
ready: boolean;
cacheStats: ReturnType<typeof vi.fn>;
purgeCache: ReturnType<typeof vi.fn>;
};
let dialogMock: {
openConfirmationDialog: ReturnType<typeof vi.fn>;
};

beforeEach(async () => {
toastMock = {
show: vi.fn()
};
imagesMock = {
ready: true,
cacheStats: vi.fn(() => of({ bytes: 1048576, files: 3 })),
purgeCache: vi.fn(() => of({ ok: true }))
};
dialogMock = {
openConfirmationDialog: vi.fn(() => of(true))
};

await TestBed.configureTestingModule({
imports: [SettingsMediaComponent],
providers: [
{ provide: ToastService, useValue: toastMock },
{ provide: ImageAssetService, useValue: imagesMock },
{ provide: DialogService, useValue: dialogMock }
]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(SettingsMediaComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should be created', () => {
expect(component).toBeTruthy();
});

it('loads and formats the image cache size on init', () => {
expect(imagesMock.cacheStats).toHaveBeenCalled();
const api = component as unknown as { imageCacheDisplay: () => string };
expect(api.imageCacheDisplay()).toBe('1.0 MB · 3 files');
});

it('shows Unavailable when the image service is not ready', () => {
imagesMock.ready = false;
component.refreshImageCache();
const api = component as unknown as { imageCacheDisplay: () => string };
expect(api.imageCacheDisplay()).toBe('Unavailable');
});

it('purges the image cache after confirmation and refreshes', () => {
imagesMock.cacheStats.mockClear();
component.purgeImageCache();

expect(dialogMock.openConfirmationDialog).toHaveBeenCalled();
expect(imagesMock.purgeCache).toHaveBeenCalled();
expect(imagesMock.cacheStats).toHaveBeenCalled();
expect(toastMock.show).toHaveBeenCalledWith('Cache cleared', 1000, true, 'success');
});

it('does not purge when the confirmation is declined', () => {
dialogMock.openConfirmationDialog.mockReturnValueOnce(of(false));
component.purgeImageCache();

expect(imagesMock.purgeCache).not.toHaveBeenCalled();
});
});
Loading
Loading