443 lines
18 KiB
HTML
443 lines
18 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Create Schedule - SneakyScanner{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row mt-4">
|
||
<div class="col-12">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h1 style="color: #60a5fa;">Create Schedule</h1>
|
||
<a href="{{ url_for('main.schedules') }}" class="btn btn-secondary">
|
||
<i class="bi bi-arrow-left"></i> Back to Schedules
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<form id="create-schedule-form">
|
||
<!-- Basic Information Card -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<h5 class="mb-0" style="color: #60a5fa;">Basic Information</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- Schedule Name -->
|
||
<div class="mb-3">
|
||
<label for="schedule-name" class="form-label">Schedule Name <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="schedule-name" name="name"
|
||
placeholder="e.g., Daily Infrastructure Scan"
|
||
required>
|
||
<small class="form-text text-muted">A descriptive name for this schedule</small>
|
||
</div>
|
||
|
||
<!-- Config File -->
|
||
<div class="mb-3">
|
||
<label for="config-file" class="form-label">Configuration File <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="config-file" name="config_file" required>
|
||
<option value="">Select a configuration file...</option>
|
||
{% for config in config_files %}
|
||
<option value="{{ config }}">{{ config }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<small class="form-text text-muted">The scan configuration to use for this schedule</small>
|
||
</div>
|
||
|
||
<!-- Enable/Disable -->
|
||
<div class="mb-3">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" id="schedule-enabled"
|
||
name="enabled" checked>
|
||
<label class="form-check-label" for="schedule-enabled">
|
||
Enable schedule immediately
|
||
</label>
|
||
</div>
|
||
<small class="form-text text-muted">If disabled, the schedule will be created but not executed</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cron Expression Card -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<h5 class="mb-0" style="color: #60a5fa;">Schedule Configuration</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- Quick Templates -->
|
||
<div class="mb-3">
|
||
<label class="form-label">Quick Templates:</label>
|
||
<div class="btn-group-vertical btn-group-sm w-100" role="group">
|
||
<button type="button" class="btn btn-outline-secondary text-start" onclick="setCron('0 0 * * *')">
|
||
<strong>Daily at Midnight (local)</strong> <code class="float-end">0 0 * * *</code>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary text-start" onclick="setCron('0 2 * * *')">
|
||
<strong>Daily at 2 AM (local)</strong> <code class="float-end">0 2 * * *</code>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary text-start" onclick="setCron('0 */6 * * *')">
|
||
<strong>Every 6 Hours</strong> <code class="float-end">0 */6 * * *</code>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary text-start" onclick="setCron('0 0 * * 0')">
|
||
<strong>Weekly (Sunday at Midnight)</strong> <code class="float-end">0 0 * * 0</code>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary text-start" onclick="setCron('0 0 1 * *')">
|
||
<strong>Monthly (1st at Midnight)</strong> <code class="float-end">0 0 1 * *</code>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Manual Cron Entry -->
|
||
<div class="mb-3">
|
||
<label for="cron-expression" class="form-label">
|
||
Cron Expression <span class="text-danger">*</span>
|
||
<span class="badge bg-info">LOCAL TIME</span>
|
||
</label>
|
||
<input type="text" class="form-control font-monospace" id="cron-expression"
|
||
name="cron_expression" placeholder="0 2 * * *"
|
||
oninput="validateCron()" required>
|
||
<small class="form-text text-muted">
|
||
Format: <code>minute hour day month weekday</code><br>
|
||
<strong class="text-info">ℹ All times use your local timezone (CST/UTC-6)</strong>
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Cron Validation Feedback -->
|
||
<div id="cron-feedback" class="alert" style="display: none;"></div>
|
||
|
||
<!-- Human-Readable Description -->
|
||
<div id="cron-description-container" style="display: none;">
|
||
<div class="alert alert-info">
|
||
<strong>Description:</strong>
|
||
<div id="cron-description" class="mt-1"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Next Run Times Preview -->
|
||
<div id="next-runs-container" style="display: none;">
|
||
<label class="form-label">Next 5 execution times (local time):</label>
|
||
<ul id="next-runs-list" class="list-group">
|
||
<!-- Populated by JavaScript -->
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Buttons -->
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between">
|
||
<a href="{{ url_for('main.schedules') }}" class="btn btn-secondary">Cancel</a>
|
||
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||
<i class="bi bi-plus-circle"></i> Create Schedule
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Help Sidebar -->
|
||
<div class="col-lg-4">
|
||
<div class="card sticky-top" style="top: 20px;">
|
||
<div class="card-header">
|
||
<h5 class="mb-0" style="color: #60a5fa;">Cron Expression Help</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<h6>Field Format:</h6>
|
||
<table class="table table-sm">
|
||
<thead>
|
||
<tr>
|
||
<th>Field</th>
|
||
<th>Values</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Minute</td>
|
||
<td>0-59</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Hour</td>
|
||
<td>0-23</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Day</td>
|
||
<td>1-31</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Month</td>
|
||
<td>1-12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Weekday</td>
|
||
<td>0-6 (0=Sunday)</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h6 class="mt-3">Special Characters:</h6>
|
||
<ul class="list-unstyled">
|
||
<li><code>*</code> - Any value</li>
|
||
<li><code>*/n</code> - Every n units</li>
|
||
<li><code>1,2,3</code> - Specific values</li>
|
||
<li><code>1-5</code> - Range of values</li>
|
||
</ul>
|
||
|
||
<h6 class="mt-3">Examples:</h6>
|
||
<ul class="list-unstyled">
|
||
<li><code>0 0 * * *</code> - Daily at midnight</li>
|
||
<li><code>*/15 * * * *</code> - Every 15 minutes</li>
|
||
<li><code>0 9-17 * * 1-5</code> - Hourly, 9am-5pm, Mon-Fri</li>
|
||
</ul>
|
||
|
||
<div class="alert alert-info mt-3">
|
||
<strong><i class="bi bi-info-circle"></i> Timezone Information:</strong><br>
|
||
All cron expressions use your <strong>local system time</strong>.<br><br>
|
||
<strong>Current local time:</strong> <span id="user-local-time"></span><br>
|
||
<strong>Your timezone:</strong> <span id="timezone-offset"></span><br><br>
|
||
<small>Schedules will run at the specified time in your local timezone.</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Update local time and timezone info every second
|
||
function updateServerTime() {
|
||
const now = new Date();
|
||
const localTime = now.toLocaleTimeString();
|
||
const offset = -now.getTimezoneOffset() / 60;
|
||
const offsetStr = `CST (UTC${offset >= 0 ? '+' : ''}${offset})`;
|
||
|
||
if (document.getElementById('user-local-time')) {
|
||
document.getElementById('user-local-time').textContent = localTime;
|
||
}
|
||
if (document.getElementById('timezone-offset')) {
|
||
document.getElementById('timezone-offset').textContent = offsetStr;
|
||
}
|
||
}
|
||
updateServerTime();
|
||
setInterval(updateServerTime, 1000);
|
||
|
||
// Set cron expression from template button
|
||
function setCron(expression) {
|
||
document.getElementById('cron-expression').value = expression;
|
||
validateCron();
|
||
}
|
||
|
||
// Validate cron expression (client-side basic validation)
|
||
function validateCron() {
|
||
const input = document.getElementById('cron-expression');
|
||
const expression = input.value.trim();
|
||
const feedback = document.getElementById('cron-feedback');
|
||
const descContainer = document.getElementById('cron-description-container');
|
||
const description = document.getElementById('cron-description');
|
||
const nextRunsContainer = document.getElementById('next-runs-container');
|
||
|
||
if (!expression) {
|
||
feedback.style.display = 'none';
|
||
descContainer.style.display = 'none';
|
||
nextRunsContainer.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Basic validation: should have 5 fields
|
||
const parts = expression.split(/\s+/);
|
||
if (parts.length !== 5) {
|
||
feedback.className = 'alert alert-danger';
|
||
feedback.textContent = 'Invalid format: Cron expression must have exactly 5 fields (minute hour day month weekday)';
|
||
feedback.style.display = 'block';
|
||
descContainer.style.display = 'none';
|
||
nextRunsContainer.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Basic field validation
|
||
const [minute, hour, day, month, weekday] = parts;
|
||
|
||
const errors = [];
|
||
|
||
if (!isValidCronField(minute, 0, 59)) errors.push('minute (0-59)');
|
||
if (!isValidCronField(hour, 0, 23)) errors.push('hour (0-23)');
|
||
if (!isValidCronField(day, 1, 31)) errors.push('day (1-31)');
|
||
if (!isValidCronField(month, 1, 12)) errors.push('month (1-12)');
|
||
if (!isValidCronField(weekday, 0, 6)) errors.push('weekday (0-6)');
|
||
|
||
if (errors.length > 0) {
|
||
feedback.className = 'alert alert-danger';
|
||
feedback.textContent = 'Invalid fields: ' + errors.join(', ');
|
||
feedback.style.display = 'block';
|
||
descContainer.style.display = 'none';
|
||
nextRunsContainer.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Valid expression
|
||
feedback.className = 'alert alert-success';
|
||
feedback.textContent = 'Valid cron expression';
|
||
feedback.style.display = 'block';
|
||
|
||
// Show human-readable description
|
||
description.textContent = describeCron(parts);
|
||
descContainer.style.display = 'block';
|
||
|
||
// Calculate and show next run times
|
||
calculateNextRuns(expression);
|
||
nextRunsContainer.style.display = 'block';
|
||
}
|
||
|
||
// Validate individual cron field
|
||
function isValidCronField(field, min, max) {
|
||
if (field === '*') return true;
|
||
|
||
// Handle ranges: 1-5
|
||
if (field.includes('-')) {
|
||
const [start, end] = field.split('-').map(Number);
|
||
return start >= min && end <= max && start <= end;
|
||
}
|
||
|
||
// Handle steps: */5 or 1-10/2
|
||
if (field.includes('/')) {
|
||
const [range, step] = field.split('/');
|
||
if (range === '*') return Number(step) > 0;
|
||
return isValidCronField(range, min, max) && Number(step) > 0;
|
||
}
|
||
|
||
// Handle lists: 1,2,3
|
||
if (field.includes(',')) {
|
||
return field.split(',').every(v => {
|
||
const num = Number(v);
|
||
return !isNaN(num) && num >= min && num <= max;
|
||
});
|
||
}
|
||
|
||
// Single number
|
||
const num = Number(field);
|
||
return !isNaN(num) && num >= min && num <= max;
|
||
}
|
||
|
||
// Generate human-readable description
|
||
function describeCron(parts) {
|
||
const [minute, hour, day, month, weekday] = parts;
|
||
|
||
// Common patterns
|
||
if (minute === '0' && hour === '0' && day === '*' && month === '*' && weekday === '*') {
|
||
return 'Runs daily at midnight (local time)';
|
||
}
|
||
if (minute === '0' && hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||
return `Runs daily at ${hour.padStart(2, '0')}:00 (local time)`;
|
||
}
|
||
if (minute !== '*' && hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||
return `Runs daily at ${hour.padStart(2, '0')}:${minute.padStart(2, '0')} (local time)`;
|
||
}
|
||
if (minute === '0' && hour === '0' && day === '*' && month === '*' && weekday !== '*') {
|
||
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||
return `Runs weekly on ${days[Number(weekday)]} at midnight`;
|
||
}
|
||
if (minute === '0' && hour === '0' && day !== '*' && month === '*' && weekday === '*') {
|
||
return `Runs monthly on day ${day} at midnight`;
|
||
}
|
||
if (minute.startsWith('*/')) {
|
||
const interval = minute.split('/')[1];
|
||
return `Runs every ${interval} minutes`;
|
||
}
|
||
if (hour.startsWith('*/') && minute === '0') {
|
||
const interval = hour.split('/')[1];
|
||
return `Runs every ${interval} hours`;
|
||
}
|
||
|
||
return `Runs at ${minute} ${hour} ${day} ${month} ${weekday} (cron format)`;
|
||
}
|
||
|
||
// Calculate next 5 run times (simplified - server will do actual calculation)
|
||
function calculateNextRuns(expression) {
|
||
const list = document.getElementById('next-runs-list');
|
||
list.innerHTML = '<li class="list-group-item"><em>Will be calculated by server...</em></li>';
|
||
|
||
// In production, this would call an API endpoint to get accurate next runs
|
||
// For now, just show placeholder
|
||
}
|
||
|
||
// Handle form submission
|
||
document.getElementById('create-schedule-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const submitBtn = document.getElementById('submit-btn');
|
||
const originalText = submitBtn.innerHTML;
|
||
|
||
// Get form data
|
||
const formData = {
|
||
name: document.getElementById('schedule-name').value.trim(),
|
||
config_file: document.getElementById('config-file').value,
|
||
cron_expression: document.getElementById('cron-expression').value.trim(),
|
||
enabled: document.getElementById('schedule-enabled').checked
|
||
};
|
||
|
||
// Validate
|
||
if (!formData.name || !formData.config_file || !formData.cron_expression) {
|
||
showNotification('Please fill in all required fields', 'warning');
|
||
return;
|
||
}
|
||
|
||
// Disable submit button
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating...';
|
||
|
||
try {
|
||
const response = await fetch('/api/schedules', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(formData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
showNotification('Schedule created successfully! Redirecting...', 'success');
|
||
|
||
// Redirect to schedules list
|
||
setTimeout(() => {
|
||
window.location.href = '/schedules';
|
||
}, 1500);
|
||
|
||
} catch (error) {
|
||
console.error('Error creating schedule:', error);
|
||
showNotification(`Error: ${error.message}`, 'danger');
|
||
|
||
// Re-enable submit button
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = originalText;
|
||
}
|
||
});
|
||
|
||
// Show notification
|
||
function showNotification(message, type = 'info') {
|
||
const notification = document.createElement('div');
|
||
notification.className = `alert alert-${type} alert-dismissible fade show`;
|
||
notification.style.position = 'fixed';
|
||
notification.style.top = '20px';
|
||
notification.style.right = '20px';
|
||
notification.style.zIndex = '9999';
|
||
notification.style.minWidth = '300px';
|
||
|
||
notification.innerHTML = `
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
`;
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 5000);
|
||
}
|
||
</script>
|
||
{% endblock %}
|