4 Commits

3 changed files with 48 additions and 3 deletions

View File

@@ -5,6 +5,14 @@ All notable changes to Seismo Fleet Manager will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.1] - 2026 1-5
### Added
- **SLM integration**: Sound Level meters are now managable in SFM
## Fixed ##
- Fixed an issue where unit status wat loading from a saved cache and not based on when it was actually heard from last. Unit status is now accurate.
## [0.4.0] - 2025-12-16
### Added

View File

@@ -1,4 +1,4 @@
# Seismo Fleet Manager v0.4.0
# Seismo Fleet Manager v0.4.1
Backend API and HTMX-powered web interface for managing a mixed fleet of seismographs and field modems. Track deployments, monitor health in real time, merge roster intent with incoming telemetry, and control your fleet through a unified database and dashboard.
## Features

View File

@@ -24,13 +24,47 @@ def format_age(last_seen):
return f"{int(hours)}h {int(mins)}m"
def calculate_status(last_seen, status_ok_threshold=12, status_pending_threshold=24):
"""
Calculate status based on how long ago the unit was last seen.
Args:
last_seen: datetime of last seen (UTC)
status_ok_threshold: hours before status becomes Pending (default 12)
status_pending_threshold: hours before status becomes Missing (default 24)
Returns:
"OK", "Pending", or "Missing"
"""
if not last_seen:
return "Missing"
last_seen = ensure_utc(last_seen)
now = datetime.now(timezone.utc)
hours_ago = (now - last_seen).total_seconds() / 3600
if hours_ago > status_pending_threshold:
return "Missing"
elif hours_ago > status_ok_threshold:
return "Pending"
else:
return "OK"
def emit_status_snapshot():
"""
Merge roster (what we *intend*) with emitter data (what is *actually happening*).
Status is recalculated based on current time to ensure accuracy.
"""
db = get_db_session()
try:
# Get user preferences for status thresholds
from backend.models import UserPreferences
prefs = db.query(UserPreferences).filter_by(id=1).first()
status_ok_threshold = prefs.status_ok_threshold_hours if prefs else 12
status_pending_threshold = prefs.status_pending_threshold_hours if prefs else 24
roster = {r.id: r for r in db.query(RosterUnit).all()}
emitters = {e.id: e for e in db.query(Emitter).all()}
ignored = {i.id for i in db.query(IgnoredUnit).all()}
@@ -48,8 +82,9 @@ def emit_status_snapshot():
fname = ""
else:
if e:
status = e.status
last_seen = ensure_utc(e.last_seen)
# RECALCULATE status based on current time, not stored value
status = calculate_status(last_seen, status_ok_threshold, status_pending_threshold)
age = format_age(last_seen)
fname = e.last_file
else:
@@ -86,9 +121,11 @@ def emit_status_snapshot():
for unit_id, e in emitters.items():
if unit_id not in roster:
last_seen = ensure_utc(e.last_seen)
# RECALCULATE status for unknown units too
status = calculate_status(last_seen, status_ok_threshold, status_pending_threshold)
units[unit_id] = {
"id": unit_id,
"status": e.status,
"status": status,
"age": format_age(last_seen),
"last": last_seen.isoformat(),
"fname": e.last_file,