lot4
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Méthode :** pour chaque lot, l’agent décrit ce qu’il compte faire → **tu valides ou ajustes** → on exécute → on coche.
|
> **Méthode :** pour chaque lot, l’agent décrit ce qu’il compte faire → **tu valides ou ajustes** → on exécute → on coche.
|
||||||
|
|
||||||
**Dernière mise à jour :** lot 3 livré ([fiche-soutenance-orale.md](fiche-soutenance-orale.md)).
|
**Dernière mise à jour :** lot 4 livré (commentaires `lib/` + DEBUG conservé).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
| `.env` | **Uniquement** `swifty_companion/.env` en local ; **jamais** dans Git ; `.env.example` versionné (sujet : échec direct si secrets publics) |
|
| `.env` | **Uniquement** `swifty_companion/.env` en local ; **jamais** dans Git ; `.env.example` versionné (sujet : échec direct si secrets publics) |
|
||||||
| Fiche orale (lot 3) | **Déroulé de soutenance** détaillé (sujet, points critiques, réalisation, commandes expliquées) — un fichier structuré logiquement |
|
| Fiche orale (lot 3) | **Déroulé de soutenance** détaillé (sujet, points critiques, réalisation, commandes expliquées) — un fichier structuré logiquement |
|
||||||
| Commentaires code | **Français** — sujet ne l’interdit pas |
|
| Commentaires code | **Français** — sujet ne l’interdit pas |
|
||||||
| Bouton DEBUG | Voir [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md) — pas lié aux secrets ; visible seulement en build debug ; choix garder/retirer avant rendu final |
|
| Bouton DEBUG | **Garder** — aide à démontrer le bonus refresh en `flutter run` ; absent en release (`kDebugMode`) — [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
| B | 1 Dépôt + branche | ✅ [fiche-depot-soutenance.md](fiche-depot-soutenance.md) |
|
| B | 1 Dépôt + branche | ✅ [fiche-depot-soutenance.md](fiche-depot-soutenance.md) |
|
||||||
| C | 2 README rendu autonome | ✅ fait |
|
| C | 2 README rendu autonome | ✅ fait |
|
||||||
| D | 3 Fiche soutenance orale | ✅ [fiche-soutenance-orale.md](fiche-soutenance-orale.md) |
|
| D | 3 Fiche soutenance orale | ✅ [fiche-soutenance-orale.md](fiche-soutenance-orale.md) |
|
||||||
| E | 4 Commentaires `lib/` | ☐ à faire |
|
| E | 4 Commentaires `lib/` | ✅ fait |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,19 +47,18 @@
|
|||||||
- `.env.example` commenté (racine + `swifty_companion/.env.example`).
|
- `.env.example` commenté (racine + `swifty_companion/.env.example`).
|
||||||
- `swifty_companion/README.md` : pointeur court vers le README racine.
|
- `swifty_companion/README.md` : pointeur court vers le README racine.
|
||||||
|
|
||||||
**Prochaine étape :** dis **« OK lot 4 »** pour les commentaires FR dans `lib/` (+ décision bouton DEBUG).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lot 3 — Livré
|
## Lot 3 — Livré
|
||||||
|
|
||||||
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — déroulé oral, commandes commentées, démo, FAQ.
|
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — déroulé oral, commandes commentées, démo, FAQ.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lot 4 — Commentaires code
|
## Lot 4 — Livré
|
||||||
|
|
||||||
`lib/` en français, sans lien vers doc externe ; décision DEBUG bouton documentée en oral.
|
- Commentaires / dartdoc en **français** dans `swiftyCompanion/swifty_companion/lib/`.
|
||||||
|
- Bouton **DEBUG** conservé (`kDebugMode`) pour la démo du bonus refresh — pas présent en APK release.
|
||||||
|
|
||||||
|
**Plan préparation :** tous les lots 0–4 sont cochés ; prochaine action opérationnelle = branche `soutenance` + push Vogosphere ([fiche-depot-soutenance.md](fiche-depot-soutenance.md)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,4 +70,4 @@
|
|||||||
| 1 | ✅ |
|
| 1 | ✅ |
|
||||||
| 2 | ✅ |
|
| 2 | ✅ |
|
||||||
| 3 | ✅ |
|
| 3 | ✅ |
|
||||||
| 4 | ☐ |
|
| 4 | ✅ |
|
||||||
|
|||||||
@@ -204,9 +204,9 @@ Dans `search_screen.dart`, un bouton **« DEBUG : simuler token expiré »** n
|
|||||||
|----------|---------|
|
|----------|---------|
|
||||||
| Le sujet l’interdit ? | **Non** — rien dans le PDF sur les boutons de debug. |
|
| Le sujet l’interdit ? | **Non** — rien dans le PDF sur les boutons de debug. |
|
||||||
| Risque en soutenance ? | L’évaluateur peut le voir en `flutter run` ; tu peux l’expliquer pour le **bonus refresh**, ou le retirer avant rendu final si tu veux une UI plus « propre ». |
|
| Risque en soutenance ? | L’évaluateur peut le voir en `flutter run` ; tu peux l’expliquer pour le **bonus refresh**, ou le retirer avant rendu final si tu veux une UI plus « propre ». |
|
||||||
| Décision à prendre (lot 4 ou avant push) | **Garder** (démo bonus) **ou** **retirer** le bouton — les deux sont défendables. |
|
| Décision (lot 4) | **Garder** — le bonus sujet = refresh token ; le bouton ne fait qu’**invalider l’access** en debug pour tester le refresh sans attendre l’expiration. Absent en `flutter build apk --release`. |
|
||||||
|
|
||||||
→ Détails oraux : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) (lot 3, à rédiger).
|
→ Démo orale : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ Si refresh échoue → IntraUnauthorizedException, l’utilisateur se reconnecte
|
|||||||
|
|
||||||
**Si évaluateur en release :** montrer `flutter test` sur `intra_oauth_tokens_test.dart` + expliquer le flux §7.
|
**Si évaluateur en release :** montrer `flutter test` sur `intra_oauth_tokens_test.dart` + expliquer le flux §7.
|
||||||
|
|
||||||
**Bouton DEBUG au rendu final :** voir [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md) — garder (visible seulement en debug) ou retirer avant push ; décision à trancher au lot 4.
|
**Bouton DEBUG au rendu :** **conservé** — visible uniquement en build debug (`kDebugMode`), utile pour prouver le bonus refresh ; absent en release. Voir [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Point d'entrée : charge les secrets depuis `.env` (asset, non versionné) puis vue recherche.
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import '../services/intra_api_exception.dart';
|
|||||||
import '../services/intra_profile_service.dart';
|
import '../services/intra_profile_service.dart';
|
||||||
import '../theme/swifty_ui.dart';
|
import '../theme/swifty_ui.dart';
|
||||||
|
|
||||||
/// Vue 2 — profil complet (étape 4).
|
/// Vue 2 — photo, ≥ 4 champs, skills (niveau + %), projets validés et failed.
|
||||||
class ProfileScreen extends StatefulWidget {
|
class ProfileScreen extends StatefulWidget {
|
||||||
const ProfileScreen({super.key, required this.user});
|
const ProfileScreen({super.key, required this.user});
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
loading: _searching,
|
loading: _searching,
|
||||||
onPressed: _search,
|
onPressed: _search,
|
||||||
),
|
),
|
||||||
|
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|||||||
@@ -139,8 +139,7 @@ class IntraAuthService {
|
|||||||
_refreshToken = refresh;
|
_refreshToken = refresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simule un access token expiré (garde le refresh) — tests bonus en debug.
|
/// Simule un access token expiré (garde le refresh) — démo bonus, appelé depuis l’UI en `kDebugMode`.
|
||||||
@visibleForTesting
|
|
||||||
void debugInvalidateAccessToken() {
|
void debugInvalidateAccessToken() {
|
||||||
if (!canRefresh) return;
|
if (!canRefresh) return;
|
||||||
_accessToken = 'invalid_access_token_for_refresh_test';
|
_accessToken = 'invalid_access_token_for_refresh_test';
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Duration _retryDelay(http.Response response, int attempt) {
|
|||||||
return Duration(milliseconds: ms.clamp(800, 8000));
|
return Duration(milliseconds: ms.clamp(800, 8000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lance une [IntraApiException] si le code HTTP n'est pas 200.
|
||||||
void ensureIntraOk(http.Response response) {
|
void ensureIntraOk(http.Response response) {
|
||||||
if (response.statusCode == 401) {
|
if (response.statusCode == 401) {
|
||||||
throw const IntraUnauthorizedException();
|
throw const IntraUnauthorizedException();
|
||||||
@@ -67,6 +68,7 @@ void ensureIntraOk(http.Response response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Décode le corps JSON après contrôle du statut HTTP.
|
||||||
dynamic decodeIntraBody(http.Response response) {
|
dynamic decodeIntraBody(http.Response response) {
|
||||||
ensureIntraOk(response);
|
ensureIntraOk(response);
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ class _ProfileCacheEntry {
|
|||||||
final DateTime fetchedAt;
|
final DateTime fetchedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Agrège fiche user, skills (cursus, pas `/experiences`) et `projects_users` (scope `projects`).
|
||||||
class IntraProfileService {
|
class IntraProfileService {
|
||||||
IntraProfileService._();
|
IntraProfileService._();
|
||||||
static final IntraProfileService instance = IntraProfileService._();
|
static final IntraProfileService instance = IntraProfileService._();
|
||||||
|
|
||||||
|
/// Cache court + délai entre GET pour limiter les 429 sur l'API 42.
|
||||||
static const Duration _cacheTtl = Duration(minutes: 2);
|
static const Duration _cacheTtl = Duration(minutes: 2);
|
||||||
static const Duration _betweenRequests = Duration(milliseconds: 450);
|
static const Duration _betweenRequests = Duration(milliseconds: 450);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'intra_api_exception.dart';
|
|||||||
import 'intra_auth_service.dart';
|
import 'intra_auth_service.dart';
|
||||||
import 'intra_http.dart';
|
import 'intra_http.dart';
|
||||||
|
|
||||||
|
/// Recherche d'un login via `GET /v2/users?filter[login]=` (même token OAuth que le profil).
|
||||||
class IntraUserService {
|
class IntraUserService {
|
||||||
IntraUserService._();
|
IntraUserService._();
|
||||||
static final IntraUserService instance = IntraUserService._();
|
static final IntraUserService instance = IntraUserService._();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Constantes UI partagées (étape 5 — layout).
|
/// Constantes UI partagées (layout responsive, thème 42).
|
||||||
abstract final class SwiftyUi {
|
abstract final class SwiftyUi {
|
||||||
static const Color brand = Color(0xFF00BABC);
|
static const Color brand = Color(0xFF00BABC);
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ class ResponsiveScrollBody extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Message d'erreur centré (recherche ou profil).
|
||||||
class ErrorMessage extends StatelessWidget {
|
class ErrorMessage extends StatelessWidget {
|
||||||
const ErrorMessage({super.key, required this.message});
|
const ErrorMessage({super.key, required this.message});
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ class ErrorMessage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bouton principal avec indicateur de chargement.
|
||||||
class LoadingFilledButton extends StatelessWidget {
|
class LoadingFilledButton extends StatelessWidget {
|
||||||
const LoadingFilledButton({
|
const LoadingFilledButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
Reference in New Issue
Block a user