aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/undo.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 01:50:17 -0400
committerhistoria <[not public]>2026-09-18 01:50:17 -0400
commit87d08f0151e643951769e323995f1c5ed89ee200 (patch)
treedc42a0b30ebb5a00dd7ba505cc4eaa44f4f308bc /internal/admin/undo.go
parent32a3c59cebf2a16277e54d715a67adfdbe00b456 (diff)
downloadthehouseoficarus-87d08f0151e643951769e323995f1c5ed89ee200.tar.gz
fix: harden economy recovery, shutdown concurrency, world validation, and inventory-loss paths
Diffstat (limited to 'internal/admin/undo.go')
-rw-r--r--internal/admin/undo.go95
1 files changed, 93 insertions, 2 deletions
diff --git a/internal/admin/undo.go b/internal/admin/undo.go
index 51610f8..1786295 100644
--- a/internal/admin/undo.go
+++ b/internal/admin/undo.go
@@ -17,6 +17,7 @@ type ExtraFile struct {
NewFilePath string `json:"new_file_path,omitempty"`
IsDelete bool `json:"is_delete,omitempty"`
IsCreate bool `json:"is_create,omitempty"`
+ IsDirDelete bool `json:"is_dir_delete,omitempty"`
}
type ChangeDesc struct {
@@ -28,6 +29,7 @@ type ChangeDesc struct {
NewContent []byte `json:"new_content"`
IsDelete bool `json:"is_delete"`
IsCreate bool `json:"is_create"`
+ IsDirDelete bool `json:"is_dir_delete,omitempty"`
ExtraFiles []ExtraFile `json:"extra_files,omitempty"`
// IDMap records a room-ID change (oldID -> newID) included in the
// change. Undo applies its inverse, redo the forward map, through the
@@ -78,11 +80,13 @@ type UndoStack struct {
}
func NewUndoStack(dataDir string) *UndoStack {
- return &UndoStack{
+ us := &UndoStack{
dataDir: dataDir,
maxSize: 100,
filePath: filepath.Join(dataDir, ".admin_history.json"),
}
+ us.load()
+ return us
}
// SetMigrateRoomIDs wires the runtime room-ID migration used by undo/redo
@@ -174,11 +178,14 @@ func (us *UndoStack) applyUndo(c ChangeDesc) error {
}
}
if c.IsDelete {
- if c.OldContent == nil {
+ if c.IsDirDelete {
if err := os.MkdirAll(c.FilePath, 0755); err != nil {
return fmt.Errorf("recreate dir %s: %w", c.FilePath, err)
}
} else {
+ if c.OldContent == nil {
+ return fmt.Errorf("restore deleted %s: no snapshot recorded", c.FilePath)
+ }
if err := os.MkdirAll(filepath.Dir(c.FilePath), 0755); err != nil {
return fmt.Errorf("mkdir: %w", err)
}
@@ -266,7 +273,14 @@ func applyUndoExtraFile(ef ExtraFile) error {
if err := os.Remove(ef.FilePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("undo remove %s: %w", ef.FilePath, err)
}
+ case ef.IsDelete && ef.IsDirDelete:
+ if err := os.MkdirAll(ef.FilePath, 0755); err != nil {
+ return fmt.Errorf("undo mkdir: %w", err)
+ }
case ef.IsDelete:
+ if ef.OldContent == nil {
+ return fmt.Errorf("undo restore %s: no snapshot recorded", ef.FilePath)
+ }
if err := os.MkdirAll(filepath.Dir(ef.FilePath), 0755); err != nil {
return fmt.Errorf("undo mkdir: %w", err)
}
@@ -354,6 +368,7 @@ func (us *UndoStack) save() {
NewContent string `json:"new_content"`
IsDelete bool `json:"is_delete,omitempty"`
IsCreate bool `json:"is_create,omitempty"`
+ IsDirDelete bool `json:"is_dir_delete,omitempty"`
}
type entry struct {
Time string `json:"time"`
@@ -364,6 +379,7 @@ func (us *UndoStack) save() {
NewContent string `json:"new_content"`
IsDelete bool `json:"is_delete"`
IsCreate bool `json:"is_create"`
+ IsDirDelete bool `json:"is_dir_delete,omitempty"`
ExtraFiles []extraEntry `json:"extra_files,omitempty"`
IDMap map[int]int `json:"id_map,omitempty"`
}
@@ -383,6 +399,7 @@ func (us *UndoStack) save() {
NewContent: string(ef.NewContent),
IsDelete: ef.IsDelete,
IsCreate: ef.IsCreate,
+ IsDirDelete: ef.IsDirDelete,
})
}
entries = append(entries, entry{
@@ -394,6 +411,7 @@ func (us *UndoStack) save() {
NewContent: string(c.NewContent),
IsDelete: c.IsDelete,
IsCreate: c.IsCreate,
+ IsDirDelete: c.IsDirDelete,
ExtraFiles: extra,
IDMap: c.IDMap,
})
@@ -414,6 +432,79 @@ func (us *UndoStack) save() {
}
}
+// load restores persisted undo/redo history written by save. A missing or
+// unreadable history file is not fatal: the stack simply starts empty. The
+// editor documents that history survives restarts, so NewUndoStack calls this.
+func (us *UndoStack) load() {
+ data, err := os.ReadFile(us.filePath)
+ if err != nil {
+ return
+ }
+ type extraEntry struct {
+ FilePath string `json:"file_path"`
+ NewFilePath string `json:"new_file_path,omitempty"`
+ OldContent string `json:"old_content"`
+ NewContent string `json:"new_content"`
+ IsDelete bool `json:"is_delete,omitempty"`
+ IsCreate bool `json:"is_create,omitempty"`
+ IsDirDelete bool `json:"is_dir_delete,omitempty"`
+ }
+ type entry struct {
+ Time string `json:"time"`
+ Description string `json:"description"`
+ FilePath string `json:"file_path"`
+ NewFilePath string `json:"new_file_path,omitempty"`
+ OldContent string `json:"old_content"`
+ NewContent string `json:"new_content"`
+ IsDelete bool `json:"is_delete"`
+ IsCreate bool `json:"is_create"`
+ IsDirDelete bool `json:"is_dir_delete,omitempty"`
+ ExtraFiles []extraEntry `json:"extra_files,omitempty"`
+ IDMap map[int]int `json:"id_map,omitempty"`
+ }
+ type saveData struct {
+ History []entry `json:"history"`
+ Redo []entry `json:"redo"`
+ }
+ var sd saveData
+ if err := json.Unmarshal(data, &sd); err != nil {
+ log.Printf("undo: failed to load history: %v", err)
+ return
+ }
+ toChanges := func(entries []entry) []ChangeDesc {
+ out := make([]ChangeDesc, 0, len(entries))
+ for _, e := range entries {
+ c := ChangeDesc{
+ Time: e.Time,
+ Description: e.Description,
+ FilePath: e.FilePath,
+ NewFilePath: e.NewFilePath,
+ OldContent: []byte(e.OldContent),
+ NewContent: []byte(e.NewContent),
+ IsDelete: e.IsDelete,
+ IsCreate: e.IsCreate,
+ IsDirDelete: e.IsDirDelete,
+ IDMap: e.IDMap,
+ }
+ for _, ef := range e.ExtraFiles {
+ c.ExtraFiles = append(c.ExtraFiles, ExtraFile{
+ FilePath: ef.FilePath,
+ NewFilePath: ef.NewFilePath,
+ OldContent: []byte(ef.OldContent),
+ NewContent: []byte(ef.NewContent),
+ IsDelete: ef.IsDelete,
+ IsCreate: ef.IsCreate,
+ IsDirDelete: ef.IsDirDelete,
+ })
+ }
+ out = append(out, c)
+ }
+ return out
+ }
+ us.history = toChanges(sd.History)
+ us.redo = toChanges(sd.Redo)
+}
+
func (us *UndoStack) Clear() {
us.mu.Lock()
defer us.mu.Unlock()