[ADD] script todo: télémétrie de navigation + diagramme arborescent (TUI)
Nouvelle entrée « [5] Télémétrie de navigation (TUI) » dans le menu principal. - Enregistrement : Todo._menu_header (seul constructeur du fil d'Ariane) appelle todo_telemetry.record(fil) à chaque affichage de menu. Dédup des ré-affichages consécutifs -> on ne compte que les TRANSITIONS de navigation. Persistant dans ~/.erplibre/todo_telemetry.json. Best-effort : ne casse jamais la navigation. - Visualisation : todo_telemetry.run_tui() ouvre un TUI Textual affichant l'ARBRE des fonctionnalités visitées (chaque nœud = un menu, avec son nombre de visites), trié par usage décroissant. Touches : q (quitter), r (réinitialiser), e (tout déplier). Sommaire : total navigations + menus. Validé headless : enregistrement + dédup, arbre imbriqué à compteurs, montage du TUI, peuplement, touches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
62ea6ebb7f
commit
f2cb9be665
3 changed files with 191 additions and 0 deletions
|
|
@ -129,6 +129,7 @@ class TODO:
|
|||
[2] {t("Install")}
|
||||
[3] {t("Question")}
|
||||
[4] {t("Fork - Open TODO in a new tab")}
|
||||
[5] {t("Navigation telemetry (TUI)")}
|
||||
[0] {t("Quit")}
|
||||
"""
|
||||
while True:
|
||||
|
|
@ -160,6 +161,8 @@ class TODO:
|
|||
# )
|
||||
cmd = "make todo"
|
||||
self.execute.exec_command_live(cmd, source_erplibre=True)
|
||||
elif status == "5":
|
||||
self._todo_telemetry_tui()
|
||||
# elif status == "3" or status == "install":
|
||||
# print("install")
|
||||
else:
|
||||
|
|
@ -565,8 +568,24 @@ class TODO:
|
|||
header = ""
|
||||
if crumbs:
|
||||
header = "📍 " + " › ".join(crumbs) + "\n"
|
||||
# Télémétrie de navigation (best-effort, ne casse jamais le menu).
|
||||
try:
|
||||
from script.todo import todo_telemetry
|
||||
|
||||
todo_telemetry.record(" › ".join(crumbs))
|
||||
except Exception:
|
||||
pass
|
||||
return header + t("Command:")
|
||||
|
||||
def _todo_telemetry_tui(self):
|
||||
"""Ouvre le TUI arborescent de télémétrie de navigation."""
|
||||
try:
|
||||
from script.todo.todo_telemetry import run_tui
|
||||
|
||||
run_tui()
|
||||
except ImportError:
|
||||
print(t("Install textual for the telemetry TUI (pip)."))
|
||||
|
||||
def fill_help_info(self, choices):
|
||||
# Une entrée {"section": "..."} affiche un titre de section SANS
|
||||
# consommer de numéro : la numérotation reste continue sur les vraies
|
||||
|
|
|
|||
|
|
@ -1282,6 +1282,30 @@ TRANSLATIONS = {
|
|||
"fr": "suivi d'installation",
|
||||
"en": "install monitoring",
|
||||
},
|
||||
"Navigation telemetry (TUI)": {
|
||||
"fr": "Télémétrie de navigation (TUI)",
|
||||
"en": "Navigation telemetry (TUI)",
|
||||
},
|
||||
"TODO navigation telemetry": {
|
||||
"fr": "TODO — télémétrie de navigation",
|
||||
"en": "TODO navigation telemetry",
|
||||
},
|
||||
"navigations": {
|
||||
"fr": "navigations",
|
||||
"en": "navigations",
|
||||
},
|
||||
"menus": {
|
||||
"fr": "menus",
|
||||
"en": "menus",
|
||||
},
|
||||
"Telemetry reset.": {
|
||||
"fr": "Télémétrie réinitialisée.",
|
||||
"en": "Telemetry reset.",
|
||||
},
|
||||
"Install textual for the telemetry TUI (pip).": {
|
||||
"fr": "Installez textual pour le TUI de télémétrie (pip).",
|
||||
"en": "Install textual for the telemetry TUI (pip).",
|
||||
},
|
||||
"Resize a VM disk": {
|
||||
"fr": "Redimensionner le disque d'une VM",
|
||||
"en": "Resize a VM disk",
|
||||
|
|
|
|||
148
script/todo/todo_telemetry.py
Normal file
148
script/todo/todo_telemetry.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""Télémétrie de navigation du CLI TODO.
|
||||
|
||||
Enregistre les fils d'Ariane visités (« A › B › C » -> compteur) et affiche un
|
||||
DIAGRAMME arborescent des fonctionnalités dans un TUI Textual, trié par usage.
|
||||
|
||||
- record(path) : appelé par Todo._menu_header à chaque affichage de menu ; on
|
||||
dédupe les ré-affichages consécutifs pour ne compter que les TRANSITIONS.
|
||||
- run_tui() : ouvre l'arbre de navigation (compteurs par menu).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from script.todo.todo_i18n import t
|
||||
except Exception: # pragma: no cover - repli si i18n indisponible
|
||||
|
||||
def t(key: str) -> str:
|
||||
return key
|
||||
|
||||
|
||||
# Dernier chemin enregistré : évite de recompter chaque ré-affichage du même
|
||||
# menu (une commande qui revient au menu ne compte pas comme une navigation).
|
||||
_LAST = [None]
|
||||
|
||||
|
||||
def _path() -> Path:
|
||||
base = Path(os.path.expanduser("~/.erplibre"))
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
return base / "todo_telemetry.json"
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
try:
|
||||
return json.loads(_path().read_text())
|
||||
except (OSError, ValueError):
|
||||
return {"paths": {}}
|
||||
|
||||
|
||||
def _save(data: dict) -> None:
|
||||
try:
|
||||
_path().write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def record(path: str) -> None:
|
||||
"""Incrémente le compteur du menu `path`. Best-effort : ne lève jamais
|
||||
(la télémétrie ne doit jamais casser la navigation)."""
|
||||
if not path or path == _LAST[0]:
|
||||
return
|
||||
_LAST[0] = path
|
||||
try:
|
||||
data = load()
|
||||
paths = data.setdefault("paths", {})
|
||||
paths[path] = paths.get(path, 0) + 1
|
||||
data["updated"] = int(time.time())
|
||||
_save(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def reset() -> None:
|
||||
_save({"paths": {}})
|
||||
|
||||
|
||||
def _nested(paths: dict) -> dict:
|
||||
"""{« A › B › C »: count} -> arbre {nom: {'count', 'children'}}. Chaque
|
||||
menu enregistre son propre chemin, donc chaque nœud a son compteur."""
|
||||
root: dict = {}
|
||||
for path, count in paths.items():
|
||||
cur = root
|
||||
parts = path.split(" › ")
|
||||
for i, name in enumerate(parts):
|
||||
node = cur.setdefault(name, {"count": 0, "children": {}})
|
||||
if i == len(parts) - 1:
|
||||
node["count"] += count
|
||||
cur = node["children"]
|
||||
return root
|
||||
|
||||
|
||||
def run_tui(run_app: bool = True):
|
||||
"""Ouvre le TUI arborescent. `run_app=False` renvoie l'app (tests)."""
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Footer, Header, Static, Tree
|
||||
|
||||
class Telemetry(App):
|
||||
CSS = """
|
||||
#summary { height: 1; color: $text-muted; }
|
||||
Tree { border: solid $accent; }
|
||||
"""
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quitter"),
|
||||
("r", "reset", "Réinitialiser"),
|
||||
("e", "expand_all", "Tout déplier"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
yield Static("", id="summary")
|
||||
yield Tree("📍 TODO", id="nav")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
self.title = t("TODO navigation telemetry")
|
||||
self._populate()
|
||||
|
||||
def _populate(self):
|
||||
data = load()
|
||||
paths = data.get("paths", {})
|
||||
total = sum(paths.values())
|
||||
self.query_one("#summary", Static).update(
|
||||
f" {total} {t('navigations')} · {len(paths)} {t('menus')}"
|
||||
)
|
||||
tree = self.query_one("#nav", Tree)
|
||||
tree.clear()
|
||||
nested = _nested(paths)
|
||||
todo = nested.get("TODO", {"count": 0, "children": nested})
|
||||
tree.root.set_label(f"📍 TODO ({todo['count']})")
|
||||
self._add(tree.root, todo["children"])
|
||||
tree.root.expand()
|
||||
|
||||
def _add(self, tnode, children):
|
||||
# Tri par visites décroissantes : les plus utilisés en tête.
|
||||
for name, node in sorted(
|
||||
children.items(), key=lambda kv: -kv[1]["count"]
|
||||
):
|
||||
child = tnode.add(f"{name} ({node['count']})", expand=True)
|
||||
self._add(child, node["children"])
|
||||
|
||||
def action_reset(self):
|
||||
reset()
|
||||
self._populate()
|
||||
self.notify(t("Telemetry reset."))
|
||||
|
||||
def action_expand_all(self):
|
||||
self.query_one("#nav", Tree).root.expand_all()
|
||||
|
||||
app = Telemetry()
|
||||
if run_app:
|
||||
app.run()
|
||||
return app
|
||||
Loading…
Reference in a new issue