Files
AcWeb/CLAUDE.md
T

6.1 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.

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:

  1. ApiClient — transport only. Builds requests, applies the timeout, maps every failure onto a sealed ApiException subtype (ApiHttpException, ApiNetworkException, ApiTimeoutException, ApiParsingException), decodes json. Knows nothing about controllers.
  2. Services/*Service extends ApiServiceBase — one class per backend controller, exposing controllerRoute (e.g. agents) and typed methods that call client.getList / getObject with a dto fromJson.
  3. AcApi — the single entry point. Holds the shared instance (AcApi.instance), lazily creates services, and is reconfigurable via AcApi.configure(...) / AcApi.reset() (tests inject a MockClient through the httpClient parameter).

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/)

  • MainApp picks the layout by width: >= DesktopView.minimumWidth (900) → DesktopView (three columns side by side: Reports | LatestReports | Agents), otherwise MobileView (same three pages behind a NavigationBar). 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> mXFuture assigned in initState from AcApi.instance... → FutureBuilder with explicit loading / snapshot.hasError / empty-list branches. Reload = reassign the future inside setState. Navigation is plain Navigator.of(context).push(MaterialPageRoute(...)).
  • Icons per enum value live in extensions (AgentTypeIcons, ReportTypeIcons — type.icon) so every page renders a type identically. Human labels come from displayName on the enum itself.
  • MarkdownText is a hand-written markdown renderer (no flutter_markdown dependency) 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 State classes are PascalCase (ReloadAgents, ReloadEntries); private build helpers are camelCase with _.
  • Enums are prefixed E, dtos suffixed Dto.
  • Comments explain why (e.g. why enum order matters, why http is fine) in full sentences — keep that density.