Pour tenir les heures de la semaine, les préconisations n'ajustaient que
les départs du soir : un retard trop gros était déclaré rattrapage
impossible dès minuit atteint. Deux leviers s'enchaînent désormais :
avancer les arrivées du matin (jamais avant le début de la présence
matin, le retour de repos, ni l'heure actuelle), puis les retours de midi
(jamais avant la fin de la présence matin, ou la sortie réelle du matin).
Les entrées déjà pointées restent intactes ; l'aide des Réglages
documente la cascade.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
550 lines
22 KiB
HTML
550 lines
22 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<div class="stats-head">
|
||
<div>
|
||
<span class="stats-title">Réglages</span>
|
||
<div class="stats-meta">Rappels de pointage sur ton téléphone</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card">
|
||
<span class="hist-label">Plages horaires</span>
|
||
<p class="settings-p">
|
||
Bornes de présence obligatoire matin/après-midi et heure de reprise par défaut de
|
||
l'après-midi, utilisées pour les avertissements de saisie et les préconisations de
|
||
fin de demi-journée. Les heures visées (arrivée le matin, départ le soir) guident
|
||
les préconisations : le matin est planifié depuis ton arrivée visée, et le rattrapage
|
||
est casé avant ton départ visé tant que c'est possible. En rattrapage extrême :
|
||
départs du soir allongés en premier (jusqu'à minuit), puis arrivée du matin avancée
|
||
(jamais avant le début de la présence matin), puis retour de midi avancé (jamais
|
||
avant la fin de la présence matin). Propres à ton compte —
|
||
ne change rien pour les autres.
|
||
</p>
|
||
|
||
{% if plages_saved %}
|
||
<div class="settings-saved">Enregistré.</div>
|
||
{% endif %}
|
||
{% if plages_error %}
|
||
<div class="settings-error">Horaires invalides (format HH:MM, matin_debut < matin_fin < aprem_debut < aprem_fin).</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings/plages" class="settings-form">
|
||
<label class="login-label" for="matin_debut">Présence matin</label>
|
||
<div class="settings-range">
|
||
<input id="matin_debut" class="login-input time-range-input" type="time" name="matin_debut" value="{{ plages.matin_debut }}">
|
||
<span class="settings-range-sep">→</span>
|
||
<input id="matin_fin" class="login-input time-range-input" type="time" name="matin_fin" value="{{ plages.matin_fin }}">
|
||
</div>
|
||
<label class="login-label" for="aprem_debut">Présence après-midi</label>
|
||
<div class="settings-range">
|
||
<input id="aprem_debut" class="login-input time-range-input" type="time" name="aprem_debut" value="{{ plages.aprem_debut }}">
|
||
<span class="settings-range-sep">→</span>
|
||
<input id="aprem_fin" class="login-input time-range-input" type="time" name="aprem_fin" value="{{ plages.aprem_fin }}">
|
||
</div>
|
||
<label class="login-label" for="pause_dejeuner_fin">Reprise par défaut de l'après-midi</label>
|
||
<input id="pause_dejeuner_fin" class="login-input time-range-input" type="time" name="pause_dejeuner_fin" value="{{ plages.pause_dejeuner_fin }}">
|
||
<label class="login-label" for="arrivee_visee">Heures visées arrivée / départ (optionnel)</label>
|
||
<div class="settings-range">
|
||
<input id="arrivee_visee" class="login-input time-range-input" type="time" name="arrivee_visee" value="{{ plages.arrivee_visee }}">
|
||
<span class="settings-range-sep">→</span>
|
||
<input id="depart_vise" class="login-input time-range-input" type="time" name="depart_vise" value="{{ plages.depart_vise }}" aria-label="Départ visé le soir">
|
||
</div>
|
||
<button class="login-btn" type="submit">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="settings-card">
|
||
<span class="hist-label">Gestion des astreintes</span>
|
||
<p class="settings-p">
|
||
Préférence personnelle : si tu ne fais jamais d'astreinte, décoche pour
|
||
retirer de tes pages tout ce qui les concerne (bouton <b>AS</b>, saisie
|
||
d'interventions, forfaits, repos de 11h, récapitulatif et ratios). Rien
|
||
n'est supprimé — réactiver retrouve les données telles quelles.
|
||
</p>
|
||
|
||
{% if astreinte_saved %}
|
||
<div class="settings-saved">Enregistré.</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings/astreinte-active" class="settings-form">
|
||
<label class="settings-check">
|
||
<input type="checkbox" name="astreinte_active" value="1" {% if plages.astreinte_active %}checked{% endif %}>
|
||
<span>Je gère des astreintes sur ce compte</span>
|
||
</label>
|
||
<button class="login-btn" type="submit">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
|
||
{% if plages.astreinte_active %}
|
||
<div class="settings-card">
|
||
<span class="hist-label">Repos hebdomadaire de 35h</span>
|
||
<p class="settings-p">
|
||
La loi impose, une fois par période de <b>7 jours glissants</b>, un repos de
|
||
<b>35h consécutives</b> (24h de repos hebdomadaire + 11h de repos quotidien).
|
||
Un week-end normal le satisfait tout seul ; un week-end d'astreinte fragmenté
|
||
par plusieurs interventions, pas forcément. Activée, cette option repousse
|
||
l'heure de retour à <b>dernière activité + 35h</b> quand aucune période de
|
||
35h sans activité n'existe dans les 7 derniers jours — et toute nouvelle
|
||
intervention avant cette échéance <b>redémarre</b> le décompte. La présence
|
||
couverte par ce repos est créditée comme effectuée, comme pour les 11h
|
||
quotidiennes. Désactivée (défaut), seul le repos de 11h décale le retour.
|
||
</p>
|
||
|
||
{% if repos_hebdo_saved %}
|
||
<div class="settings-saved">Enregistré.</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings/repos-hebdo" class="settings-form">
|
||
<label class="settings-check">
|
||
<input type="checkbox" name="repos_hebdo_active" value="1" {% if plages.repos_hebdo_active %}checked{% endif %}>
|
||
<span>Respecter le repos hebdomadaire de 35h (7 jours glissants)</span>
|
||
</label>
|
||
<button class="login-btn" type="submit">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if plages.astreinte_active %}
|
||
<div class="settings-card">
|
||
<span class="hist-label">Ratios des heures d'astreinte</span>
|
||
<p class="settings-p">
|
||
Pondération des heures d'astreinte selon le type de jour et la fenêtre horaire :
|
||
le décompte se fait par durée réelle arrondie au quart d'heure supérieur (2, 5
|
||
ou 12 min réelles → 15 min ; 22 ou 28 → 30 min — ex. 22:39 → 23:49 = 1h10
|
||
réelles → 1h15), même si l'arrondi ne tombe pas juste sur un quart d'heure de
|
||
l'horloge — puis chaque minute comptée est multipliée par le ratio de la
|
||
première règle qui la couvre. Une minute hors règle compte ×1 (« Autre ») ; la
|
||
première règle qui couvre une minute gagne, place les fenêtres spécifiques avant
|
||
les générales. Samedi, dimanche et jours fériés comptent de 0h00 à 0h00. Règles
|
||
globales à tous les comptes.
|
||
</p>
|
||
|
||
{% if facteurs_saved %}
|
||
<div class="settings-saved">Enregistré.</div>
|
||
{% endif %}
|
||
{% if facteurs_error %}
|
||
<div class="settings-error">Règles invalides : chaque règle doit avoir un nom, au moins un type de jour, une fenêtre HH:MM et un ratio > 0 ; au moins une règle est requise (efface le nom pour supprimer une règle).</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings/astreinte-facteurs" class="as-facteurs-form">
|
||
<div id="as-ratio-rows">
|
||
{% for f in facteurs %}
|
||
{% set i = loop.index0 %}
|
||
<div class="as-ratio-edit" data-idx="{{ i }}">
|
||
<input type="text" class="login-input as-f-nom" name="f_nom_{{ i }}" value="{{ f.nom }}" maxlength="40" placeholder="Nom de la règle">
|
||
<div class="as-f-jours">
|
||
{% for j, lbl in [('semaine', 'Lun–Ven'), ('sam', 'Sam'), ('dim', 'Dim'), ('ferie', 'Férié')] %}
|
||
<label class="as-f-jour" title="Types de jours couverts par la règle"><input type="checkbox" name="f_jours_{{ i }}" value="{{ j }}" {% if j in f.jours %}checked{% endif %}> {{ lbl }}</label>
|
||
{% endfor %}
|
||
</div>
|
||
<input class="login-input time-range-input as-f-h" type="time" name="f_debut_{{ i }}" value="{{ f.debut }}" aria-label="Début de fenêtre">
|
||
<span class="settings-range-sep">→</span>
|
||
<input class="login-input time-range-input as-f-h" type="time" name="f_fin_{{ i }}" value="{{ f.fin }}" aria-label="Fin de fenêtre (avant début = à cheval sur minuit)">
|
||
<label class="as-f-x">×<input class="login-input as-f-facteur" type="number" name="f_facteur_{{ i }}" value="{{ '%g' % f.facteur }}" step="0.05" min="0.05" aria-label="Ratio"></label>
|
||
<button type="button" class="as-f-del" title="Supprimer la règle" onclick="asDelRatio(this)">×</button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="as-ratio-actions">
|
||
<button type="button" class="as-f-add" onclick="asAddRatio()">+ règle</button>
|
||
<button class="login-btn" type="submit">Enregistrer les ratios</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
function asAddRatio() {
|
||
const rows = document.getElementById('as-ratio-rows');
|
||
const idx = Math.max(-1, ...[...rows.querySelectorAll('.as-ratio-edit')].map(r => +r.dataset.idx)) + 1;
|
||
const div = document.createElement('div');
|
||
div.className = 'as-ratio-edit';
|
||
div.dataset.idx = idx;
|
||
div.innerHTML =
|
||
'<input type="text" class="login-input as-f-nom" name="f_nom_' + idx + '" maxlength="40" placeholder="Nom de la règle">' +
|
||
'<div class="as-f-jours">' +
|
||
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="semaine" checked> Lun–Ven</label>' +
|
||
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="sam"> Sam</label>' +
|
||
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="dim"> Dim</label>' +
|
||
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="ferie"> Férié</label>' +
|
||
'</div>' +
|
||
'<input class="login-input time-range-input as-f-h" type="time" name="f_debut_' + idx + '" value="20:00">' +
|
||
'<span class="settings-range-sep">→</span>' +
|
||
'<input class="login-input time-range-input as-f-h" type="time" name="f_fin_' + idx + '" value="08:00">' +
|
||
'<label class="as-f-x">×<input class="login-input as-f-facteur" type="number" name="f_facteur_' + idx + '" value="1.5" step="0.05" min="0.05"></label>' +
|
||
'<button type="button" class="as-f-del" title="Supprimer la règle" onclick="asDelRatio(this)">×</button>';
|
||
rows.appendChild(div);
|
||
div.querySelector('.as-f-nom').focus();
|
||
}
|
||
function asDelRatio(btn) { btn.closest('.as-ratio-edit').remove(); }
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% if plages.astreinte_active %}
|
||
<div class="settings-card">
|
||
<span class="hist-label">Forfait par jour d'astreinte</span>
|
||
<p class="settings-p">
|
||
Durée forfaitaire attribuée à chaque jour d'astreinte déclaré (bouton <b>AS</b>),
|
||
en plus des interventions saisies. Elle ne compte jamais dans les heures
|
||
travaillées : suivie à part dans le récapitulatif d'astreinte, elle est
|
||
considérée réalisée en journée du jour AS et bénéficie donc de la
|
||
bonification « journée » correspondante (×1 en semaine, ×1,5 le samedi,
|
||
×2 le dimanche ou un jour férié) ; le lendemain de l'astreinte n'est pas
|
||
concerné.
|
||
Vide = pas de forfait.
|
||
</p>
|
||
|
||
{% if forfait_saved %}
|
||
<div class="settings-saved">Enregistré.</div>
|
||
{% endif %}
|
||
{% if forfait_error %}
|
||
<div class="settings-error">Durée invalide (format HH:MM, 12h00 maximum).</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings/astreinte-forfait" class="settings-form">
|
||
<label class="login-label" for="astreinte_forfait">Durée ajoutée par jour d'astreinte</label>
|
||
<input id="astreinte_forfait" class="login-input time-range-input" type="time" step="900"
|
||
name="astreinte_forfait" value="{{ plages.astreinte_forfait }}">
|
||
<button class="login-btn" type="submit">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="settings-card">
|
||
<span class="hist-label">Notifications (ntfy) <a href="/aide" class="settings-help-link">Aide</a></span>
|
||
<p class="settings-p">
|
||
Crée un topic sur ton serveur <a href="https://ntfy.sh" target="_blank" rel="noopener">ntfy</a>
|
||
(public ou auto-hébergé), un identifiant secret que toi seul connais, abonne-toi dessus avec
|
||
l'app ntfy sur ton téléphone, puis renseigne les deux champs ci-dessous. Guide complet sur
|
||
la page <a href="/aide">Aide</a>.
|
||
</p>
|
||
|
||
{% if saved %}
|
||
<div class="settings-saved">Enregistré.</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings" class="settings-form">
|
||
<label class="login-label" for="ntfy_server">Serveur ntfy</label>
|
||
<input
|
||
id="ntfy_server"
|
||
class="login-input"
|
||
type="text"
|
||
name="ntfy_server"
|
||
value="{{ ntfy_server }}"
|
||
placeholder="https://ntfy.sh"
|
||
autocomplete="off"
|
||
>
|
||
<label class="login-label" for="ntfy_topic">Topic ntfy</label>
|
||
<input
|
||
id="ntfy_topic"
|
||
class="login-input"
|
||
type="text"
|
||
name="ntfy_topic"
|
||
value="{{ ntfy_topic }}"
|
||
placeholder="ex: antoine-pointeuse-x7f2"
|
||
autocomplete="off"
|
||
>
|
||
<label class="login-label" for="ntfy_token">Jeton d'accès (optionnel, serveur protégé)</label>
|
||
<input
|
||
id="ntfy_token"
|
||
class="login-input"
|
||
type="password"
|
||
name="ntfy_token"
|
||
value="{{ ntfy_token }}"
|
||
placeholder="ex: tk_xxxxxxxxxxxxxxxx"
|
||
autocomplete="off"
|
||
>
|
||
<label class="login-label" for="rappel_debut_h">Plage horaire des rappels</label>
|
||
<div class="settings-range">
|
||
<input
|
||
id="rappel_debut_h"
|
||
class="login-input settings-range-input"
|
||
type="number" min="0" max="23" name="rappel_debut_h"
|
||
value="{{ rappel_debut_h }}"
|
||
>
|
||
<span class="settings-range-sep">h → </span>
|
||
<input
|
||
id="rappel_fin_h"
|
||
class="login-input settings-range-input"
|
||
type="number" min="1" max="24" name="rappel_fin_h"
|
||
value="{{ rappel_fin_h }}"
|
||
>
|
||
<span class="settings-range-sep">h, jours ouvrés</span>
|
||
</div>
|
||
<button class="login-btn" type="submit">Enregistrer</button>
|
||
</form>
|
||
<div class="settings-test-row">
|
||
<button
|
||
class="login-btn settings-test-btn"
|
||
hx-post="/settings/test-ntfy"
|
||
hx-target="#ntfy-test-result"
|
||
hx-swap="innerHTML"
|
||
hx-disabled-elt="this"
|
||
>Tester la notification</button>
|
||
<span id="ntfy-test-result"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card">
|
||
<span class="hist-label">Détection de présence (Automation) <a href="/aide" class="settings-help-link">Aide</a></span>
|
||
<p class="settings-p">
|
||
Crée une automatisation <a href="https://f-droid.org/en/packages/com.jens.automation2/" target="_blank" rel="noopener">Automation</a>
|
||
déclenchée par le Wi-Fi du bureau (ou une géofence) : à la <b>connexion</b>/entrée, appelle l'URL « arrivée » ;
|
||
à la <b>déconnexion</b>/sortie, l'URL « départ ». Tant que tu es détecté présent sans
|
||
avoir pointé l'entrée du matin, ou parti sans avoir pointé la sortie, tu reçois un rappel
|
||
selon l'intervalle réglé ci-dessus, dans la plage horaire configurée (jours ouvrés).
|
||
Étapes détaillées sur la page <a href="/aide">Aide</a>.
|
||
</p>
|
||
<div id="presence-urls">
|
||
<div class="settings-urlrow">
|
||
<span class="settings-urllabel">Arrivée</span>
|
||
<code class="settings-url" id="url-arrivee">{{ arrivee_url }}</code>
|
||
<button type="button" class="settings-copy-btn" data-copy-target="url-arrivee">Copier</button>
|
||
</div>
|
||
<div class="settings-urlrow">
|
||
<span class="settings-urllabel">Départ</span>
|
||
<code class="settings-url" id="url-depart">{{ depart_url }}</code>
|
||
<button type="button" class="settings-copy-btn" data-copy-target="url-depart">Copier</button>
|
||
</div>
|
||
</div>
|
||
<p class="settings-hint">
|
||
Ces URLs sont secrètes (elles contiennent ton token) — ne les partage pas.
|
||
En cas de compromission, régénère le token : les anciennes URLs cesseront de fonctionner
|
||
et tu devras mettre à jour Tasker avec les nouvelles.
|
||
</p>
|
||
<button
|
||
class="login-btn settings-regen-btn"
|
||
hx-post="/settings/regenerate-presence-token"
|
||
hx-target="#presence-urls"
|
||
hx-swap="outerHTML"
|
||
hx-confirm="Régénérer le token ? Les URLs actuelles cesseront immédiatement de fonctionner, tu devras mettre à jour Tasker."
|
||
hx-disabled-elt="this"
|
||
>Régénérer le token</button>
|
||
</div>
|
||
|
||
<style>
|
||
.settings-card {
|
||
background: var(--surface);
|
||
border: 1.5px solid var(--rule);
|
||
padding: 1.25rem 1.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: .75rem;
|
||
}
|
||
.settings-p { font-size: 13px; color: var(--text); line-height: 1.6; }
|
||
.settings-p a { color: var(--accent); }
|
||
.settings-p code {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
background: var(--ground);
|
||
border: 1px solid var(--rule-l);
|
||
padding: 0 .25rem;
|
||
}
|
||
.as-facteurs-form { display: flex; flex-direction: column; gap: .5rem; }
|
||
.as-ratio-edit {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: .5rem .75rem;
|
||
padding: .5rem .8rem;
|
||
border: 1px solid var(--rule-l);
|
||
}
|
||
.as-ratio-edit + .as-ratio-edit { margin-top: .4rem; }
|
||
.as-ratio-edit .as-f-nom { width: 140px; flex-shrink: 0; }
|
||
.as-f-jours { display: flex; gap: .75rem; flex-wrap: wrap; flex: 1; }
|
||
.as-f-jour { font-size: 12px; color: var(--text); display: inline-flex; align-items: center; gap: .3rem; white-space: nowrap; }
|
||
.as-ratio-edit .as-f-h { width: 90px; flex-shrink: 0; }
|
||
.as-f-x { font-family: var(--mono); color: var(--muted); display: inline-flex; align-items: center; gap: .3rem; }
|
||
.as-ratio-edit .as-f-facteur { width: 70px; }
|
||
.as-f-del {
|
||
border: 1px solid var(--rule);
|
||
background: var(--surface);
|
||
color: var(--muted);
|
||
border-radius: 4px;
|
||
width: 22px;
|
||
height: 22px;
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
flex-shrink: 0;
|
||
}
|
||
.as-f-del:hover { color: var(--neg); border-color: var(--neg); }
|
||
.as-ratio-actions { display: flex; gap: .75rem; align-items: center; margin-top: .3rem; }
|
||
.as-f-add {
|
||
border: 1px dashed var(--rule);
|
||
background: none;
|
||
color: var(--muted);
|
||
border-radius: 4px;
|
||
padding: .35rem .7rem;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
}
|
||
.as-f-add:hover { color: var(--accent); border-color: var(--accent); }
|
||
@media (max-width: 600px) {
|
||
.as-f-jours { order: 3; width: 100%; }
|
||
}
|
||
.settings-form { display: flex; flex-direction: column; gap: .5rem; max-width: 420px; }
|
||
.settings-check {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: .55rem;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
.settings-check input { margin-top: .2rem; accent-color: var(--accent); }
|
||
.settings-saved {
|
||
background: var(--pos-bg);
|
||
border-left: 3px solid var(--pos);
|
||
color: var(--pos);
|
||
font-size: 12px;
|
||
padding: .5rem .8rem;
|
||
font-family: var(--mono);
|
||
}
|
||
.settings-error {
|
||
background: var(--neg-bg);
|
||
border-left: 3px solid var(--neg);
|
||
color: var(--neg);
|
||
font-size: 12px;
|
||
padding: .5rem .8rem;
|
||
font-family: var(--mono);
|
||
}
|
||
.settings-urlrow { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
|
||
.settings-urllabel {
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
width: 70px;
|
||
flex-shrink: 0;
|
||
}
|
||
.settings-url {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
color: var(--text);
|
||
background: var(--ground);
|
||
border: 1px solid var(--rule);
|
||
padding: .4rem .6rem;
|
||
word-break: break-all;
|
||
}
|
||
.settings-hint { font-size: 11px; color: var(--muted); }
|
||
.settings-range { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
|
||
.settings-range-input { width: 70px; text-align: center; }
|
||
.settings-range-sep { font-size: 12px; color: var(--muted); font-family: var(--mono); }
|
||
.time-range-input {
|
||
width: 128px;
|
||
text-align: left;
|
||
padding-right: .4rem;
|
||
}
|
||
.time-range-input::-webkit-calendar-picker-indicator {
|
||
margin-left: .5rem;
|
||
}
|
||
.settings-help-link {
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: .04em;
|
||
color: var(--accent);
|
||
text-decoration: none;
|
||
border: 1px solid var(--rule);
|
||
padding: .1rem .4rem;
|
||
margin-left: .5rem;
|
||
vertical-align: middle;
|
||
transition: border-color .15s, background .15s;
|
||
}
|
||
.settings-help-link:hover {
|
||
border-color: var(--accent);
|
||
background: rgba(47,95,160,.06);
|
||
}
|
||
.settings-test-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.settings-test-btn {
|
||
flex-shrink: 0;
|
||
}
|
||
.settings-regen-btn {
|
||
align-self: flex-start;
|
||
border-color: var(--neg);
|
||
color: var(--neg);
|
||
}
|
||
.settings-regen-btn:hover:not([disabled]) {
|
||
background: var(--neg-bg);
|
||
}
|
||
.settings-copy-btn {
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: .04em;
|
||
color: var(--accent);
|
||
background: none;
|
||
border: 1px solid var(--rule);
|
||
padding: .35rem .6rem;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
transition: border-color .15s, background .15s;
|
||
}
|
||
.settings-copy-btn:hover {
|
||
border-color: var(--accent);
|
||
background: rgba(47,95,160,.06);
|
||
}
|
||
.settings-copy-btn.settings-copy-btn--done {
|
||
color: var(--pos);
|
||
border-color: var(--pos);
|
||
}
|
||
#ntfy-test-result {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
#ntfy-test-result:empty { display: none; }
|
||
#ntfy-test-result:not(:empty) {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
padding: .5rem .8rem;
|
||
border-left: 3px solid var(--muted);
|
||
background: var(--ground);
|
||
line-height: 1.45;
|
||
}
|
||
</style>
|
||
<script>
|
||
(function () {
|
||
// Délégation sur document : #presence-urls est remplacé par un swap OOB
|
||
// htmx quand le token est régénéré, les boutons "Copier" doivent continuer
|
||
// à fonctionner après ce remplacement.
|
||
document.addEventListener("click", function (e) {
|
||
var btn = e.target.closest(".settings-copy-btn");
|
||
if (!btn) return;
|
||
var target = document.getElementById(btn.dataset.copyTarget);
|
||
if (!target) return;
|
||
var text = target.textContent;
|
||
|
||
var onDone = function (ok) {
|
||
var previous = btn.textContent;
|
||
btn.textContent = ok ? "Copié !" : "Échec";
|
||
btn.classList.toggle("settings-copy-btn--done", ok);
|
||
setTimeout(function () {
|
||
btn.textContent = previous;
|
||
btn.classList.remove("settings-copy-btn--done");
|
||
}, 1500);
|
||
};
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(
|
||
function () { onDone(true); },
|
||
function () { onDone(false); }
|
||
);
|
||
} else {
|
||
var textarea = document.createElement("textarea");
|
||
textarea.value = text;
|
||
textarea.style.position = "fixed";
|
||
textarea.style.opacity = "0";
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
var ok = false;
|
||
try { ok = document.execCommand("copy"); } catch (err) { ok = false; }
|
||
document.body.removeChild(textarea);
|
||
onDone(ok);
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|