refactor+ui: distinction nette Technique / Médical
Séparation des deux domaines produit, code ET interface. - code : features/technical/ (voices, fleet) vs features/medical/ (prêt pour patients, conversations, rag, questionnaires). Imports cross-domaine en absolu (robustes aux déplacements). 42/42. - UI : nav en 2 blocs distincts — 🔧 Technique (Flotte, Voix, Mises à jour, Config & moteur, Audit) et ⚕️ Médical/clinique (Patients & profils, Conversations, RAG thérapeutique, Questionnaires — nouvelle section). Accents de couleur par domaine. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d4ab9a07f
commit
bf02f6ca5d
|
|
@ -22,10 +22,10 @@ from .core.adb import Adb
|
|||
from .core.context import VERSION, Ctx
|
||||
from .core.operator_router import make_router as operator_router
|
||||
from .core.store import Store
|
||||
from .features.fleet.router import make_router as fleet_router
|
||||
from .features.voices.autosync import VoiceAutoSync
|
||||
from .features.voices.bridge import VoiceBridge
|
||||
from .features.voices.router import make_router as voices_router
|
||||
from .features.technical.fleet.router import make_router as fleet_router
|
||||
from .features.technical.voices.autosync import VoiceAutoSync
|
||||
from .features.technical.voices.bridge import VoiceBridge
|
||||
from .features.technical.voices.router import make_router as voices_router
|
||||
|
||||
_WEB_DIR = Path(__file__).resolve().parent / "web"
|
||||
_STORE_PATH = Path(os.environ.get("KAZEIA_STORE", Path.cwd() / "data" / "central.db"))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...core.context import Ctx
|
||||
from kazeia_central.core.context import Ctx
|
||||
from .service import device_overview, fan_out
|
||||
|
||||
|
||||
|
|
@ -13,8 +13,8 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any, Callable
|
||||
|
||||
from ...core.adb import Adb
|
||||
from ...core.provider import ProviderClient
|
||||
from kazeia_central.core.adb import Adb
|
||||
from kazeia_central.core.provider import ProviderClient
|
||||
|
||||
|
||||
def fan_out(adb: Adb, fn: Callable[[str], Any], *,
|
||||
|
|
@ -20,7 +20,7 @@ import posixpath
|
|||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
from ...core.adb import Adb
|
||||
from kazeia_central.core.adb import Adb
|
||||
|
||||
# getExternalFilesDir("voix") de com.kazeia.admin. /sdcard = /storage/emulated/0.
|
||||
ADMIN_VOIX_DIR = os.environ.get(
|
||||
|
|
@ -45,7 +45,7 @@ class VoiceBridge:
|
|||
f"(provisionner ov_venv, ou définir OV_PYTHON)")
|
||||
env = dict(os.environ, PYTHONPATH=str(_REPO), PYTHONUNBUFFERED="1")
|
||||
self._proc = subprocess.Popen(
|
||||
[self.ov_python, "-m", "kazeia_central.features.voices.worker"],
|
||||
[self.ov_python, "-m", "kazeia_central.features.technical.voices.worker"],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, # stderr → console
|
||||
text=True, env=env, bufsize=1,
|
||||
)
|
||||
|
|
@ -18,9 +18,9 @@ import posixpath
|
|||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
from ...core.adb import Adb
|
||||
from ...core.provider import ProviderClient
|
||||
from ...core.store import Store
|
||||
from kazeia_central.core.adb import Adb
|
||||
from kazeia_central.core.provider import ProviderClient
|
||||
from kazeia_central.core.store import Store
|
||||
from . import admin_ingest, transfer
|
||||
from .bridge import VoiceBridge
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...core.context import Ctx
|
||||
from kazeia_central.core.context import Ctx
|
||||
from . import orchestrator as vo
|
||||
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ from __future__ import annotations
|
|||
|
||||
import posixpath
|
||||
|
||||
from ...core.adb import Adb, AdbError
|
||||
from kazeia_central.core.adb import Adb, AdbError
|
||||
|
||||
# Le provider expose VOICE_WAV_DIR = "${MODELS_DIR}/../voix/voix" → ce marqueur
|
||||
# permet d'extraire MODELS_DIR depuis un wav_path, donc le dossier voix OmniVoice cible.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"""Worker d'enrôlement vocal — process persistant sous ov_venv.
|
||||
|
||||
Lancé par `voice/bridge.py` (côté API py3.14) via :
|
||||
ov_venv/bin/python -m kazeia_central.features.voices.worker
|
||||
ov_venv/bin/python -m kazeia_central.features.technical.voices.worker
|
||||
Garde le modèle OmniVoice (enrôlement + ASR de réf) **chaud**, et sert une file de
|
||||
jobs via un protocole JSON-lines sur stdin/stdout.
|
||||
|
||||
|
|
@ -25,15 +25,17 @@ async function withBusy(btn, fn) { const o = btn.textContent; btn.disabled = tru
|
|||
|
||||
// ---- sections -------------------------------------------------------------
|
||||
const SECTIONS = [
|
||||
{ group: "🔧 Technique", domain: "tech" },
|
||||
{ id: "flotte", ic: "🛰", label: "Flotte", render: renderFlotte },
|
||||
{ id: "voix", ic: "🎙", label: "Voix", render: renderVoix },
|
||||
{ id: "updates", ic: "⬆", label: "Mises à jour", render: renderUpdates },
|
||||
{ id: "config", ic: "⚙", label: "Config & moteur", render: () => stub("Config & moteur", "Config sampling (température, presets…) et prompts moteur, avec push par tablette. Dépend des méthodes call() base64-JSON côté app patiente (PROVIDER_RPC_SPEC).") },
|
||||
{ id: "audit", ic: "📋", label: "Audit", render: renderAudit },
|
||||
{ sep: "Parité admin (à venir)" },
|
||||
{ id: "profils", ic: "👤", label: "Profils", render: () => stub("Profils & fiche patient", "Fiche clinique chiffrée côté Kazeia-central (identité, contact, clinique) + langue par patient. La fiche est constructible maintenant ; la propagation de la langue passe par l'app patiente (PROFILE_LANGUAGE_SPEC livrée).") },
|
||||
{ id: "conversations", ic: "💬", label: "Conversations", render: () => stub("Conversations cliniques", "Rapatriement chiffré des conversations + export PDF/JSON + purge + collecteur de fond. Dépend de conversations_export côté app patiente.") },
|
||||
{ id: "config", ic: "⚙", label: "Config & presets", render: () => stub("Config & presets", "Édition de la config sampling + presets et push. Dépend des méthodes call() base64-JSON côté app patiente (PROVIDER_RPC_SPEC).") },
|
||||
{ id: "rag", ic: "📚", label: "RAG", render: () => stub("Corpus RAG", "Authoring du corpus + test retrieval + publication OTA. Dépend de rag_upsert_json côté app patiente.") },
|
||||
{ group: "⚕️ Médical / clinique", domain: "med" },
|
||||
{ id: "patients", ic: "👤", label: "Patients & profils", render: () => stub("Patients & fiche patient", "Dossier patient chiffré côté Kazeia-central (identité, contact, clinique) + profils on-device + langue par patient. La fiche est constructible maintenant ; la propagation de la langue passe par l'app patiente (PROFILE_LANGUAGE_SPEC livrée).") },
|
||||
{ id: "conversations", ic: "💬", label: "Conversations", render: () => stub("Conversations cliniques", "Rapatriement chiffré des conversations + export PDF/JSON + purge + collecteur de fond. Cœur RGPD. Dépend de conversations_export côté app patiente.") },
|
||||
{ id: "rag", ic: "📚", label: "RAG thérapeutique", render: () => stub("Corpus RAG thérapeutique", "Authoring du corpus clinique + test retrieval + publication OTA. Dépend de rag_upsert_json côté app patiente.") },
|
||||
{ id: "questionnaires", ic: "📝", label: "Questionnaires", render: () => stub("Questionnaires cliniques", "Passation d'échelles/questionnaires cliniques au patient et suivi des scores dans le temps. À concevoir avec toi.") },
|
||||
];
|
||||
|
||||
// ---- boot + shell ---------------------------------------------------------
|
||||
|
|
@ -73,8 +75,8 @@ async function renderShell() {
|
|||
</div>
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
<main><div id="content"></div></main>`;
|
||||
$("#sidebar").innerHTML = SECTIONS.map(s => s.sep
|
||||
? `<div class="nav-label">${s.sep}</div>`
|
||||
$("#sidebar").innerHTML = SECTIONS.map(s => s.group
|
||||
? `<div class="nav-label ${s.domain === "med" ? "dom-med" : "dom-tech"}">${s.group}</div>`
|
||||
: `<div class="nav-item" data-id="${s.id}"><span class="ic">${s.ic}</span>${s.label}</div>`).join("");
|
||||
$("#sidebar").querySelectorAll(".nav-item").forEach(n => n.onclick = () => { S.section = n.dataset.id; renderSection(); });
|
||||
await loadDevices();
|
||||
|
|
@ -94,7 +96,7 @@ async function loadDevices() {
|
|||
|
||||
function renderSection() {
|
||||
document.querySelectorAll(".nav-item").forEach(n => n.classList.toggle("active", n.dataset.id === S.section));
|
||||
const sec = SECTIONS.find(s => s.id === S.section) || SECTIONS[0];
|
||||
const sec = SECTIONS.find(s => s.id === S.section) || SECTIONS.find(s => s.render);
|
||||
const c = $("#content"); c.innerHTML = `<div class="empty"><span class="spinner"></span></div>`;
|
||||
Promise.resolve(sec.render(c)).catch(e => { c.innerHTML = `<div class="card"><div class="err">${esc(e.message)}</div></div>`; });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,10 @@ h1,h2,h3 { margin: 0; font-weight: 650; }
|
|||
.nav-item.active { background: var(--primary-soft); color: var(--primary-d); font-weight: 650; }
|
||||
.nav-sep { height: 1px; background: var(--line); margin: 10px 6px; }
|
||||
.nav-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
|
||||
padding: 6px 12px 4px; }
|
||||
padding: 12px 12px 5px; font-weight: 700; }
|
||||
.nav-label.dom-tech { color: var(--primary-d); border-top: 1px solid var(--line); margin-top: 4px; }
|
||||
.nav-label.dom-med { color: var(--ok); border-top: 1px solid var(--line); margin-top: 10px; }
|
||||
.nav-label.dom-tech:first-child { border-top: 0; margin-top: 0; }
|
||||
|
||||
main { margin: var(--top) 0 0 var(--side); padding: 22px 26px; min-height: calc(100vh - var(--top)); }
|
||||
.page-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from fastapi.testclient import TestClient
|
|||
from kazeia_central.core.adb import Adb, AdbError
|
||||
from kazeia_central.core.adb.client import AdbDevice
|
||||
from kazeia_central.app import create_app
|
||||
from kazeia_central.features.fleet.service import device_overview, fan_out
|
||||
from kazeia_central.features.technical.fleet.service import device_overview, fan_out
|
||||
from kazeia_central.core.store import Store
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import nacl.pwhash
|
|||
from kazeia_central.core.adb import Adb
|
||||
from kazeia_central.core.adb.client import AdbDevice
|
||||
from kazeia_central.core.store import Store
|
||||
from kazeia_central.features.voices import admin_ingest
|
||||
from kazeia_central.features.voices import orchestrator as vo
|
||||
from kazeia_central.features.voices import transfer
|
||||
from kazeia_central.features.technical.voices import admin_ingest
|
||||
from kazeia_central.features.technical.voices import orchestrator as vo
|
||||
from kazeia_central.features.technical.voices import transfer
|
||||
|
||||
_OPS = nacl.pwhash.argon2id.OPSLIMIT_MIN
|
||||
_MEM = nacl.pwhash.argon2id.MEMLIMIT_MIN
|
||||
|
|
@ -249,14 +249,14 @@ def test_sync_admin_delete_cleans(tmp_path):
|
|||
|
||||
# ---- déclencheur auto -----------------------------------------------------
|
||||
def test_autosync_blocked_when_locked(tmp_path):
|
||||
from kazeia_central.features.voices.autosync import VoiceAutoSync
|
||||
from kazeia_central.features.technical.voices.autosync import VoiceAutoSync
|
||||
locked = Store(tmp_path / "l.db", kdf_ops=_OPS, kdf_mem=_MEM) # pas unlock
|
||||
a = VoiceAutoSync(_Adb([_manifest("paul", "global")]), locked, _Bridge())
|
||||
assert a.sync_now("D1")["state"] == "store_verrouille"
|
||||
|
||||
|
||||
def test_autosync_enabled_ingests(tmp_path):
|
||||
from kazeia_central.features.voices.autosync import VoiceAutoSync
|
||||
from kazeia_central.features.technical.voices.autosync import VoiceAutoSync
|
||||
s = _store(tmp_path)
|
||||
a = VoiceAutoSync(_Adb([_manifest("paul", "global")]), s, _Bridge())
|
||||
a.set_enabled(True, delete_source=False)
|
||||
|
|
|
|||
Loading…
Reference in New Issue