@calebcall/camera-ui-notify 0.4.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 +106 -0
- package/LICENSE.md +22 -0
- package/README.md +295 -0
- package/bundle.zip +0 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **Notify** are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.4.0] - 2026-07-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Pushover backend** — hosted push to the Pushover app (app token + user key); sends the snapshot
|
|
13
|
+
as an attachment and maps severity to Pushover priority (Info=0, higher=1).
|
|
14
|
+
- **Telegram backend** — delivery to a chat via a bot (bot token + chat ID); `sendPhoto` with the
|
|
15
|
+
snapshot when present, else `sendMessage`, with an inline "Open camera" button for the deep link.
|
|
16
|
+
- **Discord backend** — delivery via a channel webhook; a severity-colored embed with the snapshot
|
|
17
|
+
attached and the title linked to the deep link.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **Redact secrets from transport errors.** Telegram's bot token (in the request URL path), a
|
|
22
|
+
Discord webhook URL (itself a credential), and any secret embedded in a generic webhook URL are
|
|
23
|
+
no longer leaked into logs when a delivery request fails — request URLs are stripped from
|
|
24
|
+
`*url.Error` before wrapping.
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-07-25
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Inline thumbnail image (ntfy).** When a notification carries inline thumbnail bytes (e.g. the
|
|
31
|
+
NVR's detection snapshot) and no image URL, the ntfy backend uploads it as a file attachment so
|
|
32
|
+
the image renders in the notification.
|
|
33
|
+
- **Absolute deep links.** A new optional plugin config field, **camera.ui Base URL**, turns the
|
|
34
|
+
publisher's relative deep link (e.g. `/cameras/<name>?startTs=…`) into an absolute URL so ntfy's
|
|
35
|
+
tap-through (`Click`) works. Empty base URL leaves the link untouched.
|
|
36
|
+
- **Webhook thumbnail.** The generic webhook backend now includes the thumbnail as `thumbnailBase64`
|
|
37
|
+
in its JSON payload when present.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **Gotify visibility.** Severity now maps to Gotify priority 4–10 (Info=4 … Critical=10). Gotify
|
|
42
|
+
treats priority 0–3 as silent (no system notification), so the previous 0–10 mapping made
|
|
43
|
+
Info-severity notifications appear to deliver nothing.
|
|
44
|
+
|
|
45
|
+
### Notes
|
|
46
|
+
|
|
47
|
+
- Gotify cannot display an inline thumbnail image (it requires a hosted image URL, not raw bytes);
|
|
48
|
+
images are delivered on ntfy only.
|
|
49
|
+
|
|
50
|
+
## [0.2.0] - 2026-07-24
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- **Config-driven single target replaces device registration.** The stock camera.ui UI has no
|
|
55
|
+
way to *create* a notifier device — `registerDevice` is only ever called by the mobile app's
|
|
56
|
+
push-registration flow, hardcoded to the official NVR's plugin name, and the generic
|
|
57
|
+
notification settings panel renders read-only. The plugin's own settings page, however, does
|
|
58
|
+
render an editable, savable form from any plugin's `StorageSchema`. Notify now holds exactly
|
|
59
|
+
one target — a selected service (`ntfy`/`gotify`/`webhook`) plus that service's fields — in its
|
|
60
|
+
own persisted config, set from the plugin's settings page in camera.ui rather than through a
|
|
61
|
+
device-add flow.
|
|
62
|
+
- **`getDevices` synthesizes the device from config.** Instead of reading a stored device list, it
|
|
63
|
+
reads the configured service and fields on every call, validates them (`ParseTarget`), and
|
|
64
|
+
returns a single `sdk.NotifierDevice` (or none, if unconfigured/incomplete). `getDevice` mirrors
|
|
65
|
+
this for the one synthesized id (`cfg:<service>`).
|
|
66
|
+
- **`registerDevice` / `updateDevice` / `revokeDevice` are now no-ops** (the first returns an
|
|
67
|
+
error directing users to the plugin's settings page) — targets are configured, not registered.
|
|
68
|
+
These remain only to satisfy the `Notifier` interface.
|
|
69
|
+
- **`notificationSettings()` returns nothing.** Target configuration now lives on the plugin's
|
|
70
|
+
config tab; duplicating it in the notification "send test" panel would only confuse.
|
|
71
|
+
|
|
72
|
+
### Removed
|
|
73
|
+
|
|
74
|
+
- The device store (`src/store.go`) and its persisted `devices` key — replaced by the
|
|
75
|
+
`StorageSchema`-based config described above.
|
|
76
|
+
|
|
77
|
+
Backends (ntfy, Gotify, generic webhook), their config fields, delivery format, and severity
|
|
78
|
+
mapping are unchanged from 0.1.0.
|
|
79
|
+
|
|
80
|
+
## [0.1.0] - 2026-07-24
|
|
81
|
+
|
|
82
|
+
Initial release — a fully-local, multi-backend camera.ui `Notifier` plugin.
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- **Notifier plugin** — implements the full `Notifier` RPC surface (`getDevices`, `getDevice`,
|
|
87
|
+
`registerDevice`, `updateDevice`, `revokeDevice`, `sendNotification`, `notificationSettings`)
|
|
88
|
+
against a `Notifier`-only contract (`role: Hub`, `interfaces: [Notifier]`).
|
|
89
|
+
- **Pluggable-backend registry** — a `Backend` interface (id, label, config schema, target
|
|
90
|
+
validation, delivery) with a package-level registry; each backend self-registers from its own
|
|
91
|
+
`init()`, so adding a new backend later is one new file and a version bump.
|
|
92
|
+
- **Per-device backend selection** — each registered device is bound to one backend and holds
|
|
93
|
+
that backend's validated config; the registration form is built dynamically from the union of
|
|
94
|
+
every registered backend's schema, condition-gated on the selected service.
|
|
95
|
+
- **ntfy backend** — publishes to [ntfy.sh](https://ntfy.sh) or a self-hosted server (`server`,
|
|
96
|
+
`topic`, optional `token`), mapping severity to ntfy's 1–5 priority scale.
|
|
97
|
+
- **Gotify backend** — publishes to a self-hosted [Gotify](https://gotify.net) server (`server`,
|
|
98
|
+
`token`), mapping severity to Gotify's 0–10 priority scale.
|
|
99
|
+
- **Generic webhook backend** — delivers a JSON payload of the full notification to any HTTP
|
|
100
|
+
endpoint (`url`, `method` [`POST`/`PUT`], optional custom header) for services without a
|
|
101
|
+
dedicated backend.
|
|
102
|
+
- **Device persistence** — devices are stored as JSON under the plugin's own `DeviceStorage`,
|
|
103
|
+
requiring no external database.
|
|
104
|
+
|
|
105
|
+
[0.2.0]: https://github.com/calebcall/camera-ui-notify/releases/tag/v0.2.0
|
|
106
|
+
[0.1.0]: https://github.com/calebcall/camera-ui-notify/releases/tag/v0.1.0
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 calebcall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<h1 align="center">Notify</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
A fully-local, multi-backend <a href="https://github.com/seydx/camera.ui">camera.ui</a> notifier
|
|
5
|
+
plugin — delivers notifications to a service you control (<a href="https://ntfy.sh">ntfy</a>,
|
|
6
|
+
<a href="https://gotify.net">Gotify</a>, Pushover, Telegram, Discord, or a generic webhook),
|
|
7
|
+
entirely on your own hardware, with no cloud dependency and no license check.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why this exists
|
|
13
|
+
|
|
14
|
+
camera.ui separates two notification roles:
|
|
15
|
+
|
|
16
|
+
- **Publishers** (capability `PublishNotifications`) emit events. Our open-source NVR plugin
|
|
17
|
+
(`@calebcall/camera-ui-nvr-local`) is a publisher — it calls `NotificationManager.Publish` for
|
|
18
|
+
object-detection events.
|
|
19
|
+
- **Notifiers** (interface `Notifier`) own delivery *devices* and actually deliver notifications.
|
|
20
|
+
The host's `NotificationManager.notify()` fans every notification out to **all** running plugins
|
|
21
|
+
implementing `Notifier`.
|
|
22
|
+
|
|
23
|
+
There is intentionally no notifier in the open ecosystem: the closed, official NVR bundled a
|
|
24
|
+
notifier that pushed to the camera.ui mobile app through camera.ui's proprietary FCM/APNs cloud
|
|
25
|
+
relay — a license-gated path we neither have credentials for nor can replicate. This plugin takes
|
|
26
|
+
the only fully-local road to real background push instead: delivering to a push/webhook service
|
|
27
|
+
*you* run or control.
|
|
28
|
+
|
|
29
|
+
This plugin is a pure **notifier**. It is decoupled from any single publisher — it delivers
|
|
30
|
+
notifications from **any** publisher, including our NVR plugin's detection events and camera.ui's
|
|
31
|
+
own system notifications, not just one plugin's output.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
One plugin, contract `interfaces: [Notifier]`, built around a pluggable-backend (strategy) pattern:
|
|
36
|
+
|
|
37
|
+
- A `Backend` interface abstracts one delivery service (id, label, config schema, target
|
|
38
|
+
validation, and delivery).
|
|
39
|
+
- A package-level registry maps each backend's stable id to its implementation; every backend
|
|
40
|
+
self-registers from its own `init()`.
|
|
41
|
+
- The plugin holds **one active target** in its own persisted config (`StorageSchema`): which
|
|
42
|
+
service is selected, plus that service's validated fields. `getDevices` synthesizes a single
|
|
43
|
+
`sdk.NotifierDevice` from this config on every call — there is no device registry.
|
|
44
|
+
- `sendNotification` dispatches the synthesized device to its backend's `Send`. A backend failure
|
|
45
|
+
is logged and returned to the host.
|
|
46
|
+
|
|
47
|
+
### Why config, not device registration
|
|
48
|
+
|
|
49
|
+
Earlier versions of this plugin modeled targets as registrable `NotifierDevice`s, the way
|
|
50
|
+
camera.ui's own mobile-push notifier does. That model turned out to be unreachable from the stock
|
|
51
|
+
UI: `registerDevice` is only ever called by the camera.ui mobile app's push-registration flow,
|
|
52
|
+
which is hardcoded to the official NVR's plugin name. The generic notification settings panel
|
|
53
|
+
renders `notificationSettings()` read-only — it has no "add a device" affordance for third-party
|
|
54
|
+
plugins. The one part of the stock UI that **does** render an editable, savable form for a
|
|
55
|
+
third-party plugin is its own settings page, which renders whatever `StorageSchema` the plugin
|
|
56
|
+
declares. So Notify now models its target as plugin config instead of a device: you configure it
|
|
57
|
+
once, in the plugin's own settings, and `getDevices` synthesizes the device the host's
|
|
58
|
+
`Notifier` interface expects from that config.
|
|
59
|
+
|
|
60
|
+
Adding a new backend later is **one new file** — `src/backend/<name>.go` implementing `Backend`
|
|
61
|
+
plus `Register(...)` in its `init()` — and a version bump. No new plugin, no core change, no
|
|
62
|
+
changes to `plugin.go`.
|
|
63
|
+
|
|
64
|
+
## Backends (v1)
|
|
65
|
+
|
|
66
|
+
Severity is mapped consistently across backends via `backend.PriorityScale`, which spreads
|
|
67
|
+
camera.ui's four severity levels (`info` → `warn` → `error` → `critical`) evenly across each
|
|
68
|
+
backend's native priority range, `info` at the low end and `critical` at the high end.
|
|
69
|
+
|
|
70
|
+
### ntfy
|
|
71
|
+
|
|
72
|
+
Publishes to [ntfy.sh](https://ntfy.sh) or a self-hosted ntfy server.
|
|
73
|
+
|
|
74
|
+
| Field | Required | Default | Notes |
|
|
75
|
+
| -------- | -------- | --------------------- | --------------------------------------------------- |
|
|
76
|
+
| `server` | no | `https://ntfy.sh` | Base URL of the ntfy server. Trailing `/` trimmed. |
|
|
77
|
+
| `topic` | yes | — | The ntfy topic to publish to. |
|
|
78
|
+
| `token` | no | — | Access token for a protected/self-hosted topic, sent as `Authorization: Bearer <token>`. |
|
|
79
|
+
|
|
80
|
+
Delivery: `POST {server}/{topic}` with the notification body as the request body, plus `Title`,
|
|
81
|
+
`Priority` (1–5, from severity), `Click` (deep link, if set), and `Attach`/`Icon` (image URL, if
|
|
82
|
+
set) headers.
|
|
83
|
+
|
|
84
|
+
### Gotify
|
|
85
|
+
|
|
86
|
+
Publishes to a self-hosted [Gotify](https://gotify.net) server.
|
|
87
|
+
|
|
88
|
+
| Field | Required | Notes |
|
|
89
|
+
| -------- | -------- | ----------------------------------------------- |
|
|
90
|
+
| `server` | yes | Base URL of the Gotify server. Trailing `/` trimmed. |
|
|
91
|
+
| `token` | yes | Gotify application token, used to authenticate published messages. |
|
|
92
|
+
|
|
93
|
+
Delivery: `POST {server}/message?token={token}` with a JSON body `{title, message, priority}`
|
|
94
|
+
(priority 0–10, from severity), plus a `client::notification.click` extra for the deep link and a
|
|
95
|
+
`bigImageUrl` extra when an image URL is set.
|
|
96
|
+
|
|
97
|
+
### Generic webhook
|
|
98
|
+
|
|
99
|
+
Delivers to any HTTP endpoint you provide — the fallback for anything without a dedicated backend.
|
|
100
|
+
|
|
101
|
+
| Field | Required | Default | Notes |
|
|
102
|
+
| ------------- | -------- | ------- | -------------------------------------------------------------- |
|
|
103
|
+
| `url` | yes | — | Endpoint that receives the notification. |
|
|
104
|
+
| `method` | no | `POST` | `POST` or `PUT`. |
|
|
105
|
+
| `headerName` | no | — | Optional custom header name (e.g. for a shared secret). Requires `headerValue` if set. |
|
|
106
|
+
| `headerValue` | no | — | Value of the custom header. Requires `headerName` if set. |
|
|
107
|
+
|
|
108
|
+
Delivery: `{method} {url}` with `Content-Type: application/json` and (if configured) the custom
|
|
109
|
+
header, carrying a JSON body of `{title, subtitle, body, severity, tag, imageUrl, deepLink, data,
|
|
110
|
+
createdAt, thumbnailBase64}`.
|
|
111
|
+
|
|
112
|
+
### Pushover
|
|
113
|
+
|
|
114
|
+
Hosted push to the [Pushover](https://pushover.net) app.
|
|
115
|
+
|
|
116
|
+
| Field | Required | Notes |
|
|
117
|
+
| -------- | -------- | ---------------------------------------------- |
|
|
118
|
+
| `token` | yes | Pushover application API token/key. |
|
|
119
|
+
| `user` | yes | Your Pushover user or group key. |
|
|
120
|
+
|
|
121
|
+
Delivery: `POST https://api.pushover.net/1/messages.json` with title/message and a priority
|
|
122
|
+
(Info→0 normal, everything higher→1 high; never emergency). The snapshot **image** is sent as an
|
|
123
|
+
`attachment`, and an absolute deep link becomes a supplementary `url`.
|
|
124
|
+
|
|
125
|
+
### Telegram
|
|
126
|
+
|
|
127
|
+
Delivers to a chat via a [Telegram bot](https://core.telegram.org/bots).
|
|
128
|
+
|
|
129
|
+
| Field | Required | Notes |
|
|
130
|
+
| ------- | -------- | -------------------------------------------------------- |
|
|
131
|
+
| `token` | yes | Bot token from @BotFather. |
|
|
132
|
+
| `chat` | yes | Chat ID to deliver to. |
|
|
133
|
+
|
|
134
|
+
Delivery: `sendPhoto` (with the snapshot **image** + caption) when a thumbnail is present, otherwise
|
|
135
|
+
`sendMessage`. An absolute deep link is added as an inline "Open camera" button.
|
|
136
|
+
|
|
137
|
+
### Discord
|
|
138
|
+
|
|
139
|
+
Delivers to a channel via a Discord [webhook](https://support.discord.com/hc/en-us/articles/228383668).
|
|
140
|
+
|
|
141
|
+
| Field | Required | Notes |
|
|
142
|
+
| --------- | -------- | --------------------------------------- |
|
|
143
|
+
| `webhook` | yes | Channel webhook URL. |
|
|
144
|
+
|
|
145
|
+
Delivery: a rich embed (title, body, severity color — blue/yellow/red) with the snapshot **image**
|
|
146
|
+
attached; an absolute deep link makes the title a link.
|
|
147
|
+
|
|
148
|
+
> **Images:** ntfy, Pushover, Telegram, and Discord all render the detection snapshot. Gotify is
|
|
149
|
+
> text + link only (it needs a hosted image URL, which this fully-local plugin doesn't provide).
|
|
150
|
+
|
|
151
|
+
> **Secrets in logs:** transport failures never log the bot token / webhook URL / other
|
|
152
|
+
> URL-embedded secret — request URLs are redacted from delivery errors.
|
|
153
|
+
|
|
154
|
+
## Configuring your target (v1: one active target)
|
|
155
|
+
|
|
156
|
+
There is no "add device" flow. Instead, configure the plugin itself:
|
|
157
|
+
|
|
158
|
+
1. Open the **Notify** plugin's page in camera.ui (Plugins → Notify).
|
|
159
|
+
2. In its settings, pick a **Service** (`ntfy`, `Gotify`, `Generic webhook`, `Pushover`, `Telegram`,
|
|
160
|
+
or `Discord`) from the dropdown built from the registered backends.
|
|
161
|
+
3. Fill in that service's fields — only the selected service's fields are shown; the rest are
|
|
162
|
+
condition-gated out.
|
|
163
|
+
4. Save. The config is validated (`ParseTarget`) the next time a notification is dispatched;
|
|
164
|
+
`getDevices` then synthesizes one delivery target from it, and notifications from any publisher
|
|
165
|
+
are delivered there.
|
|
166
|
+
|
|
167
|
+
This is a **single, instance-wide target** in v1 — there's no way to register several devices at
|
|
168
|
+
once. Changing the config replaces the previous target rather than adding to it. Delivery for that
|
|
169
|
+
one target is a single request per notification (no fan-out to worry about, since there's only one
|
|
170
|
+
device).
|
|
171
|
+
|
|
172
|
+
## Tech stack
|
|
173
|
+
|
|
174
|
+
- **Go** (matches the NVR plugin; same build/deploy toolchain).
|
|
175
|
+
- **camera.ui SDK (Go)** for the plugin runtime, the `Notifier` RPC surface, and device storage.
|
|
176
|
+
- Delivery via each backend's plain HTTP API using the Go stdlib `net/http` — no third-party HTTP
|
|
177
|
+
client libraries.
|
|
178
|
+
- Config persistence via the plugin's own `DeviceStorage`, holding the selected service and its
|
|
179
|
+
fields as declared by `StorageSchema()` — no SQLite; it's a handful of scalar values.
|
|
180
|
+
|
|
181
|
+
## Prerequisites
|
|
182
|
+
|
|
183
|
+
- **Go 1.26+** (to build the plugin binary).
|
|
184
|
+
- **Node.js 22+** and the plugin's dev dependencies (`npm install`) to produce the `contract.cjs`
|
|
185
|
+
bundle via the camera.ui CLI.
|
|
186
|
+
- A running camera.ui instance you control (the host where the plugin gets installed).
|
|
187
|
+
|
|
188
|
+
## Build & deploy locally
|
|
189
|
+
|
|
190
|
+
camera.ui loads the **built artifact**, not source — a `git pull` alone does nothing. You must
|
|
191
|
+
build the bundle (`contract.cjs` + the platform binary) and copy it into the install slot.
|
|
192
|
+
|
|
193
|
+
The install slot is:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
<camera.ui-install>/plugins/@calebcall/camera-ui-notify/
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
and the plugin's config (the selected service and its fields) lives under
|
|
200
|
+
`<camera.ui-install>/volume/plugins/storage/@calebcall/camera-ui-notify/` — reinstalling the code
|
|
201
|
+
does not touch it.
|
|
202
|
+
|
|
203
|
+
> **Unlike the NVR plugin**, there is no hardcoded-package-id requirement here: the camera.ui host
|
|
204
|
+
> discovers notifiers by the `Notifier` **interface** declared in the plugin's contract, not by a
|
|
205
|
+
> fixed package name. This plugin can be installed under its own real name/namespace.
|
|
206
|
+
|
|
207
|
+
### Option A — build natively on the server (preferred)
|
|
208
|
+
|
|
209
|
+
If Go 1.26+ and Node are available on the host, build there and skip the cross-compile round-trip:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# 1. Get the latest source
|
|
213
|
+
cd ~/path/to/plugins && git pull
|
|
214
|
+
cd camera-ui-notify
|
|
215
|
+
npm install # first time only — pulls the camera.ui CLI bundler
|
|
216
|
+
|
|
217
|
+
# 2. Build the bundle (produces bundle/{contract.cjs, package.json, dist/bin/plugin})
|
|
218
|
+
npm run bundle:dev
|
|
219
|
+
|
|
220
|
+
# 3. Install into the @calebcall/camera-ui-notify slot
|
|
221
|
+
D=<camera.ui-install>/plugins/@calebcall/camera-ui-notify
|
|
222
|
+
mkdir -p "$D" && rm -rf "$D"/* && cp -a bundle/. "$D/"
|
|
223
|
+
|
|
224
|
+
# 4. First time only: enable it — remove the "@calebcall/camera-ui-notify" line
|
|
225
|
+
# from disabledPlugins in <camera.ui-install>/volume/camera.ui.yaml
|
|
226
|
+
|
|
227
|
+
# 5. Restart camera.ui
|
|
228
|
+
systemctl restart cameraui # or however your instance is managed
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Option B — cross-compile from a workstation
|
|
232
|
+
|
|
233
|
+
Build a statically-linked Linux binary locally, then ship the bundle to the server:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
cd camera-ui-notify
|
|
237
|
+
|
|
238
|
+
# 1. Produce contract.cjs (+ package.json) under bundle/
|
|
239
|
+
npm install
|
|
240
|
+
npm run bundle:dev
|
|
241
|
+
|
|
242
|
+
# 2. Cross-compile the Linux binary into the bundle's dev path
|
|
243
|
+
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
|
244
|
+
go build -ldflags "-s -w" -o bundle/dist/bin/plugin ./src/
|
|
245
|
+
chmod 755 bundle/dist/bin/plugin
|
|
246
|
+
|
|
247
|
+
# 3. Ship it (strip macOS junk so the server dir stays clean)
|
|
248
|
+
COPYFILE_DISABLE=1 tar czf /tmp/notify-install.tgz -C bundle .
|
|
249
|
+
scp /tmp/notify-install.tgz root@YOUR_SERVER:/tmp/
|
|
250
|
+
|
|
251
|
+
# 4. On the server: install into the slot, enable (first time), restart
|
|
252
|
+
ssh root@YOUR_SERVER '
|
|
253
|
+
D=<camera.ui-install>/plugins/@calebcall/camera-ui-notify
|
|
254
|
+
mkdir -p "$D" && rm -rf "$D"/* && tar xzf /tmp/notify-install.tgz -C "$D"
|
|
255
|
+
systemctl restart cameraui
|
|
256
|
+
'
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Set `GOARCH=arm64` for ARM hosts. The binary is resolved from the dev path
|
|
260
|
+
`<slot>/dist/bin/plugin` before any platform `node_modules` package, so no npm publish is involved.
|
|
261
|
+
|
|
262
|
+
### Verify it loaded and is being used
|
|
263
|
+
|
|
264
|
+
Tail the camera.ui log after restart:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
grep -iE "Notify|Spawning Go|notify: rpc" <camera.ui-install>/volume/camera.ui.log | tail
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
You want to see the plugin spawn (`Spawning Go plugin ... dist/bin/plugin`) **and** the host
|
|
271
|
+
actually calling into it — e.g. `notify: rpc getDevices` / `notificationSettings` /
|
|
272
|
+
`sendNotification`.
|
|
273
|
+
|
|
274
|
+
## Adding a new backend
|
|
275
|
+
|
|
276
|
+
1. Create `src/backend/<name>.go` implementing the `Backend` interface (`ID`, `Label`, `Schema`,
|
|
277
|
+
`ParseTarget`, `Send`) — see `ntfy.go`, `gotify.go`, or `webhook.go` for the shape.
|
|
278
|
+
2. Gate every schema field with `Condition: []sdk.SchemaCondition{{Key: "service", Value: ID()}}`
|
|
279
|
+
so it only renders when the new backend is selected.
|
|
280
|
+
3. Call `backend.Register(new<Name>())` from an `init()` function in the same file.
|
|
281
|
+
4. Bump the version in `package.json`.
|
|
282
|
+
|
|
283
|
+
Nothing outside `src/backend/` needs to change — the registry, the `service` enum, and the
|
|
284
|
+
dispatch logic in `notifier.go` all pick up the new backend automatically.
|
|
285
|
+
|
|
286
|
+
## Development
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
go test ./src/... # full suite
|
|
290
|
+
go test ./src/... -race -count=1 # race detector
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
[MIT](./LICENSE.md).
|
package/bundle.zip
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"displayName": "Notify",
|
|
3
|
+
"name": "@calebcall/camera-ui-notify",
|
|
4
|
+
"version": "0.4.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.",
|
|
6
|
+
"author": "calebcall (https://github.com/calebcall/camera-ui-notify)",
|
|
7
|
+
"main": "./main.go",
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rimraf dist && go build -o /dev/null ./src/",
|
|
11
|
+
"bundle": "npm run build && cui bundle",
|
|
12
|
+
"bundle:dev": "npm run build && cross-env MODE=development cui bundle",
|
|
13
|
+
"format": "gofmt -w ./src/* && go fix ./src/...",
|
|
14
|
+
"install-updates": "npm i --save --force && go mod tidy",
|
|
15
|
+
"lint": "staticcheck ./src/... && golangci-lint run ./src/...",
|
|
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
|
+
"publish:alpha": "npm i --save --force && npm run bundle && cui publish --alpha",
|
|
18
|
+
"publish:beta": "npm i --save --force && npm run bundle && cui publish --beta",
|
|
19
|
+
"publish:latest": "npm i --save --force && npm run bundle && cui publish --latest",
|
|
20
|
+
"test": "go test ./...",
|
|
21
|
+
"update": "updates --update ./"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/calebcall/camera-ui-notify/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"camera.ui": ">=2.0.15",
|
|
29
|
+
"node": ">=22.0.0"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/calebcall/camera-ui-notify/tree/main#readme",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"camera-ui-plugin",
|
|
34
|
+
"notifier",
|
|
35
|
+
"notifications",
|
|
36
|
+
"ntfy",
|
|
37
|
+
"gotify",
|
|
38
|
+
"webhook",
|
|
39
|
+
"local"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/calebcall/camera-ui-notify.git"
|
|
44
|
+
},
|
|
45
|
+
"os": [],
|
|
46
|
+
"cpu": [],
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"@calebcall/camera-ui-notify-darwin-arm64": "0.4.0",
|
|
49
|
+
"@calebcall/camera-ui-notify-darwin-amd64": "0.4.0",
|
|
50
|
+
"@calebcall/camera-ui-notify-linux-amd64": "0.4.0",
|
|
51
|
+
"@calebcall/camera-ui-notify-linux-arm64": "0.4.0",
|
|
52
|
+
"@calebcall/camera-ui-notify-windows-amd64": "0.4.0",
|
|
53
|
+
"@calebcall/camera-ui-notify-windows-arm64": "0.4.0",
|
|
54
|
+
"@calebcall/camera-ui-notify-linux-amd64-musl": "0.4.0",
|
|
55
|
+
"@calebcall/camera-ui-notify-linux-arm64-musl": "0.4.0"
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"bundle.zip",
|
|
59
|
+
"CHANGELOG.md"
|
|
60
|
+
]
|
|
61
|
+
}
|