import 'dart:async'; import 'package:flutter/material.dart'; import '../api/client.dart'; import '../api/models.dart'; import '../widgets/run_status_badge.dart'; import '../widgets/step_progress.dart'; import 'log_viewer.dart'; class RunScreen extends StatefulWidget { final Pipeline pipeline; final APIClient api; const RunScreen({super.key, required this.pipeline, required this.api}); @override State createState() => _RunScreenState(); } class _RunScreenState extends State { String? _runId; RunState? _runState; String? _error; bool _starting = true; Timer? _pollTimer; Future _start() async { try { final data = await widget.api.startRun(widget.pipeline.id); setState(() { _runId = data['run_id'] as String; _runState = RunState( runId: data['run_id'] as String, pipelineId: data['pipeline_id'] as String? ?? '', status: parseRunStatus(data['status'] as String? ?? 'running'), currentStep: -1, steps: [], ); _starting = false; }); _startPolling(); } catch (e) { setState(() { _error = e.toString(); _starting = false; }); } } void _startPolling() { _pollTimer = Timer.periodic(const Duration(seconds: 2), (_) => _poll()); } Future _poll() async { if (_runId == null) return; try { final state = await widget.api.getRunStatus(_runId!); setState(() { _runState = state; }); if (state.isFinished) { _pollTimer?.cancel(); } } catch (e) { // Silently ignore polling errors } } @override void dispose() { _pollTimer?.cancel(); super.dispose(); } @override void initState() { super.initState(); _start(); } void _viewLog() { if (_runId == null) return; Navigator.push( context, MaterialPageRoute( builder: (_) => LogViewerScreen( runId: _runId!, api: widget.api, onStatusChanged: (finished) { if (finished) _pollTimer?.cancel(); }, ), ), ); } @override Widget build(BuildContext context) { if (_starting) { return Scaffold( appBar: AppBar(title: Text('Run: ${widget.pipeline.name}')), body: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Starting run...'), ], ), ), ); } if (_error != null) { return Scaffold( appBar: AppBar(title: Text('Run: ${widget.pipeline.name}')), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.red), const SizedBox(height: 16), Text(_error!, textAlign: TextAlign.center), const SizedBox(height: 16), FilledButton( onPressed: () => Navigator.pop(context), child: const Text('Go Back'), ), ], ), ), ); } if (_runId == null || _runState == null) { return Scaffold( appBar: AppBar(title: Text('Run: ${widget.pipeline.name}')), body: const Center(child: Text('Failed to start run')), ); } final state = _runState!; return Scaffold( appBar: AppBar( title: Text('Run: ${widget.pipeline.name}'), actions: [RunStatusBadge(status: state.status)], ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Run ID', style: TextStyle(color: Colors.grey, fontSize: 12), ), Text( _runId!, style: const TextStyle(fontFamily: 'monospace'), ), const SizedBox(height: 8), Row( children: [ const Text( 'Status: ', style: TextStyle(fontWeight: FontWeight.bold), ), RunStatusBadge(status: state.status), const SizedBox(width: 12), if (state.currentStep >= 0) Text( 'Step ${state.currentStep + 1}/${state.steps.length}', ), ], ), ], ), ), ), const SizedBox(height: 20), const Text( 'Steps', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), StepProgress(steps: state.steps, totalSteps: state.steps.length), const SizedBox(height: 24), Row( children: [ Expanded( child: SizedBox( height: 48, child: FilledButton.icon( onPressed: _viewLog, icon: const Icon(Icons.article_outlined), label: const Text('View Log'), ), ), ), if (state.isFinished) ...[ const SizedBox(width: 12), Expanded( child: SizedBox( height: 48, child: FilledButton.icon( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.check), label: const Text('Done'), ), ), ), ], ], ), if (!state.isFinished) Padding( padding: const EdgeInsets.only(top: 16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 12), Text( state.status == RunStatus.running ? 'Running... (polling every 2s)' : 'Waiting...', ), ], ), ), ], ), ), ); } }