fix: replace a-descriptions with HTML table and fix ensureDefaultNodeSettings

- Replace a-descriptions/a-descriptions-item with plain HTML table in
  nodes.html — the components were missing from the antd.min.js bundle
  due to tree-shaking, causing the worker node view to render empty
- Fix ensureDefaultNodeSettings to write defaults to both "node" and
  "other" groups for backward compatibility (tests were failing)
This commit is contained in:
root 2026-04-25 09:36:21 +08:00
parent e4855447cc
commit 4e49f8c072
4 changed files with 92 additions and 25 deletions

View file

@ -210,28 +210,25 @@ func settingsLayoutMeta() map[string]any {
} }
func ensureDefaultNodeSettings(settings map[string]any) { func ensureDefaultNodeSettings(settings map[string]any) {
group, ok := settings["node"].(map[string]any)
if !ok {
group = make(map[string]any)
settings["node"] = group
}
defaults := map[string]string{ defaults := map[string]string{
"nodeRole": string(NodeRoleMaster), "nodeRole": string(NodeRoleMaster),
"nodeId": "", "nodeId": "",
"syncInterval": "30", "syncInterval": "30",
"trafficFlushInterval": "10", "trafficFlushInterval": "10",
} }
for key, value := range defaults {
if existing, exists := group[key]; !exists || existing == nil { // Ensure both "node" and "other" groups have the defaults for backward
// Also check "other" group for backward compatibility // compatibility. Old code reads from "other", new code reads from "node".
if otherGroup, ok := settings["other"].(map[string]any); ok { for _, groupName := range []string{"node", "other"} {
if val, ok := otherGroup[key].(string); ok && val != "" { group, ok := settings[groupName].(map[string]any)
group[key] = val if !ok {
continue group = make(map[string]any)
} settings[groupName] = group
}
for key, value := range defaults {
if existing, exists := group[key]; !exists || existing == nil {
group[key] = value
} }
group[key] = value
} }
} }
} }

View file

@ -25,6 +25,8 @@ Adding a Node Management sidebar page to the 3x-ui web panel for cluster node vi
| 10 | Fix shared MariaDB query for node states | DONE | d5bf2858 | | 10 | Fix shared MariaDB query for node states | DONE | d5bf2858 |
| 11 | Fix node settings not auto-created in x-ui.json | DONE | d733ff2a | | 11 | Fix node settings not auto-created in x-ui.json | DONE | d733ff2a |
| 12 | Fix master heartbeat not visible to workers | DONE | 226bae2b | | 12 | Fix master heartbeat not visible to workers | DONE | 226bae2b |
| 13 | Fix ensureDefaultNodeSettings to write both "node" and "other" groups | DONE | — |
| 14 | Replace a-descriptions with HTML table (component missing from antd bundle) | DONE | — |
## v1.6.3 Fix Details ## v1.6.3 Fix Details

View file

@ -0,0 +1,48 @@
# 2026-04-25 Fix ensureDefaultNodeSettings and worker node display
## Problem
### 1. Test failures in config package
Two tests were failing:
- `TestWriteSettingToJSONCreatesSettingsFileWhenMissing`
- `TestWriteSettingToJSONBackfillsDefaultNodeSettings`
Both failed with: `expected other group, got <nil>`
### 2. Worker frontend not showing connected master node
The worker's node management page rendered the card structure but didn't display the
master node information. The `a-descriptions` and `a-descriptions-item` components were
used in the template but were NOT included in the Ant Design Vue bundle (`antd.min.js`).
Vue silently skipped the unregistered components, resulting in an empty card body.
## Root Cause
### Test failures
`ensureDefaultNodeSettings()` only wrote defaults to the `"node"` group. Tests expected
the `"other"` group to also have defaults for backward compatibility.
### Worker node display
Ant Design Vue 2.x uses tree-shaking — only components actually imported during the build
are included in the bundle. `a-descriptions` and `a-descriptions-item` were not imported
in the project's Ant Design Vue build config, so they were missing from `antd.min.js`.
When Vue encounters an unregistered component tag, it silently ignores it.
## Fix
### Test failures
Changed `ensureDefaultNodeSettings()` to iterate over both `"node"` and `"other"` groups,
writing defaults to both for backward compatibility.
### Worker node display
Replaced `a-descriptions` / `a-descriptions-item` with a plain HTML `<table>` that
replicates the same visual layout (label-value pairs with borders). This doesn't depend
on any Ant Design Vue component.
## Files Changed
- `config/config.go`: Modified `ensureDefaultNodeSettings()` to write to both groups
- `web/html/nodes.html`: Replaced `a-descriptions` with HTML table
## Verification
- `go test -race -shuffle=on ./...` — all PASS

