Commit graph

58 commits

Author SHA1 Message Date
Adam Moussa
07cd5ae72a
Repo hygiene: PR labeler + README badges (INFRA-56/57) (#24)
Some checks failed
Deploy / deploy (push) Has been cancelled
2026-06-11 14:14:09 -04:00
Adam Moussa
b92c25faf2
docs: CLAUDE.md cdk pin follows handbook Pinning Principle, not a hardcoded version (#22)
Some checks failed
Deploy / deploy (push) Has been cancelled
2026-06-05 18:44:12 -04:00
dependabot[bot]
ec363e6adf
Bump aws-cdk-lib in /cdk in the minor-and-patch group (#21)
Some checks are pending
Deploy / deploy (push) Waiting to run
Bumps the minor-and-patch group in /cdk with 1 update: [aws-cdk-lib](https://github.com/aws/aws-cdk).


Updates `aws-cdk-lib` from 2.257.0 to 2.258.0
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.alpha.md)
- [Commits](https://github.com/aws/aws-cdk/compare/v2.257.0...v2.258.0)

---
updated-dependencies:
- dependency-name: aws-cdk-lib
  dependency-version: 2.258.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:53:10 -04:00
Adam Moussa
6c392a7362
fix(deps): bump aws-cdk-lib pin to 2.257.0 (#20)
Some checks are pending
Deploy / deploy (push) Waiting to run
Org pin moved from 2.253.1 (bundles vulnerable fast-uri 3.1.0, 2 high
GHSAs) to 2.257.0 (bundles patched 3.1.2). Removes the blanket
dependabot ignore per the new handbook pinning policy (exact pins kept
current by Dependabot; blanket ignores banned).
2026-06-05 13:18:47 -04:00
Adam Moussa
47b6a9404f
chore(deps): ignore aws-cdk-lib in Dependabot (org pins ==2.253.1) (#19) 2026-06-05 12:40:47 -04:00
Adam Moussa
e8f375b03b
Add dependency-review caller workflow (#18)
* Add dependency-review caller workflow

Add a pull_request-triggered caller that invokes the org-level
callable-dependency-review workflow to scan dependency changes and
fail on high-severity advisories.

* chore: retrigger checks

* chore: retrigger dep review (post-fix)
2026-06-05 12:26:32 -04:00
Adam Moussa
ac610d1626 Re-enable CD deploy (remove leftover stack-merge gate)
Some checks failed
Deploy / deploy (push) Has been cancelled
2026-05-29 15:25:24 -04:00
Adam Moussa
6535b92e85
Merge pull request #16 from Sea-Haven-Industries/test/suite-hardening
Harden the test suite and wire it into CI
2026-05-29 15:20:54 -04:00
Adam Moussa
094c212323 Set default AWS region in conftest so boto3 clients construct in CI 2026-05-29 15:16:24 -04:00
Adam Moussa
17ce1b918b Install boto3/pandas/awswrangler as test deps so handler tests import in CI 2026-05-29 15:11:19 -04:00
Adam Moussa
0678f3f46f Document CI-run test suite and coverage in README 2026-05-29 15:05:29 -04:00
Adam Moussa
f47757adf0 Add coverage reporting via pytest-cov 2026-05-29 15:02:47 -04:00
Adam Moussa
8e87fe669d Add tests for slack-post handler and slackio 2026-05-29 15:01:25 -04:00
Adam Moussa
76f60abade Add per-bucket and precedence tests for comment intent 2026-05-29 14:59:40 -04:00
Adam Moussa
e555a75d97 Add tests for the Haiku fallback 2026-05-29 14:54:47 -04:00
Adam Moussa
48c030ed13 Add tests for the classifier handler transforms 2026-05-29 14:52:53 -04:00
Adam Moussa
63b6f11bf6 Add tests for the Slack interactions endpoint 2026-05-29 14:33:26 -04:00
Adam Moussa
817e1f6a74 Add synthetic export fixture and run classification quality gate in CI 2026-05-29 14:30:44 -04:00
Adam Moussa
1a126c8179 Enable test execution in CI 2026-05-29 14:30:44 -04:00
Adam Moussa
dcbb0606e6 Add pytest config and conftest for test discovery 2026-05-29 14:30:44 -04:00
Adam Moussa
54ef5400b1
Merge pull request #13 from Sea-Haven-Industries/feature/phase-6-docs
Some checks are pending
Deploy / deploy (push) Waiting to run
Phase 6: docs and re-enable CD on merge
2026-05-29 14:03:59 -04:00
Adam Moussa
6ddcd56f6c
Merge pull request #11 from Sea-Haven-Industries/feature/phase-5-grafana
Phase 5: self-hosted Grafana (EC2, ALB, dashboards-as-code)
2026-05-29 13:58:30 -04:00
Adam Moussa
c41836b319
Merge pull request #10 from Sea-Haven-Industries/feature/phase-4-slack
Phase 4: Slack post + interactions Lambdas (drill-down modals)
2026-05-29 13:55:24 -04:00
Adam Moussa
4cc58f5683
Merge pull request #9 from Sea-Haven-Industries/feature/phase-3-analytics
Some checks are pending
Deploy / deploy (push) Waiting to run
Phase 3: analytics dataset (Glue projection table + Athena)
2026-05-29 13:49:47 -04:00
Adam Moussa
0e96cb59d7
Merge pull request #8 from Sea-Haven-Industries/feature/phase-2-classifier
Phase 2: two-axis classifier Lambda
2026-05-29 13:43:59 -04:00
Adam Moussa
9ee3d84fe0 Enable QEMU for arm64 Lambda bundling in CI/CD 2026-05-29 13:37:01 -04:00
Adam Moussa
0ca738ca8c Enable QEMU for arm64 Lambda bundling in CI/CD 2026-05-29 13:37:00 -04:00
Adam Moussa
c4b9bd4305 Enable QEMU for arm64 Lambda bundling in CI/CD 2026-05-29 13:36:59 -04:00
Adam Moussa
5106853cb9 Enable QEMU for arm64 Lambda bundling in CI/CD 2026-05-29 13:36:58 -04:00
Adam Moussa
d33b82bd92 Enable QEMU for arm64 Lambda bundling in CI/CD 2026-05-29 13:36:56 -04:00
Adam Moussa
9346e29763
Merge pull request #7 from Sea-Haven-Industries/feature/phase-1-ingestion
Phase 1: add drop-folder ingestion and scoped uploader IAM user (Phase 1)
2026-05-29 13:24:07 -04:00
Adam Moussa
f86b4ba1c5
Merge pull request #6 from Sea-Haven-Industries/feature/phase-0-scaffold
Phase 0: scaffold apm-wo-analysis repository
2026-05-29 13:14:37 -04:00
Adam Moussa
1a231131bd Revert "Temporarily disable CD deploy during stack merges"
This reverts commit 611979e44e.
2026-05-29 13:07:02 -04:00
Adam Moussa
fee0fe9a2c Merge phase-0 to carry the CD-disable into phase-6 for an explicit revert 2026-05-29 13:06:57 -04:00
Adam Moussa
dec02170e6 Mark Phase 6 Confluence/Slack docs complete 2026-05-29 12:59:22 -04:00
Adam Moussa
dcda61e728 Add operational runbook (Phase 6)
docs/RUNBOOK.md: incident runbook for a missing daily analysis (detection →
context → triage → resolution-by-cause → post-incident), plus operational
procedures — export upload (direct + drop-folder agent), Grafana OS/app/plugin
patching cadence (clean-replacement preferred), dashboard-JSON redeploy flow +
gotchas, config + grafana.db/EBS backup-restore (DLM snapshot), and a common-
failures quick index. Mirrors to Confluence.
2026-05-29 12:31:14 -04:00
Adam Moussa
60e0b878e3 Refresh README to full operational doc (Phase 6)
Rewrite to the Sea Haven operational template with real resource names from both
stacks: AWS Resources + Lambda Functions tables, Configuration (Secrets/SSM/env/
context), Operations (verify, logs, classifier DLQ, reprocess, Grafana admin),
Documentation, and Notes/Gotchas incl. the deploy-time lessons + a known-debt
list. Corrects stale bits: ALB is internet-facing + office-IP-restricted (not
internal); classifier uses partition projection (no runtime Glue registration);
summary/details JSON live under meta/ not analytics/; grafana uses an instance
role. Adds the resources missing from the old README (classifier DLQ, slack-
interactions Lambda, HTTP API, meta/ + grafana-config/ prefixes, DLM backup,
encrypted volume).
2026-05-29 12:27:05 -04:00
Adam Moussa
611979e44e Temporarily disable CD deploy during stack merges
Gate the deploy job with if:${{ false }} so merging the Phase 0-5 stack into
main does not fire cdk deploy --all on every merge. Both stacks are already
deployed manually and validated in prod. Re-enable at the start of Phase 6 by
reverting this commit.
2026-05-29 11:43:48 -04:00
Adam Moussa
eae4d67e00 Apply cross-review findings (Phase 2/4/5 hardening)
From the cross_reviewer (GPT-4.1) per-PR passes, now that the orchestrator is
back up:

Phase 2 (classifier):
- Process ALL S3 records, not just event["Records"][0] — batched notifications
  no longer silently dropped (the review's only BLOCK).
- Derive the partition dt from the S3 event time, not the Lambda wall-clock —
  stable across retries / the midnight boundary.
- Add an SQS dead-letter queue so a failed run surfaces instead of dropping a
  day's data after Lambda's retries.

Phase 4 (Slack):
- Stage throttling (rate 10 / burst 20) on the public /slack/interactions HTTP
  API. (AWS WAF doesn't attach to apigwv2 HTTP APIs; stage throttling is the
  mechanism.)

Phase 5 (Grafana):
- Explicit encrypted=True on the gp3 root volume.

Tests: synth assertions for the DLQ, stage throttling, and the encrypted volume.
60/60 pass; cdk synth green for both stacks. Deferred NITs (print->logging, sig-
failure source-IP logging, S3 versioning, CIDR-maintenance runbook) -> Phase 6.

NOTE: like the earlier deploy fixes these sit on phase-5 but span phases — the
classifier/DLQ to #8, throttling to #10, encryption to #11 — reconcile at merge.
The encrypted-volume change needs the deferred clean instance replacement to
take effect (can't encrypt a live volume in place).
2026-05-29 11:29:14 -04:00
Adam Moussa
c970635be1 Fix WO table filter: drop custom allValue so :singlequote expands All
The 5 multi-select filter vars had allValue='All'. Grafana does NOT apply the
:singlequote format to a custom allValue, so 'All' was injected bare into
site IN (ALL) -> Athena read ALL as a column ('Column ALL cannot be resolved').
Removing the custom allValue lets :singlequote expand the All selection to the
real quoted value list, so the IN clause is valid SQL.
2026-05-29 11:13:10 -04:00
Adam Moussa
cb01b892bc Fix Grafana panels: rawSQL not rawSql (Athena plugin query key)
All 13 panel/variable queries keyed the SQL as rawSql (lowercase); the
grafana-athena-datasource plugin reads rawSQL (capital SQL). With the wrong key
the plugin saw an empty query, so no Athena query ever fired — variables had no
options and every panel showed a clean 'No data' (no error). This was the root
cause of the empty dashboard; data/datasource/permissions were all fine.
2026-05-29 10:55:02 -04:00
Adam Moussa
d884de8fcc Fix Grafana config-sync: --exact-timestamps for same-size updates
aws s3 sync skips same-size files on download unless --exact-timestamps is set,
so a dashboard edit that doesn't change file size (e.g. refresh 2->1, or a query
tweak) never propagated to the instance. Add --exact-timestamps to all four
sync invocations (boot + 15-min timer).
2026-05-28 19:06:06 -04:00
Adam Moussa
dce53fdc86 Fix Grafana template vars: refresh on dashboard load, not time-range change
All 6 query variables had refresh=2 (on time-range change) with no cached value,
so a plain dashboard load never populated them — $dt resolved to empty and every
panel filtered WHERE dt='' (no data). Set refresh=1 (on dashboard load).
2026-05-28 19:01:24 -04:00
Adam Moussa
3c8b7704f6 Fix Grafana Athena auth: use default credential chain, not ec2_iam_role
Grafana rejected the datasource with 'trying to use non-allowed auth method
ec2_iam_role: Failed to create client' — the plugin's allowed_auth_providers
defaults to default,keys,credentials and excludes ec2_iam_role. Switch authType
to 'default' (AWS SDK default chain), which on EC2 resolves to the instance role
via IMDS (still no static keys) and is allowed out of the box.
2026-05-28 18:59:09 -04:00
Adam Moussa
68f3acded8 Fix dashboard rendering: barchart panel type + metadata off the table prefix
Two issues found loading the deployed dashboard:

1. Panels used type "bar-chart" (hyphenated); Grafana's core panel is "barchart"
   — hence "plugin bar-chart required". Fixed both panels.

2. ALL panels showed "no data" because Athena failed with HIVE_BAD_DATA:
   the classifier wrote summary.json/details.json INTO analytics/dt=*/ — the
   same prefix the Glue table scans — so Athena tried to read the JSON as
   Parquet and every query failed. Move the metadata to a separate meta/dt=*/
   prefix: classifier writes there (grant_read_write meta/*), the Slack Lambdas
   read there (read_meta_json, grant_read meta/*), and analytics/ holds only
   Parquet. Verified: the category GROUP BY query now succeeds against Athena.
2026-05-28 18:49:41 -04:00
Adam Moussa
ea54cb1e60 Fix Grafana bootstrap: grafana-cli --homepath + valid plugin version
Found on first boot (cloud-init errored, grafana-server never started):
- grafana-cli needs --homepath=/usr/share/grafana or it can't find config
  defaults; under set -e that aborted the whole bootstrap.
- pinned plugin version 2.18.2 doesn't exist (conflated with awswrangler's
  version) — grafana-athena-datasource latest is 3.2.0.

Verified by running the corrected bootstrap on the instance via SSM: plugin
installs, grafana-server active, /api/health 200, ALB target healthy.

NOTE: the running instance was repaired in-place (the user-data change updated
the launch template but did not replace the instance). The committed user-data
is now correct, so a fresh launch boots clean — a one-time clean instance
replacement should validate that before prod sign-off.
2026-05-28 18:43:08 -04:00
Adam Moussa
f193754c27 Fix deploy-time failures found in prod testing
Two issues only a real deploy/run surfaced (synth + offline tests passed):

1. Classifier exceeded Lambda's 250 MB unzipped limit (bundled awswrangler +
   pandas + pyarrow + numpy). Move them to the AWS-managed SDK-for-pandas layer
   (AWSSDKPandas-Python312-Arm64:27, awswrangler 3.16.1, pre-stripped to fit);
   bundle only openpyxl. Drop the unused anthropic SDK — _call_haiku uses stdlib
   urllib. Function package now ~890 KB.

2. Slack rejected the daily post with invalid_blocks: every category drill
   button shared action_id "drill_category". Qualify it as "drill_category:<cat>"
   for uniqueness; the interactions handler now matches on the prefix. Add a
   regression test asserting all daily-summary action_ids are unique.

Verified in prod: classifier writes Parquet + summary.json + details.json;
slack-post posts the daily summary + 3rd-escalation alert; the interactions
endpoint (apm-wo.seahaven.com) returns 401 on a bad signature. 58/58 tests pass.

NOTE: these fixes sit on the phase-5 branch but logically belong to earlier
phases — the layer fix to #8 (classifier), the Slack fix to #10 — and must be
moved/cherry-picked there before those PRs merge independently. See cleanup.
2026-05-28 18:29:16 -04:00
Adam Moussa
c04c239774 Update README status for Phase 5 Grafana stack 2026-05-28 18:06:10 -04:00
Adam Moussa
4870784fbd Add self-hosted Grafana stack: EC2, ALB, dashboards-as-code (Phase 5)
The one non-serverless piece — Grafana OSS on a t4g.small (AL2023, ARM64) in the
imported seahaven-vpc, fronted by an internet-facing ALB locked by SG to the
office CIDRs (no Client VPN exists, so "VPN-only" = office-IP restriction, the
syslog-server pattern). Instance in private subnets, reachable only from the ALB
SG, administered via SSM Session Manager (no SSH/key pair).

grafana_stack.py: ALB (HTTPS, *.seahaven.com cert, open=False so the SG office
rules aren't undone by an auto 0.0.0.0/0), instance role (Athena query + Glue
read + S3 analytics/athena-results, no static keys), Route53 grafana.seahaven.com
alias, gp3 root volume RETAINed, daily DLM snapshot of the tagged instance, and a
BucketDeployment that uploads grafana/ to the S3 config prefix.

grafana_userdata.sh: install Grafana OSS, pin the Athena datasource plugin, write
grafana.ini (root_url grafana.seahaven.com, kiosk embedding), sync provisioning +
dashboards from S3 on boot, and a systemd timer re-syncs every 15 min so repo
edits land without an instance rebuild.

Dashboard (grafana-author agent, grafana/dashboards/apm-work-orders.json, uid
apm-wo so the Slack 📊 button resolves): 7 panels — category distribution,
escalation summary, action/routine, escalations-by-site, trend time-series over
dt (the new capability), filterable WO table (5 template vars, escalation row
coloring, CSV export, no APM links), and the mismatch panel. Datasource uid
"athena" pinned in the provisioning yaml.

Tests: tests/test_grafana_synth.py — ALB admits only the office CIDRs on 443
(caught and fixed a default 0.0.0.0/0 listener rule), instance only-from-ALB,
no static keys, scoped instance role + SSM, gp3+retained root volume, daily DLM
backup, grafana.seahaven.com alias. 57/57 tests pass; full cdk synth green.
2026-05-28 18:05:20 -04:00
Adam Moussa
cfb9fb85d2 Update README for Phase 4 Slack surfaces 2026-05-28 17:49:57 -04:00