fix(web): field-mapping + fragile-hx-vals sweep across profile and library surfaces

Field-by-field audit of the profile surface (set → save → reopen →
read back, driven through the emulator browser via CDP):

- getTemplateUserWithTheme (every SSR page + the /profile self-edit
  form) never mapped FirstName/LastName/CreatedAt — stored names
  rendered as empty inputs. Now mapped (timezone was already mapped).
- Admin profile-modal Timezone mapping fixed earlier this round held:
  all seven fields verified set → 200 + HX-Refresh → reopen → correct
  values, DB cross-checked.
- Role promote/demote via the card select verified against the DB (the
  PUT answers HX-Refresh, so the page reload shows the new role).
- Admin password reset verified end-to-end: new password logs in, old
  one 401s, original restored.
- admin_library folder-remove hx-vals was string-concatenated JSON —
  a folder path containing a quote or backslash made it invalid and
  the remove button silently died under htmx 2 (the same class as the
  profile-save bug). Now built with json.Marshal behind the js:
  prefix. Verified the generated attribute on the live page.
- Other hx-vals/hx-headers sites audited: constants and uuids only
  (safe charset), left alone.
This commit is contained in:
John O'Keefe committed 2026-10-13 18:25:56 -04:00
1 parent dc353b0f3f
commit 320f31086d
4 files changed
+24 -10

No files matched your search

+10 -7
View File
@@ -52,13 +52,16 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
} }
return templates.User{ return templates.User{
ID: userID, ID: userID,
Email: userEmail, Email: userEmail,
Username: userUsername, Username: userUsername,
Role: userRole, Role: userRole,
Theme: userTheme, Theme: userTheme,
Token: token, Token: token,
Timezone: userTimezone, Timezone: userTimezone,
FirstName: getText(userDB.FirstName),
LastName: getText(userDB.LastName),
CreatedAt: userDB.CreatedAt.Time,
}, nil }, nil
} }
+1 -1
View File
@@ -307,7 +307,7 @@ templ LibraryPanel(user User, libraryID string, library LibraryData, folders []F
<button <button
class="icon-btn h-7 w-7 shrink-0" class="icon-btn h-7 w-7 shrink-0"
hx-delete={ "/admin/library/" + libraryID + "/folders" } hx-delete={ "/admin/library/" + libraryID + "/folders" }
hx-vals={ `{"folder_path": "` + folder.FolderPath + `"}` } hx-vals={ hxValsFolderDelete(folder.FolderPath) }
hx-target={ "#library-panel-" + libraryID } hx-target={ "#library-panel-" + libraryID }
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Remove this folder from the library?" hx-confirm="Remove this folder from the library?"
+2 -2
View File
@@ -432,9 +432,9 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"folder_path": "` + folder.FolderPath + `"}`) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(hxValsFolderDelete(folder.FolderPath))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 310, Col: 64} return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 310, Col: 55}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+11
View File
@@ -339,3 +339,14 @@ func GroupTunableSettings(entries []SettingEntry) (live, restart []SettingGroup)
} }
return liveGroups, restartGroups return liveGroups, restartGroups
} }
// hxValsFolderDelete builds the hx-vals attribute for removing a library
// folder: htmx 2 requires valid JSON (or a js: expression) — string
// concatenation broke on paths containing quotes or backslashes.
func hxValsFolderDelete(folderPath string) string {
b, err := json.Marshal(map[string]string{"folder_path": folderPath})
if err != nil {
return `js:{"folder_path": ""}`
}
return "js:" + string(b)
}