Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# v4.9.0
## New Features
* Image Widget: Display a picture from a shared, boat-wide library on your dashboards — a safety-equipment diagram, electrical panel layout, or boat plan. Upload JPG, PNG, WebP, GIF (including animated), HEIC/HEIF, or SVG (up to 10 MB) directly from the KIP UI; images are stored on the Signal K server and shared across all displays. Pictures scale to fit or fill their widget while preserving aspect ratio, over a solid or transparent background. Uploaded content is validated, raster images are re-encoded to WebP and SVGs are sanitized for safety, and resized copies are generated on demand and cached on disk. A new **Image Cache** card in Settings shows the on-disk cache size and lets you purge generated copies. Uploading requires logging in to the Signal K server.
# v4.8.0
## New Features
* Solar Charger Widget: Get instant clarity on your solar system with a compact, purpose-built Solar Charger Widget. Track individual panels or full arrays in real time, including State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature. Device discovery is automatic, and Zones support keeps warnings and alarms state highly visible.
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