aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_map.go48
-rw-r--r--internal/admin/api_rooms.go70
-rw-r--r--internal/admin/server.go39
-rw-r--r--internal/admin/static/map.js184
-rw-r--r--internal/admin/undo.go2
5 files changed, 165 insertions, 178 deletions
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 5de41a7..7298f1e 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -44,7 +44,6 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
Y int `json:"y"`
Name string `json:"name"`
Color string `json:"color"`
- Symbol string `json:"symbol"`
}
type LinkEntry struct {
@@ -249,47 +248,22 @@ func findDisconnectedRooms(s *AdminServer, dir string, seed int, g world.RoomGri
func listRoomIDsInDir(s *AdminServer, dir string) []int {
base := filepath.Join(s.dataDir, "rooms")
- var dirs []string
- if dir == "" {
- dirs = append(dirs, base)
- entries, err := os.ReadDir(base)
- if err != nil {
- return nil
- }
- for _, e := range entries {
- if e.IsDir() {
- dirs = append(dirs, filepath.Join(base, e.Name()))
- subE, err := os.ReadDir(filepath.Join(base, e.Name()))
- if err != nil {
- continue
- }
- for _, se := range subE {
- if se.IsDir() {
- dirs = append(dirs, filepath.Join(base, e.Name(), se.Name()))
- }
- }
- }
- }
- } else {
- dirs = append(dirs, filepath.Join(base, dir))
+ walkRoot := base
+ if dir != "" {
+ walkRoot = filepath.Join(base, dir)
}
var ids []int
- for _, d := range dirs {
- entries, err := os.ReadDir(d)
- if err != nil {
- continue
+ filepath.WalkDir(walkRoot, func(path string, d os.DirEntry, err error) error {
+ if err != nil || d.IsDir() || filepath.Ext(d.Name()) != ".yaml" {
+ return nil
}
- for _, e := range entries {
- if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" {
- continue
- }
- id, err := strconv.Atoi(e.Name()[:len(e.Name())-5])
- if err != nil {
- continue
- }
+ name := d.Name()
+ id, convErr := strconv.Atoi(name[:len(name)-5])
+ if convErr == nil && id > 0 {
ids = append(ids, id)
}
- }
+ return nil
+ })
return ids
}
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index b60aefc..a477cc3 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -106,7 +106,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
})
}
- exits[string(opp)] = float64(*linkFrom)
+ exits[string(opp)] = *linkFrom
}
}
@@ -161,7 +161,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
return
}
m["id"] = id
- writeJSON(w, map[string]any{"room": m, "path": path, "file": path, "raw": string(data)})
+ writeJSON(w, map[string]any{"room": m, "path": path, "file": path})
case http.MethodPut:
path, ok := s.world.GetRoomPath(id)
@@ -298,45 +298,27 @@ func (s *AdminServer) handleRoomDirs(w http.ResponseWriter, r *http.Request) {
return
}
base := filepath.Join(s.dataDir, "rooms")
- entries, err := os.ReadDir(base)
- if err != nil {
- writeJSON(w, []string{""})
- return
- }
dirs := []string{""}
- for _, e := range entries {
- if !e.IsDir() {
- continue
- }
- if hasYAMLFiles(filepath.Join(base, e.Name())) {
- dirs = append(dirs, e.Name())
- }
- subEntries, err := os.ReadDir(filepath.Join(base, e.Name()))
- if err != nil {
- continue
- }
- for _, se := range subEntries {
- if se.IsDir() && hasYAMLFiles(filepath.Join(base, e.Name(), se.Name())) {
- dirs = append(dirs, e.Name()+"/"+se.Name())
+ filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
+ if err != nil || !d.IsDir() || path == base {
+ return nil
+ }
+ entries, rdErr := os.ReadDir(path)
+ if rdErr != nil {
+ return nil
+ }
+ for _, e := range entries {
+ if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" {
+ rel, _ := filepath.Rel(base, path)
+ dirs = append(dirs, rel)
+ return nil
}
}
- }
+ return nil
+ })
writeJSON(w, dirs)
}
-func hasYAMLFiles(dir string) bool {
- entries, err := os.ReadDir(dir)
- if err != nil {
- return false
- }
- for _, e := range entries {
- if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" {
- return true
- }
- }
- return false
-}
-
func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
@@ -377,10 +359,14 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
cA, okA := grid.Coord[body.From]
cB, okB := grid.Coord[body.To]
- if !okA || !okB || cA[2] != cB[2] {
- writeJSON(w, map[string]any{"error": "rooms must be on same z-level"})
- return
- }
+ if !okA || !okB {
+ writeJSON(w, map[string]any{"error": "one or both rooms are not reachable from the seed room"})
+ return
+ }
+ if cA[2] != cB[2] {
+ writeJSON(w, map[string]any{"error": "rooms must be on same z-level"})
+ return
+ }
dx, dy := cB[0]-cA[0], cB[1]-cA[1]
for d, delta := range world.DirectionDeltas3D {
if delta[0] == dx && delta[1] == dy && delta[2] == 0 {
@@ -510,12 +496,6 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"error": "both rooms not found"})
return
}
- if (errA != nil && roomA == nil) {
- roomA = nil
- }
- if (errB != nil && roomB == nil) {
- roomB = nil
- }
var dir, oppDir world.ExitDir
if roomA != nil {
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 8facc4a..823d7ba 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -34,6 +34,7 @@ var embedded embed.FS
type AdminServer struct {
cfg *config.Config
+ useTLS bool
accountStore *player.AccountStore
world *world.World
itemStore *item.ItemStore
@@ -46,7 +47,7 @@ type AdminServer struct {
cookieSecret []byte
}
-func NewServer(cfg *config.Config, accountStore *player.AccountStore, w *world.World, is *item.ItemStore, os *object.ObjectStore, ms *world.MobStore, dataDir string) (*AdminServer, error) {
+func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStore, w *world.World, is *item.ItemStore, os *object.ObjectStore, ms *world.MobStore, dataDir string) (*AdminServer, error) {
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("cookie secret: %w", err)
@@ -62,13 +63,24 @@ func NewServer(cfg *config.Config, accountStore *player.AccountStore, w *world.W
return nil, fmt.Errorf("parse templates: %w", err)
}
- cert, err := loadOrGenerateCert(cfg)
- if err != nil {
- return nil, fmt.Errorf("admin cert: %w", err)
+ var port int
+ if useTLS {
+ port = cfg.AdminHTTPS.Port
+ } else {
+ port = cfg.AdminHTTP.Port
+ }
+
+ var cert tls.Certificate
+ if useTLS {
+ cert, err = loadOrGenerateCert(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("admin cert: %w", err)
+ }
}
s := &AdminServer{
cfg: cfg,
+ useTLS: useTLS,
accountStore: accountStore,
world: w,
itemStore: is,
@@ -159,12 +171,14 @@ func NewServer(cfg *config.Config, accountStore *player.AccountStore, w *world.W
})
s.httpServer = &http.Server{
- Addr: fmt.Sprintf(":%d", cfg.AdminHTTPS.Port),
+ Addr: fmt.Sprintf(":%d", port),
Handler: mux,
- TLSConfig: &tls.Config{
+ }
+ if useTLS {
+ s.httpServer.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
- },
+ }
}
return s, nil
@@ -175,8 +189,11 @@ func (s *AdminServer) ListenAndServe() error {
if err != nil {
return fmt.Errorf("admin listen: %w", err)
}
- tlsLn := tls.NewListener(ln, s.httpServer.TLSConfig)
- return s.httpServer.Serve(tlsLn)
+ if s.useTLS {
+ tlsLn := tls.NewListener(ln, s.httpServer.TLSConfig)
+ return s.httpServer.Serve(tlsLn)
+ }
+ return s.httpServer.Serve(ln)
}
func (s *AdminServer) authMiddleware(next http.Handler) http.Handler {
@@ -228,7 +245,7 @@ func (s *AdminServer) setAuthCookie(w http.ResponseWriter, account string) {
Value: account + ":" + sig,
Path: "/",
HttpOnly: true,
- Secure: true,
+ Secure: s.useTLS,
SameSite: http.SameSiteStrictMode,
MaxAge: 86400,
})
@@ -270,7 +287,7 @@ func (s *AdminServer) handleLogout(w http.ResponseWriter, r *http.Request) {
Path: "/",
MaxAge: -1,
HttpOnly: true,
- Secure: true,
+ Secure: s.useTLS,
})
http.Redirect(w, r, "/login", http.StatusFound)
}
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index cd53bed..85364d5 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -47,7 +47,7 @@ function loadMap() {
if (currentDir) url += '&dir=' + encodeURIComponent(currentDir);
var wasDirChange = dirChangedByUser;
dirChangedByUser = false;
- API.get(url).then(function(data) {
+ return API.get(url).then(function(data) {
mapData = data;
if (!currentDir && data.dir) {
currentDir = data.dir;
@@ -64,11 +64,11 @@ function loadMap() {
}
function blendColors(colorA, colorB) {
+ if (!colorA && !colorB) return '#666666';
+ if (!colorB) return xtermToCss(parseXtermIdx(colorA));
+ if (!colorA) return xtermToCss(parseXtermIdx(colorB));
var idxA = parseXtermIdx(colorA);
var idxB = parseXtermIdx(colorB);
- if (colorA && !colorB) return xtermToCss(idxA);
- if (!colorA && colorB) return xtermToCss(idxB);
- if (!colorA && !colorB) return '#666666';
var rgbA = xtermToRGB(idxA);
var rgbB = xtermToRGB(idxB);
var r = Math.round((rgbA[0] + rgbB[0]) / 2);
@@ -90,17 +90,21 @@ function renderMap(data) {
var W = container.clientWidth;
var H = container.clientHeight;
- roomMap = {};
- if (data.rooms) data.rooms.forEach(function(r) { roomMap[r.id] = r; });
-
- occupied = {};
- if (data.rooms) data.rooms.forEach(function(r) { occupied[r.x+','+r.y] = r.id; });
-
+ roomMap = {}; occupied = {};
upTarget = {}; downTarget = {};
- if (data.upLinks) data.upLinks.forEach(function(l) { upTarget[l.from] = l.to; });
- if (data.downLinks) data.downLinks.forEach(function(l) { downTarget[l.from] = l.to; });
+ var hasUp = {}, hasDown = {};
+
+ if (data.rooms) data.rooms.forEach(function(r) {
+ roomMap[r.id] = r;
+ occupied[r.x+','+r.y] = r.id;
+ });
+ if (data.upLinks) data.upLinks.forEach(function(l) {
+ upTarget[l.from] = l.to; hasUp[l.from] = true;
+ });
+ if (data.downLinks) data.downLinks.forEach(function(l) {
+ downTarget[l.from] = l.to; hasDown[l.from] = true;
+ });
- var svgNS = 'http://www.w3.org/2000/svg';
var g = '';
if (data.links) {
@@ -109,16 +113,11 @@ function renderMap(data) {
if (!fr || !tr) return;
var x1 = fr.x * CELL + CELL/2, y1 = fr.y * CELL + CELL/2;
var x2 = tr.x * CELL + CELL/2, y2 = tr.y * CELL + CELL/2;
- var c = blendColors(fr.color, tr.color);
- c = c.replace('#','');
+ var c = blendColors(fr.color, tr.color).replace('#','');
g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#'+c+'" stroke-width="6" opacity="0.55"'+ (l.bidirectional ? '' : ' stroke-dasharray="4,3"')+' vector-effect="non-scaling-stroke"/>';
});
}
- var hasUp = {}, hasDown = {};
- if (data.upLinks) data.upLinks.forEach(function(l) { hasUp[l.from] = true; });
- if (data.downLinks) data.downLinks.forEach(function(l) { hasDown[l.from] = true; });
-
var roomHTML = '';
if (data.rooms) {
data.rooms.forEach(function(r) {
@@ -126,7 +125,7 @@ function renderMap(data) {
var fill = resolveColor(r.color);
var txtColor = hexLuminance(fill) > 0.35 ? '#111' : '#fff';
var sel = selectedRoom === r.id;
- roomHTML += '<rect class="rm" x="'+x+'" y="'+y+'" width="'+w+'" height="'+h+'" rx="5" fill="'+fill+'" stroke="'+(sel?'#fff':fill)+'" stroke-width="'+(sel?3:1.5)+'" data-id="'+r.id+'" data-name="'+esc(r.name)+'"/>';
+ roomHTML += '<rect class="rm" x="'+x+'" y="'+y+'" width="'+w+'" height="'+h+'" rx="5" fill="'+fill+'" stroke="'+(sel?'#fff':fill)+'" stroke-width="'+(sel?3:1.5)+'" data-id="'+r.id+'"/>';
var dimColor = hexLuminance(fill) > 0.35 ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.4)';
roomHTML += '<text x="'+(x+w/2)+'" y="'+(y+14)+'" text-anchor="middle" font-size="8" fill="'+dimColor+'" font-family="monospace" pointer-events="none">#'+r.id+'</text>';
@@ -167,7 +166,7 @@ function renderMap(data) {
svg.setAttribute('height', H);
svg.innerHTML = '<g>' + innerHTML + '</g>';
- svg.addEventListener('contextmenu', function(e) { e.preventDefault(); });
+ svg.oncontextmenu = function(e) { e.preventDefault(); };
svg.onmousedown = function(e) {
var onRoom = e.target.classList.contains('rm');
@@ -180,9 +179,7 @@ function renderMap(data) {
var dz = parseInt(e.target.getAttribute('data-dz'));
var tid = parseInt(e.target.getAttribute('data-target'));
currentZ += dz;
- selectedRoom = tid;
- loadMap();
- selectRoom(tid);
+ loadMap().then(function() { selectRoom(tid); });
return;
}
@@ -292,7 +289,6 @@ function renderMap(data) {
if (scale > 5) scale = 5;
updateView();
};
-
}
function wrapText(name, cx, cy, maxWidth, color) {
@@ -338,8 +334,24 @@ function updateView() {
function selectRoom(id) {
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }
+ var prev = selectedRoom;
selectedRoom = id;
- loadMap();
+
+ if (mapData && roomMap) {
+ if (prev && prev !== id) {
+ var prevEl = document.querySelector('.rm[data-id="'+prev+'"]');
+ if (prevEl) {
+ prevEl.setAttribute('stroke', prevEl.getAttribute('fill') || '#3a3a6a');
+ prevEl.setAttribute('stroke-width', '1.5');
+ }
+ }
+ var el = document.querySelector('.rm[data-id="'+id+'"]');
+ if (el) {
+ el.setAttribute('stroke', '#fff');
+ el.setAttribute('stroke-width', '3');
+ }
+ }
+
API.get('/api/rooms/' + id).then(function(data) {
renderSidePanel(data);
}).catch(function(e) {
@@ -443,9 +455,7 @@ function renderHazardTab(r) {
}
function searchHazards(input) {
- searchAndShow(input, 'hazards', function(r) {
- addHazard(r.id);
- });
+ searchAndShow(input, 'hazards', function(r) { addHazard(r.id); });
}
function addHazard(id) {
if (!currentRoomData || !currentRoomData.room) return;
@@ -597,7 +607,6 @@ function renderItemsTab(spawns) {
function renderExitsSection(r) {
var exits = r.exits || {};
var exitDirs = ['north','south','east','west','northeast','northwest','southeast','southwest','up','down'];
- var dirAliases = {n:'north',s:'south',e:'east',w:'west',ne:'northeast',nw:'northwest',se:'southeast',sw:'southwest',u:'up',d:'down'};
var h = '<div class="exit-section"><h3>Exits</h3>';
exitDirs.forEach(function(d) {
var exit = exits[d];
@@ -676,7 +685,6 @@ function editExitCondition(dir) {
menu._condClauses = parsed.clauses;
menu._condMode = parsed.mode;
- // Fix: mode is always 'all' for our editor, but store what was parsed
renderConditionClauses(menu);
}
@@ -801,7 +809,6 @@ function saveExitCondition() {
exit.set_flags = parseFlagInput(flagsStr ? flagsStr.value : '');
exit.set_player_flags = parseFlagInput(pflagsStr ? pflagsStr.value : '');
- // Update clause data from DOM before saving
var clauseEls = menu.querySelectorAll('.cond-clause');
clauseEls.forEach(function(el) { updateClauseData(el); });
@@ -868,8 +875,7 @@ function addExit() {
API.post('/api/rooms/link', {from: selectedRoom, to: target, dir: dir}).then(function() {
notify('Exit added: ' + dir + ' #' + target, 'success');
updateUndoBar();
- loadMap();
- selectRoom(selectedRoom);
+ loadMap().then(function() { selectRoom(selectedRoom); });
}).catch(function(e) { notify('Add exit failed: ' + extractError(e), 'error'); });
}
@@ -887,8 +893,7 @@ function deleteExit(dir, target) {
}).then(function() {
notify('Removed exit ' + dir + ' to #' + target, 'success');
updateUndoBar();
- loadMap();
- selectRoom(selectedRoom);
+ loadMap().then(function() { selectRoom(selectedRoom); });
}).catch(function(e) { notify('Delete exit failed: ' + extractError(e), 'error'); });
}
@@ -997,9 +1002,7 @@ function addRefObject(id) {
}
function searchRefObjects(input) {
- searchAndShow(input, 'objects', function(r) {
- addRefObject(r.id);
- });
+ searchAndShow(input, 'objects', function(r) { addRefObject(r.id); });
}
function removeSpawn(i) {
@@ -1029,9 +1032,7 @@ function addSpawn(id) {
}
function searchItemSpawns(input) {
- searchAndShow(input, 'items', function(r) {
- addSpawn(r.id);
- });
+ searchAndShow(input, 'items', function(r) { addSpawn(r.id); });
}
function removeMob(i) {
@@ -1049,9 +1050,7 @@ function addMob(id) {
}
function searchMobs(input) {
- searchAndShow(input, 'mobs', function(r) {
- addMob(r.id);
- });
+ searchAndShow(input, 'mobs', function(r) { addMob(r.id); });
}
function searchAndShow(input, type, onSelect) {
@@ -1112,20 +1111,14 @@ function saveNow() {
doSavePanel();
}
-function doSavePanel() {
+async function doSavePanel() {
var id = selectedRoom;
if (!id) return;
- var save = function(r) {
- API.put('/api/rooms/' + id, r).then(function() {
- notify('Room #' + id + ' saved', 'success');
- updateUndoBar();
- loadMap();
- }).catch(function(e) { notify('Save failed: '+e.message, 'error'); });
- };
-
- API.get('/api/rooms/' + id).then(function(data) {
+ try {
+ var data = await API.get('/api/rooms/' + id);
var r = data.room || {};
r.id = id;
+
var el = $('#roomName'); if (el) r.name = el.value;
el = $('#roomColor'); if (el) r.color = el.value;
el = $('#roomDesc'); if (el && el.value) r.description = [{text: el.value}];
@@ -1140,29 +1133,29 @@ function doSavePanel() {
if (r.block_transport === undefined) r.block_transport = currentRoomData.room.block_transport;
}
- var el = $('#roomID'); var newID = el ? parseInt(el.value) || id : id;
+ el = $('#roomID');
+ var newID = el ? parseInt(el.value) || id : id;
var dirEl = $('#roomDir');
var newDir = dirEl ? dirEl.value : null;
var currentDir = (data.file || '').replace(/^data\/rooms\/?/, '').split('/')[0] || '';
if (dirEl && newDir !== currentDir) {
- API.post('/api/rooms/move', {id: id, dir: newDir}).then(function() {
- if (newID !== id) {
- API.post('/api/rooms/rename', {id: id, new_id: newID}).then(function() {
- selectRoom(newID);
- }).catch(function(e) { notify('Rename failed: '+e.message, 'error'); save(r); });
- } else { save(r); }
- }).catch(function(e) { notify('Move failed: '+e.message, 'error'); save(r); });
- } else if (newID !== id) {
- API.post('/api/rooms/rename', {id: id, new_id: newID}).then(function() {
- selectRoom(newID);
- }).catch(function(e) {
- notify('Rename failed: '+e.message, 'error');
- var rid = $('#roomID'); if (rid) rid.value = id;
- save(r);
- });
- } else { save(r); }
- }).catch(function(e) { notify('Save failed: '+e.message, 'error'); });
+ await API.post('/api/rooms/move', {id: id, dir: newDir});
+ }
+ if (newID !== id) {
+ await API.post('/api/rooms/rename', {id: id, new_id: newID});
+ selectedRoom = newID;
+ id = newID;
+ }
+
+ await API.put('/api/rooms/' + id, r);
+ notify('Room #' + id + ' saved', 'success');
+ updateUndoBar();
+ await loadMap();
+ selectRoom(id);
+ } catch (e) {
+ notify('Save failed: ' + extractError(e), 'error');
+ }
}
function deleteRoom(id) {
@@ -1175,6 +1168,18 @@ function deleteRoom(id) {
if (l.to === id && l.from !== id) neighbors.push(l.from);
});
}
+ if (mapData.upLinks) {
+ mapData.upLinks.forEach(function(l) {
+ if (l.from === id && l.to !== id) neighbors.push(l.to);
+ if (l.to === id && l.from !== id) neighbors.push(l.from);
+ });
+ }
+ if (mapData.downLinks) {
+ mapData.downLinks.forEach(function(l) {
+ if (l.from === id && l.to !== id) neighbors.push(l.to);
+ if (l.to === id && l.from !== id) neighbors.push(l.from);
+ });
+ }
if (neighbors.length > 1) {
var remaining = mapData.rooms.filter(function(r) { return r.id !== id; });
var adj = {};
@@ -1186,6 +1191,20 @@ function deleteRoom(id) {
if (adj[l.to]) adj[l.to].push(l.from);
});
}
+ if (mapData.upLinks) {
+ mapData.upLinks.forEach(function(l) {
+ if (l.from === id || l.to === id) return;
+ if (adj[l.from]) adj[l.from].push(l.to);
+ if (adj[l.to]) adj[l.to].push(l.from);
+ });
+ }
+ if (mapData.downLinks) {
+ mapData.downLinks.forEach(function(l) {
+ if (l.from === id || l.to === id) return;
+ if (adj[l.from]) adj[l.from].push(l.to);
+ if (adj[l.to]) adj[l.to].push(l.from);
+ });
+ }
if (remaining.length > 0) {
var visited = {}, queue = [remaining[0].id];
visited[remaining[0].id] = true;
@@ -1219,7 +1238,7 @@ function createRoom(fromID, dir) {
var createdId = (resp && resp.room && resp.room.id) || newID;
notify('Room #' + createdId + ' created', 'success');
updateUndoBar();
- selectRoom(createdId);
+ loadMap().then(function() { selectRoom(createdId); });
}).catch(function(e) { notify('Create failed: ' + e.message, 'error'); });
});
}
@@ -1323,7 +1342,7 @@ function applyGhostLinkHighlight(fromRoom, gx, gy) {
fromEl.setAttribute('stroke', '#0ff');
fromEl.setAttribute('stroke-width', '3');
- var g = document.querySelector('#mapSvg g');
+ var g = document.querySelector('#mapSvg g');
if (!g) return;
var svgNS = 'http://www.w3.org/2000/svg';
var line = document.createElementNS(svgNS, 'line');
@@ -1353,7 +1372,7 @@ function applyLinkHighlight(fromId, toId) {
toEl.setAttribute('stroke', '#f0f');
toEl.setAttribute('stroke-width', '3');
- var g = document.querySelector('#mapSvg g');
+ var g = document.querySelector('#mapSvg g');
if (!g) return;
var svgNS = 'http://www.w3.org/2000/svg';
var line = document.createElementNS(svgNS, 'line');
@@ -1393,12 +1412,9 @@ function createLink(fromId, toId) {
API.post('/api/rooms/link', {from: fromId, to: toId}).then(function() {
notify('Linked rooms #'+fromId+' '+toId, 'success');
updateUndoBar();
- loadMap();
- if (selectedRoom) {
- API.get('/api/rooms/' + selectedRoom).then(function(data) {
- renderSidePanel(data);
- });
- }
+ loadMap().then(function() {
+ if (selectedRoom) selectRoom(selectedRoom);
+ });
}).catch(function(e) {
notify('Link failed: ' + e.message, 'error');
});
@@ -1441,7 +1457,7 @@ function applyDelHighlight(fromId, toId) {
toEl.setAttribute('stroke', '#f55');
toEl.setAttribute('stroke-width', '3');
- var g = document.querySelector('#mapSvg g');
+ var g = document.querySelector('#mapSvg g');
if (!g) return;
var svgNS = 'http://www.w3.org/2000/svg';
var line = document.createElementNS(svgNS, 'line');
@@ -1587,7 +1603,7 @@ function escAttr(s) {
return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;');
}
-window.refreshPage = function() { loadMap(); if (selectedRoom) selectRoom(selectedRoom); };
+window.refreshPage = function() { loadMap().then(function() { if (selectedRoom) selectRoom(selectedRoom); }); };
function loadRoomDirs(cb) {
API.get('/api/room-dirs').then(function(dirs) {
diff --git a/internal/admin/undo.go b/internal/admin/undo.go
index 39e8c4e..bdd16e6 100644
--- a/internal/admin/undo.go
+++ b/internal/admin/undo.go
@@ -42,7 +42,7 @@ func NewUndoStack(dataDir string) *UndoStack {
us := &UndoStack{
dataDir: dataDir,
maxSize: 100,
- filePath: filepath.Join(dataDir, ".admin_history.yaml"),
+ filePath: filepath.Join(dataDir, ".admin_history.json"),
}
us.load()
return us