From b7757c738784c2c5f149e4c5eba98e652a277b34 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 20 Jul 2026 15:25:29 +0800 Subject: [PATCH] fix: normalize percent-encoding case in serveFacePhoto via PathUnescape; remove /ui prefix globally --- cmd/safesightd/main.go | 4 +- internal/web/ui.go | 190 +++++++++--------- internal/web/ui/templates/alarms.html | 24 +-- internal/web/ui/templates/asset_overlay.html | 2 +- internal/web/ui/templates/asset_overlays.html | 16 +- internal/web/ui/templates/asset_profiles.html | 14 +- internal/web/ui/templates/asset_template.html | 2 +- .../ui/templates/asset_template_graph.html | 10 +- .../web/ui/templates/asset_templates.html | 18 +- internal/web/ui/templates/assets.html | 50 ++--- .../web/ui/templates/config_friendly.html | 16 +- internal/web/ui/templates/config_preview.html | 4 +- internal/web/ui/templates/config_ui.html | 10 +- .../web/ui/templates/config_versions.html | 6 +- internal/web/ui/templates/console.html | 10 +- internal/web/ui/templates/dashboard.html | 12 +- internal/web/ui/templates/device.html | 32 +-- internal/web/ui/templates/device_add.html | 4 +- .../web/ui/templates/device_assignments.html | 2 +- .../web/ui/templates/device_batch_config.html | 4 +- internal/web/ui/templates/device_config.html | 4 +- internal/web/ui/templates/device_control.html | 14 +- internal/web/ui/templates/devices.html | 6 +- internal/web/ui/templates/diagnostics.html | 6 +- internal/web/ui/templates/face_gallery.html | 22 +- internal/web/ui/templates/layout.html | 68 +++---- internal/web/ui/templates/logs.html | 8 +- internal/web/ui/templates/models.html | 4 +- internal/web/ui/templates/monitor.html | 8 +- internal/web/ui/templates/recognition.html | 8 +- .../web/ui/templates/recognition_units.html | 14 +- internal/web/ui/templates/resources.html | 2 +- .../web/ui/templates/scene_templates.html | 14 +- internal/web/ui/templates/system.html | 4 +- internal/web/ui/templates/task.html | 2 +- internal/web/ui/templates/tasks.html | 2 +- internal/web/ui/templates/template.html | 2 +- internal/web/ui/templates/templates.html | 2 +- internal/web/ui/templates/wizard.html | 8 +- safesightd-linux-arm64 | Bin 15073442 -> 15073442 bytes 40 files changed, 316 insertions(+), 312 deletions(-) diff --git a/cmd/safesightd/main.go b/cmd/safesightd/main.go index 0a3e911..48d0906 100644 --- a/cmd/safesightd/main.go +++ b/cmd/safesightd/main.go @@ -96,7 +96,7 @@ func main() { }) r.Get("/openapi.json", api.OpenAPI) r.Get("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui", http.StatusFound) + http.Redirect(w, r, "/dashboard", http.StatusFound) }) previewSvc := service.NewConfigPreviewService(cfg, assetsRepo) @@ -118,7 +118,7 @@ func main() { if err != nil { log.Fatalf("failed to init ui routes: %v", err) } - r.Mount("/ui", http.StripPrefix("/ui", uiRouter)) + r.Mount("/", uiRouter) ui.RegisterAlarmRoutes(r) // API Routes diff --git a/internal/web/ui.go b/internal/web/ui.go index 1054fc2..4334a06 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -721,7 +721,7 @@ func (u *UI) Routes() (chi.Router, error) { })) r.Get("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui/dashboard", http.StatusFound) + http.Redirect(w, r, "/dashboard", http.StatusFound) }) r.Get("/dashboard", u.pageDashboard) @@ -919,7 +919,7 @@ func (u *UI) pageDashboard(w http.ResponseWriter, r *http.Request) { // First-time setup: if no recognition units exist, guide to wizard. if u.preview != nil { if units, err := u.preview.ListRecognitionUnits(); err == nil && len(units) == 0 { - http.Redirect(w, r, "/ui/wizard", http.StatusFound) + http.Redirect(w, r, "/wizard", http.StatusFound) return } } @@ -1261,7 +1261,7 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { return } if u.autoConfig == nil || u.preview == nil { - http.Redirect(w, r, "/ui/console?error="+url.QueryEscape("自动配置服务未初始化"), http.StatusFound) + http.Redirect(w, r, "/console?error="+url.QueryEscape("自动配置服务未初始化"), http.StatusFound) return } @@ -1309,7 +1309,7 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { } if len(requests) == 0 { - http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("未选择检测功能"), http.StatusFound) + http.Redirect(w, r, "/console?msg="+url.QueryEscape("未选择检测功能"), http.StatusFound) return } @@ -1334,18 +1334,18 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { } if len(taskIDs) == 1 { - http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("正在下发配置...")+"&task="+taskIDs[0], http.StatusFound) + http.Redirect(w, r, "/console?msg="+url.QueryEscape("正在下发配置...")+"&task="+taskIDs[0], http.StatusFound) return } if len(taskIDs) > 1 { - http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape(fmt.Sprintf("正在为 %d 台设备下发配置...", len(taskIDs))), http.StatusFound) + http.Redirect(w, r, "/console?msg="+url.QueryEscape(fmt.Sprintf("正在为 %d 台设备下发配置...", len(taskIDs))), http.StatusFound) return } if len(errs) > 0 { - http.Redirect(w, r, "/ui/console?error="+url.QueryEscape(strings.Join(errs, "; ")), http.StatusFound) + http.Redirect(w, r, "/console?error="+url.QueryEscape(strings.Join(errs, "; ")), http.StatusFound) return } - http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("配置已保存"), http.StatusFound) + http.Redirect(w, r, "/console?msg="+url.QueryEscape("配置已保存"), http.StatusFound) } func (u *UI) pageWizard(w http.ResponseWriter, r *http.Request) { @@ -1534,7 +1534,7 @@ func (u *UI) pageDeviceConfig(w http.ResponseWriter, r *http.Request) { func (u *UI) pageDeviceConfigDetail(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - http.Redirect(w, r, "/ui/devices/"+url.PathEscape(id)+"#device-config", http.StatusFound) + http.Redirect(w, r, "/devices/"+url.PathEscape(id)+"#device-config", http.StatusFound) } func (u *UI) actionDeviceAdd(w http.ResponseWriter, r *http.Request) { @@ -1566,7 +1566,7 @@ func (u *UI) actionDeviceAdd(w http.ResponseWriter, r *http.Request) { LastSeenMs: time.Now().UnixMilli(), } u.registry.UpdateDevice(dev) - http.Redirect(w, r, "/ui/devices", http.StatusFound) + http.Redirect(w, r, "/devices", http.StatusFound) } func (u *UI) actionDiscoverySearch(w http.ResponseWriter, r *http.Request) { @@ -1628,7 +1628,7 @@ func (u *UI) actionDevicesBatchAction(w http.ResponseWriter, r *http.Request) { return } - http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound) + http.Redirect(w, r, "/tasks/"+task.ID, http.StatusFound) } func (u *UI) pageDeviceBatchConfig(w http.ResponseWriter, r *http.Request) { @@ -1678,7 +1678,7 @@ func (u *UI) actionDeviceBatchConfig(w http.ResponseWriter, r *http.Request) { return } - http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound) + http.Redirect(w, r, "/tasks/"+task.ID, http.StatusFound) } func (u *UI) pageDevice(w http.ResponseWriter, r *http.Request) { @@ -1738,7 +1738,7 @@ func (u *UI) deviceDetailPageData(dev *models.Device) PageData { func (u *UI) pageDeviceControl(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - http.Redirect(w, r, "/ui/devices/"+url.PathEscape(id)+"#device-config", http.StatusFound) + http.Redirect(w, r, "/devices/"+url.PathEscape(id)+"#device-config", http.StatusFound) } func (u *UI) actionDeviceAction(w http.ResponseWriter, r *http.Request) { @@ -2114,7 +2114,7 @@ func (u *UI) actionCreateTask(w http.ResponseWriter, r *http.Request) { u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: err.Error(), RawJSON: raw, DeviceIDs: ids}) return } - http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound) + http.Redirect(w, r, "/tasks/"+task.ID, http.StatusFound) } func (u *UI) pageTask(w http.ResponseWriter, r *http.Request) { @@ -2138,11 +2138,11 @@ func (u *UI) pageTask(w http.ResponseWriter, r *http.Request) { } func (u *UI) pageTemplates(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui/assets/templates", http.StatusFound) + http.Redirect(w, r, "/assets/templates", http.StatusFound) } func (u *UI) pageTemplate(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui/assets/templates/"+url.PathEscape(chi.URLParam(r, "name")), http.StatusFound) + http.Redirect(w, r, "/assets/templates/"+url.PathEscape(chi.URLParam(r, "name")), http.StatusFound) } func (u *UI) pageModels(w http.ResponseWriter, r *http.Request) { @@ -2251,7 +2251,7 @@ func (u *UI) actionModelSync(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, "/ui/tasks/"+url.PathEscape(task.ID), http.StatusFound) + http.Redirect(w, r, "/tasks/"+url.PathEscape(task.ID), http.StatusFound) } func (u *UI) actionModelUpload(w http.ResponseWriter, r *http.Request) { @@ -2336,12 +2336,12 @@ func (u *UI) actionModelUpload(w http.ResponseWriter, r *http.Request) { if len(deviceIDs) > 0 { payload := map[string]any{"model_name": name} task, _ := u.tasks.CreateTask("model_sync_all", deviceIDs, payload) - http.Redirect(w, r, "/ui/tasks/"+url.PathEscape(task.ID), http.StatusFound) + http.Redirect(w, r, "/tasks/"+url.PathEscape(task.ID), http.StatusFound) return } } - http.Redirect(w, r, "/ui/models?msg="+url.QueryEscape("模型 "+name+" 上传成功"), http.StatusFound) + http.Redirect(w, r, "/models?msg="+url.QueryEscape("模型 "+name+" 上传成功"), http.StatusFound) } func (u *UI) pageDiagnostics(w http.ResponseWriter, r *http.Request) { @@ -2625,15 +2625,15 @@ func (u *UI) actionResourceSync(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, "/ui/tasks/"+url.PathEscape(task.ID), http.StatusFound) + http.Redirect(w, r, "/tasks/"+url.PathEscape(task.ID), http.StatusFound) } func (u *UI) pageRecognition(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui/assets", http.StatusFound) + http.Redirect(w, r, "/assets", http.StatusFound) } func (u *UI) pageLogs(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui/diagnostics", http.StatusFound) + http.Redirect(w, r, "/diagnostics", http.StatusFound) } func (u *UI) pageAPIConsole(w http.ResponseWriter, r *http.Request) { @@ -2744,7 +2744,7 @@ func (u *UI) actionAssetTemplateCreate(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), status) return } - http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(name)+"&edit=1&msg="+urlQueryEscape("模板已创建"), http.StatusFound) + http.Redirect(w, r, "/assets/templates?name="+url.PathEscape(name)+"&edit=1&msg="+urlQueryEscape("模板已创建"), http.StatusFound) } func (u *UI) actionAssetTemplateClone(w http.ResponseWriter, r *http.Request) { @@ -2759,18 +2759,18 @@ func (u *UI) actionAssetTemplateClone(w http.ResponseWriter, r *http.Request) { return } if !item.ReadOnly { - http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape("只允许从标准模板复制创建"), http.StatusFound) + http.Redirect(w, r, "/assets/templates?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape("只允许从标准模板复制创建"), http.StatusFound) return } targetName, err := u.nextTemplateCloneName(item.Name) if err != nil { - http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/assets/templates?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) return } cloneSource := url.QueryEscape(sourceName) cloneName := url.QueryEscape(targetName) cloneDesc := url.QueryEscape(item.Description) - http.Redirect(w, r, "/ui/assets/templates?clone_source="+cloneSource+"&clone_name="+cloneName+"&clone_desc="+cloneDesc, http.StatusFound) + http.Redirect(w, r, "/assets/templates?clone_source="+cloneSource+"&clone_name="+cloneName+"&clone_desc="+cloneDesc, http.StatusFound) } func (u *UI) nextTemplateCloneName(sourceName string) (string, error) { @@ -2930,7 +2930,7 @@ func (u *UI) actionAssetTemplateGraphSave(w http.ResponseWriter, r *http.Request if targetName != name { message = "模板已保存,名称已更新" } - http.Redirect(w, r, "/ui/assets/templates/"+url.PathEscape(targetName)+"/graph?msg="+urlQueryEscape(message), http.StatusFound) + http.Redirect(w, r, "/assets/templates/"+url.PathEscape(targetName)+"/graph?msg="+urlQueryEscape(message), http.StatusFound) } func (u *UI) actionAssetTemplateRename(w http.ResponseWriter, r *http.Request) { @@ -2980,10 +2980,10 @@ func (u *UI) actionAssetTemplateRename(w http.ResponseWriter, r *http.Request) { err = u.preview.SaveTemplateAsset(name, description, string(body)+"\n") } if err != nil { - http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/assets/templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) return } - http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(targetName)+"&msg="+urlQueryEscape("模板已保存,名称已更新"), http.StatusFound) + http.Redirect(w, r, "/assets/templates?name="+url.PathEscape(targetName)+"&msg="+urlQueryEscape("模板已保存,名称已更新"), http.StatusFound) } func (u *UI) actionAssetTemplateDelete(w http.ResponseWriter, r *http.Request) { @@ -2993,10 +2993,10 @@ func (u *UI) actionAssetTemplateDelete(w http.ResponseWriter, r *http.Request) { return } if err := u.preview.DeleteTemplateAsset(name); err != nil { - http.Redirect(w, r, "/ui/assets/templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/assets/templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) return } - http.Redirect(w, r, "/ui/assets/templates?msg="+urlQueryEscape("用户模板已删除"), http.StatusFound) + http.Redirect(w, r, "/assets/templates?msg="+urlQueryEscape("用户模板已删除"), http.StatusFound) } func (u *UI) apiGraphNodeTypes(w http.ResponseWriter, r *http.Request) { @@ -3057,7 +3057,7 @@ func (u *UI) pagePlans(w http.ResponseWriter, r *http.Request) { } data.AssetProfileEditor = editor data.AssetProfileEditing = true - data.AssetProfileFormAction = "/ui/scene-templates/create" + data.AssetProfileFormAction = "/scene-templates/create" data.ActiveInstanceIndex = clampActiveInstanceIndex(len(data.AssetProfileEditor.Instances), 0) u.render(w, r, "scene_templates", data) return @@ -3068,7 +3068,7 @@ func (u *UI) pagePlans(w http.ResponseWriter, r *http.Request) { data.AssetProfileEditor = editor data.SelectedProfile = editor.Name data.AssetProfileEditing = editMode - data.AssetProfileFormAction = "/ui/scene-templates/" + url.PathEscape(editor.Name) + data.AssetProfileFormAction = "/scene-templates/" + url.PathEscape(editor.Name) if len(editor.Instances) > 0 && editor.Instances[0].Template != "" { data.SelectedTemplate = editor.Instances[0].Template } @@ -3083,7 +3083,7 @@ func (u *UI) pagePlans(w http.ResponseWriter, r *http.Request) { } data.AssetProfileEditor = editor data.AssetProfileEditing = true - data.AssetProfileFormAction = "/ui/scene-templates/create" + data.AssetProfileFormAction = "/scene-templates/create" data.ActiveInstanceIndex = clampActiveInstanceIndex(len(data.AssetProfileEditor.Instances), 0) } u.render(w, r, "scene_templates", data) @@ -3102,7 +3102,7 @@ func (u *UI) pagePlan(w http.ResponseWriter, r *http.Request) { } data.Title = "场景" data.AssetProfileEditing = strings.TrimSpace(r.URL.Query().Get("edit")) == "1" - data.AssetProfileFormAction = "/ui/scene-templates/" + url.PathEscape(name) + data.AssetProfileFormAction = "/scene-templates/" + url.PathEscape(name) data.ActiveInstanceIndex = clampActiveInstanceIndex(len(data.AssetProfileEditor.Instances), activeInstanceIndexFromValues(r.URL.Query())) u.render(w, r, "scene_templates", data) } @@ -3149,7 +3149,7 @@ func (u *UI) actionPlanSaveWithName(w http.ResponseWriter, r *http.Request, name } data.AssetProfileEditor = &editor data.AssetProfileEditing = true - data.AssetProfileFormAction = "/ui/scene-templates/create" + data.AssetProfileFormAction = "/scene-templates/create" data.ActiveInstanceIndex = clampActiveInstanceIndex(len(editor.Instances), activeInstanceIndexFromValues(r.Form)) } else { editor, data, err = u.profileEditorActionData(r, name) @@ -3158,7 +3158,7 @@ func (u *UI) actionPlanSaveWithName(w http.ResponseWriter, r *http.Request, name return } data.AssetProfileEditing = true - data.AssetProfileFormAction = "/ui/scene-templates/" + url.PathEscape(name) + data.AssetProfileFormAction = "/scene-templates/" + url.PathEscape(name) } if err := u.preview.SaveProfileEditor(editor); err != nil { data.Error = err.Error() @@ -3171,7 +3171,7 @@ func (u *UI) actionPlanSaveWithName(w http.ResponseWriter, r *http.Request, name if strings.TrimSpace(name) != "" && editor.Name != name { msg = "场景已保存,名称已更新" } - http.Redirect(w, r, "/ui/scene-templates?msg="+urlQueryEscape(msg)+"&name="+url.PathEscape(editor.Name), http.StatusFound) + http.Redirect(w, r, "/scene-templates?msg="+urlQueryEscape(msg)+"&name="+url.PathEscape(editor.Name), http.StatusFound) } func (u *UI) actionPlanDelete(w http.ResponseWriter, r *http.Request) { @@ -3183,7 +3183,7 @@ func (u *UI) actionPlanDelete(w http.ResponseWriter, r *http.Request) { if u.stateRepo != nil { items, err := u.stateRepo.ListByProfileName(name) if err != nil { - http.Redirect(w, r, "/ui/scene-templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/scene-templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) return } if len(items) > 0 { @@ -3192,15 +3192,15 @@ func (u *UI) actionPlanDelete(w http.ResponseWriter, r *http.Request) { deviceIDs = append(deviceIDs, item.DeviceID) } msg := fmt.Sprintf("场景 %q 正被以下设备使用,无法删除:%s", name, strings.Join(deviceIDs, "、")) - http.Redirect(w, r, "/ui/scene-templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(msg), http.StatusFound) + http.Redirect(w, r, "/scene-templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(msg), http.StatusFound) return } } if err := u.preview.DeleteProfileAsset(name); err != nil { - http.Redirect(w, r, "/ui/scene-templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/scene-templates?name="+url.PathEscape(name)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) return } - http.Redirect(w, r, "/ui/scene-templates?msg="+urlQueryEscape("场景已删除"), http.StatusFound) + http.Redirect(w, r, "/scene-templates?msg="+urlQueryEscape("场景已删除"), http.StatusFound) } func (u *UI) actionAssetProfileSave(w http.ResponseWriter, r *http.Request) { @@ -3208,7 +3208,7 @@ func (u *UI) actionAssetProfileSave(w http.ResponseWriter, r *http.Request) { } func (u *UI) redirectAssetProfilesToPlans(w http.ResponseWriter, r *http.Request) { - target := "/ui/scene-templates" + target := "/scene-templates" if r.URL.RawQuery != "" { target += "?" + r.URL.RawQuery } @@ -3216,11 +3216,11 @@ func (u *UI) redirectAssetProfilesToPlans(w http.ResponseWriter, r *http.Request } func (u *UI) redirectAssetProfileToPlan(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ui/scene-templates/"+url.PathEscape(chi.URLParam(r, "name")), http.StatusFound) + http.Redirect(w, r, "/scene-templates/"+url.PathEscape(chi.URLParam(r, "name")), http.StatusFound) } func (u *UI) redirectPlansToSceneTemplates(w http.ResponseWriter, r *http.Request) { - target := "/ui/scene-templates" + target := "/scene-templates" if r.URL.RawQuery != "" { target += "?" + r.URL.RawQuery } @@ -3228,7 +3228,7 @@ func (u *UI) redirectPlansToSceneTemplates(w http.ResponseWriter, r *http.Reques } func (u *UI) redirectPlanToSceneTemplate(w http.ResponseWriter, r *http.Request) { - target := "/ui/scene-templates/" + url.PathEscape(chi.URLParam(r, "name")) + target := "/scene-templates/" + url.PathEscape(chi.URLParam(r, "name")) if r.URL.RawQuery != "" { target += "?" + r.URL.RawQuery } @@ -3294,7 +3294,7 @@ func (u *UI) actionRecognitionUnitSave(w http.ResponseWriter, r *http.Request) { return } ref := serviceRecognitionUnitRef(asset.SceneTemplateName, asset.Name) - http.Redirect(w, r, "/ui/recognition-units?msg="+urlQueryEscape("视频通道已保存")+"&ref="+url.QueryEscape(ref), http.StatusFound) + http.Redirect(w, r, "/recognition-units?msg="+urlQueryEscape("视频通道已保存")+"&ref="+url.QueryEscape(ref), http.StatusFound) } func serviceRecognitionUnitRef(profileName string, unitName string) string { @@ -3304,10 +3304,10 @@ func serviceRecognitionUnitRef(profileName string, unitName string) string { func (u *UI) actionRecognitionUnitDelete(w http.ResponseWriter, r *http.Request) { ref := strings.TrimSpace(r.FormValue("ref")) if err := u.preview.DeleteRecognitionUnit(ref); err != nil { - http.Redirect(w, r, "/ui/recognition-units?error="+urlQueryEscape(err.Error())+"&ref="+url.QueryEscape(ref), http.StatusFound) + http.Redirect(w, r, "/recognition-units?error="+urlQueryEscape(err.Error())+"&ref="+url.QueryEscape(ref), http.StatusFound) return } - http.Redirect(w, r, "/ui/recognition-units?msg="+urlQueryEscape("视频通道已删除"), http.StatusFound) + http.Redirect(w, r, "/recognition-units?msg="+urlQueryEscape("视频通道已删除"), http.StatusFound) } func (u *UI) pageDeviceAssignments(w http.ResponseWriter, r *http.Request) { @@ -3389,10 +3389,10 @@ func (u *UI) actionDeviceAssignmentSave(w http.ResponseWriter, r *http.Request) return } if err := u.preview.SaveDeviceAssignmentBoard(filterPreviewDeviceAssignments(assignments)); err != nil { - http.Redirect(w, r, "/ui/device-assignments?error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/device-assignments?error="+urlQueryEscape(err.Error()), http.StatusFound) return } - http.Redirect(w, r, "/ui/device-assignments?msg="+urlQueryEscape("通道部署已保存"), http.StatusFound) + http.Redirect(w, r, "/device-assignments?msg="+urlQueryEscape("通道部署已保存"), http.StatusFound) } func deviceAssignmentBoardDevices(devices []*models.Device) []*models.Device { @@ -3438,10 +3438,10 @@ func filterPreviewDeviceAssignments(assignments map[string][]string) map[string] func (u *UI) actionDeviceAssignmentDelete(w http.ResponseWriter, r *http.Request) { deviceID := strings.TrimSpace(chi.URLParam(r, "id")) if err := u.preview.DeleteDeviceAssignment(deviceID); err != nil { - http.Redirect(w, r, "/ui/device-assignments?error="+urlQueryEscape(err.Error())+"&device_id="+url.PathEscape(deviceID), http.StatusFound) + http.Redirect(w, r, "/device-assignments?error="+urlQueryEscape(err.Error())+"&device_id="+url.PathEscape(deviceID), http.StatusFound) return } - http.Redirect(w, r, "/ui/device-assignments?msg="+urlQueryEscape("通道部署已删除"), http.StatusFound) + http.Redirect(w, r, "/device-assignments?msg="+urlQueryEscape("通道部署已删除"), http.StatusFound) } func (u *UI) pageAssetVideoSources(w http.ResponseWriter, r *http.Request) { @@ -3509,7 +3509,7 @@ func (u *UI) actionAssetVideoSourceSave(w http.ResponseWriter, r *http.Request) u.render(w, r, "assets", data) return } - http.Redirect(w, r, "/ui/assets/video-sources?msg="+urlQueryEscape("视频源已保存")+"&name="+url.PathEscape(asset.Name), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?msg="+urlQueryEscape("视频源已保存")+"&name="+url.PathEscape(asset.Name), http.StatusFound) } func (u *UI) actionAssetVideoSourceDelete(w http.ResponseWriter, r *http.Request) { @@ -3519,10 +3519,10 @@ func (u *UI) actionAssetVideoSourceDelete(w http.ResponseWriter, r *http.Request } name := chi.URLParam(r, "name") if err := u.preview.DeleteVideoSource(name); err != nil { - http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(name), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(name), http.StatusFound) return } - http.Redirect(w, r, "/ui/assets/video-sources?msg="+urlQueryEscape("视频源已删除"), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?msg="+urlQueryEscape("视频源已删除"), http.StatusFound) } func (u *UI) actionVideoSourceCSVTemplate(w http.ResponseWriter, r *http.Request) { @@ -3540,12 +3540,12 @@ func (u *UI) actionVideoSourceCSVImport(w http.ResponseWriter, r *http.Request) return } if err := r.ParseMultipartForm(10 << 20); err != nil { - http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("文件解析失败"), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?error="+urlQueryEscape("文件解析失败"), http.StatusFound) return } file, _, err := r.FormFile("file") if err != nil { - http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("请选择文件"), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?error="+urlQueryEscape("请选择文件"), http.StatusFound) return } defer file.Close() @@ -3554,11 +3554,11 @@ func (u *UI) actionVideoSourceCSVImport(w http.ResponseWriter, r *http.Request) reader.LazyQuotes = true records, err := reader.ReadAll() if err != nil { - http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("CSV 解析失败: "+err.Error()), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?error="+urlQueryEscape("CSV 解析失败: "+err.Error()), http.StatusFound) return } if len(records) < 2 { - http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("CSV 文件为空或无数据行"), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?error="+urlQueryEscape("CSV 文件为空或无数据行"), http.StatusFound) return } @@ -3621,7 +3621,7 @@ func (u *UI) actionVideoSourceCSVImport(w http.ResponseWriter, r *http.Request) } msg := fmt.Sprintf("导入完成:%d 个视频源,%d 条跳过", imported, skipped) - http.Redirect(w, r, "/ui/assets/video-sources?msg="+urlQueryEscape(msg), http.StatusFound) + http.Redirect(w, r, "/assets/video-sources?msg="+urlQueryEscape(msg), http.StatusFound) } func (u *UI) pageAssetIntegrations(w http.ResponseWriter, r *http.Request) { @@ -3692,7 +3692,7 @@ func (u *UI) actionAssetIntegrationSave(w http.ResponseWriter, r *http.Request) } var raw map[string]any if err := json.Unmarshal([]byte(configRaw), &raw); err != nil { - http.Redirect(w, r, "/ui/assets/integrations?error="+urlQueryEscape("自定义配置 JSON 格式不正确:"+err.Error())+"&name="+url.PathEscape(asset.Name), http.StatusFound) + http.Redirect(w, r, "/assets/integrations?error="+urlQueryEscape("自定义配置 JSON 格式不正确:"+err.Error())+"&name="+url.PathEscape(asset.Name), http.StatusFound) return } asset.Raw = raw @@ -3706,10 +3706,10 @@ func (u *UI) actionAssetIntegrationSave(w http.ResponseWriter, r *http.Request) asset.AlarmService.TenantCode = strings.TrimSpace(r.FormValue("tenant_code")) } if err := u.preview.SaveIntegrationServiceAsset(asset); err != nil { - http.Redirect(w, r, "/ui/assets/integrations?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(asset.Name), http.StatusFound) + http.Redirect(w, r, "/assets/integrations?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(asset.Name), http.StatusFound) return } - http.Redirect(w, r, "/ui/assets/integrations?msg="+urlQueryEscape("第三方服务已保存")+"&name="+url.PathEscape(asset.Name), http.StatusFound) + http.Redirect(w, r, "/assets/integrations?msg="+urlQueryEscape("第三方服务已保存")+"&name="+url.PathEscape(asset.Name), http.StatusFound) } func (u *UI) actionAssetIntegrationDelete(w http.ResponseWriter, r *http.Request) { @@ -3719,10 +3719,10 @@ func (u *UI) actionAssetIntegrationDelete(w http.ResponseWriter, r *http.Request } name := chi.URLParam(r, "name") if err := u.preview.DeleteIntegrationService(name); err != nil { - http.Redirect(w, r, "/ui/assets/integrations?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(name), http.StatusFound) + http.Redirect(w, r, "/assets/integrations?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(name), http.StatusFound) return } - http.Redirect(w, r, "/ui/assets/integrations?msg="+urlQueryEscape("第三方服务已删除"), http.StatusFound) + http.Redirect(w, r, "/assets/integrations?msg="+urlQueryEscape("第三方服务已删除"), http.StatusFound) } func (u *UI) pageAssetOverlays(w http.ResponseWriter, r *http.Request) { @@ -3833,7 +3833,7 @@ func (u *UI) actionAssetOverlaySave(w http.ResponseWriter, r *http.Request) { u.render(w, r, "asset_overlays", data) return } - http.Redirect(w, r, "/ui/assets/overlays?msg="+urlQueryEscape("调试参数已保存")+"&name="+url.PathEscape(name), http.StatusFound) + http.Redirect(w, r, "/assets/overlays?msg="+urlQueryEscape("调试参数已保存")+"&name="+url.PathEscape(name), http.StatusFound) } func (u *UI) actionAssetOverlayClone(w http.ResponseWriter, r *http.Request) { @@ -3848,17 +3848,17 @@ func (u *UI) actionAssetOverlayClone(w http.ResponseWriter, r *http.Request) { return } if !item.ReadOnly { - http.Redirect(w, r, "/ui/assets/overlays?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape("只允许从标准调试参数复制创建"), http.StatusFound) + http.Redirect(w, r, "/assets/overlays?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape("只允许从标准调试参数复制创建"), http.StatusFound) return } targetName, err := u.nextOverlayCloneName(item.Name) if err != nil { - http.Redirect(w, r, "/ui/assets/overlays?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) + http.Redirect(w, r, "/assets/overlays?name="+url.PathEscape(sourceName)+"&error="+urlQueryEscape(err.Error()), http.StatusFound) return } cloneSource := url.QueryEscape(sourceName) cloneName := url.QueryEscape(targetName) - http.Redirect(w, r, "/ui/assets/overlays?new=1&clone_source="+cloneSource+"&clone_name="+cloneName, http.StatusFound) + http.Redirect(w, r, "/assets/overlays?new=1&clone_source="+cloneSource+"&clone_name="+cloneName, http.StatusFound) } func (u *UI) nextOverlayCloneName(sourceName string) (string, error) { @@ -3890,10 +3890,10 @@ func (u *UI) actionAssetOverlayDelete(w http.ResponseWriter, r *http.Request) { } name := chi.URLParam(r, "name") if err := u.preview.DeleteOverlayAsset(name); err != nil { - http.Redirect(w, r, "/ui/assets/overlays?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(name), http.StatusFound) + http.Redirect(w, r, "/assets/overlays?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(name), http.StatusFound) return } - http.Redirect(w, r, "/ui/assets/overlays?msg="+urlQueryEscape("调试参数已删除"), http.StatusFound) + http.Redirect(w, r, "/assets/overlays?msg="+urlQueryEscape("调试参数已删除"), http.StatusFound) } func (u *UI) assetPageData(tab string) PageData { @@ -4125,7 +4125,7 @@ func (u *UI) actionSystemDBRestore(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, "/ui/system?msg="+urlQueryEscape("数据库恢复完成"), http.StatusFound) + http.Redirect(w, r, "/system?msg="+urlQueryEscape("数据库恢复完成"), http.StatusFound) } func urlQueryEscape(s string) string { @@ -4265,15 +4265,15 @@ func (u *UI) loadConfigStatus(dev *models.Device) (*ConfigStatusView, string, er } func (u *UI) loadConfigUIData(dev *models.Device) PageData { - schemaBody, schemaCode, schemaErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/config/ui/schema", nil) - stateBody, stateCode, stateErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/config/ui/state", nil) + schemaBody, schemaCode, schemaErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/config/schema", nil) + stateBody, stateCode, stateErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/config/state", nil) faceBody, faceCode, faceErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/face-gallery", nil) data := PageData{ Title: "高级识别配置", Device: dev, - SchemaJSON: fmt.Sprintf("GET /v1/config/ui/schema -> %d\n%s", schemaCode, prettyJSON(schemaBody)), - StateJSON: fmt.Sprintf("GET /v1/config/ui/state -> %d\n%s", stateCode, prettyJSON(stateBody)), + SchemaJSON: fmt.Sprintf("GET /v1/config/schema -> %d\n%s", schemaCode, prettyJSON(schemaBody)), + StateJSON: fmt.Sprintf("GET /v1/config/state -> %d\n%s", stateCode, prettyJSON(stateBody)), FaceGalleryJSON: fmt.Sprintf("GET /v1/face-gallery -> %d\n%s", faceCode, prettyJSON(faceBody)), RawJSON: strings.TrimSpace(prettyJSON(stateBody)), } @@ -4563,7 +4563,7 @@ func (u *UI) actionDevicePlanApply(w http.ResponseWriter, r *http.Request) { u.render(w, r, "device", data) return } - http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound) + http.Redirect(w, r, "/tasks/"+task.ID, http.StatusFound) } func nextProfileInstanceName(instances []service.ConfigProfileInstanceEditor) string { @@ -4918,8 +4918,8 @@ func (u *UI) deviceOverviewPageData(r *http.Request, selectedIDs []string, errMs FoundCount: attention, SelectedDeviceIDs: selectedIDs, SelectedQuery: selectedQueryString(selectedIDs), - SelectedDevicesURL: selectedURL("/ui/devices", selectedIDs), - BatchConfigURL: selectedURL("/ui/devices/batch-config", selectedIDs), + SelectedDevicesURL: selectedURL("/devices", selectedIDs), + BatchConfigURL: selectedURL("/devices/batch-config", selectedIDs), ReloadSummary: batchActionSummary(rows, selectedIDs, "reload"), RollbackSummary: batchActionSummary(rows, selectedIDs, "rollback"), } @@ -5121,9 +5121,9 @@ func (u *UI) actionDeviceConfigUIPlan(w http.ResponseWriter, r *http.Request) { return } - body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/ui/plan", []byte(raw)) + body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/plan", []byte(raw)) data := u.loadConfigUIData(dev) - data.Message = fmt.Sprintf("POST /v1/config/ui/plan -> %d", code) + data.Message = fmt.Sprintf("POST /v1/config/plan -> %d", code) data.RawText = prettyJSON(body) data.RawJSON = raw if err != nil { @@ -5152,9 +5152,9 @@ func (u *UI) actionDeviceConfigUIApply(w http.ResponseWriter, r *http.Request) { return } - body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/ui/apply", []byte(raw)) + body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/apply", []byte(raw)) data := u.loadConfigUIData(dev) - data.Message = fmt.Sprintf("POST /v1/config/ui/apply -> %d", code) + data.Message = fmt.Sprintf("POST /v1/config/apply -> %d", code) data.RawText = prettyJSON(body) data.RawJSON = raw if err != nil { @@ -5305,7 +5305,7 @@ func (u *UI) actionFaceGalleryAdd(w http.ResponseWriter, r *http.Request) { pid, _ := repo.FindOrCreatePerson(name) _ = repo.AddPhoto(pid, name+"/"+hdr.Filename) } - http.Redirect(w, r, "/ui/face-gallery?msg=ok", http.StatusFound) + http.Redirect(w, r, "/face-gallery?msg=ok", http.StatusFound) } func (u *UI) actionFaceGalleryBuild(w http.ResponseWriter, r *http.Request) { @@ -5327,7 +5327,7 @@ func (u *UI) actionFaceGalleryBuild(w http.ResponseWriter, r *http.Request) { output, err := builder.Build() if err != nil { msg := fmt.Sprintf("生成失败: %v", err) - http.Redirect(w, r, "/ui/face-gallery?msg="+url.QueryEscape(msg), http.StatusFound) + http.Redirect(w, r, "/face-gallery?msg="+url.QueryEscape(msg), http.StatusFound) return } // Parse build report for quality diagnostics. @@ -5406,7 +5406,7 @@ func (u *UI) actionFaceGalleryBuild(w http.ResponseWriter, r *http.Request) { } } - http.Redirect(w, r, "/ui/face-gallery?msg="+url.QueryEscape(msg), http.StatusFound) + http.Redirect(w, r, "/face-gallery?msg="+url.QueryEscape(msg), http.StatusFound) } func (u *UI) pageFaceGallery(w http.ResponseWriter, r *http.Request) { @@ -5470,7 +5470,7 @@ func (u *UI) actionFaceGallerySync(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, "/ui/tasks/"+url.PathEscape(task.ID), http.StatusFound) + http.Redirect(w, r, "/tasks/"+url.PathEscape(task.ID), http.StatusFound) } func (u *UI) actionFaceGalleryDelete(w http.ResponseWriter, r *http.Request) { @@ -5482,7 +5482,7 @@ func (u *UI) actionFaceGalleryDelete(w http.ResponseWriter, r *http.Request) { if strings.TrimSpace(u.dbPath) != "" { repo := storage.NewFaceGalleryRepo(u.dbPath) repo.DeletePerson(id) } - http.Redirect(w, r, "/ui/face-gallery", http.StatusFound) + http.Redirect(w, r, "/face-gallery", http.StatusFound) } @@ -5524,7 +5524,7 @@ func (u *UI) actionFaceGalleryImport(w http.ResponseWriter, r *http.Request) { } } } - http.Redirect(w, r, fmt.Sprintf("/ui/face-gallery?msg=已导入 %d 张照片", count), http.StatusFound) + http.Redirect(w, r, fmt.Sprintf("/face-gallery?msg=已导入 %d 张照片", count), http.StatusFound) } func (u *UI) rebuildFaceGallery() { @@ -5565,6 +5565,10 @@ func personNameFromPath(path string) string { func (u *UI) serveFacePhoto(w http.ResponseWriter, r *http.Request) { path := chi.URLParam(r, "*") + // URL-decode to normalize case-insensitive percent-encoding + if decoded, err := url.PathUnescape(path); err == nil { + path = decoded + } if path == "" || strings.Contains(path, "..") { http.Error(w, "invalid", http.StatusBadRequest) return @@ -5583,7 +5587,7 @@ func (u *UI) actionFaceGalleryRename(w http.ResponseWriter, r *http.Request) { if strings.TrimSpace(u.dbPath) != "" { storage.NewFaceGalleryRepo(u.dbPath).RenamePerson(id, name) } - http.Redirect(w, r, "/ui/face-gallery", http.StatusFound) + http.Redirect(w, r, "/face-gallery", http.StatusFound) } func (u *UI) actionFaceGalleryAddPhoto(w http.ResponseWriter, r *http.Request) { @@ -5594,7 +5598,7 @@ func (u *UI) actionFaceGalleryAddPhoto(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, "photo required", http.StatusBadRequest); return } defer file.Close() // Get person name for directory - if strings.TrimSpace(u.dbPath) == "" { http.Redirect(w, r, "/ui/face-gallery", http.StatusFound); return } + if strings.TrimSpace(u.dbPath) == "" { http.Redirect(w, r, "/face-gallery", http.StatusFound); return } repo := storage.NewFaceGalleryRepo(u.dbPath) persons, _ := repo.ListPersons() var personName string @@ -5608,7 +5612,7 @@ func (u *UI) actionFaceGalleryAddPhoto(w http.ResponseWriter, r *http.Request) { defer f.Close() io.Copy(f, file) repo.AddPhoto(id, personName+"/"+hdr.Filename) - http.Redirect(w, r, "/ui/face-gallery", http.StatusFound) + http.Redirect(w, r, "/face-gallery", http.StatusFound) } func (u *UI) actionFaceGalleryDeletePhoto(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(r.FormValue("id")) diff --git a/internal/web/ui/templates/alarms.html b/internal/web/ui/templates/alarms.html index a18d9ea..b9d5821 100644 --- a/internal/web/ui/templates/alarms.html +++ b/internal/web/ui/templates/alarms.html @@ -1,6 +1,6 @@ {{define "alarms"}}