Update to v 0.4.0 #6
+19
@@ -38,6 +38,25 @@ async def lifespan(app: FastAPI):
|
|||||||
await poller.start()
|
await poller.start()
|
||||||
logger.info("Background poller started")
|
logger.info("Background poller started")
|
||||||
|
|
||||||
|
# Auto-start keepalive live monitors for units configured for 24/7 monitoring
|
||||||
|
# (monitor_enabled). This is what keeps alerting running unattended across
|
||||||
|
# restarts — without it a feed only runs while someone has the live view open.
|
||||||
|
try:
|
||||||
|
from app.monitor import monitor_manager
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models import NL43Config
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
units = db.query(NL43Config).filter_by(monitor_enabled=True, tcp_enabled=True).all()
|
||||||
|
for cfg in units:
|
||||||
|
m = await monitor_manager.get(cfg.unit_id)
|
||||||
|
await m.set_keepalive(True)
|
||||||
|
logger.info(f"Auto-started keepalive monitor for {cfg.unit_id}")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to auto-start monitors: {e}")
|
||||||
|
|
||||||
yield # Application runs
|
yield # Application runs
|
||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ class NL43Config(Base):
|
|||||||
poll_interval_seconds = Column(Integer, nullable=True, default=60) # Polling interval (10-3600 seconds)
|
poll_interval_seconds = Column(Integer, nullable=True, default=60) # Polling interval (10-3600 seconds)
|
||||||
poll_enabled = Column(Boolean, default=True) # Enable/disable background polling for this device
|
poll_enabled = Column(Boolean, default=True) # Enable/disable background polling for this device
|
||||||
|
|
||||||
|
# Live monitor (fan-out DOD feed). Keepalive runs it 24/7 even with no viewer,
|
||||||
|
# which is what makes alerting continuous. On by default; toggleable from the UI.
|
||||||
|
monitor_enabled = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
|
||||||
class NL43Status(Base):
|
class NL43Status(Base):
|
||||||
"""
|
"""
|
||||||
|
|||||||
+18
-7
@@ -295,22 +295,32 @@ async def monitor_stream(websocket: WebSocket, unit_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/{unit_id}/monitor/start")
|
@router.post("/{unit_id}/monitor/start")
|
||||||
async def monitor_start(unit_id: str):
|
async def monitor_start(unit_id: str, db: Session = Depends(get_db)):
|
||||||
"""Keep the device's feed running even with no browser attached, so alerting
|
"""Enable 24/7 keepalive monitoring: persist monitor_enabled and start the feed
|
||||||
evaluates continuously. Runtime-only (resets on restart)."""
|
now, so alerting evaluates continuously even with no viewer. Survives restarts
|
||||||
|
(auto-started on boot from the persisted flag)."""
|
||||||
|
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
||||||
|
if cfg:
|
||||||
|
cfg.monitor_enabled = True
|
||||||
|
db.commit()
|
||||||
from app.monitor import monitor_manager
|
from app.monitor import monitor_manager
|
||||||
monitor = await monitor_manager.get(unit_id)
|
monitor = await monitor_manager.get(unit_id)
|
||||||
await monitor.set_keepalive(True)
|
await monitor.set_keepalive(True)
|
||||||
return {"status": "ok", "unit_id": unit_id, "running": monitor.running, "keepalive": True}
|
return {"status": "ok", "unit_id": unit_id, "monitor_enabled": True, "running": monitor.running}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{unit_id}/monitor/stop")
|
@router.post("/{unit_id}/monitor/stop")
|
||||||
async def monitor_stop(unit_id: str):
|
async def monitor_stop(unit_id: str, db: Session = Depends(get_db)):
|
||||||
"""Drop the keep-alive; the feed stops once no browser subscribers remain."""
|
"""Disable keepalive monitoring: persist monitor_enabled=False and drop the
|
||||||
|
keepalive (the feed stops once no browser subscribers remain)."""
|
||||||
|
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
||||||
|
if cfg:
|
||||||
|
cfg.monitor_enabled = False
|
||||||
|
db.commit()
|
||||||
from app.monitor import monitor_manager
|
from app.monitor import monitor_manager
|
||||||
monitor = await monitor_manager.get(unit_id)
|
monitor = await monitor_manager.get(unit_id)
|
||||||
await monitor.set_keepalive(False)
|
await monitor.set_keepalive(False)
|
||||||
return {"status": "ok", "unit_id": unit_id, "keepalive": False}
|
return {"status": "ok", "unit_id": unit_id, "monitor_enabled": False}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/_monitor/status")
|
@router.get("/_monitor/status")
|
||||||
@@ -501,6 +511,7 @@ def get_roster(db: Session = Depends(get_db)):
|
|||||||
"web_enabled": cfg.web_enabled,
|
"web_enabled": cfg.web_enabled,
|
||||||
"poll_enabled": cfg.poll_enabled,
|
"poll_enabled": cfg.poll_enabled,
|
||||||
"poll_interval_seconds": cfg.poll_interval_seconds,
|
"poll_interval_seconds": cfg.poll_interval_seconds,
|
||||||
|
"monitor_enabled": cfg.monitor_enabled,
|
||||||
"status": None
|
"status": None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migration: add monitor_enabled column to nl43_config.
|
||||||
|
|
||||||
|
Controls whether the live fan-out DOD monitor is kept alive 24/7 for a unit
|
||||||
|
(which is what makes alerting continuous). Defaults to enabled. Run once per DB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DB_PATH = Path(__file__).parent / "data" / "slmm.db"
|
||||||
|
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
if not DB_PATH.exists():
|
||||||
|
print(f"Database not found at {DB_PATH}")
|
||||||
|
print("No migration needed - database will be created with new schema")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute("PRAGMA table_info(nl43_config)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
if "monitor_enabled" in columns:
|
||||||
|
print("✓ monitor_enabled column already exists, no migration needed")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Adding monitor_enabled column (default enabled)...")
|
||||||
|
# SQLite stores booleans as 0/1; default 1 = enabled.
|
||||||
|
cursor.execute("ALTER TABLE nl43_config ADD COLUMN monitor_enabled BOOLEAN DEFAULT 1")
|
||||||
|
conn.commit()
|
||||||
|
print("✓ Added monitor_enabled column")
|
||||||
|
print("\n✓ Migration completed successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"✗ Migration failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
Reference in New Issue
Block a user