reclaim/app/lib/widgets/mode_banner.dart
flemming-it 51beb332c4 feat(ui): ModeBanner replaces DemoBanner + no silent mock-fallback
Two related fixes so the app never silently presents itself as
a demo when it is in fact running against a real hub:

  - The orange 'Demo-Daten · KEINE Rechtsberatung' banner was
    hard-coded into every data screen, regardless of whether
    the active repository was the in-memory MockRepository or
    the live HubRepository. Replaced with ModeBanner, which
    picks one of three voices by repository.mode:

      demo      orange T4 — 'Demo-Daten · KEINE Rechtsberatung'
      hub-live  petrol     — 'Live aus Hub · <endpoint>'
      hub-down  warm-warn  — 'Hub nicht erreichbar — leere Liste'

  - main.dart's _pickRepository used to fall back to
    MockRepository whenever HubRepository.connect() came back
    unhealthy. That silently downgraded a live deployment to
    demo data, which is exactly what the user is trying to avoid.
    Removed: when useHub=true, the HubRepository is returned
    regardless. Its list() returns [] on hub-down, and the
    'hub-down' banner explains why the screens are empty.
    Mock data only shows when the user explicitly turns useHub
    off in the Hub-Reiter.

Also: legend arrow icons now point in the actual axis direction
(→ for Schaden, ↑ for Nutzen) instead of the bidirectional ↔/↕,
which suggested the axes ran both ways. Mini-chips and the
expanded legend rows are consistent.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-18 15:57:50 +02:00

118 lines
3.7 KiB
Dart

import 'package:flutter/material.dart';
import '../data/models.dart';
import '../data/repository.dart';
import '../theme/reclaim_tokens.dart';
/// Status banner above the data screens.
///
/// Picks one of three voices by the active repository's mode:
///
/// demo → orange T4: 'Demo-Daten · keine Rechtsberatung'
/// (MockRepository on, Beirat + Juristen-Pipeline
/// still missing)
///
/// hub-live → petrol-signal: 'Live aus Hub' + hub label
/// (HubRepository connected, list() reads real
/// flow.completed events)
///
/// hub-down → warm-warning: 'Hub nicht erreichbar — leere
/// Liste, prüfe Hub-Tab'
/// (HubRepository.connect timed out; fallback to
/// empty list rather than mocked data)
class ModeBanner extends StatelessWidget {
const ModeBanner({super.key, required this.repository});
final EvaluationRepository repository;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
final (color, icon, message) = switch (repository.mode) {
'hub-live' => (
ReclaimColors.signal,
Icons.bolt_outlined,
'Live aus Hub · ${repository.hubLabel}. '
'Werte stammen aus den flow.completed-Events des '
'Ch∆In-Audit-Logs. Volljuristen-Approval-Kette ist '
'in Phase 0 noch Selbst-Review — siehe „Recht".',
),
'hub-down' => (
ReclaimColors.harmWarm,
Icons.signal_wifi_off,
'Hub nicht erreichbar — leere Liste. Konfiguration prüfen '
'unter „Hub". Bis dahin: kein Recl∆Im-Datenpunkt '
'sichtbar.',
),
_ => (
ReclaimColors.tierT4,
Icons.science_outlined,
'Demo-Daten · KEINE Rechtsberatung. Alle Zahlen sind als '
'T4 markiert (qualitatives Signal), bis NKR-/Verbands-'
'Pipeline + Juristen-Approval laufen. Erläuterung der '
'Stufen unter „Methodik", Disclaimer unter „Recht".',
),
};
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: ReclaimSpace.lg,
vertical: ReclaimSpace.sm,
),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
border: Border(
bottom: BorderSide(color: color.withValues(alpha: 0.55)),
),
),
child: Row(
children: [
Icon(icon, size: 18, color: color),
const SizedBox(width: ReclaimSpace.sm),
Expanded(
child: Text(
message,
style: t.labelSmall?.copyWith(color: color),
),
),
],
),
);
}
}
/// Backwards-compat shim for pages that still import DemoBanner.
/// Resolves to the dynamic ModeBanner if a repository is passed,
/// otherwise stays demo-flavoured.
class DemoBanner extends StatelessWidget {
const DemoBanner({super.key, this.repository});
final EvaluationRepository? repository;
@override
Widget build(BuildContext context) {
final repo = repository;
if (repo != null) return ModeBanner(repository: repo);
return const ModeBanner(repository: _DemoMockRepository());
}
}
/// Internal sentinel so the bare-form DemoBanner() still renders
/// the orange demo voice even when no repository is plumbed in.
class _DemoMockRepository implements EvaluationRepository {
const _DemoMockRepository();
@override
Future<List<Evaluation>> list() async => const [];
@override
Future<Evaluation?> byEli(String eli) async => null;
@override
String get mode => 'demo';
@override
String get hubLabel => '';
}