Feat: Scheduler implemented, WIP

This commit is contained in:
serversdwn
2026-01-21 23:11:58 +00:00
parent 1f3fa7a718
commit 65ea0920db
20 changed files with 3682 additions and 15 deletions

View 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 %}

View 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>

View 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>

View 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>

View 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>