Merge pull request #2891 from SiYue-ZO/feat/factory-reset
feat: add reset to factory defaults
This commit is contained in:
commit
33f9d63862
11 changed files with 225 additions and 27 deletions
57
cmd/picoclaw/internal/config/command.go
Normal file
57
cmd/picoclaw/internal/config/command.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConfigCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage configuration",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newResetCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResetCommand() *cobra.Command {
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reset",
|
||||||
|
Short: "Reset configuration to factory defaults",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Example: ` picoclaw config reset
|
||||||
|
picoclaw config reset --force`,
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
if !force {
|
||||||
|
fmt.Print("Reset config to factory defaults? API keys will be preserved. (y/n): ")
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if err := config.ResetToDefaults(configPath); err != nil {
|
||||||
|
return fmt.Errorf("reset failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Configuration has been reset to factory defaults.")
|
||||||
|
fmt.Println("A backup of the previous config was created in the same directory.")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&force, "force", "f", false,
|
||||||
|
"Skip confirmation prompt")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
|
configcmd "github.com/sipeed/picoclaw/cmd/picoclaw/internal/config"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/mcp"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/mcp"
|
||||||
|
|
@ -82,6 +83,7 @@ picoclaw --no-color status`,
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
configcmd.NewConfigCommand(),
|
||||||
onboard.NewOnboardCommand(),
|
onboard.NewOnboardCommand(),
|
||||||
agent.NewAgentCommand(),
|
agent.NewAgentCommand(),
|
||||||
auth.NewAuthCommand(),
|
auth.NewAuthCommand(),
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ func TestNewPicoclawCommand(t *testing.T) {
|
||||||
allowedCommands := []string{
|
allowedCommands := []string{
|
||||||
"agent",
|
"agent",
|
||||||
"auth",
|
"auth",
|
||||||
|
"config",
|
||||||
"cron",
|
"cron",
|
||||||
"gateway",
|
"gateway",
|
||||||
"mcp",
|
"mcp",
|
||||||
|
|
|
||||||
|
|
@ -1266,7 +1266,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = makeBackup(path)
|
err = MakeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1320,7 +1320,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = makeBackup(path)
|
err = MakeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1372,7 +1372,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = makeBackup(path)
|
err = MakeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1523,7 +1523,7 @@ func applySkillsRegistryEnvCompat(cfg *Config) {
|
||||||
cfg.Tools.Skills.Registries.Set("github", githubCfg)
|
cfg.Tools.Skills.Registries.Set("github", githubCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeBackup(path string) error {
|
func MakeBackup(path string) error {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -1651,6 +1651,19 @@ func (c *Config) SecurityCopyFrom(path string) error {
|
||||||
return loadSecurityConfig(c, securityPath(path))
|
return loadSecurityConfig(c, securityPath(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetToDefaults backs up the current config, creates a default config,
|
||||||
|
// preserves security credentials from the existing config, and saves it.
|
||||||
|
func ResetToDefaults(configPath string) error {
|
||||||
|
if err := MakeBackup(configPath); err != nil {
|
||||||
|
return fmt.Errorf("backup before reset: %w", err)
|
||||||
|
}
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if err := cfg.SecurityCopyFrom(configPath); err != nil {
|
||||||
|
logger.WarnF("could not preserve security config", map[string]any{"error": err})
|
||||||
|
}
|
||||||
|
return SaveConfig(configPath, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
|
func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
|
||||||
var expanded []*ModelConfig
|
var expanded []*ModelConfig
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2681,7 +2681,7 @@ func TestFilterSensitiveData_AllTokenTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// makeBackup tests
|
// MakeBackup tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// TestMakeBackup_WithDateSuffix verifies backup files include a date suffix.
|
// TestMakeBackup_WithDateSuffix verifies backup files include a date suffix.
|
||||||
|
|
@ -2692,8 +2692,8 @@ func TestMakeBackup_WithDateSuffix(t *testing.T) {
|
||||||
t.Fatalf("WriteFile: %v", err)
|
t.Fatalf("WriteFile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
|
|
@ -2732,8 +2732,8 @@ func TestMakeBackup_AlsoBacksSecurityFile(t *testing.T) {
|
||||||
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
||||||
os.WriteFile(secPath, []byte(`model_list:\n test:0:\n api_keys:\n - "sk-test"\n`), 0o600)
|
os.WriteFile(secPath, []byte(`model_list:\n test:0:\n api_keys:\n - "sk-test"\n`), 0o600)
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
|
|
@ -2759,14 +2759,14 @@ func TestMakeBackup_AlsoBacksSecurityFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMakeBackup_NonexistentFileSkipsBackup verifies that makeBackup returns nil
|
// TestMakeBackup_NonexistentFileSkipsBackup verifies that MakeBackup returns nil
|
||||||
// when the config file does not exist (no error, no panic).
|
// when the config file does not exist (no error, no panic).
|
||||||
func TestMakeBackup_NonexistentFileSkipsBackup(t *testing.T) {
|
func TestMakeBackup_NonexistentFileSkipsBackup(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
configPath := filepath.Join(dir, "nonexistent.json")
|
configPath := filepath.Join(dir, "nonexistent.json")
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup on nonexistent file should return nil, got: %v", err)
|
t.Fatalf("MakeBackup on nonexistent file should return nil, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2777,8 +2777,8 @@ func TestMakeBackup_OnlyConfigNoSecurity(t *testing.T) {
|
||||||
configPath := filepath.Join(dir, "config.json")
|
configPath := filepath.Join(dir, "config.json")
|
||||||
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, _ := os.ReadDir(dir)
|
entries, _ := os.ReadDir(dir)
|
||||||
|
|
@ -2801,7 +2801,7 @@ func TestMakeBackup_OnlyConfigNoSecurity(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMakeBackup_SameDateSuffix verifies that config and security backups
|
// TestMakeBackup_SameDateSuffix verifies that config and security backups
|
||||||
// share the same date suffix (they are created in the same makeBackup call).
|
// share the same date suffix (they are created in the same MakeBackup call).
|
||||||
func TestMakeBackup_SameDateSuffix(t *testing.T) {
|
func TestMakeBackup_SameDateSuffix(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
configPath := filepath.Join(dir, "config.json")
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
|
@ -2810,8 +2810,8 @@ func TestMakeBackup_SameDateSuffix(t *testing.T) {
|
||||||
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
||||||
os.WriteFile(secPath, []byte(`key: value`), 0o600)
|
os.WriteFile(secPath, []byte(`key: value`), 0o600)
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, _ := os.ReadDir(dir)
|
entries, _ := os.ReadDir(dir)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ func (h *Handler) registerConfigRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("GET /api/config", h.handleGetConfig)
|
mux.HandleFunc("GET /api/config", h.handleGetConfig)
|
||||||
mux.HandleFunc("PUT /api/config", h.handleUpdateConfig)
|
mux.HandleFunc("PUT /api/config", h.handleUpdateConfig)
|
||||||
mux.HandleFunc("PATCH /api/config", h.handlePatchConfig)
|
mux.HandleFunc("PATCH /api/config", h.handlePatchConfig)
|
||||||
|
mux.HandleFunc("POST /api/config/reset", h.handleResetConfig)
|
||||||
mux.HandleFunc("POST /api/config/test-command-patterns", h.handleTestCommandPatterns)
|
mux.HandleFunc("POST /api/config/test-command-patterns", h.handleTestCommandPatterns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +212,32 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleResetConfig resets the configuration to factory defaults.
|
||||||
|
// API keys and security credentials are preserved.
|
||||||
|
//
|
||||||
|
// POST /api/config/reset
|
||||||
|
func (h *Handler) handleResetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := config.ResetToDefaults(h.configPath); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to reset config: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.applyRuntimeLogLevel()
|
||||||
|
logger.Infof("configuration reset to factory defaults")
|
||||||
|
|
||||||
|
// Restart gateway if running
|
||||||
|
status := h.gatewayStatusData()
|
||||||
|
gatewayStatus, _ := status["gateway_status"].(string)
|
||||||
|
if gatewayStatus == "running" {
|
||||||
|
if _, err := h.RestartGateway(); err != nil {
|
||||||
|
logger.ErrorF("failed to restart gateway after config reset", map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
// handleTestCommandPatterns tests a command against whitelist and blacklist patterns.
|
// handleTestCommandPatterns tests a command against whitelist and blacklist patterns.
|
||||||
//
|
//
|
||||||
// POST /api/config/test-command-patterns
|
// POST /api/config/test-command-patterns
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,12 @@ export async function patchAppConfig(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetAppConfig(): Promise<ConfigActionResponse> {
|
||||||
|
return request<ConfigActionResponse>("/api/config/reset", {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// WeChat QR login flow API
|
// WeChat QR login flow API
|
||||||
|
|
||||||
export interface WeixinFlowResponse {
|
export interface WeixinFlowResponse {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { patchAppConfig } from "@/api/channels"
|
import { patchAppConfig, resetAppConfig } from "@/api/channels"
|
||||||
import { launcherFetch } from "@/api/http"
|
import { launcherFetch } from "@/api/http"
|
||||||
import { postLauncherDashboardSetup } from "@/api/launcher-auth"
|
import { postLauncherDashboardSetup } from "@/api/launcher-auth"
|
||||||
import {
|
import {
|
||||||
|
|
@ -41,6 +41,17 @@ import {
|
||||||
} from "@/components/config/form-model"
|
} from "@/components/config/form-model"
|
||||||
import { PageHeader } from "@/components/page-header"
|
import { PageHeader } from "@/components/page-header"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
|
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
|
||||||
import { refreshGatewayState } from "@/store/gateway"
|
import { refreshGatewayState } from "@/store/gateway"
|
||||||
|
|
@ -72,15 +83,24 @@ export function ConfigPage() {
|
||||||
const [autoStartEnabled, setAutoStartEnabled] = useState(false)
|
const [autoStartEnabled, setAutoStartEnabled] = useState(false)
|
||||||
const [autoStartBaseline, setAutoStartBaseline] = useState(false)
|
const [autoStartBaseline, setAutoStartBaseline] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [showFactoryResetDialog, setShowFactoryResetDialog] = useState(false)
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["config"],
|
queryKey: ["config"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await launcherFetch("/api/config")
|
const controller = new AbortController()
|
||||||
if (!res.ok) {
|
const timer = setTimeout(() => controller.abort(), 5000)
|
||||||
throw new Error("Failed to load config")
|
try {
|
||||||
|
const res = await launcherFetch("/api/config", {
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Failed to load config")
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
}
|
}
|
||||||
return res.json()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -208,6 +228,27 @@ export function ConfigPage() {
|
||||||
toast.info(t("pages.config.reset_success"))
|
toast.info(t("pages.config.reset_success"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleFactoryReset = async () => {
|
||||||
|
try {
|
||||||
|
await resetAppConfig()
|
||||||
|
const fresh = await launcherFetch("/api/config").then((r) => r.json())
|
||||||
|
const parsed = buildFormFromConfig(fresh)
|
||||||
|
setForm(parsed)
|
||||||
|
setBaseline(parsed)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["config"] })
|
||||||
|
await refreshGatewayState()
|
||||||
|
toast.success(t("pages.config.factory_reset_success"))
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: t("pages.config.factory_reset_error"),
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setShowFactoryResetDialog(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
@ -625,8 +666,38 @@ export function ConfigPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const factoryResetButton = (
|
||||||
|
<AlertDialog
|
||||||
|
open={showFactoryResetDialog}
|
||||||
|
onOpenChange={setShowFactoryResetDialog}
|
||||||
|
>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" disabled={saving}>
|
||||||
|
{t("pages.config.factory_reset")}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("pages.config.factory_reset_confirm_title")}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("pages.config.factory_reset_confirm_desc")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleFactoryReset}>
|
||||||
|
{t("pages.config.factory_reset_confirm")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
|
||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
|
{factoryResetButton}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
|
|
@ -672,8 +743,11 @@ export function ConfigPage() {
|
||||||
{t("labels.loading")}
|
{t("labels.loading")}
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-destructive py-6 text-sm">
|
<div className="space-y-4">
|
||||||
{t("pages.config.load_error")}
|
<div className="text-destructive py-6 text-sm">
|
||||||
|
{t("pages.config.load_error")}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">{factoryResetButton}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -901,7 +901,13 @@
|
||||||
"format_success": "JSON formatted successfully.",
|
"format_success": "JSON formatted successfully.",
|
||||||
"format_error": "Invalid JSON format.",
|
"format_error": "Invalid JSON format.",
|
||||||
"format": "Format",
|
"format": "Format",
|
||||||
"unsaved_changes": "You have unsaved changes."
|
"unsaved_changes": "You have unsaved changes.",
|
||||||
|
"factory_reset": "Factory Reset",
|
||||||
|
"factory_reset_confirm_title": "Reset to Factory Defaults",
|
||||||
|
"factory_reset_confirm_desc": "This will reset all configuration to factory defaults. API keys and security credentials will be preserved. A backup of the current config will be created.",
|
||||||
|
"factory_reset_confirm": "Reset to Defaults",
|
||||||
|
"factory_reset_success": "Configuration has been reset to factory defaults.",
|
||||||
|
"factory_reset_error": "Failed to reset configuration."
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"log_level_error": "Failed to update log level.",
|
"log_level_error": "Failed to update log level.",
|
||||||
|
|
|
||||||
|
|
@ -844,7 +844,13 @@
|
||||||
"format_success": "JSON formatado com sucesso.",
|
"format_success": "JSON formatado com sucesso.",
|
||||||
"format_error": "Formato JSON inválido.",
|
"format_error": "Formato JSON inválido.",
|
||||||
"format": "Formatar",
|
"format": "Formatar",
|
||||||
"unsaved_changes": "Você tem alterações não salvas."
|
"unsaved_changes": "Você tem alterações não salvas.",
|
||||||
|
"factory_reset": "Restaurar Padrões",
|
||||||
|
"factory_reset_confirm_title": "Restaurar Configurações de Fábrica",
|
||||||
|
"factory_reset_confirm_desc": "Isso redefinirá todas as configurações para os padrões de fábrica. As chaves de API e credenciais de segurança serão preservadas. Um backup da configuração atual será criado.",
|
||||||
|
"factory_reset_confirm": "Redefinir para Padrões",
|
||||||
|
"factory_reset_success": "A configuração foi redefinida para os padrões de fábrica.",
|
||||||
|
"factory_reset_error": "Falha ao redefinir a configuração."
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"log_level_error": "Falha ao atualizar nível de log.",
|
"log_level_error": "Falha ao atualizar nível de log.",
|
||||||
|
|
|
||||||
|
|
@ -901,7 +901,13 @@
|
||||||
"format_success": "JSON 格式化成功",
|
"format_success": "JSON 格式化成功",
|
||||||
"format_error": "JSON 格式无效",
|
"format_error": "JSON 格式无效",
|
||||||
"format": "格式化",
|
"format": "格式化",
|
||||||
"unsaved_changes": "您有未保存的更改"
|
"unsaved_changes": "您有未保存的更改",
|
||||||
|
"factory_reset": "恢复出厂设置",
|
||||||
|
"factory_reset_confirm_title": "恢复出厂设置",
|
||||||
|
"factory_reset_confirm_desc": "这将把所有配置重置为出厂默认值。API 密钥和安全凭证将被保留。当前配置将被备份。",
|
||||||
|
"factory_reset_confirm": "确认重置",
|
||||||
|
"factory_reset_success": "配置已重置为出厂默认值。",
|
||||||
|
"factory_reset_error": "重置配置失败。"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"log_level_error": "更新日志等级失败。",
|
"log_level_error": "更新日志等级失败。",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue