mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Stockage : corrections de relecture — le remède Unraid décrit tel qu'il existe
« Exclusive access » n'est pas un réglage qu'on passe à Yes sur la page du partage : c'est un état, que Unraid 6.12 accorde quand « Permit exclusive shares » est activé et que le partage vit tout entier sur un pool, sans stockage secondaire (/mnt/user/<partage> devient un lien vers le pool). La consigne précédente envoyait chercher une option introuvable. - conseil, compose et README : le vrai chemin (Global Share Settings, mover vers le pool puis Secondary = None, « Exclusive access : Yes »), et redémarrer le conteneur, Docker ne résolvant le lien qu'au démarrage ; - un seul chemin par montage fuse.shfs : /data/models répétait /data ; - l'encart de l'UI, permanent tant que la cause dure, devient masquable (localStorage, pour ce texte seulement — un autre conseil réapparaît). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
056f307983
commit
9f6bd725e6
7 files changed
+86
-26
No files matched your search
@@ -174,9 +174,13 @@ utilisateur, ce qui ralentit le décodage. Loki le détecte au démarrage
|
|||||||
(`fuse.shfs` dans `/proc/self/mountinfo`) et l'affiche en encart d'information —
|
(`fuse.shfs` dans `/proc/self/mountinfo`) et l'affiche en encart d'information —
|
||||||
rien n'est perdu ni altéré. Pour l'éviter :
|
rien n'est perdu ni altéré. Pour l'éviter :
|
||||||
|
|
||||||
- **recommandé** — passe le partage `appdata` (et celui des modèles) en
|
- **recommandé** — partages en *Exclusive access* (Unraid 6.12+). Ce n'est
|
||||||
*Exclusive access* (Unraid 6.12+, partage sur un seul pool) : le chemin
|
pas une case mais un état : *Global Share Settings* → *Permit exclusive
|
||||||
`/mnt/user/…` ne change pas, FUSE est court-circuité ;
|
shares* = Yes, puis le partage `appdata` (et celui des modèles) entièrement
|
||||||
|
sur **un** pool — mover d'abord vers le pool, ensuite *Secondary storage* =
|
||||||
|
None — jusqu'à lire *Exclusive access : Yes* sur la page du partage.
|
||||||
|
Redémarre le conteneur : le chemin `/mnt/user/…` ne change pas, FUSE est
|
||||||
|
court-circuité ;
|
||||||
- **avancé** — mappe `/mnt/<ton-pool>/appdata/loki/…` (ex. `/mnt/cache` si ton
|
- **avancé** — mappe `/mnt/<ton-pool>/appdata/loki/…` (ex. `/mnt/cache` si ton
|
||||||
pool s'appelle `cache`), avec une migration manuelle : `Compose Down`,
|
pool s'appelle `cache`), avec une migration manuelle : `Compose Down`,
|
||||||
`rsync -a` de l'ancien dossier vers le nouveau, puis modification des deux
|
`rsync -a` de l'ancien dossier vers le nouveau, puis modification des deux
|
||||||
|
|||||||
@@ -45,9 +45,14 @@ services:
|
|||||||
# modèle relue depuis le disque (modèle plus gros que la RAM) traverse un
|
# modèle relue depuis le disque (modèle plus gros que la RAM) traverse un
|
||||||
# démon en espace utilisateur. Loki le signale au démarrage (encart
|
# démon en espace utilisateur. Loki le signale au démarrage (encart
|
||||||
# d'information, rien n'est perdu). Deux remèdes, au choix :
|
# d'information, rien n'est perdu). Deux remèdes, au choix :
|
||||||
# - Recommandé : Shares -> appdata (et le partage des modèles) ->
|
# - Recommandé (Unraid 6.12+) : partages en « Exclusive access ».
|
||||||
# « Exclusive access » = Yes (Unraid 6.12+, partage sur un seul
|
# Ce n'est pas une case mais un état : Settings -> Global Share
|
||||||
# pool). Le chemin /mnt/user reste le même, FUSE est court-circuité.
|
# Settings -> « Permit exclusive shares » = Yes, puis le partage
|
||||||
|
# appdata (et celui des modèles) entièrement sur UN pool — mover
|
||||||
|
# d'abord vers le pool, ensuite Secondary storage = None. La page du
|
||||||
|
# partage affiche alors « Exclusive access : Yes ». Redémarre le
|
||||||
|
# conteneur : le chemin /mnt/user reste le même, FUSE est
|
||||||
|
# court-circuité.
|
||||||
# - Avancé : mapper /mnt/<ton-pool>/appdata/loki/… (ex. /mnt/cache si
|
# - Avancé : mapper /mnt/<ton-pool>/appdata/loki/… (ex. /mnt/cache si
|
||||||
# ton pool s'appelle « cache »). Migration MANUELLE obligatoire :
|
# ton pool s'appelle « cache »). Migration MANUELLE obligatoire :
|
||||||
# Compose Down, rsync -a de l'ancien dossier vers le nouveau, puis
|
# Compose Down, rsync -a de l'ancien dossier vers le nouveau, puis
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ package loki
|
|||||||
// donnerait un /data vide et ferait croire à une perte de données (voir le
|
// donnerait un /data vide et ferait croire à une perte de données (voir le
|
||||||
// README). Il se contente de le VOIR et de le DIRE, sur un ton d'information —
|
// README). Il se contente de le VOIR et de le DIRE, sur un ton d'information —
|
||||||
// pas le bandeau rouge de sys_datavolume.go, réservé à la perte de données. Le
|
// pas le bandeau rouge de sys_datavolume.go, réservé à la perte de données. Le
|
||||||
// remède sans changement de chemin : passer les partages en « Exclusive
|
// remède sans changement de chemin : des partages en « Exclusive access »
|
||||||
// access » (Unraid 6.12+), qui court-circuite shfs.
|
// (Unraid 6.12+). Ce n'est pas une case à cocher mais un état : « Permit
|
||||||
|
// exclusive shares » activé, partage tout entier sur un pool, sans stockage
|
||||||
|
// secondaire — /mnt/user/<partage> devient alors un lien symbolique vers le
|
||||||
|
// pool, que Docker résout au démarrage du conteneur.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -93,22 +96,30 @@ func unescapeMountinfo(s string) string {
|
|||||||
// "" quand aucun montage ne couvre le chemin. Chemins Linux : package path, et
|
// "" quand aucun montage ne couvre le chemin. Chemins Linux : package path, et
|
||||||
// non filepath, pour que les tests lisent la même chose sous Windows.
|
// non filepath, pour que les tests lisent la même chose sous Windows.
|
||||||
func mountFSType(mounts []mountEntry, target string) string {
|
func mountFSType(mounts []mountEntry, target string) string {
|
||||||
|
m, _ := mountOf(mounts, target)
|
||||||
|
return m.fsType
|
||||||
|
}
|
||||||
|
|
||||||
|
// mountOf renvoie le montage qui porte target (règle de mountFSType) ; false
|
||||||
|
// quand aucun ne le couvre.
|
||||||
|
func mountOf(mounts []mountEntry, target string) (mountEntry, bool) {
|
||||||
p := path.Clean(target)
|
p := path.Clean(target)
|
||||||
best, fs := -1, ""
|
best, found := -1, mountEntry{}
|
||||||
for _, m := range mounts {
|
for _, m := range mounts {
|
||||||
mp := m.point
|
mp := m.point
|
||||||
if mp != "/" && p != mp && !strings.HasPrefix(p, mp+"/") {
|
if mp != "/" && p != mp && !strings.HasPrefix(p, mp+"/") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(mp) >= best {
|
if len(mp) >= best {
|
||||||
best, fs = len(mp), m.fsType
|
best, found = len(mp), m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fs
|
return found, best >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// shfsPaths renvoie, parmi paths, ceux qui passent par fuse.shfs (dédoublonnés,
|
// shfsPaths renvoie, parmi paths, ceux qui passent par fuse.shfs, dans l'ordre
|
||||||
// dans l'ordre reçu).
|
// reçu et un seul par montage : /data/models, qui vit sur le montage de /data,
|
||||||
|
// n'apprendrait rien de plus à l'utilisateur et allongerait le conseil.
|
||||||
func shfsPaths(mounts []mountEntry, paths []string) []string {
|
func shfsPaths(mounts []mountEntry, paths []string) []string {
|
||||||
var out []string
|
var out []string
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
@@ -116,14 +127,12 @@ func shfsPaths(mounts []mountEntry, paths []string) []string {
|
|||||||
if strings.TrimSpace(p) == "" {
|
if strings.TrimSpace(p) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p = path.Clean(p)
|
m, ok := mountOf(mounts, p)
|
||||||
if seen[p] {
|
if !ok || m.fsType != "fuse.shfs" || seen[m.point] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[p] = true
|
seen[m.point] = true
|
||||||
if mountFSType(mounts, p) == "fuse.shfs" {
|
out = append(out, path.Clean(p))
|
||||||
out = append(out, p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
@@ -137,8 +146,9 @@ func storageHintText(paths []string) string {
|
|||||||
return "ℹ️ Stockage via la couche FUSE des partages Unraid (fuse.shfs) : " + strings.Join(paths, ", ") + ". " +
|
return "ℹ️ Stockage via la couche FUSE des partages Unraid (fuse.shfs) : " + strings.Join(paths, ", ") + ". " +
|
||||||
"Les écritures de la base et les pages du modèle relues depuis le disque (gros modèle qui ne tient pas en RAM) " +
|
"Les écritures de la base et les pages du modèle relues depuis le disque (gros modèle qui ne tient pas en RAM) " +
|
||||||
"traversent un démon en espace utilisateur, ce qui ralentit le décodage. Rien n'est perdu. " +
|
"traversent un démon en espace utilisateur, ce qui ralentit le décodage. Rien n'est perdu. " +
|
||||||
"Remède sans changer de chemin : partages appdata/modèles en « Exclusive access » (Unraid 6.12+). " +
|
"Remède sans changer de chemin (Unraid 6.12+) : Global Share Settings → « Permit exclusive shares » = Yes, " +
|
||||||
"Voir docker-compose.unraid.yml."
|
"partages appdata/modèles entièrement sur un pool sans stockage secondaire, jusqu'à lire « Exclusive access : Yes », " +
|
||||||
|
"puis redémarrer le conteneur. Voir docker-compose.unraid.yml."
|
||||||
}
|
}
|
||||||
|
|
||||||
// storageHint mis en cache : /api/status est interrogé en boucle, et la liste
|
// storageHint mis en cache : /api/status est interrogé en boucle, et la liste
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ func TestShfsPaths(t *testing.T) {
|
|||||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||||
t.Fatalf("shfsPaths = %v, attendu %v", got, want)
|
t.Fatalf("shfsPaths = %v, attendu %v", got, want)
|
||||||
}
|
}
|
||||||
|
// Un sous-dossier du même montage (LOKI_MODEL_DIRS=/data/gguf) n'est pas
|
||||||
|
// répété : un seul chemin par montage fuse.shfs.
|
||||||
|
got = shfsPaths(m, []string{"/data/gguf", "/data", "/mnt/mes modeles/a"})
|
||||||
|
want = []string{"/data/gguf", "/mnt/mes modeles/a"}
|
||||||
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||||
|
t.Fatalf("shfsPaths (même montage) = %v, attendu %v", got, want)
|
||||||
|
}
|
||||||
// Partages en « Exclusive access » : plus de fuse.shfs, plus de conseil.
|
// Partages en « Exclusive access » : plus de fuse.shfs, plus de conseil.
|
||||||
excl := `1 0 0:1 / / rw - overlay overlay rw
|
excl := `1 0 0:1 / / rw - overlay overlay rw
|
||||||
2 1 0:2 /appdata/loki/data /data rw - btrfs /dev/nvme0n1p1 rw
|
2 1 0:2 /appdata/loki/data /data rw - btrfs /dev/nvme0n1p1 rw
|
||||||
@@ -79,7 +86,7 @@ func TestStorageHintText(t *testing.T) {
|
|||||||
t.Fatalf("rien à signaler : %q", s)
|
t.Fatalf("rien à signaler : %q", s)
|
||||||
}
|
}
|
||||||
s := storageHintText([]string{"/data", "/models"})
|
s := storageHintText([]string{"/data", "/models"})
|
||||||
for _, w := range []string{"/data, /models", "fuse.shfs", "Exclusive access"} {
|
for _, w := range []string{"/data, /models", "fuse.shfs", "Permit exclusive shares", "Exclusive access", "Rien n'est perdu"} {
|
||||||
if !strings.Contains(s, w) {
|
if !strings.Contains(s, w) {
|
||||||
t.Errorf("conseil sans %q : %s", w, s)
|
t.Errorf("conseil sans %q : %s", w, s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2301,7 +2301,9 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
|||||||
</details>
|
</details>
|
||||||
<!-- Avertissement de lancement (App Translocation macOS) — masqué par défaut. -->
|
<!-- Avertissement de lancement (App Translocation macOS) — masqué par défaut. -->
|
||||||
<div id="app-warn" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
|
<div id="app-warn" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
|
||||||
<div id="app-hint" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;border:1px solid var(--border)"></div>
|
<!-- Conseil de performance (stockage Unraid via fuse.shfs) : permanent tant
|
||||||
|
que la cause dure, donc masquable — mémorisé pour CE texte seulement. -->
|
||||||
|
<div id="app-hint" class="muted" style="display:none;position:relative;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 24px 8px 10px;border-radius:4px;border:1px solid var(--border)"><span id="app-hint-text"></span><button type="button" onclick="dismissHint()" title="masquer ce conseil" aria-label="masquer ce conseil" style="position:absolute;top:3px;right:3px;margin:0;padding:0 5px;border:0;background:none;color:inherit;font-size:14px;line-height:1.3">×</button></div>
|
||||||
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)"></div>
|
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)"></div>
|
||||||
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état
|
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état
|
||||||
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
||||||
@@ -4045,9 +4047,16 @@ async function loadStatus(){
|
|||||||
}
|
}
|
||||||
// Conseil de performance (stockage Unraid via fuse.shfs) : encart discret,
|
// Conseil de performance (stockage Unraid via fuse.shfs) : encart discret,
|
||||||
// pas le bandeau d'alerte — rien n'est perdu, seul le décodage ralentit.
|
// pas le bandeau d'alerte — rien n'est perdu, seul le décodage ralentit.
|
||||||
|
// Il reste affiché tant que la cause dure : masquable, et seulement pour CE
|
||||||
|
// texte — un autre conseil (autre chemin concerné) réapparaît.
|
||||||
const hb=document.getElementById('app-hint');
|
const hb=document.getElementById('app-hint');
|
||||||
if(hb){
|
if(hb){
|
||||||
if(s.hint){ hb.textContent=s.hint; hb.style.display=''; }
|
let gone='';
|
||||||
|
try{ gone=localStorage.getItem('loki-hint-dismissed')||''; }catch(e){}
|
||||||
|
if(s.hint && s.hint!==gone){
|
||||||
|
document.getElementById('app-hint-text').textContent=s.hint;
|
||||||
|
hb.dataset.hint=s.hint; hb.style.display='';
|
||||||
|
}
|
||||||
else { hb.style.display='none'; }
|
else { hb.style.display='none'; }
|
||||||
}
|
}
|
||||||
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
||||||
@@ -4058,6 +4067,14 @@ async function loadStatus(){
|
|||||||
else { me.style.display='none'; }
|
else { me.style.display='none'; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Masque le conseil de stockage pour ce navigateur (simple confort : sans
|
||||||
|
// localStorage, il revient au prochain rafraîchissement, rien de plus).
|
||||||
|
function dismissHint(){
|
||||||
|
const hb=document.getElementById('app-hint');
|
||||||
|
if(!hb) return;
|
||||||
|
try{ localStorage.setItem('loki-hint-dismissed', hb.dataset.hint||''); }catch(e){}
|
||||||
|
hb.style.display='none';
|
||||||
|
}
|
||||||
// Journal du moteur — section repliable comme les autres. La pastille d'état
|
// Journal du moteur — section repliable comme les autres. La pastille d'état
|
||||||
// reste un raccourci ; le chargement du journal est déclenché par l'événement
|
// reste un raccourci ; le chargement du journal est déclenché par l'événement
|
||||||
// `toggle` du <details>, qu'on l'ouvre par la pastille ou par son chevron.
|
// `toggle` du <details>, qu'on l'ouvre par la pastille ou par son chevron.
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
|||||||
</details>
|
</details>
|
||||||
<!-- Avertissement de lancement (App Translocation macOS) — masqué par défaut. -->
|
<!-- Avertissement de lancement (App Translocation macOS) — masqué par défaut. -->
|
||||||
<div id="app-warn" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
|
<div id="app-warn" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
|
||||||
<div id="app-hint" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;border:1px solid var(--border)"></div>
|
<!-- Conseil de performance (stockage Unraid via fuse.shfs) : permanent tant
|
||||||
|
que la cause dure, donc masquable — mémorisé pour CE texte seulement. -->
|
||||||
|
<div id="app-hint" class="muted" style="display:none;position:relative;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 24px 8px 10px;border-radius:4px;border:1px solid var(--border)"><span id="app-hint-text"></span><button type="button" onclick="dismissHint()" title="masquer ce conseil" aria-label="masquer ce conseil" style="position:absolute;top:3px;right:3px;margin:0;padding:0 5px;border:0;background:none;color:inherit;font-size:14px;line-height:1.3">×</button></div>
|
||||||
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)"></div>
|
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)"></div>
|
||||||
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état
|
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état
|
||||||
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
||||||
|
|||||||
@@ -52,9 +52,16 @@ async function loadStatus(){
|
|||||||
}
|
}
|
||||||
// Conseil de performance (stockage Unraid via fuse.shfs) : encart discret,
|
// Conseil de performance (stockage Unraid via fuse.shfs) : encart discret,
|
||||||
// pas le bandeau d'alerte — rien n'est perdu, seul le décodage ralentit.
|
// pas le bandeau d'alerte — rien n'est perdu, seul le décodage ralentit.
|
||||||
|
// Il reste affiché tant que la cause dure : masquable, et seulement pour CE
|
||||||
|
// texte — un autre conseil (autre chemin concerné) réapparaît.
|
||||||
const hb=document.getElementById('app-hint');
|
const hb=document.getElementById('app-hint');
|
||||||
if(hb){
|
if(hb){
|
||||||
if(s.hint){ hb.textContent=s.hint; hb.style.display=''; }
|
let gone='';
|
||||||
|
try{ gone=localStorage.getItem('loki-hint-dismissed')||''; }catch(e){}
|
||||||
|
if(s.hint && s.hint!==gone){
|
||||||
|
document.getElementById('app-hint-text').textContent=s.hint;
|
||||||
|
hb.dataset.hint=s.hint; hb.style.display='';
|
||||||
|
}
|
||||||
else { hb.style.display='none'; }
|
else { hb.style.display='none'; }
|
||||||
}
|
}
|
||||||
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
||||||
@@ -65,6 +72,14 @@ async function loadStatus(){
|
|||||||
else { me.style.display='none'; }
|
else { me.style.display='none'; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Masque le conseil de stockage pour ce navigateur (simple confort : sans
|
||||||
|
// localStorage, il revient au prochain rafraîchissement, rien de plus).
|
||||||
|
function dismissHint(){
|
||||||
|
const hb=document.getElementById('app-hint');
|
||||||
|
if(!hb) return;
|
||||||
|
try{ localStorage.setItem('loki-hint-dismissed', hb.dataset.hint||''); }catch(e){}
|
||||||
|
hb.style.display='none';
|
||||||
|
}
|
||||||
// Journal du moteur — section repliable comme les autres. La pastille d'état
|
// Journal du moteur — section repliable comme les autres. La pastille d'état
|
||||||
// reste un raccourci ; le chargement du journal est déclenché par l'événement
|
// reste un raccourci ; le chargement du journal est déclenché par l'événement
|
||||||
// `toggle` du <details>, qu'on l'ouvre par la pastille ou par son chevron.
|
// `toggle` du <details>, qu'on l'ouvre par la pastille ou par son chevron.
|
||||||
|
|||||||
Reference in new issue
Block a user