afterhours-shift-manager/src/slack-bot/app.py

1991 lines
70 KiB
Python
Raw Normal View History

Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
"""Slack Bolt app — /oncall command handlers and interactive actions.
The handler functions are module-level (not closures over ``create_app``) so they
can be unit-tested directly. ``schedule`` (a ``ShiftSchedule``) and
``schedule_channel`` are threaded through as explicit parameters. ``create_app``
is a thin wiring layer that registers the Bolt routes and delegates to them.
"""
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
import functools
import json
import logging
import os
import re
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import boto3
from boto3.dynamodb.conditions import Key
from slack_bolt import App
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
from shared.blocks import (
build_help_blocks,
build_holiday_added_blocks,
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
build_home_view,
build_pay_summary_blocks,
build_pickup_request_blocks,
build_pickup_resolved_blocks,
build_roster_blocks,
build_shift_change_message,
build_swap_request_blocks,
build_swap_resolved_blocks,
build_week_schedule,
)
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
from shared.changelog import latest_entry
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
from shared.ring_scheduler import update_queue_routing
from shared.schedule import (
FALLBACK_EXTENSION,
WEEKEND_DAYS,
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
ExtensionAlreadyRegistered,
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
ShiftSchedule,
determine_shift_type,
)
from shared.secrets import get_secret
from shared.three_cx_client import ThreeCXClient
logger = logging.getLogger(__name__)
EASTERN = ZoneInfo("America/New_York")
DAY_NAMES = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
def parse_date(text: str) -> datetime | None:
"""Parse flexible date input: today, tomorrow, day names, m/d, YYYY-MM-DD."""
now = datetime.now(EASTERN)
text = text.strip().lower()
if text == "today":
return now
if text == "tomorrow":
return now + timedelta(days=1)
if text in DAY_NAMES:
target = DAY_NAMES.index(text)
current = now.weekday()
delta = (target - current) % 7
if delta == 0:
delta = 7
return now + timedelta(days=delta)
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m/%d", "%m-%d"):
try:
parsed = datetime.strptime(text, fmt)
if "%Y" not in fmt:
parsed = parsed.replace(year=now.year)
if parsed.replace(tzinfo=EASTERN) < now - timedelta(days=1):
parsed = parsed.replace(year=now.year + 1)
return parsed.replace(tzinfo=EASTERN)
except ValueError:
continue
return None
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
def _update_3cx_routing(extension: str) -> None:
"""Update the 3CX queue to forward calls to the given extension."""
queue_number = os.environ.get("QUEUE_NUMBER")
secret_prefix = os.environ.get("TCX_SECRET_PREFIX")
if not queue_number or not secret_prefix:
logger.warning("3CX env vars not set — skipping queue update")
return
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
try:
update_queue_routing(
extension=extension,
queue_number=queue_number,
domain=get_secret(f"{secret_prefix}domain"),
client_id=get_secret(f"{secret_prefix}client-id"),
client_secret=get_secret(f"{secret_prefix}client-secret"),
)
except Exception:
logger.exception("Failed to update 3CX queue")
def _make_3cx_client() -> ThreeCXClient | None:
"""Build an OAuth ThreeCXClient from the configured secret prefix, or None.
Mirrors ``_update_3cx_routing``'s tolerance of a missing config: returns
None (and logs) rather than raising when ``TCX_SECRET_PREFIX`` is unset, so
holiday flows degrade gracefully in environments without 3CX wired up.
"""
secret_prefix = os.environ.get("TCX_SECRET_PREFIX")
if not secret_prefix:
logger.warning("3CX env vars not set — skipping 3CX call")
return None
return ThreeCXClient(
domain=get_secret(f"{secret_prefix}domain"),
auth_mode="oauth",
client_id=get_secret(f"{secret_prefix}client-id"),
client_secret=get_secret(f"{secret_prefix}client-secret"),
)
def _set_holiday_queue_agents(schedule, date_str: str) -> None:
"""Point the holiday queue (802) at the current holiday assignees.
Used by the inline-activation path and the late-pickup approval path when
the holiday window is open — the membership must reflect the live assignee
set. Falls back to ``[FALLBACK_EXTENSION]`` ("100") when no slot is filled.
Best-effort: any failure is logged and swallowed so the Slack flow still
completes.
"""
holiday = schedule.get_holiday(date_str)
if holiday is None:
return
assignees = holiday.get("assignees", {}) or {}
extensions = list(assignees.keys()) or [FALLBACK_EXTENSION]
try:
client = _make_3cx_client()
if client is None:
return
queue_number = schedule.get_holiday_queue()
queue = client.get_queue(queue_number)
client.set_queue_agents(queue["Id"], extensions)
except Exception:
logger.exception("Failed to set holiday queue agents for %s", date_str)
def _activate_holiday_inline(schedule, date_str: str) -> None:
"""Invoke the holiday router's activate path now, for a holiday added late.
When an admin schedules a holiday whose window is already open (08:00 ≤ now <
17:00 ET), the 08:00 activation schedule has already passed, so the call flow
must be repointed immediately. We invoke the holiday-router Lambda
asynchronously so the (idempotent) activate logic — IVR capture/repoint,
queue membership, ``activated`` flag — runs exactly as it would at 08:00.
Best-effort: a missing ARN or invoke failure is logged, not raised.
"""
router_arn = os.environ.get("HOLIDAY_ROUTER_ARN")
if not router_arn:
logger.warning("HOLIDAY_ROUTER_ARN not set — skipping inline activation")
return
try:
boto3.client("lambda").invoke(
FunctionName=router_arn,
InvocationType="Event",
Payload=json.dumps({"action": "activate", "date": date_str}).encode(),
)
logger.info("Invoked holiday router inline activate for %s", date_str)
except Exception:
logger.exception("Failed to invoke holiday router for %s", date_str)
def _holiday_schedule_names(date_str: str) -> tuple[str, str]:
"""The (activate, deactivate) one-off schedule names for a holiday date."""
compact = date_str.replace("-", "")
return f"holiday-activate-{compact}", f"holiday-deactivate-{compact}"
def _create_holiday_schedules(date_str: str) -> list[str]:
"""Create the two one-off EventBridge schedules for a holiday and return names.
One schedule fires the holiday router's ``activate`` at 08:00 ET on the
date, the other its ``deactivate`` at 17:00 ET. Both use a flexible
one-time ``at(...)`` expression in ``America/New_York``,
``ActionAfterCompletion=DELETE`` (self-cleanup once fired), and target the
holiday-router Lambda via the passed scheduler execution role.
Returns the created schedule names (stored on the HOLIDAY record so a later
``remove`` can delete any that have not yet fired). Best-effort: returns the
names it managed to create; missing config short-circuits to ``[]``.
"""
router_arn = os.environ.get("HOLIDAY_ROUTER_ARN")
role_arn = os.environ.get("HOLIDAY_SCHEDULER_ROLE_ARN")
group = os.environ.get("HOLIDAY_SCHEDULE_GROUP", "default")
if not router_arn or not role_arn:
logger.warning(
"HOLIDAY_ROUTER_ARN/HOLIDAY_SCHEDULER_ROLE_ARN not set — "
"skipping schedule creation"
)
return []
activate_name, deactivate_name = _holiday_schedule_names(date_str)
client = boto3.client("scheduler")
created: list[str] = []
specs = [
(activate_name, "activate", "08:00:00"),
(deactivate_name, "deactivate", "17:00:00"),
]
for name, action, at_time in specs:
try:
client.create_schedule(
Name=name,
GroupName=group,
ScheduleExpression=f"at({date_str}T{at_time})",
ScheduleExpressionTimezone="America/New_York",
FlexibleTimeWindow={"Mode": "OFF"},
ActionAfterCompletion="DELETE",
Target={
"Arn": router_arn,
"RoleArn": role_arn,
"Input": json.dumps({"action": action, "date": date_str}),
},
)
created.append(name)
except Exception:
logger.exception("Failed to create %s schedule for %s", action, date_str)
return created
def _delete_holiday_schedules(schedule_names: list[str]) -> None:
"""Delete any still-outstanding one-off holiday schedules (best-effort).
A schedule that has already fired self-deletes (ActionAfterCompletion=DELETE),
so a ResourceNotFound on delete is expected and ignored.
"""
if not schedule_names:
return
group = os.environ.get("HOLIDAY_SCHEDULE_GROUP", "default")
try:
client = boto3.client("scheduler")
except Exception:
logger.exception("Could not create scheduler client to delete schedules")
return
for name in schedule_names:
try:
client.delete_schedule(Name=name, GroupName=group)
except client.exceptions.ResourceNotFoundException:
logger.info("Holiday schedule %s already gone — nothing to delete", name)
except Exception:
logger.exception("Failed to delete holiday schedule %s", name)
def is_today(date_str: str) -> bool:
return date_str == datetime.now(EASTERN).strftime("%Y-%m-%d")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
def _is_active_shift_type(shift_type: str) -> bool:
return determine_shift_type() == shift_type
def _shift_start(date_str: str, shift_type: str) -> datetime:
"""The datetime (ET) a shift begins — weekend day at 08:00, otherwise 17:00."""
d = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=EASTERN)
return d.replace(hour=8 if shift_type == "day" else 17)
def _shift_started(date_str: str, shift_type: str) -> bool:
return datetime.now(EASTERN) >= _shift_start(date_str, shift_type)
def _shift_end(date_str: str, shift_type: str) -> datetime:
"""The datetime (ET) a shift ends.
A day shift (weekend day or holiday, 08:00–17:00 ET) ends at 17:00 the same
day. A night shift (17:00–08:00 ET) ends at 08:00 the *next* day.
"""
d = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=EASTERN)
if shift_type == "day":
return d.replace(hour=17)
return d.replace(hour=8) + timedelta(days=1)
def _shift_ended(date_str: str, shift_type: str) -> bool:
return datetime.now(EASTERN) >= _shift_end(date_str, shift_type)
def _holiday_window_active(date_str: str) -> bool:
"""True when a holiday day-shift window (08:00–17:00 ET) is currently open.
The 3CX call flow is only repointed to the holiday queue during this window,
so inline activation (admin add) and late-pickup membership refreshes are
gated on it.
"""
now = datetime.now(EASTERN)
return _shift_start(date_str, "day") <= now < _shift_end(date_str, "day")
def _within_drop_lock(date_str: str, shift_type: str) -> bool:
"""True inside the 24h-before-start window where dropping a shift is locked.
Within this window a shift can't be abandoned via drop — it must be handed
off through a verified swap (target accepts) or opened by an admin.
"""
return datetime.now(EASTERN) >= _shift_start(date_str, shift_type) - timedelta(
hours=24
)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
def _shift_type_label(day_name: str, shift_type: str) -> str:
if day_name not in WEEKEND_DAYS:
return ""
label = "Day" if shift_type == "day" else "Night"
return f" ({label})"
def _find_employee_shift(schedule, date_str, day_name, employee_ext):
"""Find which shift type an employee is assigned to on a given date.
On weekends, checks both day and night shifts. Returns (ext, name, source, shift_type)
or None if not found on any shift.
"""
if day_name in WEEKEND_DAYS:
for st in ("day", "night"):
ext, name, source = schedule.resolve_shift(date_str, day_name, st)
if ext == employee_ext:
return ext, name, source, st
return None
ext, name, source = schedule.resolve_shift(date_str, day_name, "night")
if ext == employee_ext:
return ext, name, source, "night"
return None
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _refresh_schedule_post(schedule, schedule_channel, client):
"""Update the pinned schedule message in-place after a shift change."""
channel = schedule_channel
if not channel:
return
post = schedule.get_schedule_post(channel)
if not post or not post.get("message_ts"):
return
try:
blocks = build_week_schedule(schedule)
now = datetime.now(EASTERN)
this_monday = now - timedelta(days=now.weekday())
end_date = this_monday + timedelta(days=13)
client.chat_update(
channel=channel,
ts=post["message_ts"],
blocks=blocks,
text=f"After-Hours Schedule — {this_monday.strftime('%b %-d')} to {end_date.strftime('%b %-d')}",
)
except Exception:
logger.warning("Could not update schedule post", exc_info=True)
# ── Command dispatch ────────────────────────────────────────────────────
def dispatch_oncall(command, respond, client, schedule, schedule_channel):
"""Route a /oncall slash command to the appropriate subcommand handler."""
text = (command.get("text") or "").strip()
user_id = command["user_id"]
channel_id = command["channel_id"]
is_admin = user_id in schedule.get_admin_users()
post_channel = schedule_channel or channel_id
if not text or text == "schedule":
_show_schedule(respond, schedule)
elif text == "next":
_show_next_week(respond, schedule)
elif text == "help":
respond(blocks=build_help_blocks(is_admin=is_admin))
elif text == "roster":
respond(blocks=build_roster_blocks(schedule.get_roster()))
elif text == "pay":
_show_pay(respond, schedule)
elif text.startswith("rate"):
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
_handle_rate(respond, schedule, text, is_admin)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
elif text.startswith("register"):
_handle_register(respond, schedule, user_id, text)
elif text.startswith("pick"):
_handle_pick(
respond, schedule, user_id, text, post_channel, client, schedule_channel
)
elif text.startswith("drop"):
_handle_drop(
respond, schedule, user_id, text, post_channel, client, schedule_channel
)
elif text.startswith("swap"):
_handle_swap(
respond, schedule, user_id, text, post_channel, client, schedule_channel
)
elif text.startswith("admin"):
_handle_admin(
respond, schedule, user_id, text, is_admin, client, schedule_channel
)
else:
respond(text="Unknown command. Try `/oncall help`")
def handle_pickup(body, respond, client, schedule, schedule_channel):
"""Handle the interactive "pick up open shift" button.
Routes holiday day-shift buttons to an atomic slot claim and gates pickups
of an already-started shift behind admin approval (same flow as ``pick``).
"""
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
action_id = body["actions"][0]["action_id"]
remainder = action_id[len("pickup_") :]
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if remainder.endswith("_day"):
date_str = remainder[:-4]
shift_type = "day"
else:
date_str = remainder
shift_type = "night"
user_id = body["user"]["id"]
channel_id = body["channel"]["id"]
today_str = datetime.now(EASTERN).strftime("%Y-%m-%d")
if date_str < today_str:
client.chat_postEphemeral(
channel=channel_id,
user=user_id,
text="That shift has already passed and can't be picked up.",
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
employee = schedule.get_employee_by_slack_id(user_id)
if not employee:
client.chat_postEphemeral(
channel=channel_id,
user=user_id,
text="You're not registered. Use `/oncall register <extension>` first.",
)
return
day_name = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A")
ctx = schedule.get_shift_context(date_str, day_name, shift_type)
is_holiday = ctx["kind"] == "holiday"
def _ephemeral(text):
client.chat_postEphemeral(channel=channel_id, user=user_id, text=text)
# A pickup after the shift has ended is rejected outright; after it has
# started (but before it ends) it needs admin approval.
if _shift_ended(date_str, shift_type):
_ephemeral("That shift has already ended and can't be picked up.")
return
if _shift_started(date_str, shift_type):
_request_late_pickup(
_ephemeral_respond(_ephemeral),
schedule,
employee,
date_str,
shift_type,
is_holiday=is_holiday,
client=client,
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
)
return
if is_holiday:
claimed = schedule.claim_holiday_slot(
date_str, employee["extension"], employee["name"]
)
else:
claimed = schedule.claim_open_shift(
date_str, employee["extension"], employee["name"], shift_type
)
if not claimed:
_ephemeral(f"That shift on *{date_str}* was already picked up by someone else.")
return
if not is_holiday and is_today(date_str) and _is_active_shift_type(shift_type):
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
_update_3cx_routing(employee["extension"])
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
blocks = build_shift_change_message(
user_id,
date_str,
"picked_up",
employee["extension"],
employee["name"],
shift_type=shift_type,
)
respond(
response_type="in_channel",
replace_original=False,
blocks=blocks,
text=f"Shift picked up for {date_str}",
)
_refresh_schedule_post(schedule, schedule_channel, client)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
def _ephemeral_respond(post_ephemeral):
"""Adapt an ephemeral-poster into a ``respond(text=...)`` callable.
``_request_late_pickup`` reports its outcome via ``respond(text=...)``; the
button path has no ``respond`` that targets the clicker, so this wraps the
ephemeral poster so the same helper serves both the slash command and the
button.
"""
def _respond(text="", **_kwargs):
if text:
post_ephemeral(text)
return _respond
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
# ── Subcommand handlers ─────────────────────────────────────────────────
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _show_schedule(respond, schedule):
blocks = build_week_schedule(schedule)
respond(blocks=blocks)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _show_next_week(respond, schedule):
now = datetime.now(EASTERN)
# Jump 2 weeks ahead from this week's Monday
this_monday = now - timedelta(days=now.weekday())
next_start = this_monday + timedelta(days=14)
blocks = build_week_schedule(schedule, start_date=next_start)
respond(blocks=blocks)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _show_pay(respond, schedule):
now = datetime.now(EASTERN)
# Show last completed week's pay (previous Monday–Sunday)
this_monday = now - timedelta(days=now.weekday())
prev_monday = this_monday - timedelta(days=7)
week_key = prev_monday.strftime("%Y-%m-%d")
pay_record = schedule.get_pay_record(week_key)
if pay_record and pay_record.get("breakdown"):
prev_sunday = prev_monday + timedelta(days=6)
week_label = (
f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}"
)
blocks = build_pay_summary_blocks(
week_label, pay_record["breakdown"], pay_record["totals"]
)
respond(blocks=blocks)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
else:
respond(
text=f"No pay record found for the week of {prev_monday.strftime('%b %-d')}."
)
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
def _handle_rate(respond, schedule, text, is_admin):
# Setting pay rates is an admin-only operation; reading them is gated too
# since rates are sensitive payroll data.
if not is_admin:
respond(text="Rate commands are restricted. Contact an administrator.")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
parts = text.split()
# /oncall rate — show current rates
if len(parts) == 1:
default_rate = schedule.get_shift_rate()
roster = schedule.get_roster()
lines = [f"*Default rate:* ${default_rate:.2f}/shift\n"]
custom = [
(e["SK"], e.get("name", "Unknown"), float(e["shift_rate"]))
for e in roster
if e.get("shift_rate")
]
if custom:
lines.append("*Per-person rates:*")
for ext, name, rate in sorted(custom, key=lambda x: x[0]):
lines.append(f"• {name} (Ext {ext}) — ${rate:.2f}/shift")
else:
lines.append("_No per-person rates set — everyone uses the default._")
respond(text="\n".join(lines))
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
# /oncall rate default <amount>
if parts[1] == "default":
if len(parts) < 3:
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text="Usage: `/oncall rate default <amount>` (e.g. `/oncall rate default 50`)"
)
return
try:
amount = float(parts[2].replace("$", ""))
except ValueError:
respond(text=f"Invalid amount: `{parts[2]}`")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
schedule.set_default_shift_rate(amount)
respond(text=f"Default shift rate set to *${amount:.2f}*.")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
# /oncall rate <extension> <amount>
if len(parts) < 3:
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text="Usage: `/oncall rate <extension> <amount>` (e.g. `/oncall rate 114 75`)"
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
ext = parts[1]
employee = schedule.get_employee_by_extension(ext)
if not employee:
respond(text=f"Extension `{ext}` not found in the roster.")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
try:
amount = float(parts[2].replace("$", ""))
except ValueError:
respond(text=f"Invalid amount: `{parts[2]}`")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
schedule.set_employee_shift_rate(ext, amount)
respond(
text=f"Shift rate for *{employee['name']}* (Ext {ext}) set to *${amount:.2f}*."
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _handle_register(respond, schedule, user_id, text):
parts = text.split()
if len(parts) < 2:
respond(
text="Usage: `/oncall register <extension>` (e.g. `/oncall register 114`)"
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
ext = parts[1].strip()
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
try:
employee = schedule.register_user(user_id, ext)
except ExtensionAlreadyRegistered:
respond(
text=(
f"Extension {ext} is already registered to another person. "
"If this is your extension, ask an admin to clear it."
)
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if not employee:
respond(
text=f"Extension {ext} not found in the roster. Check `/oncall roster`."
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
respond(text=f"Linked your account to *{employee['name']}* (Ext {ext}).")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _handle_pick(
respond, schedule, user_id, text, channel_id, client, schedule_channel
):
parts = text.split()
if len(parts) < 2:
respond(
text="Usage: `/oncall pick <date> [day|night]` (e.g. `/oncall pick friday`)"
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
employee = schedule.get_employee_by_slack_id(user_id)
if not employee:
respond(text="You're not registered. Use `/oncall register <extension>` first.")
return
explicit_shift = (
parts[2] if len(parts) > 2 and parts[2] in ("day", "night") else None
)
date_text = parts[1]
date = parse_date(date_text)
if not date:
respond(
text=f"Couldn't parse date: `{date_text}`. Try: today, tomorrow, friday, 4/5, 2026-04-05"
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
date_str = date.strftime("%Y-%m-%d")
if date_str < datetime.now(EASTERN).strftime("%Y-%m-%d"):
respond(text="You can't pick up a shift in the past.")
return
day_name = date.strftime("%A")
shift_type = _resolve_pick_shift_type(schedule, date_str, day_name, explicit_shift)
ctx = schedule.get_shift_context(date_str, day_name, shift_type)
if ctx["kind"] == "holiday":
_pick_holiday(
respond,
schedule,
employee,
date,
date_str,
ctx,
channel_id,
client,
schedule_channel,
)
return
_pick_regular(
respond,
schedule,
employee,
date,
date_str,
day_name,
shift_type,
ctx,
channel_id,
client,
schedule_channel,
)
def _resolve_pick_shift_type(schedule, date_str, day_name, explicit_shift) -> str:
"""Pick the shift type a bare ``/oncall pick <date>`` should target.
Honours an explicit ``day``/``night``. Otherwise a holiday (day-only) wins,
then any open day/night slot on a weekend; falls back to ``night``.
"""
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if explicit_shift:
return explicit_shift
if schedule.get_shift_context(date_str, day_name, "day")["kind"] == "holiday":
return "day"
if day_name in WEEKEND_DAYS:
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
for st in ("day", "night"):
_ext, _name, source = schedule.resolve_shift(date_str, day_name, st)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if source == "available":
return st
return "night"
def _pick_regular(
respond,
schedule,
employee,
date,
date_str,
day_name,
shift_type,
ctx,
channel_id,
client,
schedule_channel,
):
"""Pick up a regular (non-holiday) shift, gating late pickups on approval."""
assignees = ctx["assignees"]
# Already assigned to someone else (not open).
if assignees and assignees[0]["extension"] != employee["extension"]:
name = assignees[0]["name"]
ext = assignees[0]["extension"]
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
respond(
text=f"That shift is already covered by {name} (Ext {ext}). They'd need to drop it first."
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
date_label = date.strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
# A pickup after the shift has started (but before it ends) needs an admin to
# approve it. After the shift ends it's too late to pick up at all.
if _shift_ended(date_str, shift_type):
respond(text=f"The *{date_label}*{shift_label} shift has already ended.")
return
if _shift_started(date_str, shift_type):
_request_late_pickup(
respond,
schedule,
employee,
date_str,
shift_type,
is_holiday=False,
client=client,
)
return
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Already this employee's own shift — nothing to claim, just confirm.
already_mine = (
bool(assignees) and assignees[0]["extension"] == employee["extension"]
)
if not already_mine:
# Atomic conditional claim: only one of two concurrent pickers wins, so
# the loser is told it's taken instead of silently overwriting (TOCTOU).
claimed = schedule.claim_open_shift(
date_str, employee["extension"], employee["name"], shift_type
)
if not claimed:
respond(
text=(
f"The *{date_label}*{shift_label} shift was just picked up by "
"someone else."
)
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(employee["extension"])
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
respond(text=f"You picked up the shift for *{date_label}*{shift_label}.")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
blocks = build_shift_change_message(
user_id=employee.get("slack_user_id", ""),
date_str=date_str,
action="picked_up",
ext=employee["extension"],
name=employee["name"],
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
shift_type=shift_type,
)
try:
client.chat_postMessage(
channel=channel_id,
blocks=blocks,
text=f"Shift picked up for {date_str}",
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
except Exception:
logger.exception("Failed to post pickup notification to channel")
_refresh_schedule_post(schedule, schedule_channel, client)
def _pick_holiday(
respond,
schedule,
employee,
date,
date_str,
ctx,
channel_id,
client,
schedule_channel,
):
"""Pick up a holiday day-shift slot, gating late pickups on approval.
Holidays support multiple concurrent assignees; the claim is atomic via
``claim_holiday_slot`` so concurrent pickers can't oversubscribe the slots.
"""
ext = employee["extension"]
date_label = date.strftime("%A, %b %-d")
label = ctx.get("label") or "Holiday"
if any(a["extension"] == ext for a in ctx["assignees"]):
respond(text=f"You're already on the *{date_label}* ({label}) holiday shift.")
return
# Late pickup (window open) or too late (window closed).
if _shift_ended(date_str, "day"):
respond(text=f"The *{date_label}* ({label}) holiday shift has already ended.")
return
if _shift_started(date_str, "day"):
_request_late_pickup(
respond,
schedule,
employee,
date_str,
"day",
is_holiday=True,
client=client,
)
return
claimed = schedule.claim_holiday_slot(date_str, ext, employee["name"])
if not claimed:
respond(
text=(
f"Couldn't claim a slot on the *{date_label}* ({label}) holiday — "
"it's full or you're already on it."
)
)
return
respond(text=f"You picked up a slot on the *{date_label}* ({label}) holiday shift.")
blocks = build_shift_change_message(
user_id=employee.get("slack_user_id", ""),
date_str=date_str,
action="picked_up",
ext=ext,
name=employee["name"],
shift_type="day",
)
try:
client.chat_postMessage(
channel=channel_id,
blocks=blocks,
text=f"Holiday shift picked up for {date_str}",
)
except Exception:
logger.exception("Failed to post holiday pickup notification to channel")
_refresh_schedule_post(schedule, schedule_channel, client)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _handle_drop(
respond, schedule, user_id, text, channel_id, client, schedule_channel
):
parts = text.split(maxsplit=1)
if len(parts) < 2:
respond(text="Usage: `/oncall drop <date>` (e.g. `/oncall drop friday`)")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
employee = schedule.get_employee_by_slack_id(user_id)
if not employee:
respond(text="You're not registered. Use `/oncall register <extension>` first.")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
date = parse_date(parts[1])
if not date:
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text=f"Couldn't parse date: `{parts[1]}`. Try: today, tomorrow, friday, 4/5, 2026-04-05"
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
date_str = date.strftime("%Y-%m-%d")
if date_str < datetime.now(EASTERN).strftime("%Y-%m-%d"):
respond(text="You can't drop a shift in the past.")
return
day_name = date.strftime("%A")
# A holiday slot the employee holds is dropped (released) ahead of regular
# day/night shifts — holidays take priority on their date.
holiday_ctx = schedule.get_shift_context(date_str, day_name, "day")
if holiday_ctx["kind"] == "holiday" and any(
a["extension"] == employee["extension"] for a in holiday_ctx["assignees"]
):
_drop_holiday(
respond,
schedule,
employee,
date,
date_str,
channel_id,
client,
schedule_channel,
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
found = _find_employee_shift(schedule, date_str, day_name, employee["extension"])
if not found:
ext, name, _source = schedule.resolve_shift(date_str, day_name)
respond(text=f"That's not your shift — it belongs to {name} (Ext {ext}).")
return
ext, name, source, shift_type = found
if _within_drop_lock(date_str, shift_type):
respond(
text=(
"This shift starts in under 24 hours — you can't drop it now. "
"Hand it off with `/oncall swap <date> @person` (they'll need to accept), "
"or ask an admin to open it."
)
)
return
# No 3CX repoint here: a same-day shift is always inside the 24h lock above,
# so a drop that reaches this point is never today's active shift.
schedule.mark_open(date_str, shift_type)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
date_label = date.strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
respond(
text=(
f"You dropped the shift for *{date_label}*{shift_label}. "
"It's now open for pickup."
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
blocks = build_shift_change_message(
user_id, date_str, "dropped", ext, name, shift_type=shift_type
)
try:
client.chat_postMessage(
channel=channel_id, blocks=blocks, text=f"Shift dropped for {date_str}"
)
except Exception:
logger.exception("Failed to post drop notification to channel")
_refresh_schedule_post(schedule, schedule_channel, client)
def _drop_holiday(
respond, schedule, employee, date, date_str, channel_id, client, schedule_channel
):
"""Release the employee's holiday slot (subject to the 24h drop lock).
The slot becomes open for someone else to pick up. The released slot does
not repoint 3CX here: a same-day drop is always inside the 24h lock (the
holiday starts 08:00 ET), so a drop that reaches the release is never the
live window.
"""
if _within_drop_lock(date_str, "day"):
respond(
text=(
"This holiday shift starts in under 24 hours — you can't drop it now. "
"Hand it off with `/oncall swap <date> @person` (they'll need to accept), "
"or ask an admin to open it."
)
)
return
released = schedule.release_holiday_slot(date_str, employee["extension"])
if not released:
respond(text="You're not on that holiday shift.")
return
date_label = date.strftime("%A, %b %-d")
respond(
text=(
f"You dropped your slot on the *{date_label}* holiday shift. "
"It's now open for pickup."
)
)
blocks = build_shift_change_message(
user_id=employee.get("slack_user_id", ""),
date_str=date_str,
action="dropped",
ext=employee["extension"],
name=employee["name"],
shift_type="day",
)
try:
client.chat_postMessage(
channel=channel_id,
blocks=blocks,
text=f"Holiday shift dropped for {date_str}",
)
except Exception:
logger.exception("Failed to post holiday drop notification to channel")
_refresh_schedule_post(schedule, schedule_channel, client)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _handle_swap(
respond, schedule, user_id, text, channel_id, client, schedule_channel
):
# Expected format: swap <date> @user OR swap <date> <extension>
parts = text.split(maxsplit=2)
if len(parts) < 3:
respond(
text="Usage: `/oncall swap <date> @person` (e.g. `/oncall swap friday @sarah`)"
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
employee = schedule.get_employee_by_slack_id(user_id)
if not employee:
respond(text="You're not registered. Use `/oncall register <extension>` first.")
return
date = parse_date(parts[1])
if not date:
respond(text=f"Couldn't parse date: `{parts[1]}`.")
return
date_str = date.strftime("%Y-%m-%d")
if date_str < datetime.now(EASTERN).strftime("%Y-%m-%d"):
respond(text="You can't swap a shift in the past.")
return
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
day_name = date.strftime("%A")
# A holiday slot the employee holds is swappable as a day shift; holidays
# take priority on their date.
holiday_ctx = schedule.get_shift_context(date_str, day_name, "day")
holiday_swap = holiday_ctx["kind"] == "holiday" and any(
a["extension"] == employee["extension"] for a in holiday_ctx["assignees"]
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if holiday_swap:
shift_type = "day"
else:
found = _find_employee_shift(
schedule, date_str, day_name, employee["extension"]
)
if not found:
ext, name, _source = schedule.resolve_shift(date_str, day_name)
respond(
text=f"That's not your shift — it belongs to {name} (Ext {ext}). You can only swap your own shifts."
)
return
_ext, _name, _source, shift_type = found
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
# Resolve target user — could be <@U12345> or an extension number
target_text = parts[2].strip()
slack_id_match = re.match(r"<@(\w+)(?:\|[^>]*)?>", target_text)
if slack_id_match:
target_slack_id = slack_id_match.group(1)
target = schedule.get_employee_by_slack_id(target_slack_id)
if not target:
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text=f"<@{target_slack_id}> isn't registered. They need to run `/oncall register <extension>`."
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
else:
target = schedule.get_employee_by_extension(target_text)
if not target:
respond(text=f"Extension `{target_text}` not found in the roster.")
return
if target["extension"] == employee["extension"]:
respond(text="That shift is already yours — nothing to swap.")
return
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
# The target must be linked to Slack so we can DM them the request.
if not target.get("slack_user_id"):
respond(
text=f"*{target['name']}* (Ext {target['extension']}) isn't linked to Slack yet — "
"they need to run `/oncall register <extension>` before they can be swapped a shift."
)
return
# Create a pending swap and DM the target Accept/Decline. The shift does NOT
# move until they accept — the original owner stays responsible until then.
expires_at = int(_shift_start(date_str, shift_type).timestamp())
schedule.create_pending_swap(date_str, shift_type, employee, target, expires_at)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
date_label = date.strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
try:
client.chat_postMessage(
channel=target["slack_user_id"],
blocks=build_swap_request_blocks(user_id, date_str, shift_type),
text=f"{employee['name']} wants to swap you the {date_str} shift",
)
except Exception:
logger.exception("Failed to DM swap request to target")
respond(
text=f"Couldn't reach *{target['name']}* on Slack to send the request. Try again later."
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
respond(
text=(
f"Swap request sent to <@{target['slack_user_id']}> for "
f"*{date_label}*{shift_label}. The shift moves to them once they accept."
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
)
)
def _parse_swap_action(action_id: str, prefix: str) -> tuple[str, str]:
"""Split a swap action_id into (date_str, shift_type)."""
remainder = action_id[len(prefix) :]
if remainder.endswith("_day"):
return remainder[:-4], "day"
return remainder, "night"
def handle_swap_accept(body, respond, client, schedule, schedule_channel):
"""Target accepted a swap — apply the override and mark it verified."""
date_str, shift_type = _parse_swap_action(
body["actions"][0]["action_id"], "swap_accept_"
)
user_id = body["user"]["id"]
swap = schedule.get_swap(date_str, shift_type)
if (
not swap
or swap.get("status") != "pending"
or swap.get("target_slack") != user_id
):
respond(
replace_original=True,
blocks=build_swap_resolved_blocks("This swap request is no longer valid."),
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
)
return
if _shift_started(date_str, shift_type):
schedule.clear_swap(date_str, shift_type)
respond(
replace_original=True,
blocks=build_swap_resolved_blocks(
"This swap request has expired — the shift has already started."
),
)
return
# A day-shift swap on a holiday date hands off a holiday slot rather than a
# plain override: atomically move the slot from requester to target.
is_holiday = shift_type == "day" and schedule.get_holiday(date_str) is not None
if is_holiday:
moved = schedule.swap_holiday_assignee(
date_str,
swap["requester_ext"],
swap["target_ext"],
swap["target_name"],
)
if not moved:
schedule.clear_swap(date_str, shift_type)
respond(
replace_original=True,
blocks=build_swap_resolved_blocks(
"This swap request is no longer valid."
),
)
return
if _holiday_window_active(date_str):
_set_holiday_queue_agents(schedule, date_str)
else:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Re-resolve the current holder at accept time. reassign_if_held_by
# below trusts "no override row" as "still the requester's", but a
# weekly-held shift also has no override row — so an admin clear or a
# weekly-schedule edit between swap-init and accept could move the shift
# to a third party without ever creating an override, and the bare
# conditional write would not catch it. Confirm the requester is still
# the resolved holder before reassigning.
accept_day_name = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A")
current_ext, _, _ = schedule.resolve_shift(
date_str, accept_day_name, shift_type
)
if current_ext != swap["requester_ext"]:
schedule.clear_swap(date_str, shift_type)
respond(
replace_original=True,
blocks=build_swap_resolved_blocks(
"This shift is no longer assigned to the person who "
"requested the swap, so it couldn't be applied."
),
)
return
# Only move the shift if it's still the requester's (or still on the
# weekly fallback). If it was independently claimed after the swap was
# initiated, abort instead of silently overwriting the new holder.
moved = schedule.reassign_if_held_by(
date_str,
swap["requester_ext"],
swap["target_ext"],
swap["target_name"],
shift_type,
)
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
if not moved:
schedule.clear_swap(date_str, shift_type)
respond(
replace_original=True,
blocks=build_swap_resolved_blocks(
"This shift was already picked up by someone else, so the "
"swap couldn't be applied."
),
)
return
if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(swap["target_ext"])
schedule.mark_swap_verified(date_str, shift_type)
day_name = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A")
date_label = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
respond(
replace_original=True,
blocks=build_swap_resolved_blocks(
f"You're now covering the *{date_label}*{shift_label} shift. Thanks!"
),
)
if swap.get("requester_slack"):
try:
client.chat_postMessage(
channel=swap["requester_slack"],
text=f"<@{user_id}> accepted your swap — they're now on the *{date_label}*{shift_label} shift.",
)
except Exception:
logger.exception("Failed to DM swap requester on accept")
if schedule_channel:
blocks = build_shift_change_message(
user_id,
date_str,
"swapped",
swap["target_ext"],
swap["target_name"],
shift_type=shift_type,
)
try:
client.chat_postMessage(
channel=schedule_channel,
blocks=blocks,
text=f"Shift swapped for {date_str}",
)
except Exception:
logger.exception("Failed to post swap notification to channel")
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
_refresh_schedule_post(schedule, schedule_channel, client)
def handle_swap_decline(body, respond, client, schedule, schedule_channel):
"""Target declined a swap — clear it and notify the requester."""
date_str, shift_type = _parse_swap_action(
body["actions"][0]["action_id"], "swap_decline_"
)
user_id = body["user"]["id"]
swap = schedule.get_swap(date_str, shift_type)
if (
not swap
or swap.get("status") != "pending"
or swap.get("target_slack") != user_id
):
respond(
replace_original=True,
blocks=build_swap_resolved_blocks("This swap request is no longer valid."),
)
return
schedule.clear_swap(date_str, shift_type)
day_name = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A")
date_label = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
respond(
replace_original=True,
blocks=build_swap_resolved_blocks(
f"You declined the *{date_label}*{shift_label} swap. No change."
),
)
if swap.get("requester_slack"):
try:
client.chat_postMessage(
channel=swap["requester_slack"],
text=f"<@{user_id}> declined your swap for *{date_label}*{shift_label} — it's still your shift.",
)
except Exception:
logger.exception("Failed to DM swap requester on decline")
# ── Late-pickup approval ─────────────────────────────────────────────────
def _request_late_pickup(
respond, schedule, employee, date_str, shift_type, is_holiday, client
):
"""Create a PICKUP_REQUEST and DM every admin an Approve/Deny prompt.
Used when someone tries to pick up a shift that has already *started* (but
not ended). The shift is NOT claimed yet — the first admin to approve wins
(a conditional claim). Mirrors the verified-swap DM pattern, but the
approvers are the admins rather than the swap target.
"""
schedule.create_pickup_request(
date_str,
shift_type,
employee,
expires_at=int(_shift_end(date_str, shift_type).timestamp()),
is_holiday=is_holiday,
)
blocks = build_pickup_request_blocks(
requester_slack=employee.get("slack_user_id", ""),
requester_name=employee["name"],
date_str=date_str,
shift_type=shift_type,
requester_ext=employee["extension"],
is_holiday=is_holiday,
)
text = f"{employee['name']} wants to pick up the already-started {date_str} shift"
admins = schedule.get_admin_users()
delivered = 0
for admin_id in admins:
try:
client.chat_postMessage(channel=admin_id, blocks=blocks, text=text)
delivered += 1
except Exception:
logger.exception("Failed to DM late-pickup request to admin %s", admin_id)
if delivered == 0:
# Nobody to approve it — roll the request back so it doesn't dangle.
schedule.clear_pickup_request(date_str, shift_type, employee["extension"])
respond(
text=(
"That shift has already started and needs an admin to approve a "
"pickup, but I couldn't reach any admin. Please contact one directly."
)
)
return
respond(
text=(
"That shift has already started, so a pickup needs admin approval. "
"I've sent your request to the admins — you'll be notified once it's decided."
)
)
def _parse_pickup_action(action_id: str, prefix: str) -> tuple[str, str, str]:
"""Split a pickup-approval action_id into (date_str, shift_type, ext).
The tail is ``<date>[_day]_<ext>`` (see ``build_pickup_request_blocks``).
"""
remainder = action_id[len(prefix) :]
ext_start = remainder.rfind("_")
ext = remainder[ext_start + 1 :]
rest = remainder[:ext_start]
if rest.endswith("_day"):
return rest[:-4], "day", ext
return rest, "night", ext
def handle_pickup_approve(body, respond, client, schedule, schedule_channel):
"""An admin approved a late pickup — claim the shift for the requester.
First-approve-wins: the claim is conditional (``set_override`` after a guard,
or atomic ``claim_holiday_slot``), so a second admin approving a
just-resolved request gets a "no longer pending" response. On success the
requester is DM'd and 3CX is repointed if the shift window is currently live.
"""
date_str, shift_type, ext = _parse_pickup_action(
body["actions"][0]["action_id"], "pickup_approve_"
)
admin_id = body["user"]["id"]
# The Approve button is DM'd only to admins, but action handlers receive
# whoever clicks — re-verify the actor is an admin (mirrors swap handlers).
if admin_id not in schedule.get_admin_users():
respond(
replace_original=False,
text="Only an admin can approve a pickup request.",
)
return
req = schedule.get_pickup_request(date_str, shift_type, ext)
if not req or req.get("status") != "pending":
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
"This pickup request is no longer pending."
),
)
return
if _shift_ended(date_str, shift_type):
schedule.clear_pickup_request(date_str, shift_type, ext)
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
"This pickup request has expired — the shift has already ended."
),
)
return
# Atomic first-approve-wins: only the admin who flips the request
# pending->approved proceeds; a second concurrent approver is turned away.
if not schedule.approve_pickup_request(date_str, shift_type, ext):
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
"This pickup request is no longer pending."
),
)
return
requester_name = req.get("requester_name", ext)
requester_slack = req.get("requester_slack", "")
is_holiday = bool(req.get("is_holiday"))
day_name = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A")
date_label = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
if is_holiday:
claimed = schedule.claim_holiday_slot(date_str, ext, requester_name)
if not claimed:
schedule.clear_pickup_request(date_str, shift_type, ext)
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
"Couldn't assign the holiday slot — it's full or already taken."
),
)
return
if _holiday_window_active(date_str):
_set_holiday_queue_agents(schedule, date_str)
else:
# Conditional claim so two different requesters' approvals for the SAME
# shift can't silently clobber each other — the per-request approve gate
# only serializes a single request's SK, not the OVERRIDE row.
claimed = schedule.claim_open_shift(date_str, ext, requester_name, shift_type)
if not claimed:
schedule.clear_pickup_request(date_str, shift_type, ext)
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
"Couldn't assign the shift — it's already covered."
),
)
return
if is_today(date_str) and _is_active_shift_type(shift_type):
# Best-effort: a 3CX failure must not strand the request as approved.
try:
_update_3cx_routing(ext)
except Exception:
logger.exception("3CX repoint failed after approving late pickup")
schedule.clear_pickup_request(date_str, shift_type, ext)
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
f"Approved — {requester_name} (Ext {ext}) is now on the "
f"*{date_label}*{shift_label} shift."
),
)
if requester_slack:
try:
client.chat_postMessage(
channel=requester_slack,
text=(
f"<@{admin_id}> approved your pickup — you're now on the "
f"*{date_label}*{shift_label} shift."
),
)
except Exception:
logger.exception("Failed to DM late-pickup requester on approve")
if schedule_channel:
try:
client.chat_postMessage(
channel=schedule_channel,
blocks=build_shift_change_message(
user_id=requester_slack,
date_str=date_str,
action="picked_up",
ext=ext,
name=requester_name,
shift_type=shift_type,
),
text=f"Shift picked up for {date_str}",
)
except Exception:
logger.exception("Failed to post late-pickup notification to channel")
_refresh_schedule_post(schedule, schedule_channel, client)
def handle_pickup_deny(body, respond, client, schedule, schedule_channel):
"""An admin denied a late pickup — clear the request and notify the requester."""
date_str, shift_type, ext = _parse_pickup_action(
body["actions"][0]["action_id"], "pickup_deny_"
)
admin_id = body["user"]["id"]
if admin_id not in schedule.get_admin_users():
respond(
replace_original=False,
text="Only an admin can deny a pickup request.",
)
return
req = schedule.get_pickup_request(date_str, shift_type, ext)
if not req or req.get("status") != "pending":
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
"This pickup request is no longer pending."
),
)
return
schedule.clear_pickup_request(date_str, shift_type, ext)
day_name = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A")
date_label = datetime.strptime(date_str, "%Y-%m-%d").strftime("%A, %b %-d")
shift_label = _shift_type_label(day_name, shift_type)
respond(
replace_original=True,
blocks=build_pickup_resolved_blocks(
f"Denied — the *{date_label}*{shift_label} pickup was not approved."
),
)
requester_slack = req.get("requester_slack", "")
if requester_slack:
try:
client.chat_postMessage(
channel=requester_slack,
text=(
f"<@{admin_id}> denied your pickup for the "
f"*{date_label}*{shift_label} shift."
),
)
except Exception:
logger.exception("Failed to DM late-pickup requester on deny")
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def _handle_admin(respond, schedule, user_id, text, is_admin, client, schedule_channel):
if not is_admin:
respond(text="Admin commands are restricted. Contact an administrator.")
return
parts = text.split()
if len(parts) < 2:
respond(
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
text=(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
"*Admin Commands:*\n"
"`admin override <date> <ext> [day|night]` — Assign shift\n"
"`admin open <date> [day|night]` — Mark open\n"
"`admin clear <date> [day|night]` — Remove override\n"
"`admin roster add <ext> <name>` — Add employee\n"
"`admin roster remove <ext>` — Remove employee\n"
"`admin roster rename <ext> <name>` — Rename\n"
"`admin holiday add <date> <slots> [x<mult>] <label>` — Schedule holiday\n"
"`admin holiday remove <date>` — Remove holiday\n"
"`admin holiday list` — List upcoming holidays"
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
subcmd = parts[1]
if subcmd == "override":
if len(parts) < 4:
respond(
text="Usage: `/oncall admin override <date> <extension> [day|night]`"
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
return
date = parse_date(parts[2])
if not date:
respond(text=f"Couldn't parse date: `{parts[2]}`")
return
ext = parts[3]
shift_type = (
parts[4] if len(parts) > 4 and parts[4] in ("day", "night") else "night"
)
employee = schedule.get_employee_by_extension(ext)
if not employee:
respond(text=f"Extension `{ext}` not found in the roster.")
return
date_str = date.strftime("%Y-%m-%d")
schedule.set_override(
date_str, employee["extension"], employee["name"], shift_type
)
if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(employee["extension"])
label = "Day" if shift_type == "day" else "Night"
respond(
text=f"Override set: *{date.strftime('%A, %b %-d')}* ({label}) → {employee['name']} (Ext {ext})"
)
_refresh_schedule_post(schedule, schedule_channel, client)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
elif subcmd == "open":
if len(parts) < 3:
respond(text="Usage: `/oncall admin open <date> [day|night]`")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
date = parse_date(parts[2])
if not date:
respond(text=f"Couldn't parse date: `{parts[2]}`")
return
shift_type = (
parts[3] if len(parts) > 3 and parts[3] in ("day", "night") else "night"
)
date_str = date.strftime("%Y-%m-%d")
schedule.mark_open(date_str, shift_type)
if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(FALLBACK_EXTENSION)
label = "Day" if shift_type == "day" else "Night"
respond(text=f"*{date.strftime('%A, %b %-d')}* ({label}) marked as open.")
_refresh_schedule_post(schedule, schedule_channel, client)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
elif subcmd == "clear":
if len(parts) < 3:
respond(text="Usage: `/oncall admin clear <date> [day|night]`")
return
date = parse_date(parts[2])
if not date:
respond(text=f"Couldn't parse date: `{parts[2]}`")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
shift_type = (
parts[3] if len(parts) > 3 and parts[3] in ("day", "night") else "night"
)
date_str = date.strftime("%Y-%m-%d")
schedule.remove_override(date_str, shift_type)
if is_today(date_str) and _is_active_shift_type(shift_type):
day_name = date.strftime("%A")
ext, _name, _source = schedule.resolve_shift(date_str, day_name, shift_type)
_update_3cx_routing(ext)
label = "Day" if shift_type == "day" else "Night"
respond(
text=f"Override cleared for *{date.strftime('%A, %b %-d')}* ({label}) — reverted to weekly schedule."
)
_refresh_schedule_post(schedule, schedule_channel, client)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
elif subcmd == "roster":
if len(parts) < 3:
respond(text="Usage: `admin roster add|remove|rename <ext> [name]`")
return
roster_cmd = parts[2]
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
if roster_cmd == "add":
if len(parts) < 5:
respond(text="Usage: `/oncall admin roster add <ext> <name>`")
return
ext = parts[3]
name = " ".join(parts[4:])
added = schedule.add_roster_entry(ext, name)
if not added:
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text=f"Extension `{ext}` already exists. Use `roster rename` to change the name."
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
respond(text=f"Added *{name}* (Ext {ext}) to the roster.")
elif roster_cmd == "remove":
if len(parts) < 4:
respond(text="Usage: `/oncall admin roster remove <ext>`")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
return
ext = parts[3]
employee = schedule.get_employee_by_extension(ext)
if not employee:
respond(text=f"Extension `{ext}` not found in the roster.")
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
schedule.remove_roster_entry(ext)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text=f"Removed *{employee.get('name', ext)}* (Ext {ext}) from the roster."
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
elif roster_cmd == "rename":
if len(parts) < 5:
respond(text="Usage: `/oncall admin roster rename <ext> <name>`")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
ext = parts[3]
employee = schedule.get_employee_by_extension(ext)
if not employee:
respond(text=f"Extension `{ext}` not found in the roster.")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
return
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
new_name = " ".join(parts[4:])
schedule.rename_roster_entry(ext, new_name)
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
respond(
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
text=f"Renamed Ext {ext}: {employee.get('name', '?')} → *{new_name}*"
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
else:
respond(text="Unknown roster command. Use `add`, `remove`, or `rename`.")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
elif subcmd == "holiday":
_handle_admin_holiday(respond, schedule, parts, client, schedule_channel)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
else:
respond(text=f"Unknown admin command: `{subcmd}`. Try `/oncall help`.")
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer 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`. * Migrate secrets from SSM Parameter Store to Secrets Manager - 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 * Merge ring-scheduler-3cx as 4th Lambda function - 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 * Add schedule post live-update and old post deletion (#40, #41) - 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 * Disallow past shifts and add day/night labels (#43, #42) - 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 * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 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. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - 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 * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - 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 * Fix dependabot directories and admin weekend shift handling 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. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications 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. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
# ── Admin: holidays ──────────────────────────────────────────────────────
def _handle_admin_holiday(respond, schedule, parts, client, schedule_channel):
"""Dispatch ``admin holiday add|remove|list``.
``parts`` is the whitespace-split command, where ``parts[1] == "holiday"``.
"""
if len(parts) < 3:
respond(text="Usage: `admin holiday add|remove|list ...`")
return
holiday_cmd = parts[2]
if holiday_cmd == "add":
_admin_holiday_add(respond, schedule, parts, client, schedule_channel)
elif holiday_cmd == "remove":
_admin_holiday_remove(respond, schedule, parts, client, schedule_channel)
elif holiday_cmd == "list":
_admin_holiday_list(respond, schedule)
else:
respond(text="Unknown holiday command. Use `add`, `remove`, or `list`.")
def _admin_holiday_add(respond, schedule, parts, client, schedule_channel):
"""`admin holiday add <date> <slots> [x<mult>] <label>`.
Creates the HOLIDAY record, provisions the 08:00 activate / 17:00 deactivate
one-off schedules (storing their names on the record), inline-activates when
the window is already open, and announces the holiday in the channel.
"""
if len(parts) < 6:
respond(
text=(
"Usage: `/oncall admin holiday add <date> <slots> [x<mult>] <label>`\n"
"e.g. `/oncall admin holiday add 2026-07-04 2 x2 Independence Day`"
)
)
return
date = parse_date(parts[3])
if not date:
respond(text=f"Couldn't parse date: `{parts[3]}`")
return
date_str = date.strftime("%Y-%m-%d")
if date_str < datetime.now(EASTERN).strftime("%Y-%m-%d"):
respond(text="You can't schedule a holiday in the past.")
return
try:
slots = int(parts[4])
except ValueError:
respond(text=f"Slots must be a whole number, got `{parts[4]}`.")
return
if slots < 1:
respond(text="Slots must be at least 1.")
return
# Optional ``x<mult>`` token before the label.
rest = parts[5:]
multiplier = None
if rest and re.fullmatch(r"x[0-9]+(\.[0-9]+)?", rest[0], re.IGNORECASE):
multiplier = rest[0][1:]
rest = rest[1:]
label = " ".join(rest).strip()
if not label:
respond(text="A holiday label is required.")
return
schedule_names = _create_holiday_schedules(date_str)
created = schedule.create_holiday(
date_str,
slots=slots,
label=label,
created_by=schedule_channel or "",
multiplier=multiplier,
schedule_names=schedule_names,
)
if not created:
# Roll back the schedules we just made for a date that already has one.
_delete_holiday_schedules(schedule_names)
respond(
text=f"A holiday already exists on *{date.strftime('%A, %b %-d')}*. "
"Remove it first to recreate."
)
return
# If the window is already open (admin added it mid-day), the 08:00 schedule
# has passed, so repoint the call flow now via the holiday router.
if _holiday_window_active(date_str):
_activate_holiday_inline(schedule, date_str)
holiday = schedule.get_holiday(date_str)
multiplier_value = holiday["multiplier"] if holiday else (multiplier or "1.5")
respond(
text=(
f"Scheduled *{label}* holiday on *{date.strftime('%A, %b %-d')}* — "
f"{slots} slot{'s' if slots != 1 else ''} at {float(multiplier_value):g}x pay."
)
)
if schedule_channel:
try:
client.chat_postMessage(
channel=schedule_channel,
blocks=build_holiday_added_blocks(
date_str, label, slots, multiplier_value
),
text=f"Holiday added: {label} on {date_str}",
)
except Exception:
logger.exception("Failed to post holiday-added notification to channel")
_refresh_schedule_post(schedule, schedule_channel, client)
def _admin_holiday_remove(respond, schedule, parts, client, schedule_channel):
"""`admin holiday remove <date>` — delete the record + outstanding schedules."""
if len(parts) < 4:
respond(text="Usage: `/oncall admin holiday remove <date>`")
return
date = parse_date(parts[3])
if not date:
respond(text=f"Couldn't parse date: `{parts[3]}`")
return
date_str = date.strftime("%Y-%m-%d")
holiday = schedule.get_holiday(date_str)
if not holiday:
respond(text=f"No holiday scheduled on *{date.strftime('%A, %b %-d')}*.")
return
_delete_holiday_schedules(holiday.get("schedule_names", []))
schedule.remove_holiday(date_str)
respond(
text=(
f"Removed the *{holiday.get('label', 'holiday')}* holiday on "
f"*{date.strftime('%A, %b %-d')}*."
)
)
_refresh_schedule_post(schedule, schedule_channel, client)
def _admin_holiday_list(respond, schedule):
"""`admin holiday list` — show today-and-future scheduled holidays."""
today_str = datetime.now(EASTERN).strftime("%Y-%m-%d")
resp = schedule.table.query(
KeyConditionExpression=Key("PK").eq("HOLIDAY") & Key("SK").gte(today_str)
)
items = resp.get("Items", [])
if not items:
respond(text="No upcoming holidays scheduled.")
return
lines = ["*Upcoming Holidays*\n"]
for item in sorted(items, key=lambda x: x["SK"]):
date_str = item["SK"]
dt = datetime.strptime(date_str, "%Y-%m-%d")
slots = int(item.get("slots", 0))
filled = len(item.get("assignees", {}) or {})
mult = f"{float(item.get('multiplier', 1.5)):g}x"
lines.append(
f"• {dt.strftime('%a %b %-d')} — _{item.get('label', 'Holiday')}_ "
f"({filled}/{slots} filled, {mult})"
)
respond(text="\n".join(lines))
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
# ── App Home ────────────────────────────────────────────────────────────
@functools.lru_cache(maxsize=1)
def _changelog_text() -> str:
"""Read the CHANGELOG shipped in this function's package.
Lazy (never at import) and tolerant of a missing file, so the App Home tab
degrades to "no What's New section" rather than erroring. The copy lives at
``$LAMBDA_TASK_ROOT/CHANGELOG.md`` (synced from the repo root).
"""
path = os.path.join(os.environ.get("LAMBDA_TASK_ROOT", "."), "CHANGELOG.md")
try:
with open(path, encoding="utf-8") as fh:
return fh.read()
except OSError:
logger.warning("CHANGELOG.md not found at %s — App Home omits What's New", path)
return ""
def publish_home(client, user_id: str, changelog_text: str) -> None:
"""Render and publish the App Home view for ``user_id``."""
entry = latest_entry(changelog_text)
view = build_home_view(
version=entry.version if entry else None,
notes=entry.body if entry else "",
date_label=entry.date_label if entry else "",
)
client.views_publish(user_id=user_id, view=view)
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
# ── App factory ─────────────────────────────────────────────────────────
def create_app(
bot_token: str, signing_secret: str, schedule_channel: str | None = None
) -> App:
app = App(
token=bot_token,
signing_secret=signing_secret,
process_before_response=True,
)
schedule = ShiftSchedule()
@app.command("/oncall")
def handle_oncall(ack, command, respond, client):
ack()
dispatch_oncall(command, respond, client, schedule, schedule_channel)
# Open-shift pickup buttons (``pickup_<date>[_day]``). The negative
# lookahead keeps this from also matching the late-pickup approval buttons
# (``pickup_approve_…`` / ``pickup_deny_…``), which have their own handlers.
@app.action(re.compile(r"^pickup_(?!approve_|deny_)"))
Add pytest suite and wire it into CI (#85) (#86) * Add pytest suite and wire it into CI Stands up the first automated tests for the repo (151 tests) and turns on the CI test step. - Lift slack-bot handlers out of create_app() closures to module level so they're unit-testable; create_app is now a thin Bolt-wiring layer. No behavior change (handler entrypoints and create_app signature unchanged). - tests/ mirrors src/: shared layer (schedule, blocks, 3CX client, ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/ admin/register/rate, pickup button, roster sync, queue scheduler). - All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via responses, Slack via fakes, time via freezegun. No real network/AWS. - pyproject.toml pytest config (pythonpath=src/shared, importlib mode); per-package conftest loads each app.py under a unique name to avoid the four-app.py collision. tests/requirements.txt for test-only deps. - ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint the tests dir too. - README Testing section. Closes #85 * Add least-privilege permissions block to CI workflow Resolves the CodeQL actions/missing-workflow-permissions alert: the CI workflow now restricts GITHUB_TOKEN to contents: read (it only checks out, lints, and runs tests). * Stop logging extension numbers in 3CX queue updates Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the queue/ring-group forwarding logs no longer include the routed extension values (closed/holiday/extension). Non-sensitive context (resource id, queue number) is retained.
2026-06-01 19:07:08 -04:00
def handle_pickup_button(ack, body, client, respond):
ack()
handle_pickup(body, respond, client, schedule, schedule_channel)
@app.action(re.compile(r"^pickup_approve_"))
def handle_pickup_approve_button(ack, body, client, respond):
ack()
handle_pickup_approve(body, respond, client, schedule, schedule_channel)
@app.action(re.compile(r"^pickup_deny_"))
def handle_pickup_deny_button(ack, body, client, respond):
ack()
handle_pickup_deny(body, respond, client, schedule, schedule_channel)
@app.action(re.compile(r"^swap_accept_"))
def handle_swap_accept_button(ack, body, client, respond):
ack()
handle_swap_accept(body, respond, client, schedule, schedule_channel)
@app.action(re.compile(r"^swap_decline_"))
def handle_swap_decline_button(ack, body, client, respond):
ack()
handle_swap_decline(body, respond, client, schedule, schedule_channel)
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
@app.event("app_home_opened")
def handle_app_home_opened(event, client):
# Fires for the Messages tab too; only (re)publish the Home tab.
if event.get("tab") != "home":
return
publish_home(client, event["user"], _changelog_text())
return app