Files
MATTERMOST_OPENCLAW_LIVESTATUS/plugin/server/plugin.go
sol 79d5e82803 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
2026-03-09 14:15:04 +00:00

105 lines
2.8 KiB
Go

package main
import (
"sync"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
// Plugin implements the Mattermost plugin interface.
type Plugin struct {
plugin.MattermostPlugin
// configurationLock synchronizes access to the configuration.
configurationLock sync.RWMutex
// configuration is the active plugin configuration.
configuration *Configuration
// store wraps KV store operations for session persistence.
store *Store
// botUserID is the plugin's bot user ID (created on activation).
botUserID string
// stopCleanup signals the cleanup goroutine to stop.
stopCleanup chan struct{}
}
// OnActivate is called when the plugin is activated.
func (p *Plugin) OnActivate() error {
p.store = NewStore(p.API)
// Ensure plugin bot user exists
botID, appErr := p.API.EnsureBotUser(&model.Bot{
Username: "livestatus",
DisplayName: "Live Status",
Description: "OpenClaw Live Status plugin bot",
})
if appErr != nil {
p.API.LogWarn("Failed to ensure bot user", "error", appErr.Error())
} else {
p.botUserID = botID
p.API.LogInfo("Plugin bot user ensured", "botUserID", botID)
}
// Start session cleanup goroutine
p.stopCleanup = make(chan struct{})
go p.sessionCleanupLoop()
p.API.LogInfo("OpenClaw Live Status plugin activated")
return nil
}
// sessionCleanupLoop runs periodically to clean up stale and expired sessions.
func (p *Plugin) sessionCleanupLoop() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
staleThreshold := int64(30 * 60 * 1000) // 30 minutes — active sessions with no update
expireThreshold := int64(60 * 60 * 1000) // 1 hour — completed/interrupted sessions
cleaned, expired, err := p.store.CleanStaleSessions(staleThreshold, expireThreshold)
if err != nil {
p.API.LogWarn("Session cleanup error", "error", err.Error())
} else if cleaned > 0 || expired > 0 {
p.API.LogInfo("Session cleanup completed", "stale_marked", cleaned, "expired_deleted", expired)
}
case <-p.stopCleanup:
return
}
}
}
// getBotUserID returns the plugin's bot user ID.
func (p *Plugin) getBotUserID() string {
return p.botUserID
}
// OnDeactivate is called when the plugin is deactivated.
func (p *Plugin) OnDeactivate() error {
// Stop cleanup goroutine
if p.stopCleanup != nil {
close(p.stopCleanup)
}
// Mark all active sessions as interrupted
sessions, err := p.store.ListAllSessions()
if err != nil {
p.API.LogWarn("Failed to list sessions on deactivate", "error", err.Error())
} else {
for _, s := range sessions {
s.Status = "interrupted"
_ = p.store.SaveSession(s.SessionKey, s)
p.broadcastUpdate(s.ChannelID, s)
}
}
p.API.LogInfo("OpenClaw Live Status plugin deactivated")
return nil
}