Files
php-qml/PLAN.md

618 lines
47 KiB
Markdown
Raw Normal View History

# php-qml — Plan for a Symfony/FrankenPHP + Qt/QML Desktop Framework
> **Status (2026-05):** v0.1.0 + v0.1.1 + v0.1.2 shipped 2026-05-03 (LGPL-3.0-or-later). Planning is version-based — see §13.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
>
> **Where else to look:**
>
> - `docs/` — how to use what's built (architecture, getting-started, makers, packaging, API references).
> - `CHANGELOG.md` — what shipped in each release.
> - `PLAN.md` (this file) — *why* the architecture is the way it is, plus what's coming next per version.
## 1. Vision
A small, opinionated framework that lets PHP developers ship native desktop applications with a Qt/QML frontend and a modern Symfony backend, packaged as a single distributable per OS.
The framework is **not** a PHP↔Qt language binding. It is a **process-pair architecture**:
- A Qt/QML host process owns the window, input, and rendering.
- A bundled FrankenPHP binary runs a Symfony application in worker mode as a child process.
- The two communicate over local HTTP (commands/queries) and Mercure SSE (push/state sync).
The framework's job is to make that boring: lifecycle, transport, conventions, and reactive models — so an application author writes ordinary Symfony code and ordinary QML, and they just connect.
## 2. Architecture Overview
```text
+-----------------------------------------------------------+
| Single OS-native bundle |
| |
| +----------------------+ +-----------------------+ |
| | Qt/QML host (C++) | | FrankenPHP (child) | |
| | | | | |
| | - QGuiApplication | | - Caddy + Mercure | |
| | - QML engine | | - PHP worker mode | |
| | - BackendConnection |<---->| - Symfony kernel | |
| | - SSE client | HTTP | (boots once) | |
| | - Reactive models | SSE | - Doctrine + SQLite | |
| +----------------------+ +-----------------------+ |
| |
+-----------------------------------------------------------+
\_____________ one binary to ship _________________/
```
Transport choice:
- **Linux / macOS:** Unix domain socket (preferred — no port allocation, no firewall prompts).
- **Windows:** loopback TCP on a random ephemeral port, bound to `127.0.0.1`.
## 3. Process Lifecycle
### Run modes
The host supports two run modes, selected at build time and surfaced to the rest of the system through the same `BackendConnection` API:
- **Bundled** (default for shipped binaries) — host spawns the embedded `frankenphp` child as described in *Startup* below.
- **Dev** — host does not spawn anything. It connects to a developer-managed backend at a URL given via env var or CLI flag, typically `frankenphp run --watch` or `symfony serve` running from the Symfony source tree. PHP code changes are picked up by the watcher; QML changes are picked up by Qt's own live-reload tooling (Qt Creator, `qmlls`, or running QML from source rather than from the compiled `qrc:`).
The dev/bundled split is a first-class concern, not a Phase-5 nicety: the framework skeleton must boot in dev mode from Phase 1 onward (§13). Bundled mode is the *additional* thing introduced when packaging arrives.
In dev mode, auth is simplified — the bearer token is read from a shared `.env.local` rather than generated per-session, so the developer can curl the API freely.
### Single-instance
The host is single-instance per OS user. On launch, before anything else:
1. Acquire the single-instance lock by binding a named local endpoint — `QLocalServer` on a fixed-name socket on Unix, a named pipe on Windows. The endpoint name is derived from the application identifier and the current user.
2. If the bind fails, an instance is already running. Connect to that endpoint, forward the launch arguments (e.g. `open <path>`, `--new-window`, deep-link URI), then exit 0.
3. Otherwise, this process becomes the live instance and continues to *Startup*. The bound endpoint stays open for the lifetime of the process and accepts launch-arg messages from subsequent invocations; each message is dispatched to the application (default action: focus the existing main window, or open a new one if `--new-window` was passed).
The single-instance lock and the backend transport are *separate* endpoints — do not conflate them. The lock lives at a stable, predictable path; the backend socket is per-session and ephemeral.
### Startup
1. Resolve the user's app data directory (per OS convention) and ensure `var/`, `var/data.sqlite`, `var/cache/`, `var/log/` exist.
2. Generate a per-session shared secret (random 32 bytes) and write it to an env var passed to the child. (Dev mode skips this; the secret comes from `.env.local`.)
3. **Bundled mode only:** spawn the bundled `frankenphp` binary with a generated `Caddyfile` pointing at the embedded Symfony `public/index.php`.
4. Wait for a readiness signal (HTTP `GET /healthz` returns 200, with bounded retry / timeout). In dev mode the host waits on the configured URL; in bundled mode, on the freshly spawned child.
5. QML root window loads, opens its Mercure subscription, fetches initial state.
### Steady state
- Symfony kernel persists across requests (worker mode); Doctrine connection pooled.
- Mercure hub is in-process inside FrankenPHP — no separate daemon.
- The single-instance endpoint accepts and dispatches launch-arg messages from new invocations.
### Shutdown
- Bundled mode: host sends `SIGTERM` to the child; on timeout, `SIGKILL`. Host removes the unix socket file (Linux/macOS).
- Dev mode: host disconnects; the dev backend keeps running.
- The single-instance lock is released as the host process exits.
- A crash supervisor restarts the bundled child up to N times; after that, the host shows a fatal-error UI. Dev mode does not supervise the backend — that's the developer's terminal.
### Edge cases
- **macOS launch dispatch.** File-open and URL-handler events on macOS arrive via Apple Events (`QFileOpenEvent`), not argv. The single-instance dispatcher must subscribe to that event in addition to the named-endpoint launch-arg channel; otherwise double-clicking a file or following a `myapp://` URL silently does nothing.
- **Sleep / wake.** Closing a laptop lid drops the SSE connection. Reconnect uses `Last-Event-ID`, but if the wake happens after the Mercure hub has rolled past that ID, clients get an explicit *gap* signal and re-issue initial fetches (§5, *Connection state*).
- **Per-session secret rotation.** When the crash supervisor restarts the bundled child, a new shared secret is generated. The host updates `BackendConnection.token` in place and emits `tokenRotated`; `MercureClient` and `RestClient` pick up the new value on next request. In-flight requests with the old token are retried.
- **Single-instance launch race.** If two instances launch in the same millisecond, both can fail to bind. The bind is retried briefly with a short backoff before the host gives up and connects as the second instance — preventing a deadlock where both processes exit thinking the other one won.
## 4. Communication Contract
### Commands and queries (QML → PHP)
- Plain HTTP, JSON in/out.
- Conventional routes:
- `GET /api/{resource}` — list / read
- `POST /api/{resource}` — create / invoke command
- `PATCH /api/{resource}/{id}` — update
- `DELETE /api/{resource}/{id}` — delete
- Authentication: per-session shared secret in `Authorization: Bearer …` header, validated by a Symfony firewall.
- Errors: RFC 7807 `application/problem+json`.
### Push (PHP → QML) via Mercure
- Topic convention:
- `app://model/{name}` — collection-level changes
- `app://model/{name}/{id}` — single-entity changes
- `app://event/{domainEvent}` — arbitrary domain events
- Update envelope:
```json
{
"op": "upsert" | "delete" | "replace" | "event",
"id": "...",
"data": { ... },
"version": 42,
"correlationKey": "optional-uuid-from-originating-request"
}
```
- `version` is monotonic per topic, used by clients to detect gaps and trigger a re-fetch.
- `correlationKey` echoes the `Idempotency-Key` of the request that caused this update, when applicable. Clients use it to match server-pushed updates back to their pending optimistic mutations (§5).
### Why this split
- HTTP for commands keeps semantics obvious (status codes, idempotency keys, file uploads later).
- SSE for push is one-way which matches the actual data-flow direction. We avoid bidirectional WebSocket state machines for the POC.
### Pagination
Collections larger than a few hundred entries need cursor-based pagination, not full-table fetches.
- `GET /api/{resource}?cursor={opaque}&limit=50` returns `{ items, nextCursor }`. The cursor is server-generated and opaque to the client.
- `ReactiveListModel` requests pages on demand (initial fetch plus scroll-driven fetch-more) and integrates Mercure-pushed inserts at the head.
- Mercure topics for paginated resources publish single-row diffs only; no "the whole list changed" envelope. Clients needing a full reset issue a new initial fetch.
### File uploads
- `POST /api/{resource}` accepts `multipart/form-data` for resources with file fields.
- The controller streams uploads via Symfony's `UploadedFile` API into FrankenPHP's `upload_tmp_dir`, then moves the validated file to `var/uploads/{resource}/{id}/...` under the user data dir.
- Long uploads emit progress envelopes to a per-upload Mercure topic so QML renders a progress bar without polling.
- Maximum upload size is configured per resource and surfaced via `OPTIONS /api/{resource}` so the client can pre-check.
### Idempotency
- Mutating routes (`POST`, `PATCH`, `DELETE`) accept an optional `Idempotency-Key` header carrying a client-generated UUID.
- Symfony stores the `(key, response)` tuple for 24 h in a Doctrine-backed cache; replays return the cached response without re-running the handler.
- `RestClient` generates and attaches keys automatically for non-GET requests so retries after a transient SSE drop or app relaunch are safe by default.
## 5. Update Semantics and Disconnected UX
How state changes flow between QML and PHP, and what the UI does when the backend is slow, restarting, or unreachable. These are framework decisions, not application choices, because the reactive models embed them.
### Optimistic updates
When QML invokes a mutating command, the framework applies the change locally first and reconciles with the server-pushed Mercure echo:
- `ReactiveListModel` exposes a `pending` role per row; delegates render in-flight rows differently (reduced opacity, inline spinner).
- Each command carries an `Idempotency-Key` (§4); the Mercure echo carries the same value as `correlationKey` so the model can match the push back to the originating optimistic mutation and clear `pending`.
- On `4xx` / `5xx` response, the model rolls back the optimistic mutation and emits `commandFailed(error)` for the application to surface as a toast or inline error.
- Timeout: if no Mercure echo arrives within N seconds (default 10 s), the model rolls back and emits `commandTimedOut` — usually because the backend died mid-command.
### Connection state
`BackendConnection` exposes connection health as a first-class property:
- `connectionState` enum: `Connecting`, `Online`, `Reconnecting`, `Offline`.
- `Online``Reconnecting` after one SSE drop and one failed reconnect. Subsequent failures stay at `Reconnecting` with exponential backoff.
- `Reconnecting``Offline` after a configurable threshold (default 30 s of failed reconnects).
- On recovery, `ReactiveListModel`s issue a fresh initial fetch (the SSE buffer may have rolled past `Last-Event-ID`), and queued commands are flushed in order. Idempotency keys make the flush safe.
### Disconnected behaviour
- In `Reconnecting`, optimistic mutations still apply locally but their Mercure echoes are deferred — the UI shows a soft "syncing" indicator without blocking input.
- In `Offline`, queued commands are paused (not failed). The application can either keep accepting writes (queued for flush) or render a read-only state — the framework exposes both options as a `BackendConnection.offlineWritePolicy` setting.
- `AppShell.qml` provides default UI for `Reconnecting` (top banner) and `Offline` (full-screen overlay with retry button). Applications can override by binding to `connectionState` directly.
## 6. PHP-side Components (Symfony bundle)
Working name: `PhpQml\Bridge`.
- **`BridgeBundle`** — Symfony bundle, autoconfigures the rest.
- **`SessionAuthenticator`** — validates the per-session bearer token from env.
- **`Publisher`** — thin wrapper over `symfony/mercure`'s `HubInterface`, enforces topic naming and envelope shape.
- **`ModelPublisher`** — given a Doctrine entity + change type, publishes the right `app://model/...` update. Wired via Doctrine lifecycle listeners or Messenger.
- **`EventBridge`** — Symfony event subscriber that converts selected domain events into `app://event/...` updates.
- **`HealthController`** — `/healthz` for the host's readiness probe.
- **`AppPaths`** — service that reads OS-appropriate paths (passed in via env from the host) so kernel, cache, logs, and SQLite all live in the user's data dir, not next to the binary.
- **Recipe / skeleton** — a Composer-installable starter that wires `framework`, `doctrine`, `mercure`, `runtime/frankenphp-symfony`, and this bundle, with a sane default `Caddyfile`.
Configuration source-of-truth is Symfony YAML, *not* QML — backend is the model, frontend is the view.
## 7. QML-side Components (Qt module)
Working name: `PhpQml.Bridge` QML module + a small C++ plugin where required.
- **`BackendConnection` (C++ singleton, exposed to QML)**
- Owns the backend lifecycle: in bundled mode, the child process handle; in dev mode, just the configured URL.
- Exposes `ready`, `error`, `mode` (`"bundled"` | `"dev"`), `restart()` (no-op in dev mode).
- Holds the session `token` and injects it into requests; emits `tokenRotated` when the bundled child restarts and the secret changes (§3, *Edge cases*).
- Exposes `connectionState` (`Connecting` / `Online` / `Reconnecting` / `Offline`) and `offlineWritePolicy` (`queue` | `readOnly`); these drive the Update Semantics layer (§5).
- **`SingleInstance` (C++ singleton)**
- Owns the named-endpoint lock described in §3.
- Emits `launchArgsReceived(args)` when a second invocation forwards its arguments.
- Application code subscribes to that signal to decide whether to focus the existing window, open a new one, or handle a deep link.
- **`RestClient` (QML/JS)**
- Promise-style `get/post/patch/del`. Built on `QNetworkAccessManager`.
- Centralised error mapping (problem+json → JS error objects).
- Auto-attaches a freshly generated `Idempotency-Key` to every non-GET request (§4) so retries — manual or via the Update Semantics queue — are safe by default.
- **`MercureClient` (C++)**
- Implements the `text/event-stream` protocol against `QNetworkReply`.
- Reconnects with exponential backoff and `Last-Event-ID` resume.
- Emits `update(topic, envelope)` signals into QML.
- **`ReactiveListModel` (C++, extends `QAbstractListModel`)**
- Constructed with a topic and an initial-fetch URL.
- On creation: GET the URL (paginated, §4), populate; subscribe to the topic; apply `upsert`/`delete`/`replace` ops with role-name mapping derived from the first row. Scroll-driven `fetchMore()` pulls subsequent pages.
- Detects version gaps → triggers a refetch.
- Exposes a `pending` role per row for optimistic updates (§5); cleared when the matching Mercure echo arrives, rolled back on `commandFailed` / `commandTimedOut`.
- **`ReactiveObject` (C++)**
- Single-entity equivalent of `ReactiveListModel`. Properties become QML-bindable.
- **`AppShell.qml`**
- Optional convenience root component: renders a splash while `BackendConnection` is starting, swaps to the user's root component on `ready`, shows the fatal-error UI on terminal failure.
- Renders default UI for the disconnected states defined in §5: a top banner during `Reconnecting`, a full-screen overlay with a retry button during `Offline`. Applications can override by binding to `connectionState` directly.
The application author should be able to write:
```qml
ListView {
model: ReactiveListModel {
topic: "app://model/todos"
source: "/api/todos"
}
delegate: TodoItem { ... }
}
```
…and have it Just Work.
## 8. Developer Experience and Scaffolding
DX is part of the design surface, not a Phase-5 polish item. We lean on `symfony/maker-bundle` to scaffold code interactively, ship our own bridge-aware makers, and provide inspection commands to manage a running application.
### Why maker-bundle
Maker-bundle is the canonical Symfony scaffolding tool — interactive, idiomatic, familiar to every Symfony developer, and pluggable. Building on it rather than inventing a parallel CLI means we:
- avoid teaching a second mental model,
- inherit `make:entity`, `make:controller`, `make:command`, `make:migration`, `make:test`, etc. for free,
- compose cleanly: a developer can run `make:entity Todo` and then `make:bridge:resource Todo` to wire that entity through to QML.
`symfony/maker-bundle` is a `require-dev` dependency of the skeleton from Phase 1.
### Custom makers (`make:bridge:*`)
All under the `make:bridge:*` namespace, runnable via `bin/console`. Each writes to both the PHP side and the configured QML source directory (`framework.bridge.qml_path` in `config/packages/php_qml.yaml`, default `../qml/`). After-generation hints suggest the next logical command.
- **`make:bridge:resource <Name>`** — the headline maker. Composes `make:entity` plus:
- Generates a CRUD `ApiController` mounted at `/api/{name}`.
- Registers a Doctrine lifecycle listener so persist/update/remove publish to `app://model/{name}`.
- Emits a starter `<Name>List.qml` snippet that binds a `ReactiveListModel` to the resource.
- After-hint: run `make:migration`.
- **`make:bridge:command <Name>`** — non-CRUD actions (e.g. `MarkAllRead`, `ImportCsv`).
- Generates a Messenger command + handler.
- Generates a controller route that dispatches it.
- Adds a helper to the QML bridge module so the call site reads as `Bridge.markAllRead(...)`.
- **`make:bridge:event <Name>`** — domain event plus its bridge wiring.
- Generates the event class and a subscriber that republishes it on `app://event/{name}`.
- Generates a QML signal-handler stub.
- **`make:bridge:read-model <Name>`** — query-only views (no entity, no writes).
- Generates a controller + a query service stub.
- Generates a `ReactiveListModel`-bound QML component.
- **`make:bridge:window <Name>`** — top-level QML window scaffold.
- Generates `<Name>Window.qml` using `AppShell` boilerplate.
- Registers it with the host's window registry so it's openable from menus or from launch-arg dispatch (§3, single-instance).
### Lifecycle and inspection commands
Beyond scaffolding, the framework exposes commands to inspect a running application — saving the "why doesn't it work" half-hour:
- `bin/console bridge:doctor` — readiness checks (FrankenPHP binary present, Mercure key configured, QML path writable, env vars set, dev backend reachable).
- `bin/console bridge:resources` — list all registered bridge resources with their topics and routes.
- `bin/console bridge:topics` — list active Mercure topics with current subscriber counts.
- `bin/console bridge:publish <topic> <json>` — manually publish a test envelope to smoke-test wiring without needing UI.
- Standard Symfony commands continue to work: `make:migration`, `doctrine:migrations:migrate`, `cache:clear`, `debug:router`, etc.
### Customisation
Maker-bundle templates are overridable via skeleton paths. We document the override locations so application authors can tailor generated code style (preferred property visibility, custom base classes, naming conventions) without forking the bundle.
### One-command dev loop
A `Makefile` at the skeleton root provides:
- `make dev` — starts `frankenphp run --watch` against the Symfony source and launches the Qt host in dev mode in parallel; tears both down on `Ctrl-C`.
- `make doctor` — passthrough to `bridge:doctor`.
- `make quality` — runs the same checks the CI `quality` job runs.
Plain Make rather than Task/Just keeps the toolchain lean — no extra install, available on every dev box and CI runner. Scaffolding still goes through `bin/console make:bridge:*` directly.
Intent: clone the skeleton, run `make dev`, code within 30 seconds.
### Editor support and debugging
Documented in the skeleton's README so first-day setup doesn't require guesswork.
- **Recommended editors:** VS Code with the Qt extension + `qmlls` for QML, plus Intelephense or Phpactor for PHP. JetBrains users get PhpStorm + Qt Creator side-by-side; both can attach to the running pair.
- **PHP debugger:** Xdebug into FrankenPHP works out of the box — `make dev-debug` starts FrankenPHP with `XDEBUG_MODE=debug,develop` and `XDEBUG_TRIGGER=1`, listening on the Symfony container's port. Skeleton ships a `.vscode/launch.json` and a `.idea/runConfigurations/` example wired up.
- **QML debugger:** Qt's QML debugger attaches via `-qmljsdebugger=port:N`. The dev-mode host accepts a `--qml-debug-port=N` flag that opens it; off by default.
- **Logs:** in dev mode, child stdout/stderr is forwarded to the host's stderr so everything streams to one terminal. Bundled mode writes to `var/log/` and the host surfaces a "Open log folder" menu item.
### Roadmap impact
`symfony/maker-bundle` is a Phase-1 dependency. The first scaffolded application in Phase 3 (`examples/todo`) is itself produced by running the makers — this is how we validate that generator output is genuinely usable rather than a glossy demo.
## 9. Project Layout
```txt
php-qml/
PLAN.md
README.md
framework/
php/ # the Symfony bundle (composer package)
src/
config/
composer.json
qml/ # the Qt module
src/ # C++ plugin (BackendConnection, MercureClient, models)
qml/ # pure-QML helpers (AppShell, RestClient)
CMakeLists.txt
skeleton/ # `composer create-project php-qml/app` template
symfony/ # minimal Symfony app pre-wired with the bundle
qml/ # minimal QML app pre-wired with the module
CMakeLists.txt
Caddyfile.tmpl
examples/
todo/ # POC application (see §10)
packaging/
linux/ # AppImage recipe
macos/ # .app + codesign script
windows/ # NSIS / MSIX recipe
.gitea/
workflows/
ci.yml # quality + build on every push/PR
release.yml # signed artefacts on v* tags
docs/
```
The framework ships as two artefacts plus a skeleton:
- A Composer package (the Symfony bundle).
- A Qt module (header-only QML + a compiled plugin).
- A skeleton repo that wires them together.
## 10. POC Scope — `examples/todo`
Smallest application that exercises every framework capability.
Features:
1. Single window with a list of todos and an "add" input.
2. Add / toggle-done / delete commands hit `/api/todos`.
3. List updates via `app://model/todos` push.
4. **Multi-window test:** "Open another window" menu item — opens a second top-level window. Editing in one window updates the other within ~50 ms. This proves the reactive model is actually reactive and not just a glorified fetch.
5. Persistence via Doctrine + SQLite in the user's app data dir.
6. Survives backend crash: kill the child, host restarts it, Mercure re-subscribes, models recover.
What this proves:
- Process lifecycle works.
- Transport works on the target OS.
- The reactive list model handles upsert/delete correctly.
- SSE reconnect with `Last-Event-ID` actually resumes without dupes/gaps.
- Packaging produces something a non-developer can double-click.
## 11. Build, Packaging, and CI
### Build inputs
- **Qt host:** CMake, Qt 6.x (LTS), `qt_add_qml_module` for the framework's QML module.
- **PHP runtime:** download the official static `frankenphp` binary per platform at build time, with a pinned URL and verified sha256, embedded next to the host executable.
- **Symfony app:** `composer install --no-dev` into a `dist/symfony/` directory; shipped as a directory tree (not a phar — keeps debugging sane and lets the runtime lazy-load).
### Per-OS packaging
- **Linux:** AppImage. `linuxdeployqt` for Qt, FrankenPHP and the Symfony tree dropped into the AppDir.
- **macOS:** standard `.app` bundle, FrankenPHP at `Contents/MacOS/frankenphp`, Symfony at `Contents/Resources/symfony/`. Codesign + notarize.
- **Windows:** NSIS or MSIX installer, `windeployqt` for the Qt deps.
### First-run migrations
Host invokes `bin/console doctrine:migrations:migrate --no-interaction` against the user's data dir before opening the SSE connection.
### Auto-update
Shipping a desktop binary means planning for updates from day one — there is no `composer update` for an end user.
- **Release feed:** the framework looks for a JSON appcast at a configurable URL (default: a static `latest.json` attached to the most recent Gitea Release) describing the current version, per-platform installer URLs, sha256, and signed release notes.
- **Per-platform mechanism:**
- Linux: AppImageUpdate (delta updates, signed by the release GPG key).
- macOS: Sparkle 2 (EdDSA-signed feed; integrates with notarized `.app` bundles).
- Windows: WinSparkle (DSA/Ed25519-signed feed; works with both NSIS installers and standalone).
- **UX:** check on launch and once per N hours; offer install on next restart, never auto-restart. User can opt out via settings; opt-out is respected even on critical updates (we *show* a banner, we don't force).
- **Release artefacts:** the CI release job (§*Continuous integration* below) emits both the installer and the appcast entry, so updating the feed is part of cutting a release, not a separate manual step.
### Distribution UX
The first-launch experience on each OS hides several non-obvious foot-guns. They're cheap to address if planned for, expensive to retrofit.
- **macOS Gatekeeper:** notarization is mandatory; without it, users get "app is damaged" instead of a meaningful prompt. Notarization requires an Apple Developer account ($99/year — operational cost).
- **Windows SmartScreen:** Authenticode signing reduces but does not eliminate the unknown-publisher prompt; reputation is built over time. EV (Extended Validation) certs bypass it but cost more.
- **Antivirus heuristics:** Qt + a Go binary (FrankenPHP) + an embedded PHP runtime + a SQLite file is exactly the layered structure that trips heuristic AV scanners on Windows. We pre-emptively submit signed builds to Microsoft Defender and major vendors after each release.
- **Linux file-association / desktop-entry:** AppImage doesn't auto-register file types or `myapp://` URL schemes. Document the per-distro install steps, or recommend AppImageLauncher.
- **App Store paths are separately scoped.** The Mac App Store sandbox forbids spawning arbitrary subprocesses, which is incompatible with our bundled-FrankenPHP architecture; MAS distribution is out of scope. The Microsoft Store accepts MSIX with full-trust, which is feasible but requires its own packaging variant; defer past v1.
### Performance budgets
Numbers we hold the line on; regressions block a release.
- **Cold start (bundled mode), commodity hardware:** ≤ 2 s from process launch to first window paint with backend ready.
- **Warm start:** ≤ 1 s.
- **Idle memory (single window, no data):** ≤ 200 MB resident.
- **Bundle size on disk:** target ≤ 120 MB per platform installer; hard cap 200 MB.
- **First-fetch list render (1 000 rows):** ≤ 250 ms from request to fully painted `ListView`.
CI captures these via a "performance smoke" job that runs the POC todo app's automated launch and lists; trends are tracked across builds. Detail-level tests (perf benchmarks per component) are out of scope for v0 but the harness is in place from Phase 4.
### Continuous integration (Gitea Actions)
The build pipeline runs in **Gitea Actions**. Workflow YAML is largely portable from GitHub Actions (Gitea's `act_runner` consumes the same syntax), so we can reuse community actions like `shivammathur/setup-php` and `jurplel/install-qt-action` directly. We pin every action by SHA, never `@main`.
#### Trigger model
- PRs and pushes to `main` — quality checks plus a full build matrix; artifacts uploaded for inspection, nothing published.
- Tag pushes matching `v*` — same as above, plus signing and a Gitea Release attaching the per-platform installers and a GPG-signed `SHA256SUMS`.
#### Jobs
1. `quality` — single Linux job, runs on every push: PHPStan, php-cs-fixer (check mode), `qmllint`, PHPUnit, QML tests. Cancel-fast on first failure.
2. `build` — fan-out matrix per target platform:
- Linux runner → AppImage
- macOS runner (self-hosted; see *Runners*) → signed `.app` + `.dmg`
- Windows runner → NSIS installer (signed when release secrets are present)
3. `release` — only on `v*` tags. Aggregates artifacts from `build`, generates `SHA256SUMS`, signs it with GPG, creates the Gitea Release via the Gitea release action, and uploads everything.
#### Per-build steps (shared shape across platforms)
Checkout → restore caches → install Qt SDK via `aqtinstall` → install PHP toolchain → fetch and verify the pinned FrankenPHP static binary → `composer install --no-dev` for the embedded Symfony app → CMake configure + build → run the platform-native deploy tool (`linuxdeployqt` / `macdeployqt` / `windeployqt`) → run the installer builder (`appimagetool` / `create-dmg` / `makensis`) → code-sign if release secrets are present → upload the artifact. Each per-OS workflow is a thin wrapper around this shared shape, so behaviour stays consistent across platforms.
#### Runners
- Linux and Windows: commodity self-hosted runners (Linux containerised, Windows VM). Fine on modest hardware.
- macOS: bare-metal or Apple-licensed VM under the org's control — typically a self-hosted Mac mini. This is a hard prerequisite for shipping the macOS build; document it in the project README.
#### Secrets
Held in Gitea repo secrets, scoped to the `release` job only — PRs from forks see none of these and skip signing automatically.
- `MACOS_CERTIFICATE`, `MACOS_CERT_PASSWORD`, `MACOS_NOTARY_KEY` — codesign + notarize.
- `WINDOWS_PFX`, `WINDOWS_PFX_PASSWORD` — Authenticode signing.
- `GPG_KEY`, `GPG_PASSPHRASE``SHA256SUMS.asc` signing.
#### Caching
- Qt SDK is the dominant cost (~3 GB). Cache key: `qt-${qt_version}-${os}`.
- Composer cache keyed on `composer.lock` hash.
- CMake build directory cache keyed on `os + qt_version + cmake_config_hash` for non-clean builds; opt-in per workflow.
#### Reproducibility
Every dependency is version-pinned: Qt, the FrankenPHP binary URL with verified sha256, the PHP version, every action reference. The Caddyfile and Symfony build are baked at build time; the produced binary fetches nothing at runtime.
#### Workflow files
- `.gitea/workflows/ci.yml``quality` + `build` on every push and PR.
Phase 2 sub-commit 3: full Update Semantics + ReactiveListModel + AppShell BackendConnection's ConnectionState enum is now Connecting / Online / Reconnecting / Offline (PLAN.md §5). The probe loop tracks the first failure since the last Online and transitions to Reconnecting on any failed probe, then to Offline once the configurable threshold (30 s default) is exceeded. The Error state is gone; Reconnecting + the exposed `error` string subsume its UI role. ReactiveListModel is the headline QML type: - QAbstractListModel that GETs `baseUrl + source` for an initial JSON array and then keeps in sync via an internal MercureClient subscribed to `topic`. - Role names are derived dynamically from the first row's keys plus an internal `pending` boolean role used by optimistic mutations. - Diff application: upsert (insert-or-update), delete, replace; gap detection via the envelope `version` field with auto re-fetch. - `invoke(method, path, body, optimistic)` is the optimistic command primitive. Generates an Idempotency-Key, applies the local diff, POST/PATCH/DELETEs with that key, and resolves on the matching Mercure echo (correlation-key matched in ModelPublisher's envelope). Rolls back and emits commandFailed on 4xx/5xx, commandTimedOut after 10 s without an echo. Phase 4 packaging will surface configuration for the timeout. AppShell.qml is the optional convenience root: - Reads BackendConnection.connectionState. - Reconnecting → top banner. - Offline → modal overlay with the error string and a Retry button (calls BackendConnection.restart()). - Wraps user content via `default property alias content`. Apps that want full chrome control can skip AppShell entirely; the skeleton's Main.qml keeps its own status display for demonstration and is unaffected. ReactiveObject (single-entity twin of ReactiveListModel) is intentionally deferred — same envelope handling, smaller surface; will land in Phase 2 follow-up or Phase 3 alongside the todo example. Cursor pagination is similarly deferred (the Phase 2 done criterion uses small lists). Smoke tested: /healthz + /api/ping round-trip cleanly, zero Mercure 401s, clean shutdown. composer quality stays green (16 tests, 45 assertions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:40:12 +02:00
- `.gitea/workflows/release.yml``v*` tag triggered, depends on `build`, signs and uploads.
## 12. Open Questions and Risks
| Topic | Question / Risk | Current bias |
| --- | --- | --- |
| Cold start | Symfony kernel boot on first launch will be visible. | Acceptable for v0; show splash. Investigate `app.cache` warmup at build time. |
| Bundle size | Qt + FrankenPHP + Symfony is heavy (likely 60120 MB). | Budgeted (§11, *Performance budgets*). Document and accept. |
| QML SSE client | No first-party SSE in Qt. We implement it. | Estimated ~200 LOC, well-trodden protocol. Low risk but real work. |
| Native dialogs (file pickers, notifications) | Where do they live? | QML side, via Qt — PHP shouldn't know. Document the boundary. |
| Auth model | Per-session bearer is enough? | Yes for local-only. If we ever expose the Mercure hub on LAN, revisit. |
| FrankenPHP-as-library | CGo-embed FrankenPHP into the Qt host as a single process? | Out of scope for POC. Note as a future optimisation; subprocess is fine and debuggable. |
| Migrations on schema change | Auto-run on launch is convenient but risky. | Auto-run for v0; add a "backup before migrate" step before v1. |
| Qt LGPL relinkability | Shipping Qt under LGPL legally requires letting users relink against their own Qt build. | Document the relink procedure for v0; revisit a commercial Qt licence if/when distribution scales. |
| Linux distribution channels | AppImage only, or also Flathub / Snap? | AppImage for POC (simplest). Flathub a v1 stretch goal — better discoverability, but adds packaging burden. |
| Multi-arch | macOS arm64+x86_64 universal? Linux/Windows ARM? | macOS universal from day one (Apple Silicon dominant). Linux ARM and Windows ARM deferred until user demand surfaces. |
| Testing strategy | What test layers exist beyond PHPUnit and qmllint? | Add `qmltestrunner` for QML units plus a thin bridge-integration suite that boots host + child and exercises the IPC stack. End-to-end UI testing deferred. |
| Maker output drift | Generated code rots silently as the framework evolves. | The POC todo app (built via the makers) doubles as a snapshot test; CI compares maker output to a checked-in reference. |
| i18n | Symfony Translator (XLIFF) and Qt Translator (.ts) are separate pipelines. | Keep them separate. Document the boundary (PHP strings in XLIFF, QML strings in .ts) and provide a single locale switch that fans out. |
| Data backup / export | SQLite in user data dir is convenient until the user changes machines or recovers from a botched migration. | Ship a `bridge:export` console command and a UI hook for backup-to-file from v1. Pre-migration auto-backup as in the migrations row. |
| Telemetry / crash reporting | Diagnose issues in user environments. | Opt-in only. Sentry on the PHP side is straightforward; crash dumps from the Qt host are platform-specific and deferred. |
| Security model | Could the bundled FrankenPHP be tricked into binding to `0.0.0.0`? | Caddyfile is generated from a hard-coded template that binds to the unix socket / loopback; fail closed if env says otherwise. Audit before v1. |
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
## 13. Versions
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
Phases 05 (the original POC roadmap) shipped as **v0.1.0** on 2026-05-03. From here on, work is organised by SemVer version rather than by phase:
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
- `v0.1.x` — bugfix releases (no new public API, no behaviour changes beyond the fix).
- `v0.x.0` — minor releases (new features, may break API in pre-1.0 — SemVer permits this).
- `v1.0.0` — when the public API is stable enough to commit to.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
Pre-1.0 tags (`v0.*`) are marked **prerelease** in Gitea (`.gitea/workflows/release.yml`).
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
Per-phase scope detail is preserved in `CHANGELOG.md` (per-version summary) and `git log` (per-commit detail) — no need to duplicate it here.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
### v0.1.0 — shipped 2026-05-03
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
First public preview. Linux AppImage. Full entry in `CHANGELOG.md`; binaries at <https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0>.
### v0.1.1 — shipped 2026-05-03
bundled: SIGTERM the frankenphp child via aboutToQuit, not just the dtor Symptom (user report on v0.1.1): QProcess: Destroyed while process ("/tmp/.mount_Todo-xbNoHHL/usr/bin/frankenphp") is still running. …and the frankenphp child + its PHP workers were left orphaned after the host exited. Cause: teardownChild() was only called from ~BackendConnection. By the time that destructor runs, app.exec() has already returned, QQmlApplicationEngine is mid-destruction, and Qt's event loop is half-torn-down. waitForFinished() doesn't reliably reap the child in that window — QProcess gets destroyed by the QObject parent-chain cleanup before the kernel reports the child as exited. Fix: in BackendConnection's constructor, connect QCoreApplication::aboutToQuit → teardownChild. aboutToQuit fires while the event loop is still active and BEFORE main() starts unwinding the stack, so SIGTERM + waitForFinished can do their job properly. The destructor's teardownChild call stays as belt-and- suspenders (no-op once aboutToQuit has already cleaned up — the function is idempotent via the m_child = nullptr at its end). The connect happens unconditionally in the constructor (not just for bundled mode) because m_child is also nullptr in dev mode and teardownChild handles that with its leading `if (!m_child) return;`. Regression guard: examples/todo/tests/bundled-supervisor.sh gains a "graceful shutdown" step: - Snapshots the host's child PIDs before SIGTERM - SIGTERMs the host, waits up to 3s for clean exit - Greps the host log for "QProcess: Destroyed while" — fail if found - Iterates the snapshotted PIDs, fails on any frankenphp orphan still alive Verified locally: real AppImage + the integration test both clean up without Qt warnings or orphan processes. PLAN.md: new v0.1.2 section above v0.1.1, this is its first entry. CHANGELOG.md: [0.1.2] — TBD section with the same Fixed entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:49:13 +02:00
Closed the four shakedown follow-ups identified during v0.1.0 shipping:
- **perfsmoke gap closed.** `HealthController` now constructor-injects `Publisher`; `/healthz` returns 200 only when `BridgeBundle` is fully container-resolvable. The `bundle` field in the response is the canary value perfsmoke + the bundled-mode integration test both check.
- **Bundled-mode supervisor integration test.** `examples/todo/tests/bundled-supervisor.sh` (run via `make integration-bundled`) stages a fake AppImage layout in `/tmp` and exercises the whole supervisor codepath (`resolveFrankenphpBin``runMigrations``spawnChild` → cache/log redirect to user data dir) without needing a real `.AppImage` build. Wired into ci.yml. Catches every v0.1.0 shakedown bug.
- **Skeleton AppImage parity.** `framework/skeleton/Makefile` gains `staging-symfony` + `appimage` targets mirroring the example's; `framework/skeleton/packaging/` ships starter `.desktop` + `.png`; `bin/php-qml-init` rewrites `BUNDLE_SRC` / `PACKAGING` Make variables and renames packaging files at scaffold time. `--vendor` mode also vendors `packaging/linux/` to `.bridge-packaging/`. Scaffolded apps inherit a working `make appimage` flow.
- **Caddyfile fmt.** `framework/skeleton/Caddyfile` and `examples/todo/Caddyfile` reformatted per `caddy fmt`; the "Caddyfile input is not formatted" boot warning is gone.
- **Cache-wipe on bundled launch** (added during v0.1.1 shakedown). Symfony bakes `kernel.project_dir` into its compiled cache; the AppImage's FUSE mount path changes per launch, so cache from launch N is stale by N+1. Supervisor now wipes `var/cache/` on every `initBundledMode`. Build-time cache warmup is the v0.2.0 follow-up.
### v0.1.2 — shipped 2026-05-03
Bugfix release. Bundles the v0.1.1 follow-up that surfaced during the cycle (clean child shutdown) with three non-breaking fixes from a post-v0.1.1 architecture audit:
- **Bundled supervisor: clean child shutdown.** The destructor's `teardownChild()` only ran during stack unwinding *after* `app.exec()` returned, by which point Qt's event loop was already mid-shutdown — so `QProcess::waitForFinished` couldn't reliably reap the child and Qt warned `QProcess: Destroyed while process is still running`, leaving an orphan frankenphp + its workers behind. Fix: connect `QCoreApplication::aboutToQuit` to `teardownChild` in the constructor, so the child is SIGTERM'd while the event loop is still active. Bundled-supervisor integration test gained a clean-shutdown assertion (no Qt warning, no orphan frankenphp under the host's PGID after SIGTERM).
- **`bridge.qml_path` is now actually configurable.** `BridgeResourceMaker` and `BridgeWindowMaker` carried docstrings claiming the QML output dir was settable via the bundle's `qml_path` option, but the bundle never wired one — the constructor default was the only knob. `BridgeBundle::configure` now defines a `qml_path` node (default `../qml/`); `loadExtension` exposes it as the `bridge.qml_path` container parameter; `services.yaml` binds it into both makers. Apps configure with `config/packages/bridge.yaml`: `bridge: { qml_path: ../qml/ }`. Default is unchanged so existing skeleton/example apps need no edit.
- **`SessionAuthenticator` problem+json on entry-point path.** `onAuthenticationFailure` already returned RFC 7807 `application/problem+json` for *bad-token* requests, but Symfony's default entry point fired for *no-token* requests — yielding a Form-flavoured 302/401 instead. Implemented `AuthenticationEntryPointInterface::start`, factored the response into a `problemJson()` helper, so QML's RestClient sees one shape regardless of which path the firewall takes. Added test coverage.
- **`CorrelationKeyListener::onTerminate` sub-request guard.** `onRequest` already had `isMainRequest()`, but `onTerminate` cleared unconditionally — so a sub-request finishing mid-controller would wipe the main request's correlation key, causing the matching Mercure echo to lose its `correlationKey` field and the optimistic UI to never reconcile. Defensive: real-world impact is low (FrankenPHP worker mode does not currently emit sub-requests), but cheap to fix and the obvious correctness bug.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
### v0.2.0 — next minor
Pulls in the originally-Phase-3/5-deferred items that don't need new operational dependencies, plus the smaller §12 risks **and** the public-API / DX items surfaced by the post-v0.1.2 audit. Cross-platform packaging is parked at v0.9.0 — the operational lift (self-hosted runners + platform certs) is too big to fold into the next minor and Linux remains the primary target until the framework's surface stabilises.
**Shipped on `dev` toward v0.2.0** (audit-driven cleanup batch — see CHANGELOG `[Unreleased]` for full notes):
-`PublisherInterface` / `ModelPublisherInterface` / `CorrelationContextInterface`. Concrete classes implement them; internal typehints switched over.
-`BridgeOp` enum (`Upsert` / `Delete` / `Replace` / `Event`). `ModelPublisher::publishEntityChange()` now takes `BridgeOp` instead of `string` (pre-1.0 SemVer break).
-`BridgeBundleInfo` VO. `HealthController` constructor-injects this instead of `PublisherInterface` as the deep-load canary; `/healthz` `bundle` field reports `PhpQml\Bridge\BridgeBundle`, plus a new `name` field (`php-qml/bridge`).
-`Maker\Support\NameInput::askOrFail()` + `Maker\Support\Naming::camelTo()`. The duplicated name-prompt closure and camel-conversion regex collapsed into single call sites.
-`make:bridge:resource --with-dto` opt-in. Generates `Create<Name>Dto` + `Update<Name>Dto` and rewrites controller actions to `#[MapRequestPayload]` dispatch — closes the input-validation gap (malformed JSON → 400 problem+json automatically; no more `if (isset($data['title']))` boilerplate). Skeleton + example/todo composer.json pull `symfony/validator`. Snapshot test covers both modes.
**Still open for v0.2.0** (PLAN.md notes preserved below):
- **Generated controller `findOr404` boilerplate.** `update()` and `delete()` still inline the find-or-404 problem+json response. Audit suggested factoring a private helper or migrating to Symfony's `#[MapEntity]` attribute. MapEntity changes the 404 response shape away from problem+json unless framework-level error config is updated; a private helper is net-zero on lines. Parking until either (a) we bake skeleton-level RFC 7807 error wiring (so MapEntity preserves shape), or (b) we flip `--with-dto` to default-on and the legacy template's polish becomes irrelevant.
- **Flip `--with-dto` to default-on.** Once snapshot-test churn settles and the DTO templates have surface-feedback, make it the default and gate `--no-dto` for users who want the legacy shape.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
**Makers + reactive types (Phase 3.x deferred):**
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
- **`make:bridge:event` maker.** Generate an event class + listener stub for app-side domain events.
- **`make:bridge:read-model` maker.** Generate a read-only projection (one or more entities → one denormalised view).
- **`ReactiveObject` cursor pagination.** Bring single-entity model up to par with `ReactiveListModel`'s pagination.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
**Testing (Phase 3/5 deferred + §12 testing-strategy row):**
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
- **`qmltestrunner`-driven QML unit tests.** Wires into the `quality` job alongside qmllint.
- **End-to-end UI test (Squish or Qt Test).** Was §12's deferral; bridge-integration covers IPC, this would catch UI-only regressions.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
**Operations (§12):**
- **Bundled-mode port negotiation.** `BackendConnection::m_port` is hardcoded to 8765 with no env override or negotiation, so two php-qml apps installed on the same machine collide on first launch (whichever loses the race goes Offline). Fix: bind a transient `QTcpServer` to `QHostAddress::LocalHost` port 0, grab `serverPort()`, hand it to FrankenPHP via the `PORT` env var. Needs a port-discovery mechanism for tests/perfsmoke that currently hardcode 8765 — likely write the chosen port to a sentinel file under the user data dir on supervisor activation. Surfaced from a v0.1.1 follow-up question; deferred to v0.2.0 because the test/consumer migration is wider than v0.1.x scope.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
- **Pre-migration auto-backup** (§12, *Migrations on schema change*). Supervisor copies `var/data.sqlite` to `var/data.sqlite.<timestamp>.bak` before invoking `doctrine:migrations:migrate`; trims to N most recent.
- **`bridge:export` console command + UI hook** (§12, *Data backup / export*). Lets users copy their data out before machine moves or risky migrations.
- **Periodic auto-update check.** Phase 5 noted this as a polish item but didn't ship; v0.1.0 only has menu-triggered manual checks.
- **Build-time Symfony cache warmup** (§12, *Cold start*). Bake `var/cache/prod` into the AppImage so first launch skips warmup; first-launch supervisor copies it into the user data dir.
- **Native dialogs boundary doc.** §12 noted file pickers / notifications belong on the QML side via Qt — document the boundary and ship a small `Q_INVOKABLE` helper for the common cases.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
### v0.3.0 — later minor
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
Bigger pieces still deferred (each warrants its own minor, not v0.2.0 noise):
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
- **i18n bridge** (§12). Symfony Translator (XLIFF) + Qt Translator (.ts) with a shared locale switch fanning out to both.
- **Persistent log files + rotation** (Phase 5 out-of-scope). Symfony monolog wiring + a Qt-host log file with rotation. The dev console stays for live tails.
### v0.9.0 — cross-platform packaging (release-candidate milestone)
Locks down the cross-platform story before promoting to v1.0.0. Held until v0.9.0 (rather than v0.2.0) because each item carries operational prerequisites (paid certs, self-hosted runners, platform-specific notarisation pipelines) that are easier to absorb in a single concentrated push than to drip-feed across minors. **Linux AppImage stays the only packaged target through v0.2.0/v0.3.0 and the v1.0.0 prep work** — alternate Linux channels (Flathub, Snap) and the macOS/Windows ports all land here.
- **macOS packaging** (was Phase 4b). `.app` bundle + `.dmg` + Sparkle 2 + notarization. Prerequisites: self-hosted macOS runner, Apple Developer cert ($99/yr), notarisation toolchain.
- **Windows packaging** (was Phase 4c). NSIS installer + WinSparkle + Authenticode signing. Prerequisites: self-hosted Windows runner, code-signing cert (EV preferred to dodge SmartScreen reputation warm-up).
- **Multi-arch builds** (§12). Linux ARM64, Windows ARM, macOS universal (arm64 + x86_64). Each adds a CI matrix dimension. Lands here rather than v0.3.0 because the runner / cert prerequisites overlap with the macOS / Windows ports above — fan-out is one operational push, not three.
- **Flathub / Snap packaging** (§12). Alternate Linux channels for better discoverability than AppImage. Each adds its own packaging surface (Flatpak manifest + Flathub PR review; snapcraft.yaml + Snap Store listing).
- **Per-platform first-launch UX** (§11, *Distribution UX*). Gatekeeper / SmartScreen / AV-vendor pre-submissions, file-association docs, App Store path decisions.
- **Composer `create-project` package** (Phase 5 out-of-scope). Publish `php-qml/skeleton` as a composer template so `composer create-project php-qml/skeleton my-app` works. Bash `bin/php-qml-init` stays for curl-bootstrap. Lands at v0.9.0 alongside the OS-installer paths so all the "how does a new user get the framework" channels stabilise together.
- **Telemetry + crash reporting** (§12). Opt-in only, off by default; PHP-side Sentry for backend errors + a per-platform crash-dump pipeline for the Qt host (each OS does this differently — fits the cross-platform-packaging milestone). Plumbing settled before v1.0.0 even if no default endpoint ships.
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
### v1.0.0 — when
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
When the public API (Symfony bundle services + attributes, Qt module C++ types + QML elements, maker output) is stable enough to commit to compatibility for. Items still in flux that should settle before this:
plan: condense + switch to version-based planning post-v0.1.0 §13 was 332 lines of phase-by-phase implementation history (Phase 0 spike → Phase 5 closure). All of that is now redundant with CHANGELOG.md (per-version summary) and `git log` (per-commit detail); keeping it in PLAN.md was duplication that would only rot. Replaced with a Versions section organised around SemVer: - v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page). - v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap, bin/php-qml-init parity with the AppImage fixes that landed in examples/todo, bundled-mode integration test, Caddyfile fmt). - v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c), deferred makers (event/read-model), ReactiveObject pagination, qmltestrunner + end-to-end UI test, pre-migration auto-backup, bridge:export, periodic auto-update check, build-time cache warmup, native-dialogs boundary doc. - v0.3.0 — bigger pieces: i18n bridge, persistent log files + rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub / Snap, Composer create-project package. - v1.0.0 — when public API stabilises: auth model, Mercure storage, AppImage relinkability, telemetry plumbing, security audit, FrankenPHP-as-library evaluation. Every deferred item from the original §11/§12/Phase 3-5 deferral lists got a version target — no orphans. Top-of-file status line and "Where else to look" pointer added so readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and PLAN.md keeps why + what's next. Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:48:19 +02:00
- **Auth model** (§12). Per-session bearer is fine for local-only; revisit if Mercure ever leaves loopback.
- **Mercure storage strategy.** In-memory works for bundled mode now; document or switch if persistence is needed.
- **AppImage relinkability** (§12, Qt LGPL row). Document and test the user-side relink procedure end-to-end.
- **Security model audit** (§12). Caddyfile generation hardened against `0.0.0.0` binding; loopback-only enforcement audited end-to-end.
- **FrankenPHP-as-library evaluation** (§12 — future optimisation). CGo-embed FrankenPHP into the Qt host as a single process. Subprocess model stays the default; this is a perf optimisation only if measurements warrant.