Compare commits
3 Commits
0f47b69c92
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ea64c3561 | |||
| 1a87ff13c9 | |||
| 22c62c0729 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,23 @@ All notable changes to Terra-View will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.8.0] - 2026-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Watcher Manager**: New admin page (`/admin/watchers`) for monitoring field watcher agents
|
||||||
|
- Live status cards per agent showing connectivity, version, IP, last-seen age, and log tail
|
||||||
|
- Trigger Update button to queue a self-update on the agent's next heartbeat
|
||||||
|
- Expand/collapse log tail with full-log expand mode
|
||||||
|
- Live surgical refresh every 30 seconds via `/api/admin/watchers` — no full page reload, open logs stay open
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Watcher status logic**: Agent status now reflects whether Terra-View is hearing from the watcher (ok if seen within 60 minutes, missing otherwise) — previously reflected the worst unit status from the last heartbeat payload, which caused false alarms when units went missing
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Watcher Manager meta row**: Dark mode background was white due to invalid `dark:bg-slate-850` Tailwind class; corrected to `dark:bg-slate-800`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.7.1] - 2026-03-12
|
## [0.7.1] - 2026-03-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Terra-View v0.7.1
|
# Terra-View v0.8.0
|
||||||
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.
|
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
|
## Features
|
||||||
@@ -496,6 +496,11 @@ docker compose down -v
|
|||||||
|
|
||||||
## Release Highlights
|
## Release Highlights
|
||||||
|
|
||||||
|
### v0.8.0 — 2026-03-18
|
||||||
|
- **Watcher Manager**: Admin page for monitoring field watcher agents with live status cards, log tails, and one-click update triggering
|
||||||
|
- **Watcher Status Fix**: Agent status now reflects heartbeat connectivity (missing if not heard from in >60 min) rather than unit-level data staleness
|
||||||
|
- **Live Refresh**: Watcher Manager surgically patches status, last-seen, and pending indicators every 30s without a full page reload
|
||||||
|
|
||||||
### v0.7.0 — 2026-03-07
|
### v0.7.0 — 2026-03-07
|
||||||
- **Project Status Management**: On-hold and archived project states with automatic cancellation of pending actions
|
- **Project Status Management**: On-hold and archived project states with automatic cancellation of pending actions
|
||||||
- **Manual SD Card Upload**: Upload offline NRL/SLM data directly from SD card (ZIP or multi-file); auto-creates monitoring sessions from `.rnh` metadata
|
- **Manual SD Card Upload**: Upload offline NRL/SLM data directly from SD card (ZIP or multi-file); auto-creates monitoring sessions from `.rnh` metadata
|
||||||
@@ -594,9 +599,13 @@ MIT
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
**Current: 0.7.0** — Project status management, manual SD card upload, combined report wizard, NL32 support, MonitoringSession rename (2026-03-07)
|
**Current: 0.8.0** — Watcher Manager admin page, live agent status refresh, watcher connectivity-based status (2026-03-18)
|
||||||
|
|
||||||
Previous: 0.6.1 — One-off recording schedules, bidirectional pairing sync, scheduler timezone fix (2026-02-16)
|
Previous: 0.7.1 — Out-for-calibration status, reservation modal, migration fixes (2026-03-12)
|
||||||
|
|
||||||
|
0.7.0 — Project status management, manual SD card upload, combined report wizard, NL32 support, MonitoringSession rename (2026-03-07)
|
||||||
|
|
||||||
|
0.6.1 — One-off recording schedules, bidirectional pairing sync, scheduler timezone fix (2026-02-16)
|
||||||
|
|
||||||
0.6.0 — Calendar & reservation mode, device pairing interface, calibration UX overhaul, modem dashboard enhancements (2026-02-06)
|
0.6.0 — Calendar & reservation mode, device pairing interface, calibration UX overhaul, modem dashboard enhancements (2026-02-06)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Base.metadata.create_all(bind=engine)
|
|||||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
|
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
|
||||||
|
|
||||||
# Initialize FastAPI app
|
# Initialize FastAPI app
|
||||||
VERSION = "0.7.1"
|
VERSION = "0.8.0"
|
||||||
if ENVIRONMENT == "development":
|
if ENVIRONMENT == "development":
|
||||||
_build = os.getenv("BUILD_NUMBER", "0")
|
_build = os.getenv("BUILD_NUMBER", "0")
|
||||||
if _build and _build != "0":
|
if _build and _build != "0":
|
||||||
|
|||||||
@@ -31,11 +31,15 @@ router = APIRouter(tags=["admin"])
|
|||||||
def _agent_to_dict(agent: WatcherAgent) -> dict:
|
def _agent_to_dict(agent: WatcherAgent) -> dict:
|
||||||
last_seen = agent.last_seen
|
last_seen = agent.last_seen
|
||||||
if last_seen:
|
if last_seen:
|
||||||
# Compute age in minutes (last_seen stored as UTC naive)
|
|
||||||
now_utc = datetime.utcnow()
|
now_utc = datetime.utcnow()
|
||||||
age_minutes = int((now_utc - last_seen).total_seconds() // 60)
|
age_minutes = int((now_utc - last_seen).total_seconds() // 60)
|
||||||
|
if age_minutes > 60:
|
||||||
|
status = "missing"
|
||||||
|
else:
|
||||||
|
status = "ok"
|
||||||
else:
|
else:
|
||||||
age_minutes = None
|
age_minutes = None
|
||||||
|
status = "missing"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": agent.id,
|
"id": agent.id,
|
||||||
@@ -43,7 +47,7 @@ def _agent_to_dict(agent: WatcherAgent) -> dict:
|
|||||||
"version": agent.version,
|
"version": agent.version,
|
||||||
"last_seen": last_seen.isoformat() if last_seen else None,
|
"last_seen": last_seen.isoformat() if last_seen else None,
|
||||||
"age_minutes": age_minutes,
|
"age_minutes": age_minutes,
|
||||||
"status": agent.status,
|
"status": status,
|
||||||
"ip_address": agent.ip_address,
|
"ip_address": agent.ip_address,
|
||||||
"log_tail": agent.log_tail,
|
"log_tail": agent.log_tail,
|
||||||
"update_pending": bool(agent.update_pending),
|
"update_pending": bool(agent.update_pending),
|
||||||
|
|||||||
@@ -216,20 +216,9 @@ async def series3_heartbeat(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
results.append({"unit": uid, "status": status})
|
results.append({"unit": uid, "status": status})
|
||||||
|
|
||||||
# Determine overall worst status for the watcher agent row
|
|
||||||
statuses = [r["status"] for r in results]
|
|
||||||
if "Missing" in statuses:
|
|
||||||
agent_status = "missing"
|
|
||||||
elif "Pending" in statuses:
|
|
||||||
agent_status = "pending"
|
|
||||||
elif statuses:
|
|
||||||
agent_status = "ok"
|
|
||||||
else:
|
|
||||||
agent_status = "unknown"
|
|
||||||
|
|
||||||
if source:
|
if source:
|
||||||
_upsert_watcher_agent(db, source, "series3_watcher", version,
|
_upsert_watcher_agent(db, source, "series3_watcher", version,
|
||||||
client_ip, log_tail_str, agent_status)
|
client_ip, log_tail_str, "ok")
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -340,20 +329,9 @@ async def series4_heartbeat(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
results.append({"unit": uid, "status": status})
|
results.append({"unit": uid, "status": status})
|
||||||
|
|
||||||
# Determine overall worst status for the watcher agent row
|
|
||||||
statuses = [r["status"] for r in results]
|
|
||||||
if any(s.lower() == "stale" for s in statuses):
|
|
||||||
agent_status = "missing"
|
|
||||||
elif any(s.lower() == "late" for s in statuses):
|
|
||||||
agent_status = "pending"
|
|
||||||
elif statuses:
|
|
||||||
agent_status = "ok"
|
|
||||||
else:
|
|
||||||
agent_status = "unknown"
|
|
||||||
|
|
||||||
if source:
|
if source:
|
||||||
_upsert_watcher_agent(db, source, "series4_watcher", version,
|
_upsert_watcher_agent(db, source, "series4_watcher", version,
|
||||||
client_ip, log_tail_str, agent_status)
|
client_ip, log_tail_str, "ok")
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user