@calebcall/camera-ui-notify 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +45 -4
- package/bundle.zip +0 -0
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,54 @@ All notable changes to **Notify** are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.8.0] - 2026-08-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **No more duplicate notifications for AI descriptions.** camera.ui announces a detection, then
|
|
15
|
+
republishes the same notification once the AI description is ready — same `tag`, plus
|
|
16
|
+
`silent: true` to mark it as an update rather than a new alert. Both fields were previously
|
|
17
|
+
ignored, so every detection arrived twice, both times with sound. Now:
|
|
18
|
+
- **Telegram** and **Discord** replace the original message in place (`editMessageText` /
|
|
19
|
+
`editMessageCaption`, `PATCH .../messages/{id}`), so one notification's text improves rather
|
|
20
|
+
than a second one appearing. Message ids are held in memory per tag for 15 minutes; after a
|
|
21
|
+
restart, or if the original was deleted, the update falls back to a new quiet message. Only
|
|
22
|
+
the `silent` follow-up replaces — detection tags repeat across events, so a new alert reusing
|
|
23
|
+
a tag always posts a new message rather than rewriting chat history.
|
|
24
|
+
- **ntfy** (priority `1`), **Gotify** (priority `3`) and **Pushover** (priority `-1`) deliver the
|
|
25
|
+
update without sound or vibration.
|
|
26
|
+
- **Grafana** resolves the update onto the event it already opened: annotations mode patches the
|
|
27
|
+
existing annotation's text, Alertmanager and IRM modes re-send under the same fingerprint so
|
|
28
|
+
the alert updates instead of a second one firing.
|
|
29
|
+
- The **generic webhook** payload forwards the new `silent` field alongside the existing `tag`.
|
|
30
|
+
- `critical` notifications ignore `silent` and always alert, per the SDK contract.
|
|
31
|
+
|
|
32
|
+
- **README no longer documents `grafana_irm_ttl`.** The field was removed from the code in 0.7.1
|
|
33
|
+
when IRM turned out to ignore `endsAt`, but its row survived in the config table — directly
|
|
34
|
+
contradicting the paragraph below it stating that IRM groups do not auto-resolve.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **"Follow-up updates" setting** (`silent_updates`) — `Deliver quietly (no sound)` (default) or
|
|
39
|
+
`Skip the update entirely`, for anyone who wants exactly one notification per event on the
|
|
40
|
+
backends that can't replace a delivered message. Backends that replace in place still receive
|
|
41
|
+
the update under `skip`, since it adds no entry to the notification list.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **camera.ui Go SDK bumped to v1.2.22** (from v1.2.6) for `Notification.Silent`.
|
|
46
|
+
- **Linting consolidated on golangci-lint**, configured by a new `.golangci.yml`. `npm run lint`
|
|
47
|
+
previously chained `staticcheck && golangci-lint run`, so a staticcheck finding short-circuited
|
|
48
|
+
the `&&` and golangci-lint never ran at all. golangci-lint already bundles staticcheck's
|
|
49
|
+
analyzers, so the standalone binary is gone from the script; the config excludes three
|
|
50
|
+
deliberate idioms (`defer resp.Body.Close()`, and nil-`Context` / De Morgan findings in tests)
|
|
51
|
+
and `npm run lint` now reports zero issues.
|
|
52
|
+
- **`package.json` description now lists every backend.** It had said "ntfy, Gotify, or a generic
|
|
53
|
+
webhook" since 0.4.0, omitting Pushover, Telegram, Discord and Grafana. This is the text npm
|
|
54
|
+
shows on the package listing. ([#29](https://github.com/calebcall/camera-ui-notify/issues/29))
|
|
55
|
+
|
|
8
56
|
## [0.7.1] - 2026-08-02
|
|
9
57
|
|
|
10
58
|
### Fixed
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Delivers to any HTTP endpoint you provide — the fallback for anything without
|
|
|
69
69
|
| `headerName` | no | — | Optional custom header name (e.g. for a shared secret). Requires `headerValue` if set. |
|
|
70
70
|
| `headerValue` | no | — | Value of the custom header. Requires `headerName` if set. |
|
|
71
71
|
|
|
72
|
-
Delivery: `{method} {url}` with `Content-Type: application/json` and (if configured) the custom header, carrying a JSON body of `{title, subtitle, body, severity, tag, imageUrl, deepLink, data, createdAt, thumbnailBase64}`.
|
|
72
|
+
Delivery: `{method} {url}` with `Content-Type: application/json` and (if configured) the custom header, carrying a JSON body of `{title, subtitle, body, severity, tag, silent, imageUrl, deepLink, data, createdAt, thumbnailBase64}`.
|
|
73
73
|
|
|
74
74
|
### Pushover
|
|
75
75
|
|
|
@@ -121,7 +121,6 @@ different services, so each has its own connection fields.
|
|
|
121
121
|
| `grafana_alertname` | no | alertmanager | `alertname` label. Defaults to `CameraUINotification`. |
|
|
122
122
|
| `grafana_ttl` | no | alertmanager | Seconds before Alertmanager auto-resolves the alert. Default `900`, minimum `30`. |
|
|
123
123
|
| `grafana_irm_url` | yes | irm | Inbound webhook URL of an IRM / OnCall integration. The token is in the URL, so it is masked and kept out of every error message. |
|
|
124
|
-
| `grafana_irm_ttl` | no | irm | Seconds before the alert is eligible to auto-resolve. Default `300`, minimum `30`. Whether IRM acts on it depends on the integration's templates. |
|
|
125
124
|
|
|
126
125
|
**Annotations** — `POST {server}/api/annotations` with a point-in-time, organization-wide
|
|
127
126
|
annotation tagged `camera.ui`, `camera:<name>`, `severity:<level>`, plus your extra tags. Surface it
|
|
@@ -208,6 +207,32 @@ timer and per-event state, and a restart would strand the group open anyway. Clo
|
|
|
208
207
|
> **Secrets in logs:** transport failures never log the bot token / webhook URL / other
|
|
209
208
|
> URL-embedded secret — request URLs are redacted from delivery errors.
|
|
210
209
|
|
|
210
|
+
## Follow-up updates (AI descriptions)
|
|
211
|
+
|
|
212
|
+
camera.ui announces a detection immediately, then republishes the same notification a few seconds later once the AI description is ready. Both publishes carry the same `tag` (the collapse key), and the second carries `silent: true`, meaning *this only updates the first one — don't alert again*.
|
|
213
|
+
|
|
214
|
+
Handled per backend, according to what each platform can actually do:
|
|
215
|
+
|
|
216
|
+
| Backend | Follow-up behaviour |
|
|
217
|
+
| --------------- | ------------------------------------------------------------------------------------ |
|
|
218
|
+
| Telegram | **Replaces** the original message (`editMessageText` / `editMessageCaption`). |
|
|
219
|
+
| Discord | **Replaces** the original message (`PATCH .../messages/{id}`). |
|
|
220
|
+
| Grafana | **Revises** the record it already filed — see below. |
|
|
221
|
+
| ntfy | Delivered at priority `1` — visible, no sound or vibration. |
|
|
222
|
+
| Gotify | Delivered at priority `3` — joins the in-app list, raises no system notification. |
|
|
223
|
+
| Pushover | Delivered at priority `-1` (quiet) — no sound or vibration. |
|
|
224
|
+
| Generic webhook | `silent: true` is forwarded in the JSON payload; your endpoint decides. |
|
|
225
|
+
|
|
226
|
+
Grafana revises per mode: **annotations** patches the annotation it created (`PATCH /api/annotations/:id`), so the dashboard keeps one marker at the detection's own timestamp whose text improves. **Alertmanager** and **IRM** re-file under the `event_id` / `alert_uid` the first alert used — that identity is what each surface deduplicates on, so the existing alert or group picks up the description instead of a second one firing. When the publisher supplies its own `Data["eventId"]`, that id is authoritative and the two publishes already share it, so those modes update correctly even across a plugin restart.
|
|
227
|
+
|
|
228
|
+
The replacing backends therefore show **one** notification whose text improves in place. The message id is remembered in memory per tag for 15 minutes; after a plugin restart, or if the original message was deleted, the update is delivered as a new (quiet) message instead of being lost.
|
|
229
|
+
|
|
230
|
+
Only the `silent` follow-up replaces. Detection tags repeat across events (`motion:cam-1` is the same tag every time that camera sees something), so a *new* alert reusing a tag always posts a new message — your chat history is never rewritten by a later event.
|
|
231
|
+
|
|
232
|
+
**Critical alerts ignore `silent`** — a `critical` severity notification always alerts, per the SDK contract.
|
|
233
|
+
|
|
234
|
+
If you would rather never see the follow-up on a backend that can't replace, set **Follow-up updates** to `Skip the update entirely` in the plugin settings. Backends that replace in place still receive it under that setting, since editing adds nothing to the notification list — with the one caveat that a lost message id (restart, deleted message) turns that edit into a new quiet message.
|
|
235
|
+
|
|
211
236
|
## Configuring your target (v1: one active target)
|
|
212
237
|
|
|
213
238
|
There is no "add device" flow. Instead, configure the plugin itself:
|
|
@@ -215,7 +240,8 @@ There is no "add device" flow. Instead, configure the plugin itself:
|
|
|
215
240
|
1. Open the **Notify** plugin's page in camera.ui (Plugins → Notify).
|
|
216
241
|
2. In its settings, pick a **Service** (`ntfy`, `Gotify`, `Generic webhook`, `Pushover`, `Telegram`, `Discord`, or `Grafana`) from the dropdown built from the registered backends.
|
|
217
242
|
3. Fill in that service's fields — only the selected service's fields are shown; the rest are condition-gated out.
|
|
218
|
-
4.
|
|
243
|
+
4. Optionally set **Follow-up updates** (see [Follow-up updates](#follow-up-updates-ai-descriptions)) — defaults to delivering the AI description quietly.
|
|
244
|
+
5. Save. The config is validated (`ParseTarget`) the next time a notification is dispatched; `getDevices` then synthesizes one delivery target from it, and notifications from any publisher are delivered there.
|
|
219
245
|
|
|
220
246
|
This is a **single, instance-wide target** in v1 — there's no way to register several devices at once. Changing the config replaces the previous target rather than adding to it. Delivery for that one target is a single request per notification (no fan-out to worry about, since there's only one device).
|
|
221
247
|
|
|
@@ -318,9 +344,24 @@ dispatch logic in `notifier.go` all pick up the new backend automatically.
|
|
|
318
344
|
|
|
319
345
|
```bash
|
|
320
346
|
go test ./src/... # full suite
|
|
321
|
-
go test ./src/... -race -count=1 # race detector
|
|
347
|
+
go test ./src/... -race -count=1 # race detector (needs cgo: apt-get install gcc)
|
|
348
|
+
npm run lint # golangci-lint, configured by .golangci.yml
|
|
349
|
+
npm run format # gofmt + go fix
|
|
322
350
|
```
|
|
323
351
|
|
|
352
|
+
Linting is golangci-lint only. It bundles staticcheck's analyzers (`SA`/`S`/`ST`/`QF`), so running
|
|
353
|
+
the standalone `staticcheck` binary alongside it only duplicates findings — and, being a separate
|
|
354
|
+
tool, it can't read the path-scoped exclusions in `.golangci.yml`.
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`.golangci.yml` excludes three deliberate idioms, each scoped as narrowly as the linter allows so
|
|
361
|
+
the check stays live everywhere else: `defer resp.Body.Close()` (errcheck), and — in `_test.go`
|
|
362
|
+
only — passing a nil `Context` to exercise each backend's `ctx == nil` fallback (SA1012) and the
|
|
363
|
+
`!(a <= b && b <= c)` monotonicity assertions (QF1001).
|
|
364
|
+
|
|
324
365
|
## License
|
|
325
366
|
|
|
326
367
|
[MIT](./LICENSE.md).
|
package/bundle.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Notify",
|
|
3
3
|
"name": "@calebcall/camera-ui-notify",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "A fully-local, multi-backend camera.ui notifier plugin: delivers notifications to ntfy, Gotify, or a generic webhook, entirely on your own hardware with no cloud dependency.",
|
|
4
|
+
"version": "0.8.0",
|
|
5
|
+
"description": "A fully-local, multi-backend camera.ui notifier plugin: delivers notifications to ntfy, Gotify, Pushover, Telegram, Discord, Grafana, or a generic webhook, entirely on your own hardware with no cloud dependency.",
|
|
6
6
|
"author": "calebcall (https://github.com/calebcall/camera-ui-notify)",
|
|
7
7
|
"main": "./main.go",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"bundle:dev": "npm run build && cross-env MODE=development cui bundle",
|
|
13
13
|
"format": "gofmt -w ./src/* && go fix ./src/...",
|
|
14
14
|
"install-updates": "npm i --save --force && go mod tidy",
|
|
15
|
-
"lint": "
|
|
15
|
+
"lint": "golangci-lint run ./src/...",
|
|
16
16
|
"prepublishOnly": "node -e \"if(!process.env.SAFE_PUBLISH){console.error('Error: Please use @camera.ui/cli to publish the plugin:\\n npm run publish:alpha\\n npm run publish:beta\\n npm run publish:latest\\n');process.exit(1)}\"",
|
|
17
17
|
"publish:alpha": "npm i --save --force && npm run bundle && cui publish --alpha",
|
|
18
18
|
"publish:beta": "npm i --save --force && npm run bundle && cui publish --beta",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"protocolLevel": 1
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
|
-
"@calebcall/camera-ui-notify-darwin-arm64": "0.
|
|
52
|
-
"@calebcall/camera-ui-notify-darwin-amd64": "0.
|
|
53
|
-
"@calebcall/camera-ui-notify-linux-amd64": "0.
|
|
54
|
-
"@calebcall/camera-ui-notify-linux-arm64": "0.
|
|
55
|
-
"@calebcall/camera-ui-notify-windows-amd64": "0.
|
|
56
|
-
"@calebcall/camera-ui-notify-windows-arm64": "0.
|
|
57
|
-
"@calebcall/camera-ui-notify-linux-amd64-musl": "0.
|
|
58
|
-
"@calebcall/camera-ui-notify-linux-arm64-musl": "0.
|
|
51
|
+
"@calebcall/camera-ui-notify-darwin-arm64": "0.8.0",
|
|
52
|
+
"@calebcall/camera-ui-notify-darwin-amd64": "0.8.0",
|
|
53
|
+
"@calebcall/camera-ui-notify-linux-amd64": "0.8.0",
|
|
54
|
+
"@calebcall/camera-ui-notify-linux-arm64": "0.8.0",
|
|
55
|
+
"@calebcall/camera-ui-notify-windows-amd64": "0.8.0",
|
|
56
|
+
"@calebcall/camera-ui-notify-windows-arm64": "0.8.0",
|
|
57
|
+
"@calebcall/camera-ui-notify-linux-amd64-musl": "0.8.0",
|
|
58
|
+
"@calebcall/camera-ui-notify-linux-arm64-musl": "0.8.0"
|
|
59
59
|
},
|
|
60
60
|
"files": [
|
|
61
61
|
"bundle.zip",
|