Files
AcWeb/CLAUDE.md
T
florian 2751bd8b4c
Build Android Apk / android (release) Failing after 11m8s
Build Linux Application / linux (release) Failing after 4m49s
Publish Docker Image / docker (release) Successful in 13m55s
Fixed issues with Build files for Android and Linux
2026-09-01 15:24:41 +02:00

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

CI (Gitea Actions)

.gitea/workflows/ holds one workflow per delivery target. All of them are triggered by release: types: [published], so they run when a release is published through "Releases -> New Release" and never on an ordinary push or on a tag pushed with plain git:

Workflow Builds Result
BuildLinux.yml flutter build linux --release ac_web-linux-x64-<tag>.tar.gz (the whole bundle directory) as an artifact
BuildAndroid.yml flutter build apk --release ac_web-<tag>.apk as an artifact
PublishDocker.yml ac_web/Dockerfile image pushed to the Gitea registry as <tag>, latest and the short sha

Notes that are easy to get wrong:

  • The act-runner of the gitea instance only supports actions up to node20. Anything newer fails with The runs.using key in action.yml must be one of: [...], got node24, which is why the Flutter sdk is unpacked from the official archive instead of through subosito/flutter-action (that one caches through actions/cache@v5). Check the runs.using of an action before adding it.
  • The Flutter version is pinned in three places (FLUTTER_VERSION in both build workflows, ARG FLUTTER_VERSION in ac_web/Dockerfile) and must be kept in sync.
  • Steps run with working-directory: ac_web, but upload-artifact paths are relative to the repository root.
  • The workflows are read from the tag the release points at, so they have to be committed before the release is created.
  • Artifacts and the built files carry the tag name (github.ref_name); the docker image is pushed as the tag, as latest and as the short sha.
  • The apk is signed with a debug key, because android/app/build.gradle.kts still has no release signing config.
  • The docker image is self contained: it compiles the web bundle itself, so docker build -f ac_web/Dockerfile ac_web gives the same result locally. It serves the static bundle with nginx (ac_web/nginx.conf), which falls back to index.html so deep links survive a reload. The backend address is compiled into the bundle, so the host in main.dart must be reachable from the browser of every visitor, not only from the container.

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(...)).
  • Automatic refresh: every widget that shows api data polls the backend (it has no push channel) through the AutoRefreshState mixin (lib/Widgets/AutoRefreshState.dart): mix it into the State and implement RefreshData, 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 in AutoRefreshState.refreshInterval. Because a refresh must not flash the spinner or drop the list, the FutureBuilders branch on snapshot.hasData / hasError, not on connectionState. A refresh the user did not ask for stays silent — no snackbars, no disabled buttons (see AgentDetails._reloadAgent).
  • 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.