apm-wo-analysis/lambdas/slack_post/handler.py

60 lines
2.3 KiB
Python
Raw Permalink Normal View History

Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
"""Slack post Lambda: daily summary + batched 3rd-escalation alert.
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
Async-invoked by the classifier with ``{"dt": "YYYY-MM-DD"}`` once the day's
snapshot is written. Reads today's (and yesterday's, for deltas) ``summary.json``
from ``analytics/``, posts the daily summary to the WO channel via the reused bot
token (Secrets Manager), and — only when the 3rd-escalation count > 0 — posts the
standalone batched alert (reading ``details.json`` for the 3rd-escalation rows).
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
Runtime: Python 3.12, ARM64. No App Home — two push surfaces only (see CLAUDE.md).
"""
from __future__ import annotations
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
from datetime import datetime, timedelta, timezone
import blockkit
import slackio
def _yesterday(dt: str) -> str:
return (datetime.strptime(dt, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
def handler(event, context):
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
"""Post the daily summary and (conditionally) the 3rd-escalation alert."""
dt = (event or {}).get("dt") or datetime.now(timezone.utc).strftime("%Y-%m-%d")
today = slackio.read_meta_json(dt, "summary.json")
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
if today is None:
print(f"No summary.json for dt={dt}; nothing to post.")
return {"posted": False, "reason": "no summary", "dt": dt}
yesterday = slackio.read_meta_json(_yesterday(dt), "summary.json")
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
client = slackio.web_client()
channel = slackio.channel_id()
dashboard_url = slackio.get_dashboard_url()
summary_blocks = blockkit.build_daily_summary(today, yesterday, dashboard_url)
client.chat_postMessage(
channel=channel, blocks=summary_blocks, text=f"APM Work Orders — {dt}"
)
posted = {"summary": True, "alert": False}
# Standalone batched alert — only when there are 3rd escalations. Pull the
# rows from details.json so the alert can name the WOs.
if today.get("third_escalation_count", 0) > 0:
details = slackio.read_meta_json(dt, "details.json") or []
Add Slack post + interactions Lambdas with drill-down modals (Phase 4) Two push surfaces (no App Home) + interactive drill-down, per CLAUDE.md. Block Kit (blockkit.py, pure/offline): build_daily_summary (header, vs-yesterday deltas, escalation breakdown with 3rd highlighted, action/routine, top sites, mismatch callout, category drill buttons + 📊 Open dashboard link, footer), build_escalation_alert (one @here, returns None on zero-3rd — suppression), and build_wo_modal (views.open payload, capped under Slack's 100-block limit). Lambdas: slack_post/handler.py (classifier-invoked: read today/yesterday summary.json, post daily summary, conditionally post the batched alert from details.json) and slack_post/interactions.py (API Gateway: verify Slack signature, filter details.json, views.open the WO modal within the 3s trigger_id window). slackio.py centralizes Secrets Manager creds, the SSM dashboard URL, signature verification, and analytics/ reads — keeping blockkit pure. Classifier: emit analytics/dt=*/details.json (per-WO index for the modals) and async-invoke slack-post after the snapshot write (best-effort; a Slack failure never fails classification). CDK: slack-post + interactions Lambdas (Docker-bundled slack_sdk), HTTP API on apm-wo.seahaven.com (wildcard ACM cert + Route53 alias; signature-verified, so the route is unauthenticated by design), SSM /apm-wo-analysis/grafana-base-url, and scoped IAM (read analytics/, read the Slack secret + dashboard param; classifier granted lambda:InvokeFunction on slack-post). Slack creds live in one Secrets Manager secret apm-wo-analysis/slack-credentials {botToken, signingSecret, channelId}; cdk.json gains cert/zone/domain context. WO drill-downs link to Grafana only — no APM deep-links (per decision). Deliverables for test time: slack/manifest.yaml (app manifest, interactivity request_url = apm-wo.seahaven.com). Tests: tests/test_blockkit.py (30 offline cases — deltas, zero-3rd None, <100 blocks under large inputs, modal truncation/overflow, dashboard URL) and Phase 4 assertions in test_pipeline_synth.py (both Lambdas, the API route/domain/alias, and no broad/write IAM on the Slack roles). 49/49 tests pass; cdk synth green.
2026-05-28 17:48:51 -04:00
thirds = [d for d in details if d.get("category") == "3rd Escalation"]
alert_blocks = blockkit.build_escalation_alert(thirds)
if alert_blocks:
client.chat_postMessage(
channel=channel,
blocks=alert_blocks,
text=f"🚨 {len(thirds)} 3rd-escalation work orders",
)
posted["alert"] = True
print(f"Posted dt={dt}: {posted}")
return {"posted": True, "dt": dt, **posted}