Feat: Scheduler implemented, WIP
This commit is contained in:
87
templates/partials/alerts/alert_dropdown.html
Normal file
87
templates/partials/alerts/alert_dropdown.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!-- Alert Dropdown Content -->
|
||||
<!-- Loaded via HTMX into the alert dropdown in the navbar -->
|
||||
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
{% if alerts %}
|
||||
{% for item in alerts %}
|
||||
<div class="p-3 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors
|
||||
{% if item.alert.severity == 'critical' %}bg-red-50 dark:bg-red-900/20{% endif %}">
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Severity icon -->
|
||||
{% if item.alert.severity == 'critical' %}
|
||||
<span class="text-red-500 flex-shrink-0 mt-0.5">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</span>
|
||||
{% elif item.alert.severity == 'warning' %}
|
||||
<span class="text-yellow-500 flex-shrink-0 mt-0.5">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-blue-500 flex-shrink-0 mt-0.5">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{{ item.alert.title }}
|
||||
</p>
|
||||
{% if item.alert.message %}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mt-0.5">
|
||||
{{ item.alert.message }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
{{ item.time_ago }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<button hx-post="/api/alerts/{{ item.alert.id }}/acknowledge"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#alert-dropdown-content', 'refresh')"
|
||||
class="p-1.5 text-gray-400 hover:text-green-600 dark:hover:text-green-400 rounded transition-colors"
|
||||
title="Acknowledge">
|
||||
<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="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button hx-post="/api/alerts/{{ item.alert.id }}/dismiss"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#alert-dropdown-content', 'refresh')"
|
||||
class="p-1.5 text-gray-400 hover:text-red-600 dark:hover:text-red-400 rounded transition-colors"
|
||||
title="Dismiss">
|
||||
<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="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="p-8 text-center">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">No active alerts</p>
|
||||
<p class="text-gray-400 dark:text-gray-500 text-xs mt-1">All systems operational</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- View all link -->
|
||||
{% if total_count > 0 %}
|
||||
<div class="p-3 border-t border-gray-200 dark:border-gray-700 text-center bg-gray-50 dark:bg-gray-800/50">
|
||||
<a href="/alerts" class="text-sm text-seismo-orange hover:text-seismo-navy dark:hover:text-orange-300 font-medium">
|
||||
View all {{ total_count }} alert{{ 's' if total_count != 1 else '' }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
125
templates/partials/alerts/alert_list.html
Normal file
125
templates/partials/alerts/alert_list.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!-- Alert List Partial -->
|
||||
<!-- Full list of alerts for the alerts page -->
|
||||
|
||||
<div class="space-y-3">
|
||||
{% if alerts %}
|
||||
{% for item in alerts %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4
|
||||
{% if item.alert.severity == 'critical' and item.alert.status == 'active' %}border-l-4 border-l-red-500{% endif %}
|
||||
{% if item.alert.severity == 'warning' and item.alert.status == 'active' %}border-l-4 border-l-yellow-500{% endif %}
|
||||
{% if item.alert.status != 'active' %}opacity-60{% endif %}">
|
||||
<div class="flex items-start gap-4">
|
||||
<!-- Severity icon -->
|
||||
<div class="flex-shrink-0">
|
||||
{% if item.alert.severity == 'critical' %}
|
||||
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% elif item.alert.severity == 'warning' %}
|
||||
<div class="w-10 h-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{{ item.alert.title }}
|
||||
</h3>
|
||||
<!-- Status badge -->
|
||||
{% if item.alert.status == 'active' %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300">
|
||||
Active
|
||||
</span>
|
||||
{% elif item.alert.status == 'acknowledged' %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300">
|
||||
Acknowledged
|
||||
</span>
|
||||
{% elif item.alert.status == 'resolved' %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
Resolved
|
||||
</span>
|
||||
{% elif item.alert.status == 'dismissed' %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||
Dismissed
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if item.alert.message %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
{{ item.alert.message }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{{ item.time_ago }}</span>
|
||||
{% if item.alert.unit_id %}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
|
||||
</svg>
|
||||
{{ item.alert.unit_id }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="capitalize">{{ item.alert.alert_type | replace('_', ' ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
{% if item.alert.status == 'active' %}
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button hx-post="/api/alerts/{{ item.alert.id }}/acknowledge"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#alert-list', 'refresh')"
|
||||
class="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
||||
Acknowledge
|
||||
</button>
|
||||
<button hx-post="/api/alerts/{{ item.alert.id }}/resolve"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#alert-list', 'refresh')"
|
||||
class="px-3 py-1.5 text-sm bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg hover:bg-green-200 dark:hover:bg-green-900/50 transition-colors">
|
||||
Resolve
|
||||
</button>
|
||||
<button hx-post="/api/alerts/{{ item.alert.id }}/dismiss"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#alert-list', 'refresh')"
|
||||
class="px-3 py-1.5 text-sm text-gray-500 hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
title="Dismiss">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No alerts</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{% if status_filter %}
|
||||
No {{ status_filter }} alerts found.
|
||||
{% else %}
|
||||
All systems are operating normally.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
151
templates/partials/projects/recurring_schedule_list.html
Normal file
151
templates/partials/projects/recurring_schedule_list.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!-- Recurring Schedule List -->
|
||||
<!-- Displays all recurring schedules for a project -->
|
||||
|
||||
<div class="space-y-4">
|
||||
{% if schedules %}
|
||||
{% for item in schedules %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4
|
||||
{% if not item.schedule.enabled %}opacity-60{% endif %}">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{{ item.schedule.name }}
|
||||
</h4>
|
||||
|
||||
<!-- Type badge -->
|
||||
{% if item.schedule.schedule_type == 'weekly_calendar' %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
||||
Weekly
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
||||
24/7 Cycle
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status badge -->
|
||||
{% if item.schedule.enabled %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
Active
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||
Disabled
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Location info -->
|
||||
{% if item.location %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
<span class="text-gray-500">Location:</span>
|
||||
<a href="/projects/{{ project_id }}/nrl/{{ item.location.id }}"
|
||||
class="text-seismo-orange hover:text-seismo-navy font-medium ml-1">
|
||||
{{ item.location.name }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Schedule details -->
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
{% if item.schedule.schedule_type == 'weekly_calendar' and item.pattern %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% set days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] %}
|
||||
{% set day_abbr = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] %}
|
||||
{% for day in days %}
|
||||
{% if item.pattern.get(day, {}).get('enabled') %}
|
||||
<span class="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 rounded">
|
||||
{{ day_abbr[loop.index0] }}
|
||||
{{ item.pattern[day].get('start', '') }}-{{ item.pattern[day].get('end', '') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif item.schedule.schedule_type == 'simple_interval' %}
|
||||
<div>
|
||||
Cycle at {{ item.schedule.cycle_time or '00:00' }} daily
|
||||
{% if item.schedule.include_download %}
|
||||
(with download)
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.schedule.next_occurrence %}
|
||||
<div class="text-xs">
|
||||
<span class="text-gray-400">Next:</span>
|
||||
{{ item.schedule.next_occurrence.strftime('%Y-%m-%d %H:%M') }} {{ item.schedule.timezone }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
{% if item.schedule.enabled %}
|
||||
<button hx-post="/api/projects/{{ project_id }}/recurring-schedules/{{ item.schedule.id }}/disable"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#recurring-schedule-list', 'refresh')"
|
||||
class="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
||||
Disable
|
||||
</button>
|
||||
{% else %}
|
||||
<button hx-post="/api/projects/{{ project_id }}/recurring-schedules/{{ item.schedule.id }}/enable"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#recurring-schedule-list', 'refresh')"
|
||||
class="px-3 py-1.5 text-sm bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg hover:bg-green-200 dark:hover:bg-green-900/50 transition-colors">
|
||||
Enable
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button onclick="editSchedule('{{ item.schedule.id }}')"
|
||||
class="px-3 py-1.5 text-sm bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors">
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<button hx-delete="/api/projects/{{ project_id }}/recurring-schedules/{{ item.schedule.id }}"
|
||||
hx-confirm="Delete this recurring schedule?"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#recurring-schedule-list', 'refresh')"
|
||||
class="p-1.5 text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 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"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No recurring schedules</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||
Create a schedule to automate monitoring start/stop times.
|
||||
</p>
|
||||
<button onclick="showCreateScheduleModal()"
|
||||
class="px-4 py-2 bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors">
|
||||
Create Schedule
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function editSchedule(scheduleId) {
|
||||
// For now, redirect to a future edit page or show details
|
||||
// The edit modal will be implemented later
|
||||
alert('Edit schedule: ' + scheduleId + '\n\nNote: Full edit functionality coming soon. For now, you can delete and recreate the schedule.');
|
||||
}
|
||||
|
||||
function showCreateScheduleModal() {
|
||||
// Call the parent page's openScheduleModal function
|
||||
if (typeof openScheduleModal === 'function') {
|
||||
openScheduleModal();
|
||||
} else {
|
||||
alert('Please use the "Create Schedule" button in the Schedules tab.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
231
templates/partials/projects/schedule_calendar.html
Normal file
231
templates/partials/projects/schedule_calendar.html
Normal file
@@ -0,0 +1,231 @@
|
||||
<!-- Weekly Calendar Schedule Editor -->
|
||||
<!-- Used in modals/forms for creating/editing weekly_calendar type schedules -->
|
||||
|
||||
<div id="schedule-calendar-editor" class="space-y-4">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Weekly Schedule</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Select which days to monitor and set start/end times for each day.
|
||||
For overnight monitoring (e.g., 7pm to 7am), the end time will be on the following day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Day rows -->
|
||||
<div class="space-y-3">
|
||||
{% set days = [
|
||||
('monday', 'Monday'),
|
||||
('tuesday', 'Tuesday'),
|
||||
('wednesday', 'Wednesday'),
|
||||
('thursday', 'Thursday'),
|
||||
('friday', 'Friday'),
|
||||
('saturday', 'Saturday'),
|
||||
('sunday', 'Sunday')
|
||||
] %}
|
||||
|
||||
{% for day_key, day_name in days %}
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<!-- Day toggle -->
|
||||
<label class="flex items-center gap-2 w-28 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
id="day-{{ day_key }}"
|
||||
name="weekly_pattern[{{ day_key }}][enabled]"
|
||||
class="rounded text-seismo-orange focus:ring-seismo-orange"
|
||||
onchange="toggleDayTimes('{{ day_key }}', this.checked)"
|
||||
{% if pattern and pattern.get(day_key, {}).get('enabled') %}checked{% endif %}>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ day_name }}</span>
|
||||
</label>
|
||||
|
||||
<!-- Time inputs -->
|
||||
<div class="flex items-center gap-2 day-times flex-1" id="times-{{ day_key }}"
|
||||
{% if not pattern or not pattern.get(day_key, {}).get('enabled') %}style="opacity: 0.4; pointer-events: none;"{% endif %}>
|
||||
<label class="text-xs text-gray-500 dark:text-gray-400">Start:</label>
|
||||
<input type="time"
|
||||
name="weekly_pattern[{{ day_key }}][start]"
|
||||
value="{{ pattern.get(day_key, {}).get('start', '19:00') if pattern else '19:00' }}"
|
||||
class="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-seismo-orange focus:border-seismo-orange">
|
||||
|
||||
<span class="text-gray-400 mx-1">to</span>
|
||||
|
||||
<label class="text-xs text-gray-500 dark:text-gray-400">End:</label>
|
||||
<input type="time"
|
||||
name="weekly_pattern[{{ day_key }}][end]"
|
||||
value="{{ pattern.get(day_key, {}).get('end', '07:00') if pattern else '07:00' }}"
|
||||
class="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-seismo-orange focus:border-seismo-orange">
|
||||
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2" id="overnight-hint-{{ day_key }}"
|
||||
style="display: none;">
|
||||
(next day)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Quick select buttons -->
|
||||
<div class="flex flex-wrap gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 mr-2">Quick select:</span>
|
||||
<button type="button" onclick="selectWeekdays()"
|
||||
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
|
||||
Weekdays
|
||||
</button>
|
||||
<button type="button" onclick="selectWeekends()"
|
||||
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
|
||||
Weekends
|
||||
</button>
|
||||
<button type="button" onclick="selectAllDays()"
|
||||
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
|
||||
All Days
|
||||
</button>
|
||||
<button type="button" onclick="clearAllDays()"
|
||||
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Automation Options -->
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Automation Options</h5>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Download data option -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="include_download"
|
||||
id="include_download_calendar"
|
||||
class="rounded text-seismo-orange focus:ring-seismo-orange mt-0.5"
|
||||
{% if include_download is not defined or include_download %}checked{% endif %}>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Download data after each monitoring period
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
When enabled, measurement data will be downloaded via FTP after each stop.
|
||||
Disable if you prefer to download manually or if FTP is not configured.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Auto-increment index option -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="auto_increment_index"
|
||||
id="auto_increment_index_calendar"
|
||||
class="rounded text-seismo-orange focus:ring-seismo-orange mt-0.5"
|
||||
{% if auto_increment_index is not defined or auto_increment_index %}checked{% endif %}>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Auto-increment store index before each start
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
When enabled, the system will find an unused store/index number before starting.
|
||||
This prevents "overwrite existing data?" prompts on the device.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
const weekdays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
|
||||
const weekends = ['saturday', 'sunday'];
|
||||
|
||||
function toggleDayTimes(day, enabled) {
|
||||
const timesDiv = document.getElementById('times-' + day);
|
||||
if (enabled) {
|
||||
timesDiv.style.opacity = '1';
|
||||
timesDiv.style.pointerEvents = 'auto';
|
||||
} else {
|
||||
timesDiv.style.opacity = '0.4';
|
||||
timesDiv.style.pointerEvents = 'none';
|
||||
}
|
||||
updateOvernightHints();
|
||||
}
|
||||
|
||||
function setDayEnabled(day, enabled) {
|
||||
const checkbox = document.getElementById('day-' + day);
|
||||
checkbox.checked = enabled;
|
||||
toggleDayTimes(day, enabled);
|
||||
}
|
||||
|
||||
function selectWeekdays() {
|
||||
days.forEach(day => setDayEnabled(day, weekdays.includes(day)));
|
||||
}
|
||||
|
||||
function selectWeekends() {
|
||||
days.forEach(day => setDayEnabled(day, weekends.includes(day)));
|
||||
}
|
||||
|
||||
function selectAllDays() {
|
||||
days.forEach(day => setDayEnabled(day, true));
|
||||
}
|
||||
|
||||
function clearAllDays() {
|
||||
days.forEach(day => setDayEnabled(day, false));
|
||||
}
|
||||
|
||||
function updateOvernightHints() {
|
||||
days.forEach(day => {
|
||||
const startInput = document.querySelector(`input[name="weekly_pattern[${day}][start]"]`);
|
||||
const endInput = document.querySelector(`input[name="weekly_pattern[${day}][end]"]`);
|
||||
const hint = document.getElementById('overnight-hint-' + day);
|
||||
|
||||
if (startInput && endInput && hint) {
|
||||
const start = startInput.value;
|
||||
const end = endInput.value;
|
||||
hint.style.display = (end && start && end <= start) ? 'inline' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update hints on time change
|
||||
document.querySelectorAll('input[type="time"]').forEach(input => {
|
||||
input.addEventListener('change', updateOvernightHints);
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateOvernightHints();
|
||||
|
||||
// Function to collect form data as JSON
|
||||
function getWeeklyPatternData() {
|
||||
const pattern = {};
|
||||
days.forEach(day => {
|
||||
const checkbox = document.getElementById('day-' + day);
|
||||
const startInput = document.querySelector(`input[name="weekly_pattern[${day}][start]"]`);
|
||||
const endInput = document.querySelector(`input[name="weekly_pattern[${day}][end]"]`);
|
||||
|
||||
pattern[day] = {
|
||||
enabled: checkbox.checked,
|
||||
start: startInput.value,
|
||||
end: endInput.value
|
||||
};
|
||||
});
|
||||
return pattern;
|
||||
}
|
||||
|
||||
// Function to get auto-increment setting for calendar mode
|
||||
function getCalendarAutoIncrement() {
|
||||
const checkbox = document.getElementById('auto_increment_index_calendar');
|
||||
return checkbox ? checkbox.checked : true;
|
||||
}
|
||||
|
||||
// Function to get include_download setting for calendar mode
|
||||
function getCalendarIncludeDownload() {
|
||||
const checkbox = document.getElementById('include_download_calendar');
|
||||
return checkbox ? checkbox.checked : true;
|
||||
}
|
||||
|
||||
// Function to get all calendar options as object
|
||||
function getCalendarOptions() {
|
||||
return {
|
||||
weekly_pattern: getWeeklyPatternData(),
|
||||
auto_increment_index: getCalendarAutoIncrement(),
|
||||
include_download: getCalendarIncludeDownload()
|
||||
};
|
||||
}
|
||||
</script>
|
||||
158
templates/partials/projects/schedule_interval.html
Normal file
158
templates/partials/projects/schedule_interval.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!-- Simple Interval Schedule Editor -->
|
||||
<!-- Used for 24/7 continuous monitoring with daily stop/download/restart cycles -->
|
||||
|
||||
<div id="schedule-interval-editor" class="space-y-4">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Continuous Monitoring (24/7)</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
For uninterrupted monitoring. The device will automatically stop, download data,
|
||||
and restart at the configured cycle time each day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Info box -->
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div class="flex gap-3">
|
||||
<svg class="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<p class="font-medium mb-1">How it works:</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>At the cycle time, the measurement will <strong>stop</strong></li>
|
||||
<li>If enabled, data will be <strong>downloaded</strong> via FTP</li>
|
||||
<li>The measurement will <strong>restart</strong> automatically</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cycle time -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Daily Cycle Time
|
||||
</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="time"
|
||||
name="cycle_time"
|
||||
id="cycle_time"
|
||||
value="{{ cycle_time or '00:00' }}"
|
||||
class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-seismo-orange focus:border-seismo-orange">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Time when stop/download/restart cycle runs
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-2">
|
||||
Recommended: midnight (00:00) to minimize disruption to data collection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Download option -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="include_download"
|
||||
id="include_download"
|
||||
class="rounded text-seismo-orange focus:ring-seismo-orange mt-0.5"
|
||||
{% if include_download is not defined or include_download %}checked{% endif %}>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Download data before restart
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
When enabled, measurement data will be downloaded via FTP during the cycle.
|
||||
Disable if you prefer to download manually or if FTP is not configured.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Auto-increment index option -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="auto_increment_index"
|
||||
id="auto_increment_index_interval"
|
||||
class="rounded text-seismo-orange focus:ring-seismo-orange mt-0.5"
|
||||
{% if auto_increment_index is not defined or auto_increment_index %}checked{% endif %}>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Auto-increment store index before restart
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
When enabled, the store/index number is incremented before starting a new measurement.
|
||||
This prevents "overwrite existing data?" prompts on the device.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Interval type (hidden for now, default to daily) -->
|
||||
<input type="hidden" name="interval_type" value="daily">
|
||||
|
||||
<!-- Cycle preview -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Cycle Sequence Preview</h5>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center text-xs text-red-700 dark:text-red-300">1</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">Stop</span>
|
||||
</div>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<div class="flex items-center gap-2" id="download-step">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-xs text-blue-700 dark:text-blue-300">2</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">Download</span>
|
||||
</div>
|
||||
<svg class="w-4 h-4 text-gray-400" id="download-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center text-xs text-green-700 dark:text-green-300" id="start-step-num">3</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">Start</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-3" id="cycle-timing">
|
||||
At <span id="preview-time">00:00</span>: Stop → Download (1 min) → Start (2 min)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Update preview when download checkbox changes
|
||||
document.getElementById('include_download').addEventListener('change', function() {
|
||||
const downloadStep = document.getElementById('download-step');
|
||||
const downloadArrow = document.getElementById('download-arrow');
|
||||
const startStepNum = document.getElementById('start-step-num');
|
||||
const cycleTiming = document.getElementById('cycle-timing');
|
||||
const timeValue = document.getElementById('cycle_time').value || '00:00';
|
||||
|
||||
if (this.checked) {
|
||||
downloadStep.style.display = 'flex';
|
||||
downloadArrow.style.display = 'block';
|
||||
startStepNum.textContent = '3';
|
||||
cycleTiming.innerHTML = `At <span id="preview-time">${timeValue}</span>: Stop → Download (1 min) → Start (2 min)`;
|
||||
} else {
|
||||
downloadStep.style.display = 'none';
|
||||
downloadArrow.style.display = 'none';
|
||||
startStepNum.textContent = '2';
|
||||
cycleTiming.innerHTML = `At <span id="preview-time">${timeValue}</span>: Stop → Start (1 min)`;
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview time when cycle time changes
|
||||
document.getElementById('cycle_time').addEventListener('change', function() {
|
||||
document.getElementById('preview-time').textContent = this.value || '00:00';
|
||||
});
|
||||
|
||||
// Function to get interval data as object
|
||||
function getIntervalData() {
|
||||
return {
|
||||
interval_type: 'daily',
|
||||
cycle_time: document.getElementById('cycle_time').value,
|
||||
include_download: document.getElementById('include_download').checked,
|
||||
auto_increment_index: document.getElementById('auto_increment_index_interval').checked
|
||||
};
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user