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>
This commit is contained in:
parent
e6e9c0753d
commit
51beb332c4
6 changed files with 138 additions and 67 deletions
|
|
@ -16,16 +16,18 @@ void main() async {
|
|||
|
||||
/// Decide which repository to start with.
|
||||
///
|
||||
/// useHub=false → Mock, no probe (fast offline start)
|
||||
/// useHub=true, healthy → HubRepository
|
||||
/// useHub=true, NOT ok → MockRepository (graceful fallback;
|
||||
/// UI banner shows hub-down so the
|
||||
/// user can re-try from settings)
|
||||
/// useHub=false → MockRepository, explicit opt-in to
|
||||
/// demo data via the Hub-Reiter.
|
||||
/// useHub=true → HubRepository regardless of probe
|
||||
/// outcome. When unhealthy the
|
||||
/// repository's list() returns [] and
|
||||
/// the ModeBanner shows 'hub-down' —
|
||||
/// no silent mock fallback that would
|
||||
/// quietly demote the app to demo
|
||||
/// mode.
|
||||
Future<EvaluationRepository> _pickRepository(HubSettings settings) async {
|
||||
if (!settings.useHub) return const MockRepository();
|
||||
final hub = await HubRepository.connect(settings);
|
||||
if (hub.isHealthy) return hub;
|
||||
return const MockRepository();
|
||||
return HubRepository.connect(settings);
|
||||
}
|
||||
|
||||
class ReclaimApp extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/models.dart';
|
||||
import '../data/repository.dart';
|
||||
import '../theme/reclaim_tokens.dart';
|
||||
import '../widgets/demo_banner.dart';
|
||||
import '../widgets/mode_banner.dart';
|
||||
import '../widgets/heatmap_grid.dart';
|
||||
import '../widgets/reclaim_card.dart';
|
||||
import '../widgets/tier_badge.dart';
|
||||
|
|
@ -32,7 +32,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
final t = Theme.of(context).textTheme;
|
||||
return Column(
|
||||
children: [
|
||||
const DemoBanner(),
|
||||
ModeBanner(repository: widget.repository),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(ReclaimSpace.xl),
|
||||
|
|
@ -190,8 +190,8 @@ class _LegendCardState extends State<_LegendCard> {
|
|||
spacing: ReclaimSpace.lg,
|
||||
runSpacing: ReclaimSpace.xs,
|
||||
children: [
|
||||
_MiniLabel(symbol: '↔', text: 'Schaden'),
|
||||
_MiniLabel(symbol: '↕', text: 'Nutzen'),
|
||||
_MiniLabel(symbol: '→', text: 'Schaden'),
|
||||
_MiniLabel(symbol: '↑', text: 'Nutzen'),
|
||||
_MiniLabel(
|
||||
symbol: '●', text: 'Betroffenheit'),
|
||||
_MiniLabel(
|
||||
|
|
@ -219,7 +219,7 @@ class _LegendCardState extends State<_LegendCard> {
|
|||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
_LegendRow(
|
||||
symbol: '↔',
|
||||
symbol: '→',
|
||||
axis: 'Abszisse (X)',
|
||||
label: 'Schaden',
|
||||
description:
|
||||
|
|
@ -231,7 +231,7 @@ class _LegendCardState extends State<_LegendCard> {
|
|||
),
|
||||
SizedBox(height: ReclaimSpace.sm),
|
||||
_LegendRow(
|
||||
symbol: '↕',
|
||||
symbol: '↑',
|
||||
axis: 'Ordinate (Y)',
|
||||
label: 'Nutzen',
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/models.dart';
|
||||
import '../data/repository.dart';
|
||||
import '../theme/reclaim_tokens.dart';
|
||||
import '../widgets/demo_banner.dart';
|
||||
import '../widgets/mode_banner.dart';
|
||||
import '../widgets/evidence_sidebar.dart';
|
||||
import '../widgets/norm_text_card.dart';
|
||||
import '../widgets/reclaim_card.dart';
|
||||
|
|
@ -29,7 +29,7 @@ class NormDetailPage extends StatelessWidget {
|
|||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const DemoBanner(),
|
||||
ModeBanner(repository: repository),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(ReclaimSpace.xl),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/models.dart';
|
||||
import '../data/repository.dart';
|
||||
import '../theme/reclaim_tokens.dart';
|
||||
import '../widgets/demo_banner.dart';
|
||||
import '../widgets/mode_banner.dart';
|
||||
import '../widgets/reclaim_card.dart';
|
||||
import '../widgets/tier_badge.dart';
|
||||
import 'norm_detail_page.dart';
|
||||
|
|
@ -18,7 +18,7 @@ class NormsListPage extends StatelessWidget {
|
|||
final t = Theme.of(context).textTheme;
|
||||
return Column(
|
||||
children: [
|
||||
const DemoBanner(),
|
||||
ModeBanner(repository: repository),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(ReclaimSpace.xl),
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/reclaim_tokens.dart';
|
||||
|
||||
/// Permanent banner shown above all data screens in mock mode.
|
||||
/// Carries the political-defensibility caveat: this is demo data,
|
||||
/// not peer-reviewed, not Juristen-approved.
|
||||
class DemoBanner extends StatelessWidget {
|
||||
const DemoBanner({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context).textTheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ReclaimSpace.lg,
|
||||
vertical: ReclaimSpace.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ReclaimColors.tierT4.withValues(alpha: 0.12),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ReclaimColors.tierT4.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.science_outlined,
|
||||
size: 18, color: ReclaimColors.tierT4),
|
||||
const SizedBox(width: ReclaimSpace.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'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".',
|
||||
style: t.labelSmall?.copyWith(
|
||||
color: ReclaimColors.tierT4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
app/lib/widgets/mode_banner.dart
Normal file
118
app/lib/widgets/mode_banner.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
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 => '—';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue