Add monitor-only mode: suppress all actions without stopping EMS
New internal/monitor package provides a thread-safe, file-backed toggle. Flag file presence (/etc/ems/monitor-only) = monitor-only active, survives reboots. Deletion = resume normal operation. Behaviour when active: - EMS continues polling Prometheus, running the decision engine, and updating the status page every 2 minutes — full visibility maintained - All actuator calls suppressed (Shelly switches + Viessmann WW writes) - Suppressed actions logged as [MONITOR-ONLY] for audit trail - Startup warns if flag file is already present Web UI: - Amber banner "⏸ Monitor-Only — Keine Schaltvorgänge" with inline "▶ Automatik" resume button when active - Small unobtrusive "⏸ Monitor-Only" button at page bottom when inactive - POST /monitor endpoint toggles state and redirects back to status page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
61
internal/monitor/mode.go
Normal file
61
internal/monitor/mode.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Package monitor provides a persistent monitor-only mode toggle.
|
||||
// When active, the EMS continues polling and showing the status page
|
||||
// but suppresses all actuator calls (Shelly switches, Viessmann API).
|
||||
// State is stored as a flag file: presence = active, absence = normal.
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Mode is a thread-safe, file-backed monitor-only flag.
|
||||
type Mode struct {
|
||||
mu sync.RWMutex
|
||||
active bool
|
||||
filePath string
|
||||
}
|
||||
|
||||
// New creates a Mode and loads the current state from the flag file.
|
||||
// If filePath is empty, the mode is in-memory only (not persistent).
|
||||
func New(filePath string) *Mode {
|
||||
m := &Mode{filePath: filePath}
|
||||
if filePath != "" {
|
||||
_, err := os.Stat(filePath)
|
||||
m.active = err == nil // file exists → monitor-only
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// IsActive reports whether monitor-only mode is currently enabled.
|
||||
func (m *Mode) IsActive() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.active
|
||||
}
|
||||
|
||||
// Set enables or disables monitor-only mode and persists the change to disk.
|
||||
func (m *Mode) Set(active bool) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.active = active
|
||||
|
||||
if m.filePath == "" {
|
||||
return nil // in-memory only
|
||||
}
|
||||
|
||||
if active {
|
||||
f, err := os.Create(m.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
err := os.Remove(m.filePath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user