146 lines
4.8 KiB
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
|
|
}
|