**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>
Merges node discovery onboarding (FN-3506) with two steps: a pre-registration discovery contract wired into the AddNodeModal UI, and full remote discovery integration with documentation. Also adds a local embedded runtime manager to the desktop app (FN-3404) and wires permanent-agent approval contex
Fusion-Task-Id: FN-3506
- Add onboarding project-mapping API contracts and dashboard UI wiring in AddNodeModal/NodesView
- Persist node-to-project mappings with rollback-safe failure handling and expanded hook/API test coverage
- Document node onboarding mapping behavior in architecture and multi-project docs
- Add tsup bundle asset gates for openclaw bridge and droid runtime, with changesets and bundle output tests
Fusion-Task-Id: FN-3505
- Add useMobileScrollLock hook using position:fixed body lock (Bootstrap/
Headless UI/Stripe pattern) to prevent iOS Safari from shifting the
document and visualViewport when an input inside a fixed-position modal
is focused. Wire into 15 input-bearing modals plus ChatView, replacing
ChatView's inline body-overflow effect.
- Widen computeBuildVersion in vite.config.ts to hash the entire app/ tree
so the version-check poll actually notices rebuilds (FN-3333 follow-up;
previously only main.tsx and package.json were hashed).
- ChatView: on input blur, suppress keyboard-aware sizing for 450ms while
reserving mobile-nav-bar space, so the composer snaps to its final
height in one move instead of crawling down with iOS's keyboard slide
and then jumping again when the nav bar reappears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add managed Docker node API client/routes, onboarding hook, and NodesView integration with new onboarding modal UX
- Refine Add Node, mobile nav, agent/node detail, and onboarding-related component styling/interaction flows
- Expand dashboard/core/cli test coverage for onboarding flows, node routes, insights persistence, and mobile regressions
- Add CLI/plugin packaging updates including bundled dependency-graph install and plugin schema initialization support
Fusion-Task-Id: FN-3108
This merge lands the managed Docker nodes feature (FN-3107) end-to-end: core schema migration for nodes with types, CRUD operations in CentralCore, and dashboard UI (AddNodeModal, NodeDetailModal) with accessibility fixes and tokenized CSS. Also included are manual PR linking (FN-3202), plugin schem
Fusion-Task-Id: FN-3107
- 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
- Add mobile responsive styles for AddNodeModal and ConnectNodeModal
- Create comprehensive AddNodeModal tests with type toggle, validation, and transitions
- Improve CSS consistency across node modals with proper spacing and transitions
- Refactor AddNodeModal TSX markup with clean structure and type toggle
- Remove duplicate CSS block in ConnectNodeModal
- Add markOnboardingCompleted() to set completedAt timestamp when user finishes onboarding
- Add isOnboardingCompleted() to check if onboarding was completed (vs dismissed)
- Update ModelOnboardingModal to call markOnboardingCompleted on successful completion
- Skip onboarding auto-open when already completed locally
- Add completion state tracking tests in ModelOnboardingModal.test.tsx
- Add model-onboarding-state unit tests
- Update useAuthOnboarding to check local completion state before auto-opening
- Document localStorage completion state tracking pattern in .fusion/memory.md
- Add node API typings and client methods plus a polling useNodes hook with visibility-triggered refresh
- Introduce NodesView with NodeCard, AddNodeModal, and NodeDetailModal to register, inspect, update, health-check, and remove nodes
- Wire the Nodes surface into the app shell/header and add project node assignment UI with project card node badges
- Add unit coverage for useNodes, NodesView, NodeCard, Header, and ProjectCard behavior, plus node-specific dashboard styling