feat(umgebung): Server-Umgebung im Login-Screen waehlbar (Produktion/Test)

- Umgebungen (Name, API-URL, Keycloak-Issuer) kommen aus
  assets/hl_server_config.json statt hartcodiertem BackendConfig.prod;
  Testumgebung 192.168.1.9:3001 ist dort hinterlegt
- AppBloc laedt die Config, aktiviert die gespeicherte Wahl (Fallback
  Default) und registriert das Networking; Wechsel persistiert per
  SharedPreferences und registriert neu
- Bloc-Baum ist pro Umgebung gekeyt: beim Wechsel entstehen alle Blocs
  mit dem neuen API-Client neu
- Login-Screen: Zahnrad oben rechts oeffnet Einstellungen mit Umgebungs-
  Cards (Name + IP:Port); Hinweis auf aktive Umgebung unter dem Button
- Fahrzeugauswahl pro Umgebung getrennt (eigene DB = eigene Car-IDs)
- Tests fuer Config-Parsing, Auswahl-Fallback, Card-Wechsel, Re-Registrierung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-25 13:06:49 +02:00
parent c239cdfd3e
commit f832b69b9d
17 changed files with 717 additions and 72 deletions

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/bloc/app_bloc.dart';
import 'package:hl_lieferservice/bloc/app_states.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
import 'package:hl_lieferservice/feature/settings/presentation/environment_settings_page.dart';
/// Login-Page nach der Migration auf Keycloak OIDC (Phase B).
///
@ -28,7 +31,25 @@ class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
appBar: AppBar(
actions: [
// Während eines laufenden Logins gesperrt: Ein Umgebungswechsel
// würde den Token-Provider mitten im Browser-Flow austauschen.
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) => IconButton(
tooltip: 'Einstellungen',
icon: const Icon(Icons.settings),
onPressed: state is Authenticating
? null
: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const EnvironmentSettingsPage(),
),
),
),
),
],
),
body: Column(
children: [
if (sessionExpired)
@ -111,6 +132,7 @@ class LoginPage extends StatelessWidget {
),
),
),
const _ActiveEnvironmentHint(),
],
),
),
@ -120,3 +142,23 @@ class LoginPage extends StatelessWidget {
);
}
}
/// Zeigt, gegen welchen Server die Anmeldung läuft — damit niemand
/// versehentlich in der Testumgebung ausliefert.
class _ActiveEnvironmentHint extends StatelessWidget {
const _ActiveEnvironmentHint();
@override
Widget build(BuildContext context) {
final state = context.watch<AppBloc>().state;
if (state is! AppConfigLoaded) return const SizedBox.shrink();
final env = state.active;
return Text(
'Umgebung: ${env.name} (${env.hostAndPort})',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
}
}