fix(nfs): reconcile clients[i].advanced type mismatch (object vs string)

backend: db.NFSClient.Advanced is db.NFSAdvanced (struct → JSON object)
handler: nfsClientRequest.Advanced was string → now db.NFSAdvanced
frontend: api.ts NFSClient.advanced string → NFSAdvanced (object)
frontend: Nfs.tsx updated to work with object advanced per host

Without this fix, editing an existing NFS export with per-host options
(loaded from DB via migration 0005) would 400 on PUT because the
backend expected a string but received a JSON object.
This commit is contained in:
2026-07-06 11:45:44 -04:00
parent 512feaffd7
commit 59505b443f
3 changed files with 40 additions and 41 deletions
+2 -11
View File
@@ -19,7 +19,7 @@ type nfsClientRequest struct {
Async bool `json:"async"` Async bool `json:"async"`
RootSquash bool `json:"root_squash"` RootSquash bool `json:"root_squash"`
SubtreeCheck bool `json:"subtree_check"` SubtreeCheck bool `json:"subtree_check"`
Advanced string `json:"advanced"` Advanced db.NFSAdvanced `json:"advanced"`
} }
type nfsExportRequest struct { type nfsExportRequest struct {
@@ -54,7 +54,7 @@ func (req nfsExportRequest) toModel() db.NFSExport {
Async: c.Async, Async: c.Async,
RootSquash: c.RootSquash, RootSquash: c.RootSquash,
SubtreeCheck: c.SubtreeCheck, SubtreeCheck: c.SubtreeCheck,
Advanced: parseNFSAdvanced(c.Advanced), Advanced: c.Advanced,
}) })
} }
return db.NFSExport{ return db.NFSExport{
@@ -68,15 +68,6 @@ func (req nfsExportRequest) toModel() db.NFSExport {
} }
} }
func parseNFSAdvanced(raw string) db.NFSAdvanced {
var adv db.NFSAdvanced
if raw == "" || raw == "{}" {
return adv
}
_ = json.Unmarshal([]byte(raw), &adv)
return adv
}
func (s *Server) handleListNFSExports(w http.ResponseWriter, r *http.Request) { func (s *Server) handleListNFSExports(w http.ResponseWriter, r *http.Request) {
exports, err := s.DB.ListNFSExports() exports, err := s.DB.ListNFSExports()
if err != nil { if err != nil {
+10 -1
View File
@@ -15,7 +15,16 @@ export interface NFSClient {
async: boolean; async: boolean;
root_squash: boolean; root_squash: boolean;
subtree_check: boolean; subtree_check: boolean;
advanced: string; advanced: NFSAdvanced;
}
export interface NFSAdvanced {
all_squash: boolean;
secure: boolean;
wdelay: boolean;
hide: boolean;
crossmnt: boolean;
[key: string]: boolean;
} }
export interface NFSExport { export interface NFSExport {
+15 -16
View File
@@ -30,14 +30,18 @@ function serializeAdvanced(m: Record<string, boolean>): string {
return JSON.stringify(m); return JSON.stringify(m);
} }
function clientDefaults(exp: Partial<NFSExport>): Partial<NFSClient> { function emptyAdvanced() {
return { all_squash: false, secure: false, wdelay: false, hide: false, crossmnt: false };
}
function clientDefaults(exp: Partial<NFSExport>): NFSClient {
return { return {
host: "", host: "",
read_only: exp.read_only ?? false, read_only: exp.read_only ?? false,
async: exp.async ?? false, async: exp.async ?? false,
root_squash: exp.root_squash ?? true, root_squash: exp.root_squash ?? true,
subtree_check: exp.subtree_check ?? false, subtree_check: exp.subtree_check ?? false,
advanced: exp.advanced ?? "{}", advanced: emptyAdvanced(),
}; };
} }
@@ -72,7 +76,10 @@ export default function Nfs() {
setAdvanced(parseAdvanced(x.advanced ?? "{}")); setAdvanced(parseAdvanced(x.advanced ?? "{}"));
setShowAdvanced(false); setShowAdvanced(false);
setEditing(x); setEditing(x);
setHostDrafts(x.clients.map(c => ({ ...c }))); setHostDrafts(x.clients.map(c => ({
...c,
advanced: c.advanced ?? emptyAdvanced(),
})));
} }
function openNew() { function openNew() {
@@ -89,10 +96,7 @@ export default function Nfs() {
const payload = { const payload = {
...editing, ...editing,
clients: hostDrafts.map(h => ({ clients: hostDrafts.map(h => ({ ...h })),
...h,
advanced: h.advanced !== undefined ? h.advanced : serializeAdvanced(advanced),
})),
advanced: serializeAdvanced(advanced), advanced: serializeAdvanced(advanced),
}; };
@@ -138,9 +142,7 @@ export default function Nfs() {
function toggleHostAdvanced(idx: number, key: string) { function toggleHostAdvanced(idx: number, key: string) {
setHostDrafts(prev => prev.map((h, i) => { setHostDrafts(prev => prev.map((h, i) => {
if (i !== idx) return h; if (i !== idx) return h;
const adv = parseAdvanced(h.advanced || "{}"); return { ...h, advanced: { ...h.advanced, [key]: !h.advanced[key] } };
adv[key] = !adv[key];
return { ...h, advanced: serializeAdvanced(adv) };
})); }));
} }
@@ -262,20 +264,17 @@ export default function Nfs() {
{showAdvanced ? "▾" : "▸"} Avanzado {showAdvanced ? "▾" : "▸"} Avanzado
</summary> </summary>
<div className="mt-1 grid grid-cols-2 gap-y-1"> <div className="mt-1 grid grid-cols-2 gap-y-1">
{ADVANCED_KEYS.map(({ key, label }) => { {ADVANCED_KEYS.map(({ key, label }) => (
const adv = parseAdvanced(c.advanced || "{}");
return (
<label key={key} className="flex items-center gap-1.5 text-xs text-slate-300"> <label key={key} className="flex items-center gap-1.5 text-xs text-slate-300">
<input <input
type="checkbox" type="checkbox"
className="checkbox" className="checkbox"
checked={!!adv[key]} checked={!!c.advanced?.[key]}
onChange={() => toggleHostAdvanced(idx, key)} onChange={() => toggleHostAdvanced(idx, key)}
/> />
{label} {label}
</label> </label>
); ))}
})}
</div> </div>
</details> </details>
</div> </div>