From 0561109e3c5b228f52f309d5aa50be8c8b8887b0 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 31 Aug 2026 15:27:26 +0200 Subject: [PATCH] Added Agent Availability screen --- ac_web/lib/Api/Dtos/EAgentStatus.dart | 14 ++-- ac_web/lib/Api/Dtos/EAgentType.dart | 30 +++++--- ac_web/lib/Api/JsonReader.dart | 8 +- ac_web/lib/Layouts/MobileView.dart | 5 +- ac_web/lib/Pages/AgentAvailabilities.dart | 12 +++ .../lib/Widgets/AgentAvailabilityColumn.dart | 77 +++++++++++++++++++ ac_web/lib/Widgets/AgentStatusIndicator.dart | 39 ++++++++++ ac_web/test/Api/ApiLayerTest.dart | 16 ++-- 8 files changed, 171 insertions(+), 30 deletions(-) create mode 100644 ac_web/lib/Pages/AgentAvailabilities.dart create mode 100644 ac_web/lib/Widgets/AgentAvailabilityColumn.dart create mode 100644 ac_web/lib/Widgets/AgentStatusIndicator.dart diff --git a/ac_web/lib/Api/Dtos/EAgentStatus.dart b/ac_web/lib/Api/Dtos/EAgentStatus.dart index 3bec08a..4d915e4 100644 --- a/ac_web/lib/Api/Dtos/EAgentStatus.dart +++ b/ac_web/lib/Api/Dtos/EAgentStatus.dart @@ -2,22 +2,22 @@ /// /// See the note in `EAgentType.dart` about ordering and [unknown]. enum EAgentStatus { - offline, - idle, + available, working, + offline, error, unknown; - bool get isOnline => this == EAgentStatus.idle || this == EAgentStatus.working; + bool get isOnline => this == EAgentStatus.available || this == EAgentStatus.working; String get displayName { switch (this) { - case EAgentStatus.offline: - return 'Offline'; - case EAgentStatus.idle: - return 'Idle'; + case EAgentStatus.available: + return 'Available'; case EAgentStatus.working: return 'Working'; + case EAgentStatus.offline: + return 'Offline'; case EAgentStatus.error: return 'Error'; case EAgentStatus.unknown: diff --git a/ac_web/lib/Api/Dtos/EAgentType.dart b/ac_web/lib/Api/Dtos/EAgentType.dart index 6ca5ca7..57a8f64 100644 --- a/ac_web/lib/Api/Dtos/EAgentType.dart +++ b/ac_web/lib/Api/Dtos/EAgentType.dart @@ -5,23 +5,29 @@ /// so that it never shadows a real backend value and acts as fallback for /// values this client does not know yet. enum EAgentType { - developer, - tester, - reviewer, - manager, + acApi, + logAnalyzer, + coder, + researcher, + securityAnalyst, + journalist, unknown; /// Human readable label, e.g. for chips or table cells. String get displayName { switch (this) { - case EAgentType.developer: - return 'Developer'; - case EAgentType.tester: - return 'Tester'; - case EAgentType.reviewer: - return 'Reviewer'; - case EAgentType.manager: - return 'Manager'; + case EAgentType.acApi: + return 'AC Api'; + case EAgentType.logAnalyzer: + return 'Log Analyzer'; + case EAgentType.coder: + return 'Coder'; + case EAgentType.researcher: + return 'Researcher'; + case EAgentType.securityAnalyst: + return 'Security Analyst'; + case EAgentType.journalist: + return 'Journalist'; case EAgentType.unknown: return 'Unknown'; } diff --git a/ac_web/lib/Api/JsonReader.dart b/ac_web/lib/Api/JsonReader.dart index f0f048f..2c72558 100644 --- a/ac_web/lib/Api/JsonReader.dart +++ b/ac_web/lib/Api/JsonReader.dart @@ -123,8 +123,9 @@ class JsonReader { } final text = raw.toString().trim().toLowerCase(); + final normalized = _normalizeEnumName(text); for (final candidate in values) { - if (nameOf(candidate).toLowerCase() == text) { + if (_normalizeEnumName(nameOf(candidate).toLowerCase()) == normalized) { return candidate; } } @@ -135,4 +136,9 @@ class JsonReader { } return fallback; } + + /// Removes separators so that `AC_API` also matches `acApi`. + static String _normalizeEnumName(String value) { + return value.replaceAll(RegExp(r'[^a-z0-9]'), ''); + } } diff --git a/ac_web/lib/Layouts/MobileView.dart b/ac_web/lib/Layouts/MobileView.dart index f5c9b09..6267e3d 100644 --- a/ac_web/lib/Layouts/MobileView.dart +++ b/ac_web/lib/Layouts/MobileView.dart @@ -1,3 +1,4 @@ +import 'package:ac_web/Pages/AgentAvailabilities.dart'; import 'package:ac_web/Pages/LatestReports.dart'; import 'package:flutter/material.dart'; @@ -43,8 +44,8 @@ class MobileViewState extends State { // Eighteen LatestReports(), - // Notifications - Text("Notifications") + // Agents + AgentAvailabilities() ][currentPageIndex], ); } diff --git a/ac_web/lib/Pages/AgentAvailabilities.dart b/ac_web/lib/Pages/AgentAvailabilities.dart new file mode 100644 index 0000000..f5588d3 --- /dev/null +++ b/ac_web/lib/Pages/AgentAvailabilities.dart @@ -0,0 +1,12 @@ +import 'package:ac_web/Widgets/AgentAvailabilityColumn.dart'; +import 'package:flutter/material.dart'; + +/// Page that lists all agents together with their current availability. +class AgentAvailabilities extends StatelessWidget { + const AgentAvailabilities({super.key}); + + @override + Widget build(BuildContext context) { + return const AgentAvailabilityColumn(); + } +} diff --git a/ac_web/lib/Widgets/AgentAvailabilityColumn.dart b/ac_web/lib/Widgets/AgentAvailabilityColumn.dart new file mode 100644 index 0000000..c82864a --- /dev/null +++ b/ac_web/lib/Widgets/AgentAvailabilityColumn.dart @@ -0,0 +1,77 @@ +import 'package:ac_web/Api/AcApi.dart'; +import 'package:ac_web/Api/Dtos/AgentDto.dart'; +import 'package:ac_web/Widgets/AgentStatusIndicator.dart'; +import 'package:flutter/material.dart'; + +/// Fetches all agents from the AgentsController and lists them with a colored +/// status indicator in front of their name. +class AgentAvailabilityColumn extends StatefulWidget{ + const AgentAvailabilityColumn({super.key}); + + @override + State createState() => _AgentAvailabilityColumnState(); +} + +class _AgentAvailabilityColumnState extends State{ + + late Future> mAgentsFuture; + + @override + void initState() { + super.initState(); + mAgentsFuture = AcApi.instance.agents.getAgents(); + } + + void ReloadAgents(){ + setState(() { + mAgentsFuture = AcApi.instance.agents.getAgents(); + }); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: mAgentsFuture, + builder: (context, snapshot) { + + if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center(child: Text('Could not load agents: ${snapshot.error}')); + } + + // The list of agents fetched from the AgentsController. + final List agents = snapshot.data ?? []; + + return _buildList(agents); + }, + ); + } + + Widget _buildList(List agents) { + + if (agents.isEmpty) { + return const Center(child: Text("No agents available")); + } + + return RefreshIndicator( + onRefresh: () async => ReloadAgents(), + child: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: agents.length, + itemBuilder: (context, index) { + + final AgentDto agent = agents[index]; + + return ListTile( + leading: AgentStatusIndicator(status: agent.status), + title: Text(agent.name), + subtitle: Text("${agent.type.displayName} - ${agent.status.displayName}"), + ); + }, + ), + ); + } +} diff --git a/ac_web/lib/Widgets/AgentStatusIndicator.dart b/ac_web/lib/Widgets/AgentStatusIndicator.dart new file mode 100644 index 0000000..403b2a2 --- /dev/null +++ b/ac_web/lib/Widgets/AgentStatusIndicator.dart @@ -0,0 +1,39 @@ +import 'package:ac_web/Api/Dtos/EAgentStatus.dart'; +import 'package:flutter/material.dart'; + +/// Small colored circle that visualizes the current [EAgentStatus] of an agent. +/// +/// Green = available, blue = working, red = error, white = unavailable. +class AgentStatusIndicator extends StatelessWidget { + const AgentStatusIndicator({super.key, required this.status, this.size = 16}); + + final EAgentStatus status; + final double size; + + static Color colorOf(EAgentStatus status) { + switch (status) { + case EAgentStatus.available: + return Colors.green; + case EAgentStatus.working: + return Colors.blue; + case EAgentStatus.error: + return Colors.red; + case EAgentStatus.offline: + case EAgentStatus.unknown: + return Colors.white; + } + } + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: colorOf(status), + shape: BoxShape.circle, + border: Border.all(color: Colors.black26), + ), + ); + } +} diff --git a/ac_web/test/Api/ApiLayerTest.dart b/ac_web/test/Api/ApiLayerTest.dart index fb9ad64..9634f53 100644 --- a/ac_web/test/Api/ApiLayerTest.dart +++ b/ac_web/test/Api/ApiLayerTest.dart @@ -31,9 +31,9 @@ void main() { jsonEncode([ { 'name': 'Ada', - 'type': 'Developer', + 'type': 'CODER', 'lastSeen': '2026-08-28T12:00:00Z', - 'status': 'Working', + 'status': 'WORKING', 'tasks': [ {'name': 'Implement api layer', 'status': 'InProgress', 'progress': 42}, ], @@ -47,7 +47,7 @@ void main() { expect(requested.toString(), 'http://api.local:5000/api/agents'); expect(agents, hasLength(1)); expect(agents.single.name, 'Ada'); - expect(agents.single.type, EAgentType.developer); + expect(agents.single.type, EAgentType.coder); expect(agents.single.status, EAgentStatus.working); expect(agents.single.lastSeen.isUtc, isTrue); expect(agents.single.tasks.single.name, 'Implement api layer'); @@ -76,15 +76,15 @@ void main() { final agents = await api.agents.getAgents(); - expect(agents.first.type, EAgentType.tester); - expect(agents.first.status, EAgentStatus.working); + expect(agents.first.type, EAgentType.logAnalyzer); + expect(agents.first.status, EAgentStatus.offline); expect(agents.first.tasks, isEmpty); expect(agents.last.type, EAgentType.unknown); expect(agents.last.status, EAgentStatus.unknown); }); test('throws ApiParsingException on a malformed payload', () async { - final api = apiReturning(jsonEncode([{'Type': 'Developer'}])); + final api = apiReturning(jsonEncode([{'Type': 'CODER'}])); expect(() => api.agents.getAgents(), throwsA(isA())); }); @@ -147,10 +147,10 @@ void main() { test('agent survives toJson/fromJson', () { final original = AgentDto( name: 'Ada', - type: EAgentType.reviewer, + type: EAgentType.securityAnalyst, lastSeen: DateTime.utc(2026, 8, 28, 12), tasks: const [AgentTaskDto(name: 'Review', status: 'Open')], - status: EAgentStatus.idle, + status: EAgentStatus.available, ); final restored = AgentDto.fromJson(original.toJson());