diff --git a/.gitignore b/.gitignore index 466d45f..8458942 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Terra-View Specifics # Dev build counter (local only, never commit) build_number.txt +docker-compose.override.yml # SQLite database files *.db @@ -9,7 +10,7 @@ data/ data-dev/ .aider* .aider* -docker-compose.override.yml + # Byte-compiled / optimized / DLL files __pycache__/ @@ -219,3 +220,14 @@ marimo/_static/ marimo/_lsp/ __marimo__/ +<<<<<<< HEAD +# Seismo Fleet Manager +# SQLite database files +*.db +*.db-journal +/data/ +/data-dev/ +.aider* +.aider* +======= +>>>>>>> 0c2186f5d89d948b0357d674c0773a67a67d8027 diff --git a/CHANGELOG.md b/CHANGELOG.md index 756074f..0801df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.1] - 2026-03-12 + +### Added +- **"Out for Calibration" Unit Status**: New `out_for_cal` status for units currently away for calibration, with visual indicators in the roster, unit list, and seismograph stats panel +- **Reservation Modal**: Fleet calendar reservation modal is now fully functional for creating and managing device reservations + +### Changed +- **Retire Unit Button**: Redesigned to be more visually prominent/destructive to reduce accidental clicks + +### Fixed +- **Migration Scripts**: Fixed database path references in several migration scripts +- **Docker Compose**: Removed dev override file from the repository; dev environment config kept separate + +### Migration Notes +Run the following migration script once per database before deploying: +```bash +python backend/migrate_add_out_for_calibration.py +``` + +--- + ## [0.7.0] - 2026-03-07 ### Added diff --git a/backend/main.py b/backend/main.py index fe95f59..ee55012 100644 --- a/backend/main.py +++ b/backend/main.py @@ -30,7 +30,7 @@ Base.metadata.create_all(bind=engine) ENVIRONMENT = os.getenv("ENVIRONMENT", "production") # Initialize FastAPI app -VERSION = "0.7.0" +VERSION = "0.7.1" if ENVIRONMENT == "development": _build = os.getenv("BUILD_NUMBER", "0") if _build and _build != "0": @@ -659,6 +659,7 @@ async def devices_all_partial(request: Request): "last_seen": unit_data.get("last", "Never"), "deployed": True, "retired": False, + "out_for_calibration": False, "ignored": False, "note": unit_data.get("note", ""), "device_type": unit_data.get("device_type", "seismograph"), @@ -683,6 +684,32 @@ async def devices_all_partial(request: Request): "last_seen": unit_data.get("last", "Never"), "deployed": False, "retired": False, + "out_for_calibration": False, + "ignored": False, + "note": unit_data.get("note", ""), + "device_type": unit_data.get("device_type", "seismograph"), + "address": unit_data.get("address", ""), + "coordinates": unit_data.get("coordinates", ""), + "project_id": unit_data.get("project_id", ""), + "last_calibrated": unit_data.get("last_calibrated"), + "next_calibration_due": unit_data.get("next_calibration_due"), + "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"), + "deployed_with_unit_id": unit_data.get("deployed_with_unit_id"), + "ip_address": unit_data.get("ip_address"), + "phone_number": unit_data.get("phone_number"), + "hardware_model": unit_data.get("hardware_model"), + }) + + # Add out-for-calibration units + for unit_id, unit_data in snapshot["out_for_calibration"].items(): + units_list.append({ + "id": unit_id, + "status": "Out for Calibration", + "age": "N/A", + "last_seen": "N/A", + "deployed": False, + "retired": False, + "out_for_calibration": True, "ignored": False, "note": unit_data.get("note", ""), "device_type": unit_data.get("device_type", "seismograph"), @@ -707,6 +734,7 @@ async def devices_all_partial(request: Request): "last_seen": "N/A", "deployed": False, "retired": True, + "out_for_calibration": False, "ignored": False, "note": unit_data.get("note", ""), "device_type": unit_data.get("device_type", "seismograph"), @@ -731,6 +759,7 @@ async def devices_all_partial(request: Request): "last_seen": "N/A", "deployed": False, "retired": False, + "out_for_calibration": False, "ignored": True, "note": unit_data.get("note", unit_data.get("reason", "")), "device_type": unit_data.get("device_type", "unknown"), @@ -748,15 +777,17 @@ async def devices_all_partial(request: Request): # Sort by status category, then by ID def sort_key(unit): - # Priority: deployed (active) -> benched -> retired -> ignored + # Priority: deployed (active) -> benched -> out_for_calibration -> retired -> ignored if unit["deployed"]: return (0, unit["id"]) - elif not unit["retired"] and not unit["ignored"]: + elif not unit["retired"] and not unit["out_for_calibration"] and not unit["ignored"]: return (1, unit["id"]) - elif unit["retired"]: + elif unit["out_for_calibration"]: return (2, unit["id"]) - else: + elif unit["retired"]: return (3, unit["id"]) + else: + return (4, unit["id"]) units_list.sort(key=sort_key) diff --git a/backend/migrate_add_out_for_calibration.py b/backend/migrate_add_out_for_calibration.py new file mode 100644 index 0000000..e373cd0 --- /dev/null +++ b/backend/migrate_add_out_for_calibration.py @@ -0,0 +1,54 @@ +""" +Database Migration: Add out_for_calibration field to roster table + +Changes: +- Adds out_for_calibration BOOLEAN column (default FALSE) to roster table +- Safe to run multiple times (idempotent) +- No data loss + +Usage: + python backend/migrate_add_out_for_calibration.py +""" + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./data/seismo_fleet.db" +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def migrate(): + db = SessionLocal() + try: + print("=" * 60) + print("Migration: Add out_for_calibration to roster") + print("=" * 60) + + # Check if column already exists + result = db.execute(text("PRAGMA table_info(roster)")).fetchall() + columns = [row[1] for row in result] + + if "out_for_calibration" in columns: + print("Column out_for_calibration already exists. Skipping.") + else: + db.execute(text("ALTER TABLE roster ADD COLUMN out_for_calibration BOOLEAN DEFAULT FALSE")) + db.commit() + print("Added out_for_calibration column to roster table.") + + print("Migration complete.") + except Exception as e: + db.rollback() + print(f"Error: {e}") + raise + finally: + db.close() + + +if __name__ == "__main__": + migrate() diff --git a/backend/migrate_add_project_deleted_at.py b/backend/migrate_add_project_deleted_at.py index d15ed34..1f07431 100644 --- a/backend/migrate_add_project_deleted_at.py +++ b/backend/migrate_add_project_deleted_at.py @@ -44,7 +44,7 @@ def migrate(db_path: str): if __name__ == "__main__": - db_path = "./data/terra-view.db" + db_path = "./data/seismo_fleet.db" if len(sys.argv) > 1: db_path = sys.argv[1] diff --git a/backend/migrate_rename_recording_to_monitoring_sessions.py b/backend/migrate_rename_recording_to_monitoring_sessions.py index 475ed67..c62f416 100644 --- a/backend/migrate_rename_recording_to_monitoring_sessions.py +++ b/backend/migrate_rename_recording_to_monitoring_sessions.py @@ -42,7 +42,7 @@ def migrate(db_path: str): if __name__ == "__main__": - db_path = "./data/terra-view.db" + db_path = "./data/seismo_fleet.db" if len(sys.argv) > 1: db_path = sys.argv[1] diff --git a/backend/models.py b/backend/models.py index 1c0a39d..9abee13 100644 --- a/backend/models.py +++ b/backend/models.py @@ -32,6 +32,7 @@ class RosterUnit(Base): device_type = Column(String, default="seismograph") # "seismograph" | "modem" | "slm" deployed = Column(Boolean, default=True) retired = Column(Boolean, default=False) + out_for_calibration = Column(Boolean, default=False) note = Column(String, nullable=True) project_id = Column(String, nullable=True) location = Column(String, nullable=True) # Legacy field - use address/coordinates instead diff --git a/backend/routers/roster_edit.py b/backend/routers/roster_edit.py index e4083f7..ea799ed 100644 --- a/backend/routers/roster_edit.py +++ b/backend/routers/roster_edit.py @@ -146,6 +146,7 @@ async def add_roster_unit( unit_type: str = Form("series3"), deployed: str = Form(None), retired: str = Form(None), + out_for_calibration: str = Form(None), note: str = Form(""), project_id: str = Form(None), location: str = Form(None), @@ -177,6 +178,7 @@ async def add_roster_unit( # Convert boolean strings to actual booleans deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False + out_for_calibration_bool = out_for_calibration in ['true', 'True', '1', 'yes'] if out_for_calibration else False # Convert port strings to integers slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None @@ -211,6 +213,7 @@ async def add_roster_unit( unit_type=unit_type, deployed=deployed_bool, retired=retired_bool, + out_for_calibration=out_for_calibration_bool, note=note, project_id=project_id, location=location, @@ -463,6 +466,7 @@ def get_roster_unit(unit_id: str, db: Session = Depends(get_db)): "unit_type": unit.unit_type, "deployed": unit.deployed, "retired": unit.retired, + "out_for_calibration": unit.out_for_calibration or False, "note": unit.note or "", "project_id": unit.project_id or "", "location": unit.location or "", @@ -494,6 +498,7 @@ async def edit_roster_unit( unit_type: str = Form("series3"), deployed: str = Form(None), retired: str = Form(None), + out_for_calibration: str = Form(None), note: str = Form(""), project_id: str = Form(None), location: str = Form(None), @@ -535,6 +540,7 @@ async def edit_roster_unit( # Convert boolean strings to actual booleans deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False + out_for_calibration_bool = out_for_calibration in ['true', 'True', '1', 'yes'] if out_for_calibration else False # Convert port strings to integers slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None @@ -564,12 +570,14 @@ async def edit_roster_unit( old_note = unit.note old_deployed = unit.deployed old_retired = unit.retired + old_out_for_calibration = unit.out_for_calibration # Update all fields unit.device_type = device_type unit.unit_type = unit_type unit.deployed = deployed_bool unit.retired = retired_bool + unit.out_for_calibration = out_for_calibration_bool unit.note = note unit.project_id = project_id unit.location = location @@ -677,6 +685,11 @@ async def edit_roster_unit( old_status_text = "retired" if old_retired else "active" record_history(db, unit_id, "retired_change", "retired", old_status_text, status_text, "manual") + if old_out_for_calibration != out_for_calibration_bool: + status_text = "out_for_calibration" if out_for_calibration_bool else "available" + old_status_text = "out_for_calibration" if old_out_for_calibration else "available" + record_history(db, unit_id, "calibration_status_change", "out_for_calibration", old_status_text, status_text, "manual") + # Handle cascade to paired device cascaded_unit_id = None if cascade_to_unit_id and cascade_to_unit_id.strip(): diff --git a/backend/routers/seismo_dashboard.py b/backend/routers/seismo_dashboard.py index 7e48f83..fde33b9 100644 --- a/backend/routers/seismo_dashboard.py +++ b/backend/routers/seismo_dashboard.py @@ -28,7 +28,8 @@ async def get_seismo_stats(request: Request, db: Session = Depends(get_db)): total = len(seismos) deployed = sum(1 for s in seismos if s.deployed) - benched = sum(1 for s in seismos if not s.deployed) + benched = sum(1 for s in seismos if not s.deployed and not s.out_for_calibration) + out_for_calibration = sum(1 for s in seismos if s.out_for_calibration) # Count modems assigned to deployed seismographs with_modem = sum(1 for s in seismos if s.deployed and s.deployed_with_modem_id) @@ -41,6 +42,7 @@ async def get_seismo_stats(request: Request, db: Session = Depends(get_db)): "total": total, "deployed": deployed, "benched": benched, + "out_for_calibration": out_for_calibration, "with_modem": with_modem, "without_modem": without_modem } @@ -77,7 +79,9 @@ async def get_seismo_units( if status == "deployed": query = query.filter(RosterUnit.deployed == True) elif status == "benched": - query = query.filter(RosterUnit.deployed == False) + query = query.filter(RosterUnit.deployed == False, RosterUnit.out_for_calibration == False) + elif status == "out_for_calibration": + query = query.filter(RosterUnit.out_for_calibration == True) # Apply modem filter if modem == "with": diff --git a/backend/services/snapshot.py b/backend/services/snapshot.py index ec0dc2c..e4340f2 100644 --- a/backend/services/snapshot.py +++ b/backend/services/snapshot.py @@ -80,6 +80,12 @@ def emit_status_snapshot(): age = "N/A" last_seen = None fname = "" + elif r.out_for_calibration: + # Out for calibration units get separated later + status = "Out for Calibration" + age = "N/A" + last_seen = None + fname = "" else: if e: last_seen = ensure_utc(e.last_seen) @@ -103,6 +109,7 @@ def emit_status_snapshot(): "deployed": r.deployed, "note": r.note or "", "retired": r.retired, + "out_for_calibration": r.out_for_calibration or False, # Device type and type-specific fields "device_type": r.device_type or "seismograph", "last_calibrated": r.last_calibrated.isoformat() if r.last_calibrated else None, @@ -133,6 +140,7 @@ def emit_status_snapshot(): "deployed": False, # default "note": "", "retired": False, + "out_for_calibration": False, # Device type and type-specific fields (defaults for unknown units) "device_type": "seismograph", # default "last_calibrated": None, @@ -179,12 +187,12 @@ def emit_status_snapshot(): # Separate buckets for UI active_units = { uid: u for uid, u in units.items() - if not u["retired"] and u["deployed"] and uid not in ignored + if not u["retired"] and not u["out_for_calibration"] and u["deployed"] and uid not in ignored } benched_units = { uid: u for uid, u in units.items() - if not u["retired"] and not u["deployed"] and uid not in ignored + if not u["retired"] and not u["out_for_calibration"] and not u["deployed"] and uid not in ignored } retired_units = { @@ -192,6 +200,11 @@ def emit_status_snapshot(): if u["retired"] } + out_for_calibration_units = { + uid: u for uid, u in units.items() + if u["out_for_calibration"] + } + # Unknown units - emitters that aren't in the roster and aren't ignored unknown_units = { uid: u for uid, u in units.items() @@ -204,12 +217,14 @@ def emit_status_snapshot(): "active": active_units, "benched": benched_units, "retired": retired_units, + "out_for_calibration": out_for_calibration_units, "unknown": unknown_units, "summary": { "total": len(active_units) + len(benched_units), "active": len(active_units), "benched": len(benched_units), "retired": len(retired_units), + "out_for_calibration": len(out_for_calibration_units), "unknown": len(unknown_units), # Status counts only for deployed units (active_units) "ok": sum(1 for u in active_units.values() if u["status"] == "OK"), diff --git a/docker-compose.yml b/docker-compose.yml index 3a8964c..5ea8549 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,7 @@ services: - # --- TERRA-VIEW PRODUCTION --- terra-view: build: . - container_name: terra-view ports: - "8001:8001" volumes: @@ -29,7 +27,6 @@ services: build: context: ../slmm dockerfile: Dockerfile - container_name: slmm network_mode: host volumes: - ../slmm/data:/app/data diff --git a/scripts/rename_unit.py b/scripts/rename_unit.py index 68d4dc6..d181b3e 100644 --- a/scripts/rename_unit.py +++ b/scripts/rename_unit.py @@ -8,7 +8,7 @@ import sys from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker -DATABASE_URL = "sqlite:///data/sfm.db" +DATABASE_URL = "sqlite:///data/seismo_fleet.db" def rename_unit(old_id: str, new_id: str): """ diff --git a/templates/fleet_calendar.html b/templates/fleet_calendar.html index 09879cf..d67e695 100644 --- a/templates/fleet_calendar.html +++ b/templates/fleet_calendar.html @@ -397,13 +397,14 @@
-

