feat(push): plumb hostname-aware prefix into Web Push notifications

Closes the Web Push gap left by #82: in-page Notification API and tab
title flash both showed `codeman:<host>` after that PR, but OS-level
notifications dispatched via the service worker — the surface that
matters most when the tab is closed and the user is reading their
system notification center across multiple Codeman instances —
still hardcoded the literal "Codeman" prefix.

Service workers run in an isolated context with no access to
document.title or any in-page state, so the hostname has to ride
along in the push payload itself.

Server (server.ts:sendPushNotifications): emit `hostTitle: this.windowTitle`
in the JSON payload alongside the existing `title` (event-specific text
like "Permission Required"). The two stay separate so the SW can compose
them — the server knows the host, the SW knows the OS context.

Service worker (sw.js): compose `${hostTitle}: ${title}` when both
present, mirroring the in-page Notification format from
notification-manager.js. Fall back to `title || hostTitle || 'Codeman'`
so older servers (which omit hostTitle) keep working — the field is
purely additive on the wire.

Tests (test/push-payload-host-title.test.ts): mock the `web-push` module
via vi.hoisted(), instantiate WebServer without binding a port, stub
the push store with one fake subscription, and verify the JSON payload
shipped to webpush.sendNotification carries the right hostTitle for
both --title-hostname overrides and the os.hostname() default. Also
mirrors the SW's title-composition logic in a small helper so any
future change to the format breaks the test instead of being caught
only by users running multiple Codeman instances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-05-12 10:03:59 +02:00
co-authored by Claude Opus 4.7
parent 7beec7194a
commit 56c2c29009
3 changed files with 178 additions and 2 deletions
+9 -2
View File
@@ -110,7 +110,7 @@ self.addEventListener('push', (event) => {
return;
}
const { title, body, tag, sessionId, urgency, actions } = payload;
const { title, hostTitle, body, tag, sessionId, urgency, actions } = payload;
const options = {
body: body || '',
@@ -126,8 +126,15 @@ self.addEventListener('push', (event) => {
options.actions = actions;
}
// Match the in-page Notification format: "codeman:<host>: <event title>".
// hostTitle is sent by servers >= the hostname-aware push payload change;
// older servers omit it and we fall back to the bare title.
const displayTitle = hostTitle && title
? `${hostTitle}: ${title}`
: (title || hostTitle || 'Codeman');
event.waitUntil(
self.registration.showNotification(title || 'Codeman', options)
self.registration.showNotification(displayTitle, options)
);
});
+4
View File
@@ -1437,6 +1437,10 @@ export class WebServer extends EventEmitter {
const payload = JSON.stringify({
title: template.title,
// Hostname-aware prefix so OS-level notifications from multiple Codeman
// instances (laptop / dev box / NAS) are unambiguous in the system tray.
// Mirrors the in-page Notification format in notification-manager.js.
hostTitle: this.windowTitle,
body,
tag: `codeman-${event}-${sessionId}`,
sessionId,