fonzarely/regine-photos-archiverpublic⑂ Fork 0
⑂ 9c96cb0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/fonzarely/regine-photos-archiver.git
git clone ssh://git@rickub.com/fonzarely/regine-photos-archiver.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

GUI config

Fabien Champigny committed 2026-09-21T15:43:24+02:00 Browse files
9c96cb0 parent: b50cd7b
modified packages/regine-gui/src/regine_gui/app.py +6 -3
@@ -7,14 +7,16 @@ from pathlib import Path
77
88 from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget
99
10+from regine_gui.config_view import EcranConfiguration
1011 from regine_gui.consultation_view import EcranConsultation
1112 from regine_gui.tri_view import EcranTri
1213
1314
1415 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)."""
1820
1921 def __init__(self, contexte_db: Path | None = None) -> None:
2022 super().__init__()
@@ -25,6 +27,7 @@ class FenetrePrincipale(QMainWindow):
2527 self.setCentralWidget(onglets)
2628 onglets.addTab(EcranTri(), "Tri")
2729 onglets.addTab(EcranConsultation(contexte_db=contexte_db), "Consultation")
30+ onglets.addTab(EcranConfiguration(contexte_db=contexte_db), "Configuration")
2831
2932
3033 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, QTabWidget8 from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget
9 9
10+from regine_gui.config_view import EcranConfiguration
10 from regine_gui.consultation_view import EcranConsultation11 from regine_gui.consultation_view import EcranConsultation
11 from regine_gui.tri_view import EcranTri12 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 deux16+ """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"
118118
119119 ---
120120
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
122142
123143 - [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
124144 - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli`
125145 - [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)
126147
127148 ---
128149
@@ -133,8 +154,9 @@ description: "Task list for feature implementation"
133154 - **US1 (Phase 3)** : dépend de Foundational uniquement. MVP.
134155 - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul).
135156 - **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).
137159
138160 ## Implementation Strategy
139161
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 Concerns121+## 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éelle143 - [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 explicite145 - [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 Strategy160 ## 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.