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.
-`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.
`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() }
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.