package web import ( "encoding/json" "fmt" "net/http" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" ) // updateManagerSuccessAutoCloseSeconds is the countdown shown in the // update-manager-success modal before the app closes itself: the manager // update only takes effect after a restart. const updateManagerSuccessAutoCloseSeconds = 5 func (s *Server) handleModalAPI(w http.ResponseWriter, r *http.Request) { modalType := r.URL.Query().Get(constants.QueryParamType) if modalType == "" { sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingModalType) return } var modalConfig ModalConfig switch modalType { case constants.ModalTypeShutdown: modalConfig = ModalConfig{ Title: "Close MPV.Rocks Installer", Message: "Are you sure you want to close the MPV.Rocks Installer? The server will shut down and this tab will attempt to close automatically. If it doesn't close, please close it manually.", ConfirmText: "Close App", CancelText: "Cancel", ConfirmURL: "/api/shutdown", Danger: true, } case constants.ModalTypeUpdateManager: latestVersion := r.URL.Query().Get(constants.QueryParamVersion) // Sanitize the version before interpolating it into the message: // fall back to "latest" for anything that is not a plain version string. if latestVersion == "" || ValidateVersionString(latestVersion) != nil { latestVersion = "latest" } modalConfig = ModalConfig{ Title: "Update MPV Manager", Message: fmt.Sprintf("Update MPV Manager to version %s?", latestVersion), ConfirmText: "Update Now", CancelText: "Cancel", ConfirmURL: "/api/manager/update", Danger: false, } case constants.ModalTypeUpdateManagerSuccess: modalConfig = ModalConfig{ Title: "Update Ready", Message: "The update has been downloaded and verified. MPV Manager will close automatically, then its updater will install the new version. Restart MPV Manager after it closes.", ConfirmText: "Close & Exit", CancelText: "", ConfirmURL: "/api/shutdown", Danger: false, Success: true, } case constants.ModalTypeRemoveFileAssociations: modalConfig = ModalConfig{ Title: "Remove File Associations", Message: "Are you sure you want to remove file associations?", ConfirmText: "Remove", CancelText: "Cancel", ConfirmURL: "/api/file-associations/remove", Danger: true, } case constants.ModalTypeResetConfig: modalConfig = ModalConfig{ Title: "Reset MPV Config", Message: "This will reset your config to recommended settings. A backup will be created. Continue?", ConfirmText: "Reset Config", CancelText: "Cancel", ConfirmURL: "/api/config/reset", Danger: true, } case constants.ModalTypeRestoreConfig: backupPath := r.URL.Query().Get(constants.QueryParamBackupPath) if backupPath == "" { sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingBackupPath) return } // Validate backup path if err := config.ValidateBackupPath(backupPath); err != nil { log.Error(constants.LogPrefixAPI + " Invalid backup path: " + err.Error()) sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageInvalidBackupPath) return } // Build confirm data as proper JSON (never fmt.Sprintf raw params) confirmData, err := json.Marshal(map[string]string{"backup_path": backupPath}) if err != nil { sendJSONError(w, http.StatusInternalServerError, err.Error()) return } modalConfig = ModalConfig{ Title: "Restore Config Backup", Message: "Are you sure you want to restore this configuration backup?", ConfirmText: "Restore", CancelText: "Cancel", ConfirmURL: "/api/config/restore", ConfirmData: string(confirmData), Danger: true, // HTMX partial refresh configuration HTMXRefresh: "/api/config/backups/list", HTMXTarget: "#backup-list-container", } case constants.ModalTypeDeleteBackup: backupPath := r.URL.Query().Get(constants.QueryParamBackupPath) if backupPath == "" { sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingBackupPath) return } // Validate backup path if err := config.ValidateBackupPath(backupPath); err != nil { log.Error(constants.LogPrefixAPI + " Invalid backup path: " + err.Error()) sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageInvalidBackupPath) return } // Build confirm data as proper JSON (never fmt.Sprintf raw params) confirmData, err := json.Marshal(map[string]string{"backup_path": backupPath}) if err != nil { sendJSONError(w, http.StatusInternalServerError, err.Error()) return } modalConfig = ModalConfig{ Title: "Delete Backup", Message: "Are you sure you want to delete this configuration backup?", ConfirmText: "Delete", CancelText: "Cancel", ConfirmURL: "/api/config/backup/delete", ConfirmData: string(confirmData), Danger: true, // HTMX partial refresh configuration HTMXRefresh: "/api/config/backups/list", HTMXTarget: "#backup-list-container", } case constants.ModalTypeClearLogs: modalConfig = ModalConfig{ Title: "Clear Application Log", Message: "Are you sure you want to clear the application log file? This action cannot be undone.", ConfirmText: "Clear Log", CancelText: "Cancel", ConfirmURL: "/api/logs/clear", Danger: true, // HTMX partial refresh configuration HTMXRefresh: "/api/logs", HTMXTarget: "#log-content", } default: sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageUnknownModalType) return } w.Header().Set("Content-Type", constants.ContentTypeJSON) // The update-manager-success modal carries auto_close_seconds so the web UI // counts down and fires the confirm action (shutdown) on its own. The field // is added via a wrapper so no other modal type can auto-close. if modalType == constants.ModalTypeUpdateManagerSuccess { json.NewEncoder(w).Encode(struct { ModalConfig AutoCloseSeconds int `json:"auto_close_seconds"` Dismissible bool `json:"dismissible"` }{ ModalConfig: modalConfig, AutoCloseSeconds: updateManagerSuccessAutoCloseSeconds, Dismissible: false, }) return } json.NewEncoder(w).Encode(modalConfig) }