aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/undo.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-29 22:31:11 -0400
committerhistoria <[not public]>2026-06-29 22:31:11 -0400
commit069d3c1c81d71042706372df153254487a765dfc (patch)
treed83a4a3954d2b8304f49d83e365f4c2b075b91eb /internal/admin/undo.go
parent6c5271fb085b4571a10f106391974c249cd84317 (diff)
downloadthehouseoficarus-069d3c1c81d71042706372df153254487a765dfc.tar.gz
feat: wip web admin for mapping and crud
Diffstat (limited to 'internal/admin/undo.go')
-rw-r--r--internal/admin/undo.go247
1 files changed, 247 insertions, 0 deletions
diff --git a/internal/admin/undo.go b/internal/admin/undo.go
new file mode 100644
index 0000000..39e8c4e
--- /dev/null
+++ b/internal/admin/undo.go
@@ -0,0 +1,247 @@
+package admin
+
+import (
+ "encoding/json"
+ "log"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+type ChangeDesc struct {
+ Time string `json:"time"`
+ Description string `json:"description"`
+ FilePath string `json:"file_path"`
+ NewFilePath string `json:"new_file_path,omitempty"`
+ OldContent []byte `json:"old_content"`
+ NewContent []byte `json:"new_content"`
+ IsDelete bool `json:"is_delete"`
+ IsCreate bool `json:"is_create"`
+}
+
+type UndoInfo struct {
+ CanUndo bool `json:"can_undo"`
+ CanRedo bool `json:"can_redo"`
+ UndoDesc string `json:"undo_desc"`
+ RedoDesc string `json:"redo_desc"`
+ StackSize int `json:"stack_size"`
+ RedoSize int `json:"redo_size"`
+}
+
+type UndoStack struct {
+ mu sync.Mutex
+ dataDir string
+ history []ChangeDesc
+ redo []ChangeDesc
+ maxSize int
+ filePath string
+}
+
+func NewUndoStack(dataDir string) *UndoStack {
+ us := &UndoStack{
+ dataDir: dataDir,
+ maxSize: 100,
+ filePath: filepath.Join(dataDir, ".admin_history.yaml"),
+ }
+ us.load()
+ return us
+}
+
+func (us *UndoStack) Push(desc ChangeDesc) {
+ us.mu.Lock()
+ defer us.mu.Unlock()
+ desc.Time = time.Now().Format(time.RFC3339)
+ us.history = append(us.history, desc)
+ us.redo = nil
+ if len(us.history) > us.maxSize {
+ us.history = us.history[len(us.history)-us.maxSize:]
+ }
+ us.save()
+}
+
+func (us *UndoStack) Undo() *ChangeDesc {
+ us.mu.Lock()
+ defer us.mu.Unlock()
+ if len(us.history) == 0 {
+ return nil
+ }
+ last := us.history[len(us.history)-1]
+ us.history = us.history[:len(us.history)-1]
+
+ if last.NewFilePath != "" {
+ os.Remove(last.NewFilePath)
+ }
+ if last.IsDelete {
+ if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil {
+ log.Printf("undo: failed to restore deleted file %s: %v", last.FilePath, err)
+ return nil
+ }
+ } else if last.IsCreate {
+ os.Remove(last.FilePath)
+ } else {
+ if last.NewFilePath != "" {
+ if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil {
+ log.Printf("undo: failed to revert move %s: %v", last.FilePath, err)
+ return nil
+ }
+ } else {
+ if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil {
+ log.Printf("undo: failed to revert file %s: %v", last.FilePath, err)
+ return nil
+ }
+ }
+ }
+
+ us.redo = append(us.redo, last)
+ us.save()
+ return &last
+}
+
+func (us *UndoStack) Redo() *ChangeDesc {
+ us.mu.Lock()
+ defer us.mu.Unlock()
+ if len(us.redo) == 0 {
+ return nil
+ }
+ last := us.redo[len(us.redo)-1]
+ us.redo = us.redo[:len(us.redo)-1]
+
+ if last.NewFilePath != "" {
+ os.Remove(last.FilePath)
+ }
+ if last.IsCreate {
+ if last.NewFilePath != "" {
+ if err := os.WriteFile(last.NewFilePath, last.NewContent, 0644); err != nil {
+ log.Printf("redo: failed to recreate file %s: %v", last.NewFilePath, err)
+ return nil
+ }
+ } else {
+ if err := os.WriteFile(last.FilePath, last.NewContent, 0644); err != nil {
+ log.Printf("redo: failed to recreate file %s: %v", last.FilePath, err)
+ return nil
+ }
+ }
+ } else if last.IsDelete {
+ os.Remove(last.FilePath)
+ } else {
+ if last.NewFilePath != "" {
+ if err := os.WriteFile(last.NewFilePath, last.NewContent, 0644); err != nil {
+ log.Printf("redo: failed to reapply move %s: %v", last.NewFilePath, err)
+ return nil
+ }
+ } else {
+ if err := os.WriteFile(last.FilePath, last.NewContent, 0644); err != nil {
+ log.Printf("redo: failed to reapply file %s: %v", last.FilePath, err)
+ return nil
+ }
+ }
+ }
+
+ us.history = append(us.history, last)
+ us.save()
+ return &last
+}
+
+func (us *UndoStack) Info() UndoInfo {
+ us.mu.Lock()
+ defer us.mu.Unlock()
+ info := UndoInfo{
+ StackSize: len(us.history),
+ RedoSize: len(us.redo),
+ }
+ if len(us.history) > 0 {
+ info.CanUndo = true
+ info.UndoDesc = us.history[len(us.history)-1].Description
+ }
+ if len(us.redo) > 0 {
+ info.CanRedo = true
+ info.RedoDesc = us.redo[len(us.redo)-1].Description
+ }
+ return info
+}
+
+func (us *UndoStack) save() {
+ 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"`
+ }
+ type saveData struct {
+ History []entry `json:"history"`
+ Redo []entry `json:"redo"`
+ }
+ toEntries := func(changes []ChangeDesc) []entry {
+ var entries []entry
+ for _, c := range changes {
+ entries = append(entries, entry{
+ Time: c.Time,
+ Description: c.Description,
+ FilePath: c.FilePath,
+ NewFilePath: c.NewFilePath,
+ OldContent: string(c.OldContent),
+ NewContent: string(c.NewContent),
+ IsDelete: c.IsDelete,
+ IsCreate: c.IsCreate,
+ })
+ }
+ return entries
+ }
+ data := saveData{
+ History: toEntries(us.history),
+ Redo: toEntries(us.redo),
+ }
+ b, err := json.Marshal(data)
+ if err != nil {
+ log.Printf("undo: failed to marshal history: %v", err)
+ return
+ }
+ os.WriteFile(us.filePath, b, 0644)
+}
+
+func (us *UndoStack) load() {
+ data, err := os.ReadFile(us.filePath)
+ if err != nil {
+ return
+ }
+ 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"`
+ }
+ var saveData struct {
+ History []entry `json:"history"`
+ Redo []entry `json:"redo"`
+ }
+ if err := json.Unmarshal(data, &saveData); err != nil {
+ return
+ }
+ fromEntries := func(entries []entry) []ChangeDesc {
+ var changes []ChangeDesc
+ for _, e := range entries {
+ changes = append(changes, 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,
+ })
+ }
+ return changes
+ }
+ us.history = fromEntries(saveData.History)
+ us.redo = fromEntries(saveData.Redo)
+}