restructure of dirs, huge docs update
This commit is contained in:
442
app/web/templates/schedule_create.html
Normal file
442
app/web/templates/schedule_create.html
Normal file
@@ -0,0 +1,442 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user