mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 07:53:11 +00:00
151 lines
7.2 KiB
Markdown
151 lines
7.2 KiB
Markdown
# Evaluation: sticky / reused weekly schedule post
|
|
|
|
_Spike for issue #135 — decide how the Monday two-week schedule post should
|
|
behave across weeks. This document records the decision and the rationale; the
|
|
recommended option (b) is implemented in the same PR._
|
|
|
|
## Background
|
|
|
|
`src/weekly-post/app.py` (the Monday 7am ET Lambda) originally, every week:
|
|
|
|
1. `chat_delete` the previous week's schedule post (stored `ts` from
|
|
`get_schedule_post`),
|
|
2. `chat_postMessage` a brand-new two-week schedule message, and
|
|
3. `save_schedule_post(channel_id, new_ts, …)`.
|
|
|
|
So every Monday the post got a new `ts` (new permalink), re-notified the
|
|
channel, and lost any thread/reactions. During the week it steadily sank as
|
|
people chatted.
|
|
|
|
The infrastructure to edit in place **already exists**: in-week shift changes
|
|
(`pick` / `drop` / `swap` / `admin`) call `_refresh_schedule_post()`
|
|
(`src/slack-bot/app.py`), which does a `chat_update` against the stored `ts`.
|
|
|
|
## Options considered
|
|
|
|
### (a) Reuse the same post across weeks — pin for reachability
|
|
|
|
On Monday, `chat_update` the existing message to roll the two-week window
|
|
forward instead of delete + repost, and add a native Slack pin so the post is
|
|
reachable from the channel header.
|
|
|
|
- **Pros:** stable permalink; no weekly re-notification; keeps any
|
|
thread/reactions; trivial change (reuses the `_refresh_schedule_post`
|
|
`chat_update` pattern already in the codebase).
|
|
- **Cons:** the message stays where it was first posted — it does **not** rise
|
|
to the bottom as the channel gets new activity. A native pin only surfaces the
|
|
post in the channel's pinned-items panel (the header); it does **not** hold the
|
|
message at the bottom of the timeline, which is the actual requirement from
|
|
#135.
|
|
|
|
### (b) True stickybot behavior — keep it at the bottom — **recommended**
|
|
|
|
Slack has no native "sticky" message. Stickybot-style bots subscribe to channel
|
|
message events and **delete + repost** the bot message whenever someone else
|
|
posts, so it is always last.
|
|
|
|
- **Pros:** the schedule post is always at the literal bottom of the channel
|
|
timeline — exactly the behavior #135 asks for.
|
|
- **Cons:** every bump is a new `ts` (the permalink changes); the post
|
|
re-surfaces / re-notifies on activity; requires a new `message.channels` event
|
|
subscription, the `channels:history` scope (a one-time reinstall), and
|
|
debounce machinery to avoid thrashing on bursts.
|
|
|
|
### (c) Status quo — delete + repost weekly
|
|
|
|
- **Pros:** the weekly repost bumps the post to the bottom once a week.
|
|
- **Cons:** new permalink every week; a weekly re-notification; loses
|
|
thread/reactions; the post still sinks for the rest of the week.
|
|
|
|
## Decision
|
|
|
|
Adopt **(b) true bottom-of-channel stickiness**. Adam wants literal
|
|
bottom-of-channel placement of the schedule post, and explicitly accepts the
|
|
tradeoffs that come with it:
|
|
|
|
1. **The permalink changes on each bump** — every delete + repost produces a new
|
|
`ts`, so any saved link to the post goes stale.
|
|
2. **The post re-surfaces / re-notifies on activity** — re-posting pings the
|
|
channel (subject to the debounce window below), rather than sitting quietly.
|
|
3. **New machinery** — a `message.channels` event subscription, the
|
|
`channels:history` scope (one-time reinstall), and a debounce to avoid
|
|
thrashing on bursts.
|
|
|
|
The **native pin (option a's complement) was rejected**: it only gives header
|
|
reachability — the post shows up in the pinned-items panel — but it does **not**
|
|
hold the message at the bottom of the timeline, which is the requirement. A pin
|
|
is therefore not a substitute for bottom-stickiness.
|
|
|
|
## What changes (option b)
|
|
|
|
### Weekly rollover — `src/weekly-post/app.py`
|
|
|
|
- **Drop the unconditional `chat_delete` + always-repost.** On Monday,
|
|
`chat_update` the stored post to roll the two-week window forward, keeping the
|
|
same `ts` (the activity bump is what moves it to the bottom; the rollover just
|
|
refreshes content).
|
|
- **First-run / recovery fallback:** when there is no stored post or the
|
|
`chat_update` fails (e.g. the message was deleted manually), `chat_postMessage`
|
|
a fresh message and store its `ts`.
|
|
- The `pins:write` scope and the `_pin_schedule_post` helper are removed — the
|
|
pin is fully replaced by bottom-stickiness.
|
|
|
|
### Activity bump — `src/slack-bot/app.py`
|
|
|
|
The bump lives in the **slack-bot** Lambda, which already owns the Slack events
|
|
request URL. On a `message.channels` event in the schedule channel
|
|
(`handle_channel_message`):
|
|
|
|
- **Debounced** so a burst of chatter triggers at most one bump:
|
|
`SCHEDULE_BUMP_DEBOUNCE_SECONDS = 180` (3 minutes). The last bump time is
|
|
stored as `last_bump_ts` on the schedule-post record, so the debounce check
|
|
reads no channel history.
|
|
- **Idempotent / safe** — the bump is skipped when:
|
|
- the delivery is a Slack **retry** (`X-Slack-Retry-Num` set) — under
|
|
`process_before_response=True` the delete+repost runs before the 200 ack, so
|
|
a slow run can be retried; we never bump on a retry (the next real message
|
|
bumps anyway),
|
|
- the triggering message is a **thread reply** (`thread_ts`) — a threaded reply
|
|
doesn't push the schedule down the main timeline,
|
|
- there is no stored post,
|
|
- the triggering message is the bot's own (`bot_id`) or a non-user message
|
|
subtype (edits, deletes, joins, …),
|
|
- the debounce window has not elapsed, or
|
|
- the bot isn't in the channel (`not_in_channel`).
|
|
- Otherwise it **stamps `last_bump_ts` optimistically before** the delete/repost
|
|
(so a retry fired mid-bump — or after the run dies before the final save — is
|
|
debounced and can't orphan a duplicate), `chat_delete`s the stored post,
|
|
`chat_postMessage`s the current `build_week_schedule` blocks, then
|
|
`save_schedule_post`s the new `ts`. If the old message is already gone, it
|
|
still reposts so the channel always ends with the schedule.
|
|
|
|
### Interaction with `_refresh_schedule_post` and the rollover
|
|
|
|
All three paths converge on the **single stored `ts`** and never fight:
|
|
|
|
- **Monday rollover** — `chat_update` the stored `ts` (content refresh; same
|
|
message), clearing `last_bump_ts` so the next activity is free to bump.
|
|
- **In-week shift edits** — `_refresh_schedule_post` `chat_update`s the same
|
|
stored `ts`; it does not change the `ts` or touch `last_bump_ts`.
|
|
- **Activity bump** — deletes the stored `ts`, reposts the same content at the
|
|
bottom, and saves the new `ts`. Subsequent rollovers and edits then operate on
|
|
that new `ts`.
|
|
|
|
Because only the bump ever mints a new `ts` (and it always re-saves it
|
|
immediately), the other two paths always read the current `ts` from the record.
|
|
|
|
## Scope / manifest impact
|
|
|
|
- `channels:history` (bot scope) and `message.channels` (bot event) are added to
|
|
`slack-app-manifest.yaml` so the slack-bot Lambda receives channel messages and
|
|
can run the bump. This is a new scope + event, so the app must be
|
|
**reinstalled** once for them to take effect.
|
|
- `pins:write` is **removed** — the pin is no longer used.
|
|
|
|
## Follow-ups (not in this PR)
|
|
|
|
- After deploy, reinstall the Slack app so `channels:history` /
|
|
`message.channels` take effect, then confirm a new channel message bumps the
|
|
schedule post to the bottom (and that bursts are debounced).
|
|
- If the re-notification on each bump proves noisy, revisit the debounce window
|
|
or consider posting without a broadcast.
|