feat: add frontend app, improve static file serving with lazy loading and root redirect

This commit is contained in:
2026-07-20 21:03:48 +03:00
parent 799e939037
commit d1e96d35aa
27 changed files with 2053 additions and 14 deletions
+124
View File
@@ -0,0 +1,124 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LogViewerScreen extends StatefulWidget {
final String runId;
final dynamic api;
final ValueChanged<bool>? onStatusChanged;
const LogViewerScreen({
super.key,
required this.runId,
required this.api,
this.onStatusChanged,
});
@override
State<LogViewerScreen> createState() => _LogViewerScreenState();
}
class _LogViewerScreenState extends State<LogViewerScreen> {
String _log = '';
bool _loading = true;
String? _error;
Timer? _pollTimer;
final ScrollController _scrollController = ScrollController();
bool _autoScroll = true;
Future<void> _loadLog() async {
try {
final log = await widget.api.getRunLog(widget.runId);
final changed = log != _log;
setState(() {
_log = log;
_loading = false;
});
if (changed && _autoScroll && mounted) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
} catch (e) {
setState(() {
_error = e.toString();
_loading = false;
});
}
}
@override
void initState() {
super.initState();
_loadLog();
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) => _loadLog());
}
@override
void dispose() {
_pollTimer?.cancel();
_scrollController.dispose();
super.dispose();
}
Future<void> _copyLog() async {
await Clipboard.setData(ClipboardData(text: _log));
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Log copied to clipboard')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Run Log'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadLog,
tooltip: 'Refresh',
),
IconButton(
icon: const Icon(Icons.copy),
onPressed: _log.isNotEmpty ? _copyLog : null,
tooltip: 'Copy',
),
IconButton(
icon: Icon(
_autoScroll ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_up,
),
onPressed: () => setState(() => _autoScroll = !_autoScroll),
tooltip: _autoScroll ? 'Auto-scroll ON' : 'Auto-scroll OFF',
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: _log.isEmpty
? const Center(
child: Text('Log is empty', style: TextStyle(color: Colors.grey)),
)
: SelectionArea(
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
child: Text(
_log,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
height: 1.5,
),
),
),
),
);
}
}