Skip to content
Open
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. The images are served by the **SK Image** Signal K plugin (Node.js 22.13+), which KIP recommends and the App Store offers to install. Uploading requires a Signal K account with write access.
# v4.8.5
## Improvements
* New Widgets help documentation section and AIS Radar Widget help documentation.
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
18 changes: 18 additions & 0 deletions e2e/image-screenshots/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Reproducible Signal K harness for capturing KIP image-widget screenshots.
#
# Installs the STANDALONE sk-image plugin plus the KIP webapp into a security-OFF Signal K server, so
# the image library, thumbnails and on-disk cache all work without a login. No VOLUME on the config
# dir, so the bake survives at runtime.
#
# sk-image requires Node.js 22.13+. Recent signalk-server images run Node 22+, so `latest` normally
# satisfies this; if a pinned/older base image lags below 22.13, install onto a node:22 (or newer)
# base with signalk-server instead.
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
# Install the standalone image plugin (sk-image) and the KIP webapp (this repo, packed to kip.tgz).
# sk-image ships a prebuilt sharp; --omit=dev keeps the image lean.
RUN cd /home/node/.signalk \
&& npm install --no-audit --no-fund --omit=dev sk-image /tmp/kip.tgz \
&& rm /tmp/kip.tgz
66 changes: 66 additions & 0 deletions e2e/image-screenshots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# KIP image-widget screenshot harness

A reproducible Signal K + KIP stack for capturing the Image widget help screenshots and for
verifying the widget against the **real, standalone** image plugin.

## Architecture (post-split)

Image storage/processing is no longer part of KIP. It lives in the standalone **`sk-image`** Signal K
plugin (published on npm, in the App Store). KIP ships only the display widget + config UI, which talk
to the plugin's crew-reachable REST API at **`/signalk/v1/api/sk-image`**. So this harness installs two
separate packages into a Signal K server:

- **`sk-image`** — the image plugin. **Requires Node.js 22.13+** on the server.
- **`@mxtommy/kip`** — the webapp (this repo), served at `/@mxtommy/kip`.

## Build the KIP webapp package (kip.tgz)

From a clone of KIP:

```
npm install && npm run build:prod # -> public/
npm pack # -> mxtommy-kip-<v>.tgz (ships public/**)
cp mxtommy-kip-*.tgz e2e/image-screenshots/kip.tgz
```

`npm pack` ships `public/**`; the container installs `sk-image` alongside it and resolves the plugin's
native dep (`sharp`) for the container's platform.

## Run (open server — screenshots)

