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:
co-authored by
Claude Fable 5.1
parent
f0fbf8a8f6
commit
babe8263eb
@@ -135,6 +135,22 @@ body { overflow: auto; }
|
||||
.module-form { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
|
||||
.module-form .field { max-width: 420px; }
|
||||
|
||||
/* a multi-line string (manifest type `text`): the same box as the other
|
||||
inputs (those rules live in the shell's style.css), a few lines tall */
|
||||
.field textarea {
|
||||
padding: 7px 9px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
line-height: 1.4;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
resize: vertical;
|
||||
min-height: 72px;
|
||||
}
|
||||
.field textarea:focus { outline: none; border-color: var(--accent); }
|
||||
|
||||
/* endpoint fields: every module's upstream is entered the same way */
|
||||
.field .endpoint-row { display: flex; align-items: center; gap: 7px; }
|
||||
.endpoint-row .ep-host { flex: 1; min-width: 0; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user