feat: initial CronHub release

- FastAPI backend avec APScheduler
- API REST complète (CRUD + toggle + run + logs)
- UI Jinja2 + Tailwind (charte orange/slate)
- SQLite pour la persistance
- Docker + Traefik labels
- Swagger docs auto sur /docs
This commit is contained in:
Luca
2026-03-15 15:19:36 +01:00
commit d94e340d31
10 changed files with 1109 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CronHub{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
.status-badge { @apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold; }
.pulse-dot { animation: pulse 2s cubic-bezier(0.4,0,0.6,1) infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
</style>
</head>
<body class="bg-gray-100 min-h-screen flex">
<!-- Sidebar -->
<div class="w-64 bg-slate-800 min-h-screen flex flex-col fixed left-0 top-0 z-10">
<div class="p-6 border-b border-slate-700">
<h1 class="text-white text-xl font-bold flex items-center gap-2">
<i class="fas fa-clock text-orange-400"></i> CronHub
</h1>
<p class="text-slate-400 text-xs mt-1">Gestionnaire de crons</p>
</div>
<nav class="flex-1 p-4 space-y-1">
<a href="/" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition
{% if page == 'dashboard' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<i class="fas fa-gauge-high w-5"></i> Dashboard
</a>
<a href="/jobs/new" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition
{% if page == 'new_job' %}bg-orange-500 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<i class="fas fa-plus-circle w-5"></i> Nouveau job
</a>
<a href="/docs" target="_blank" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="fas fa-book w-5"></i> API Docs
</a>
</nav>
<div class="p-4 border-t border-slate-700 text-xs text-slate-500 text-center">CronHub v1.0</div>
</div>
<!-- Main content -->
<div class="ml-64 flex-1 p-8">
{% block content %}{% endblock %}
</div>
<script>
// Auto-refresh toutes les 30s sur le dashboard
if (window.location.pathname === '/') {
setTimeout(() => location.reload(), 30000);
}
function confirmDelete(formId) {
if (confirm('Supprimer ce job ? Cette action est irréversible.')) {
document.getElementById(formId).submit();
}
}
</script>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
{% extends "base.html" %}
{% block title %}Dashboard — CronHub{% endblock %}
{% block content %}
<div class="mb-8 flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-slate-800">Dashboard</h2>
<p class="text-slate-500 text-sm mt-1">Vue d'ensemble des cron jobs</p>
</div>
<a href="/jobs/new"
class="flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg font-medium transition">
<i class="fas fa-plus"></i> Nouveau job
</a>
</div>
<!-- Stats -->
<div class="grid grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<i class="fas fa-list text-blue-600"></i>
</div>
<div>
<p class="text-2xl font-bold text-slate-800">{{ stats.total }}</p>
<p class="text-xs text-slate-500">Total</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<i class="fas fa-circle-check text-green-600"></i>
</div>
<div>
<p class="text-2xl font-bold text-slate-800">{{ stats.active }}</p>
<p class="text-xs text-slate-500">Actifs</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
<i class="fas fa-check text-emerald-600"></i>
</div>
<div>
<p class="text-2xl font-bold text-slate-800">{{ stats.success }}</p>
<p class="text-xs text-slate-500">Succès</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
<i class="fas fa-triangle-exclamation text-red-600"></i>
</div>
<div>
<p class="text-2xl font-bold text-slate-800">{{ stats.failed }}</p>
<p class="text-xs text-slate-500">Échecs</p>
</div>
</div>
</div>
</div>
<!-- Jobs table -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
<h3 class="font-semibold text-slate-700">Jobs planifiés</h3>
<span class="text-xs text-slate-400">Rafraîchissement auto toutes les 30s</span>
</div>
{% if jobs %}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-slate-50 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">
<th class="px-6 py-3">Nom</th>
<th class="px-6 py-3">Schedule</th>
<th class="px-6 py-3">Commande</th>
<th class="px-6 py-3">Statut</th>
<th class="px-6 py-3">Dernière exéc.</th>
<th class="px-6 py-3">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for job in jobs %}
<tr class="hover:bg-slate-50 transition">
<td class="px-6 py-4">
<a href="/jobs/{{ job.id }}" class="font-semibold text-slate-800 hover:text-orange-600 transition">
{{ job.name }}
</a>
{% if job.description %}
<p class="text-xs text-slate-400 truncate max-w-xs">{{ job.description }}</p>
{% endif %}
</td>
<td class="px-6 py-4">
<code class="text-xs bg-slate-100 px-2 py-1 rounded font-mono text-slate-600">{{ job.schedule }}</code>
</td>
<td class="px-6 py-4 max-w-xs">
<code class="text-xs text-slate-500 truncate block">{{ job.command }}</code>
</td>
<td class="px-6 py-4">
{% if not job.enabled %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-500">
<i class="fas fa-pause text-xs"></i> Désactivé
</span>
{% elif job.last_status == 'running' %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">
<span class="pulse-dot w-1.5 h-1.5 bg-blue-500 rounded-full"></span> En cours
</span>
{% elif job.last_status == 'success' %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-700">
<i class="fas fa-check text-xs"></i> Succès
</span>
{% elif job.last_status == 'failed' %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">
<i class="fas fa-xmark text-xs"></i> Échec
</span>
{% else %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-orange-100 text-orange-700">
<i class="fas fa-clock text-xs"></i> Actif
</span>
{% endif %}
</td>
<td class="px-6 py-4 text-xs text-slate-400">
{% if job.last_run %}
{{ job.last_run | replace("T", " ") | truncate(16, True, '') }}
{% else %}
<span class="text-slate-300">Jamais</span>
{% endif %}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<!-- Toggle -->
<form method="POST" action="/jobs/{{ job.id }}/toggle">
<button type="submit"
title="{% if job.enabled %}Désactiver{% else %}Activer{% endif %}"
class="w-8 h-8 flex items-center justify-center rounded-lg transition
{% if job.enabled %}bg-green-100 text-green-600 hover:bg-green-200{% else %}bg-slate-100 text-slate-400 hover:bg-slate-200{% endif %}">
<i class="fas {% if job.enabled %}fa-toggle-on{% else %}fa-toggle-off{% endif %} text-sm"></i>
</button>
</form>
<!-- Run now -->
<form method="POST" action="/jobs/{{ job.id }}/run-now">
<button type="submit" title="Exécuter maintenant"
class="w-8 h-8 flex items-center justify-center rounded-lg bg-orange-100 text-orange-600 hover:bg-orange-200 transition">
<i class="fas fa-play text-xs"></i>
</button>
</form>
<!-- Edit -->
<a href="/jobs/{{ job.id }}/edit" title="Modifier"
class="w-8 h-8 flex items-center justify-center rounded-lg bg-blue-100 text-blue-600 hover:bg-blue-200 transition">
<i class="fas fa-pen text-xs"></i>
</a>
<!-- Delete -->
<form id="del-{{ job.id }}" method="POST" action="/jobs/{{ job.id }}/delete">
<button type="button" title="Supprimer"
onclick="confirmDelete('del-{{ job.id }}')"
class="w-8 h-8 flex items-center justify-center rounded-lg bg-red-100 text-red-600 hover:bg-red-200 transition">
<i class="fas fa-trash text-xs"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="py-16 text-center">
<i class="fas fa-clock text-4xl text-slate-200 mb-4"></i>
<p class="text-slate-400 font-medium">Aucun job planifié</p>
<a href="/jobs/new" class="mt-4 inline-flex items-center gap-2 text-orange-500 hover:text-orange-600 text-sm font-medium">
<i class="fas fa-plus"></i> Créer votre premier job
</a>
</div>
{% endif %}
</div>
{% endblock %}
+162
View File
@@ -0,0 +1,162 @@
{% extends "base.html" %}
{% block title %}{{ job.name }} — CronHub{% endblock %}
{% block content %}
<div class="mb-6">
<a href="/" class="text-slate-400 hover:text-slate-600 text-sm flex items-center gap-1 mb-4 transition">
<i class="fas fa-arrow-left text-xs"></i> Retour au dashboard
</a>
<div class="flex items-start justify-between">
<div>
<h2 class="text-2xl font-bold text-slate-800">{{ job.name }}</h2>
{% if job.description %}
<p class="text-slate-500 text-sm mt-1">{{ job.description }}</p>
{% endif %}
</div>
<div class="flex items-center gap-2">
<!-- Toggle -->
<form method="POST" action="/jobs/{{ job.id }}/toggle">
<button type="submit"
class="flex items-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition
{% if job.enabled %}bg-green-100 text-green-700 hover:bg-green-200{% else %}bg-slate-100 text-slate-500 hover:bg-slate-200{% endif %}">
<i class="fas {% if job.enabled %}fa-toggle-on{% else %}fa-toggle-off{% endif %}"></i>
{% if job.enabled %}Actif{% else %}Inactif{% endif %}
</button>
</form>
<!-- Run now -->
<form method="POST" action="/jobs/{{ job.id }}/run-now">
<button type="submit"
class="flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg font-medium text-sm transition">
<i class="fas fa-play"></i> Exécuter
</button>
</form>
<!-- Edit -->
<a href="/jobs/{{ job.id }}/edit"
class="flex items-center gap-2 px-4 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg font-medium text-sm transition">
<i class="fas fa-pen"></i> Modifier
</a>
<!-- Delete -->
<form id="del-form" method="POST" action="/jobs/{{ job.id }}/delete">
<button type="button" onclick="confirmDelete('del-form')"
class="flex items-center gap-2 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg font-medium text-sm transition">
<i class="fas fa-trash"></i> Supprimer
</button>
</form>
</div>
</div>
</div>
<!-- Job details -->
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-2">Schedule</p>
<code class="text-lg font-bold text-slate-800 font-mono">{{ job.schedule }}</code>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-2">Dernière exécution</p>
<p class="text-slate-800 font-semibold">
{% if job.last_run %}
{{ job.last_run | replace("T", " ") | truncate(19, True, '') }}
{% else %}
<span class="text-slate-300">Jamais</span>
{% endif %}
</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-2">Dernier statut</p>
{% if job.last_status == 'success' %}
<span class="inline-flex items-center gap-1.5 text-green-700 font-semibold">
<i class="fas fa-circle-check"></i> Succès
</span>
{% elif job.last_status == 'failed' %}
<span class="inline-flex items-center gap-1.5 text-red-700 font-semibold">
<i class="fas fa-circle-xmark"></i> Échec
</span>
{% elif job.last_status == 'running' %}
<span class="inline-flex items-center gap-1.5 text-blue-700 font-semibold">
<span class="pulse-dot w-2 h-2 bg-blue-500 rounded-full inline-block"></span> En cours
</span>
{% else %}
<span class="text-slate-400"></span>
{% endif %}
</div>
</div>
<!-- Command -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6 mb-6">
<h3 class="font-semibold text-slate-700 mb-3 flex items-center gap-2">
<i class="fas fa-terminal text-orange-400"></i> Commande
</h3>
<code class="block bg-slate-900 text-green-400 p-4 rounded-lg text-sm font-mono break-all">{{ job.command }}</code>
</div>
<!-- Logs -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div class="px-6 py-4 border-b border-slate-100">
<h3 class="font-semibold text-slate-700 flex items-center gap-2">
<i class="fas fa-scroll text-orange-400"></i> Historique d'exécution
<span class="text-xs text-slate-400 font-normal">(20 dernières)</span>
</h3>
</div>
{% if logs %}
<div class="divide-y divide-slate-100">
{% for log in logs %}
<details class="group">
<summary class="px-6 py-4 flex items-center gap-4 cursor-pointer hover:bg-slate-50 transition list-none">
<div class="flex items-center gap-2 w-24">
{% if log.status == 'success' %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-700">
<i class="fas fa-check"></i> OK
</span>
{% elif log.status == 'failed' %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">
<i class="fas fa-xmark"></i> Erreur
</span>
{% elif log.status == 'running' %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">
<span class="pulse-dot w-1.5 h-1.5 bg-blue-500 rounded-full"></span> En cours
</span>
{% endif %}
</div>
<div class="flex-1 text-sm">
<span class="text-slate-700 font-medium">{{ log.started_at | replace("T", " ") | truncate(19, True, '') }}</span>
{% if log.ended_at %}
<span class="text-slate-400 ml-2 text-xs">
→ {% set dur = log.ended_at | replace("T", " ") %}{{ log.ended_at | replace("T", " ") | truncate(19, True, '') }}
</span>
{% endif %}
</div>
{% if log.exit_code is not none %}
<code class="text-xs bg-slate-100 px-2 py-0.5 rounded text-slate-500">exit: {{ log.exit_code }}</code>
{% endif %}
<i class="fas fa-chevron-down text-slate-400 text-xs group-open:rotate-180 transition-transform"></i>
</summary>
<div class="px-6 pb-4 bg-slate-50">
{% if log.stdout %}
<div class="mb-3">
<p class="text-xs font-semibold text-slate-500 mb-1">STDOUT</p>
<pre class="bg-slate-900 text-green-300 p-3 rounded text-xs font-mono overflow-x-auto max-h-48 overflow-y-auto">{{ log.stdout }}</pre>
</div>
{% endif %}
{% if log.stderr %}
<div>
<p class="text-xs font-semibold text-slate-500 mb-1">STDERR</p>
<pre class="bg-slate-900 text-red-300 p-3 rounded text-xs font-mono overflow-x-auto max-h-48 overflow-y-auto">{{ log.stderr }}</pre>
</div>
{% endif %}
{% if not log.stdout and not log.stderr %}
<p class="text-xs text-slate-400 italic">Pas de sortie</p>
{% endif %}
</div>
</details>
{% endfor %}
</div>
{% else %}
<div class="py-12 text-center">
<i class="fas fa-scroll text-3xl text-slate-200 mb-3"></i>
<p class="text-slate-400 text-sm">Aucune exécution pour ce job</p>
</div>
{% endif %}
</div>
{% endblock %}
+119
View File
@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}{% if job %}Modifier{% else %}Nouveau job{% endif %} — CronHub{% endblock %}
{% block content %}
<div class="mb-8">
<a href="/" class="text-slate-400 hover:text-slate-600 text-sm flex items-center gap-1 mb-4 transition">
<i class="fas fa-arrow-left text-xs"></i> Retour
</a>
<h2 class="text-2xl font-bold text-slate-800">
{% if job %}Modifier le job{% else %}Nouveau cron job{% endif %}
</h2>
</div>
<div class="max-w-2xl">
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-8">
{% if error %}
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-center gap-2">
<i class="fas fa-circle-exclamation"></i> {{ error }}
</div>
{% endif %}
<form method="POST" action="{{ action }}" class="space-y-6">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-2">
Nom <span class="text-red-500">*</span>
</label>
<input type="text" name="name" required
value="{{ form.name if form else (job.name if job else '') }}"
placeholder="Ex: Backup quotidien"
class="w-full px-4 py-3 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 text-slate-700 placeholder-slate-300">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-2">
Schedule (cron) <span class="text-red-500">*</span>
</label>
<input type="text" name="schedule" required
value="{{ form.schedule if form else (job.schedule if job else '0 3 * * *') }}"
placeholder="0 3 * * *"
class="w-full px-4 py-3 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 font-mono text-slate-700 placeholder-slate-300">
<div class="mt-2 flex flex-wrap gap-2">
<button type="button" onclick="setCron('0 3 * * *')" class="text-xs bg-slate-100 hover:bg-orange-100 hover:text-orange-600 text-slate-500 px-2 py-1 rounded transition">Chaque jour 3h</button>
<button type="button" onclick="setCron('0 * * * *')" class="text-xs bg-slate-100 hover:bg-orange-100 hover:text-orange-600 text-slate-500 px-2 py-1 rounded transition">Chaque heure</button>
<button type="button" onclick="setCron('*/5 * * * *')" class="text-xs bg-slate-100 hover:bg-orange-100 hover:text-orange-600 text-slate-500 px-2 py-1 rounded transition">Toutes les 5min</button>
<button type="button" onclick="setCron('0 0 * * 0')" class="text-xs bg-slate-100 hover:bg-orange-100 hover:text-orange-600 text-slate-500 px-2 py-1 rounded transition">Chaque dimanche</button>
<button type="button" onclick="setCron('0 0 1 * *')" class="text-xs bg-slate-100 hover:bg-orange-100 hover:text-orange-600 text-slate-500 px-2 py-1 rounded transition">1er du mois</button>
</div>
<p class="text-xs text-slate-400 mt-1">Format : minute heure jour mois jour_semaine</p>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-2">
Commande <span class="text-red-500">*</span>
</label>
<input type="text" name="command" required
value="{{ form.command if form else (job.command if job else '') }}"
placeholder="/app/scripts/mon-script.sh ou docker exec mon-container ..."
class="w-full px-4 py-3 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 font-mono text-sm text-slate-700 placeholder-slate-300">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-2">
Description
</label>
<textarea name="description" rows="3"
placeholder="Description optionnelle..."
class="w-full px-4 py-3 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 text-slate-700 placeholder-slate-300 resize-none">{{ job.description if job else '' }}</textarea>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" name="enabled" id="enabled" value="on"
{% if not job or job.enabled %}checked{% endif %}
class="w-4 h-4 accent-orange-500">
<label for="enabled" class="text-sm font-semibold text-slate-700">Activer immédiatement</label>
</div>
<div class="flex gap-3 pt-2">
<button type="submit"
class="flex-1 bg-orange-500 hover:bg-orange-600 text-white py-3 rounded-lg font-semibold transition flex items-center justify-center gap-2">
<i class="fas {% if job %}fa-save{% else %}fa-plus{% endif %}"></i>
{% if job %}Enregistrer{% else %}Créer le job{% endif %}
</button>
<a href="/"
class="px-6 py-3 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg font-semibold transition flex items-center gap-2">
Annuler
</a>
</div>
</form>
</div>
<!-- Aide cron -->
<div class="mt-6 bg-slate-800 rounded-xl p-6 text-slate-300 text-sm">
<h4 class="text-white font-semibold mb-3 flex items-center gap-2">
<i class="fas fa-lightbulb text-orange-400"></i> Aide — Format cron
</h4>
<pre class="font-mono text-xs text-slate-400">
┌───────────── minute (0-59)
│ ┌─────────── heure (0-23)
│ │ ┌───────── jour (1-31)
│ │ │ ┌─────── mois (1-12)
│ │ │ │ ┌───── jour semaine (0-7, 0=dim)
│ │ │ │ │
* * * * *
</pre>
<div class="mt-3 grid grid-cols-2 gap-2 text-xs">
<div><code class="text-orange-400">*</code> — toutes les valeurs</div>
<div><code class="text-orange-400">*/5</code> — toutes les 5 unités</div>
<div><code class="text-orange-400">1,3,5</code> — valeurs spécifiques</div>
<div><code class="text-orange-400">1-5</code> — plage</div>
</div>
</div>
</div>
<script>
function setCron(val) {
document.querySelector('input[name="schedule"]').value = val;
}
</script>
{% endblock %}