GUI config
9c96cb0 parent: b50cd7b modified
packages/regine-gui/src/regine_gui/app.py +6 -3 | @@ -7,14 +7,16 @@ from pathlib import Path | ||
| 7 | 7 | |
| 8 | 8 | from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget |
| 9 | 9 | |
| 10 | +from regine_gui.config_view import EcranConfiguration | |
| 10 | 11 | from regine_gui.consultation_view import EcranConsultation |
| 11 | 12 | from regine_gui.tri_view import EcranTri |
| 12 | 13 | |
| 13 | 14 | |
| 14 | 15 | class FenetrePrincipale(QMainWindow): |
| 15 | - """Regroupe l'écran de tri (US1) et l'écran de consultation (US2) sous deux | |
| 16 | - onglets d'une même fenêtre — chacun appelle sa propre partie de regine-core, | |
| 17 | - aucune logique partagée entre les deux ici (Principe VI).""" | |
| 16 | + """Regroupe l'écran de tri (US1), l'écran de consultation (US2) et l'écran de | |
| 17 | + configuration (specs/003 User Story 4) sous trois onglets d'une même fenêtre — | |
| 18 | + chacun appelle sa propre partie de regine-core, aucune logique partagée entre | |
| 19 | + eux ici (Principe VI).""" | |
| 18 | 20 | |
| 19 | 21 | def __init__(self, contexte_db: Path | None = None) -> None: |
| 20 | 22 | super().__init__() |
| @@ -25,6 +27,7 @@ class FenetrePrincipale(QMainWindow): | ||
| 25 | 27 | self.setCentralWidget(onglets) |
| 26 | 28 | onglets.addTab(EcranTri(), "Tri") |
| 27 | 29 | onglets.addTab(EcranConsultation(contexte_db=contexte_db), "Consultation") |
| 30 | + onglets.addTab(EcranConfiguration(contexte_db=contexte_db), "Configuration") | |
| 28 | 31 | |
| 29 | 32 | |
| 30 | 33 | def main(argv: list[str] | None = None) -> int: |
| @@ -7,14 +7,16 @@ from pathlib import Path | |||
| 7 | 7 | ||
| 8 | from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget | 8 | from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget |
| 9 | 9 | ||
| 10 | +from regine_gui.config_view import EcranConfiguration | ||
| 10 | from regine_gui.consultation_view import EcranConsultation | 11 | from regine_gui.consultation_view import EcranConsultation |
| 11 | from regine_gui.tri_view import EcranTri | 12 | from regine_gui.tri_view import EcranTri |
| 12 | 13 | ||
| 13 | 14 | ||
| 14 | class FenetrePrincipale(QMainWindow): | 15 | class FenetrePrincipale(QMainWindow): |
| 15 | - """Regroupe l'écran de tri (US1) et l'écran de consultation (US2) sous deux | 16 | + """Regroupe l'écran de tri (US1), l'écran de consultation (US2) et l'écran de |
| 16 | - onglets d'une même fenêtre — chacun appelle sa propre partie de regine-core, | 17 | + configuration (specs/003 User Story 4) sous trois onglets d'une même fenêtre — |
| 17 | - aucune logique partagée entre les deux ici (Principe VI).""" | 18 | + chacun appelle sa propre partie de regine-core, aucune logique partagée entre |
| 19 | + eux ici (Principe VI).""" | ||
| 18 | 20 | ||
| 19 | def __init__(self, contexte_db: Path | None = None) -> None: | 21 | def __init__(self, contexte_db: Path | None = None) -> None: |
| 20 | super().__init__() | 22 | super().__init__() |
| @@ -25,6 +27,7 @@ class FenetrePrincipale(QMainWindow): | |||
| 25 | self.setCentralWidget(onglets) | 27 | self.setCentralWidget(onglets) |
| 26 | onglets.addTab(EcranTri(), "Tri") | 28 | onglets.addTab(EcranTri(), "Tri") |
| 27 | onglets.addTab(EcranConsultation(contexte_db=contexte_db), "Consultation") | 29 | onglets.addTab(EcranConsultation(contexte_db=contexte_db), "Consultation") |
| 30 | + onglets.addTab(EcranConfiguration(contexte_db=contexte_db), "Configuration") | ||
| 28 | 31 | ||
| 29 | 32 | ||
| 30 | def main(argv: list[str] | None = None) -> int: | 33 | def main(argv: list[str] | None = None) -> int: |
added
packages/regine-gui/src/regine_gui/config_view.py +179 -0 | new file mode 100644 | ||
| @@ -0,0 +1,179 @@ | ||
| 1 | +"""Écran de configuration (User Story 4, specs/003) — appelle exclusivement | |
| 2 | +`regine_core.config`, aucune logique de validation propre à la GUI (Principe VI). | |
| 3 | + | |
| 4 | +Chaque exception de `regine_core.config.context.enregistrer_chemins` est | |
| 5 | +présentée clairement (boîte de dialogue), jamais comme une exception technique | |
| 6 | +brute — mêmes règles que la CLI (`regine config set-paths`), jamais devinées ou | |
| 7 | +recalculées ici. | |
| 8 | +""" | |
| 9 | + | |
| 10 | +from __future__ import annotations | |
| 11 | + | |
| 12 | +from pathlib import Path | |
| 13 | + | |
| 14 | +from PySide6.QtCore import Qt | |
| 15 | +from PySide6.QtWidgets import ( | |
| 16 | + QCheckBox, | |
| 17 | + QFormLayout, | |
| 18 | + QHBoxLayout, | |
| 19 | + QInputDialog, | |
| 20 | + QLabel, | |
| 21 | + QLineEdit, | |
| 22 | + QListWidget, | |
| 23 | + QListWidgetItem, | |
| 24 | + QMessageBox, | |
| 25 | + QPushButton, | |
| 26 | + QVBoxLayout, | |
| 27 | + QWidget, | |
| 28 | +) | |
| 29 | +from regine_core.camera_profile.db import Boitier | |
| 30 | +from regine_core.config.cameras_screen import BoitierInconnuError, lister_boitiers, nommer_boitier | |
| 31 | +from regine_core.config.context import ( | |
| 32 | + ArchiveInaccessibleError, | |
| 33 | + CheminArchiveInvalideError, | |
| 34 | + CheminsAmbigusError, | |
| 35 | + RepertoireAbsentError, | |
| 36 | + TravailNonReconcilieError, | |
| 37 | + charger_contexte, | |
| 38 | + enregistrer_chemins, | |
| 39 | +) | |
| 40 | +from regine_core.config.db import open_context_db | |
| 41 | + | |
| 42 | +_ROLE_ID = Qt.ItemDataRole.UserRole | |
| 43 | + | |
| 44 | + | |
| 45 | +class EcranConfiguration(QWidget): | |
| 46 | + """Onglet de configuration : chemins, état SMB, boîtiers nommés — même | |
| 47 | + contexte que la CLI (`regine config ...`), jamais un état parallèle (SC-008).""" | |
| 48 | + | |
| 49 | + def __init__(self, contexte_db: Path | None = None) -> None: | |
| 50 | + super().__init__() | |
| 51 | + self._contexte_db = contexte_db | |
| 52 | + | |
| 53 | + disposition = QVBoxLayout(self) | |
| 54 | + | |
| 55 | + formulaire = QFormLayout() | |
| 56 | + self._champ_temp_dir = QLineEdit() | |
| 57 | + formulaire.addRow("Répertoire temporaire :", self._champ_temp_dir) | |
| 58 | + self._champ_local_work_dir = QLineEdit() | |
| 59 | + formulaire.addRow("Répertoire de travail local :", self._champ_local_work_dir) | |
| 60 | + self._champ_archive_smb = QLineEdit() | |
| 61 | + formulaire.addRow("Archive (smb://...) :", self._champ_archive_smb) | |
| 62 | + disposition.addLayout(formulaire) | |
| 63 | + | |
| 64 | + self._case_creer = QCheckBox("Créer les répertoires manquants") | |
| 65 | + disposition.addWidget(self._case_creer) | |
| 66 | + self._case_forcer_collision = QCheckBox("Forcer malgré une collision temp/local") | |
| 67 | + disposition.addWidget(self._case_forcer_collision) | |
| 68 | + self._case_ignorer_verification = QCheckBox( | |
| 69 | + "Ignorer la vérification de travail non réconcilié (archive inaccessible)" | |
| 70 | + ) | |
| 71 | + disposition.addWidget(self._case_ignorer_verification) | |
| 72 | + | |
| 73 | + boutons = QHBoxLayout() | |
| 74 | + bouton_rafraichir = QPushButton("Rafraîchir") | |
| 75 | + bouton_rafraichir.clicked.connect(self._rafraichir) | |
| 76 | + boutons.addWidget(bouton_rafraichir) | |
| 77 | + bouton_enregistrer = QPushButton("Enregistrer") | |
| 78 | + bouton_enregistrer.clicked.connect(self._enregistrer) | |
| 79 | + boutons.addWidget(bouton_enregistrer) | |
| 80 | + disposition.addLayout(boutons) | |
| 81 | + | |
| 82 | + self._label_etat_smb = QLabel("État SMB : (non chargé)") | |
| 83 | + disposition.addWidget(self._label_etat_smb) | |
| 84 | + | |
| 85 | + disposition.addWidget(QLabel("Boîtiers connus :")) | |
| 86 | + self._liste_boitiers = QListWidget() | |
| 87 | + disposition.addWidget(self._liste_boitiers) | |
| 88 | + bouton_nommer = QPushButton("Nommer la sélection…") | |
| 89 | + bouton_nommer.clicked.connect(self._nommer_boitier_selectionne) | |
| 90 | + disposition.addWidget(bouton_nommer) | |
| 91 | + | |
| 92 | + self._rafraichir() | |
| 93 | + | |
| 94 | + def _ouvrir_conn(self): | |
| 95 | + return open_context_db(self._contexte_db) | |
| 96 | + | |
| 97 | + def _rafraichir(self) -> None: | |
| 98 | + conn = self._ouvrir_conn() | |
| 99 | + try: | |
| 100 | + contexte = charger_contexte(conn) | |
| 101 | + self._champ_temp_dir.setText(str(contexte.temp_dir) if contexte.temp_dir else "") | |
| 102 | + self._champ_local_work_dir.setText( | |
| 103 | + str(contexte.local_work_dir) if contexte.local_work_dir else "" | |
| 104 | + ) | |
| 105 | + self._champ_archive_smb.setText(contexte.archive_smb_path or "") | |
| 106 | + self._label_etat_smb.setText(f"État SMB : {contexte.smb_last_known_state}") | |
| 107 | + | |
| 108 | + self._liste_boitiers.clear() | |
| 109 | + for boitier in lister_boitiers(conn): | |
| 110 | + self._ajouter_boitier(boitier) | |
| 111 | + finally: | |
| 112 | + conn.close() | |
| 113 | + | |
| 114 | + def _ajouter_boitier(self, boitier: Boitier) -> None: | |
| 115 | + nom = boitier.nom_lisible or "(non nommé)" | |
| 116 | + serie = f", {boitier.numero_serie}" if boitier.numero_serie else "" | |
| 117 | + item = QListWidgetItem(f"[{boitier.id}] {nom} — {boitier.modele}{serie}") | |
| 118 | + item.setData(_ROLE_ID, boitier.id) | |
| 119 | + self._liste_boitiers.addItem(item) | |
| 120 | + | |
| 121 | + def _enregistrer(self) -> None: | |
| 122 | + conn = self._ouvrir_conn() | |
| 123 | + try: | |
| 124 | + enregistrer_chemins( | |
| 125 | + conn, | |
| 126 | + temp_dir=Path(self._champ_temp_dir.text()) if self._champ_temp_dir.text() else None, | |
| 127 | + local_work_dir=Path(self._champ_local_work_dir.text()) | |
| 128 | + if self._champ_local_work_dir.text() | |
| 129 | + else None, | |
| 130 | + archive_smb_path=self._champ_archive_smb.text().strip() or None, | |
| 131 | + creer_repertoires_manquants=self._case_creer.isChecked(), | |
| 132 | + forcer_collision=self._case_forcer_collision.isChecked(), | |
| 133 | + ignorer_verification_archive=self._case_ignorer_verification.isChecked(), | |
| 134 | + ) | |
| 135 | + except RepertoireAbsentError as exc: | |
| 136 | + QMessageBox.warning( | |
| 137 | + self, "Répertoire manquant", f"{exc.chemin} n'existe pas — cocher « Créer... »." | |
| 138 | + ) | |
| 139 | + return | |
| 140 | + except CheminsAmbigusError as exc: | |
| 141 | + QMessageBox.warning(self, "Chemins ambigus", str(exc)) | |
| 142 | + return | |
| 143 | + except CheminArchiveInvalideError as exc: | |
| 144 | + QMessageBox.warning(self, "Archive invalide", str(exc)) | |
| 145 | + return | |
| 146 | + except TravailNonReconcilieError as exc: | |
| 147 | + dossiers = "\n".join(f" - {d}" for d in exc.dossiers) | |
| 148 | + QMessageBox.warning( | |
| 149 | + self, "Travail non réconcilié", f"Réconcilier ou archiver d'abord :\n{dossiers}" | |
| 150 | + ) | |
| 151 | + return | |
| 152 | + except ArchiveInaccessibleError as exc: | |
| 153 | + QMessageBox.warning(self, "Archive inaccessible", str(exc)) | |
| 154 | + return | |
| 155 | + finally: | |
| 156 | + conn.close() | |
| 157 | + | |
| 158 | + self._rafraichir() | |
| 159 | + | |
| 160 | + def _nommer_boitier_selectionne(self) -> None: | |
| 161 | + item = self._liste_boitiers.currentItem() | |
| 162 | + if item is None: | |
| 163 | + return | |
| 164 | + boitier_id = item.data(_ROLE_ID) | |
| 165 | + | |
| 166 | + nom, ok = QInputDialog.getText(self, "Nommer le boîtier", "Nom lisible :") | |
| 167 | + if not ok or not nom.strip(): | |
| 168 | + return | |
| 169 | + | |
| 170 | + conn = self._ouvrir_conn() | |
| 171 | + try: | |
| 172 | + nommer_boitier(conn, boitier_id, nom.strip()) | |
| 173 | + except BoitierInconnuError as exc: | |
| 174 | + QMessageBox.warning(self, "Boîtier inconnu", str(exc)) | |
| 175 | + return | |
| 176 | + finally: | |
| 177 | + conn.close() | |
| 178 | + | |
| 179 | + self._rafraichir() | |
| new file mode 100644 | |||
| @@ -0,0 +1,179 @@ | |||
| 1 | +"""Écran de configuration (User Story 4, specs/003) — appelle exclusivement | ||
| 2 | +`regine_core.config`, aucune logique de validation propre à la GUI (Principe VI). | ||
| 3 | + | ||
| 4 | +Chaque exception de `regine_core.config.context.enregistrer_chemins` est | ||
| 5 | +présentée clairement (boîte de dialogue), jamais comme une exception technique | ||
| 6 | +brute — mêmes règles que la CLI (`regine config set-paths`), jamais devinées ou | ||
| 7 | +recalculées ici. | ||
| 8 | +""" | ||
| 9 | + | ||
| 10 | +from __future__ import annotations | ||
| 11 | + | ||
| 12 | +from pathlib import Path | ||
| 13 | + | ||
| 14 | +from PySide6.QtCore import Qt | ||
| 15 | +from PySide6.QtWidgets import ( | ||
| 16 | + QCheckBox, | ||
| 17 | + QFormLayout, | ||
| 18 | + QHBoxLayout, | ||
| 19 | + QInputDialog, | ||
| 20 | + QLabel, | ||
| 21 | + QLineEdit, | ||
| 22 | + QListWidget, | ||
| 23 | + QListWidgetItem, | ||
| 24 | + QMessageBox, | ||
| 25 | + QPushButton, | ||
| 26 | + QVBoxLayout, | ||
| 27 | + QWidget, | ||
| 28 | +) | ||
| 29 | +from regine_core.camera_profile.db import Boitier | ||
| 30 | +from regine_core.config.cameras_screen import BoitierInconnuError, lister_boitiers, nommer_boitier | ||
| 31 | +from regine_core.config.context import ( | ||
| 32 | + ArchiveInaccessibleError, | ||
| 33 | + CheminArchiveInvalideError, | ||
| 34 | + CheminsAmbigusError, | ||
| 35 | + RepertoireAbsentError, | ||
| 36 | + TravailNonReconcilieError, | ||
| 37 | + charger_contexte, | ||
| 38 | + enregistrer_chemins, | ||
| 39 | +) | ||
| 40 | +from regine_core.config.db import open_context_db | ||
| 41 | + | ||
| 42 | +_ROLE_ID = Qt.ItemDataRole.UserRole | ||
| 43 | + | ||
| 44 | + | ||
| 45 | +class EcranConfiguration(QWidget): | ||
| 46 | + """Onglet de configuration : chemins, état SMB, boîtiers nommés — même | ||
| 47 | + contexte que la CLI (`regine config ...`), jamais un état parallèle (SC-008).""" | ||
| 48 | + | ||
| 49 | + def __init__(self, contexte_db: Path | None = None) -> None: | ||
| 50 | + super().__init__() | ||
| 51 | + self._contexte_db = contexte_db | ||
| 52 | + | ||
| 53 | + disposition = QVBoxLayout(self) | ||
| 54 | + | ||
| 55 | + formulaire = QFormLayout() | ||
| 56 | + self._champ_temp_dir = QLineEdit() | ||
| 57 | + formulaire.addRow("Répertoire temporaire :", self._champ_temp_dir) | ||
| 58 | + self._champ_local_work_dir = QLineEdit() | ||
| 59 | + formulaire.addRow("Répertoire de travail local :", self._champ_local_work_dir) | ||
| 60 | + self._champ_archive_smb = QLineEdit() | ||
| 61 | + formulaire.addRow("Archive (smb://...) :", self._champ_archive_smb) | ||
| 62 | + disposition.addLayout(formulaire) | ||
| 63 | + | ||
| 64 | + self._case_creer = QCheckBox("Créer les répertoires manquants") | ||
| 65 | + disposition.addWidget(self._case_creer) | ||
| 66 | + self._case_forcer_collision = QCheckBox("Forcer malgré une collision temp/local") | ||
| 67 | + disposition.addWidget(self._case_forcer_collision) | ||
| 68 | + self._case_ignorer_verification = QCheckBox( | ||
| 69 | + "Ignorer la vérification de travail non réconcilié (archive inaccessible)" | ||
| 70 | + ) | ||
| 71 | + disposition.addWidget(self._case_ignorer_verification) | ||
| 72 | + | ||
| 73 | + boutons = QHBoxLayout() | ||
| 74 | + bouton_rafraichir = QPushButton("Rafraîchir") | ||
| 75 | + bouton_rafraichir.clicked.connect(self._rafraichir) | ||
| 76 | + boutons.addWidget(bouton_rafraichir) | ||
| 77 | + bouton_enregistrer = QPushButton("Enregistrer") | ||
| 78 | + bouton_enregistrer.clicked.connect(self._enregistrer) | ||
| 79 | + boutons.addWidget(bouton_enregistrer) | ||
| 80 | + disposition.addLayout(boutons) | ||
| 81 | + | ||
| 82 | + self._label_etat_smb = QLabel("État SMB : (non chargé)") | ||
| 83 | + disposition.addWidget(self._label_etat_smb) | ||
| 84 | + | ||
| 85 | + disposition.addWidget(QLabel("Boîtiers connus :")) | ||
| 86 | + self._liste_boitiers = QListWidget() | ||
| 87 | + disposition.addWidget(self._liste_boitiers) | ||
| 88 | + bouton_nommer = QPushButton("Nommer la sélection…") | ||
| 89 | + bouton_nommer.clicked.connect(self._nommer_boitier_selectionne) | ||
| 90 | + disposition.addWidget(bouton_nommer) | ||
| 91 | + | ||
| 92 | + self._rafraichir() | ||
| 93 | + | ||
| 94 | + def _ouvrir_conn(self): | ||
| 95 | + return open_context_db(self._contexte_db) | ||
| 96 | + | ||
| 97 | + def _rafraichir(self) -> None: | ||
| 98 | + conn = self._ouvrir_conn() | ||
| 99 | + try: | ||
| 100 | + contexte = charger_contexte(conn) | ||
| 101 | + self._champ_temp_dir.setText(str(contexte.temp_dir) if contexte.temp_dir else "") | ||
| 102 | + self._champ_local_work_dir.setText( | ||
| 103 | + str(contexte.local_work_dir) if contexte.local_work_dir else "" | ||
| 104 | + ) | ||
| 105 | + self._champ_archive_smb.setText(contexte.archive_smb_path or "") | ||
| 106 | + self._label_etat_smb.setText(f"État SMB : {contexte.smb_last_known_state}") | ||
| 107 | + | ||
| 108 | + self._liste_boitiers.clear() | ||
| 109 | + for boitier in lister_boitiers(conn): | ||
| 110 | + self._ajouter_boitier(boitier) | ||
| 111 | + finally: | ||
| 112 | + conn.close() | ||
| 113 | + | ||
| 114 | + def _ajouter_boitier(self, boitier: Boitier) -> None: | ||
| 115 | + nom = boitier.nom_lisible or "(non nommé)" | ||
| 116 | + serie = f", {boitier.numero_serie}" if boitier.numero_serie else "" | ||
| 117 | + item = QListWidgetItem(f"[{boitier.id}] {nom} — {boitier.modele}{serie}") | ||
| 118 | + item.setData(_ROLE_ID, boitier.id) | ||
| 119 | + self._liste_boitiers.addItem(item) | ||
| 120 | + | ||
| 121 | + def _enregistrer(self) -> None: | ||
| 122 | + conn = self._ouvrir_conn() | ||
| 123 | + try: | ||
| 124 | + enregistrer_chemins( | ||
| 125 | + conn, | ||
| 126 | + temp_dir=Path(self._champ_temp_dir.text()) if self._champ_temp_dir.text() else None, | ||
| 127 | + local_work_dir=Path(self._champ_local_work_dir.text()) | ||
| 128 | + if self._champ_local_work_dir.text() | ||
| 129 | + else None, | ||
| 130 | + archive_smb_path=self._champ_archive_smb.text().strip() or None, | ||
| 131 | + creer_repertoires_manquants=self._case_creer.isChecked(), | ||
| 132 | + forcer_collision=self._case_forcer_collision.isChecked(), | ||
| 133 | + ignorer_verification_archive=self._case_ignorer_verification.isChecked(), | ||
| 134 | + ) | ||
| 135 | + except RepertoireAbsentError as exc: | ||
| 136 | + QMessageBox.warning( | ||
| 137 | + self, "Répertoire manquant", f"{exc.chemin} n'existe pas — cocher « Créer... »." | ||
| 138 | + ) | ||
| 139 | + return | ||
| 140 | + except CheminsAmbigusError as exc: | ||
| 141 | + QMessageBox.warning(self, "Chemins ambigus", str(exc)) | ||
| 142 | + return | ||
| 143 | + except CheminArchiveInvalideError as exc: | ||
| 144 | + QMessageBox.warning(self, "Archive invalide", str(exc)) | ||
| 145 | + return | ||
| 146 | + except TravailNonReconcilieError as exc: | ||
| 147 | + dossiers = "\n".join(f" - {d}" for d in exc.dossiers) | ||
| 148 | + QMessageBox.warning( | ||
| 149 | + self, "Travail non réconcilié", f"Réconcilier ou archiver d'abord :\n{dossiers}" | ||
| 150 | + ) | ||
| 151 | + return | ||
| 152 | + except ArchiveInaccessibleError as exc: | ||
| 153 | + QMessageBox.warning(self, "Archive inaccessible", str(exc)) | ||
| 154 | + return | ||
| 155 | + finally: | ||
| 156 | + conn.close() | ||
| 157 | + | ||
| 158 | + self._rafraichir() | ||
| 159 | + | ||
| 160 | + def _nommer_boitier_selectionne(self) -> None: | ||
| 161 | + item = self._liste_boitiers.currentItem() | ||
| 162 | + if item is None: | ||
| 163 | + return | ||
| 164 | + boitier_id = item.data(_ROLE_ID) | ||
| 165 | + | ||
| 166 | + nom, ok = QInputDialog.getText(self, "Nommer le boîtier", "Nom lisible :") | ||
| 167 | + if not ok or not nom.strip(): | ||
| 168 | + return | ||
| 169 | + | ||
| 170 | + conn = self._ouvrir_conn() | ||
| 171 | + try: | ||
| 172 | + nommer_boitier(conn, boitier_id, nom.strip()) | ||
| 173 | + except BoitierInconnuError as exc: | ||
| 174 | + QMessageBox.warning(self, "Boîtier inconnu", str(exc)) | ||
| 175 | + return | ||
| 176 | + finally: | ||
| 177 | + conn.close() | ||
| 178 | + | ||
| 179 | + self._rafraichir() | ||
modified
specs/003-config-contexte-travail/tasks.md +25 -3 | @@ -118,11 +118,32 @@ description: "Task list for feature implementation" | ||
| 118 | 118 | |
| 119 | 119 | --- |
| 120 | 120 | |
| 121 | -## Phase 6: Polish & Cross-Cutting Concerns | |
| 121 | +## Phase 6: User Story 4 - Configurer le contexte de travail depuis l'interface graphique (Priority: P4) | |
| 122 | + | |
| 123 | +**Ajoutée le 2026-09-21** (clarification + extension de plan post-implémentation) — cf. `spec.md` § Clarifications Session 2026-09-21. | |
| 124 | + | |
| 125 | +**Objectif** : consulter et modifier le contexte de travail (chemins, état SMB, boîtiers nommés) depuis un 3e onglet de `regine-gui` (déjà implémentée par `specs/006-gui-tri-consultation`), en appelant exclusivement les fonctions `regine_core.config` déjà existantes — aucune nouvelle fonction `regine-core` requise (cf. `plan.md` extension du 2026-09-21). | |
| 126 | + | |
| 127 | +**Test indépendant** : ouvrir l'onglet Configuration, y modifier un chemin, vérifier via `regine config show` (CLI) que le changement est bien reflété — sans dépendre des onglets Tri/Consultation. | |
| 128 | + | |
| 129 | +### Implementation for User Story 4 | |
| 130 | + | |
| 131 | +- [X] T029 [US4] Créer `packages/regine-gui/src/regine_gui/config_view.py` (squelette, docstring de module uniquement) | |
| 132 | +- [X] T030 [US4] Implémenter `EcranConfiguration` dans `config_view.py` : affichage des 3 chemins et de l'état SMB au chargement (`regine_core.config.context.charger_contexte`), formulaire de modification appelant `context.enregistrer_chemins` — chaque exception (`RepertoireAbsentError`, `CheminsAmbigusError`, `CheminArchiveInvalideError`, `TravailNonReconcilieError`, `ArchiveInaccessibleError`) présentée clairement à l'utilisateur plutôt qu'une exception technique brute (FR-013, US4 Acceptance Scenario 2/3) | |
| 133 | +- [X] T031 [US4] Étendre `config_view.py` avec la liste des boîtiers et leur nommage (`cameras_screen.lister_boitiers`/`nommer_boitier`, US4 Acceptance Scenario 4) | |
| 134 | +- [X] T032 [US4] Câbler l'onglet « Configuration » dans `packages/regine-gui/src/regine_gui/app.py` (`FenetrePrincipale`), aux côtés des onglets Tri/Consultation déjà existants | |
| 135 | +- [X] T033 [US4] Valider manuellement en mode headless (`QT_QPA_PLATFORM=offscreen`, même méthode que `specs/006`) : affichage/modification des chemins, gestion d'erreur, liste/nommage de boîtiers, parité avec `regine config show`/`cameras list` (Scénario 7 de `quickstart.md`, SC-008) — **découverte de méthode de test** : `QMessageBox.warning` bloque en attente d'un clic (comportement normal en usage réel), non testable tel quel en script headless ; validé en monkeypatchant `QMessageBox.warning` pour capturer l'appel sans dialogue modal réel — `RepertoireAbsentError` et `TravailNonReconcilieError` confirmées gérées sans crash, message clair, rien enregistré suite au refus | |
| 136 | + | |
| 137 | +**Checkpoint** : les 4 user stories sont fonctionnelles indépendamment. | |
| 138 | + | |
| 139 | +--- | |
| 140 | + | |
| 141 | +## Phase 7: Polish & Cross-Cutting Concerns | |
| 122 | 142 | |
| 123 | 143 | - [X] T026 [P] Exécuter manuellement les 6 scénarios de `specs/003-config-contexte-travail/quickstart.md` (adaptés à la surface CLI réellement livrée) et consigner le résultat — scénarios 1, 2, 3, 5, 6 exécutés via la vraie CLI (`uv run python -m regine_cli.config_cmd config ...`), tous conformes. Scénario 4 (montage/démontage SMB réel) non exécutable dans cet environnement de développement (pas de partage SMB physique disponible) — le mécanisme sous-jacent (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté (T019), et `config show` a correctement détecté `smb://nas.local/regine-archive` (inexistant dans cet environnement) comme `unavailable` lors du scénario 1, confirmant la détection réelle |
| 124 | 144 | - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli` |
| 125 | 145 | - [X] T028 Mettre à jour `contracts/cli-config.md` et créer `contracts/regine-core-api.md` — signatures réelles documentées, écarts de conception consignés (cf. § Écarts en tête de fichier) ; note explicite que `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) ne consomment pas encore ce contexte configuré (ils prennent `--archive-root`/`--local-root` en flags explicites) — intégration future hors périmètre de cette spec, non construite ici sans nouvelle demande explicite |
| 146 | +- [X] T034 [P] Exécuter le Scénario 7 de `quickstart.md` (déjà couvert par T033, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui` (nouveau fichier `config_view.py`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert (aucune régression, aucun nouveau test requis côté core puisqu'aucune nouvelle fonction n'a été ajoutée) | |
| 126 | 147 | |
| 127 | 148 | --- |
| 128 | 149 | |
| @@ -133,8 +154,9 @@ description: "Task list for feature implementation" | ||
| 133 | 154 | - **US1 (Phase 3)** : dépend de Foundational uniquement. MVP. |
| 134 | 155 | - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul). |
| 135 | 156 | - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2. |
| 136 | -- **Polish (Phase 6)** : dépend des user stories livrées (au minimum US1). | |
| 157 | +- **US4 (Phase 6)** : dépend de US1/US2/US3 (appelle leurs fonctions telles quelles) et de `specs/006-gui-tri-consultation` (déjà livrée, fournit `regine-gui`/`app.py`/PySide6) ; aucune dépendance nouvelle côté `regine-core`. | |
| 158 | +- **Polish (Phase 7)** : dépend des user stories livrées (au minimum US1 ; T034 dépend spécifiquement de US4). | |
| 137 | 159 | |
| 138 | 160 | ## Implementation Strategy |
| 139 | 161 | |
| 140 | -MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. | |
| 162 | +MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. US4 (ajoutée le 2026-09-21) est un incrément de confort pur, livrable à tout moment après `specs/006-gui-tri-consultation` — sans lui, la CLI (US1-3) reste pleinement fonctionnelle et suffisante. | |
| @@ -118,11 +118,32 @@ description: "Task list for feature implementation" | |||
| 118 | 118 | ||
| 119 | --- | 119 | --- |
| 120 | 120 | ||
| 121 | -## Phase 6: Polish & Cross-Cutting Concerns | 121 | +## Phase 6: User Story 4 - Configurer le contexte de travail depuis l'interface graphique (Priority: P4) |
| 122 | + | ||
| 123 | +**Ajoutée le 2026-09-21** (clarification + extension de plan post-implémentation) — cf. `spec.md` § Clarifications Session 2026-09-21. | ||
| 124 | + | ||
| 125 | +**Objectif** : consulter et modifier le contexte de travail (chemins, état SMB, boîtiers nommés) depuis un 3e onglet de `regine-gui` (déjà implémentée par `specs/006-gui-tri-consultation`), en appelant exclusivement les fonctions `regine_core.config` déjà existantes — aucune nouvelle fonction `regine-core` requise (cf. `plan.md` extension du 2026-09-21). | ||
| 126 | + | ||
| 127 | +**Test indépendant** : ouvrir l'onglet Configuration, y modifier un chemin, vérifier via `regine config show` (CLI) que le changement est bien reflété — sans dépendre des onglets Tri/Consultation. | ||
| 128 | + | ||
| 129 | +### Implementation for User Story 4 | ||
| 130 | + | ||
| 131 | +- [X] T029 [US4] Créer `packages/regine-gui/src/regine_gui/config_view.py` (squelette, docstring de module uniquement) | ||
| 132 | +- [X] T030 [US4] Implémenter `EcranConfiguration` dans `config_view.py` : affichage des 3 chemins et de l'état SMB au chargement (`regine_core.config.context.charger_contexte`), formulaire de modification appelant `context.enregistrer_chemins` — chaque exception (`RepertoireAbsentError`, `CheminsAmbigusError`, `CheminArchiveInvalideError`, `TravailNonReconcilieError`, `ArchiveInaccessibleError`) présentée clairement à l'utilisateur plutôt qu'une exception technique brute (FR-013, US4 Acceptance Scenario 2/3) | ||
| 133 | +- [X] T031 [US4] Étendre `config_view.py` avec la liste des boîtiers et leur nommage (`cameras_screen.lister_boitiers`/`nommer_boitier`, US4 Acceptance Scenario 4) | ||
| 134 | +- [X] T032 [US4] Câbler l'onglet « Configuration » dans `packages/regine-gui/src/regine_gui/app.py` (`FenetrePrincipale`), aux côtés des onglets Tri/Consultation déjà existants | ||
| 135 | +- [X] T033 [US4] Valider manuellement en mode headless (`QT_QPA_PLATFORM=offscreen`, même méthode que `specs/006`) : affichage/modification des chemins, gestion d'erreur, liste/nommage de boîtiers, parité avec `regine config show`/`cameras list` (Scénario 7 de `quickstart.md`, SC-008) — **découverte de méthode de test** : `QMessageBox.warning` bloque en attente d'un clic (comportement normal en usage réel), non testable tel quel en script headless ; validé en monkeypatchant `QMessageBox.warning` pour capturer l'appel sans dialogue modal réel — `RepertoireAbsentError` et `TravailNonReconcilieError` confirmées gérées sans crash, message clair, rien enregistré suite au refus | ||
| 136 | + | ||
| 137 | +**Checkpoint** : les 4 user stories sont fonctionnelles indépendamment. | ||
| 138 | + | ||
| 139 | +--- | ||
| 140 | + | ||
| 141 | +## Phase 7: Polish & Cross-Cutting Concerns | ||
| 122 | 142 | ||
| 123 | - [X] T026 [P] Exécuter manuellement les 6 scénarios de `specs/003-config-contexte-travail/quickstart.md` (adaptés à la surface CLI réellement livrée) et consigner le résultat — scénarios 1, 2, 3, 5, 6 exécutés via la vraie CLI (`uv run python -m regine_cli.config_cmd config ...`), tous conformes. Scénario 4 (montage/démontage SMB réel) non exécutable dans cet environnement de développement (pas de partage SMB physique disponible) — le mécanisme sous-jacent (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté (T019), et `config show` a correctement détecté `smb://nas.local/regine-archive` (inexistant dans cet environnement) comme `unavailable` lors du scénario 1, confirmant la détection réelle | 143 | - [X] T026 [P] Exécuter manuellement les 6 scénarios de `specs/003-config-contexte-travail/quickstart.md` (adaptés à la surface CLI réellement livrée) et consigner le résultat — scénarios 1, 2, 3, 5, 6 exécutés via la vraie CLI (`uv run python -m regine_cli.config_cmd config ...`), tous conformes. Scénario 4 (montage/démontage SMB réel) non exécutable dans cet environnement de développement (pas de partage SMB physique disponible) — le mécanisme sous-jacent (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté (T019), et `config show` a correctement détecté `smb://nas.local/regine-archive` (inexistant dans cet environnement) comme `unavailable` lors du scénario 1, confirmant la détection réelle |
| 124 | - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli` | 144 | - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli` |
| 125 | - [X] T028 Mettre à jour `contracts/cli-config.md` et créer `contracts/regine-core-api.md` — signatures réelles documentées, écarts de conception consignés (cf. § Écarts en tête de fichier) ; note explicite que `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) ne consomment pas encore ce contexte configuré (ils prennent `--archive-root`/`--local-root` en flags explicites) — intégration future hors périmètre de cette spec, non construite ici sans nouvelle demande explicite | 145 | - [X] T028 Mettre à jour `contracts/cli-config.md` et créer `contracts/regine-core-api.md` — signatures réelles documentées, écarts de conception consignés (cf. § Écarts en tête de fichier) ; note explicite que `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) ne consomment pas encore ce contexte configuré (ils prennent `--archive-root`/`--local-root` en flags explicites) — intégration future hors périmètre de cette spec, non construite ici sans nouvelle demande explicite |
| 146 | +- [X] T034 [P] Exécuter le Scénario 7 de `quickstart.md` (déjà couvert par T033, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui` (nouveau fichier `config_view.py`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert (aucune régression, aucun nouveau test requis côté core puisqu'aucune nouvelle fonction n'a été ajoutée) | ||
| 126 | 147 | ||
| 127 | --- | 148 | --- |
| 128 | 149 | ||
| @@ -133,8 +154,9 @@ description: "Task list for feature implementation" | |||
| 133 | - **US1 (Phase 3)** : dépend de Foundational uniquement. MVP. | 154 | - **US1 (Phase 3)** : dépend de Foundational uniquement. MVP. |
| 134 | - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul). | 155 | - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul). |
| 135 | - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2. | 156 | - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2. |
| 136 | -- **Polish (Phase 6)** : dépend des user stories livrées (au minimum US1). | 157 | +- **US4 (Phase 6)** : dépend de US1/US2/US3 (appelle leurs fonctions telles quelles) et de `specs/006-gui-tri-consultation` (déjà livrée, fournit `regine-gui`/`app.py`/PySide6) ; aucune dépendance nouvelle côté `regine-core`. |
| 158 | +- **Polish (Phase 7)** : dépend des user stories livrées (au minimum US1 ; T034 dépend spécifiquement de US4). | ||
| 137 | 159 | ||
| 138 | ## Implementation Strategy | 160 | ## Implementation Strategy |
| 139 | 161 | ||
| 140 | -MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. | 162 | +MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. US4 (ajoutée le 2026-09-21) est un incrément de confort pur, livrable à tout moment après `specs/006-gui-tri-consultation` — sans lui, la CLI (US1-3) reste pleinement fonctionnelle et suffisante. |