examples/todo/tests/perfsmoke.sh asserts the PLAN.md §11 budgets
against the built AppImage:
- Bundle size ≤ 200 MB (hard cap; ≤ 120 MB target)
- Cold start ≤ 2000 ms from launch to first /healthz 200
- Idle RSS (host + descendants in the process group) ≤ 200 MB after
a 2 s settle.
Each budget is overridable via env (PERF_COLD_START_MS etc.) for slow
shared CI runners; defaults are the strict numbers from the plan. Runs
the AppImage under xvfb-run when DISPLAY is unset; falls back to
QT_QPA_PLATFORM=offscreen otherwise (the build script already bundles
libqoffscreen.so via EXTRA_PLATFORM_PLUGINS).
Wired into:
- examples/todo/Makefile → `make perf`
- .gitea/workflows/release.yml → runs after AppImage build, before
zsync + upload, with cold-start budget bumped to 4 s for CI.
CI now also installs zsync + xvfb in one step.
examples/todo/README.md gains an "AppImage packaging (Phase 4a)"
section walking through `make appimage`, bundled-mode behaviour, the
auto-update QML hooks (BackendConnection.checkForUpdates() / applyUpdate()),
and `make perf`.
PLAN.md §13 Phase 4 marked **4a closed**. 4b (macOS) and 4c (Windows)
stay stubs until their runners + certs exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
5.7 KiB
Markdown
119 lines
5.7 KiB
Markdown
# examples/todo
|
|
|
|
The framework's POC application: a real Qt/QML todo app whose backend is a Symfony service generated entirely by the `make:bridge:*` makers. Demonstrates every architectural primitive in PLAN.md §13 Phase 3.
|
|
|
|
## What's here that wasn't in the skeleton
|
|
|
|
- `Todo` resource (entity + controller + QML snippet) generated via `make:bridge:resource Todo`.
|
|
- `MarkAllDone` command generated via `make:bridge:command MarkAllDone`, with a body that flips `done = true` on every todo.
|
|
- `TodoWindow.qml` second-window scaffold generated via `make:bridge:window Todo`, customised to embed a read-only mirror of the same `ReactiveListModel`.
|
|
- `Main.qml` rewritten as a real list UI with add input, per-row toggle/delete, "mark all done", and "open second window".
|
|
|
|
Everything cross-side (PHP ↔ QML) is maker-generated. There is no handwritten bridge glue in this example.
|
|
|
|
## Run it
|
|
|
|
```bash
|
|
make install # composer install
|
|
make build # cmake + qt host
|
|
make doctor # bridge:doctor — readiness check
|
|
make dev # FrankenPHP --watch + Qt host
|
|
```
|
|
|
|
Add a few todos, toggle them, click "Mark all done", click "Open second window" — both windows stay in sync.
|
|
|
|
## Multi-window test
|
|
|
|
1. `make dev` starts the app.
|
|
2. Add three todos via the input field.
|
|
3. Click **Open second window**. A `Todos (mirror)` window opens.
|
|
4. In the main window, toggle one of the todos. The mirror flips its row within ~50 ms (Mercure SSE).
|
|
5. Add a new todo in the main window. It appears in the mirror within ~50 ms.
|
|
6. Click **Mark all done** in the main window. Both windows show all rows ticked simultaneously.
|
|
|
|
Each window has its own `ReactiveListModel` instance subscribed to the same `app://model/todo` topic; the framework keeps them coherent without per-window glue.
|
|
|
|
## Crash-and-recover test
|
|
|
|
1. `make dev` is running.
|
|
2. Add a todo so the list is non-empty.
|
|
3. From another terminal: `pkill -f 'examples/todo/symfony.*frankenphp'`.
|
|
4. The app's `AppShell` shows the **Reconnecting** banner (visible on the second window; the main window keeps its own status display).
|
|
5. Restart the FrankenPHP child: from the example dir, `cd symfony && frankenphp run --watch --config ../Caddyfile` — or just re-run `make dev`.
|
|
6. The app flips back to **Online** and re-fetches the list. No row corruption.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
todo/
|
|
Caddyfile # FrankenPHP / Mercure config (port 8765, anonymous SSE)
|
|
Makefile # build / dev / doctor / quality
|
|
scripts/dev.sh
|
|
symfony/
|
|
composer.json # path repo points one level deeper at framework/php
|
|
config/
|
|
src/Entity/Todo.php # generated by make:bridge:resource
|
|
src/Controller/TodoController.php # generated, CRUD on /api/todos
|
|
src/Controller/MarkAllDoneController.php # generated, body filled in
|
|
public/index.php
|
|
bin/console
|
|
.env
|
|
migrations/
|
|
qml/
|
|
CMakeLists.txt
|
|
main.cpp
|
|
Main.qml # real todo UI (add / toggle / delete / mark-all / 2nd-window)
|
|
TodoList.qml # generated; also reused by the second window
|
|
TodoWindow.qml # generated, customised to embed mirror list
|
|
```
|
|
|
|
## What this example proves
|
|
|
|
- The headline workflow scales: a non-trivial app's PHP/QML wiring is **all generated**.
|
|
- `ReactiveListModel` handles in-flight optimistic mutations (`pending` role drops opacity on toggled rows until the Mercure echo lands).
|
|
- Two windows of the same QML app stay coherent under mutations from either side.
|
|
- Stopping FrankenPHP mid-session triggers `Reconnecting` → `Offline` UI; restart restores `Online` and re-fetches without dupes.
|
|
- `Idempotency-Key` round-trips through to Mercure as `correlationKey` — visible in `dev.log` if you `grep correlationKey`.
|
|
|
|
## Out of scope here
|
|
|
|
- A CI-driven version of the multi-window / crash-recover tests lives in Phase 3 sub-commit 4 as a bridge-integration suite.
|
|
- No persistence options (export, backup) — same SQLite `var/data.sqlite` as the skeleton. Apps move to Postgres by overriding `DATABASE_URL`.
|
|
|
|
## AppImage packaging (Phase 4a)
|
|
|
|
```bash
|
|
FRANKENPHP=/path/to/frankenphp make appimage
|
|
./build/Todo-x86_64.AppImage
|
|
```
|
|
|
|
`make appimage` stages a `composer install --no-dev` copy of the Symfony app, downloads `linuxdeploy` + `appimagetool` + the AppImageUpdate sidecar (cached under `packaging/linux/tools/`, gitignored), and produces `build/Todo-x86_64.AppImage` (~150 MB). The AppImage carries everything needed: Qt runtime, the bundled FrankenPHP binary, the Symfony app, and an `AppImageUpdate.AppImage` sidecar for in-place updates.
|
|
|
|
When the AppImage runs without `BRIDGE_URL` set, `BackendConnection` switches to **bundled mode**: it spawns the embedded FrankenPHP, generates a per-session bearer token, runs first-launch migrations into `~/.local/share/php-qml/todo/var/data.sqlite`, and supervises the child. Killing FrankenPHP from outside the AppImage triggers an automatic restart with a fresh token (PLAN.md §3 *Edge cases — Per-session secret rotation*).
|
|
|
|
### Auto-update
|
|
|
|
The AppImage is built with an embedded `update-info` ELF section pointing at the canonical Gitea Releases URL for its tag (set via `APPIMAGE_UPDATE_INFO` at build time). The bundled sidecar implements the actual download and patch via zsync.
|
|
|
|
From QML:
|
|
|
|
```qml
|
|
Connections {
|
|
target: BackendConnection
|
|
function onUpdatesAvailable() { ... }
|
|
function onNoUpdatesAvailable() { ... }
|
|
function onUpdateApplied() { ... } // restart prompt
|
|
}
|
|
|
|
Button { text: "Check for updates"; onClicked: BackendConnection.checkForUpdates() }
|
|
Button { text: "Update"; onClicked: BackendConnection.applyUpdate() }
|
|
```
|
|
|
|
### Performance smoke
|
|
|
|
```bash
|
|
make perf
|
|
```
|
|
|
|
Asserts the §11 *Performance budgets*: bundle ≤ 200 MB, cold start ≤ 2 s (4 s on shared CI runners), idle RSS ≤ 200 MB. CI runs this on every release tag before publishing.
|