```
./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 library (public read): http://localhost:3015/signalk/v1/api/sk-image/images
- Image cache: http://localhost:3015/signalk/v1/api/sk-image/images/cache

Security is OFF (open), so the gallery, thumbnails, upload and cache all work without a login. Do not
expose it. Sample images live in `sample-images/` (SVG diagrams + PNG rasters; the PNGs populate the
raster cache).

## Verify on a SECURED server (role behavior)

The widget's important behavior only shows with server security ON. To check it, enable security on the
server (`signalk-server` admin UI → Security, or add a `security` block to `settings.json`) and create
two accounts — one **admin/read-write**, one **read-only** — then confirm, in KIP pointed at the server:

- **Anonymous / read-only crew can VIEW** images (the widget renders; the config gallery lists them),
because reads on `/signalk/v1/api/sk-image` are public. This is the reason KIP must target that mount
and not the admin-gated `/plugins/sk-image` alias.
- A **read-only** account gets a clear "your account is read-only" message on upload/delete (HTTP 403),
not a "check your connection" error.
- An **admin / read-write** account can upload, delete, and purge the cache.
- Adding the Image widget as non-admin crew does **not** dead-end with a false "plugin not installed"
prompt (the plugin-state API is admin-only, so KIP treats an unreadable state as "can't verify").

> Note: this secured, multi-role flow is a manual verification. The `run.sh`/compose here automate only
> the open (screenshot) server; a fully automated secured e2e with seeded accounts is a possible
> follow-up.

## Updating screenshots as the UI evolves

Rebuild `kip.tgz` (above) and `./run.sh` again, then re-capture the Image widget's Add panel, options
dialog, and the Settings → Media → Image Cache card into `src/assets/help-docs/img/`.
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'
24 changes: 24 additions & 0 deletions e2e/image-screenshots/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Brings up the KIP image-widget screenshot harness and seeds it.
# Prereq: kip.tgz (the packed KIP webapp — `npm run build:prod && npm pack`, see README) next to
# this script. The Dockerfile installs the standalone sk-image plugin from npm alongside it.
# ./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 pack the KIP webapp."; exit 1; }
echo "==> Building + starting (open Signal K on :3015 with the KIP webapp + the standalone sk-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 library: http://localhost:3015/signalk/v1/api/sk-image/images
Image cache: http://localhost:3015/signalk/v1/api/sk-image/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/signalk/v1/api/sk-image/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/signalk/v1/api/sk-image/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/signalk/v1/api/sk-image/images/${id}?w=${w}" || true; done
done
echo "Done. Cache: $(curl -fsS "$BASE/signalk/v1/api/sk-image/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": {} }
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" }
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mxtommy/kip",
"version": "4.8.5",
"version": "4.9.0",
"description": "An advanced and versatile marine instrumentation package to display Signal K data.",
"license": "MIT",
"author": {
Expand Down Expand Up @@ -59,7 +59,8 @@
"signalk-tides",
"@meri-imperiumi/signalk-autostate",
"signalk-venus-plugin",
"signalk-victron-battery-monitor"
"signalk-victron-battery-monitor",
"sk-image"
]
},
"main": "plugin/index.js",
Expand Down
17 changes: 17 additions & 0 deletions src/app/core/components/dashboard/dashboard.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface DashboardComponentPrivateApi {
nextDashboard: () => void;
previousDashboard: () => void;
loadDashboard: (dashboardId: number) => void;
getDisabledRequiredPlugins: (requiredPlugins: string[]) => Promise<string[]>;
_gridstack: () => {
grid: {
save: (saveContent: boolean, saveGridOpt: boolean) => unknown;
Expand Down Expand Up @@ -134,6 +135,22 @@ describe('DashboardComponent', () => {
expect(component).toBeTruthy();
});

it('does not treat a required plugin as disabled when its state cannot be read (secured, non-admin)', async () => {
const pluginConfig = TestBed.inject(PluginConfigClientService) as unknown as { getPlugin: Mock };
pluginConfig.getPlugin.mockResolvedValue({ ok: false, error: { reason: 'forbidden' } });

// A 401/403 from the admin-only plugin-state API must not block the add — ordinary crew
// would be dead-ended with an "enable" prompt they have no permission to satisfy.
expect(await privateApi.getDisabledRequiredPlugins(['sk-image'])).toEqual([]);
});

it('treats an installed-but-disabled required plugin as disabled', async () => {
const pluginConfig = TestBed.inject(PluginConfigClientService) as unknown as { getPlugin: Mock };
pluginConfig.getPlugin.mockResolvedValue({ ok: true, data: { state: { enabled: false } } });

expect(await privateApi.getDisabledRequiredPlugins(['sk-image'])).toEqual(['sk-image']);
});

it('should save dashboard configuration', () => {
privateApi.saveDashboard();

Expand Down
17 changes: 12 additions & 5 deletions src/app/core/components/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,15 +396,22 @@ export class DashboardComponent implements AfterViewInit, OnDestroy {
const statusList = await Promise.all(
uniqueRequiredPlugins.map(async pluginId => {
const result = await this._pluginConfig.getPlugin(pluginId);
return {
pluginId,
enabled: result.ok && result.data.state.enabled
};
if (result.ok) {
return { pluginId, block: !result.data.state.enabled };
}
// The plugin-state API (/plugins/{id}) is admin-only on a secured server. When the caller
// lacks the rights to read it (auth-required/forbidden) we cannot tell whether the plugin is
// installed or enabled, so we must not block the add — the widget's own endpoints are
// crew-reachable and will render (or surface an install hint) on their own. Blocking here
// would dead-end ordinary crew with an "enable" prompt they have no permission to satisfy.
const cannotVerify =
result.error.reason === 'auth-required' || result.error.reason === 'forbidden';
return { pluginId, block: !cannotVerify };
})
);

return statusList
.filter(pluginStatus => !pluginStatus.enabled)
.filter(pluginStatus => pluginStatus.block)
.map(pluginStatus => pluginStatus.pluginId);
}

Expand Down
Loading
Loading