mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
fix: node config save, dbType mismatch, and dark theme support
- ShouldBindJSON → ShouldBind with form tags (axios sends url-encoded) - dbType dropdown value "mysql" → "mariadb" to match backend - Replace inline styles with theme-aware CSS classes for dark mode
This commit is contained in:
parent
5bf2b5ef88
commit
66de42f21b
4 changed files with 81 additions and 33 deletions
|
|
@ -1 +1 @@
|
|||
v1.6.6
|
||||
v1.6.6.1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# 2026-04-25 Fix node config save, dbType mismatch, and dark theme
|
||||
|
||||
## Problem
|
||||
|
||||
### 1. Node config save always fails
|
||||
The `saveConfig` endpoint in `node.go` used `c.ShouldBindJSON()` which expects
|
||||
`Content-Type: application/json`. But the global axios interceptor in `axios-init.js`
|
||||
converts all POST data via `Qs.stringify()` and sends it as
|
||||
`application/x-www-form-urlencoded`. The backend rejected every save with:
|
||||
`invalid request (invalid character 's' looking for beginning of value)`.
|
||||
|
||||
### 2. dbType dropdown value mismatch
|
||||
The frontend `<a-select>` used `value="mysql"` for the MySQL/MariaDB option, but the
|
||||
backend checks for `"mariadb"` everywhere (database init, node list query, validation).
|
||||
Saving through the UI would write `"mysql"`, which the backend would treat as SQLite.
|
||||
|
||||
### 3. Worker node info table invisible in dark theme
|
||||
The HTML table for the worker's master node info used hardcoded inline styles
|
||||
(`background:#fafafa`, `border-color:#e8e8e8`). In dark theme, inherited white text
|
||||
on `#fafafa` background made label cells nearly invisible.
|
||||
|
||||
## Fix
|
||||
|
||||
### Config save
|
||||
- Changed `ShouldBindJSON` to `ShouldBind` (matches all other controllers in the project)
|
||||
- Added `form` struct tags to `updateConfigRequest` fields
|
||||
|
||||
### dbType mismatch
|
||||
- Changed dropdown value from `"mysql"` to `"mariadb"` to match the backend constant
|
||||
|
||||
### Dark theme
|
||||
- Extracted inline styles into CSS classes (`.node-info-wrap`, `.node-info-table`)
|
||||
- Added `.dark` theme overrides using existing CSS custom properties
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `web/controller/node.go`: `ShouldBindJSON` → `ShouldBind`, added `form` tags
|
||||
- `web/html/nodes.html`: Fixed dbType value, replaced inline styles with theme-aware CSS
|
||||
|
|
@ -151,22 +151,22 @@ func (a *NodeController) getConfig(c *gin.Context) {
|
|||
}, nil)
|
||||
}
|
||||
|
||||
// updateConfigRequest is the JSON body for updating node config.
|
||||
// updateConfigRequest is the form body for updating node config.
|
||||
type updateConfigRequest struct {
|
||||
SyncInterval int `json:"syncInterval"`
|
||||
TrafficFlushInterval int `json:"trafficFlushInterval"`
|
||||
DBType string `json:"dbType"`
|
||||
DBHost string `json:"dbHost"`
|
||||
DBPort string `json:"dbPort"`
|
||||
DBUser string `json:"dbUser"`
|
||||
DBPass string `json:"dbPass"`
|
||||
DBName string `json:"dbName"`
|
||||
SyncInterval int `json:"syncInterval" form:"syncInterval"`
|
||||
TrafficFlushInterval int `json:"trafficFlushInterval" form:"trafficFlushInterval"`
|
||||
DBType string `json:"dbType" form:"dbType"`
|
||||
DBHost string `json:"dbHost" form:"dbHost"`
|
||||
DBPort string `json:"dbPort" form:"dbPort"`
|
||||
DBUser string `json:"dbUser" form:"dbUser"`
|
||||
DBPass string `json:"dbPass" form:"dbPass"`
|
||||
DBName string `json:"dbName" form:"dbName"`
|
||||
}
|
||||
|
||||
// updateConfig updates the node configuration in x-ui.json.
|
||||
func (a *NodeController) updateConfig(c *gin.Context) {
|
||||
var req updateConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
jsonMsg(c, "invalid request", err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
.node-info-table { width:100%; border-collapse:collapse; }
|
||||
.node-info-table td { padding:8px 12px; border-bottom:1px solid #e8e8e8; }
|
||||
.node-info-table tr:last-child td { border-bottom:none; }
|
||||
.node-info-table .label { font-weight:500; background:#fafafa; width:30%; }
|
||||
.node-info-wrap { border:1px solid #e8e8e8; border-radius:4px; overflow:hidden; }
|
||||
.dark .node-info-wrap { border-color:var(--dark-color-stroke); }
|
||||
.dark .node-info-table td { border-color:var(--dark-color-stroke); color:var(--dark-color-text-primary); }
|
||||
.dark .node-info-table .label { background:var(--dark-color-surface-200); }
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
|
|
@ -49,34 +59,34 @@
|
|||
</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" }}'" />
|
||||
<div v-if="nodes.length > 0" style="border:1px solid #e8e8e8;border-radius:4px;overflow:hidden;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<div v-if="nodes.length > 0" class="node-info-wrap">
|
||||
<table class="node-info-table">
|
||||
<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>
|
||||
<td class="label">{{ i18n "pages.nodes.nodeId" }}</td>
|
||||
<td>[[ 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;">
|
||||
<tr>
|
||||
<td class="label">{{ i18n "pages.nodes.status" }}</td>
|
||||
<td>
|
||||
<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>
|
||||
<td class="label">{{ i18n "pages.nodes.lastHeartbeat" }}</td>
|
||||
<td>[[ nodes[0].lastHeartbeatAt ? formatTime(nodes[0].lastHeartbeatAt) : '-' ]]</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>
|
||||
<td class="label">{{ i18n "pages.nodes.lastSync" }}</td>
|
||||
<td>[[ nodes[0].lastSyncAt ? formatTime(nodes[0].lastSyncAt) : '-' ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">{{ i18n "pages.nodes.syncVersion" }}</td>
|
||||
<td>[[ nodes[0].lastSeenVersion ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">{{ i18n "pages.nodes.error" }}</td>
|
||||
<td>[[ nodes[0].lastError || '-' ]]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -111,7 +121,7 @@
|
|||
<a-form-item label='{{ i18n "pages.nodes.dbType" }}'>
|
||||
<a-select v-model="nodeConfig.dbType" :disabled="saving">
|
||||
<a-select-option value="sqlite">SQLite</a-select-option>
|
||||
<a-select-option value="mysql">MySQL/MariaDB</a-select-option>
|
||||
<a-select-option value="mariadb">MySQL/MariaDB</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
|
|
|||
Loading…
Reference in a new issue