Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import '../about/internal/application/TextType.dart';
import '../about/internal/application/DeploymentRecord.dart';
import '../utils/Colors.dart';
import 'text/Text.dart';
import '../utils/CommonUtils.dart';
/// Deployment metrics: success-rate ring plus stat tiles.
class MetricsPanel extends StatelessWidget {
final List<DeploymentRecord> history;
const MetricsPanel({super.key, required this.history});
@override
Widget build(BuildContext context) {
final jp = context.jp;
final total = history.length;
final success = history
.where((h) => h.status == DeployStatus.success)
.length;
final failed = history
.where((h) => h.status == DeployStatus.failure)
.length;
final reverted = history
.where((h) => h.status == DeployStatus.reverted)
.length;
final attempts = success + failed;
final rate = attempts == 0 ? 0.0 : success / attempts;
final last = history.isNotEmpty ? history.first : null;
return Container(
padding: const EdgeInsets.all(16),
decoration: PeachTheme.panel(context, glowColor: PeachTheme.seed),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
text(
'METRICS',
12,
TextType.Regular,
color: jp.textDim,
letterSpacing: 1.2,
weight: FontWeight.w700,
),
const SizedBox(height: 14),
Row(
children: [
_rateRing(jp, rate, attempts),
const SizedBox(width: 18),
Expanded(
child: Wrap(
spacing: 10,
runSpacing: 10,
children: [
_tile(jp, 'Total', '$total', jp.text),
_tile(jp, 'Success', '$success', PeachTheme.seed),
_tile(jp, 'Failed', '$failed', PeachTheme.danger),
_tile(jp, 'Reverted', '$reverted', PeachTheme.warn),
],
),
),
],
),
if (last != null) ...[
const SizedBox(height: 14),
Divider(height: 1, color: jp.border),
const SizedBox(height: 12),
Row(
children: [
Icon(Icons.schedule, size: 14, color: jp.textFaint),
const SizedBox(width: 6),
Expanded(
child: text(
'Last: ${last.serverName} · ${last.serviceLabel} · ${timeAgo(last.time)}',
12.5,
TextType.Regular,
color: jp.textDim,
overflow: TextOverflow.ellipsis,
weight: FontWeight.w400,
),
),
_statusDot(last.status),
],
),
],
],
),
);
}
Widget _rateRing(JpColors jp, double rate, int attempts) {
final pct = (rate * 100).round();
final color = rate >= 0.8
? PeachTheme.seed
: rate >= 0.5
? PeachTheme.warn
: PeachTheme.danger;
return SizedBox(
width: 84,
height: 84,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 84,
height: 84,
child: CircularProgressIndicator(
value: attempts == 0 ? 0 : rate,
strokeWidth: 8,
backgroundColor: jp.surface2,
valueColor: AlwaysStoppedAnimation(color),
strokeCap: StrokeCap.round,
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
text(
attempts == 0 ? '—' : '$pct%',
20,
TextType.Regular,
color: color,
weight: FontWeight.w800,
),
text(
'SUCCESS',
8,
TextType.Regular,
color: jp.textFaint,
letterSpacing: 1,
weight: FontWeight.w700,
),
],
),
],
),
);
}
Widget _tile(JpColors jp, String label, String value, Color color) {
return Container(
width: 92,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: jp.bg.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: jp.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
text(
value,
20,
TextType.Regular,
color: color,
weight: FontWeight.w800,
),
text(
label.toUpperCase(),
9,
TextType.Regular,
color: jp.textFaint,
letterSpacing: 0.8,
weight: FontWeight.w700,
),
],
),
);
}
Widget _statusDot(DeployStatus s) {
final color = switch (s) {
DeployStatus.success => PeachTheme.seed,
DeployStatus.failure => PeachTheme.danger,
DeployStatus.reverted => PeachTheme.warn,
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.16),
borderRadius: BorderRadius.circular(20),
),
child: text(
s.label,
10,
TextType.Regular,
color: color,
weight: FontWeight.w700,
),
);
}
}