# Peach — frontend architecture Peach follows the Autoreceptive Flutter architecture (`Frontend/Receptive` as the reference base). Everything below is enforced convention, not suggestion — new code that ignores it will look wrong next to the rest. ## 1. Stack - **Flutter**, state management **`stacked`** (MVVM) via `ViewModelBuilder.reactive`. - **Transport:** SSH/SFTP (`dartssh2`) — Peach talks to servers, not an HTTP API. - **Storage:** `shared_preferences` for config, `flutter_secure_storage` (OS keystore) for passwords. - **Codegen:** Mason brick for screens (§6). - Core idea: **every screen is 4 files**; **one** `ParentViewModel` centralises loading/error/reachability; **one** `AppDataManager` is the single data gateway. ## 2. Folder structure (`lib/Peach/`) ``` lib/Peach/ about/ # models (one class per file, named after the class) external/data/ # what comes back FROM a server (Docker, Kafka, Redis, Health…) external/initial/ # what we send TO a server (SystemdSpec) internal/application/ # app-owned models + enums (ServerProfile, ServiceConfig, # DeploymentRecord, StepTracker, NavigatorType, TextType…) internal/file/ # local file work (BuildService, WebArchive, FileStorage) comms/ # the wire ConnectComms.dart # abstract contract: every remote call Peach can make Comms.dart # SshService — the one implementation informatics/ # data gateway DataManager.dart # abstract: ConnectInternalMemory + ConnectComms + ConnectFileStorage AppDataManager.dart # the single concrete DataManager (pure delegation) memory/ # local persistence ConnectInternalMemory.dart / InternalMemory.dart ConfigStore · DeploymentStore · SecretStore configs/ # Navigator.dart (PeachNavigation), ThemeController.dart designs/ # the design system — reuse before building Responsive · LogConsole · StepList · StatCard · TransferCard · OutputSheet … text/Text.dart (the `text()` helper) input/InputFields.dart navigation/SideNav.dart see/ # screens — one folder per feature parent/ParentViewModel.dart /{Foo.dart, FooState.dart, ViewFoo.dart, ConnectFoo.dart} utils/ # Colors · CommonUtils · Validators main.dart ``` ## 3. The 4-file screen pattern (mandatory) | File | Role | |---|---| | `Foo.dart` | `StatefulWidget` shell — only creates `FooState`. | | `FooState.dart` | `State` — all UI + local state; **implements `ConnectFoo`**. Built with `ViewModelBuilder.reactive`. | | `ViewFoo.dart` | `ViewModel extends ParentViewModel` — all logic; calls the `DataManager`; pushes results back through `ConnectFoo`. Never touches widgets. | | `ConnectFoo.dart` | The callback interface between the two. | **Wiring:** ```dart class FooState extends State implements ConnectFoo { ViewFoo? _model; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewFoo(context, this), onViewModelReady: _onReady, // assign + one named init builder: (context, viewModel, child) => Scaffold(…), // NO logic before return ); } void _onReady(ViewFoo model) { _model = model; _initiate(); } Future _initiate() async { _model?.loadThings(); } @override void onThingsLoaded(List things) { setState(() => _things = things); } } ``` ## 4. `ParentViewModel` — what every ViewModel inherits Constructed once per screen; builds the single `AppDataManager(InternalMemory(), SshService(), FileStorage())`. - `showLoading(text)` / `closeLoading()` — full-screen loading overlay. - `canReach(server, retry)` — opens (or reuses) an SSH session; on failure raises the *server-unreachable* overlay wired to `retry` and returns `false`. **This is the SSH equivalent of the reference's connectivity `hasNetwork`** — Peach targets LAN/VPN boxes where a device-level internet check proves nothing. - `handleError(error, retry, close, buttonName)` — the standard `catch`. Closes loading, then maps the failure onto a `PeachError`: | Failure | Result | |---|---| | `SSHAuthFailError` / `SSHAuthAbortError` | "Authentication failed" (critical) | | `SocketException` / `SSHStateError` | "Connection failed" — host/port/VPN hints | | `TimeoutException` | "Timed out" | | `SSHError` | "SSH error" with the raw detail as helper | | `FileSystemException` | "File error" — local artifact unreadable | | anything else | generic `PeachError` | - `showApplicationNotification(type, title, description, drag, dismissible, close, {action, positiveAction})` — modal bottom sheet used for **all** confirmations (delete, install, clear). `positiveAction` must `Navigator.pop(context)` first. ## 5. Data layer `DataManager` is the union of three contracts, so a screen never reaches for a service directly: - `ConnectComms` → `SshService` — deploy, systemd, docker, kafka, redis, files, logs. - `ConnectInternalMemory` → `InternalMemory` — servers, deployment history, theme. - `ConnectFileStorage` → `FileStorage` — local build, artifact read, web packaging. Streams (`logs`, `stepStream`, `transferProgress`, `buildLogs`) are exposed as getters on the ViewModel and subscribed to by the `State` — a stream of remote output has no request/response shape to hand back one call at a time. Every *command* still goes through the ViewModel. ## 6. Code generation ``` dart pub global activate mason_cli # once, if mason is missing mason get # once per clone ``` ### Adding a screen ``` mason make mvvc_template # project: Peach, screen: MyScreen ``` Generates the four files under `lib/Peach/see/myscreen/`, pre-wired with the `Responsive` mobile/tablet/desktop split. Don't hand-scaffold them. ### Adding a persisted field ``` mason make internal_memory # field: pinnedServers, type: List ``` Injects the getter/setter pair into all three files that have to agree — `ConnectInternalMemory` (contract), `InternalMemory` (implementation + its storage key) and `AppDataManager` (delegation) — handling JSON for objects and lists, and putting secrets in the OS keystore under a random-suffixed key when you answer yes to "is this a secret". > **Passing a generic type on the command line:** `<` and `>` are shell > redirection, so `--request_object "List"` gets eaten before mason sees > it and fails with *"The system cannot find the file specified"*. Either answer > the interactive prompt, or pass a config file: > > ``` > echo '{"function_name":"pinnedServers","request_object":"List","secure":false}' > vars.json > mason make internal_memory -c vars.json > ``` Run it from the project root — the hook edits paths relative to there. It's idempotent: re-running for an existing field detects the duplicate and skips. ## 7. Hard conventions 1. **No logic in `builder:` before `return`.** Derived values are state fields or getters, set in `_initiate()`. 2. **Named event handlers only** — `onPressed: _onSave`, never `onPressed: () => _save()` (a closure is fine only when it carries an argument, e.g. `() => _onDeploy(svc)`). 3. **`onViewModelReady`** assigns `_model` and calls one named `_initiate()`. 4. **Never build raw `Text`/`TextStyle`** for display copy — use `text(…)` from `designs/text/Text.dart` with a `TextType`. `TextType` picks a **font family**, not a weight — `Bold`→Sora, `Regular`→Urbanist, `Light`→PlusJakartaSans. Weight is an independent axis: pass `weight:` for a heavy *body* string rather than reaching for `TextType.Bold`, which would reface it in the display font. Three things are deliberately *not* routed through `text()`: Material slot children (`SnackBar(content:)`, `DropdownMenuItem(child:)`, `Chip(label:)`, `ListTile(title:)`) which the theme already styles; `fontFamily: 'monospace'` terminal content; and `SelectableText`. 5. **Never hardcode a hex colour.** Named colours live in `utils/Colors.dart`; brightness-aware surfaces come from `context.jp` (`JpColors`). Terminal surfaces use `ConsoleColors` — a fixed palette that stays dark in both themes, because a console doesn't invert. 6. **Reuse `designs/` before building.** Screen-only widgets are private methods at the bottom of that screen's `State`; anything reusable moves to `designs/`. 7. **Formatting helpers live in `utils/CommonUtils.dart`** (`formatBytes`, `formatCount`, `timeAgo`, `baseName`, `joinRemote`…) — never inline them. 8. **Navigation goes through `PeachNavigation`** with a `NavigatorType`; destinations are widget instances, not route strings. Use `navigateToPageWithData` when the parent must refresh on return.