This repository has been archived on 2026-05-07. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
ccrm/server/internal/db/sessions.go

146 lines
4.8 KiB
Go

package db
import (
"database/sql"
"time"
)
// Session represents a session row in the database.
type Session struct {
ID string `json:"id"`
MachineID string `json:"machine_id"`
Name string `json:"name"`
TmuxSession string `json:"tmux_session"`
ProjectPath string `json:"project_path"`
ClaudeSessionID string `json:"claude_session_id"`
Status string `json:"status"`
IsWorktree bool `json:"is_worktree"`
WorktreeBranch string `json:"worktree_branch"`
CreatedAt time.Time `json:"created_at"`
EndedAt *time.Time `json:"ended_at,omitempty"`
}
// UpsertSession creates or updates a session.
func (db *DB) UpsertSession(s *Session) error {
_, err := db.Exec(`
INSERT INTO sessions (id, machine_id, name, tmux_session, project_path, claude_session_id, status, is_worktree, worktree_branch, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(machine_id, name) DO UPDATE SET
status = excluded.status,
claude_session_id = COALESCE(NULLIF(excluded.claude_session_id, ''), sessions.claude_session_id),
tmux_session = excluded.tmux_session,
project_path = excluded.project_path
`, s.ID, s.MachineID, s.Name, s.TmuxSession, s.ProjectPath, s.ClaudeSessionID, s.Status, s.IsWorktree, s.WorktreeBranch, s.CreatedAt)
return err
}
// UpdateSessionStatus updates a session's status.
func (db *DB) UpdateSessionStatus(name, machineID, status string) error {
query := "UPDATE sessions SET status = ?"
args := []any{status}
if status == "stopped" {
query += ", ended_at = ?"
args = append(args, time.Now().UTC())
}
query += " WHERE name = ? AND machine_id = ?"
args = append(args, name, machineID)
_, err := db.Exec(query, args...)
return err
}
// GetSession returns a session by ID.
func (db *DB) GetSession(id string) (*Session, error) {
s := &Session{}
var endedAt sql.NullTime
err := db.QueryRow(`
SELECT id, machine_id, name, tmux_session, project_path, claude_session_id, status, is_worktree, worktree_branch, created_at, ended_at
FROM sessions WHERE id = ?
`, id).Scan(&s.ID, &s.MachineID, &s.Name, &s.TmuxSession, &s.ProjectPath, &s.ClaudeSessionID, &s.Status, &s.IsWorktree, &s.WorktreeBranch, &s.CreatedAt, &endedAt)
if err != nil {
return nil, err
}
if endedAt.Valid {
s.EndedAt = &endedAt.Time
}
return s, nil
}
// GetSessionByName returns a session by name and machine ID.
func (db *DB) GetSessionByName(name, machineID string) (*Session, error) {
s := &Session{}
var endedAt sql.NullTime
err := db.QueryRow(`
SELECT id, machine_id, name, tmux_session, project_path, claude_session_id, status, is_worktree, worktree_branch, created_at, ended_at
FROM sessions WHERE name = ? AND machine_id = ?
`, name, machineID).Scan(&s.ID, &s.MachineID, &s.Name, &s.TmuxSession, &s.ProjectPath, &s.ClaudeSessionID, &s.Status, &s.IsWorktree, &s.WorktreeBranch, &s.CreatedAt, &endedAt)
if err != nil {
return nil, err
}
if endedAt.Valid {
s.EndedAt = &endedAt.Time
}
return s, nil
}
// ReconcileSessions marks any active sessions for a machine as stopped
// if they are not in the provided list of live session names. This is
// called on heartbeat to clean up dangling sessions.
func (db *DB) ReconcileSessions(machineID string, liveNames []string) error {
if len(liveNames) == 0 {
// No live sessions — stop all active sessions for this machine
_, err := db.Exec(`
UPDATE sessions SET status = 'stopped', ended_at = ?
WHERE machine_id = ? AND status != 'stopped'
`, time.Now().UTC(), machineID)
return err
}
// Build placeholders for the IN clause
placeholders := ""
args := []any{time.Now().UTC(), machineID}
for i, name := range liveNames {
if i > 0 {
placeholders += ","
}
placeholders += "?"
args = append(args, name)
}
_, err := db.Exec(`
UPDATE sessions SET status = 'stopped', ended_at = ?
WHERE machine_id = ? AND status != 'stopped' AND name NOT IN (`+placeholders+`)
`, args...)
return err
}
// ListSessions returns all sessions, optionally filtered by status.
func (db *DB) ListSessions(activeOnly bool) ([]*Session, error) {
query := `SELECT id, machine_id, name, tmux_session, project_path, claude_session_id, status, is_worktree, worktree_branch, created_at, ended_at FROM sessions`
if activeOnly {
query += ` WHERE status != 'stopped'`
}
query += ` ORDER BY created_at DESC`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var sessions []*Session
for rows.Next() {
s := &Session{}
var endedAt sql.NullTime
if err := rows.Scan(&s.ID, &s.MachineID, &s.Name, &s.TmuxSession, &s.ProjectPath, &s.ClaudeSessionID, &s.Status, &s.IsWorktree, &s.WorktreeBranch, &s.CreatedAt, &endedAt); err != nil {
return nil, err
}
if endedAt.Valid {
s.EndedAt = &endedAt.Time
}
sessions = append(sessions, s)
}
return sessions, nil
}