terminal: rename sessions + drag-and-drop reorder

Backend: POST /sessions/<name>/rename in tmux-api runs tmux
rename-session as the mapped OS user. 400 on bad name, 404 on missing
source, 409 on duplicate target, 401 on missing auth header.

Frontend:
- Rename button per card → prompt() dialog, validates against the
  shared regex. Updates currentActive + hash + iframe.src if the
  renamed session was active.
- Session order is now user-driven, persisted in localStorage
  keyed per osUser. New sessions append at the bottom. The previous
  sort-by-lastActivity is gone.
- HTML5 drag-and-drop reorders cards live during dragover; dragend
  captures the DOM order into localStorage.
- Polling renderLobby is suppressed while a drag is in flight so the
  5s tick doesn't yank the list out from under the user.
This commit is contained in:
Viktor Barzin 2026-05-13 20:18:15 +00:00
parent 0396fdadb2
commit 29ecb67d8c
2 changed files with 180 additions and 8 deletions

View file

@ -181,21 +181,39 @@ func handleSessions(w http.ResponseWriter, r *http.Request) {
}
func handleSessionByName(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/sessions/")
name = strings.TrimSuffix(name, "/")
path := strings.TrimPrefix(r.URL.Path, "/sessions/")
path = strings.TrimSuffix(path, "/")
parts := strings.Split(path, "/")
name := parts[0]
if !sessionNameRe.MatchString(name) {
http.Error(w, "invalid session name", http.StatusBadRequest)
return
}
if r.Method != http.MethodDelete {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
osUser := resolveOSUser(w, r)
if osUser == "" {
return
}
if len(parts) == 1 {
if r.Method != http.MethodDelete {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
killSession(w, osUser, name)
return
}
if len(parts) == 2 && parts[1] == "rename" {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
renameSession(w, r, osUser, name)
return
}
http.Error(w, "not found", http.StatusNotFound)
}
func killSession(w http.ResponseWriter, osUser, name string) {
out, err := tmuxCmd(osUser, "kill-session", "-t", name).CombinedOutput()
if err != nil {
msg := string(out)
@ -209,3 +227,39 @@ func handleSessionByName(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusNoContent)
}
func renameSession(w http.ResponseWriter, r *http.Request, osUser, oldName string) {
var body struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
newName := strings.TrimSpace(body.Name)
if !sessionNameRe.MatchString(newName) {
http.Error(w, "invalid new name", http.StatusBadRequest)
return
}
if newName == oldName {
w.WriteHeader(http.StatusNoContent)
return
}
out, err := tmuxCmd(osUser, "rename-session", "-t", oldName, newName).CombinedOutput()
if err != nil {
msg := string(out)
if strings.Contains(msg, "can't find session") || strings.Contains(msg, "no server running") {
http.Error(w, "session not found", http.StatusNotFound)
return
}
if strings.Contains(msg, "duplicate session") || strings.Contains(msg, "session already exists") {
http.Error(w, "target name already exists", http.StatusConflict)
return
}
log.Printf("rename-session %s→%s as %s failed: %v: %s", oldName, newName, osUser, err, msg)
http.Error(w, "rename-session failed", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}