Le forfait était suivi brut, sans bonification. Considéré réalisé en
journée du jour où il est attribué, il reçoit désormais le facteur
journée correspondant : ×1 en semaine, ×1,5 le samedi, ×2 le dimanche
ou un jour férié (suit les règles de ratios configurées). Toujours hors
heures travaillées ; affiché pondéré sur la ligne du jour, dans les
totaux du récapitulatif et expliqué dans Réglages.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
547 lines
22 KiB
HTML
547 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. 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 %}
|