Send Message 1.0.0: one PIN, one place to send from; Slack 1.3.0 and ProdCom 1.4.0 become messaging providers
A new send-message module owns everything a dashboard sends: the PIN
(switchable off in admin), the unlock tokens, the Send message and Quick
replies tiles, the quick replies and quick emojis. It opens no upstream
connection of its own — it finds every module declaring
provides: ["messaging"] through the shell's module list and hands each
message to that module's POST /messaging/send over loopback, so Slack keeps
its one Slack connection and ProdCom its one ProdCom connection. Providers
check the browser's unlock token with GET /verify over loopback before
touching their upstream, so no browser on the network can post as the
location without having entered the PIN here.
Slack 1.3.0 is view-only plus reactions: the Sending group, PIN, send tiles
and quick replies are gone; it serves the messaging descriptor (channels as
recipients) and its reactions ride on the Send Message unlock. A layout's
old Send message / Quick replies tiles draw nothing until removed.
ProdCom 1.4.0 serves the descriptor as identities — only channels with
typingEnabled, since typed text goes out *as* a channel — and posts with
POST /api/v1/channels/{id}/send. Its tile is a click-to-target like Slack's
(accent stripe along the bottom), and typed entries carry a keyboard mark.
The tiles: click any Slack or ProdCom transcript to point sends there; a
Send as menu (only for platforms with identities, hidden otherwise) picks
the ProdCom channel to speak as; the choice is saved with the tile so named
layouts remember it. Wording follows the role — "Send message to #ops…" /
"Send message to ProdCom as FOH…". The PIN pad's dots ride on a real
numeric input so a phone's keyboard appears. Outcome is an icon beside
Send (check / blocked, five seconds, sentence as tooltip). A pill on the
box's bottom edge trades box height for rows of quick-emoji keys, a row
at a time; the single row scrolls sideways on a narrow tile with Send
pinned in place.
The module guide documents the provider contract and the page bus
(window.proddashMessaging + proddash:messaging:* events) the tiles share;
the ProdCom mock gained the send route.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
3b81efcb51
commit
1274360ee6
+113
-3
@@ -202,7 +202,7 @@ line, so a Windows booth knows why the feature is off.
|
||||
| `proddash` | yes | The ProdDash versions this module works with: `">=1.1.0"`, `"^1.1.0"`, `">=1.1.0 <2.0.0"`, or `"*"`. A module whose requirement the running shell doesn't meet is listed in the admin page but never loaded, and can't be installed from the catalog. |
|
||||
| `description` | no | Shown in the admin page / picker. |
|
||||
| `permissions` | no | What macOS must allow before this module can work — see **Permissions macOS has to grant**. Ignored by the shell; read by the macOS launcher. |
|
||||
| `provides` | no | Capabilities other modules can consume — `["timers"]` today. Forwarded in `/api/modules` so a consumer can find you. See **Working with other modules**. |
|
||||
| `provides` | no | Capabilities other modules can consume — `["timers"]`, `["messaging"]`. Forwarded in `/api/modules` so a consumer can find you. See **Working with other modules**. |
|
||||
| `client` | yes | Client entry file, loaded as an ES module. |
|
||||
| `server` | no | Server entry file (CommonJS), loaded with `require()`. |
|
||||
| `style` | no | A stylesheet the shell injects once per module. |
|
||||
@@ -770,6 +770,108 @@ comment every 15 s); the consumer uses the stream when it exists and polls
|
||||
wall — the Timers module lets them hide any of them per tile, but every entry
|
||||
you add is a decision someone has to make.
|
||||
|
||||
### Offering messaging to the Send Message module
|
||||
|
||||
The Send Message module is the one place a dashboard sends from — a
|
||||
PIN-gated compose box and a grid of quick replies — whatever the platform.
|
||||
It owns nothing upstream: a module that *can* deliver a message (Slack posts
|
||||
to a channel, ProdCom puts typed text on a comms channel) offers that over
|
||||
its own routes, keeps its one upstream connection, and the Send Message
|
||||
module hands messages to it over loopback. A provider has no PIN, no send
|
||||
tile and no quick replies of its own; the shell's Add-tile picker shows the
|
||||
Send message and Quick replies tiles once, under Send Message.
|
||||
|
||||
A module offers messaging by:
|
||||
|
||||
1. Declaring it — `"provides": ["messaging"]` in `module.json`. The Send
|
||||
Message module re-reads `/api/modules` every 30 s, so a provider installed
|
||||
mid-session shows up without a restart.
|
||||
2. Serving `GET /messaging` — what it can send to, from state it already
|
||||
holds (it is polled every few seconds while a send tile is open):
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "Slack",
|
||||
"identity": "Apollo Beach",
|
||||
"destinationRole": "recipient",
|
||||
"status": { "state": "ok", "message": "Apollo Beach · #production, #ops" },
|
||||
"destinations": [
|
||||
{ "id": "C01ABC", "label": "#production" },
|
||||
{ "id": "C02DEF", "label": "#ops", "color": "#5c9de0" }
|
||||
],
|
||||
"emptyMessage": "",
|
||||
"maxChars": 4000
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `label` | The platform's name, as the sending line and the Send as menu show it. |
|
||||
| `identity` | Who a message appears to come from when that is fixed by the connection, or `''`. Shown as "as Apollo Beach". |
|
||||
| `destinationRole` | What a destination *is*. `recipient` (the default): a message goes **to** it — a Slack channel — and the operator picks one by clicking its transcript tile, so it never appears in the Send as menu. `identity`: a message goes out **as** it — a ProdCom channel — and the Send as menu lists them; the tiles then say "Send message to ProdCom as FOH" and "Sent to ProdCom as FOH". The menu exists only while some installed platform offers identities. |
|
||||
| `status` | `ok`, `connecting` or `error` plus one line — the same thing your `health()` says. A provider in error is still listed, so the operator sees why a send will fail before trying. |
|
||||
| `destinations` | Where a message can go, or who it can go out as: a stable `id` (what `/messaging/send` receives), a `label`, and an optional `color`. Offer only what will work — ProdCom lists just the channels that accept typed text. Order is the order the menu shows. |
|
||||
| `emptyMessage` | Optional: why the list is empty, when you know ("No ProdCom channel accepts typed text — turn on typing for one in ProdCom"). The tiles show it in place of a destination. |
|
||||
| `maxChars` | The longest text you accept; the Send Message module refuses longer ones before asking you. |
|
||||
|
||||
3. Serving `POST /messaging/send` — `{ "destination": "<id>", "text": "…" }`
|
||||
with the browser's unlock token as `Authorization: Bearer …`. **Check the
|
||||
token before touching your upstream**: the Send Message module owns the
|
||||
PIN, so ask it — `GET http://127.0.0.1:${shell.port}/api/modules/send-message/verify`
|
||||
with the same `Authorization` header answers `200 { ok: true, expiresAt }`
|
||||
or `401`. A `404` means no Send Message module is installed or enabled:
|
||||
refuse with a sentence that says so. Without this check, any browser on
|
||||
the network could post as the location by calling your route directly.
|
||||
Answer `200 { "ok": true, "label": "#ops" }` when it landed, otherwise
|
||||
`{ "error": "<one sentence a tech can act on>", "code": "…" }` with `400`
|
||||
(bad destination or text), `401` (the token was refused), `429` (your
|
||||
upstream is rate-limiting) or `502` (your upstream failed). Validate the
|
||||
destination against your own list — never pass an arbitrary id upstream.
|
||||
|
||||
The same verification gates any other outbound action a module offers from
|
||||
its tiles — the Slack transcript's emoji reactions ride on it — so a module
|
||||
never needs a PIN of its own: one unlock serves every messaging tile on the
|
||||
page. The `slack` and `prodcom-transcript` modules are the reference
|
||||
implementations (`verifyUnlock()` in either server entry is the whole check).
|
||||
|
||||
### Messaging on the page
|
||||
|
||||
Where the next message goes is a fact about the whole dashboard, not one
|
||||
tile: the Send message tile, the Quick replies tile and every transcript on
|
||||
the page must agree, and they belong to different modules. Modules don't
|
||||
import each other, so the agreement lives on `window` — one plain object plus
|
||||
`CustomEvent`s on `window`, created by whichever messaging-aware tile mounts
|
||||
first:
|
||||
|
||||
```js
|
||||
window.proddashMessaging = {
|
||||
version: 1,
|
||||
target: { provider: '', destination: '', providerLabel: '', destinationLabel: '', identity: '' },
|
||||
unlock: { token: '', expiresAt: 0, reason: '' },
|
||||
quickEmojis: [], // the Send Message module's admin list, plain strings
|
||||
transcripts: new Map(), // instanceId → { provider, destination }
|
||||
senders: new Set(), // instanceIds of Send message / Quick replies tiles
|
||||
};
|
||||
```
|
||||
|
||||
Events, all named `proddash:messaging:<name>` with the payload in `detail`:
|
||||
|
||||
| Event | Who sends it | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `select` | a transcript tile, or a Send as menu | `{ provider, destination }` — the operator pointed the senders here. A tile bound to one recipient (a Slack channel transcript) names it; one that shows a whole platform (ProdCom) sends `destination: ''` and the Send Message tile keeps the identity it has chosen for that platform. |
|
||||
| `target` | the Send Message tiles | `window.proddashMessaging.target` changed: where the next message goes, resolved. A transcript compares `provider` (and `destination`, when it has one) and lights its bottom edge. |
|
||||
| `unlock` | the Send Message tiles; a provider's tile on a `401` | `window.proddashMessaging.unlock` changed. Read the token from the object, never cache it; `''` means locked, with `reason` saying why. |
|
||||
| `transcripts` | transcript tiles | the `transcripts` map changed (a tile mounted, unmounted or learned its channel). Register once you know your destination; unregister in `stop()`. This is how a dashboard with one transcript sends there by default. |
|
||||
| `senders` | the Send Message tiles | the `senders` set changed — a transcript's locked note can say "add a Send message tile" rather than "enter the PIN". |
|
||||
|
||||
A transcript tile needs only three things: emit `select` on a click in its
|
||||
body, register in `transcripts`, and toggle a target class on `target`. A
|
||||
tile that performs a gated action itself (a reaction) reads
|
||||
`window.proddashMessaging.unlock.token` and sends it as the Bearer token to
|
||||
its own module's route. The Send Message tiles save the chosen platform and
|
||||
per-platform destination in their instance settings, so a named layout
|
||||
remembers them; the page object is the live copy.
|
||||
|
||||
## Complete minimal example
|
||||
|
||||
A working clock — copy-paste this as your starter. (A slightly fancier
|
||||
@@ -863,8 +965,16 @@ client-side failures (a tile that fails to start says so in the tile).
|
||||
**Mock upstreams** (so you can develop without the real gear):
|
||||
|
||||
- ProdCom: `node tools/prodcom-mock.js` (port 24480) — channels, groups,
|
||||
history, and a live stream with in-progress speech. Point the
|
||||
transcript module's ProdCom URL at `http://127.0.0.1:24480`.
|
||||
history, a live stream with in-progress speech, and
|
||||
`POST /api/v1/channels/{id}/send`, which lands typed text in the channel
|
||||
as a `source: "typed"` entry (so a send from the Send Message module can
|
||||
be watched arriving; a text containing `!429` answers RATE_LIMITED, and
|
||||
`PRODCOM_MOCK_KEY=…` makes it demand that API key). Point the transcript
|
||||
module's ProdCom endpoint at `127.0.0.1`, port `24480`.
|
||||
- Send Message: no mock — it talks to the installed modules. Run the Slack
|
||||
and ProdCom mocks, set a PIN under Send Message in `/admin` (or switch the
|
||||
PIN off), add a transcript or two plus Send message and Quick replies from
|
||||
+, and watch a send arrive in the transcript you clicked.
|
||||
- ProPresenter: `node band-lineup-display/tmp/pp-mock.js 1599` — playlists
|
||||
and an advancing slide index (including the arrangement-repeat case).
|
||||
Point the Now/Next module at host `127.0.0.1`, port `1599`.
|
||||
|
||||
Reference in New Issue
Block a user