Skip to content

Commit 6191b95

Browse files
authored
Merge pull request #122 from ProxySQL/codex/ui-restorative-topology-worktree
Modernize Orchestrator web UI and audit workflows
2 parents bc325e1 + 64b6495 commit 6191b95

80 files changed

Lines changed: 9208 additions & 1087 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ main
2323
/bin
2424
/build
2525
.claude/worktrees/
26+
.worktrees/
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Failure Analysis Workspace Final Fix Report
2+
3+
Date: 2026-08-12
4+
5+
## Scope and commit
6+
7+
- Scope: the complete final-review fix wave for the Failure Analysis Workspace.
8+
- Commit: `fix(ui): complete failure analysis final review` (this report is included in that single commit; the final hash is reported in the handoff).
9+
- Production APIs, endpoints, refresh behavior, and visual design were unchanged.
10+
11+
## Findings and TDD evidence
12+
13+
### IMPORTANT: unmatched display-relevant analysis
14+
15+
Root cause: `appendEntry` returned when `ClusterDetails.ClusterName` was absent from the `clusters-info` index, but the model did not record that discarded actionable or structural entry. The adapter consequently rendered a healthy empty or incomplete state.
16+
17+
RED command:
18+
19+
```text
20+
node --test go/http/testdata/clusters_analysis_state_test.js
21+
```
22+
23+
Test-only run outcome: 10 passed, 3 failed. Relevant failures were:
24+
25+
- `incident model tracks an unmatched structural entry`: expected `unmatchedEntryCount === 1`, received `undefined`.
26+
- `document adapter renders unavailable state when an actionable analysis has no matching cluster`: expected `Analysis unavailable`, received `0 active incidents across 0 clusters`.
27+
28+
GREEN implementation:
29+
30+
- `buildClustersAnalysisModel` increments `unmatchedEntryCount` only when an actionable or structural entry reaches `appendEntry` and lacks its cluster.
31+
- The adapter renders the unavailable state before topology URL adjustment or incident summary rendering whenever that count is nonzero.
32+
- Separate regressions cover an actionable `DeadMaster` with `clusters=[]` at the adapter boundary and a structural-only unmatched entry at the model boundary.
33+
34+
GREEN outcome: the focused JavaScript suite passed 13/13.
35+
36+
### MINOR 1: accurate test title and explicit actionable derivation
37+
38+
The mixed model test was renamed from claiming actionable and downtimed derivation to the behavior it actually asserts: blocked and structural entries. A focused test now asserts the complete literal actionable entry model.
39+
40+
Because actionable derivation was already correct, the new characterization test was mutation-checked rather than represented as a naturally failing baseline.
41+
42+
Mutation RED command:
43+
44+
```text
45+
node --test --test-name-pattern='incident model derives an actionable entry' go/http/testdata/clusters_analysis_state_test.js
46+
```
47+
48+
Outcome after temporarily changing the production actionable status label: 0 passed, 1 failed, with literal `statusLabel` mismatch (`Action required` versus `Requires attention`). The mutation was reverted.
49+
50+
Restored GREEN command: the same command passed 1/1.
51+
52+
### MINOR 2: deterministic analysis-entry ordering
53+
54+
Root cause: entries were appended in replication-analysis API order and only clusters were sorted.
55+
56+
RED command:
57+
58+
```text
59+
node --test go/http/testdata/clusters_analysis_state_test.js
60+
```
61+
62+
Test-only run outcome: `incident model sorts entries by state, instance, and analysis` failed with the reversed API order intact.
63+
64+
GREEN implementation: each cluster's entries are sorted by state precedence (`blocked`, `actionable`, `warning`, `downtimed`), then instance, then analysis, before cluster state derivation. The regression uses reversed mixed-state input and a hand-written literal expected order, including an analysis tie-break for the same instance.
65+
66+
GREEN outcome: the focused JavaScript suite passed 13/13.
67+
68+
### MINOR 3: complete workspace CSS selector scoping
69+
70+
Root cause: the stylesheet guard rejected only newline-prefixed `.popover` and `.container` strings and did not validate arbitrary rule selectors.
71+
72+
RED command:
73+
74+
```text
75+
go test ./go/http -run 'TestClustersAnalysisWorkspaceStylesAreScoped|TestUnscopedWorkspaceCSSSelectorsRejectsArbitraryGlobalRule' -count=1
76+
```
77+
78+
Test-only run outcome: build failed because the new all-selector validator did not exist.
79+
80+
GREEN implementation:
81+
82+
- `TestClustersAnalysisWorkspaceStylesAreScoped` now runs all selectors returned by the existing recursive `workspaceCSSSelectors` parser through the existing workspace-ID selector validator.
83+
- A focused real-parser regression includes `.unexpected-global` inside a media rule and asserts that it is rejected; no mock is used.
84+
85+
GREEN outcome: the focused Go test command passed.
86+
87+
## Files changed
88+
89+
- `resources/public/js/clusters-analysis.js`
90+
- `go/http/testdata/clusters_analysis_state_test.js`
91+
- `go/http/static_assets_test.go`
92+
- `.superpowers/sdd/2026-08-12-failure-analysis-workspace/final-fix-report.md`
93+
94+
## Full verification
95+
96+
Command:
97+
98+
```text
99+
node --test go/http/testdata/*.js && \
100+
node --check resources/public/js/clusters-analysis.js && \
101+
gofmt -w go/http/static_assets_test.go && \
102+
go test ./go/http -count=1 && \
103+
bash tests/functional/test-smoke.sh && \
104+
git diff --check
105+
```
106+
107+
Outcome: exit 0.
108+
109+
- Node behavior tests: 20 passed, 0 failed.
110+
- JavaScript syntax check: passed.
111+
- Go HTTP package: passed.
112+
- Functional smoke: 32 passed, 0 failed, 0 skipped.
113+
- Formatting: `gofmt` applied to the changed Go test.
114+
- Diff whitespace check: passed.
115+
- Existing healthy lab was used; no containers were recreated or restarted.
116+
117+
## Self-review
118+
119+
- Confirmed only display-relevant actionable and structural entries contribute to the unmatched count; non-interesting non-structural analysis remains ignored as before.
120+
- Confirmed any unmatched count forces unavailable rendering, preventing a partial incident list as well as a false healthy empty state.
121+
- Confirmed sorting is independent of API order and uses explicit state precedence followed by lexical instance and analysis keys.
122+
- Confirmed the CSS guard recursively checks selectors inside media rules and reports every unscoped selector.
123+
- Confirmed no production changes were made outside the JavaScript model/adapter and no CSS was altered.
124+
- Confirmed the final diff contains no unrelated workspace changes.
125+
126+
## Concerns
127+
128+
None. The lab remained healthy throughout verification.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Final safety-fix report
2+
3+
Date: 2026-08-12 (Asia/Bangkok)
4+
5+
## Outcome
6+
7+
All three Important safety findings are addressed in one focused change set.
8+
No live failover was run and no MySQL container was started, stopped, or
9+
recreated during this correction.
10+
11+
## TDD evidence
12+
13+
Initial RED command:
14+
15+
```text
16+
go test ./go/http -run 'Test(AuditFailoverHarnessSafetyContracts|SmokeEndsOnlyMaintenanceCreatedByItsBeginCall|MaintenanceBegunResponseReturnsCreatedMaintenanceKey)$' -count=1
17+
```
18+
19+
It failed at compile time with:
20+
21+
```text
22+
go/http/api_test.go:60:14: undefined: maintenanceBegunResponse
23+
```
24+
25+
After the minimal handler response helper was introduced, the same command
26+
failed on the shell regressions: missing `deadline=$((SECONDS + 90))`, curl
27+
max-time two, deadline loop, cleanup early-return, and keyed end-maintenance;
28+
it also detected `start mysql2 mysql3` and instance-based maintenance cleanup.
29+
30+
A focused boundary RED then failed because the deadline loop did not budget its
31+
final curl against the remaining seconds. That test named the missing
32+
`remaining=$((deadline - SECONDS))` and reduced curl argument.
33+
34+
Final focused GREEN:
35+
36+
```text
37+
ok github.com/proxysql/orchestrator/go/http
38+
```
39+
40+
## Implemented contracts
41+
42+
1. Recovery polling is bounded by an actual 90-second wall-clock deadline.
43+
Every curl has a two-second maximum, reduced to the remaining deadline
44+
budget when necessary, and success/failure output uses actual elapsed time.
45+
2. `restore_lab` is a true no-op while `MYSQL1_STOPPED=false`. Once mysql1 was
46+
stopped, cleanup starts mysql1 only. It never starts mysql2/mysql3; replica
47+
repair uses `docker compose exec` and therefore operates only on replicas
48+
that are already running.
49+
3. BeginMaintenance success preserves the historical `Details.Hostname` and
50+
`Details.Port` fields and adds its new maintenance ID as
51+
`Details.MaintenanceKey`, while preserving `Code: OK` and the existing
52+
Message. Smoke validates the direct response's status, code, exact instance
53+
message, instance details, and positive integer key, then calls only
54+
`/api/end-maintenance/$MAINTENANCE_KEY`. Failed or unrelated responses cause
55+
no cleanup call.
56+
57+
## Additive API compatibility correction
58+
59+
The initial safety correction represented `Details` as the maintenance-key
60+
number, which regressed the successful BeginMaintenance response contract for
61+
clients that read `Details.Hostname` and `Details.Port`. A focused TDD test
62+
against that implementation failed with:
63+
64+
```text
65+
json: cannot unmarshal number into Go struct field .Details of type struct { Hostname string; Port int; MaintenanceKey int64 }
66+
```
67+
68+
The response now embeds the original `inst.InstanceKey` fields in its details
69+
object and exposes `MaintenanceKey` additively. Failure responses were not
70+
changed.
71+
72+
## Verification
73+
74+
- `go test ./go/http -count=1`: pass.
75+
- Node UI state tests: 23/23 pass across four files.
76+
- `bash -n tests/functional/test-audit-ui-failover.sh tests/functional/test-smoke.sh`: pass.
77+
- `bash tests/functional/test-smoke.sh`: 35 passed, 0 failed, 0 skipped;
78+
begin returned `Details.MaintenanceKey` 1 alongside `Hostname`/`Port`, and
79+
cleanup ended that exact key.
80+
- `git diff --check`: pass.
81+
- Live failover: intentionally not run.
82+
83+
Only Orchestrator was recreated for smoke; a before/after comparison confirmed
84+
all three MySQL container IDs were unchanged. An initial run failed at the
85+
readiness gate because the mounted binary was Darwin rather than Linux; no
86+
maintenance began. Rebuilding with the existing Linux/arm64 Go image resolved
87+
the environment mismatch, after which smoke passed.
88+
89+
## Concerns
90+
91+
None for the three corrected findings. Recreating Orchestrator resets the
92+
functional SQLite audit database by design, so historical live failover rows
93+
from the earlier review are no longer resident; their captured evidence remains
94+
in `final-report.md`. No new failover was run.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Final verification: populated audit history
2+
3+
## Scope and commits
4+
5+
This handoff verifies the recovered, restored functional lab and the populated
6+
audit-history UI evidence produced by this work.
7+
8+
Commits created before this handoff:
9+
10+
- `5556dbe9 test(ui): persist audit history in functional lab`
11+
- `52bbe45f test(ui): verify functional audit persistence`
12+
- `e722ba7c test(ui): exercise populated audit history`
13+
14+
Initial report commit: `d3c81f67 docs(ui): record populated audit verification`.
15+
16+
No production UI correction was required after browser review.
17+
18+
## Recovery and audit evidence
19+
20+
The controlled failure produced successful `DeadMaster` recovery records for
21+
`mysql1:3306`; the recorded successor was `mysql2:3306`. Two recovery records
22+
are present, each records `IsSuccessful: true`, `AnalysisEntry.Analysis:
23+
DeadMaster`, and the successor `mysql2:3306` (the most recent is ID 2).
24+
25+
Fresh API counts from 2026-08-12 14:54 ICT:
26+
27+
| Endpoint | Records |
28+
| --- | ---: |
29+
| `/api/audit/0` | 20 |
30+
| `/api/audit-failure-detection/0` | 2 |
31+
| `/api/audit-recovery/0` | 2 |
32+
33+
Both detection records and both recovery records represent `DeadMaster` for
34+
`mysql1:3306`; the recovery records are successful with `mysql2:3306` as
35+
successor.
36+
37+
## Restored topology and identity
38+
39+
Fresh container inspection retained the IDs captured by the recovery harness:
40+
41+
| Service | Container ID | State | Role / replication |
42+
| --- | --- | --- | --- |
43+
| mysql1 | `76e92eb4a8be` | healthy | `read_only=0` |
44+
| mysql2 | `ca05b9577b38` | healthy | source `mysql1`; IO `Yes`; SQL `Yes` |
45+
| mysql3 | `cf2ffd96825e` | healthy | source `mysql1`; IO `Yes`; SQL `Yes` |
46+
47+
This matches the pre-restoration identity record: no MySQL container was
48+
recreated. `SHOW REPLICA STATUS\\G` for mysql2 and mysql3 also reported zero
49+
last IO and SQL errors and zero seconds behind source.
50+
51+
## Automated verification
52+
53+
All prescribed commands were run fresh and exited zero:
54+
55+
| Command / suite | Result |
56+
| --- | --- |
57+
| `go test ./go/http -count=1` | 1 package passed; fresh JSON run counted 77 passing Go tests |
58+
| `for file in go/http/testdata/*_test.js; do node --test "$file" \|\| exit 1; done` | 4 Node test files; 23/23 tests passed |
59+
| `node --check resources/public/js/*.js` | 30/30 JavaScript files parsed successfully |
60+
| `bash tests/functional/test-smoke.sh` | 35 passed, 0 failed, 0 skipped |
61+
| `git diff --check` | no whitespace errors |
62+
63+
The smoke run rediscovered all three instances and passed its audit-persistence,
64+
web/API, health, metrics, and ProxySQL checks.
65+
66+
## Commit hygiene
67+
68+
After the initial report commit `d3c81f67`, `git status --short` produced no
69+
output. The tracked worktree was clean; this report was the only file staged
70+
and committed for that handoff.
71+
72+
## Browser evidence
73+
74+
Task 3 inspected the populated application at the default desktop viewport and
75+
again at 390x844. At both sizes:
76+
77+
- `/web/audit` displayed its populated rows and correct pager states.
78+
- `/web/audit-failure-detection` displayed two `DeadMaster` detections; the
79+
expanded detection showed the two replicas, changelog, processing node, and
80+
its recovery link.
81+
- `/web/audit-recovery` displayed two `DeadMaster` recoveries and working UID
82+
detail links.
83+
- `/web/audit-recovery/id/2` displayed failed `mysql1:3306`, successor
84+
`mysql2:3306`, timing and acknowledgement data, affected replicas, and all
85+
26 recovery steps. Its related-detection link also rendered the corresponding
86+
detail.
87+
88+
At 390px, the table/detail shells scrolled internally without document-level
89+
horizontal overflow; empty and unavailable states stayed hidden while populated
90+
content was shown. Browser console inspection found **0 errors and 0 warnings**
91+
at both viewport sizes.
92+
93+
## Safety and unresolved concerns
94+
95+
The final state has the original mysql1 writer and two healthy replicas sourced
96+
from mysql1. The recovery workflow restored this topology without recreating
97+
containers, deleting volumes, or discarding SQLite history.
98+
99+
Unresolved concerns: **none**. Docker Compose emitted its pre-existing
100+
obsolete-top-level-`version` notice and the MySQL client emitted its standard
101+
password-on-command-line warning during the earlier live verification.
102+
103+
## Final safety corrections (2026-08-12)
104+
105+
Three Important review findings were corrected without running another live
106+
failover:
107+
108+
- The recovery poll now uses a `SECONDS + 90` wall-clock deadline, limits each
109+
curl to at most two seconds (and to the remaining deadline budget near the
110+
boundary), and reports actual elapsed seconds.
111+
- `restore_lab` returns immediately unless this harness stopped mysql1. During
112+
restoration it starts only mysql1; mysql2/mysql3 are never started, and
113+
replication repair is attempted only through `exec` against their existing
114+
running containers.
115+
- successful `begin-maintenance` responses retain the existing Code, Message,
116+
`Details.Hostname`, and `Details.Port` fields while adding the created key as
117+
`Details.MaintenanceKey`. The smoke test accepts only the direct successful
118+
response for mysql2, extracts its positive integer key, and ends maintenance
119+
only through `/api/end-maintenance/{key}`.
120+
121+
Strict RED evidence was captured before each correction. The handler contract
122+
first failed to build with `undefined: maintenanceBegunResponse`. After the
123+
minimal API response change exposed the shell regressions, the focused test
124+
reported all missing deadline/no-op/keyed-cleanup contracts and detected both
125+
unsafe instance cleanup branches. A second deadline-boundary RED reported the
126+
missing remaining-budget calculation before that behavior was added.
127+
128+
A final scoped review found that the first key-returning response had replaced
129+
the historical instance details with a number. The additive compatibility test
130+
failed against that version because numeric Details could not decode into
131+
`Hostname`, `Port`, and `MaintenanceKey`. The corrected response preserves the
132+
two historical fields and adds the key; the smoke consumer now verifies all
133+
three before cleanup. Failure responses remain unchanged.
134+
135+
Fresh GREEN verification:
136+
137+
| Command | Result |
138+
| --- | --- |
139+
| focused three-regression `go test` | pass |
140+
| `go test ./go/http -count=1` | pass |
141+
| four `go/http/testdata/*_test.js` files | 23/23 pass |
142+
| `bash -n` on both changed functional scripts | pass |
143+
| `bash tests/functional/test-smoke.sh` | 35 passed, 0 failed, 0 skipped |
144+
| `git diff --check` | pass |
145+
146+
The test binary was rebuilt for the lab's Linux/arm64 platform and only the
147+
Orchestrator service was recreated. The smoke test received
148+
`Details.MaintenanceKey` 1 alongside the historical instance fields, ended
149+
exactly that key, and passed 35/35 checks. MySQL container ID comparison before
150+
and after had no diff. The first smoke attempt failed safely at readiness
151+
because a host Darwin binary had been mounted into the Linux container; no
152+
maintenance call occurred. Rebuilding in the existing `golang:1.25.7` Linux
153+
image corrected that environment mismatch. No live failover was run.
154+
command-line-password warning during topology inspection; neither is an
155+
application/browser-console warning or a verification failure.

0 commit comments

Comments
 (0)