Partiel _header.html unique pour les 19 pages (fin des copies qui dérivaient): desktop une ligne — primaires + menu Plus + Ctrl K, page active déduite du gabarit dans render(); mobile ≤900px burger + panneau ≥44px, Échap/clic extérieur; wallboard sans header et triage sur le même partiel. nav.js partagé.
87 lines
4.3 KiB
HTML
87 lines
4.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token }}">
|
|
<title>Règles d'automatisation — Kanbante</title>
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
</head>
|
|
<body>
|
|
{% include "_header.html" %}
|
|
|
|
<main class="focus-main">
|
|
<section class="briefing-card">
|
|
<h1>⚙️ Règles d'automatisation</h1>
|
|
<p>Des réflexes déclaratifs, évalués toutes les 15 minutes par le planificateur (et à la demande ci-dessous). Chaque action est journalisée comme événement <code>rule</code>. <strong>Garde-fou v1 : une règle ne peut ni fermer ni déplacer une carte entre colonnes</strong> — seulement étiqueter, notifier (ntfy) et remonter en tête de colonne.</p>
|
|
{% if not configured %}
|
|
<p>Aucune règle configurée. Définis <code>KANBANTE_RULES</code> en JSON, par exemple :</p>
|
|
<pre class="briefing-text">[{"name": "Relance révision",
|
|
"when": {"column": "review", "min_age_days": 3},
|
|
"then": {"notify": true}},
|
|
{"name": "Urgent qui vieillit",
|
|
"when": {"column": "in_progress", "min_age_days": 7},
|
|
"then": {"add_label": "urgent"}}]</pre>
|
|
<p class="notes-hint">Conditions : <code>column</code>, <code>min_age_days</code> (temps dans la colonne), <code>label</code> (présente), <code>wip_over</code> (colonne au-delà de N cartes). Actions : <code>add_label</code>, <code>notify</code>, <code>move_top</code>.</p>
|
|
{% endif %}
|
|
{% for err in errors %}
|
|
<p class="chip-warn">⚠️ {{ err }}</p>
|
|
{% endfor %}
|
|
<p><button type="button" class="btn-save btn-inline" id="rules-run">▶️ Évaluer maintenant</button>
|
|
<span id="rules-run-result" class="notes-hint"></span></p>
|
|
</section>
|
|
|
|
{% if rules %}
|
|
<section class="briefing-card">
|
|
<h2>📜 Règles actives ({{ rules|length }})</h2>
|
|
<ul class="forecast-milestones">
|
|
{% for rule in rules %}
|
|
<li><strong>{{ rule.name }}</strong> — quand
|
|
{% for k, v in rule.when.items() %}<code>{{ k }} = {{ v }}</code>{% if not loop.last %}, {% endif %}{% endfor %}
|
|
→ {% for k, v in rule.then.items() %}<code>{{ k }}{% if v is not boolean %} = {{ v }}{% endif %}</code>{% if not loop.last %}, {% endif %}{% endfor %}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if last_run %}
|
|
<section class="briefing-card">
|
|
<h2>🕐 Dernière évaluation — {{ last_run.at }}</h2>
|
|
{% if last_run.actions %}
|
|
<ul class="forecast-milestones">
|
|
{% for action in last_run.actions %}
|
|
<li><strong>{{ action.rule }}</strong> : {{ action.repo }} #{{ action.number }} ({{ action.column }}) — {{ action.did|join(', ') }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p>Aucune action déclenchée.</p>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
</main>
|
|
<footer>
|
|
<span>Kanbante v{{ version }} · l'automatisation maison, près des données, jamais destructive</span>
|
|
</footer>
|
|
{% include "_palette.html" %}
|
|
<script src="/static/js/palette.js" defer></script>
|
|
<script>
|
|
document.getElementById('rules-run').addEventListener('click', async () => {
|
|
const out = document.getElementById('rules-run-result');
|
|
out.textContent = 'Évaluation en cours…';
|
|
const csrf = document.querySelector('meta[name="csrf-token"]').content;
|
|
const data = new FormData();
|
|
data.append('csrf_token', csrf);
|
|
try {
|
|
const r = await fetch('/api/rules/run', { method: 'POST', body: data });
|
|
const body = await r.json();
|
|
if (!r.ok || !body.ok) throw new Error((body && body.error) || ('HTTP ' + r.status));
|
|
out.textContent = '✔ ' + body.summary.actions.length + ' action(s) — rechargement…';
|
|
setTimeout(() => location.reload(), 700);
|
|
} catch (e) {
|
|
out.textContent = '❌ ' + e.message;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|