aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_drops.go10
-rw-r--r--internal/admin/api_files.go38
-rw-r--r--internal/admin/api_hazards.go10
-rw-r--r--internal/admin/api_items.go8
-rw-r--r--internal/admin/api_items_undo_test.go2
-rw-r--r--internal/admin/api_map.go6
-rw-r--r--internal/admin/api_mobs.go10
-rw-r--r--internal/admin/api_modules.go22
-rw-r--r--internal/admin/api_objects.go8
-rw-r--r--internal/admin/api_players.go10
-rw-r--r--internal/admin/api_room_courses.go40
-rw-r--r--internal/admin/api_room_insert_remove.go6
-rw-r--r--internal/admin/api_room_insert_remove_test.go49
-rw-r--r--internal/admin/api_rooms.go112
-rw-r--r--internal/admin/api_rooms_bulk.go26
-rw-r--r--internal/admin/api_rooms_bulk_delete.go14
-rw-r--r--internal/admin/api_rooms_bulk_delete_test.go8
-rw-r--r--internal/admin/api_search.go2
-rw-r--r--internal/admin/api_techs.go22
-rw-r--r--internal/admin/api_triggers.go30
-rw-r--r--internal/admin/id_alloc.go4
-rw-r--r--internal/admin/server.go31
-rw-r--r--internal/admin/xterm_to_css.go27
23 files changed, 234 insertions, 261 deletions
diff --git a/internal/admin/api_drops.go b/internal/admin/api_drops.go
index 3724a6c..4cb0ae9 100644
--- a/internal/admin/api_drops.go
+++ b/internal/admin/api_drops.go
@@ -15,7 +15,7 @@ func (s *AdminServer) handleDrops(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "drops")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -44,17 +44,17 @@ func (s *AdminServer) handleDropByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getDrop(w, r, id)
+ s.getDrop(w, id)
case http.MethodPut:
s.updateDrop(w, r, id)
case http.MethodDelete:
- s.deleteDrop(w, r, id)
+ s.deleteDrop(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
-func (s *AdminServer) getDrop(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getDrop(w http.ResponseWriter, id string) {
data, path, err := readYAMLFile(s.dataDir, "drops", id)
if err != nil {
http.Error(w, `{"error":"drop not found"}`, http.StatusNotFound)
@@ -108,7 +108,7 @@ func (s *AdminServer) updateDrop(w http.ResponseWriter, r *http.Request, id stri
writeJSON(w, m)
}
-func (s *AdminServer) deleteDrop(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteDrop(w http.ResponseWriter, id string) {
_, path, err := readYAMLFile(s.dataDir, "drops", id)
if err != nil {
http.Error(w, `{"error":"drop not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_files.go b/internal/admin/api_files.go
index ea75249..d32bda4 100644
--- a/internal/admin/api_files.go
+++ b/internal/admin/api_files.go
@@ -57,18 +57,18 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) {
NewName string `json:"new_name"`
}
if err := readJSON(r, &body); err != nil || body.Path == "" || body.NewName == "" {
- writeJSON(w, map[string]any{"error": "missing path or new_name"})
+ failJSON(w, "missing path or new_name")
return
}
newName := strings.TrimSpace(body.NewName)
if newName == "" || newName == "." || newName == ".." || strings.ContainsAny(newName, "/\\") {
- writeJSON(w, map[string]any{"error": "invalid file name"})
+ failJSON(w, "invalid file name")
return
}
for _, c := range newName {
if c < 32 || c == 127 {
- writeJSON(w, map[string]any{"error": "file name contains invalid characters"})
+ failJSON(w, "file name contains invalid characters")
return
}
}
@@ -77,28 +77,28 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) {
relPath = strings.TrimPrefix(relPath, "data/")
oldAbs, err := resolveDataPath(s.dataDir, relPath)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
oldContent, err := snapshotFile(oldAbs)
if err != nil {
- writeJSON(w, map[string]any{"error": "file not found: " + relPath})
+ failJSON(w, "file not found: "+relPath)
return
}
newAbs := filepath.Join(filepath.Dir(oldAbs), newName)
if _, err := os.Stat(newAbs); err == nil {
- writeJSON(w, map[string]any{"error": "a file named " + newName + " already exists"})
+ failJSON(w, "a file named "+newName+" already exists")
return
}
if err := os.WriteFile(newAbs, oldContent, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
if err := os.Remove(oldAbs); err != nil {
- writeJSON(w, map[string]any{"error": "remove error: " + err.Error()})
+ failJSON(w, "remove error: "+err.Error())
return
}
@@ -122,7 +122,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
relPath = strings.TrimPrefix(relPath, "data/")
fullPath, pathErr := resolveDataPath(s.dataDir, relPath)
if pathErr != nil {
- writeJSON(w, map[string]any{"error": pathErr.Error()})
+ failJSON(w, pathErr.Error())
return
}
@@ -131,7 +131,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
if relPath == "" {
entries, err := os.ReadDir(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
var files []map[string]any
@@ -148,14 +148,14 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
fullPath := filepath.Join(s.dataDir, relPath)
info, err := os.Stat(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + relPath})
+ failJSON(w, "not found: "+relPath)
return
}
if info.IsDir() {
entries, err := os.ReadDir(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
var files []map[string]any
@@ -171,7 +171,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
data, err := os.ReadFile(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "read error: " + err.Error()})
+ failJSON(w, "read error: "+err.Error())
return
}
writeJSON(w, map[string]any{
@@ -182,12 +182,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
case http.MethodPut, http.MethodPost:
if relPath == "" {
- writeJSON(w, map[string]any{"error": "missing path"})
+ failJSON(w, "missing path")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
- writeJSON(w, map[string]any{"error": "read body: " + err.Error()})
+ failJSON(w, "read body: "+err.Error())
return
}
@@ -198,12 +198,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
- writeJSON(w, map[string]any{"error": "mkdir: " + err.Error()})
+ failJSON(w, "mkdir: "+err.Error())
return
}
if err := os.WriteFile(fullPath, body, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -223,12 +223,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
if relPath == "" {
- writeJSON(w, map[string]any{"error": "missing path"})
+ failJSON(w, "missing path")
return
}
oldContent, _ := snapshotFile(fullPath)
if err := os.Remove(fullPath); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.evictDeletedRoomFile(relPath)
diff --git a/internal/admin/api_hazards.go b/internal/admin/api_hazards.go
index fac68c4..89618e7 100644
--- a/internal/admin/api_hazards.go
+++ b/internal/admin/api_hazards.go
@@ -13,7 +13,7 @@ func (s *AdminServer) handleHazards(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "hazards")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -42,17 +42,17 @@ func (s *AdminServer) handleHazardByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getHazard(w, r, id)
+ s.getHazard(w, id)
case http.MethodPut:
s.updateHazard(w, r, id)
case http.MethodDelete:
- s.deleteHazard(w, r, id)
+ s.deleteHazard(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
-func (s *AdminServer) getHazard(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getHazard(w http.ResponseWriter, id string) {
data, path, err := readYAMLFile(s.dataDir, "hazards", id)
if err != nil {
http.Error(w, `{"error":"hazard not found"}`, http.StatusNotFound)
@@ -106,7 +106,7 @@ func (s *AdminServer) updateHazard(w http.ResponseWriter, r *http.Request, id st
writeJSON(w, m)
}
-func (s *AdminServer) deleteHazard(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteHazard(w http.ResponseWriter, id string) {
_, path, err := readYAMLFile(s.dataDir, "hazards", id)
if err != nil {
http.Error(w, `{"error":"hazard not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_items.go b/internal/admin/api_items.go
index 749e8ff..8798543 100644
--- a/internal/admin/api_items.go
+++ b/internal/admin/api_items.go
@@ -40,11 +40,11 @@ func (s *AdminServer) handleItemByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getItem(w, r, id)
+ s.getItem(w, id)
case http.MethodPut:
s.updateItem(w, r, id)
case http.MethodDelete:
- s.deleteItem(w, r, id)
+ s.deleteItem(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
@@ -66,7 +66,7 @@ func (s *AdminServer) findItemFile(id string) ([]byte, string, error) {
return findYAMLFileInSubdirs(s.dataDir, "items", id)
}
-func (s *AdminServer) getItem(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getItem(w http.ResponseWriter, id string) {
data, path, err := s.findItemFile(id)
if err != nil {
http.Error(w, `{"error":"item not found"}`, http.StatusNotFound)
@@ -120,7 +120,7 @@ func (s *AdminServer) updateItem(w http.ResponseWriter, r *http.Request, id stri
writeJSON(w, m)
}
-func (s *AdminServer) deleteItem(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteItem(w http.ResponseWriter, id string) {
_, path, err := s.findItemFile(id)
if err != nil {
http.Error(w, `{"error":"item not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_items_undo_test.go b/internal/admin/api_items_undo_test.go
index 3ccbe10..d554bc6 100644
--- a/internal/admin/api_items_undo_test.go
+++ b/internal/admin/api_items_undo_test.go
@@ -31,7 +31,7 @@ func TestAccountAPIRedactsMultilinePasswordHash(t *testing.T) {
s := &AdminServer{accountStore: player.NewAccountStore(dir)}
rr := httptest.NewRecorder()
- s.getAccount(rr, httptest.NewRequest("GET", "/api/players/account/alice", nil), "alice")
+ s.getAccount(rr, "alice")
if strings.Contains(rr.Body.String(), "sensitive-hash-sentinel") {
t.Fatalf("password hash leaked in response: %s", rr.Body.String())
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 5b0464d..5eac385 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -463,10 +463,6 @@ func (s *AdminServer) handleNextRoomID(w http.ResponseWriter, r *http.Request) {
if fromStr != "" {
fromID, _ = strconv.Atoi(fromStr)
}
- id, _, err := s.nextRoomID(fromID)
- if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
- return
- }
+ id, _ := s.nextRoomID(fromID)
writeJSON(w, map[string]any{"id": id})
}
diff --git a/internal/admin/api_mobs.go b/internal/admin/api_mobs.go
index 8531e40..c3e47bb 100644
--- a/internal/admin/api_mobs.go
+++ b/internal/admin/api_mobs.go
@@ -13,7 +13,7 @@ func (s *AdminServer) handleMobs(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "mobs")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -42,11 +42,11 @@ func (s *AdminServer) handleMobByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getMob(w, r, id)
+ s.getMob(w, id)
case http.MethodPut:
s.updateMob(w, r, id)
case http.MethodDelete:
- s.deleteMob(w, r, id)
+ s.deleteMob(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
@@ -60,7 +60,7 @@ func (s *AdminServer) findMobFile(id string) ([]byte, string, error) {
return findYAMLFileInSubdirs(s.dataDir, "mobs", id)
}
-func (s *AdminServer) getMob(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getMob(w http.ResponseWriter, id string) {
data, path, err := s.findMobFile(id)
if err != nil {
http.Error(w, `{"error":"mob not found"}`, http.StatusNotFound)
@@ -110,7 +110,7 @@ func (s *AdminServer) updateMob(w http.ResponseWriter, r *http.Request, id strin
writeJSON(w, m)
}
-func (s *AdminServer) deleteMob(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteMob(w http.ResponseWriter, id string) {
_, path, err := s.findMobFile(id)
if err != nil {
http.Error(w, `{"error":"mob not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_modules.go b/internal/admin/api_modules.go
index 074eeea..1d8c25b 100644
--- a/internal/admin/api_modules.go
+++ b/internal/admin/api_modules.go
@@ -12,7 +12,7 @@ func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "modules")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -22,23 +22,23 @@ func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, ok := m["id"].(string)
if !ok || strings.TrimSpace(id) == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
delete(m, "id")
path := filepath.Join(s.dataDir, "modules", id+".yaml")
if _, err := os.Stat(path); err == nil {
- writeJSON(w, map[string]any{"error": "module already exists"})
+ failJSON(w, "module already exists")
return
}
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -68,12 +68,12 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
data, path, err := findYAMLFileInSubdirs(s.dataDir, "modules", id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
raw, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ failJSON(w, "parse error: "+err.Error())
return
}
raw["id"] = id
@@ -84,7 +84,7 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPost, http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -99,7 +99,7 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -120,12 +120,12 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
_, path, err := findYAMLFileInSubdirs(s.dataDir, "modules", id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
oldContent, _ := snapshotFile(path)
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
diff --git a/internal/admin/api_objects.go b/internal/admin/api_objects.go
index 8f9eac5..4e1e621 100644
--- a/internal/admin/api_objects.go
+++ b/internal/admin/api_objects.go
@@ -40,11 +40,11 @@ func (s *AdminServer) handleObjectByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getObject(w, r, id)
+ s.getObject(w, id)
case http.MethodPut:
s.updateObject(w, r, id)
case http.MethodDelete:
- s.deleteObject(w, r, id)
+ s.deleteObject(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
@@ -58,7 +58,7 @@ func (s *AdminServer) findObjectFile(id string) ([]byte, string, error) {
return findYAMLFileInSubdirs(s.dataDir, "objects", id)
}
-func (s *AdminServer) getObject(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getObject(w http.ResponseWriter, id string) {
data, path, err := s.findObjectFile(id)
if err != nil {
http.Error(w, `{"error":"object not found"}`, http.StatusNotFound)
@@ -111,7 +111,7 @@ func (s *AdminServer) updateObject(w http.ResponseWriter, r *http.Request, id st
writeJSON(w, m)
}
-func (s *AdminServer) deleteObject(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteObject(w http.ResponseWriter, id string) {
_, path, err := s.findObjectFile(id)
if err != nil {
http.Error(w, `{"error":"object not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_players.go b/internal/admin/api_players.go
index f0bbe31..66142c5 100644
--- a/internal/admin/api_players.go
+++ b/internal/admin/api_players.go
@@ -21,7 +21,7 @@ func (s *AdminServer) handlePlayers(w http.ResponseWriter, r *http.Request) {
charsDir := filepath.Join(s.dataDir, "players", "characters")
entries, err := os.ReadDir(charsDir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
@@ -87,14 +87,14 @@ func (s *AdminServer) handlePlayerByID(w http.ResponseWriter, r *http.Request) {
}
if strings.HasPrefix(path, "account/") {
- s.getAccount(w, r, strings.TrimPrefix(path, "account/"))
+ s.getAccount(w, strings.TrimPrefix(path, "account/"))
return
}
- s.getCharacter(w, r, path)
+ s.getCharacter(w, path)
}
-func (s *AdminServer) getCharacter(w http.ResponseWriter, r *http.Request, name string) {
+func (s *AdminServer) getCharacter(w http.ResponseWriter, name string) {
canonical, err := s.accountStore.FindCharacter(name)
if err != nil {
writeJSONError(w, "character not found: "+name, http.StatusNotFound)
@@ -136,7 +136,7 @@ func (s *AdminServer) getCharacter(w http.ResponseWriter, r *http.Request, name
writeJSON(w, m)
}
-func (s *AdminServer) getAccount(w http.ResponseWriter, r *http.Request, name string) {
+func (s *AdminServer) getAccount(w http.ResponseWriter, name string) {
canonical, err := s.accountStore.FindAccount(name)
if err != nil {
writeJSONError(w, "account not found: "+name, http.StatusNotFound)
diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go
index b8f334e..bebacff 100644
--- a/internal/admin/api_room_courses.go
+++ b/internal/admin/api_room_courses.go
@@ -131,28 +131,28 @@ func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, _ := body["id"].(string)
id = strings.TrimSpace(id)
if id == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
if !validEntityID(id) {
- writeJSON(w, map[string]any{"error": "invalid id"})
+ failJSON(w, "invalid id")
return
}
delete(body, "id")
path := s.courseFilePath(id)
if _, err := os.Stat(path); err == nil {
- writeJSON(w, map[string]any{"error": "course already exists"})
+ failJSON(w, "course already exists")
return
}
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -174,11 +174,11 @@ func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) {
func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/courses/")
if id == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
if !validEntityID(id) {
- writeJSON(w, map[string]any{"error": "invalid id"})
+ failJSON(w, "invalid id")
return
}
path := s.courseFilePath(id)
@@ -187,7 +187,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
m, data, _, err := s.loadCourseMap(id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
m["id"] = id
@@ -197,7 +197,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPost, http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -210,7 +210,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
}
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
isCreate := oldContent == nil
@@ -236,11 +236,11 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
oldContent, err := snapshotFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -269,7 +269,7 @@ func (s *AdminServer) handleRoomCourse(w http.ResponseWriter, r *http.Request, r
}
ids, err := s.listCourseIDs()
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
for _, id := range ids {
@@ -314,16 +314,16 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
Obstacle map[string]any `json:"obstacle"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
if strings.TrimSpace(body.CourseID) == "" {
- writeJSON(w, map[string]any{"error": "missing course_id"})
+ failJSON(w, "missing course_id")
return
}
m, _, path, err := s.loadCourseMap(body.CourseID)
if err != nil {
- writeJSON(w, map[string]any{"error": "course not found: " + body.CourseID})
+ failJSON(w, "course not found: "+body.CourseID)
return
}
oldContent, _ := snapshotFile(path)
@@ -331,7 +331,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
obstacles := asMapList(m["obstacles"])
for i, o := range obstacles {
if obstacleRoomID(o) == roomID {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d is already step %d of course %s", roomID, i+1, body.CourseID)})
+ failJSON(w, fmt.Sprintf("room %d is already step %d of course %s", roomID, i+1, body.CourseID))
return
}
}
@@ -379,7 +379,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
dir = "north"
}
if !isHorizontalDir(dir) {
- writeJSON(w, map[string]any{"error": "invalid direction: " + dir})
+ failJSON(w, "invalid direction: "+dir)
return
}
@@ -396,7 +396,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -479,7 +479,7 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ
}
courseID, deleted := s.detachRoomFromCourse(roomID)
if courseID == "" && !deleted {
- writeJSON(w, map[string]any{"error": "room is not part of any course"})
+ failJSON(w, "room is not part of any course")
return
}
writeJSON(w, map[string]any{"ok": true, "course_id": courseID, "deleted": deleted})
diff --git a/internal/admin/api_room_insert_remove.go b/internal/admin/api_room_insert_remove.go
index 812a3ae..45537d6 100644
--- a/internal/admin/api_room_insert_remove.go
+++ b/internal/admin/api_room_insert_remove.go
@@ -72,11 +72,7 @@ func (s *AdminServer) handleRoomInsert(w http.ResponseWriter, r *http.Request) {
return
}
- newID, subdir, allocErr := s.nextRoomID(aID)
- if allocErr != nil {
- writeJSONError(w, fmt.Sprintf("could not allocate a room id: %v", allocErr), http.StatusInternalServerError)
- return
- }
+ newID, subdir := s.nextRoomID(aID)
name := strings.TrimSpace(body.Name)
if name == "" {
diff --git a/internal/admin/api_room_insert_remove_test.go b/internal/admin/api_room_insert_remove_test.go
index f613313..9fbc022 100644
--- a/internal/admin/api_room_insert_remove_test.go
+++ b/internal/admin/api_room_insert_remove_test.go
@@ -54,16 +54,18 @@ func roomExists(s *AdminServer, id int) bool {
return err == nil
}
-func roomExitTarget(s *AdminServer, id int, dir world.ExitDir) (int, bool) {
+// roomExitTarget returns the room an exit points at, or 0 when the room or
+// exit does not exist (expected IDs are always >= 1, so 0 fails assertions).
+func roomExitTarget(s *AdminServer, id int, dir world.ExitDir) int {
r, err := s.world.LoadRoom(id)
if err != nil {
- return 0, false
+ return 0
}
e, ok := r.Exits[dir]
if !ok {
- return 0, false
+ return 0
}
- return e.Room, true
+ return e.Room
}
// postInsert drives the insert endpoint with the given body and returns the
@@ -109,16 +111,16 @@ func TestInsertSuccess(t *testing.T) {
}
// A→east→new, new→east→2, new→west→1, B→west→new.
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != newID {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != newID {
t.Errorf("A east: expected %d, got %d", newID, tgt)
}
- if tgt, _ := roomExitTarget(s, newID, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, newID, world.East); tgt != 2 {
t.Errorf("new east: expected 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, newID, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, newID, world.West); tgt != 1 {
t.Errorf("new west: expected 1, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != newID {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != newID {
t.Errorf("B west: expected %d, got %d", newID, tgt)
}
@@ -131,10 +133,10 @@ func TestInsertSuccess(t *testing.T) {
if roomExists(s, newID) {
t.Error("undo: new room file should be gone")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("undo: A east should be 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != 1 {
t.Errorf("undo: B west should be 1, got %d", tgt)
}
@@ -147,10 +149,10 @@ func TestInsertSuccess(t *testing.T) {
if !roomExists(s, newID) {
t.Error("redo: new room file should be back")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != newID {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != newID {
t.Errorf("redo: A east should be %d, got %d", newID, tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != newID {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != newID {
t.Errorf("redo: B west should be %d, got %d", newID, tgt)
}
}
@@ -202,7 +204,7 @@ func TestInsertGridConflict(t *testing.T) {
t.Errorf("insert conflict: expected both colliding room ids (#3 and #8), got %q", msg)
}
// No partial writes: the world is unchanged.
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("insert conflict: A east should still be 2, got %d", tgt)
}
}
@@ -228,10 +230,10 @@ func TestRemoveSuccess(t *testing.T) {
if roomExists(s, 2) {
t.Error("remove: B file should be gone")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 3 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 3 {
t.Errorf("remove: A east should be 3, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 3, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, 3, world.West); tgt != 1 {
t.Errorf("remove: C west should be 1, got %d", tgt)
}
@@ -244,16 +246,16 @@ func TestRemoveSuccess(t *testing.T) {
if !roomExists(s, 2) {
t.Error("undo: B file should be back")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("undo: A east should be 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 3, world.West); tgt != 2 {
+ if tgt := roomExitTarget(s, 3, world.West); tgt != 2 {
t.Errorf("undo: C west should be 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != 1 {
t.Errorf("undo: B west should be 1, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.East); tgt != 3 {
+ if tgt := roomExitTarget(s, 2, world.East); tgt != 3 {
t.Errorf("undo: B east should be 3, got %d", tgt)
}
@@ -266,7 +268,7 @@ func TestRemoveSuccess(t *testing.T) {
if roomExists(s, 2) {
t.Error("redo: B file should be gone again")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 3 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 3 {
t.Errorf("redo: A east should be 3, got %d", tgt)
}
}
@@ -386,7 +388,7 @@ func TestRemoveGridConflict(t *testing.T) {
if !roomExists(s, 2) {
t.Error("remove conflict: B file should still exist")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("remove conflict: A east should still be 2, got %d", tgt)
}
}
@@ -434,10 +436,7 @@ func TestNextRoomIDNoCrossSubdirCollision(t *testing.T) {
undoStack: NewUndoStack(dir),
}
// Allocating near room 1 (root) must skip 2 (which lives in zone/).
- id, _, err := s.nextRoomID(1)
- if err != nil {
- t.Fatal(err)
- }
+ id, _ := s.nextRoomID(1)
if id == 2 {
t.Fatalf("nextRoomID returned %d, which collides with the zone/ room", id)
}
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index 8ee16a5..16fc7a0 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -21,7 +21,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
ids, err := listRoomIDs(s.dataDir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if ids == nil {
@@ -33,13 +33,13 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
r.Body.Close()
if err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", err)})
+ failJSON(w, fmt.Sprintf("read body: %v", err))
return
}
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if m == nil {
@@ -93,11 +93,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
if linkFrom != nil {
fromID = *linkFrom
}
- id, subdir, allocErr := s.nextRoomID(fromID)
- if allocErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("alloc id: %v", allocErr)})
- return
- }
+ id, subdir := s.nextRoomID(fromID)
m["id"] = id
if linkFrom != nil && linkDir != nil && *linkDir != "" {
@@ -131,11 +127,11 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
conflicts++
})
if conflicts > 0 {
- writeJSON(w, map[string]any{"error": "Add Next Step: that grid spot is already occupied — room would cause map overlap"})
+ failJSON(w, "Add Next Step: that grid spot is already occupied — room would cause map overlap")
return
}
} else {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid direction: %s", *linkDir)})
+ failJSON(w, fmt.Sprintf("invalid direction: %s", *linkDir))
return
}
}
@@ -153,7 +149,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
if opp, ok := world.OppositeExit[dir]; ok {
srcRoom, loadErr := s.world.LoadRoom(*linkFrom)
if loadErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr)})
+ failJSON(w, fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr))
return
}
@@ -166,7 +162,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
oldContent, _ := snapshotFile(srcPath)
newContent, writeErr := writeYAMLFile(srcPath, srcRoom)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write source room: %v", writeErr)})
+ failJSON(w, fmt.Sprintf("write source room: %v", writeErr))
return
}
extraFiles = append(extraFiles, ExtraFile{
@@ -185,7 +181,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(subdir, strconv.Itoa(id)+".yaml")
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write room: %v", writeErr)})
+ failJSON(w, fmt.Sprintf("write room: %v", writeErr))
return
}
@@ -214,7 +210,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
}
id, err := strconv.Atoi(idStr)
if err != nil {
- writeJSON(w, map[string]any{"error": "invalid room id"})
+ failJSON(w, "invalid room id")
return
}
@@ -234,17 +230,17 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": "room not found"})
+ failJSON(w, "room not found")
return
}
data, err := os.ReadFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
m, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "yaml parse error"})
+ failJSON(w, "yaml parse error")
return
}
m["id"] = id
@@ -253,13 +249,13 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPut:
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, snapErr := snapshotFile(path)
if snapErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room: %v", snapErr)})
+ failJSON(w, fmt.Sprintf("snapshot room: %v", snapErr))
return
}
@@ -267,11 +263,11 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
r.Body.Close()
if readErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ failJSON(w, fmt.Sprintf("read body: %v", readErr))
return
}
if err := json.Unmarshal(body, &m); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if m == nil {
@@ -285,7 +281,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write room: %v", writeErr)})
+ failJSON(w, fmt.Sprintf("write room: %v", writeErr))
return
}
@@ -301,20 +297,20 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, backupErr := snapshotFile(path)
if backupErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("backup room: %v", backupErr)})
+ failJSON(w, fmt.Sprintf("backup room: %v", backupErr))
return
}
courseID, _ := s.detachRoomFromCourse(id)
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room: %v", err)})
+ failJSON(w, fmt.Sprintf("delete room: %v", err))
return
}
@@ -433,16 +429,16 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request
NewName string `json:"new_name"`
}
if err := readJSON(r, &body); err != nil || body.NewName == "" {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
if body.Dir == "" {
- writeJSON(w, map[string]any{"error": "cannot rename root"})
+ failJSON(w, "cannot rename root")
return
}
newName := strings.TrimSpace(body.NewName)
if newName == "" || strings.ContainsAny(newName, "/\\") || newName == "." || newName == ".." {
- writeJSON(w, map[string]any{"error": "invalid directory name"})
+ failJSON(w, "invalid directory name")
return
}
base := filepath.Join(s.dataDir, "rooms")
@@ -451,15 +447,15 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request
newRel := filepath.Join(parent, newName)
newPath := filepath.Join(base, newRel)
if _, err := os.Stat(oldPath); os.IsNotExist(err) {
- writeJSON(w, map[string]any{"error": "directory not found"})
+ failJSON(w, "directory not found")
return
}
if _, err := os.Stat(newPath); err == nil {
- writeJSON(w, map[string]any{"error": "directory already exists: " + newRel})
+ failJSON(w, "directory already exists: "+newRel)
return
}
if err := os.Rename(oldPath, newPath); err != nil {
- writeJSON(w, map[string]any{"error": "rename failed: " + err.Error()})
+ failJSON(w, "rename failed: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -475,21 +471,21 @@ func (s *AdminServer) handleCreateRoomDir(w http.ResponseWriter, r *http.Request
Name string `json:"name"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
name := strings.TrimSpace(body.Name)
if name == "" || strings.ContainsAny(name, "/\\") || name == "." || name == ".." {
- writeJSON(w, map[string]any{"error": "invalid directory name"})
+ failJSON(w, "invalid directory name")
return
}
newPath := filepath.Join(s.dataDir, "rooms", name)
if _, err := os.Stat(newPath); err == nil {
- writeJSON(w, map[string]any{"error": "directory already exists"})
+ failJSON(w, "directory already exists")
return
}
if err := os.Mkdir(newPath, 0755); err != nil {
- writeJSON(w, map[string]any{"error": "create failed: " + err.Error()})
+ failJSON(w, "create failed: "+err.Error())
return
}
writeJSON(w, map[string]any{"ok": true, "name": name})
@@ -802,37 +798,37 @@ func (s *AdminServer) handleRoomMove(w http.ResponseWriter, r *http.Request) {
Dir string `json:"dir"`
}
if err := readJSON(r, &body); err != nil || body.ID <= 0 {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
oldPath, ok := s.world.GetRoomPath(body.ID)
if !ok {
- writeJSON(w, map[string]any{"error": "room not found"})
+ failJSON(w, "room not found")
return
}
dir := strings.TrimSpace(body.Dir)
destDir := filepath.Join(s.dataDir, "rooms", dir)
if _, err := os.Stat(destDir); os.IsNotExist(err) {
- writeJSON(w, map[string]any{"error": "directory does not exist: " + dir})
+ failJSON(w, "directory does not exist: "+dir)
return
}
fileName := strconv.Itoa(body.ID) + ".yaml"
newPath := filepath.Join(destDir, fileName)
if _, err := os.Stat(newPath); err == nil {
- writeJSON(w, map[string]any{"error": "room already exists in target directory"})
+ failJSON(w, "room already exists in target directory")
return
}
data, err := os.ReadFile(oldPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "read: " + err.Error()})
+ failJSON(w, "read: "+err.Error())
return
}
if err := os.WriteFile(newPath, data, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write: " + err.Error()})
+ failJSON(w, "write: "+err.Error())
return
}
if err := os.Remove(oldPath); err != nil {
- writeJSON(w, map[string]any{"error": "remove old: " + err.Error()})
+ failJSON(w, "remove old: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -1137,7 +1133,7 @@ func (s *AdminServer) handleRoomSwap(w http.ResponseWriter, r *http.Request) {
func (s *AdminServer) handleDuplicateRooms(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- writeJSON(w, map[string]any{"error": "method not allowed"})
+ failJSON(w, "method not allowed")
return
}
dups := behavior.CheckDuplicateIDs(filepath.Join(s.dataDir, "rooms"))
@@ -1154,7 +1150,7 @@ func (s *AdminServer) handleDuplicateRooms(w http.ResponseWriter, r *http.Reques
func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
- writeJSON(w, map[string]any{"error": "method not allowed"})
+ failJSON(w, "method not allowed")
return
}
var body struct {
@@ -1163,7 +1159,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
NewID int `json:"new_id"`
}
if err := readJSON(r, &body); err != nil || body.Path == "" || body.Action == "" {
- writeJSON(w, map[string]any{"error": "invalid body"})
+ failJSON(w, "invalid body")
return
}
@@ -1171,7 +1167,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
absRooms, _ := filepath.Abs(roomsDir)
absPath, err := filepath.Abs(body.Path)
if err != nil || !strings.HasPrefix(absPath, absRooms+string(filepath.Separator)) {
- writeJSON(w, map[string]any{"error": "invalid path"})
+ failJSON(w, "invalid path")
return
}
@@ -1179,7 +1175,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
case "delete":
old, _ := snapshotFile(absPath)
if err := os.Remove(absPath); err != nil {
- writeJSON(w, map[string]any{"error": "delete failed: " + err.Error()})
+ failJSON(w, "delete failed: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -1196,27 +1192,27 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
case "rename":
if body.NewID <= 0 {
- writeJSON(w, map[string]any{"error": "new_id must be positive"})
+ failJSON(w, "new_id must be positive")
return
}
if _, ok := s.world.GetRoomPath(body.NewID); ok {
- writeJSON(w, map[string]any{"error": "room ID already exists"})
+ failJSON(w, "room ID already exists")
return
}
newRel := filepath.Join(filepath.Dir(absPath), strconv.Itoa(body.NewID)+".yaml")
if _, err := os.Stat(newRel); err == nil {
- writeJSON(w, map[string]any{"error": "target file already exists"})
+ failJSON(w, "target file already exists")
return
}
oldContent, err := os.ReadFile(absPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "read failed: " + err.Error()})
+ failJSON(w, "read failed: "+err.Error())
return
}
m, err := yamlToMap(oldContent)
if err != nil {
- writeJSON(w, map[string]any{"error": "yaml parse failed: " + err.Error()})
+ failJSON(w, "yaml parse failed: "+err.Error())
return
}
// A duplicate file was never in the room index (only one of the pair
@@ -1228,11 +1224,11 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
newContent, err := writeMapAsYAML(newRel, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write failed: " + err.Error()})
+ failJSON(w, "write failed: "+err.Error())
return
}
if err := os.Remove(absPath); err != nil {
- writeJSON(w, map[string]any{"error": "remove old failed: " + err.Error()})
+ failJSON(w, "remove old failed: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -1246,7 +1242,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
writeJSON(w, map[string]any{"ok": true})
default:
- writeJSON(w, map[string]any{"error": "unknown action: " + body.Action})
+ failJSON(w, "unknown action: "+body.Action)
}
}
@@ -1259,7 +1255,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
Dir string `json:"dir"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
base := filepath.Join(s.dataDir, "rooms")
@@ -1267,7 +1263,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
if body.Dir != "" {
targetDir = filepath.Join(base, body.Dir)
if _, err := os.Stat(targetDir); os.IsNotExist(err) {
- writeJSON(w, map[string]any{"error": "directory not found"})
+ failJSON(w, "directory not found")
return
}
}
@@ -1298,7 +1294,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
}
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": "write room: " + writeErr.Error()})
+ failJSON(w, "write room: "+writeErr.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
diff --git a/internal/admin/api_rooms_bulk.go b/internal/admin/api_rooms_bulk.go
index a7fcee7..d10e0e3 100644
--- a/internal/admin/api_rooms_bulk.go
+++ b/internal/admin/api_rooms_bulk.go
@@ -44,23 +44,23 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
r.Body.Close()
if readErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ failJSON(w, fmt.Sprintf("read body: %v", readErr))
return
}
var req bulkEditRequest
if err := json.Unmarshal(body, &req); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if len(req.IDs) == 0 {
- writeJSON(w, map[string]any{"error": "no room ids"})
+ failJSON(w, "no room ids")
return
}
if isEmptyPatch(req.Patch) {
- writeJSON(w, map[string]any{"error": "no patch fields"})
+ failJSON(w, "no patch fields")
return
}
@@ -78,17 +78,17 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
for _, id := range req.IDs {
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, err := os.ReadFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read room %d: %v", id, err)})
+ failJSON(w, fmt.Sprintf("read room %d: %v", id, err))
return
}
m, yamlErr := yamlToMap(oldContent)
if yamlErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("parse room %d: %v", id, yamlErr)})
+ failJSON(w, fmt.Sprintf("parse room %d: %v", id, yamlErr))
return
}
@@ -110,13 +110,13 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
if patch.Dir != nil && *patch.Dir != "" {
destDir := filepath.Join(s.dataDir, "rooms", *patch.Dir)
if _, statErr := os.Stat(destDir); os.IsNotExist(statErr) {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("directory does not exist: %s", *patch.Dir)})
+ failJSON(w, fmt.Sprintf("directory does not exist: %s", *patch.Dir))
return
}
newPath = filepath.Join(destDir, strconv.Itoa(id)+".yaml")
if newPath != path {
if _, statErr := os.Stat(newPath); statErr == nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d already exists in target directory", id)})
+ failJSON(w, fmt.Sprintf("room %d already exists in target directory", id))
return
}
}
@@ -127,7 +127,7 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
newContent, marshalErr := marshalMapYAML(m)
if marshalErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("marshal room %d: %v", id, marshalErr)})
+ failJSON(w, fmt.Sprintf("marshal room %d: %v", id, marshalErr))
return
}
writes = append(writes, roomWrite{id: id, oldPath: path, newPath: newPath, oldContent: oldContent, newContent: newContent})
@@ -138,17 +138,17 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
for _, wr := range writes {
if wr.newPath != wr.oldPath {
if err := txn.Move(wr.oldPath, wr.newPath); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("move room %d: %v", wr.id, err)})
+ failJSON(w, fmt.Sprintf("move room %d: %v", wr.id, err))
return
}
}
if err := txn.Replace(wr.newPath, wr.newContent, 0644); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", wr.id, err)})
+ failJSON(w, fmt.Sprintf("write room %d: %v", wr.id, err))
return
}
}
if err := txn.Commit(); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("commit bulk edit: %v", err)})
+ failJSON(w, fmt.Sprintf("commit bulk edit: %v", err))
return
}
diff --git a/internal/admin/api_rooms_bulk_delete.go b/internal/admin/api_rooms_bulk_delete.go
index 70c2eb6..5b525f6 100644
--- a/internal/admin/api_rooms_bulk_delete.go
+++ b/internal/admin/api_rooms_bulk_delete.go
@@ -24,18 +24,18 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
r.Body.Close()
if readErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ failJSON(w, fmt.Sprintf("read body: %v", readErr))
return
}
var req bulkDeleteRequest
if err := json.Unmarshal(body, &req); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if len(req.IDs) == 0 {
- writeJSON(w, map[string]any{"error": "no room ids"})
+ failJSON(w, "no room ids")
return
}
@@ -45,7 +45,7 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
seenIDs := make(map[int]bool, len(req.IDs))
for _, id := range req.IDs {
if seenIDs[id] {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("duplicate room id %d in request", id)})
+ failJSON(w, fmt.Sprintf("duplicate room id %d in request", id))
return
}
seenIDs[id] = true
@@ -61,12 +61,12 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
for _, id := range req.IDs {
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, snapErr := snapshotFile(path)
if snapErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room %d: %v", id, snapErr)})
+ failJSON(w, fmt.Sprintf("snapshot room %d: %v", id, snapErr))
return
}
snapshots = append(snapshots, roomSnapshot{id: id, path: path, oldContent: oldContent})
@@ -85,7 +85,7 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
}
}
}
- writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room %d: %v", snap.id, err)})
+ failJSON(w, fmt.Sprintf("delete room %d: %v", snap.id, err))
return
}
deletedSet[snap.id] = true
diff --git a/internal/admin/api_rooms_bulk_delete_test.go b/internal/admin/api_rooms_bulk_delete_test.go
index 083b890..66464a5 100644
--- a/internal/admin/api_rooms_bulk_delete_test.go
+++ b/internal/admin/api_rooms_bulk_delete_test.go
@@ -14,20 +14,20 @@ import (
"testing"
)
-func postBulkDelete(t *testing.T, s *AdminServer, body string) (map[string]any, int) {
+func postBulkDelete(t *testing.T, s *AdminServer, body string) map[string]any {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/api/rooms/bulk-delete", strings.NewReader(body))
rec := httptest.NewRecorder()
s.handleBulkDelete(rec, req)
var resp map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
- return resp, rec.Code
+ return resp
}
func TestBulkDeleteRejectsDuplicateIDs(t *testing.T) {
s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Room\n"})
path, _ := s.world.GetRoomPath(1)
- resp, _ := postBulkDelete(t, s, `{"ids":[1,1]}`)
+ resp := postBulkDelete(t, s, `{"ids":[1,1]}`)
if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "duplicate room id 1") {
t.Fatalf("duplicate IDs not rejected: %v", resp)
}
@@ -60,7 +60,7 @@ func TestBulkDeleteFailureRollsBackDeletions(t *testing.T) {
t.Cleanup(func() { _ = os.Chmod(sub, 0o755); os.RemoveAll(sub) })
path1, _ := s.world.GetRoomPath(1)
- resp, _ := postBulkDelete(t, s, `{"ids":[1,3]}`)
+ resp := postBulkDelete(t, s, `{"ids":[1,3]}`)
if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "delete room 3") {
t.Fatalf("expected room-3 delete failure: %v", resp)
}
diff --git a/internal/admin/api_search.go b/internal/admin/api_search.go
index d54e7f1..b97f97e 100644
--- a/internal/admin/api_search.go
+++ b/internal/admin/api_search.go
@@ -18,7 +18,7 @@ func (s *AdminServer) handleSearch(w http.ResponseWriter, r *http.Request) {
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
if q == "" {
- writeJSON(w, map[string]any{"error": "missing query parameter 'q'"})
+ failJSON(w, "missing query parameter 'q'")
return
}
diff --git a/internal/admin/api_techs.go b/internal/admin/api_techs.go
index ba3288a..91eb929 100644
--- a/internal/admin/api_techs.go
+++ b/internal/admin/api_techs.go
@@ -20,7 +20,7 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "techs")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -30,23 +30,23 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, ok := m["id"].(string)
if !ok || strings.TrimSpace(id) == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
delete(m, "id")
if _, _, err := s.findTechFile(id); err == nil {
- writeJSON(w, map[string]any{"error": "tech already exists"})
+ failJSON(w, "tech already exists")
return
}
path := filepath.Join(s.dataDir, "techs", id+".yaml")
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -76,12 +76,12 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
data, path, err := s.findTechFile(id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
raw, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ failJSON(w, "parse error: "+err.Error())
return
}
raw["id"] = id
@@ -92,7 +92,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPost, http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -114,7 +114,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -134,12 +134,12 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
_, path, err := s.findTechFile(id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
oldContent, _ := snapshotFile(path)
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
diff --git a/internal/admin/api_triggers.go b/internal/admin/api_triggers.go
index 87eb82e..5b82465 100644
--- a/internal/admin/api_triggers.go
+++ b/internal/admin/api_triggers.go
@@ -22,7 +22,7 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, &YAMLTree{Root: []string{}, Dirs: map[string][]string{}})
return
}
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -32,16 +32,16 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, ok := m["id"].(string)
if !ok || strings.TrimSpace(id) == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
if !validEntityID(id) {
- writeJSON(w, map[string]any{"error": "invalid id"})
+ failJSON(w, "invalid id")
return
}
delete(m, "id")
@@ -51,13 +51,13 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(s.dataDir, "triggers", subdir, id+".yaml")
os.MkdirAll(filepath.Dir(path), 0755)
if _, err := os.Stat(path); err == nil {
- writeJSON(w, map[string]any{"error": "trigger already exists"})
+ failJSON(w, "trigger already exists")
return
}
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -87,17 +87,17 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
case http.MethodGet:
path, _, err := findTriggerFile(s.dataDir, id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
data, err := os.ReadFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
raw, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ failJSON(w, "parse error: "+err.Error())
return
}
raw["id"] = id
@@ -109,7 +109,7 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
case http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -134,12 +134,12 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
triggerData, err := triggerMapToYAML(body)
if err != nil {
- writeJSON(w, map[string]any{"error": "serialize error: " + err.Error()})
+ failJSON(w, "serialize error: "+err.Error())
return
}
if err := os.WriteFile(newPath, triggerData, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -159,16 +159,16 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
case http.MethodDelete:
path, _, err := findTriggerFile(s.dataDir, id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
oldContent, err := snapshotFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
diff --git a/internal/admin/id_alloc.go b/internal/admin/id_alloc.go
index 3a3c973..83ca89c 100644
--- a/internal/admin/id_alloc.go
+++ b/internal/admin/id_alloc.go
@@ -13,7 +13,7 @@ import (
// collide with an ID that lives in a different subdirectory. The starting point
// is the local subdir's minimum existing ID (clustering), then the first
// globally-free ID scanning upward.
-func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) {
+func (s *AdminServer) nextRoomID(fromRoomID int) (int, string) {
base := filepath.Join(s.dataDir, "rooms")
subdir, err := findRoomSubdir(base, fromRoomID)
if err != nil {
@@ -46,7 +46,7 @@ func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) {
for used[id] {
id++
}
- return id, subdir, nil
+ return id, subdir
}
func findRoomSubdir(base string, roomID int) (string, error) {
diff --git a/internal/admin/server.go b/internal/admin/server.go
index fb0c53b..2c8681a 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -1,3 +1,6 @@
+// Package admin implements the HTTP admin server: authenticated routes for
+// editing game data (rooms, items, mobs, drops, triggers, colors, …), the
+// embedded web UI, and the undo stack backing it.
package admin
import (
@@ -516,6 +519,16 @@ func writeJSONError(w http.ResponseWriter, msg string, code int) {
json.NewEncoder(w).Encode(map[string]any{"error": msg})
}
+// failJSON writes a JSON error body without changing the HTTP status. This
+// is the long-standing wire behavior most admin editors depend on: they read
+// res.error out of a 200 response, while api wrappers in admin.js treat any
+// non-200 as a thrown raw-text error. Unifying status codes requires a
+// coordinated change across the static JS clients; do not silently switch
+// call sites to non-200 codes here.
+func failJSON(w http.ResponseWriter, msg string) {
+ writeJSONError(w, msg, http.StatusOK)
+}
+
func readJSON(r *http.Request, v any) error {
body, err := io.ReadAll(r.Body)
if err != nil {
@@ -580,20 +593,20 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc {
NewName string `json:"new_name"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
switch body.Action {
case "create":
if err := createEntityDir(s.dataDir, entityType, body.Name); err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
writeJSON(w, map[string]any{"ok": true, "name": body.Name})
case "delete":
changes, err := deleteEntityDir(s.dataDir, entityType, body.Dir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
for _, ch := range changes {
@@ -602,12 +615,12 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc {
writeJSON(w, map[string]any{"ok": true, "moved": len(changes) - 1})
case "rename_dir":
if err := renameEntityDir(s.dataDir, entityType, body.Dir, body.NewName); err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
writeJSON(w, map[string]any{"ok": true, "name": body.NewName})
default:
- writeJSON(w, map[string]any{"error": "unknown action"})
+ failJSON(w, "unknown action")
}
}
}
@@ -624,12 +637,12 @@ func (s *AdminServer) handleEntityRename(entityType string) http.HandlerFunc {
}
if err := readJSON(r, &body); err != nil || body.ID == "" || body.NewID == "" ||
!validEntityID(body.NewID) {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
oldPath, newPath, data, err := renameEntityFile(s.dataDir, entityType, body.ID, body.NewID)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
desc := fmt.Sprintf("rename %s %s to %s", entityType, body.ID, body.NewID)
@@ -656,12 +669,12 @@ func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc {
Dir string `json:"dir"`
}
if err := readJSON(r, &body); err != nil || body.ID == "" {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
oldPath, newPath, data, err := moveEntityFile(s.dataDir, entityType, body.ID, body.Dir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
desc := fmt.Sprintf("move %s %s to %s/", entityType, body.ID, body.Dir)
diff --git a/internal/admin/xterm_to_css.go b/internal/admin/xterm_to_css.go
deleted file mode 100644
index 6c1fda4..0000000
--- a/internal/admin/xterm_to_css.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package admin
-
-import (
- "fmt"
- "strconv"
- "strings"
-
- "thehouseoficarus/internal/color"
-)
-
-func XtermToCSS(xtermHex string) string {
- xtermHex = strings.TrimPrefix(xtermHex, "#")
- idx, err := strconv.ParseUint(xtermHex, 16, 8)
- if err != nil || idx > 255 {
- return "#808080"
- }
- r, g, b := color.Xterm256ToRGB(int(idx))
- return fmt.Sprintf("#%02x%02x%02x", r, g, b)
-}
-
-func XtermColorForCSS(idx int) string {
- if idx < 0 || idx > 255 {
- return "#808080"
- }
- r, g, b := color.Xterm256ToRGB(idx)
- return fmt.Sprintf("#%02x%02x%02x", r, g, b)
-}