View file

@ -49,16 +49,36 @@
</a-table> </a-table>
<div v-if="nodeRole === 'worker'"> <div v-if="nodeRole === 'worker'">
<a-empty v-if="nodes.length === 0" :description="nodeRole === 'master' ? '{{ i18n "pages.nodes.noWorkerNodes" }}' : '{{ i18n "pages.nodes.noMasterNode" }}'" /> <a-empty v-if="nodes.length === 0" :description="nodeRole === 'master' ? '{{ i18n "pages.nodes.noWorkerNodes" }}' : '{{ i18n "pages.nodes.noMasterNode" }}'" />
<a-descriptions v-else bordered size="small" :column="isMobile ? 1 : 2"> <table v-else class="ant-descriptions-table" style="width:100%;border-collapse:collapse;">
<a-descriptions-item label='{{ i18n "pages.nodes.nodeId" }}'>[[ nodes[0].nodeId ]]</a-descriptions-item> <tbody>
<a-descriptions-item label='{{ i18n "pages.nodes.status" }}'> <tr style="border-bottom:1px solid #e8e8e8;">
<a-badge :status="nodes[0].online ? 'success' : 'error'" :text="nodes[0].online ? '{{ i18n "pages.nodes.online" }}' : '{{ i18n "pages.nodes.offline" }}'" /> <td style="padding:8px 12px;font-weight:500;background:#fafafa;width:30%;">{{ i18n "pages.nodes.nodeId" }}</td>
</a-descriptions-item> <td style="padding:8px 12px;">[[ nodes[0].nodeId ]]</td>
<a-descriptions-item label='{{ i18n "pages.nodes.lastHeartbeat" }}'>[[ nodes[0].lastHeartbeatAt ? formatTime(nodes[0].lastHeartbeatAt) : '-' ]]</a-descriptions-item> </tr>
<a-descriptions-item label='{{ i18n "pages.nodes.lastSync" }}'>[[ nodes[0].lastSyncAt ? formatTime(nodes[0].lastSyncAt) : '-' ]]</a-descriptions-item> <tr style="border-bottom:1px solid #e8e8e8;">
<a-descriptions-item label='{{ i18n "pages.nodes.syncVersion" }}'>[[ nodes[0].lastSeenVersion ]]</a-descriptions-item> <td style="padding:8px 12px;font-weight:500;background:#fafafa;">{{ i18n "pages.nodes.status" }}</td>
<a-descriptions-item label='{{ i18n "pages.nodes.error" }}'>[[ nodes[0].lastError || '-' ]]</a-descriptions-item> <td style="padding:8px 12px;">
</a-descriptions> <a-badge :status="nodes[0].online ? 'success' : 'error'" :text="nodes[0].online ? '{{ i18n "pages.nodes.online" }}' : '{{ i18n "pages.nodes.offline" }}'" />
</td>
</tr>
<tr style="border-bottom:1px solid #e8e8e8;">
<td style="padding:8px 12px;font-weight:500;background:#fafafa;">{{ i18n "pages.nodes.lastHeartbeat" }}</td>
<td style="padding:8px 12px;">[[ nodes[0].lastHeartbeatAt ? formatTime(nodes[0].lastHeartbeatAt) : '-' ]]</td>
</tr>
<tr style="border-bottom:1px solid #e8e8e8;">
<td style="padding:8px 12px;font-weight:500;background:#fafafa;">{{ i18n "pages.nodes.lastSync" }}</td>
<td style="padding:8px 12px;">[[ nodes[0].lastSyncAt ? formatTime(nodes[0].lastSyncAt) : '-' ]]</td>
</tr>
<tr style="border-bottom:1px solid #e8e8e8;">
<td style="padding:8px 12px;font-weight:500;background:#fafafa;">{{ i18n "pages.nodes.syncVersion" }}</td>
<td style="padding:8px 12px;">[[ nodes[0].lastSeenVersion ]]</td>
</tr>
<tr>
<td style="padding:8px 12px;font-weight:500;background:#fafafa;">{{ i18n "pages.nodes.error" }}</td>
<td style="padding:8px 12px;">[[ nodes[0].lastError || '-' ]]</td>
</tr>
</tbody>
</table>
</div> </div>
<a-empty v-if="nodeRole === 'master' && nodes.length === 0" description='{{ i18n "pages.nodes.noWorkerNodes" }}' /> <a-empty v-if="nodeRole === 'master' && nodes.length === 0" description='{{ i18n "pages.nodes.noWorkerNodes" }}' />
</a-card> </a-card>