afterhours-shift-manager/src/server/jobs.py
Adam Moussa 26adb8e6c0
feat(api): collapse Slack, portal, and jobs onto Fargate (PLAT-216) (#259)
* feat(api): collapse Slack, portal, and jobs onto Fargate (PLAT-216)

Move HTTP and scheduled work onto one always-on Flask task so after-hours
loses Lambda cold start without changing the Cognito or roster contracts.

* fix(portal-api): keep CORS headers on unexpected 500s

Portal SPA error handling needs Access-Control-Allow-Origin even when
DynamoDB or other internals fail, otherwise the browser hides the 500.

* fix(api): retarget holidays per account and ship App Home changelog (PLAT-216)

* fix(iam): list ECS tasks and fail closed on non-prod Paychex (PLAT-216)

* fix(portal-api): serve portal JSON with an explicit JSON content type
2026-09-21 19:13:30 +00:00

79 lines
2.3 KiB
Python

"""In-process job dispatch. SQS when JOBS_QUEUE_URL is set; otherwise run inline."""
from __future__ import annotations
import importlib.util
import json
import logging
import os
import sys
from pathlib import Path
import boto3
from shared.holiday_flow import activate, deactivate
from shared.schedule import ShiftSchedule
logger = logging.getLogger(__name__)
_SRC = Path(__file__).resolve().parents[1]
_sqs = None
_handlers: dict[str, object] = {}
def _client():
global _sqs
if _sqs is None:
_sqs = boto3.client("sqs")
return _sqs
def _load_lambda_app(dirname: str):
if dirname in _handlers:
return _handlers[dirname]
path = _SRC / dirname / "app.py"
name = f"afterhours_{dirname.replace('-', '_')}"
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise ImportError(f"cannot load {path}")
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
_handlers[dirname] = mod
return mod
def enqueue_job(payload: dict) -> None:
queue_url = os.environ.get("JOBS_QUEUE_URL", "").strip()
body = json.dumps(payload, default=str)
if not queue_url:
run_job(payload)
return
_client().send_message(QueueUrl=queue_url, MessageBody=body)
def _run_holiday(payload: dict) -> dict:
action = payload.get("action")
date = payload.get("date")
if not date:
return {"error": True, "reason": "missing_date"}
schedule = ShiftSchedule()
if action == "activate":
return activate(schedule, date)
if action == "deactivate":
return deactivate(schedule, date)
return {"error": True, "reason": "unknown_action", "action": action}
def run_job(payload: dict) -> dict:
event_type = payload.get("event", "")
if event_type == "holiday":
return _run_holiday(payload)
forced = {**payload, "force": True}
if event_type == "weekly_post":
return _load_lambda_app("weekly-post").handler(forced, None)
if event_type == "roster_sync":
return _load_lambda_app("roster-sync").handler(forced, None)
if event_type in {"ring_scheduler_daily", "ring_scheduler_weekend"}:
return _load_lambda_app("ring-scheduler").handler(forced, None)
raise ValueError(f"unknown job event {event_type!r}")