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

97 lines
9.1 KiB
Markdown

# 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
```bash
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 `FutureBuilder`s 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.