"use strict";
/* =========================================================
   DATA — real resource names from the CDK stacks (infra review 12 Jul 2026)
   ========================================================= */
const E = (env) => env; // placeholder marker
let NODES = [
// Clients
{ id:"clients", g:"clients", x:30, y:240, c:"--aqua", t:"Clients", s:"mobile + web app", env:"both",
	d:{ desc:"The Fairground app on iOS, Android and web. Everything it does goes through two doors: API calls to the load balancer, and media through CloudFront signed URLs.",
	facts:["All API traffic → the ALB","All media playback → CloudFront","Never talks to S3 or the database directly"] } },
// Edge
{ id:"alb", g:"edge", x:330, y:180, c:"--blue", t:"ALB", s:"fairground-alb-<env> · HTTP :80 → :3000", env:"both",
	d:{ desc:"Internet-facing Application Load Balancer in the public subnets. Forwards to the ECS target group on port 3000.",
	facts:["Target group fairground-tg-<env>, target type IP","Health check GET /health expecting 200, every 60s","Alarm: avg response time > 3s for 5 min → SNS → team email","If the app feels slow, this is the first graph to look at"],
	cfg:[["Listener","HTTP :80"],["Target port","3000"],["Health check interval","60s"],["Slow-response alarm","3s for 5 min"]],
	logs:[["ALB monitoring is metrics-only (no access logs enabled) — use the CloudWatch dashboard", ""]],
	fixes:["Slow responses? Check TargetResponseTime + ECS memory/CPU before blaming the network","5xx spikes usually mean unhealthy targets — check target group health + service events"] } },
{ id:"cdn", g:"edge", x:330, y:520, c:"--blue", t:"CloudFront CDNs", s:"music · populator media · videos", env:"both",
	d:{ desc:"Pre-existing CloudFront distributions (not in CDK) fronting the media buckets. The backend signs URLs with a CloudFront key pair.",
	facts:["MUSIC_CDN_URL, POPULATOR_MEDIA_CDN_URL, POPULATOR_VIDEOS_CDN_URL","Signing key: CLOUDFRONT_KEY_PAIR_ID + private key in Secrets Manager","Clients never hit S3 directly for playback"] } },
{ id:"imgx", g:"edge", x:330, y:660, c:"--blue", t:"Image transform λ", s:"Function URL · sharp · 1 GB", env:"both",
	d:{ desc:"On-the-fly image resizing Lambda with a public Function URL. Reads originals from the populators media bucket, writes results to a cache bucket.",
	facts:["fairground-image-transform-handler-<env>","Usage: /image.jpg?w=300&h=200&f=webp&q=85","Bundles sharp + heic-convert (Docker bundling — needs Colima locally)","Cache bucket fairground-populators-media-<env>-cache"],
	logs:[["aws logs tail /aws/lambda/fairground-image-transform-handler-<env> --follow --profile <profile> --region eu-west-2","1 week retention"]] } },
// Core
{ id:"ecs", g:"core", x:640, y:150, c:"--violet", t:"ECS Fargate service", s:"fairground-service-<env> · the backend API", env:"both",
	d:{ desc:"The Express backend (apps/backend), one container per task, port 3000, private subnets, no public IP.",
	facts:["Cluster fairground-cluster-<env> · Container Insights on (prod)","Task size and scaling are set in infra/config/<env>.ts","Autoscaling adds tasks when CPU stays above the target","Deployment circuit breaker rolls bad deploys back automatically","Secrets injected at task start from fairground-<env>-backend-configs/-secrets","execute-command enabled — you can shell into a task"],
	cfg:[["Task CPU","1 vCPU"],["Task memory","2048 MiB"],["Min tasks","3"],["Max tasks","10"],["CPU scale-out target","70%"],["Health check path","/health"]],
	logs:[["aws logs tail /ecs/fairground-<env> --follow --profile <profile> --region eu-west-2","3 day retention"],["aws ecs execute-command --cluster fairground-cluster-<env> --task <task-id> --container FairgroundBackend --interactive --command \"/bin/sh\" --profile <profile> --region eu-west-2","shell into a running task"]],
	fixes:["Force fresh tasks: aws ecs update-service --cluster fairground-cluster-<env> --service fairground-service-<env> --force-new-deployment","Rollback: run the Backend - Rollback workflow (previous task-def revision)","Secrets changed? They load at task start — force a new deployment to pick them up"] } },
{ id:"ecr", g:"core", x:640, y:360, c:"--violet", t:"ECR", s:"fairground-backend-<env> · sha + latest tags", env:"both",
	d:{ desc:"Container registry for the backend image. CI pushes every deploy tagged with the git sha and latest.",
	facts:["Lifecycle: keeps max 10 images","Image scanning on push","⚠ Running cdk deploy by hand? Always pass -c imageTag=<running sha>, otherwise it deploys :latest"] } },
{ id:"secrets", g:"core", x:640, y:500, c:"--violet", t:"Secrets Manager", s:"fairground-<env>-backend-configs / -secrets", env:"both",
	d:{ desc:"Two secrets per env: configs (non-sensitive) and secrets (sensitive). Everything the backend needs is injected as ECS container secrets at task start.",
	facts:["Supabase, Stripe, Resend, PostHog, Sentry, Firebase, DICE, Apple IAP, MediaConvert role, CloudFront keys","Frontend has its own: fairground-<env>-frontend-config/-secrets","Mercur + SFTPGo have separate secret pairs","Changing a value does NOT restart the service — force a new deployment"] } },
{ id:"gha", g:"core", x:640, y:640, c:"--violet", t:"GitHub Actions", s:"OIDC · main → prod · dev → staging", env:"both",
	d:{ desc:"All deploys run from GitHub Actions with OIDC role assumption — no long-lived AWS keys.",
	facts:["Roles: GitHubOIDC_FairgroundBackend (per account)","Backend deploy: migrations → cdk deploy --all → docker build/push → register-task-definition → update-service","Slack notifications on every deploy","See the Deploys tab for every pipeline step-by-step"] } },
// Async
{ id:"sns", g:"async", x:1000, y:80, c:"--yellow", t:"SNS notifications", s:"fairground-notifications-<env>", env:"both",
	d:{ desc:"The backend publishes notification events here; SNS fans out to the processing queue.",
	facts:["ARN exported as FairgroundNotificationTopicArn-<env>","Backend reads it from config key NOTIFICATIONS_TOPIC_ARN"] } },
{ id:"sqsN", g:"async", x:1000, y:200, c:"--yellow", t:"SQS + DLQ", s:"notifications-processing-queue-<env>", env:"both",
	d:{ desc:"Buffers notification jobs. After 3 failed receives a message lands in the dead-letter queue.",
	facts:["Visibility timeout 60s, maxReceiveCount 3","DLQ: fairground-notifications-dead-letter-queue-<env>","Check DLQ depth when users report missing notifications"],
	cfg:[["Visibility timeout","60s"],["Max receives before DLQ","3"],["Lambda batch size","10"]] } },
{ id:"lamN", g:"async", x:1000, y:330, c:"--yellow", t:"Notifications λ", s:"fairground-notifications-handler-<env>", env:"both",
	d:{ desc:"Consumes the queue in batches of 10 and sends push (Firebase FCM) + email (Resend, mustache templates).",
	facts:["Node 20 · 256 MB · 30s timeout · batch 10 with partial-failure reporting","Bundles firebase-admin + mustache email templates"],
	logs:[["aws logs tail /aws/lambda/fairground-notifications-handler-<env> --follow --profile <profile> --region eu-west-2","no expiry set"]] } },
{ id:"mc", g:"async", x:1000, y:470, c:"--yellow", t:"MediaConvert", s:"video transcoding jobs", env:"both",
	d:{ desc:"The backend creates MediaConvert jobs for uploaded video (uses the pre-existing MediaConvertRole). Job state changes flow through EventBridge to a handler Lambda that updates Supabase.",
	facts:["EventBridge rule fairground-mediaconvert-rule-<env> (COMPLETE/ERROR/CANCELED)","Handler: fairground-mediaconvert-handler-<env> (256 MB)"],
	logs:[["aws logs tail /aws/lambda/fairground-mediaconvert-handler-<env> --follow --profile <profile> --region eu-west-2","1 week retention"]] } },
{ id:"crons", g:"async", x:1000, y:610, c:"--yellow", t:"Scheduled λs", s:"payouts ×3 · analytics · deletion retry", env:"both",
	d:{ desc:"EventBridge-scheduled Lambdas doing the recurring work.",
	facts:["fairground-payouts-disbursement-<env> — hourly","fairground-payouts-reconciliation-<env> — every 15 min","fairground-payouts-audit-<env> — daily 04:00 UTC","Analytics daily rollup — daily 03:00 UTC","Delete-account retry — daily 00:00 UTC","Payout λs: 1 GB, 5 min timeout, talk to Stripe + Supabase"],
	logs:[["aws logs tail /aws/lambda/fairground-payouts-disbursement-<env> --follow --profile <profile> --region eu-west-2","no expiry set"]] } },
{ id:"del", g:"async", x:1000, y:760, c:"--yellow", t:"Account deletion", s:"audit λ → SQS → cancel-worker λ", env:"both",
	d:{ desc:"Backend invokes the audit handler directly; it queues subscription cancellations which a worker Lambda drains (Stripe + Supabase cleanup). A daily retry sweep re-runs failures.",
	facts:["fairground-delete-account-audit-handler-<env>","Queue fairground-delete-account-cancellation-<env> (DLQ, 5 receives)","fairground-delete-account-cancel-worker-<env>","Deleting an artist deletes purchased tracks — intended behaviour"] } },
// Data / external
{ id:"supabase", g:"ext", x:1380, y:100, c:"--red", t:"Supabase", s:"Postgres · Auth · RLS · Storage", env:"both",
	d:{ desc:"The primary database and auth provider. Nearly every component talks to it: the backend per-request (forwarding the caller's JWT for RLS), and every Lambda with the service-role key.",
	facts:["Backend forwards each user's JWT so row-level security applies per request","Lambdas use the service-role key (full access)","DB migrations run automatically at the start of every backend deploy","Local dev stack runs via the supabase CLI"] } },
{ id:"s3", g:"ext", x:1380, y:280, c:"--red", t:"S3 buckets", s:"music · previews · media · videos · assets", env:"both",
	d:{ desc:"Media and asset storage, fronted by CloudFront. Buckets are pre-existing (imported by CDK, not created).",
	facts:["fairground-populators-media-<env> (+ -cache for image transform)","fairground-populators-videos-<env>","music, previews, metadata, user-assets buckets via config keys","fairground-web-builds[-staging] for web deploy artifacts","fairground-sftp-deliveries-staging-<acct> for label deliveries"] } },
{ id:"stripe", g:"ext", x:1380, y:440, c:"--red", t:"Stripe", s:"payments · Connect · webhooks", env:"both",
	d:{ desc:"Payments and payouts. The backend receives Stripe webhooks (platform + connected accounts); payout Lambdas drive disbursements and reconciliation.",
	facts:["STRIPE_WEBHOOK_SECRET + STRIPE_CONNECTED_WEBHOOK_SECRET verify signatures","Payout settlement webhooks call the payouts service in the backend","Dashboard references a fairground-stripe-webhook-<env> λ that is NOT in CDK (legacy)"] } },
{ id:"msg", g:"ext", x:1380, y:580, c:"--red", t:"Firebase + Resend", s:"push + email delivery", env:"both",
	d:{ desc:"Firebase Cloud Messaging delivers push to devices; Resend sends transactional email. Both used by the notifications Lambda (and Resend by the backend directly).",
	facts:["Firebase service-account creds in Secrets Manager","Email templates are mustache files bundled with the λ"] } },
{ id:"obs", g:"ext", x:1380, y:720, c:"--red", t:"Observability", s:"CloudWatch · Sentry · PostHog", env:"both",
	d:{ desc:"CloudWatch holds metrics, logs, the dashboard and alarms. Sentry captures backend + frontend errors, PostHog product analytics.",
	facts:["Dashboard: fairground-monitoring-<env>","Alarms → SNS fairground-alerts-prod → team email","SFTP DLQ alarm → email","Container Insights per-task metrics (prod)"],
	cfg:[["Memory alarm","Max > 70% for 5 min"],["Response-time alarm","avg > 3s for 5 min"],["ECS log retention","3 days"]] } },
// Staging-only
{ id:"mercur", g:"stag", x:330, y:840, c:"--orange", t:"Mercur backend", s:"Medusa marketplace · ECS :9000", env:"staging",
	d:{ desc:"Separate Fargate service running the Mercur/Medusa marketplace backend behind its own HTTPS ALB. Currently staging-only (prod config exists, disabled).",
	facts:["mercur.staging.api.fairground.music (ACM + Route53 alias)","Cluster fairground-mercur-backend-staging-cluster","Own secrets + shared Supabase/Stripe secrets · Redis + Algolia","Deploy: docker push → cdk deploy (WITH imageTag) → force redeploy"],
	logs:[["aws logs tail /ecs/fairground-mercur-backend-staging --follow --profile staging --region eu-west-2","3 day retention"]] } },
{ id:"sftp", g:"stag", x:640, y:840, c:"--orange", t:"SFTPGo (EC2)", s:"staging.sftp.fairground.music · t3.small", env:"staging",
	d:{ desc:"Self-hosted SFTP server for label file delivery ('Upload spreadsheet' flow). Docker on a single EC2 instance with Caddy for TLS, Elastic IP.",
	facts:["drakkan/sftpgo v2.7.0 + caddy 2.10.2 via UserData","SSM-managed (no SSH keys needed): aws ssm start-session --target <instance-id>","Delivery bucket fairground-sftp-deliveries-staging-<acct>"] } },
{ id:"sftpq", g:"stag", x:1000, y:900, c:"--orange", t:"SFTP delivery queue + λ", s:"delivery-processor · X-Ray on", env:"staging",
	d:{ desc:"The backend queues delivery jobs; the processor Lambda builds the files and writes them to the delivery bucket that SFTPGo serves.",
	facts:["Queue fairground-sftp-delivery-processor-staging (DLQ after 3)","λ 1 GB / 300s / batch 1 / X-Ray tracing","DLQ alarm → SNS → email"],
	logs:[["aws logs tail /aws/lambda/fairground-sftp-delivery-processor-staging --follow --profile staging --region eu-west-2","2 week retention"]] } },
];
let EDGES = [
["clients","alb","HTTPS API"],
["clients","cdn","signed media"],
["alb","ecs",":3000"],
["ecs","supabase","JWT / RLS"],["ecs","s3","uploads"],["ecs","sns","publish"],
["ecs","mc","create job"],["stripe","ecs","webhooks"],
["ecr","ecs","image",1],["secrets","ecs","secrets at boot",1],
["gha","ecr","push sha"],["gha","ecs","deploy"],
["sns","sqsN","fan-out"],["sqsN","lamN","batch 10"],["lamN","msg","push + email"],["lamN","supabase","",1],
["mc","supabase","job status"],
["crons","stripe","payouts"],["crons","supabase","",1],
["ecs","del","invoke"],["del","stripe","cancel subs"],["del","supabase","",1],
["cdn","s3","origin",1],["imgx","s3","resize + cache"],["clients","imgx","",1],
["ecs","obs","logs + metrics",1],
["mercur","supabase","",1],["mercur","stripe","",1],
["ecs","sftpq","queue job"],["sftpq","sftp","write files"],["sftpq","s3","",1],
];
const LANE_BOXES = [
	["Clients", 14, 200, 236, 190],
	["Edge", 276, 96, 236, 560],
	["Core — VPC · private subnets", 538, 96, 236, 620],
	["Async / events", 830, 62, 236, 800],
	["Data & external", 1130, 62, 236, 730],
	["Staging-only", 276, 892, 796, 180],
];
const LAYOUT = {
	clients:[30,240],
	alb:[292,160], cdn:[292,390], imgx:[292,540],
	ecs:[554,140], ecr:[554,320], secrets:[554,460], gha:[554,600],
	sns:[846,100], sqsN:[846,215], lamN:[846,330], mc:[846,470], crons:[846,600], del:[846,740],
	supabase:[1146,100], s3:[1146,255], stripe:[1146,405], msg:[1146,540], obs:[1146,660],
	mercur:[292,930], sftp:[554,930], sftpq:[846,930],
};
const FLOWS = {
	"Start walkthrough": { tour: true, cumulative: false, steps: [
		[["ecs"], "<b>Start here: the backend.</b> One Express app (apps/backend) running as containers on ECS Fargate — no servers to patch or manage. Everything else on this map exists to serve it."],
		[["alb","ecs"], "<b>How traffic gets in.</b> The load balancer is the only public entry point. It health-checks every container (GET /health each minute) and spreads requests across them. Containers live in private subnets — nothing reaches them directly."],
		[["ecs","supabase"], "<b>Data and auth.</b> Supabase is our Postgres + login. The backend forwards each user's token, so the database itself enforces row-level security — a user can only ever read their own rows."],
		[["secrets","ecs"], "<b>Configuration.</b> Every key and setting lives in Secrets Manager and is injected when a container starts. Nothing sensitive in code or env files. Changed a secret? Force a new deployment to pick it up."],
		[["gha","ecr","ecs"], "<b>How code ships.</b> Merge to main → GitHub Actions runs the DB migrations, builds a Docker image into ECR, and rolls ECS onto the new version container by container. A failing deploy rolls back automatically."],
		[["ecs","s3","cdn","imgx"], "<b>Media.</b> Files live in S3 buckets, never public. Clients stream through CloudFront with signed URLs, and a small Lambda resizes images on the fly."],
		[["ecs","mc","supabase"], "<b>Video pipeline.</b> Uploads are transcoded by MediaConvert. When a job finishes, an event fires a Lambda that marks the video ready in the database."],
		[["ecs","sns","sqsN","lamN","msg"], "<b>Notifications.</b> The backend publishes an event, it queues up, and a Lambda delivers push + email. Failures retry 3 times, then park in a dead-letter queue we can inspect."],
		[["crons","stripe","supabase"], "<b>Scheduled work.</b> Payout disbursement runs hourly, reconciliation every 15 minutes, audits and analytics rollups daily — all as scheduled Lambdas talking to Stripe and the database."],
		[["ecs","del","stripe"], "<b>Account deletion.</b> Its own little pipeline: an audit Lambda, a queue, and a worker that cancels Stripe subscriptions and cleans data, with a daily retry sweep."],
		[["ecs","obs"], "<b>Keeping an eye on it.</b> Logs and metrics flow to CloudWatch. Alarms — memory, slow responses, dead-letter queues — email the team via SNS. Sentry catches application errors."],
		[["mercur","sftp","sftpq"], "<b>Staging-only for now.</b> The Mercur marketplace backend, the SFTP server for label file deliveries, and its processing queue. Same patterns, separate stacks, switched on per environment."],
	]},
	"API request": { cumulative: true, steps: [
		[["clients"], "App calls the API with the user's Supabase JWT"],
		[["alb"], "<b>fairground-alb-prod</b> forwards to a healthy container on :3000"],
		[["ecs"], "Auth middleware verifies the JWT and scopes this request to the user"],
		[["supabase"], "Query runs under <b>row-level security</b>; response heads back the same path"]]},
	"Media playback": { cumulative: true, steps: [
		[["clients"], "App asks the backend for a track or image"],
		[["ecs"], "Backend signs a <b>CloudFront URL</b> with the key pair from Secrets Manager"],
		[["cdn"], "Client streams straight from CloudFront — S3 is never public"],
		[["s3"], "Origin fetch only on cache miss"]]},
	"Video upload": { cumulative: true, steps: [
		[["ecs"], "Upload lands in S3; backend creates a <b>MediaConvert job</b>"],
		[["mc"], "MediaConvert transcodes; job completion fires an event"],
		[["supabase"], "Handler λ marks the video ready → appears in feeds"]]},
	"Push notification": { cumulative: true, steps: [
		[["ecs"], "Backend publishes an event to the notifications topic"],
		[["sns"], "SNS fans out to the processing queue"],
		[["sqsN"], "Buffered; 3 failures → dead-letter queue"],
		[["lamN"], "Handler renders the message templates"],
		[["msg"], "<b>FCM</b> push + <b>Resend</b> email delivered"]]},
	"Backend deploy": { cumulative: true, steps: [
		[["gha"], "Push to <b>main</b> → DB migrations run first"],
		[["gha"], "<b>cdk deploy --all</b> keeps the AWS infra in sync with the code"],
		[["ecr"], "Docker image built, pushed tagged <b>&lt;git sha&gt;</b> + latest"],
		[["ecs"], "New task definition registered; rolling deploy with automatic rollback; /health verified"]]},
	"Payout run": { cumulative: true, steps: [
		[["crons"], "<b>Hourly</b> disbursement λ (reconciliation every 15 min, audit daily)"],
		[["supabase"], "Reads settlements and balances"],
		[["stripe"], "Creates transfers &amp; payouts; webhooks confirm back to the backend"]]},
};
const PIPELINES = [
{ id:"backend", name:"Backend → ECS", meta:"backend-deploy.yml · push to main (prod) / dev (staging) touching apps/backend/**", steps:[
	["Build workspace","npm ci, build @fairground/api-contract, install infra deps"],
	["Assume OIDC role","GitHubOIDC_FairgroundBackend on the target account — no static keys anywhere"],
	["DB migrations","migrate:validate → migrate:apply-all-prod → apply:rls → regenerate supabase types. DB creds pulled from Secrets Manager", "npm run migrate:apply-all-prod"],
	["Deploy infrastructure","cdk deploy --all -c environment=<env> --require-approval never — deploys every core stack", "cd apps/backend/infra && npm run deploy:prod"],
	["Build + push image","Docker image tagged with the git sha plus latest, pushed to fairground-backend-<env>"],
	["Register task definition","CI downloads the CURRENT task def, swaps containerDefinitions[0].image to the new sha, aws ecs register-task-definition — CDK does not own the image", "aws ecs update-service --cluster fairground-cluster-<env> --service fairground-service-<env> --task-definition NEW_TASK_DEF"],
	["Verify","waits services-stable, then curls the ALB /health; Slack notification either way"]],
	callout:["crit","<b>The imageTag gotcha:</b> the CloudFormation template references <code>:latest</code> but the running task def is pinned to a sha by CI. If you ever run <code>cdk deploy</code> by hand without <code>-c imageTag=&lt;current sha&gt;</code>, CloudFormation will replace the task def and point prod at whatever <code>:latest</code> is in ECR. Always pass the sha."] },
{ id:"rollback", name:"Backend rollback", meta:"backend-rollback.yml · manual dispatch", steps:[
	["Pick revision","Defaults to the previous ACTIVE task-definition revision of fairground-service-<env>, or pass one explicitly"],
	["Update service","Pure CLI — no CDK involved", "aws ecs update-service --cluster fairground-cluster-prod --service fairground-service-prod --task-definition fairground-task-<env>:PREVIOUS_REV --force-new-deployment"],
	["Wait stable","Rolling replacement; old images stay in ECR (last 10 kept)"]],
	callout:["warn","Rollback rolls back the <b>image only</b>. If the bad deploy also ran DB migrations, those are still applied — check whether the previous code tolerates the new schema."] },
{ id:"mercur", name:"Mercur backend", meta:"mercur-backend-deploy.yml · staging-enabled", steps:[
	["Build","yarn/corepack build, docker buildx linux/amd64"],
	["Push","ECR fairground-mercur-backend-<env> tagged latest-<shortsha> + latest"],
	["CDK (staging)","cdk deploy FairgroundMercurBackend-staging -c stackGroup=mercur -c imageTag=<tag> — this pipeline DOES pass the imageTag correctly"],
	["Redeploy","aws ecs update-service --force-new-deployment on the mercur cluster/service"]] },
];
const LOGS = [
	["Backend (ECS)","/ecs/fairground-<env>","3 days","aws logs tail /ecs/fairground-<env> --follow --since 15m"],
	["Mercur backend (ECS)","/ecs/fairground-mercur-backend-staging","3 days","aws logs tail /ecs/fairground-mercur-backend-staging --follow"],
	["Notifications λ","/aws/lambda/fairground-notifications-handler-<env>","never expires","aws logs tail /aws/lambda/fairground-notifications-handler-<env> --follow"],
	["MediaConvert λ","/aws/lambda/fairground-mediaconvert-handler-<env>","1 week","aws logs tail /aws/lambda/fairground-mediaconvert-handler-<env> --follow"],
	["Image transform λ","/aws/lambda/fairground-image-transform-handler-<env>","1 week","aws logs tail /aws/lambda/fairground-image-transform-handler-<env> --follow"],
	["SFTP processor λ","/aws/lambda/fairground-sftp-delivery-processor-staging","2 weeks","aws logs tail /aws/lambda/fairground-sftp-delivery-processor-staging --follow --profile staging"],
	["Payout λs (×3)","/aws/lambda/fairground-payouts-*-<env>","never expires","aws logs tail /aws/lambda/fairground-payouts-disbursement-<env> --follow"],
	["Delete-account λs (×3)","/aws/lambda/fairground-delete-account-*-<env>","never expires","aws logs tail /aws/lambda/fairground-delete-account-audit-handler-<env> --follow"],
];
const RUNBOOK = [
["p1","App is slow / requests taking seconds", `<ol>
<li>CloudWatch dashboard <b>fairground-monitoring-prod</b> → check ECS <b>MemoryUtilization</b> and CPU, and ALB TargetResponseTime.</li>
<li>Memory near 100%? The app slows down long before it crashes (the process spends all its time on garbage collection). Get fresh tasks now, diagnose after:</li>
</ol>
<div class="cmd">aws ecs update-service --cluster fairground-cluster-prod --service fairground-service-prod --force-new-deployment --profile production --region eu-west-2<button class="copy">copy</button></div>
<ol start="3"><li>CPU pinned instead? Autoscaling should be adding tasks (70% target) — check service events for scaling activity.</li><li>Neither? Check Supabase status + slow queries, then ALB 5xx counts.</li></ol>`],
["p1","Bad deploy — roll it back", `<ol>
<li>Fastest: GitHub → Actions → <b>Backend - Rollback</b> → run with env, it flips to the previous task-def revision.</li>
<li>Manual equivalent:</li></ol>
<div class="cmd">aws ecs describe-services --cluster fairground-cluster-prod --services fairground-service-prod --query "services[0].taskDefinition" --profile production --region eu-west-2<button class="copy">copy</button></div>
<div class="cmd">aws ecs update-service --cluster fairground-cluster-prod --service fairground-service-prod --task-definition fairground-task-prod:&lt;previous-rev&gt; --force-new-deployment --profile production --region eu-west-2<button class="copy">copy</button></div>
<p>Remember: migrations don't roll back with the image.</p>`],
["p2","Memory alarm fired (memory-high email)", `<ol>
<li>Alarm = Max task memory > 70% for 5 min. Look at the per-task breakdown (Container Insights) — one task or all?</li>
<li>All tasks climbing slowly together → leak pattern → capture evidence (task id, uptime), then force a new deployment.</li>
<li>One task spiking → check what it was doing: <div class="cmd">aws logs tail /ecs/fairground-prod --since 30m --profile production --region eu-west-2 | grep -i error<button class="copy">copy</button></div></li>
<li>All tasks climbing slowly together usually means a memory leak — note a task id and its uptime before recycling, so it can be profiled.</li></ol>`],
["p2","Deployment stuck / circuit breaker rolled back", `<ol>
<li>Service events tell you why:</li></ol>
<div class="cmd">aws ecs describe-services --cluster fairground-cluster-prod --services fairground-service-prod --query "services[0].events[0:10].message" --profile production --region eu-west-2<button class="copy">copy</button></div>
<ol start="2"><li>Usually failed /health checks — tail the new task's logs for a crash on boot (bad secret, migration mismatch).</li>
<li>Task fails before logs? Check stopped-task reason: <div class="cmd">aws ecs list-tasks --cluster fairground-cluster-prod --desired-status STOPPED --profile production --region eu-west-2<button class="copy">copy</button></div></li></ol>`],
["p2","Changed a secret and nothing happened", `<p>Secrets are injected at <b>task start</b>, not read live. After editing fairground-prod-backend-configs/-secrets, force a new deployment (command above). Same for the frontend config — redeploy the web app.</p>`],
["p3","Notifications not arriving", `<ol>
<li>DLQ depth on fairground-notifications-dead-letter-queue-prod (>0 means handler failures).</li>
<li>Handler logs: <div class="cmd">aws logs tail /aws/lambda/fairground-notifications-handler-prod --since 1h --profile production --region eu-west-2<button class="copy">copy</button></div></li>
<li>Push-only missing → Firebase creds; email-only → Resend dashboard.</li></ol>`],
["p3","SFTP delivery DLQ alarm (staging)", `<ol>
<li>Read the failed message + processor logs (2-week retention, X-Ray traces on).</li>
<li>Fix the cause, then redrive: SQS console → DLQ → "Start DLQ redrive" back to the main queue.</li></ol>`],
["p3","cdk deploy by hand — the safe way", `<p>Always <b>diff first</b>, and always pass the running image sha:</p>
<div class="cmd">cd apps/backend/infra && AWS_PROFILE=production npx cdk diff --exclusively FairgroundBackend-prod -c environment=prod -c imageTag=&lt;current-sha&gt;<button class="copy">copy</button></div>
<p>Get the current sha from the running task def's image. Staging has console-made drift (scaling policies) — expect a diff there until it's reconciled.</p>`],
];

