Compare commits

..
Author SHA1 Message Date
Waters ChurchandClaude Opus 5 f0be37796b Slack 1.2.1: a clearer Send tile, and a send target you can see
The line above the message box said "Sending as Apollo Beach → #ops" with a
Lock button beside it. The box's own placeholder already reads "Message
#ops…" and the title bar already has a Lock, so both went. Quick replies
keeps its line: it has no placeholder, so that is the only place its target
is named.

The bigger thing behind it: until a transcript was clicked, a send went to
the first channel in the admin list whether or not that channel was on the
dashboard — so a board showing only #ops would quietly aim at #general. The
target now falls to the first configured channel whose transcript is
actually up, which with a single transcript is that one. Only when the page
shows no transcript at all does it fall back to the first configured
channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:58:57 -04:00
3 changed files with 29 additions and 43 deletions
+3 -28
View File
@@ -265,29 +265,6 @@ below. Nothing is installed without someone clicking.
(gear menu): the top-bar text (placeholders such as (gear menu): the top-bar text (placeholders such as
`{series} • {part} | {serviceType} • {date}`), colors, text size, and a `{series} • {part} | {serviceType} • {date}`), colors, text size, and a
show/hide checklist for every detail. show/hide checklist for every detail.
- **Slack** — live transcripts of this location's Slack channels, plus
PIN-protected sending as the location. The + picker lists one transcript
tile per configured channel, titled `#channel`, and two send surfaces (one
of each per dashboard): **Send message**, a free-text box with the admin's
quick emojis one press away, and **Quick replies**, a grid of preset
buttons. Both post to the channel of whichever transcript was clicked last
— its bottom edge lights up in the accent color — and to the first
configured channel until one is clicked. Messages arrive with avatars,
names and times, Slack's inline formatting and the workspace's custom
emoji; a right-click on one opens the reaction menu, and text size and
jump-to-latest sit in the title bar. Nothing leaves the machine until the
PIN is entered: one PIN unlocks every Slack tile on that dashboard page
until the auto-lock idles out, a reload asks again, and the lock button on
a send tile's title bar ends it early.
Admin config is in four groups: **Slack account** (the location account's
User OAuth Token — `xoxp-…`, so messages are literally from the campus —
or a bot token plus "Send as"), **Channels** (the joined channels to
follow, one transcript tile each), **Transcript** (refresh interval,
messages kept, thread replies) and **Sending** (the switch on the group's
title row, a 4–8 digit PIN, auto-lock minutes, quick replies one per line
as `Label | Message` — add `| #channel` to bind one to a channel — and up
to eight quick emojis).
Tile settings: avatars, timestamps, names and channel notices.
- **Clock** — a big booth clock; also the smallest possible module and the - **Clock** — a big booth clock; also the smallest possible module and the
reference for module authors. reference for module authors.
@@ -295,12 +272,10 @@ To develop modules against mocks instead of real gear:
`node tools/prodcom-mock.js` (ProdCom on `127.0.0.1:24480`), `node tools/prodcom-mock.js` (ProdCom on `127.0.0.1:24480`),
`node band-lineup-display/tmp/pp-mock.js 1599` (ProPresenter slides on `node band-lineup-display/tmp/pp-mock.js 1599` (ProPresenter slides on
`127.0.0.1:1599`), `node tools/pp-timers-mock.js 1600` (ProPresenter `127.0.0.1:1599`), `node tools/pp-timers-mock.js 1600` (ProPresenter
timers + timecode on `127.0.0.1:1600`), `node tools/pco-mock.js` timers + timecode on `127.0.0.1:1600`) and `node tools/pco-mock.js`
(Planning Center Services on `127.0.0.1:24700` — run ProdDash with (Planning Center Services on `127.0.0.1:24700` — run ProdDash with
`PCO_PLAN_API_BASE=http://127.0.0.1:24700`) and `PCO_PLAN_API_BASE=http://127.0.0.1:24700`). See the testing section of the
`node tools/slack-mock.js 24716` (the Slack Web API on `127.0.0.1:24716` — [module guide](docs/MODULE-GUIDE.md).
run ProdDash with `SLACK_API_BASE=http://127.0.0.1:24716/api`). See the
testing section of the [module guide](docs/MODULE-GUIDE.md).
## Install as a desktop web app ## Install as a desktop web app
+16 -5
View File
@@ -141,13 +141,17 @@ const sendTarget = (() => {
/** /**
* Where a send goes, given the configured channels in admin order: the * Where a send goes, given the configured channels in admin order: the
* selection while it is still configured and its transcript is on the * selection while it is still configured and its transcript is on the
* page; otherwise the first configured channel. * page, else the first configured channel that IS on this page — with one
* transcript up, that one — and only if the page shows none, the first
* configured channel. A send must never quietly go to a channel the
* operator cannot see.
*/ */
resolve(configured) { resolve(configured) {
const list = Array.isArray(configured) ? configured : []; const list = Array.isArray(configured) ? configured : [];
if (!list.length) return ''; if (!list.length) return '';
if (id && list.includes(id) && this.hasTranscript(id)) return id; if (id && list.includes(id) && this.hasTranscript(id)) return id;
return list[0]; const onPage = list.find((c) => this.hasTranscript(c));
return onPage || list[0];
}, },
on(fn) { on(fn) {
bus.addEventListener('change', fn); bus.addEventListener('change', fn);
@@ -1100,7 +1104,7 @@ function createSendTile({ root, moduleApi, mode }) {
if (stopped || !state) return; if (stopped || !state) return;
if (token) { if (token) {
if (composeEls) { if (composeEls) {
composeEls.line.textContent = sendingLine(); if (composeEls.line) composeEls.line.textContent = sendingLine();
if (composeEls.input) composeEls.input.placeholder = placeholder(); if (composeEls.input) composeEls.input.placeholder = placeholder();
} }
} else renderGate(); } else renderGate();
@@ -1177,14 +1181,21 @@ function createSendTile({ root, moduleApi, mode }) {
let composeEls = null; let composeEls = null;
function buildCompose() { function buildCompose() {
compose.textContent = ''; compose.textContent = '';
// Send message names the channel in the box's own placeholder and locks
// from the title bar, so a line above the field would only say both
// again. Quick replies has no placeholder — there it is the only place
// the target is named, so it stays.
let line = null;
if (mode !== 'send') {
const head = el('div', 'sl-compose-head'); const head = el('div', 'sl-compose-head');
const line = el('span', 'sl-sending', sendingLine()); line = el('span', 'sl-sending', sendingLine());
const lockLink = el('button', 'sl-lock-link', 'Lock'); const lockLink = el('button', 'sl-lock-link', 'Lock');
lockLink.type = 'button'; lockLink.type = 'button';
lockLink.title = 'Lock now'; lockLink.title = 'Lock now';
lockLink.addEventListener('click', () => lock('Locked')); lockLink.addEventListener('click', () => lock('Locked'));
head.append(line, lockLink); head.append(line, lockLink);
compose.appendChild(head); compose.appendChild(head);
}
const foot = el('div', 'sl-compose-foot'); const foot = el('div', 'sl-compose-foot');
const msg = el('div', 'sl-msg'); const msg = el('div', 'sl-msg');
@@ -1462,7 +1473,7 @@ function createSendTile({ root, moduleApi, mode }) {
if (!s.sending?.enabled || !s.sending?.pinSet) return void lock(''); if (!s.sending?.enabled || !s.sending?.pinSet) return void lock('');
if (!composeEls) return void showCompose(); // the page was unlocked before this tile appeared if (!composeEls) return void showCompose(); // the page was unlocked before this tile appeared
if (composeEls) { if (composeEls) {
composeEls.line.textContent = sendingLine(); if (composeEls.line) composeEls.line.textContent = sendingLine();
if (composeEls.input) composeEls.input.placeholder = placeholder(); if (composeEls.input) composeEls.input.placeholder = placeholder();
} }
} else renderGate(); } else renderGate();
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"id": "slack", "id": "slack",
"name": "Slack", "name": "Slack",
"version": "1.2.0", "version": "1.2.1",
"proddash": ">=1.4.0", "proddash": ">=1.4.0",
"description": "Live transcripts of this location's Slack channels, plus PIN-protected sending as the location", "description": "Live transcripts of this location's Slack channels, plus PIN-protected sending as the location",
"client": "client.js", "client": "client.js",
@@ -21,7 +21,7 @@
"help": "Create a Slack app in the workspace (api.slack.com/apps → Create New App → From scratch) and, under OAuth & Permissions → User Token Scopes, add channels:history, channels:read, groups:history, groups:read, chat:write, users:read, reactions:write (emoji reactions) and emoji:read (the workspace's custom emoji). Sign in to Slack as this location's account — the one named for the campus — open the app's OAuth & Permissions page and press Install to Workspace (or Reinstall to Workspace), then copy the User OAuth Token (xoxp-…) it shows and paste it here: messages from this machine are then literally from the location. A bot token (xoxb-…) works too; it posts as the app, with \"Send as\" naming the location." "help": "Create a Slack app in the workspace (api.slack.com/apps → Create New App → From scratch) and, under OAuth & Permissions → User Token Scopes, add channels:history, channels:read, groups:history, groups:read, chat:write, users:read, reactions:write (emoji reactions) and emoji:read (the workspace's custom emoji). Sign in to Slack as this location's account — the one named for the campus — open the app's OAuth & Permissions page and press Install to Workspace (or Reinstall to Workspace), then copy the User OAuth Token (xoxp-…) it shows and paste it here: messages from this machine are then literally from the location. A bot token (xoxb-…) works too; it posts as the app, with \"Send as\" naming the location."
}, },
"Channels": { "Channels": {
"help": "Every channel picked here becomes its own transcript tile in the + picker, titled with the channel's name. The Send message and Quick replies tiles post to whichever transcript was clicked last — its bottom edge lights up in the accent colour — and to the first channel here until one is clicked. Only channels the account has joined are listed; join one in Slack first. Each channel is read on its own schedule, all of them together kept inside Slack's rate limit." "help": "Every channel picked here becomes its own transcript tile in the + picker, titled with the channel's name. The Send message and Quick replies tiles post to whichever transcript was clicked last — its bottom edge lights up in the accent colour — and until one is clicked, to the first channel here whose transcript is on that dashboard: with a single transcript up, that one. A send never goes to a channel the dashboard is not showing. Only channels the account has joined are listed; join one in Slack first. Each channel is read on its own schedule, all of them together kept inside Slack's rate limit."
}, },
"Sending": { "Sending": {
"toggle": "sendEnabled", "toggle": "sendEnabled",
@@ -49,7 +49,7 @@
"default": [], "default": [],
"group": "Channels", "group": "Channels",
"optionsRoute": "/channels", "optionsRoute": "/channels",
"help": "One transcript tile per channel; the first one is where sends go until a transcript is clicked. Only channels the account has joined are listed — join one in Slack first." "help": "One transcript tile per channel; until a transcript is clicked, sends go to the first channel here that the dashboard is actually showing. Only channels the account has joined are listed — join one in Slack first."
}, },
"pollSeconds": { "pollSeconds": {
"type": "number", "type": "number",