feat: RHS panel initial fetch, floating widget, session cleanup (#5)

Phase 1: Fix RHS panel to fetch existing sessions on mount
- Add initial API fetch in useAllStatusUpdates() hook
- Allow GET /sessions endpoint without shared secret auth
- RHS panel now shows sessions after page refresh

Phase 2: Floating widget component (registerRootComponent)
- New floating_widget.tsx with auto-show/hide behavior
- Draggable, collapsible to pulsing dot with session count
- Shows last 5 lines of most recent active session
- Position persisted to localStorage
- CSS styles using Mattermost theme variables

Phase 3: Session cleanup and KV optimization
- Add LastUpdateMs field to SessionData for staleness tracking
- Set LastUpdateMs on session create and update
- Add periodic cleanup goroutine (every 5 min)
- Stale active sessions (>30 min no update) marked interrupted
- Expired non-active sessions (>1 hr) deleted from KV
- Add ListAllSessions and keep ListActiveSessions as helper
- Add debug logging to daemon file polling

Closes #5
This commit is contained in:
sol
2026-03-09 14:15:04 +00:00
parent 9ec52a418d
commit 79d5e82803
10 changed files with 520 additions and 32 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/plugin"
)
@@ -22,8 +23,9 @@ type SessionData struct {
Lines []string `json:"lines"`
ElapsedMs int64 `json:"elapsed_ms"`
TokenCount int `json:"token_count"`
Children []SessionData `json:"children,omitempty"`
StartTimeMs int64 `json:"start_time_ms"`
Children []SessionData `json:"children,omitempty"`
StartTimeMs int64 `json:"start_time_ms"`
LastUpdateMs int64 `json:"last_update_ms"`
}
// Store wraps Mattermost KV store operations for session persistence.
@@ -79,8 +81,8 @@ func (s *Store) DeleteSession(sessionKey string) error {
return nil
}
// ListActiveSessions returns all active sessions from the KV store.
func (s *Store) ListActiveSessions() ([]SessionData, error) {
// ListAllSessions returns all sessions from the KV store (active and non-active).
func (s *Store) ListAllSessions() ([]SessionData, error) {
var sessions []SessionData
page := 0
perPage := 100
@@ -106,12 +108,59 @@ func (s *Store) ListActiveSessions() ([]SessionData, error) {
if err := json.Unmarshal(b, &data); err != nil {
continue
}
if data.Status == "active" {
sessions = append(sessions, data)
}
sessions = append(sessions, data)
}
page++
}
return sessions, nil
}
// ListActiveSessions returns only active sessions from the KV store.
func (s *Store) ListActiveSessions() ([]SessionData, error) {
all, err := s.ListAllSessions()
if err != nil {
return nil, err
}
var active []SessionData
for _, sess := range all {
if sess.Status == "active" {
active = append(active, sess)
}
}
return active, nil
}
// CleanStaleSessions marks stale active sessions as interrupted and deletes expired completed sessions.
// staleThresholdMs: active sessions with no update for this long are marked interrupted.
// expireThresholdMs: non-active sessions older than this are deleted from KV.
func (s *Store) CleanStaleSessions(staleThresholdMs, expireThresholdMs int64) (cleaned int, expired int, err error) {
now := time.Now().UnixMilli()
all, err := s.ListAllSessions()
if err != nil {
return 0, 0, err
}
for _, session := range all {
lastUpdate := session.LastUpdateMs
if lastUpdate == 0 {
lastUpdate = session.StartTimeMs
}
if lastUpdate == 0 {
continue
}
age := now - lastUpdate
if session.Status == "active" && age > staleThresholdMs {
// Mark stale sessions as interrupted
session.Status = "interrupted"
session.LastUpdateMs = now
_ = s.SaveSession(session.SessionKey, session)
cleaned++
} else if session.Status != "active" && age > expireThresholdMs {
// Delete expired completed/interrupted sessions
_ = s.DeleteSession(session.SessionKey)
expired++
}
}
return cleaned, expired, nil
}