Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ AI_API_KEY=
AI_BASE_URL=
HTTP_ADDR=:8080
# Local demo: keep events in processing long enough to see PENDING → PROCESSING in the app.
DEMO_PROCESS_DELAY_MS=4000
DEMO_PROCESS_DELAY_MS=8000

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fvm flutter run --dart-define=API_BASE=http://127.0.0.1:8080
# Android emulator: API_BASE=http://10.0.2.2:8080
```

In the app: sign in as `demo-user-1@walletops.local` / `ops-secret-1` → **Events → Run live demo → confirm**. Watch each webhook move **PENDING → PROCESSING → PROCESSED** (compose sets a short demo worker delay). Open an event for pipeline steps → **Explain**.
In the app: sign in as `demo-user-1@walletops.local` / `ops-secret-1` → **Events → Run live demo → confirm**. Watch each webhook move **PENDING → PROCESSING → PROCESSED** (compose holds processing ~8s so the queue is readable). Open an event for pipeline steps → **Explain**.

`POST /v1/demo/simulate` (auth) inserts fresh pending events for the journey; partner HMAC ingest remains `POST /v1/webhooks/events`.

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ services:
AI_PROVIDER: mock
AI_API_KEY: ""
HTTP_ADDR: ":8080"
DEMO_PROCESS_DELAY_MS: "4000"
DEMO_PROCESS_DELAY_MS: "8000"
ports:
- "8080:8080"
depends_on:
Expand Down
40 changes: 20 additions & 20 deletions mobile/lib/core/theme/app_theme.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';

import 'app_spacing.dart';

Expand Down Expand Up @@ -259,47 +260,45 @@ ThemeData _buildTheme(Brightness brightness) {
}

TextTheme _textTheme(ColorScheme scheme) {
final base = Typography.material2021(
platform: TargetPlatform.android,
).black.apply(
final base = GoogleFonts.ibmPlexSansTextTheme().apply(
bodyColor: scheme.onSurface,
displayColor: scheme.onSurface,
);

return base.copyWith(
headlineLarge: base.headlineLarge?.copyWith(
fontSize: 30,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.6,
height: 1.1,
height: 1.12,
),
headlineMedium: base.headlineMedium?.copyWith(
fontSize: 26,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
height: 1.15,
),
headlineSmall: base.headlineSmall?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
height: 1.2,
),
titleLarge: base.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
height: 1.25,
),
titleMedium: base.titleMedium?.copyWith(
fontSize: 15,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
height: 1.3,
),
titleSmall: base.titleSmall?.copyWith(
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.15,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.3,
),
bodyLarge: base.bodyLarge?.copyWith(
Expand All @@ -313,26 +312,27 @@ TextTheme _textTheme(ColorScheme scheme) {
height: 1.45,
color: scheme.onSurface,
),
bodySmall: base.bodySmall?.copyWith(
bodySmall: GoogleFonts.ibmPlexMono(
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.4,
color: scheme.onSurfaceVariant,
),
labelLarge: base.labelLarge?.copyWith(
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.35,
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
labelMedium: base.labelMedium?.copyWith(
labelMedium: GoogleFonts.ibmPlexMono(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 0.45,
fontWeight: FontWeight.w600,
letterSpacing: 0.4,
color: scheme.onSurface,
),
labelSmall: base.labelSmall?.copyWith(
labelSmall: GoogleFonts.ibmPlexMono(
fontSize: 10,
fontWeight: FontWeight.w600,
letterSpacing: 0.55,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
color: scheme.onSurfaceVariant,
),
);
Expand Down
6 changes: 4 additions & 2 deletions mobile/lib/features/auth/presentation/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: AppSpacing.md),
Text(
'Sign in to watch HMAC webhooks land, the worker claim '
'them, and rules match — all against a local API.',
'Sign in, then run the guided demo on Events to watch '
'webhooks move through the queue — HMAC ingest, worker '
'claim, rule match — against your local API.',
style: theme.textTheme.bodyMedium?.copyWith(
color: scheme.onSurfaceVariant,
height: 1.45,
),
),
const SizedBox(height: AppSpacing.lg),
Expand Down
60 changes: 47 additions & 13 deletions mobile/lib/features/events/presentation/cubit/events_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class EventsCubit extends Cubit<EventsState> {
final DemoRepository _demo;
Timer? _liveTimer;

static const _betweenEventsPause = Duration(seconds: 3);
static const _terminalWait = Duration(seconds: 40);

Future<void> load({String? status, bool updateFilter = false}) async {
final filter = updateFilter ? status : (status ?? state.filter);
final quiet = state.liveWatching || state.demoBusy;
Expand Down Expand Up @@ -65,7 +68,7 @@ class EventsCubit extends Cubit<EventsState> {
load(status: status, updateFilter: true);

void startLiveWatch() {
if (state.liveWatching) {
if (state.liveWatching && _liveTimer != null) {
return;
}
emit(state.copyWith(liveWatching: true));
Expand Down Expand Up @@ -94,7 +97,6 @@ class EventsCubit extends Cubit<EventsState> {
}
}

/// Injects events one-by-one so the user can watch PENDING → PROCESSING → PROCESSED.
Future<void> runGuidedDemo({int eventCount = 3}) async {
if (state.demoBusy) {
return;
Expand All @@ -103,6 +105,8 @@ class EventsCubit extends Cubit<EventsState> {
state.copyWith(
demoBusy: true,
liveWatching: true,
demoStep: 0,
demoTotal: eventCount,
journeyMessage: 'Preparing demo rule and live refresh…',
clearError: true,
),
Expand All @@ -113,8 +117,9 @@ class EventsCubit extends Cubit<EventsState> {
for (var i = 0; i < eventCount; i++) {
emit(
state.copyWith(
demoStep: i,
journeyMessage:
'Injecting webhook ${i + 1} of $eventCount as PENDING…',
'Step ${i + 1}/$eventCount — injecting webhook as PENDING…',
),
);
final result = await _demo.simulate(
Expand All @@ -127,24 +132,38 @@ class EventsCubit extends Cubit<EventsState> {
: '';
emit(
state.copyWith(
demoStep: i,
journeyMessage: created == null
? 'Webhook ${i + 1} sent.$ruleNote'
: 'Webhook ${i + 1}: ${created.type} is ${created.status.toUpperCase()}.$ruleNote '
'Watch status move to PROCESSING then PROCESSED.',
: 'Step ${i + 1}/$eventCount — ${created.type} is '
'${created.status.toUpperCase()}.$ruleNote '
'Watch PENDING → PROCESSING → PROCESSED.',
),
);
await load();
if (created != null) {
await _waitUntilTerminal(created.id);
await _waitUntilTerminal(created.id, step: i + 1, total: eventCount);
} else {
await Future<void>.delayed(const Duration(seconds: 5));
await Future<void>.delayed(const Duration(seconds: 8));
}
if (i < eventCount - 1) {
emit(
state.copyWith(
demoStep: i + 1,
journeyMessage:
'Step ${i + 1}/$eventCount done. Pausing before the next webhook…',
),
);
await Future<void>.delayed(_betweenEventsPause);
}
}
emit(
state.copyWith(
demoBusy: false,
demoStep: eventCount,
demoTotal: eventCount,
journeyMessage:
'Demo complete. Open a PROCESSED event for the pipeline steps, then Explain.',
'Demo complete. Open a PROCESSED event for pipeline detail, then Explain.',
),
);
await load();
Expand All @@ -153,6 +172,8 @@ class EventsCubit extends Cubit<EventsState> {
emit(
state.copyWith(
demoBusy: false,
demoStep: 0,
demoTotal: 0,
errorMessage: mapError(e).message,
clearJourney: true,
),
Expand All @@ -161,23 +182,36 @@ class EventsCubit extends Cubit<EventsState> {
}
}

Future<void> _waitUntilTerminal(String eventId) async {
final deadline = DateTime.now().add(const Duration(seconds: 20));
Future<void> _waitUntilTerminal(
String eventId, {
required int step,
required int total,
}) async {
final deadline = DateTime.now().add(_terminalWait);
while (DateTime.now().isBefore(deadline) && !isClosed) {
await Future<void>.delayed(const Duration(milliseconds: 800));
await Future<void>.delayed(const Duration(milliseconds: 900));
await load();
final match = state.items.where((e) => e.id == eventId);
if (match.isEmpty) {
continue;
}
final status = match.first.status;
final current = match.first;
final status = current.status;
if (status == 'processed' || status == 'failed') {
emit(
state.copyWith(
demoStep: step,
journeyMessage:
'Step $step/$total — ${current.type} finished as ${status.toUpperCase()}.',
),
);
return;
}
emit(
state.copyWith(
journeyMessage:
'Live: ${match.first.type} is ${status.toUpperCase()} — worker claim / rule match in progress…',
'Step $step/$total — ${current.type} is ${status.toUpperCase()} '
'(worker claim / rule match in progress)…',
),
);
}
Expand Down
17 changes: 17 additions & 0 deletions mobile/lib/features/events/presentation/cubit/events_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class EventsState extends Equatable {
this.liveWatching = false,
this.demoBusy = false,
this.journeyMessage,
this.demoStep = 0,
this.demoTotal = 0,
});

final EventsStatus status;
Expand All @@ -22,10 +24,19 @@ class EventsState extends Equatable {
final bool liveWatching;
final bool demoBusy;
final String? journeyMessage;
final int demoStep;
final int demoTotal;

bool get hasActiveQueue =>
items.any((e) => e.status == 'pending' || e.status == 'processing');

double get demoProgress {
if (demoTotal <= 0) {
return 0;
}
return (demoStep / demoTotal).clamp(0.0, 1.0);
}

EventsState copyWith({
EventsStatus? status,
List<OpsEvent>? items,
Expand All @@ -34,6 +45,8 @@ class EventsState extends Equatable {
bool? liveWatching,
bool? demoBusy,
String? journeyMessage,
int? demoStep,
int? demoTotal,
bool clearFilter = false,
bool clearError = false,
bool clearJourney = false,
Expand All @@ -47,6 +60,8 @@ class EventsState extends Equatable {
demoBusy: demoBusy ?? this.demoBusy,
journeyMessage:
clearJourney ? null : (journeyMessage ?? this.journeyMessage),
demoStep: demoStep ?? this.demoStep,
demoTotal: demoTotal ?? this.demoTotal,
);
}

Expand All @@ -59,5 +74,7 @@ class EventsState extends Equatable {
liveWatching,
demoBusy,
journeyMessage,
demoStep,
demoTotal,
];
}
Loading
Loading