Skip to content

Commit 6ea5aa0

Browse files
thecraftmanclaudetsenartampcode-com
authored
fix(building-dashboards): validate chart IDs to prevent corrupted (#31)
* fix(building-dashboards): validate chart IDs to prevent corrupted dashboards Charts missing `id` fields or with mismatched layout `i` references cause dashboards to enter a permanently corrupted UI state (blank, unable to save/revert, stuck edit panels). This was reported by a customer using Claude Code with the build-dashboards skill. Changes: - dashboard-validate: add check for charts missing `id` field - dashboard-validate: promote chart/layout ID mismatch from warning to error - dashboard-create: run validation before deploying - dashboard-update: run validation before deploying - SKILL.md: add explicit "Required Chart Structure" section documenting that every chart must have a unique `id` and every layout `i` must match Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback — validate before jq, detect duplicate IDs - Move dashboard-validate call before BODY=$(jq ...) in both dashboard-create and dashboard-update so malformed JSON gets the validator's friendly error instead of a raw jq crash - Add duplicate chart ID detection (check #6) — two charts sharing the same id with matching layout entries previously passed silently Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(building-dashboards): keep deploy script stdout machine-readable Amp-Thread-ID: https://ampcode.com/threads/T-019c9eee-3c59-721b-82d3-fcb245ed29ef Co-authored-by: Amp <amp@ampcode.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tomás Senart <tsenart@gmail.com> Co-authored-by: Amp <amp@ampcode.com>
1 parent fad9f1b commit 6ea5aa0

File tree

5 files changed

+168
-18
lines changed

5 files changed

+168
-18
lines changed

skills/building-dashboards/SKILL.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ The console uses `react-grid-layout` which requires `minH`, `minW`, `moved`, and
100100

101101
---
102102

103+
## Required Chart Structure
104+
105+
**Every chart MUST have a unique `id` field.** Every layout entry's `i` field MUST reference a chart `id`. Missing or mismatched IDs will corrupt the dashboard in the UI (blank state, unable to save/revert).
106+
107+
```json
108+
{
109+
"charts": [
110+
{
111+
"id": "error-rate",
112+
"name": "Error Rate",
113+
"type": "Statistic",
114+
"query": { "apl": "..." }
115+
}
116+
],
117+
"layout": [
118+
{"i": "error-rate", "x": 0, "y": 0, "w": 3, "h": 2}
119+
]
120+
}
121+
```
122+
123+
Use descriptive kebab-case IDs (e.g. `error-rate`, `p95-latency`, `traffic-rps`). The `dashboard-validate` and deploy scripts enforce this automatically.
124+
125+
---
126+
103127
## Chart Types
104128

105129
**Note:** Dashboard queries inherit time from the UI picker—no explicit `_time` filter needed.

skills/building-dashboards/scripts/dashboard-create

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,19 @@ if [[ ! -f "$JSON_FILE" ]]; then
2727
exit 1
2828
fi
2929

30+
# Validate dashboard structure before deploying
31+
if ! "$SCRIPT_DIR/dashboard-validate" "$JSON_FILE" --strict >&2; then
32+
echo "Error: Dashboard validation failed. Fix the errors above before deploying." >&2
33+
exit 1
34+
fi
35+
3036
# Read, strip server-managed fields, and normalize layout for react-grid-layout
3137
BODY=$(jq -L "$SCRIPT_DIR" '
3238
include "dashboard-normalize";
3339
del(.id, .version, .createdAt, .updatedAt, .createdBy, .updatedBy) |
3440
normalize_dashboard_layout
3541
' "$JSON_FILE")
3642

37-
# Validate required fields
38-
for field in name owner; do
39-
if [[ $(echo "$BODY" | jq -r ".$field // empty") == "" ]]; then
40-
echo "Error: Missing required field: $field" >&2
41-
exit 1
42-
fi
43-
done
44-
4543
RESPONSE=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" POST "/internal/dashboards" "$BODY")
4644

4745
# Extract and print the new dashboard ID

skills/building-dashboards/scripts/dashboard-update

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ if [[ ! -f "$JSON_FILE" ]]; then
2929
exit 1
3030
fi
3131

32+
# Validate dashboard structure before deploying
33+
if ! "$SCRIPT_DIR/dashboard-validate" "$JSON_FILE" --strict >&2; then
34+
echo "Error: Dashboard validation failed. Fix the errors above before deploying." >&2
35+
exit 1
36+
fi
37+
3238
# Normalize layout for react-grid-layout
3339
BODY=$(jq -L "$SCRIPT_DIR" '
3440
include "dashboard-normalize";

skills/building-dashboards/scripts/dashboard-validate

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
# Checks:
77
# 1. Valid JSON structure
88
# 2. Required fields present (name, owner, charts, layout)
9-
# 3. Chart IDs match layout IDs
10-
# 4. LogStream queries have take limits
11-
# 5. Grid layout doesn't exceed 12 columns
9+
# 3. All charts have an id field
10+
# 4. No duplicate chart IDs
11+
# 5. Chart IDs match layout IDs
12+
# 6. LogStream queries have take limits
13+
# 7. Grid layout doesn't exceed 12 columns
1214
#
1315
# Options:
1416
# --strict Treat warnings as errors
@@ -84,37 +86,54 @@ if [[ $(jq -r '.layout // "null"' "$FILE") == "null" ]]; then
8486
error "Missing layout array"
8587
fi
8688

87-
# 5. Check chart IDs match layout IDs
88-
chart_ids=$(jq -r '.charts[].id' "$FILE" 2>/dev/null | sort)
89+
# 5. Check all charts have an id field
90+
missing_ids=$(jq -r '.charts | to_entries[] | select(.value.id == null or .value.id == "") | .key' "$FILE" 2>/dev/null || true)
91+
if [[ -n "$missing_ids" ]]; then
92+
for idx in $missing_ids; do
93+
chart_name=$(jq -r ".charts[$idx].name // \"(unnamed)\"" "$FILE")
94+
error "Chart at index $idx ($chart_name) is missing required 'id' field"
95+
done
96+
fi
97+
98+
# 6. Check for duplicate chart IDs
99+
duplicate_ids=$(jq -r '[.charts[].id // empty] | group_by(.) | map(select(length > 1) | .[0]) | .[]' "$FILE" 2>/dev/null | sort -u || true)
100+
if [[ -n "$duplicate_ids" ]]; then
101+
for dup_id in $duplicate_ids; do
102+
error "Duplicate chart id: '$dup_id'"
103+
done
104+
fi
105+
106+
# 7. Check chart IDs match layout IDs
107+
chart_ids=$(jq -r '.charts[].id // empty' "$FILE" 2>/dev/null | sort)
89108
layout_ids=$(jq -r '.layout[].i' "$FILE" 2>/dev/null | sort)
90109

91110
if [[ "$chart_ids" != "$layout_ids" ]]; then
92-
warn "Chart IDs and layout IDs don't match"
111+
error "Chart IDs and layout IDs don't match"
93112
echo " Charts: $(echo $chart_ids | tr '\n' ' ')"
94113
echo " Layout: $(echo $layout_ids | tr '\n' ' ')"
95114
fi
96115

97-
# 6. Check LogStream queries for take limits
116+
# 8. Check LogStream queries for take limits
98117
logstream_without_take=$(jq -r '.charts[] | select(.type == "LogStream") | select(.query.apl != null) | select(.query.apl | test("take ") | not) | .id' "$FILE" 2>/dev/null || true)
99118
if [[ -n "$logstream_without_take" ]]; then
100119
for id in $logstream_without_take; do
101120
warn "LogStream '$id' may be missing 'take N' limit"
102121
done
103122
fi
104123

105-
# 7. Check grid width (should be 12 columns max)
124+
# 9. Check grid width (should be 12 columns max)
106125
max_right=$(jq '[.layout[] | (.x + .w)] | max // 0' "$FILE" 2>/dev/null || echo 0)
107126
if [[ "$max_right" -gt 12 ]]; then
108127
warn "Layout exceeds 12-column grid (max right edge = $max_right)"
109128
fi
110129

111-
# 8. Check schemaVersion
130+
# 10. Check schemaVersion
112131
schema_version=$(jq -r '.schemaVersion // 0' "$FILE")
113132
if [[ "$schema_version" != "2" ]]; then
114133
warn "schemaVersion is $schema_version, expected 2"
115134
fi
116135

117-
# 9. Check layout entries have minH/minW
136+
# 11. Check layout entries have minH/minW
118137
missing_min=$(jq -r '.layout[] | select(.minH == null or .minW == null) | .i' "$FILE" 2>/dev/null || true)
119138
if [[ -n "$missing_min" ]]; then
120139
info "$(echo "$missing_min" | wc -l | tr -d ' ') layout entries missing minH/minW (will be auto-fixed on deploy)"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env bash
2+
# test-script-output.sh: Ensure deployment scripts keep machine-readable stdout
3+
#
4+
# Usage: ./test-script-output.sh
5+
6+
set -euo pipefail
7+
8+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
SCRIPTS_DIR="$SCRIPT_DIR/../scripts"
10+
11+
passed=0
12+
failed=0
13+
14+
ok() {
15+
((passed++)) || true
16+
echo "$1"
17+
}
18+
19+
fail() {
20+
((failed++)) || true
21+
echo "$1: $2"
22+
}
23+
24+
TMPDIR="$(mktemp -d)"
25+
trap 'rm -rf "$TMPDIR"' EXIT
26+
27+
cp "$SCRIPTS_DIR/dashboard-create" "$TMPDIR/"
28+
cp "$SCRIPTS_DIR/dashboard-update" "$TMPDIR/"
29+
cp "$SCRIPTS_DIR/dashboard-validate" "$TMPDIR/"
30+
cp "$SCRIPTS_DIR/dashboard-normalize.jq" "$TMPDIR/"
31+
32+
chmod +x "$TMPDIR/dashboard-create" "$TMPDIR/dashboard-update" "$TMPDIR/dashboard-validate"
33+
34+
cat > "$TMPDIR/input.json" <<'JSON'
35+
{
36+
"id": "dashboard-root-id",
37+
"version": "v1",
38+
"createdAt": "2026-02-01T10:00:00Z",
39+
"updatedAt": "2026-02-02T11:00:00Z",
40+
"createdBy": "alice@example.com",
41+
"updatedBy": "bob@example.com",
42+
"schemaVersion": 2,
43+
"name": "Test Dashboard",
44+
"description": "Test",
45+
"owner": "user-123",
46+
"charts": [
47+
{
48+
"id": "error-rate",
49+
"name": "Error Rate",
50+
"type": "Statistic",
51+
"query": { "apl": "['logs'] | summarize c=count()" }
52+
}
53+
],
54+
"layout": [
55+
{ "i": "error-rate", "x": 0, "y": 0, "w": 3, "h": 2 }
56+
]
57+
}
58+
JSON
59+
60+
cat > "$TMPDIR/axiom-api" <<'BASH'
61+
#!/usr/bin/env bash
62+
set -euo pipefail
63+
METHOD="${2:-}"
64+
PATH_="${3:-}"
65+
66+
case "$METHOD:$PATH_" in
67+
"POST:/internal/dashboards")
68+
echo '{"id":"created-id"}'
69+
;;
70+
"PUT:/internal/dashboards/dashboard-root-id")
71+
echo '{"id":"dashboard-root-id","updated":true}'
72+
;;
73+
*)
74+
echo "Unexpected call: $METHOD $PATH_" >&2
75+
exit 1
76+
;;
77+
esac
78+
BASH
79+
80+
chmod +x "$TMPDIR/axiom-api"
81+
82+
echo "Script Stdout Contract"
83+
echo "======================"
84+
85+
create_out=$("$TMPDIR/dashboard-create" prod "$TMPDIR/input.json")
86+
if [[ "$create_out" == "created-id" ]]; then
87+
ok "dashboard-create outputs only dashboard ID"
88+
else
89+
fail "dashboard-create outputs only dashboard ID" "got: $create_out"
90+
fi
91+
92+
update_out=$("$TMPDIR/dashboard-update" prod dashboard-root-id "$TMPDIR/input.json")
93+
if echo "$update_out" | jq -e '.id == "dashboard-root-id" and .updated == true' > /dev/null 2>&1; then
94+
ok "dashboard-update outputs valid JSON only"
95+
else
96+
fail "dashboard-update outputs valid JSON only" "got: $update_out"
97+
fi
98+
99+
echo ""
100+
echo "======================"
101+
echo "Passed: $passed | Failed: $failed"
102+
103+
[[ $failed -eq 0 ]]

0 commit comments

Comments
 (0)