server: fix mesh_peer_boxes' stats prefix to match where PutStats writes (#842)
Found by re-verifying the exact S3 key path before flipping the mesh on for real: mesh_peer_boxes listed "s3://bucket/<prefix>-stats/", but PutStats (internal/activator/heartbeat.go's statsKey) actually writes to "s3://bucket/<prefix>/mesh/members-stats/<peer>" — hc.meshPrefix() (activator.go) is "<prefix>/mesh/members", and statsKey appends "-stats" to that WHOLE path, not to the bare hibernation prefix. The mismatch was invisible in every test so far because both the correct and wrong prefix are empty today (the mesh has never been on, so nothing has ever published to either) — a passing "returns []" result proved nothing about which path was actually being read. Verified properly this time: uploaded a synthetic stats object to the real path Go would use, using the shared activator-prod credentials, then confirmed the fixed function finds and correctly parses it using the status-publisher's actual restricted (read-only, *-stats/* scoped) credentials — the exact ones it runs with in production. Cleaned up the test object afterward. Dry-run fixtures regenerate identical to committed (this code path never runs in dry-run mode). The Terraform grant from #822 needs no change: its "*-stats/*" StringLike pattern already matches the real path correctly (it never assumed a specific prefix depth).
Co-authored-by: Claude <noreply@anthropic.com>
1 file changed,
+11
−3
deploy/status-publish.sh+11 −3
diff --git a/deploy/status-publish.sh b/deploy/status-publish.sh| index 1d7d78e..aceeafe 100755 |
| --- a/deploy/status-publish.sh |
| +++ b/deploy/status-publish.sh |
| @@ -358,12 +358,20 @@ mesh_peer_boxes() { |
| return |
| fi |
| - local bucket region prefix |
| + local bucket region prefix mesh_prefix |
| bucket=$(env_str "$ROUTER_ENV" LCHAT_HIBERNATE_BUCKET) |
| prefix=$(env_str "$ROUTER_ENV" LCHAT_HIBERNATE_PREFIX) |
| if [ -z "$bucket" ] || [ -z "$prefix" ]; then echo '[]'; return; fi |
| region=$(env_str "$ROUTER_ENV" LCHAT_HIBERNATE_REGION) |
| region=${region:-eu-west-1} |
| + # Must match hc.meshPrefix() (activator.go) exactly — the membership |
| + # heartbeat lives at "<prefix>/mesh/members/<peer>", and statsKey |
| + # (internal/activator/heartbeat.go) appends "-stats" to that WHOLE path, |
| + # not to the bare hibernation prefix: "<prefix>/mesh/members-stats/<peer>". |
| + # (Caught 2026-07-13 before the flip: an earlier version of this function |
| + # listed "<prefix>-stats/" instead — empty either way today since nothing |
| + # has ever published there, so it silently proved nothing.) |
| + mesh_prefix="${prefix}/mesh/members" |
| # `aws s3 ls` (the high-level command), never `aws s3api list-objects*` — |
| # the latter crashes on this fleet's aws-cli/Python combination (a Python |
| @@ -375,7 +383,7 @@ mesh_peer_boxes() { |
| # already relies on). `s3 ls` hits the same ListObjectsV2 API underneath; |
| # its plain-text output already has everything needed (date, time, key). |
| local listing |
| - listing=$(aws s3 ls "s3://$bucket/${prefix}-stats/" --region "$region" 2>/dev/null) || true |
| + listing=$(aws s3 ls "s3://$bucket/${mesh_prefix}-stats/" --region "$region" 2>/dev/null) || true |
| [ -n "$listing" ] || { echo '[]'; return; } |
| # A plain string compare against the cutoff is exact as long as both sides |
| @@ -394,7 +402,7 @@ mesh_peer_boxes() { |
| [ -n "$key" ] || continue |
| [ "$date_part $time_part" '>' "$cutoff" ] || continue # stale — treat like a dead mesh member |
| tmp="$TMP/mesh-stat-$(printf '%s' "$key" | tr '/' '_').json" |
| - aws s3 cp "s3://$bucket/${prefix}-stats/$key" "$tmp" --region "$region" >/dev/null 2>&1 || continue |
| + aws s3 cp "s3://$bucket/${mesh_prefix}-stats/$key" "$tmp" --region "$region" >/dev/null 2>&1 || continue |
| body=$(cat "$tmp" 2>/dev/null) || continue |
| [ -n "$body" ] || continue |
| peers=$(sqj "SELECT json_insert( |