Slack module: channel transcript + PIN-gated sending as the location

Each campus signs its booth machine into Slack as the account named for
the location, so a message from ProdDash reads as "Apollo Beach" rather
than as whoever is at the desk. modules/slack (1.0.0) is three tiles from
one module: the live transcript of one channel, a free-text Send tile and
a Quick-replies grid — the last two behind a PIN pad, auto-locking after
a configurable idle time.

Server: one user (xoxp-) or bot (xoxb-) token, identified with auth.test;
conversations.history polled every N seconds (thread replies via
conversations.replies, capped per poll), diffed and fanned out as one
`state` SSE snapshot; mrkdwn rendered to safe text on the server; sends
through chat.postMessage (username/icon for a bot with "Send as"); PIN
compared in constant time with a 5-wrong → 30 s lockout, 32-byte unlock
tokens with a sliding expiry, 1 message/s, every send logged; readable
messages for invalid_auth / token_revoked / missing_scope /
not_in_channel / ratelimited; health reflects token → channel → poll →
sending. SLACK_API_BASE points it at tools/slack-mock.js (new).

Shell: the admin config form gains a multi-line field type `text` (a
textarea; one item per line by convention) — used for the quick replies.
Documented in the module guide; the mock is listed in CLAUDE.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-14 11:49:40 -04:00
co-authored by Claude Fable 5.1
parent f0fbf8a8f6
commit babe8263eb
9 changed files with 2637 additions and 1 deletions
+11
View File
@@ -239,6 +239,17 @@ function fieldFor(key, spec, value, passwordSet, moduleId) {
input.value = hex(value, hex(spec?.default, '#2ee59a'));
field.append(span, input);
read = () => input.value;
} else if (type === 'text') {
// A multi-line string — one item per line by convention (a list of
// presets, say). Stored as-is, newlines included.
const span = document.createElement('span');
span.textContent = label;
const textarea = document.createElement('textarea');
textarea.rows = 5;
textarea.spellcheck = false;
textarea.value = value === undefined || value === null ? '' : String(value);
field.append(span, textarea);
read = () => textarea.value;
} else {
const span = document.createElement('span');
span.textContent = label;