Update main to 0.5.1. See changelog. #18
@@ -1,7 +1,12 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.database import get_db
|
||||
from backend.models import ScheduledAction, MonitoringLocation, Project
|
||||
from backend.services.snapshot import emit_status_snapshot
|
||||
from backend.templates_config import templates
|
||||
from backend.utils.timezone import utc_to_local, local_to_utc, get_user_timezone
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -22,3 +27,71 @@ def dashboard_benched(request: Request):
|
||||
"partials/benched_table.html",
|
||||
{"request": request, "units": snapshot["benched"]}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/dashboard/todays-actions")
|
||||
def dashboard_todays_actions(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Get today's scheduled actions for the dashboard card.
|
||||
Shows upcoming, completed, and failed actions for today.
|
||||
"""
|
||||
import json
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# Get today's date range in local timezone
|
||||
tz = ZoneInfo(get_user_timezone())
|
||||
now_local = datetime.now(tz)
|
||||
today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_end_local = today_start_local + timedelta(days=1)
|
||||
|
||||
# Convert to UTC for database query
|
||||
today_start_utc = today_start_local.astimezone(ZoneInfo("UTC")).replace(tzinfo=None)
|
||||
today_end_utc = today_end_local.astimezone(ZoneInfo("UTC")).replace(tzinfo=None)
|
||||
|
||||
# Query today's actions
|
||||
actions = db.query(ScheduledAction).filter(
|
||||
ScheduledAction.scheduled_time >= today_start_utc,
|
||||
ScheduledAction.scheduled_time < today_end_utc,
|
||||
).order_by(ScheduledAction.scheduled_time.asc()).all()
|
||||
|
||||
# Enrich with location/project info and parse results
|
||||
enriched_actions = []
|
||||
for action in actions:
|
||||
location = None
|
||||
project = None
|
||||
if action.location_id:
|
||||
location = db.query(MonitoringLocation).filter_by(id=action.location_id).first()
|
||||
if action.project_id:
|
||||
project = db.query(Project).filter_by(id=action.project_id).first()
|
||||
|
||||
# Parse module_response for result details
|
||||
result_data = None
|
||||
if action.module_response:
|
||||
try:
|
||||
result_data = json.loads(action.module_response)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
enriched_actions.append({
|
||||
"action": action,
|
||||
"location": location,
|
||||
"project": project,
|
||||
"result": result_data,
|
||||
})
|
||||
|
||||
# Count by status
|
||||
pending_count = sum(1 for a in actions if a.execution_status == "pending")
|
||||
completed_count = sum(1 for a in actions if a.execution_status == "completed")
|
||||
failed_count = sum(1 for a in actions if a.execution_status == "failed")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"partials/dashboard/todays_actions.html",
|
||||
{
|
||||
"request": request,
|
||||
"actions": enriched_actions,
|
||||
"pending_count": pending_count,
|
||||
"completed_count": completed_count,
|
||||
"failed_count": failed_count,
|
||||
"total_count": len(actions),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -493,9 +493,18 @@ async def get_project_schedules(
|
||||
"actions": [],
|
||||
}
|
||||
|
||||
# Parse module_response for display
|
||||
result_data = None
|
||||
if schedule.module_response:
|
||||
try:
|
||||
result_data = json.loads(schedule.module_response)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
schedules_by_date[date_key]["actions"].append({
|
||||
"schedule": schedule,
|
||||
"location": location,
|
||||
"result": result_data,
|
||||
})
|
||||
|
||||
return templates.TemplateResponse("partials/projects/schedule_list.html", {
|
||||
|
||||
BIN
backend/static/icons/favicon-16.png
Normal file
|
After Width: | Height: | Size: 424 B |
BIN
backend/static/icons/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
backend/static/terra-view-logo-dark.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/static/terra-view-logo-dark@2x.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
backend/static/terra-view-logo-light.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/static/terra-view-logo-light@2x.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
@@ -20,6 +20,9 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/icon-192.png">
|
||||
<meta name="theme-color" content="#f48b1c">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
@@ -68,7 +71,7 @@
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<body class="bg-gray-100 dark:bg-slate-800 text-gray-900 dark:text-gray-100">
|
||||
|
||||
<!-- Offline Indicator -->
|
||||
<div id="offlineIndicator" class="offline-indicator">
|
||||
@@ -85,10 +88,10 @@
|
||||
<aside id="sidebar" class="sidebar w-64 bg-white dark:bg-slate-800 shadow-lg flex flex-col">
|
||||
<!-- Logo -->
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h1 class="text-2xl font-bold text-seismo-navy dark:text-seismo-orange">
|
||||
Seismo<br>
|
||||
<span class="text-seismo-orange dark:text-seismo-burgundy">Fleet Manager</span>
|
||||
</h1>
|
||||
<a href="/" class="block">
|
||||
<img src="/static/terra-view-logo-light.png" srcset="/static/terra-view-logo-light.png 1x, /static/terra-view-logo-light@2x.png 2x" alt="Terra-View" class="block dark:hidden w-44 h-auto">
|
||||
<img src="/static/terra-view-logo-dark.png" srcset="/static/terra-view-logo-dark.png 1x, /static/terra-view-logo-dark@2x.png 2x" alt="Terra-View" class="hidden dark:block w-44 h-auto">
|
||||
</a>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">v {{ version }}</p>
|
||||
{% if environment == 'development' %}
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
hx-swap="none"
|
||||
hx-on::after-request="updateDashboard(event)">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
|
||||
<!-- Fleet Summary Card -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-summary-card">
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6" id="fleet-summary-card">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-summary')">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Fleet Summary</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Alerts Card -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="recent-alerts-card">
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6" id="recent-alerts-card">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-alerts')">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Alerts</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -138,7 +138,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Recently Called In Units Card -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="recent-callins-card">
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6" id="recent-callins-card">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-callins')">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Call-Ins</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -162,10 +162,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's Scheduled Actions Card -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6" id="todays-actions-card">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('todays-actions')">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Today's Schedule</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
|
||||
</path>
|
||||
</svg>
|
||||
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="todays-actions-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content" id="todays-actions-content"
|
||||
hx-get="/dashboard/todays-actions"
|
||||
hx-trigger="load, every 30s"
|
||||
hx-swap="innerHTML">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Loading scheduled actions...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Fleet Map -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8" id="fleet-map-card">
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6 mb-8" id="fleet-map-card">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-map')">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Map</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -181,7 +204,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Photos Section -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8" id="recent-photos-card">
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6 mb-8" id="recent-photos-card">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-photos')">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Recent Photos</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -201,7 +224,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Fleet Status Section with Tabs -->
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-status-card">
|
||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-700 p-6" id="fleet-status-card">
|
||||
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-status')">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Status</h2>
|
||||
@@ -316,7 +339,7 @@ function toggleCard(cardName) {
|
||||
// Restore card states from localStorage on page load
|
||||
function restoreCardStates() {
|
||||
const cardStates = JSON.parse(localStorage.getItem('dashboardCardStates') || '{}');
|
||||
const cardNames = ['fleet-summary', 'recent-alerts', 'recent-callins', 'fleet-map', 'fleet-status'];
|
||||
const cardNames = ['fleet-summary', 'recent-alerts', 'recent-callins', 'todays-actions', 'fleet-map', 'fleet-status'];
|
||||
|
||||
cardNames.forEach(cardName => {
|
||||
const content = document.getElementById(`${cardName}-content`);
|
||||
|
||||
131
templates/partials/dashboard/todays_actions.html
Normal file
@@ -0,0 +1,131 @@
|
||||
<!-- Today's Scheduled Actions - Dashboard Card Content -->
|
||||
|
||||
<!-- Summary stats -->
|
||||
<div class="flex items-center gap-4 mb-4 text-sm">
|
||||
{% if pending_count > 0 %}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-2 h-2 bg-yellow-400 rounded-full"></span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ pending_count }} pending</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if completed_count > 0 %}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-2 h-2 bg-green-400 rounded-full"></span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ completed_count }} completed</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if failed_count > 0 %}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-2 h-2 bg-red-400 rounded-full"></span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ failed_count }} failed</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if total_count == 0 %}
|
||||
<span class="text-gray-500 dark:text-gray-400">No actions scheduled for today</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Actions list -->
|
||||
{% if actions %}
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto">
|
||||
{% for item in actions %}
|
||||
<div class="flex items-center gap-3 p-2 rounded-lg
|
||||
{% if item.action.execution_status == 'pending' %}bg-yellow-50 dark:bg-yellow-900/20
|
||||
{% elif item.action.execution_status == 'completed' %}bg-green-50 dark:bg-green-900/20
|
||||
{% elif item.action.execution_status == 'failed' %}bg-red-50 dark:bg-red-900/20
|
||||
{% else %}bg-gray-50 dark:bg-gray-700/50{% endif %}">
|
||||
|
||||
<!-- Action type icon -->
|
||||
<div class="flex-shrink-0">
|
||||
{% if item.action.action_type == 'start' %}
|
||||
<div class="w-8 h-8 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% elif item.action.action_type == 'stop' %}
|
||||
<div class="w-8 h-8 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% elif item.action.action_type == 'download' %}
|
||||
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Action details -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm text-gray-900 dark:text-white capitalize">{{ item.action.action_type }}</span>
|
||||
|
||||
<!-- Status indicator -->
|
||||
{% if item.action.execution_status == 'pending' %}
|
||||
<span class="text-xs text-yellow-600 dark:text-yellow-400">
|
||||
{{ item.action.scheduled_time|local_datetime('%H:%M') }}
|
||||
</span>
|
||||
{% elif item.action.execution_status == 'completed' %}
|
||||
<svg class="w-4 h-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% elif item.action.execution_status == 'failed' %}
|
||||
<svg class="w-4 h-4 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Location/Project info -->
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{% if item.location %}
|
||||
<a href="/projects/{{ item.action.project_id }}/nrl/{{ item.location.id }}"
|
||||
class="hover:text-seismo-orange">
|
||||
{{ item.location.name }}
|
||||
</a>
|
||||
{% elif item.project %}
|
||||
<a href="/projects/{{ item.project.id }}" class="hover:text-seismo-orange">
|
||||
{{ item.project.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Result details for completed/failed -->
|
||||
{% if item.action.execution_status == 'completed' and item.result %}
|
||||
{% if item.result.cycle_response and item.result.cycle_response.downloaded_folder %}
|
||||
<div class="text-xs text-green-600 dark:text-green-400">
|
||||
{{ item.result.cycle_response.downloaded_folder }}
|
||||
{% if item.result.cycle_response.download_success %}downloaded{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif item.action.execution_status == 'failed' and item.action.error_message %}
|
||||
<div class="text-xs text-red-600 dark:text-red-400 truncate" title="{{ item.action.error_message }}">
|
||||
{{ item.action.error_message[:50] }}{% if item.action.error_message|length > 50 %}...{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
<div class="flex-shrink-0 text-right">
|
||||
{% if item.action.execution_status == 'pending' %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">Scheduled</span>
|
||||
{% elif item.action.executed_at %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ item.action.executed_at|local_datetime('%H:%M') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-6 text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-10 h-10 mx-auto mb-2 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<p class="text-sm">No scheduled actions for today</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -108,6 +108,53 @@
|
||||
<span class="ml-1 text-red-700 dark:text-red-300">{{ item.schedule.error_message }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Execution result details for completed/failed actions -->
|
||||
{% if item.result and item.schedule.execution_status in ['completed', 'failed'] %}
|
||||
<div class="mt-2 p-2 bg-gray-50 dark:bg-gray-700/50 rounded text-xs space-y-1">
|
||||
{% if item.result.cycle_response %}
|
||||
{% set cycle = item.result.cycle_response %}
|
||||
{% if cycle.new_index is defined and cycle.new_index is not none %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Index:</span>
|
||||
<span class="font-mono text-gray-700 dark:text-gray-300">{{ '%04d'|format(cycle.new_index) }}</span>
|
||||
{% if cycle.old_index is defined and cycle.old_index is not none %}
|
||||
<span class="text-gray-400">(was {{ '%04d'|format(cycle.old_index) }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cycle.downloaded_folder %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Folder:</span>
|
||||
<span class="font-mono text-gray-700 dark:text-gray-300">{{ cycle.downloaded_folder }}</span>
|
||||
{% if cycle.download_success %}
|
||||
<span class="text-green-600 dark:text-green-400">Downloaded</span>
|
||||
{% elif cycle.download_attempted %}
|
||||
<span class="text-red-600 dark:text-red-400">Download failed</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cycle.clock_synced %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Clock synced:</span>
|
||||
<span class="text-green-600 dark:text-green-400">Yes</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif item.result.device_response %}
|
||||
{% set dev = item.result.device_response %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Device:</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ dev.message or dev.status }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.result.session_id %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Session:</span>
|
||||
<span class="font-mono text-xs text-gray-600 dark:text-gray-400">{{ item.result.session_id[:8] }}...</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
@@ -162,9 +162,9 @@
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Upcoming Actions</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Individual scheduled start/stop/download actions
|
||||
<h2 id="schedules-title" class="text-xl font-semibold text-gray-900 dark:text-white">Upcoming Actions</h2>
|
||||
<p id="schedules-subtitle" class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Scheduled start/stop/download actions
|
||||
</p>
|
||||
</div>
|
||||
<select id="schedules-filter" onchange="filterScheduledActions()"
|
||||
@@ -963,6 +963,21 @@ function filterScheduledActions() {
|
||||
? `/api/projects/${projectId}/schedules`
|
||||
: `/api/projects/${projectId}/schedules?status=${filter}`;
|
||||
|
||||
// Update section title based on filter
|
||||
const titleEl = document.getElementById('schedules-title');
|
||||
const subtitleEl = document.getElementById('schedules-subtitle');
|
||||
|
||||
const titles = {
|
||||
'pending': { title: 'Upcoming Actions', subtitle: 'Scheduled start/stop/download actions' },
|
||||
'completed': { title: 'Completed Actions', subtitle: 'Successfully executed actions' },
|
||||
'failed': { title: 'Failed Actions', subtitle: 'Actions that encountered errors' },
|
||||
'all': { title: 'All Actions', subtitle: 'Complete action history' }
|
||||
};
|
||||
|
||||
const config = titles[filter] || titles['all'];
|
||||
titleEl.textContent = config.title;
|
||||
subtitleEl.textContent = config.subtitle;
|
||||
|
||||
htmx.ajax('GET', url, {
|
||||
target: '#project-schedules',
|
||||
swap: 'innerHTML'
|
||||
|
||||