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) {
group, ok := settings["node"].(map[string]any)
if !ok {
group = make(map[string]any)
settings["node"] = group
}
defaults := map[string]string{
"nodeRole": string(NodeRoleMaster),
"nodeId": "",
"syncInterval": "30",
"trafficFlushInterval": "10",
}
for key, value := range defaults {
if existing, exists := group[key]; !exists || existing == nil {
// Also check "other" group for backward compatibility
if otherGroup, ok := settings["other"].(map[string]any); ok {
if val, ok := otherGroup[key].(string); ok && val != "" {
group[key] = val
continue
}
// Ensure both "node" and "other" groups have the defaults for backward
// compatibility. Old code reads from "other", new code reads from "node".
for _, groupName := range []string{"node", "other"} {
group, ok := settings[groupName].(map[string]any)
if !ok {
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 |
| 11 | Fix node settings not auto-created in x-ui.json | DONE | d733ff2a |
| 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

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