-
Notifications
You must be signed in to change notification settings - Fork 0
Flutter client plan
Status: plan only. Nothing here is implemented, and by design none of it lands
on main or in this repository's server tree.
It is not a fork of the server. It is a third client, alongside the Android
app and the vanilla-JS web UI, speaking the same three /api/* endpoints. A
parasite: it clamps onto a Breeze Core someone is already running, and that
server neither knows nor cares it exists. Delete it and everything keeps working,
which is the same contract the existing clients honour.
The appeal is straightforward: the Flutter app already has the good controls —
the temperature dial, fan, flap, mode, power, the indoor/outdoor climate bar, the
Nerd screen, profiles, Ed25519 v2 auth — as Dart code drawn with CustomPainter.
Compiling that to the web gets the browser panel to app parity for a fraction of
what rewriting it in vanilla JS would cost, and iOS users on the web UI get the
same experience as Android users have in the app.
A separate repository, not a branch, and never main.
A branch of this repo would drag the entire server tree along for a client that
touches none of it, and would imply eventually merging back — which is exactly
what must not happen, because of the constraint in the next section. A separate
repo (say breeze-web) that depends on the published API contract is the honest
shape. If the lineage matters, forking this repo on GitHub and deleting
meow_ac/ is fine; the point is that the server's main never grows a build
step.
create_app() deliberately has no CORS middleware, and the reason is documented
inline: the UI is same-origin, and permissive CORS would let any other page on
the LAN drive the API. That is load-bearing and this plan does not get to relax
it.
So a Flutter client hosted on its own origin — a different port, bolero, a dev server — cannot talk to Breeze Core at all. Browsers will block it, and the fix is not "add CORS".
That leaves one workable shape: build elsewhere, serve from the server's own
origin. The build output (build/web/) is static files, so:
- ship it as a tarball users drop into place, or
- ship it as an optional package (
breeze-core-web-flutter) that installs the files and mounts them at a second path, - and mount it at something like
/flutter/so the existing panel stays at/and neither replaces the other.
That is also the migration story: both UIs can be served at once, from the same origin, and people can switch by changing the URL.
SecurityHeadersMiddleware sets default-src 'self' with no inline scripts, and
CLAUDE.md is explicit that adding either forces loosening the CSP. Flutter Web's
bootstrap uses an inline script, and the CanvasKit/WASM renderer additionally
needs wasm-unsafe-eval.
This is solvable but must be deliberate: a separate, narrower CSP applied only
to the /flutter/ mount, leaving the policy over / untouched. What it must
not become is a global relaxation that weakens the existing panel to accommodate
the new one. If that is unacceptable, the honest conclusion is that the Flutter
client is served by something else on the same origin (a reverse proxy path), not
by Breeze Core.
Reusing the app's Dart is worth more than the widgets:
-
Controls, layout, climate bar, Nerd screen, profiles — all
CustomPainteror plain Dart, all platform-neutral. -
ApiClient, SSE consumption, poll fallback, the 401 reason-code handling — the parts that took the longest to get right. -
Auth v2 signing, which is the interesting one. The vanilla-JS migration is
blocked on a real wall: WebCrypto has Ed25519, but it has no SHA3-512, and
the v2 canonical string hashes the body with SHA3-512. A Flutter client has no
such problem —
cryptographyandpointycastleare Dart and compile to JS/WASM, so the same signing code the app uses runs in the browser. That quietly removes the blocker from AUTH-V2-MIGRATION.md for this client.
The catch, and it is a real one: a browser has no Keychain and no Android
Keystore. An Ed25519 private key in a Flutter web client lives in IndexedDB,
readable by any script that achieves XSS on that origin. That is strictly weaker
than either existing client, and it argues for treating a browser credential as
lower-trust — a shorter TTL than the household's 3650 days, and a label that
makes it obvious in /api/auth/devices which credential is the browser's.
The current panel already does this and it must survive: the grid is
repeat(auto-fit, minmax(340px, 1fr)) collapsing to one column under 620px, so
units sit next to each other on a desktop and under each other on a phone.
Note that this is not what the Android app does — the app is one unit per
screen with PageView swiping, which suits a phone and would be wrong on a
1440px monitor. So the Flutter client cannot simply reuse the app's top-level
navigation: it needs a LayoutBuilder that chooses a responsive grid on wide
viewports and the stacked/swipe arrangement on narrow ones. Everything inside
a unit card carries over unchanged.
- Payload. The current UI is 1,531 lines of hand-written JS and CSS served as ~40 KB of text. A Flutter web build is megabytes — roughly 1.5–2 MB with the HTML renderer, considerably more with CanvasKit. On a LAN that is a non-issue on second load and noticeable on first. On a router with extroot it is a genuine consideration.
-
A build step enters the picture — just not in this repo. The parasite has
flutter build webin its own CI; Breeze Core keeps its "no build step, native ES modules" property, which is the whole reason for keeping them apart. - Two UIs to maintain. Mitigated by the vanilla panel staying deliberately simple: it now streams state, has beep and the Nerd panel, and does not need to chase every app feature. If the Flutter client succeeds, the vanilla one becomes the small dependency-free fallback rather than the flagship — a reasonable division, and worth deciding on purpose rather than by neglect.
-
Spike the CORS/CSP question first, because everything depends on it: build
the app for web, drop it into a directory served by Breeze Core at
/flutter/, and see what the CSP blocks. If it cannot be served same-origin under an acceptable policy, the plan changes shape entirely and it is better to learn that in an afternoon. - Prove auth v2 in the browser — enrol, sign one request with the Dart path, confirm the server accepts it. That is the claim most worth checking early, and it is the one that makes this route better than a JS rewrite.
-
Responsive shell:
LayoutBuilder, grid on wide, stacked on narrow. -
Port the unit card — mostly moving
CustomPaintercode. - The rest: profiles, Nerd, programs, diagnostics.
-
Distribution: publish
build/webas a tarball on bolero next to the PoC artifacts, or as an optional package. Frozen and versioned separately from the server, like a client should be.
-
Same-origin mount path —
/flutter/?/ui2/? Whatever it is, it should not be/, so both UIs coexist during the trial. -
Renderer — HTML (smaller, weaker fidelity for custom painting) or CanvasKit
(bigger, pixel-identical to the app)? Given the whole point is reusing
CustomPainterwork, CanvasKit is the honest default and the payload cost has to be accepted or measured. - Should browser credentials get a shorter TTL than app credentials, given IndexedDB cannot protect a key the way a Keystore can?
-
Does the vanilla panel stay the default at
/indefinitely? Recommended yes, at least until the Flutter client has run in the household for a while.
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
Use it
Reference
Run it safely
Develop and port