pre refactor
This commit is contained in:
@@ -98,6 +98,29 @@ async def roster_table_partial(request: Request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/partials/unknown-emitters", response_class=HTMLResponse)
|
||||||
|
async def unknown_emitters_partial(request: Request):
|
||||||
|
"""Partial template for unknown emitters (HTMX)"""
|
||||||
|
snapshot = emit_status_snapshot()
|
||||||
|
|
||||||
|
unknown_list = []
|
||||||
|
for unit_id, unit_data in snapshot.get("unknown", {}).items():
|
||||||
|
unknown_list.append({
|
||||||
|
"id": unit_id,
|
||||||
|
"status": unit_data["status"],
|
||||||
|
"age": unit_data["age"],
|
||||||
|
"fname": unit_data.get("fname", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by ID
|
||||||
|
unknown_list.sort(key=lambda x: x["id"])
|
||||||
|
|
||||||
|
return templates.TemplateResponse("partials/unknown_emitters.html", {
|
||||||
|
"request": request,
|
||||||
|
"unknown_units": unknown_list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
|
|||||||
@@ -29,3 +29,15 @@ class RosterUnit(Base):
|
|||||||
project_id = Column(String, nullable=True)
|
project_id = Column(String, nullable=True)
|
||||||
location = Column(String, nullable=True)
|
location = Column(String, nullable=True)
|
||||||
last_updated = Column(DateTime, default=datetime.utcnow)
|
last_updated = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class IgnoredUnit(Base):
|
||||||
|
"""
|
||||||
|
Ignored units: units that report but should be filtered out from unknown emitters.
|
||||||
|
Used to suppress noise from old projects.
|
||||||
|
"""
|
||||||
|
__tablename__ = "ignored_units"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True, index=True)
|
||||||
|
reason = Column(String, nullable=True)
|
||||||
|
ignored_at = Column(DateTime, default=datetime.utcnow)
|
||||||
@@ -5,7 +5,7 @@ import csv
|
|||||||
import io
|
import io
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
from backend.models import RosterUnit
|
from backend.models import RosterUnit, IgnoredUnit
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/roster", tags=["roster-edit"])
|
router = APIRouter(prefix="/api/roster", tags=["roster-edit"])
|
||||||
|
|
||||||
@@ -180,3 +180,54 @@ async def import_csv(
|
|||||||
},
|
},
|
||||||
"details": results
|
"details": results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ignore/{unit_id}")
|
||||||
|
def ignore_unit(unit_id: str, reason: str = Form(""), db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Add a unit to the ignore list to suppress it from unknown emitters.
|
||||||
|
"""
|
||||||
|
# Check if already ignored
|
||||||
|
if db.query(IgnoredUnit).filter(IgnoredUnit.id == unit_id).first():
|
||||||
|
raise HTTPException(status_code=400, detail="Unit already ignored")
|
||||||
|
|
||||||
|
ignored = IgnoredUnit(
|
||||||
|
id=unit_id,
|
||||||
|
reason=reason,
|
||||||
|
ignored_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
db.add(ignored)
|
||||||
|
db.commit()
|
||||||
|
return {"message": "Unit ignored", "id": unit_id}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/ignore/{unit_id}")
|
||||||
|
def unignore_unit(unit_id: str, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Remove a unit from the ignore list.
|
||||||
|
"""
|
||||||
|
ignored = db.query(IgnoredUnit).filter(IgnoredUnit.id == unit_id).first()
|
||||||
|
if not ignored:
|
||||||
|
raise HTTPException(status_code=404, detail="Unit not in ignore list")
|
||||||
|
|
||||||
|
db.delete(ignored)
|
||||||
|
db.commit()
|
||||||
|
return {"message": "Unit unignored", "id": unit_id}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ignored")
|
||||||
|
def list_ignored_units(db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get list of all ignored units.
|
||||||
|
"""
|
||||||
|
ignored_units = db.query(IgnoredUnit).all()
|
||||||
|
return {
|
||||||
|
"ignored": [
|
||||||
|
{
|
||||||
|
"id": unit.id,
|
||||||
|
"reason": unit.reason,
|
||||||
|
"ignored_at": unit.ignored_at.isoformat()
|
||||||
|
}
|
||||||
|
for unit in ignored_units
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from backend.database import get_db_session
|
from backend.database import get_db_session
|
||||||
from backend.models import Emitter, RosterUnit
|
from backend.models import Emitter, RosterUnit, IgnoredUnit
|
||||||
|
|
||||||
|
|
||||||
def ensure_utc(dt):
|
def ensure_utc(dt):
|
||||||
@@ -33,6 +33,7 @@ def emit_status_snapshot():
|
|||||||
try:
|
try:
|
||||||
roster = {r.id: r for r in db.query(RosterUnit).all()}
|
roster = {r.id: r for r in db.query(RosterUnit).all()}
|
||||||
emitters = {e.id: e for e in db.query(Emitter).all()}
|
emitters = {e.id: e for e in db.query(Emitter).all()}
|
||||||
|
ignored = {i.id for i in db.query(IgnoredUnit).all()}
|
||||||
|
|
||||||
units = {}
|
units = {}
|
||||||
|
|
||||||
@@ -101,17 +102,25 @@ def emit_status_snapshot():
|
|||||||
if u["retired"]
|
if u["retired"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Unknown units - emitters that aren't in the roster and aren't ignored
|
||||||
|
unknown_units = {
|
||||||
|
uid: u for uid, u in units.items()
|
||||||
|
if uid not in roster and uid not in ignored
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": datetime.utcnow().isoformat(),
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
"units": units,
|
"units": units,
|
||||||
"active": active_units,
|
"active": active_units,
|
||||||
"benched": benched_units,
|
"benched": benched_units,
|
||||||
"retired": retired_units,
|
"retired": retired_units,
|
||||||
|
"unknown": unknown_units,
|
||||||
"summary": {
|
"summary": {
|
||||||
"total": len(units),
|
"total": len(units),
|
||||||
"active": len(active_units),
|
"active": len(active_units),
|
||||||
"benched": len(benched_units),
|
"benched": len(benched_units),
|
||||||
"retired": len(retired_units),
|
"retired": len(retired_units),
|
||||||
|
"unknown": len(unknown_units),
|
||||||
"ok": sum(1 for u in units.values() if u["status"] == "OK"),
|
"ok": sum(1 for u in units.values() if u["status"] == "OK"),
|
||||||
"pending": sum(1 for u in units.values() if u["status"] == "Pending"),
|
"pending": sum(1 for u in units.values() if u["status"] == "Pending"),
|
||||||
"missing": sum(1 for u in units.values() if u["status"] == "Missing"),
|
"missing": sum(1 for u in units.values() if u["status"] == "Missing"),
|
||||||
|
|||||||
@@ -162,11 +162,11 @@ function updateDashboard(event) {
|
|||||||
const data = JSON.parse(event.detail.xhr.response);
|
const data = JSON.parse(event.detail.xhr.response);
|
||||||
|
|
||||||
// ===== Fleet summary numbers =====
|
// ===== Fleet summary numbers =====
|
||||||
document.getElementById('total-units').textContent = data.total_units ?? 0;
|
document.getElementById('total-units').textContent = data.summary?.total ?? 0;
|
||||||
document.getElementById('deployed-units').textContent = data.deployed_units ?? 0;
|
document.getElementById('deployed-units').textContent = data.summary?.active ?? 0;
|
||||||
document.getElementById('status-ok').textContent = data.status_summary.OK ?? 0;
|
document.getElementById('status-ok').textContent = data.summary?.ok ?? 0;
|
||||||
document.getElementById('status-pending').textContent = data.status_summary.Pending ?? 0;
|
document.getElementById('status-pending').textContent = data.summary?.pending ?? 0;
|
||||||
document.getElementById('status-missing').textContent = data.status_summary.Missing ?? 0;
|
document.getElementById('status-missing').textContent = data.summary?.missing ?? 0;
|
||||||
|
|
||||||
// ===== Alerts =====
|
// ===== Alerts =====
|
||||||
const alertsList = document.getElementById('alerts-list');
|
const alertsList = document.getElementById('alerts-list');
|
||||||
|
|||||||
61
templates/partials/unknown_emitters.html
Normal file
61
templates/partials/unknown_emitters.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{% if unknown_units %}
|
||||||
|
<div class="mb-6 rounded-xl shadow-lg bg-yellow-50 dark:bg-yellow-900/20 border-2 border-yellow-400 dark:border-yellow-600 p-6">
|
||||||
|
<div class="flex items-start gap-3 mb-4">
|
||||||
|
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||||
|
</svg>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-xl font-bold text-yellow-900 dark:text-yellow-200">Unknown Emitters Detected</h2>
|
||||||
|
<p class="text-sm text-yellow-800 dark:text-yellow-300 mt-1">
|
||||||
|
{{ unknown_units|length }} unit(s) are reporting but not in the roster. Add them to track them properly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
{% for unit in unknown_units %}
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 flex items-center justify-between border border-yellow-300 dark:border-yellow-700">
|
||||||
|
<div class="flex items-center gap-4 flex-1">
|
||||||
|
<div class="font-mono font-bold text-lg text-gray-900 dark:text-white">
|
||||||
|
{{ unit.id }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 text-sm">
|
||||||
|
<span class="px-2 py-1 rounded-full
|
||||||
|
{% if unit.status == 'OK' %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300
|
||||||
|
{% elif unit.status == 'Pending' %}bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300
|
||||||
|
{% else %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300{% endif %}">
|
||||||
|
{{ unit.status }}
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
|
Last seen: {{ unit.age }}
|
||||||
|
</span>
|
||||||
|
{% if unit.fname %}
|
||||||
|
<span class="text-gray-500 dark:text-gray-500 text-xs truncate max-w-xs">
|
||||||
|
{{ unit.fname }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 flex gap-2">
|
||||||
|
<button
|
||||||
|
onclick="addUnknownUnit('{{ unit.id }}')"
|
||||||
|
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg flex items-center gap-2 transition-colors whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
Add to Roster
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="ignoreUnknownUnit('{{ unit.id }}')"
|
||||||
|
class="px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg flex items-center gap-2 transition-colors whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
|
||||||
|
</svg>
|
||||||
|
Ignore
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unknown Emitters Section -->
|
||||||
|
<div hx-get="/partials/unknown-emitters" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
||||||
|
<!-- Loading placeholder -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Auto-refresh roster every 10 seconds -->
|
<!-- Auto-refresh roster every 10 seconds -->
|
||||||
<div hx-get="/partials/roster-table" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
<div hx-get="/partials/roster-table" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
||||||
<!-- Initial loading state -->
|
<!-- Initial loading state -->
|
||||||
@@ -155,6 +160,42 @@
|
|||||||
document.getElementById('addUnitForm').reset();
|
document.getElementById('addUnitForm').reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add unknown unit to roster
|
||||||
|
function addUnknownUnit(unitId) {
|
||||||
|
openAddUnitModal();
|
||||||
|
// Pre-fill the unit ID
|
||||||
|
document.querySelector('#addUnitForm input[name="id"]').value = unitId;
|
||||||
|
// Set deployed to true by default
|
||||||
|
document.querySelector('#addUnitForm input[name="deployed"]').checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore unknown unit
|
||||||
|
async function ignoreUnknownUnit(unitId) {
|
||||||
|
if (!confirm(`Ignore unit ${unitId}? It will no longer appear in the unknown emitters list.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('reason', 'Ignored from unknown emitters');
|
||||||
|
|
||||||
|
const response = await fetch(`/api/roster/ignore/${unitId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Trigger refresh of unknown emitters
|
||||||
|
htmx.trigger(document.querySelector('[hx-get="/partials/unknown-emitters"]'), 'load');
|
||||||
|
} else {
|
||||||
|
const result = await response.json();
|
||||||
|
alert(`Error ignoring unit: ${result.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Error ignoring unit: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Import Modal
|
// Import Modal
|
||||||
function openImportModal() {
|
function openImportModal() {
|
||||||
document.getElementById('importModal').classList.remove('hidden');
|
document.getElementById('importModal').classList.remove('hidden');
|
||||||
|
|||||||
Reference in New Issue
Block a user