New Reservation

+
+
@@ -522,7 +523,7 @@ class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"> Cancel - @@ -600,27 +601,83 @@ function closeDayPanel() { let reservationModeActive = false; function openReservationModal() { + // Reset to "create" mode + document.getElementById('modal-title').textContent = 'New Reservation'; + document.getElementById('modal-submit-btn').textContent = 'Create Reservation'; + document.getElementById('editing-reservation-id').value = ''; + document.getElementById('reservation-form').reset(); + document.getElementById('reservation-modal').classList.remove('hidden'); reservationModeActive = true; - // Show reservation legend, hide main legend document.getElementById('main-legend').classList.add('md:hidden'); document.getElementById('main-legend').classList.remove('md:flex'); document.getElementById('reservation-legend').classList.remove('md:hidden'); document.getElementById('reservation-legend').classList.add('md:flex'); - // Trigger availability update updateCalendarAvailability(); } +async function editReservation(id) { + try { + const response = await fetch(`/api/fleet-calendar/reservations/${id}`); + if (!response.ok) { alert('Failed to load reservation'); return; } + const res = await response.json(); + + // Switch modal to "edit" mode + document.getElementById('modal-title').textContent = 'Edit Reservation'; + document.getElementById('modal-submit-btn').textContent = 'Save Changes'; + document.getElementById('editing-reservation-id').value = id; + + // Populate fields + const form = document.getElementById('reservation-form'); + form.querySelector('input[name="name"]').value = res.name; + form.querySelector('select[name="project_id"]').value = res.project_id || ''; + form.querySelector('input[name="start_date"]').value = res.start_date; + form.querySelector('textarea[name="notes"]').value = res.notes || ''; + + // Color radio + const colorRadio = form.querySelector(`input[name="color"][value="${res.color}"]`); + if (colorRadio) colorRadio.checked = true; + + // Assignment type + const atRadio = form.querySelector(`input[name="assignment_type"][value="${res.assignment_type}"]`); + if (atRadio) { atRadio.checked = true; toggleAssignmentType(res.assignment_type); } + if (res.assignment_type === 'quantity') { + form.querySelector('input[name="quantity_needed"]').value = res.quantity_needed || 1; + } + + // End date / TBD + const tbdCheckbox = document.getElementById('end_date_tbd'); + if (res.end_date_tbd) { + tbdCheckbox.checked = true; + form.querySelector('input[name="estimated_end_date"]').value = res.estimated_end_date || ''; + } else { + tbdCheckbox.checked = false; + document.getElementById('end_date_input').value = res.end_date || ''; + } + toggleEndDateTBD(); + + document.getElementById('reservation-modal').classList.remove('hidden'); + reservationModeActive = true; + document.getElementById('main-legend').classList.add('md:hidden'); + document.getElementById('main-legend').classList.remove('md:flex'); + document.getElementById('reservation-legend').classList.remove('md:hidden'); + document.getElementById('reservation-legend').classList.add('md:flex'); + updateCalendarAvailability(); + } catch (error) { + console.error('Error loading reservation:', error); + alert('Error loading reservation'); + } +} + function closeReservationModal() { document.getElementById('reservation-modal').classList.add('hidden'); document.getElementById('reservation-form').reset(); + document.getElementById('editing-reservation-id').value = ''; reservationModeActive = false; - // Restore main legend document.getElementById('main-legend').classList.remove('md:hidden'); document.getElementById('main-legend').classList.add('md:flex'); document.getElementById('reservation-legend').classList.add('md:hidden'); document.getElementById('reservation-legend').classList.remove('md:flex'); - // Reset calendar colors resetCalendarColors(); } @@ -752,6 +809,7 @@ async function submitReservation(event) { const form = event.target; const formData = new FormData(form); const endDateTbd = formData.get('end_date_tbd') === 'on'; + const editingId = document.getElementById('editing-reservation-id').value; const data = { name: formData.get('name'), @@ -766,7 +824,6 @@ async function submitReservation(event) { notes: formData.get('notes') || null }; - // Validate: need either end_date or TBD checked if (!data.end_date && !data.end_date_tbd) { alert('Please enter an end date or check "TBD / Ongoing"'); return; @@ -778,9 +835,15 @@ async function submitReservation(event) { data.unit_ids = formData.getAll('unit_ids'); } + const isEdit = editingId !== ''; + const url = isEdit + ? `/api/fleet-calendar/reservations/${editingId}` + : '/api/fleet-calendar/reservations'; + const method = isEdit ? 'PUT' : 'POST'; + try { - const response = await fetch('/api/fleet-calendar/reservations', { - method: 'POST', + const response = await fetch(url, { + method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); @@ -789,14 +852,13 @@ async function submitReservation(event) { if (result.success) { closeReservationModal(); - // Reload the page to refresh calendar window.location.reload(); } else { - alert('Error creating reservation: ' + (result.detail || 'Unknown error')); + alert(`Error ${isEdit ? 'saving' : 'creating'} reservation: ` + (result.detail || 'Unknown error')); } } catch (error) { console.error('Error:', error); - alert('Error creating reservation'); + alert(`Error ${isEdit ? 'saving' : 'creating'} reservation`); } } diff --git a/templates/partials/devices_table.html b/templates/partials/devices_table.html index e0dafe5..c2b0127 100644 --- a/templates/partials/devices_table.html +++ b/templates/partials/devices_table.html @@ -51,7 +51,7 @@ {% for unit in units %}
- {% if not unit.deployed %} + {% if unit.out_for_calibration %} + + {% elif not unit.deployed %} {% elif unit.status == 'OK' %} @@ -72,6 +74,8 @@ {% if unit.deployed %} + {% elif unit.out_for_calibration %} + {% else %} {% endif %} @@ -203,14 +207,16 @@
- {% if not unit.deployed %} + {% if unit.out_for_calibration %} + + {% elif not unit.deployed %} {% elif unit.status == 'OK' %} @@ -224,12 +230,13 @@ {{ unit.id }}
- {% if unit.status in ['N/A', 'Unknown'] %}Benched{% else %}{{ unit.status }}{% endif %} + {% if unit.out_for_calibration %}Out for Cal{% elif unit.status in ['N/A', 'Unknown'] %}Benched{% else %}{{ unit.status }}{% endif %}
@@ -279,6 +286,10 @@ ⚡ Deployed + {% elif unit.out_for_calibration %} + + 🔧 Out for Calibration + {% else %} 📦 Benched diff --git a/templates/partials/fleet_calendar/reservations_list.html b/templates/partials/fleet_calendar/reservations_list.html index 4de84eb..060f598 100644 --- a/templates/partials/fleet_calendar/reservations_list.html +++ b/templates/partials/fleet_calendar/reservations_list.html @@ -87,10 +87,7 @@ async function deleteReservation(id, name) { } } -function editReservation(id) { - // For now, just show alert - can implement edit modal later - alert('Edit functionality coming soon. For now, delete and recreate the reservation.'); -} +// editReservation is defined in fleet_calendar.html {% else %}
diff --git a/templates/partials/seismo_stats.html b/templates/partials/seismo_stats.html index 16b10e5..7e08880 100644 --- a/templates/partials/seismo_stats.html +++ b/templates/partials/seismo_stats.html @@ -31,6 +31,9 @@

Benched

{{ benched }}

+ {% if out_for_calibration > 0 %} +

{{ out_for_calibration }} out for calibration

+ {% endif %}
diff --git a/templates/partials/seismo_unit_list.html b/templates/partials/seismo_unit_list.html index ac9ba15..09cf087 100644 --- a/templates/partials/seismo_unit_list.html +++ b/templates/partials/seismo_unit_list.html @@ -106,6 +106,13 @@ Deployed + {% elif unit.out_for_calibration %} + + + + + Out for Cal + {% else %} diff --git a/templates/seismographs.html b/templates/seismographs.html index aa79abd..b7c11a7 100644 --- a/templates/seismographs.html +++ b/templates/seismographs.html @@ -56,6 +56,7 @@ + diff --git a/templates/unit_detail.html b/templates/unit_detail.html index 2443f24..ed5a52a 100644 --- a/templates/unit_detail.html +++ b/templates/unit_detail.html @@ -92,7 +92,7 @@

--

- Retired + Unit Status

--

@@ -497,18 +497,28 @@
- -
- - + +
+
+ + +
+ + +
+ +
@@ -817,7 +827,16 @@ function populateViewMode() { } document.getElementById('deployedStatus').textContent = currentUnit.deployed ? 'Yes' : 'No'; - document.getElementById('retiredStatus').textContent = currentUnit.retired ? 'Yes' : 'No'; + if (currentUnit.retired) { + document.getElementById('retiredStatus').textContent = 'Retired'; + document.getElementById('retiredStatus').className = 'font-medium text-red-600 dark:text-red-400'; + } else if (currentUnit.out_for_calibration) { + document.getElementById('retiredStatus').textContent = 'Out for Calibration'; + document.getElementById('retiredStatus').className = 'font-medium text-purple-600 dark:text-purple-400'; + } else { + document.getElementById('retiredStatus').textContent = 'Active'; + document.getElementById('retiredStatus').className = 'font-medium text-gray-900 dark:text-white'; + } // Basic info document.getElementById('viewDeviceType').textContent = currentUnit.device_type || '--'; @@ -1009,7 +1028,9 @@ function populateEditForm() { document.getElementById('address').value = currentUnit.address || ''; document.getElementById('coordinates').value = currentUnit.coordinates || ''; document.getElementById('deployed').checked = currentUnit.deployed; - document.getElementById('retired').checked = currentUnit.retired; + document.getElementById('outForCalibration').checked = currentUnit.out_for_calibration || false; + document.getElementById('retired').value = currentUnit.retired ? 'true' : ''; + updateRetireButton(currentUnit.retired); document.getElementById('note').value = currentUnit.note || ''; // Seismograph fields @@ -1153,6 +1174,25 @@ function cancelEdit() { populateEditForm(); } +function updateRetireButton(isRetired) { + const btn = document.getElementById('retireBtn'); + if (!btn) return; + if (isRetired) { + btn.textContent = 'Un-Retire Unit'; + btn.className = 'px-4 py-2 text-sm font-medium rounded-lg border transition-colors bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'; + } else { + btn.textContent = 'Retire Unit'; + btn.className = 'px-4 py-2 text-sm font-medium rounded-lg border transition-colors bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/40'; + } +} + +function toggleRetired() { + const hiddenInput = document.getElementById('retired'); + const isCurrentlyRetired = hiddenInput.value === 'true'; + hiddenInput.value = isCurrentlyRetired ? '' : 'true'; + updateRetireButton(!isCurrentlyRetired); +} + // Handle form submission document.getElementById('editForm').addEventListener('submit', async function(e) { e.preventDefault();