Slack module: several channels, send tiles follow the transcript clicked last
One Slack tile per channel instead of one channel per machine. Admin picks
any number of the account's channels (`channels`, a multiselect fed by
/channels; a `channel` string saved by 1.0.0 is read as a one-item list, the
module never rewrites admin config). tiles() offers one transcript entry per
channel — id "transcript:<id>", named "#<channel>" so the shell titles the
tile with it, settings { view, channel } — plus Send message and Quick
replies, each `single: true`. A transcript tile from 1.0.0 (no channel in its
settings) shows the first configured channel.
Server: one poller per channel, each with its own window, thread cache,
membership and error, sharing the token, the name caches and the SSE fan-out.
Slack allows an internal app ~50+ conversations.history calls a minute per
token, so the module keeps itself under 40:
interval per channel = max(pollSeconds, ceil(channels × 60 / 40)) s
1 → 2 s, 2 → 3 s, 3 → 5 s, 5 → 8 s
with pollers phase-locked across that interval so they never fire in a
burst (first reads staggered 250 ms apart after boot), one auth.test shared
by all of them, and a 429 backing every poller off together. /state returns
`channels` keyed by id (name, isMember, messages, pollAge, error, status)
plus `channelOrder`; /stream stays one `state` snapshot per change. The
health line lists the channels and names any with a problem:
"Apollo Beach · #production, #ops · every 3 s (rate budget) · sending on".
/send and /quick take `channel`, which must be one of the configured ids
(400 otherwise) — a PIN-unlocked browser chooses among the admin's channels,
never an arbitrary one; a quick reply's own "| #channel" wins over it.
Client: a click anywhere in a transcript's body makes its channel the send
target; the chosen transcript carries a 3px accent stripe along the bottom
of the tile body (the chrome is the shell's), the others hint at it on hover.
The selection is one fact about the whole page, so it lives in a deliberate
module-level store in client.js (an EventTarget plus the id, mirrored in
localStorage under proddash:slack:sendTarget so a reload keeps it) — the
comment there says why this is not the per-instance state the guide forbids.
Send message and Quick replies show "Sending as Apollo Beach → #ops" and
follow the selection live; when the selected channel is no longer configured
or its transcript tile is gone, sends fall back to the first configured
channel, and the headers say so. PIN pad, lockout, unlock tokens, Enter to
send, auto-lock, the transcript rendering and the per-tile settings are as
they were.
The mock gains a quiet #ops trickle so two transcripts visibly move at their
own pace; a post to either channel already lands in that channel's history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7d34604f2f
commit
b9e895d324
+25
-1
@@ -26,7 +26,15 @@
|
||||
* #production is seeded with mrkdwn, mentions, a link, a thread, a bot
|
||||
* attachment and a file; it gains a new fake message every ~8 s, edits one
|
||||
* every ~25 s, deletes one every ~40 s and adds a thread reply every ~30 s.
|
||||
* Every request is logged.
|
||||
* #ops gets a quieter trickle (every ~20 s), so a dashboard with both
|
||||
* transcripts shows two channels moving at their own pace — and a
|
||||
* chat.postMessage to either lands in that channel's history, which is how
|
||||
* a send to the *selected* transcript can be watched arriving in the right
|
||||
* tile. Every request is logged.
|
||||
*
|
||||
* Multi-channel dev flow: run this, then ProdDash with SLACK_API_BASE set,
|
||||
* paste xoxp-anything in /admin → Slack, tick #production and #ops under
|
||||
* Channels, Apply, and add both transcripts plus Send message from +.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
@@ -116,6 +124,14 @@ const CHATTER = [
|
||||
];
|
||||
let chatterIx = 0;
|
||||
|
||||
const OPS_CHATTER = [
|
||||
{ user: 'U005', text: 'Ops: parking team is short two people, sending greeters over' },
|
||||
{ user: 'U002', text: 'Cafe is out of oat milk again :sweat_smile:' },
|
||||
{ user: 'U005', text: 'Second-service headcount: 412 in the room, 38 online' },
|
||||
{ user: 'U002', text: 'Facilities: the lobby HVAC is back, thanks Dan' },
|
||||
];
|
||||
let opsIx = 0;
|
||||
|
||||
function tickNew() {
|
||||
const line = CHATTER[chatterIx % CHATTER.length];
|
||||
chatterIx += 1;
|
||||
@@ -123,6 +139,13 @@ function tickNew() {
|
||||
console.log(`[slack-mock] + new message ${m.ts} from ${line.user}`);
|
||||
}
|
||||
|
||||
function tickOps() {
|
||||
const line = OPS_CHATTER[opsIx % OPS_CHATTER.length];
|
||||
opsIx += 1;
|
||||
const m = push('C002', { user: line.user, text: line.text });
|
||||
console.log(`[slack-mock] + new message ${m.ts} in #ops from ${line.user}`);
|
||||
}
|
||||
|
||||
function tickEdit() {
|
||||
const list = history.get('C001');
|
||||
const recent = list.slice(-8).filter((m) => m.user && !m.subtype && !m.edited);
|
||||
@@ -154,6 +177,7 @@ function tickReply() {
|
||||
}
|
||||
|
||||
setInterval(tickNew, 8000).unref();
|
||||
setInterval(tickOps, 20000).unref();
|
||||
setInterval(tickEdit, 25000).unref();
|
||||
setInterval(tickDelete, 40000).unref();
|
||||
setInterval(tickReply, 30000).unref();
|
||||
|
||||
Reference in New Issue
Block a user