7.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
Flutter frontend ("AC Control") for the AcAPI, a .NET Web API backend of the "Artificial Company". The UI shows AI agents, their tasks/availability, and the reports they produce.
The Flutter project lives in ac_web/, not at the repository root. All commands below must be run from ac_web/.
Commands
flutter pub get # install dependencies
flutter run -d chrome # run in the browser (primary target)
flutter run -d linux # run as a desktop app
flutter analyze # lint / static analysis (flutter_lints)
flutter test # run all tests
flutter test test/Api/ApiLayer_test.dart # single test file
flutter test test/Api/ApiLayer_test.dart --plain-name 'parses agents' # single test
flutter build web
Flutter 3.44 / Dart 3.12 (sdk: ^3.12.2).
Tests
Test files must end in _test.dart or flutter test silently ignores them. The project keeps its PascalCase file naming in front of that suffix: test/Api/ApiLayer_test.dart, test/Layouts/MainApp_test.dart.
Widget tests run against a mocked backend: build a MockClient that answers by route, hand it to AcApi.configure(..., httpClient: client) in setUp, and AcApi.reset() in tearDown. Do not use pumpAndSettle on pages that load data — the loading state is a CircularProgressIndicator, whose animation never settles; pump a couple of frames instead (see settleApiCalls in MainApp_test.dart). Layout tests set tester.view.physicalSize to cross the 900px desktop/mobile breakpoint. To test the automatic refresh, pump AutoRefreshState.refreshInterval and then a few frames (test/Widgets/AutoRefresh_test.dart).
flutter analyze reports ~40 pre-existing file_names / non_constant_identifier_names infos caused by the naming conventions below. They are expected — only care about new warnings and errors.
Backend connection
The backend host/port is configured once in lib/main.dart via AcApi.configure(const ApiConfig(host: 'localhost', port: 5118)). Plain http is intentional — the API is only reached inside a trusted network. There is no environment/config file; changing the backend address means editing main.dart.
Architecture
API layer (lib/Api/)
Three strictly separated levels — keep them that way:
ApiClient— transport only. Builds requests, applies the timeout, maps every failure onto a sealedApiExceptionsubtype (ApiHttpException,ApiNetworkException,ApiTimeoutException,ApiParsingException), decodes json. Knows nothing about controllers.Services/*Service extends ApiServiceBase— one class per backend controller, exposingcontrollerRoute(e.g.agents) and typed methods that callclient.getList/getObjectwith a dtofromJson.AcApi— the single entry point. Holds the shared instance (AcApi.instance), lazily creates services, and is reconfigurable viaAcApi.configure(...)/AcApi.reset()(tests inject aMockClientthrough thehttpClientparameter).
lib/Api/Api.dart is a barrel file; UI code should import 'package:ac_web/Api/Api.dart';.
Adding a backend controller: create the dtos under Api/Dtos, add a <Name>Service extends ApiServiceBase, expose it as a lazy getter on AcApi, and export both from Api.dart.
DTOs and .NET interop (lib/Api/Dtos/, lib/Api/JsonReader.dart)
The backend is still in development, so parsing is deliberately defensive. Never call json['key'] directly in a dto — always go through JsonReader, which resolves keys case-insensitively (PascalCase from .NET vs. camelCase), tolerates missing properties, and parses enums that may arrive as either a name or a numeric index.
Enums (EAgentType, EAgentStatus, EReportType) mirror .NET enums. The declaration order must match the .NET enum because values may be serialized as indices, and unknown is always the last value so it never shadows a real backend value and acts as the fallback for values this client does not know yet. EReportType.wireName carries the SCREAMING_SNAKE .NET spelling.
UI (lib/Layouts/, lib/Pages/, lib/Widgets/)
MainApppicks the layout by width:>= DesktopView.minimumWidth(900) →DesktopView(three columns side by side: Reports | LatestReports | Agents), otherwiseMobileView(same three pages behind aNavigationBar). Both layouts reuse the same page widgets, so pages must not assume they own the whole screen.- No state management package and no routing package. Data loading is consistently:
StatefulWidget→late Future<T> mXFutureassigned ininitStatefromAcApi.instance...→FutureBuilderwith explicit loading /snapshot.hasError/ empty-list branches. Reload = reassign the future insidesetState. Navigation is plainNavigator.of(context).push(MaterialPageRoute(...)). - Automatic refresh: every widget that shows api data polls the backend (it has no push channel) through the
AutoRefreshStatemixin (lib/Widgets/AutoRefreshState.dart): mix it into theStateand implementRefreshData, which fetches again and, only on success, assigns the result as an already completed future. The mixin owns the timer, guards against overlapping requests and swallows errors of a tick. The interval (30 s) lives once inAutoRefreshState.refreshInterval. Because a refresh must not flash the spinner or drop the list, theFutureBuilders branch onsnapshot.hasData/hasError, not onconnectionState. A refresh the user did not ask for stays silent — no snackbars, no disabled buttons (seeAgentDetails._reloadAgent). - Icons per enum value live in extensions (
AgentTypeIcons,ReportTypeIcons—type.icon) so every page renders a type identically. Human labels come fromdisplayNameon the enum itself. MarkdownTextis a hand-written markdown renderer (noflutter_markdowndependency) covering the subset the reports use: headings, lists, block quotes, fenced code, rules, and inline bold/italic/strikethrough/code/links. Extend it rather than adding a package.- Dependencies are intentionally minimal (
http,cupertino_icons). Prefer solving things with Flutter's own widgets before adding a package.
Conventions
These deviate from stock Dart style; match the surrounding code rather than the default lints:
- Files and directories are PascalCase (
lib/Widgets/AgentTaskCard.dart), one public class per file, named after the file. - Instance fields are prefixed with
m(mAgent,mReportsFuture,mIsReloading); private helpers keep the leading underscore (_buildList,_openType). - Public methods on
Stateclasses are PascalCase (ReloadAgents,ReloadEntries); private build helpers are camelCase with_. - Enums are prefixed
E, dtos suffixedDto. - Comments explain why (e.g. why enum order matters, why http is fine) in full sentences — keep that density.