Night shift notifications on weekends were missing the type label,
making them ambiguous. Also fix schedule post text fallback to use
this_monday instead of now for the start date.
Dependabot now scans per-function requirement directories instead
of the repo root. Admin override/open/clear commands accept an
optional day/night parameter for weekend day shift management.
- Add _find_employee_shift() to check both day/night on weekends
- Drop/swap now correctly find and operate on weekend day shifts
- Pick finds first available shift type on weekends
- Admin override/open/clear update 3CX for same-day active shifts
- Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query)
- Button pickup: update 3CX for active shift type, not just night
- Pick/drop/swap commands: only update 3CX when shift type is active
- Swap command: add missing past-date guard
- add_roster_entry: reject if extension already exists
- Apply ruff formatting
The 3CX routing was changed from ring group 800 to queue 801 in a
previous PR on ring-scheduler-3cx. Updates all callers and the SAM
template parameter default accordingly.
- Reject /oncall pick and /oncall drop for past dates
- Show ephemeral error when stale pickup buttons are clicked
- Hide pickup buttons for dates in the past
- Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to
schedule lines, pickup buttons, and shift change notifications
- Store schedule message timestamp in DynamoDB (SCHEDULE_POST record)
- Delete previous week's schedule post before posting the new one
- Live-update the schedule post via chat_update after any
pick/drop/swap/button-pickup so it always reflects current state
- Add afterhours-ring-scheduler Lambda with 4 EventBridge rules
(daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group
routing updates
- Extract shared ring_scheduler.py module for direct ring group
updates from both the scheduled Lambda and the Slack bot
- Replace cross-Lambda invoke with direct update_ring_group() call
in the Slack bot — eliminates lambda:InvokeFunction dependency
- Use RingGroup API (correct) instead of Queue API (was wrong in
the original ring-scheduler repo)
- Eliminate YAML config fallback — DynamoDB is the sole schedule
source
- Add RingGroupNumber CloudFormation parameter
- Slack bot token and signing secret now read from Secrets Manager
- 3CX credentials (domain, client-id, client-secret) moved to
Secrets Manager under afterhours-shift-manager/3cx-* prefix
- Channel ID is now a non-secret CloudFormation parameter (ShiftChannel)
- Add shared secrets.py helper for Secrets Manager reads
- Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue
Move from flat src/ to per-function directories:
- src/slack-bot/ — Slack Bolt Lambda handler
- src/weekly-post/ — Monday schedule + pay post
- src/roster-sync/ — Daily 3CX roster sync
- src/shared/ — Lambda Layer with schedule, blocks, three_cx_client
Each function has its own requirements.txt and CodeUri. Shared
modules are deployed as a SAM Layer (afterhours-shared) importable
as `from shared.X import Y`.
Two root causes:
- claim_open_shift ConditionExpression failed when no OVERRIDE record
existed (shift available via weekly fallback). Added attribute_not_exists
check so claims succeed for both missing and OPEN overrides.
- Slack ack timeout: chat_postMessage took too long before respond() was
called, causing Slack to show an error. Moved respond() first and made
channel notifications best-effort with try/except.
Slash command handlers (drop, pick, swap) were posting notifications to
command["channel_id"] — wherever the command was run. If someone ran
/oncall drop from a DM, the notification went there instead of the
schedule channel. Pickup buttons didn't have this problem because
body["channel"]["id"] is always the channel where the button lives.
Added SHIFT_CHANNEL_PARAM to the SlackBotFunction env vars, read it on
cold start, and route all slash command shift-change notifications to
the configured schedule channel.
Closes#21
The pickup button handler used an unconditional put_item, so concurrent
clicks would both succeed with last-write-wins. Added claim_open_shift()
which uses a DynamoDB ConditionExpression to only write if the shift is
still OPEN. The button handler now returns an ephemeral "already taken"
message when the condition fails.
Closes#19
The handler passed `now` as start_date, so mid-week runs showed
14 days from today instead of Mon–Sun x2. Also, day names were
derived from a static index (DAY_ORDER[i%7]) which assumed
index 0 = Monday — when start_date wasn't Monday, Saturday and
Sunday were mislabeled as Monday and Tuesday.
Fix: pass this week's Monday from the handler, and derive day
names from the actual date via strftime instead of a static list.
Fixes#17
- Pay summary sent as DM to designated user instead of channel
- Email shows only per-person totals, no shift breakdown
- Renamed email subject/header to "Bonus Pay Summary"
Sends an HTML pay summary email to configured payroll recipients
alongside the existing Slack post. Uses SES with noreply@seahaven.com
as sender. Recipients configured via PAYROLL_RECIPIENTS env var
(set to adam@seahaven.com for testing, switch to payroll@seahaven.com
for production).
Closes#3
Syncs DynamoDB roster from a configured 3CX group daily at 6am ET
(before the 7am schedule post). Includes ThreeCXClient for XAPI
authentication and group member queries. Preserves existing
slack_user_id links and guards against accidental roster wipes.
Closes#4
Saturdays and Sundays now have two shift slots: a day shift (8am–5pm)
and the existing after-hours shift. Extension 100 (fallback) is
excluded from pay calculations. Pickup buttons and schedule display
updated to handle both shift types.
- /oncall rate — view current default and per-person rates
- /oncall rate default <amount> — set the global fallback rate
- /oncall rate <extension> <amount> — set a person-specific rate
Each roster entry can now have its own shift_rate, falling back to the
global CONFIG rate when not set. Pay breakdowns show individual rates
per shift and per-person totals.
Calculates after-hours pay per shift using a configurable rate stored
in DynamoDB CONFIG. Pay summaries are computed on Monday mornings for
the previous week, stored in DynamoDB (PK=PAY), and posted to Slack.
Adds /oncall pay command for on-demand viewing.
Closes#2
Slack Bolt app on Lambda for managing on-call shifts. Employees can
pick up, drop, and swap shifts via /oncall commands. Changes update
3CX ring group 800 routing in real time for same-day shifts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>