**This fixes a regression I introduced in #2965, found by re-running the
full dashboard lane on `main` rather than trusting the targeted runs I
did at the time.**
`AddNodeModal` and `ConnectNodeModal` are red on main:
```
→ Found multiple elements with the text of: Add Node
→ Found multiple elements with the text of: Connect to Node
```
### Cause
#2965 dropped the redundant `" dialog"` suffix from each
`FloatingWindow`'s `ariaLabel`. That was correct — `role="dialog"`
already conveys it. What I missed is that six of those modals **also**
put an `aria-label` with the *same* text on their own inner `<div>`:
```jsx
<FloatingWindow ariaLabel={t("nodes.addNode", "Add Node")} …>
<div className="modal modal-md add-node-modal" aria-label={t("nodes.addNode", "Add Node")}>
```
Before #2965 the two differed (`"Add Node dialog"` vs `"Add Node"`), so
`getByLabelText("Add Node")` matched exactly one element. Now both
match.
### Why the inner one goes, not the dialog's
Those inner labels sit on **role-less `<div>`s**, where assistive
technology ignores `aria-label` entirely — it was never conveying
anything to anyone. Removing it restores a single accessible name per
dialog and needs no test changes.
### Surface enumeration — four of the six were latent
Only two surfaced as failures; the other four have no test querying by
that name, so they would have shipped a duplicate accessible name
silently. Found by scanning every component for an inner `aria-label`
whose expression matches its own `ariaLabel` prop:
| modal | was it red? |
|---|---|
| `AddNodeModal` | red on main |
| `ConnectNodeModal` | red on main |
| `GroupTaskModal` | latent |
| `NodeDetailModal` | latent |
| `ScriptsModal` | latent |
| `WorkflowAddStepModal` | latent |
### Five more, deliberately untouched
`AgentDetailView`, `PlanningModeModal`, `SettingsModal`
(`role="region"`), `ScheduledTasksModal` (`role="listbox"`) and
`NewTaskModal` (`role="dialog"`) also carry their dialog's name on an
inner element — but those elements **have a role**, so the label is
meaningful rather than dead markup. A listbox named "Automations" inside
a dialog named "Automations" is redundant, not broken, and renaming it
is a UX decision rather than a cleanup. Left alone and recorded here.
**Verified:** 93/93 across `AddNodeModal`, `ConnectNodeModal`,
`NodesView`, `GroupTaskModal`, `ScriptsModal` and the #2965 aria guard;
`tsc -p tsconfig.app.json` 0 errors; lint clean; FNXC gate exit 0.
Product-code change to a11y markup, so this is user-visible but needs no
operator-facing note — say the word if you want a changeset.
**Measured dashboard-lane state on main before this PR:** `3 failed |
11173 passed`. Two are these; the third is
`MainContent.planning-project-remount`, which belongs to #2420 and is
detailed there.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thirteen modals set an `aria-label` that restates the role they already
carry. `FloatingWindow` renders `role="dialog"` and
`aria-label={ariaLabel}` on the **same element**
(`FloatingWindow.tsx:628` and `:631`), so `"Settings dialog"` is
announced as **"Settings dialog, dialog."**
Fixed at all 13 call sites, plus a guard so the next copy-paste fails
instead of shipping.
### Two independent lines of evidence
I found this by inspection. Then, chasing unexplained dashboard
failures, I hit `NodesView.test.tsx`:
```
Unable to find an accessible element with the role "dialog" and name "Add Node"
...
Name "Add Node dialog":
```
Two tests were already asserting the **correct** name and failing
because the product had drifted to add the suffix. So this is not a
style preference — it is a defect with pre-existing tests that were red.
**Those 2 failures go green here**, and they were among the ones I had
not yet accounted for.
### The guard was vacuous, and mutation is the only reason I know
My first version used one regex with `[^`"']*?` for the label body. It
passed. It was worthless.
Every real call site interpolates a translator call:
```jsx
ariaLabel={`${t("scripts.title", "Scripts")} dialog`}
```
Those inner **double quotes terminate the character class**, so the
pattern matched **none of the thirteen offenders**. It only matched
hand-written samples like ``{`Settings dialog`}`` that happen to contain
no quotes — which is exactly what I had put in the case table. Re-adding
the suffix to `ScriptsModal` in its original form left the suite
**green**.
Extraction is now structural (brace matching), and the case table
carries the real quote-bearing shapes, including the nested-brace
`NodeDetailModal` form and the `+ " dialog"` concatenation variant.
**Mutation now behaves:**
| state | result |
|---|---|
| clean tree | 13/13 pass |
| suffix re-added to `ScriptsModal` (faithful form) | **fails**, naming
the file and the offending value |
I would have shipped a guard that could not fail on the defect it was
written for. It is the same error the guard exists to prevent — a cheap
proxy standing in for the real measurement — so the reasoning is
recorded in the file rather than quietly fixed.
### Verified
- `NodesView` + `FloatingWindow` + the new guard: **110/110**, then
**13/13** for the guard after the rewrite
- `tsc -p tsconfig.app.json`: **0 errors** · lint clean · FNXC gate exit
0
- **No i18n key or default string changed** — all 15 `t()` keys
byte-identical across the diff; only the literal outside the call was
dropped, and the now-pointless `` {`${…}`} `` wrappers were unwrapped
### Not fixed here
`agent-modals-mobile` (2) and `core-modals-mobile` (1) still fail on
this branch — they fail identically on `main`, are CSS-structure
assertions unrelated to aria naming, and belong to the #2915
dead-`.agent-detail-overlay` family. Left alone deliberately rather than
bundled in.
No changeset: user-visible a11y correction with no API or setting
change, and the release-notes audience is operators. Say the word if you
want one.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
carry ~5,930 keys each across common/app/errors/cli namespaces;
CLI bundles regenerated (6 locales incl. ko)
Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
literal {{placeholders}}); dashboard vitest.setup boots a minimal en
i18next instance for the same reason
Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Update node management UI components to use shared form/button classes, semantic color tokens, and accessible interactions
- Refine mesh topology rendering and related dashboard tests for improved readability and behavior coverage
- Adjust discovery routes to reuse injected centralCore instances without unnecessary init/close cycles
- Add/extend tests for shared CentralCore discovery flows and model settings scope save behavior
- Convert seed-sample-nodes into a test-only helper and remove direct execution against the real central database
- Replace private-looking host examples with RFC 5737 test-net addresses in node connection docs and ConnectNodeModal expectations
- Add Vitest aliases for @fusion/core and @fusion/engine to stabilize cross-package test imports
- Load AgentReflectionService via local engine source path in in-process runtime to avoid alias resolution failures
- Export MeshTopologyProps interface for external use
- Add peer awareness lines between remote nodes for mesh visualization
- Implement dynamic SVG sizing based on number of remote nodes
- Add status color legend showing online/offline/connecting/error states
- Improve CSS variable handling with fallback values
- Use transform-based positioning for cleaner SVG structure