de4a14da363ba51da1c8af2d0196f089f62f0c4b
92 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de4a14da36 |
v0.2.0 (13/N): qmltestrunner harness + CI wiring + close out v0.2.0 plan
Closes the testing-strategy row of PLAN.md §13 v0.2.0 and parks the
two remaining items with rationales.
Shipped:
- framework/qml/tests/{CMakeLists.txt, main.cpp, tst_smoke.qml}
Qt Quick Test scaffold: QUICK_TEST_MAIN bootstrap + one smoke test
proving the harness loads. New tests land as tst_<feature>.qml in
the same dir; qmltestrunner auto-discovers them. Built only when
-DBUILD_TESTING=ON (production AppImages stay clean).
- skeleton + example/todo Makefiles: `make qmltest` target invokes
the configure → build → ctest dance. `make quality` now depends
on qmltest.
- .gitea/workflows/ci.yml: `QML unit tests` step after qmllint in
the Quality job. Out-of-tree build dir (build-tests) so the
CTest run doesn't pollute the cached release build.
Verified locally: configure + build + ctest pass, both smoke
assertions pass, runs in 0.5s.
Closed in PLAN.md §13 v0.2.0 with rationale (no code change):
- Build-time Symfony cache warmup → moved to v0.3.0. The obvious
approach (cache:warmup at build, copy at first launch) doesn't
save any time because Symfony bakes absolute kernel.project_dir
into the compiled cache, and the AppImage's FUSE mount path
changes every launch — every cached path is stale on launch N+1.
Doing it properly requires virtualising getProjectDir(), symlink
fix-up, multi-app namespacing — its own minor's worth of design.
- ReactiveObject cursor pagination → closed N/A. ReactiveObject
already has pending / invoke() / Idempotency-Key correlation /
version-gap detection at parity with ReactiveListModel; the only
feature it lacks is *pagination*, which is meaningless for a
single-entity model.
That fully closes the v0.2.0 plan as documented. Remaining v0.2.0
items in PLAN.md §13 are the audit-ends already shipped earlier in
the cycle (interfaces / BridgeOp / BridgeBundleInfo / Maker DRY /
--with-dto / port negotiation / pre-migration backup / bridge:export
/ periodic auto-update / native-dialogs doc / event maker /
read-model maker / qmltestrunner) plus the two parked items
documented above. Ready to tag when the user gives the word.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6939278857 |
v0.2.0 (12/N): bundled-mode port negotiation
PLAN.md §13 v0.2.0 *Bundled-mode port negotiation*. Hardcoded
m_port = 8765 used to fail loudly only when a second php-qml app
launched on the same machine — whichever lost the bind race went
Offline with no recovery path.
Fix:
- Bind a transient QTcpServer to QHostAddress::LocalHost port 0,
read serverPort(), close. Linux's ephemeral-port allocator
doesn't immediately reassign the closed port, and FrankenPHP's
bind happens within milliseconds inside spawnChild() — small
TOCTOU window in theory, fail-loud in practice if it ever races.
- BRIDGE_PORT env override pins the port for tests / dev
(bundled-supervisor.sh and perfsmoke.sh now both export it
instead of the previous PERF_BACKEND_PORT-only knob).
- writePortSentinel() drops the chosen port to
$XDG_DATA_HOME/<app>/var/bridge.port so external tools can read
the runtime address without parsing Qt's log output.
Caddyfile already supported {$PORT:8765} env interpolation, so
no template churn. MERCURE_URL is computed from m_url which is
re-derived from the chosen port — no .env changes needed for
bundled mode (dev mode .env still references :8765 since the
developer controls their own frankenphp invocation).
bundled-supervisor.sh integration test gained a sentinel-file
assertion: after first launch, $USER_DATA/var/bridge.port must
exist and contain BRIDGE_PORT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
82de6cae36 |
v0.2.0 (11/N): periodic auto-update check
PLAN.md §11 *Auto-update* described "check on launch and once per N hours; offer install on next restart, never auto-restart". v0.1.0 shipped checkForUpdates() and applyUpdate() Q_INVOKABLEs but only manual triggers — no scheduling. This wires the polling. armAutoUpdateOnFirstOnline() runs from setState(Online) in bundled mode: - A QTimer::singleShot fires checkForUpdates() 10 s after the first Online transition (lets cold-boot bandwidth/CPU settle first). - A recurring QTimer fires checkForUpdates() every 6 hours after that. - One-shot guard via m_autoUpdateArmed so reconnect cycles don't re-arm the timers. Dev mode skips entirely (developers don't want their `make dev` workflow polling AppImageUpdate). Env-var knobs: - BRIDGE_AUTO_UPDATE_DISABLE=1 — skip entirely (respect-opt-out baseline; user-facing settings UI can layer on top later). - BRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes> — override the period (handy for testing or shorter intervals on power-user opt-in). The actual install (apply + restart) stays manual — never auto- restart, per PLAN.md's UX rule. checkForUpdates emits updatesAvailable(); QML decides whether/when to show a banner and call applyUpdate(). Verified locally with QT_LOGGING_RULES=phpqml.bridge.bundled.info=true: "phpqml.bridge.bundled: auto-update armed: launch check in 10000 ms, period 360 min" appears in the host log after the BackendConnection probe sees /healthz=200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
da097051ca |
v0.2.0 (10/N): bridge:export console command + QML hook
PLAN.md §12 *Data backup / export* called for "a bridge:export console command and a UI hook for backup-to-file from v1". Shipping both now so the v0.2.0 surface has the data-portability story end-to-end: PHP side — `bin/console bridge:export <destination>`: - Reads the source path from DATABASE_URL so it works in dev mode (developer's source-tree var/data.sqlite) and bundled mode (user data dir SQLite) without environment-aware logic. - SQLite-only by design (PLAN.md §6 — single-instance SQLite-first); emits a clear error for non-sqlite:// URLs rather than pretending to support drivers that need driver-specific dump tooling. - Overwrites the destination if it exists (the FileDialog or shell redirect that produced the path has already confirmed). - 4 unit tests: happy path, non-SQLite URL, missing source, overwrite. Test count 24 → 28. QML side — Q_INVOKABLE BackendConnection.exportDatabase(path): - Bundled mode only; dev mode emits databaseExportFailed and returns false (developers own their SQLite directly). - Accepts both filesystem paths and `file://` URLs (FileDialog results). - Returns synchronously with bool but also emits async signals databaseExported(dst) / databaseExportFailed(reason) so QML can drive a snackbar / log without polling the return value. - Removes any existing destination first (QFile::copy refuses to overwrite); the picker has already confirmed the choice. Drive-by: parse_url() rejects sqlite:///abs/path on PHP 8.5+ (the host-less triple-slash trips its strictness). Switched to a prefix-strip — Doctrine DBAL only emits two URL shapes for SQLite anyway (sqlite:///abs and sqlite://relative). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e0241bad64 |
v0.2.0 (9/N): pre-migration auto-backup of var/data.sqlite
PLAN.md §12 *Migrations on schema change* flagged this as a v1.0 prereq. SQLite has no transactional DDL — a half-applied migration can corrupt the user's data with no rollback path. Cheapest defence is a copy-aside before each migrate. backupDatabase() runs at the head of runMigrations() in bundled mode: - skipped on first launch (no data.sqlite yet) - copies var/data.sqlite to var/data.sqlite.<unix-timestamp>.bak - trims to kMaxDatabaseBackups=5 most recent (mtime sort, oldest go first) - copy failure logs a warning and continues; a missing safety-net is not a reason to refuse to boot Dev mode is unaffected — developers own their var/data.sqlite lifecycle and don't want a backup written every time `make dev` restarts. Integration test: bundled-supervisor.sh gained an assertion after the 2nd-launch /healthz check that at least one data.sqlite.*.bak file appears under the user data dir. Verified locally — backup landed at the expected path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1d014ae3b7 |
v0.2.0 (8/N): make:bridge:read-model maker
Implements PLAN.md §8's fourth makers-table row. Read-models are
server-side projections — joined fetches, aggregates, denormalised
views — that QML reads without going through a writable
#[BridgeResource]. The maker emits:
- src/ReadModel/<Name>ReadModel.php — query service stub injecting
EntityManagerInterface; user fills query() with DQL / QueryBuilder
/ raw SQL as fits.
- src/Controller/<Name>Controller.php — single GET handler at
/api/<kebab-plural>, just normalises the read-model output to JSON.
- {qml_path}/<Name>List.qml — ReactiveListModel bound to the route,
deliberately no Mercure topic.
The "no topic" choice is the design call worth documenting: read-models
are queries, not reactive resources, and pretending otherwise would
either auto-publish stale aggregates on every entity change or require
the user to invent invalidation logic in the listener. Better: pair
the read-model with `make:bridge:event` and call refresh() from the
QML event-handler when the underlying data really changes.
Naming convention: kebab-PLURAL routes (`/api/todo-summaries`) for
consistency with REST list semantics; resource path stays singular
under `src/ReadModel/`.
Wired into services.yaml's when@dev block. Three new snapshot
baselines (TodoSummaryReadModel.php / TodoSummaryController.php /
TodoSummaryList.qml) plus runner extension. All 14 maker outputs
verify on the committed state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
00a64c5871 |
v0.2.0 (7/N): make:bridge:event maker
Implements PLAN.md §8's third makers-table row. Single-command path
from a PHP domain event to a QML signal-handler:
- src/Event/<Name>Event.php — readonly value object stub
- src/EventSubscriber/<Name>Subscriber.php — listens to the event,
republishes via PublisherInterface on app://event/<kebab-name>
with op:"event"
- {qml_path}/<Name>EventHandler.qml — MercureClient bound to the
topic, re-emits the envelope's data as a typed signal
Stub uses an `array $payload` field so the user can substitute typed
properties for whatever shape they need. Subscriber example uses the
PublisherInterface contract from chunk 1; QML stub uses MercureClient
+ BackendConnection both already shipping.
Wired into services.yaml's when@dev block (autoconfigure picks up
maker.command tag, same pattern as existing BridgeResourceMaker /
BridgeWindowMaker). Three new snapshot baselines plus a snapshot
runner extension exercising the new maker against the same Todo /
TodoCompleted naming the existing baselines use.
End-to-end verified locally: maker output matches baselines, dev
container compiles, listing make:bridge:* shows the new command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
91f4d619fc |
v0.2.0 (6/N): docs/native-dialogs.md — boundary doc + Qt.labs.platform examples
PLAN.md §12 noted "Native dialogs (file pickers, notifications) — where do they live?" as an open question with the bias "QML side via Qt". That bias was never written up; without the doc, a Symfony developer new to Qt would reasonably reach for "POST /api/show-dialog" or roll a custom QML "FileDialog" using Window + ListView. Both are wrong. The doc: - States the boundary plainly (native UI = QML side, never PHP) plus the architectural reason (PHP's process can't reach the user's window manager; Qt's can and already wraps every platform's native API). - Walks through Qt.labs.platform.FileDialog / MessageDialog / SystemTrayIcon / StandardPaths with copy-pasteable examples so apps don't need to discover Qt.labs the hard way. - Explains the trigger-vs-effect split: user-initiated confirmations open from the QML handler that fired the action; server-side events route through Mercure and let QML decide how to surface them (toast / dialog / tray notification). Anti-pattern callouts: don't dispatch dialogs from Doctrine listeners, don't add HTTP endpoints whose only job is to trigger UI side-effects, don't roll a custom QML file browser. Notifications caveat: Qt.labs.platform.SystemTrayIcon::showMessage covers the common case but routes through the tray. Richer notifications (action buttons, replies) need platform-specific code and are deferred — flagged in-doc. PLAN.md §13 also mentioned "ship a small Q_INVOKABLE helper for the common cases". Skipped: every common case Qt.labs.platform already covers, and a wrapper would just shadow upstream's API. If a future need surfaces a real gap (XDG portal notifications without tray, say), that's the time to add framework-side code; the doc will point at it. No code changes; doc-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a589b1c30d |
plan: move multi-arch + composer create-project from v0.3.0 to v0.9.0
Both items naturally cluster with the v0.9.0 cross-platform packaging milestone: - Multi-arch builds share runner + cert prerequisites with the macOS / Windows ports already at v0.9.0; doing them as one operational push is cheaper than fanning out across minors. - Composer create-project is the "how does a new user get the framework" PHP-side channel — settling it alongside the OS-installer paths means all the entry points stabilise together for v1.0.0. v0.3.0 keeps i18n + persistent log files (both standalone work that doesn't need v0.9.0's operational lift). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f2d931e0a5 |
v0.2.0 (5/N): close audit sweep — BridgeOp contract test + PLAN.md status
The audit's substantive items shipped in chunks 1–4. Two remaining loose ends inspected and parked: - Generated controller findOr404 boilerplate. MapEntity changes the 404 response shape away from problem+json unless framework-level RFC 7807 error config is updated; a private helper is net-zero on lines. Parking until either (a) skeleton-level RFC 7807 error wiring, or (b) --with-dto flipping to default-on and the legacy template's polish becoming irrelevant. - ModelPublisher::extractId reflection branch. Looks dead because every maker-output entity has getId(), but it remains a safety net for hand-written entities that don't. Keeping. This commit ships: - BridgeOpTest — locks the enum case values against accidental rename. Every case value is a documented wire-format token QML clients hardcode, so renaming a `value` is a wire-protocol break and this fails the build before it ships. - PLAN.md §13 v0.2.0 status block with what's shipped on dev (interfaces / BridgeOp / BridgeBundleInfo / Maker DRY / --with-dto) and what's still open (findOr404 polish, --with-dto default flip). Test count 23 → 24, all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5498c3c91e |
v0.2.0 (4/N): make:bridge:resource --with-dto + symfony/validator
Closes the input-validation gap that was the audit's headline finding.
The legacy generated controller's `if (isset($data['title']))…` body
accepted any JSON: empty title slipped through, malformed JSON got
swallowed by `?? []`, wrong types were silently coerced via casts.
The --with-dto flag generates:
- src/Dto/Create<Name>Dto.php — readonly DTO with #[Assert\NotBlank]
on title and #[Assert\Length(max: 255)]
- src/Dto/Update<Name>Dto.php — same DTO with all fields nullable
so PATCH callers send only what changed
- src/Controller/<Name>Controller.php — same shape as the legacy
controller but actions dispatch via #[MapRequestPayload]
Validation failures (missing required field, wrong type, malformed
JSON, oversize string) become RFC 7807 application/problem+json
automatically — Symfony's RequestPayloadValueResolver does the work.
No `if-isset` boilerplate, no silent coercion.
Behaviour:
- --with-dto is opt-in; legacy template still ships unchanged
- audit suggests flipping to default-on once stable; that's a
follow-up
- maker fails loud (composer require hint) if symfony/validator
isn't autoloadable
- skeleton + example/todo composer.json pull symfony/validator so
scaffolded apps work out of the box
Snapshot test exercises both modes (legacy + --with-dto). New
baselines TodoControllerWithDto.php / CreateTodoDto.php /
UpdateTodoDto.php under tests/snapshot/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0710d81783 |
v0.2.0 (3/N): extract Maker shared helpers (NameInput, Naming)
DRY pass identified by the post-v0.1.2 audit: every make:bridge:* maker re-implemented the same "prompt, trim, ucfirst, reject empty" closure in interact(), and the camel-case-to-separator regex was duplicated between BridgeResourceMaker (`_`-joined route plurals) and BridgeCommandMaker (`-`-joined kebab slugs). Two helpers under PhpQml\Bridge\Maker\Support: - NameInput::askOrFail() — replaces 3× inline closures - Naming::camelTo($name, $separator) — replaces 2× inline regexes All 3 makers now go through the helpers; behaviour preserved (maker snapshot test still passes — generated Todo / TodoController / TodoList / MarkAllDoneController / TodoWindow byte-identical to the v0.1.2 baselines). NamingTest covers the documented cases plus a regression case for acronyms (HTTPClient → h-t-t-p-client; the regex splits at every internal capital, which is correct for the route-slug use case). Test count 17 → 23, all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0cca0785c0 |
v0.2.0 (2/N): HealthController deep-load canary → BridgeBundleInfo VO
Decouples /healthz from the publisher contract. v0.1.1 wired
HealthController to constructor-inject Publisher purely as a "is the
bundle resolvable" probe — that worked but cemented the publisher's
API as a readiness-test dependency, which was awkward once
PublisherInterface landed in v0.2.0 chunk 1.
Replace with BridgeBundleInfo: a tiny readonly VO carrying the
bundle's name + class FQCN. HealthController depends on this instead.
Same deep-load semantics (broken bundle → can't construct the VO →
500 on /healthz), no leaky publisher dep.
/healthz response shape:
- `bundle`: was `PhpQml\\Bridge\\Publisher`,
now `PhpQml\\Bridge\\BridgeBundle`
- `name`: new field, reports `php-qml/bridge`
bundled-supervisor.sh's grep updated to match the new canary value
plus an assertion that the new `name` field is present (catches a
botched BridgeBundleInfo wire-up that the bundle-class-name assertion
alone would miss).
Quality + maker snapshot + bundled-supervisor integration test all
pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
56e3d671d9 |
v0.2.0 (1/N): public API surface — interfaces + BridgeOp enum
Establishes the contract layer the rest of v0.2.0 builds on. Pre-1.0
SemVer break: ModelPublisher::publishEntityChange() now takes BridgeOp
instead of a raw string.
Interfaces (Symfony idiom: same namespace as concrete, like HubInterface
next to Hub):
- PublisherInterface — publish(string, array, bool)
- ModelPublisherInterface — publishEntityChange(object, BridgeOp)
- CorrelationContextInterface — set/get/clear
App code should typehint these instead of the concretes so swappable
implementations (offline-buffer publisher, multi-hub fan-out, request-
stamp correlation) remain non-breaking. Concrete classes implement them
unchanged; autowire continues to inject the implementations transparently.
BridgeOp: PHP 8.1 string-backed enum with cases Upsert / Delete /
Replace / Event matching PLAN.md §4's envelope `op` wire format.
Internal call sites updated; tests use the cases directly.
Switched typehints:
- ModelPublisher ctor: PublisherInterface + CorrelationContextInterface
- DoctrineBridgeListener ctor: ModelPublisherInterface
- HealthController ctor: PublisherInterface (still emits `Publisher`
as bundle canary value — `::class` resolves to the concrete class
name regardless of typehint, so bundled-supervisor.sh's grep stays
green)
- skeleton PingController ctor: PublisherInterface (canonical app
pattern — example/todo has no Publisher consumer to update)
Drive-by: removed deprecated setAccessible(true) call in
ModelPublisher::extractId — PHP 8.1+ allows reflection without it.
PHPStan + cs-fixer + PHPUnit (17/17) + maker snapshot all pass; dev
container compiles in the example app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4d6b9fde2c |
bundled: disconnect child signals before terminate() to prevent restart-during-shutdown
teardownChild called terminate() then waitForFinished(2000), then disconnected the QProcess signals. But waitForFinished pumps a local event loop — when frankenphp exited inside that wait, QProcess::finished fired synchronously, ran onChildFinished as the crash-supervisor's restart path, and spawned a brand-new frankenphp child during shutdown. That child's QProcess was then destroyed mid-spawn during stack unwinding, producing the "QProcess: Destroyed while process is still running" warning the bundled-supervisor.sh test catches. Fix: disconnect first, then terminate. Severing signals before the wait turns terminate() into the synchronous reap it should always have been; onChildFinished can't run for a process we're explicitly tearing down. Local integration test passes clean — both the cache-baked-mount-path relaunch and the graceful-shutdown assertion go through without the warning or any orphan frankenphp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>v0.1.2 |
||
|
|
ed4db00a62 |
bundled: relay SIGTERM/SIGINT into Qt's quit() via self-pipe
The aboutToQuit-based teardown wired in v0.1.2 only fires when something calls QCoreApplication::quit() — typically a window close. `kill -TERM` to the host process bypasses Qt entirely (no default SIGTERM handler), so teardownChild never ran on signal-driven shutdown. Local tests passed on lucky timing because PR_SET_PDEATHSIG made the kernel SIGTERM frankenphp once the host died, but the timing was racy and surfaced on CI as "frankenphp child PID outlived the host (supervisor didn't clean up)". Fix: install a SIGTERM/SIGINT handler in BackendConnection that uses the self-pipe pattern — the C signal handler writes one byte (the only truly async-signal-safe primitive), a QSocketNotifier on the read end calls QCoreApplication::quit() in the main thread, and aboutToQuit runs the existing teardownChild before app.exec() returns. The host now exits cleanly under `kill -TERM` from service managers, launchers, and the test harness. Also bumps the bundled-supervisor.sh first-relaunch grace from 2s to 3s — teardownChild itself waits up to 2s for frankenphp to finish after SIGTERM, so the host needs ~2.x seconds to exit. The graceful-shutdown step further down was already at 3s. No public-API change; production-correctness fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ee68561bae |
bridge: restore autoconfigure inside the when@dev maker block
The previous fix (
|
||
|
|
c78d471368 |
bridge: scope maker qml_path injection to when@dev
The v0.1.2 build broke the staging-symfony container compile: explicit top-level `services.PhpQml\Bridge\Maker\BridgeResourceMaker:` blocks forced ResolveClassPass to load AbstractMaker, which is excluded by `composer install --no-dev`. The glob alone tolerates the missing parent (FileLoader silently drops classes that fail class_exists), but explicit blocks bypass that check. Fix: keep v0.1.1's plain glob untouched; move the qml_path argument overrides into a `when@dev:` envelope that prod/no-dev compiles never touch. Dev builds still resolve the bound parameter (verified via debug:container — Argument value `../qml/`); prod cache:clear no longer aborts on missing AbstractMaker; integration-bundled passes end-to-end locally. No public-API change; release CI fix only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8b2fc4dd06 |
release prep v0.1.2: collapse audit fixes into v0.1.2
The previous commit ( |
||
|
|
0cceefc890 |
v0.1.3: audit-driven non-breaking fixes
Three bugs surfaced by the post-v0.1.2 architecture audit: - bridge.qml_path is now actually configurable. BridgeBundle::configure defines the qml_path scalar node (default ../qml/); loadExtension exposes it as the bridge.qml_path container parameter; services.yaml binds it into BridgeResourceMaker + BridgeWindowMaker. Apps override with `config/packages/bridge.yaml`. The existing maker docstrings claimed this worked already — they lied; now they don't. - SessionAuthenticator implements AuthenticationEntryPointInterface and routes the no-token entry-point path through the same problem+json helper as onAuthenticationFailure, so QML's RestClient sees one error shape regardless of which firewall path was taken. Test added. - CorrelationKeyListener::onTerminate guards on isMainRequest() now, matching onRequest's existing guard. No user-visible impact in worker mode (no sub-requests emitted), but the asymmetry was a defensive bug that would corrupt optimistic-update reconciliation. PLAN.md §13 gains a v0.1.3 section + folds the audit's API-surface items (PublisherInterface / ModelPublisherInterface / BridgeOp enum / maker DRY / DTO-shaped scaffold) into v0.2.0. CHANGELOG.md mirrors. PHPStan + cs-fixer + PHPUnit (17/17) + maker snapshot tests all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9f524104b9 |
plan: fix version-section ordering + bring v0.1.1 entry up to date
The v0.1.2 section had been prepended above v0.1.1 instead of inserted
after it; reads strictly chronologically now (v0.1.0 → v0.1.1 → v0.1.2
→ v0.2.0 → …).
Also brought v0.1.1's heading and bullet list up to current reality:
- heading: "ready to tag" → "shipped 2026-05-03" (it was tagged
earlier today)
- added the cache-wipe-on-bundled-launch fix, which actually landed
in v0.1.1 (rotated into the tag) but was missing from PLAN.md's
summary (CHANGELOG already had it)
Top-of-file status line: "v0.1.1 ready to tag" → "v0.1.0 + v0.1.1
shipped 2026-05-03; v0.1.2 in progress on dev".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f132c3c9b6 |
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>
|
||
|
|
597e74edcf |
bundled: wipe Symfony cache on every launch — mount path bakes into cache
Reproduces with the v0.1.1 AppImage on the second launch (same user
data dir, fresh AppImage mount):
phpqml.bridge.bundled: symfony: "/tmp/.mount_Todo-xllnOHH/..."
Cannot load migrations from "/tmp/.mount_Todo-xDBkOfG/.../migrations"
^^^^^^^
stale path from PREVIOUS launch's cache
Symfony compiles `kernel.project_dir` (an absolute path) into its
cached container under var/cache/. We redirect var/cache into the
user data dir for read-only-mount survival (v0.1.0 fix), but the
*content* of that cache references the mount path that was active
when the cache was built. Next launch gets a different
/tmp/.mount_<random>; the cached refs are stale; first
project_dir-sensitive lookup blows up (doctrine migrations was the
canary; would also surface as misrouted assets, broken Twig template
paths, etc.).
Fix: BackendConnection::initBundledMode does
QDir(cacheDir).removeRecursively() right after creating the dirs but
before runMigrations spawns the doctrine subprocess. Symfony rebuilds
the cache against the current mount on every launch. Cost: ~1-2s of
warmup per cold start.
Permanent fix is build-time cache warmup (ship the prod cache inside
the AppImage, copy to user data dir on first launch, no per-launch
warmup) — already tracked as a v0.2.0 item in PLAN.md §13. v0.1.1
takes the simpler always-wipe approach since it's bugfix-class.
Regression guard: examples/todo/tests/bundled-supervisor.sh gains a
"2nd launch from fresh staging" step that tears down the first host,
re-stages a fresh fake AppImage layout (different /tmp dir = different
"mount path" from BackendConnection's perspective), and asserts
/healthz comes back up. Without the cache wipe, that step would fail
exactly the way doctrine did in the user's report.
Verified locally:
- bundled-supervisor.sh passes (incl. 2nd-launch step)
- Real AppImage: two consecutive launches both reach
"phpqml.bridge.bundled: migrations OK" + frankenphp spawn
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.1
|
||
|
|
1c231b1bac |
Release v0.1.1: fill CHANGELOG date
[0.1.1] — TBD → 2026-05-03 immediately before tagging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
06b2289ed3 |
release prep v0.1.1: CHANGELOG entry + PLAN.md status + port-negotiation note
CHANGELOG.md: new [0.1.1] section with Fixed (HealthController deep-load, Caddyfile fmt) + Added (bundled-mode supervisor test, skeleton AppImage parity) + a Notes line acknowledging the port-collision bug deferred to v0.2.0. Date stays TBD until tag push. Compare/tag link refs updated. PLAN.md: v0.1.1 section flipped from "open follow-ups" to "ready to tag" with each item describing what shipped (handy for the release notes pass). v0.2.0 section gains an explicit "Bundled-mode port negotiation" entry under Operations — the port-collision bug surfaced during v0.1.1 prep, but the fix touches every consumer that hardcodes 8765 (perfsmoke, the new bundled-supervisor test) so it's wider than v0.1.x scope. Status line at the head of the file bumped to "v0.1.1 ready to tag". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
341bcacafe |
skeleton: bring AppImage parity, scaffolded apps inherit the packaging flow
The v0.1.0 shakedown fixes for AppImage assembly (path-repo
symlink:false sed, writable-cache redirect) all landed in
examples/todo. The skeleton — which is what bin/php-qml-init copies
when scaffolding a new app — had no `appimage` target at all, so every
scaffolded app would have to either copy the example's Makefile by
hand or re-discover the same shakedown bugs.
Brings parity:
- framework/skeleton/Makefile gains `staging-symfony` and `appimage`
targets, mirroring the example's. Two new variables (BUNDLE_SRC,
PACKAGING) parameterise the framework-tree paths so bin/php-qml-init
can rewrite them at scaffold time without sed-touching the recipe.
- framework/skeleton/packaging/skeleton.{desktop,png} added — minimum
surface for the AppImage assembly to succeed without the user
needing to author them.
- framework/skeleton/Makefile's staging-symfony recipe handles both
relative (framework default `../../php`) and absolute (post-scaffold)
BUNDLE_SRC values via a case statement.
- bin/php-qml-init renames packaging/skeleton.* → packaging/$NAME.*,
rewrites the .desktop file's Name/Exec/Icon, and updates the
Makefile's --app-name / --output / --desktop / --icon flags +
BUNDLE_SRC + PACKAGING variables. For --vendor mode, framework's
packaging/linux/ is also vendored to .bridge-packaging/ alongside
the existing .bridge/ + .bridge-qml/.
Verified by scaffolding both modes:
- non-vendored: BUNDLE_SRC + PACKAGING absolute paths
- --vendor: BUNDLE_SRC=../.bridge, PACKAGING=.bridge-packaging,
.bridge-packaging/ contains build-appimage.sh
Skeleton's `make quality` still green; staging-symfony works locally
(vendor/php-qml/bridge resolves to a real directory, not a symlink).
Closes the v0.1.1 follow-up "bin/php-qml-init parity" tracked in
PLAN.md §13.
Bundled drive-by: docs/makers.md picked up two markdownlint auto-fixes
(blank lines around lists) when the IDE saved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
813b064cc1 |
test: bundled-mode supervisor integration test (faked AppImage layout)
Stages a fake AppImage layout in /tmp without a real .AppImage build:
$ROOT/usr/bin/<app> — copy of the host binary
$ROOT/usr/bin/frankenphp — symlink to system frankenphp
$ROOT/usr/share/<app>/symfony — staged --no-dev composer copy
$ROOT/usr/share/<app>/Caddyfile
The staged Symfony tree is `chmod -R a-w` to actually exercise the
read-only-mount cache/log redirect (Kernel::getCacheDir +
APP_CACHE_DIR override) — without the override, Symfony would fail
to mkdir var/cache/prod and migrations would error out.
Then runs the host with BRIDGE_URL unset (forces bundled mode), polls
/healthz, and asserts:
- status=ok + bundle="PhpQml\Bridge\Publisher" — proves the
HealthController deep-load (predecessor commit) actually
autowired Publisher, i.e. BridgeBundle is reachable.
- User data dir's var/cache exists — APP_CACHE_DIR override fired.
- Staged tree's var/cache/prod is empty — Symfony didn't write into
the read-only mount.
Together this catches every v0.1.0 shakedown bug in CI:
- doubled bin/frankenphp path (resolveFrankenphpBin)
- composer path-repo symlink dangling (staging-symfony's symlink:false sed)
- read-only mount cache failure (Kernel + supervisor env-vars)
- bundle autoload broken (HealthController canary)
Implementation gotcha (caught during dev): the host binary must be
COPIED into the staged layout, not symlinked. Qt's
applicationDirPath() reads /proc/self/exe which dereferences
symlinks, so a symlinked host would resolve to the original build/
dir and the supervisor would hunt for frankenphp + symfony there
instead of the staged tree. Real AppImages copy the binary, mimicking
that here.
Wiring:
- examples/todo/Makefile: extracted the staging-symfony logic out
of the appimage target into its own staging-symfony target. New
integration-bundled target depends on `build` + `staging-symfony`
and runs tests/bundled-supervisor.sh. quality target now invokes
integration-bundled after the existing dev-mode integration test.
- .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration
test" step right after the dev-mode integration step.
Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked
in PLAN.md §13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7e734fec66 |
healthz: depend on Publisher to force bundle deep-load (perfsmoke gap)
v0.1.0 shipped two bugs that left /healthz returning 200 against a
half-loaded bundle: the path-repo symlink dangling at runtime in the
AppImage (vendor/php-qml/bridge → nonexistent), and the writable
cache-dir bug (Symfony couldn't create var/cache/prod). HealthController
returned a static {status:"ok"} without ever touching any BridgeBundle
service, so perfsmoke + the connection-state probe both passed even
when the bundle's autoload was broken — first sign of trouble was a
500 from /api/todos under real load.
Inject Publisher (the bundle's Mercure-publish wrapper) via constructor
and reference its FQN in the response body. Two effects:
- Symfony's container resolves Publisher when the controller is
instantiated; if the bundle's autoload is broken, the controller
can't even construct, /healthz returns 500.
- The response now includes `bundle: "PhpQml\Bridge\Publisher"` —
proves to perfsmoke + dev console that the canary is live, not a
cached static response.
Connection-state probe semantics unchanged: still 200 = Online,
non-200 = Reconnecting/Offline. Probe interval is 5s — Publisher's
construction is constant-time, no perf concern.
No new public API: /healthz response gained a `bundle` field
(additive, JSON parsers ignore unknown keys); 200 vs 500 boundary is
preserved. No existing consumer broken.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3c027255c8 |
caddyfile: apply caddy fmt — silence "input is not formatted" warning
FrankenPHP logs a warning on every boot:
Caddyfile input is not formatted; run 'caddy fmt --overwrite' to
fix inconsistencies
Cosmetic but clutters the dev console (and the bundled-mode logs).
The actual diff is one blank line in each file: caddy fmt rejects an
empty line between a leading comment and the `{` global-options
block. tests/var/Caddyfile was already clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
be3fecf64e |
plan: move Flathub/Snap to v0.9.0; AppImage stays the only target until then
Same logic as the macOS/Windows + telemetry moves: alternate distribution channels are operational work (Flatpak manifest + Flathub PR review; snapcraft.yaml + Snap Store listing) that fits the cross-platform packaging milestone, not the v0.3.0 grab-bag. Tightened the v0.9.0 framing to make this explicit: AppImage is the only packaged target through v0.2.0, v0.3.0, and the v1.0.0 prep — all packaging churn concentrated into v0.9.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
012733e8f7 |
plan: consolidate telemetry into v0.9.0
Telemetry was split awkwardly: v0.3.0 had "Sentry + opt-in telemetry" (build the pipeline) and v1.0.0 had "Telemetry / crash reporting opt-in plumbing" (settle the API). The cross-platform crash-dump side is per-OS work — Apple Crash Reporter, Windows WER, Linux core dumps all differ — so it naturally rides with the v0.9.0 cross-platform packaging push rather than landing twice. Single v0.9.0 entry now covers both: PHP-side Sentry + per-platform crash-dump pipeline, opt-in only, plumbing settled before v1.0.0 even if no default endpoint ships. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9b31b1f6e7 |
plan: defer macOS + Windows packaging from v0.2.0 to v0.9.0
Each cross-platform target carries operational prerequisites that v0.2.0 isn't ready to absorb (Apple Developer cert + notarisation pipeline + macOS runner; Authenticode cert + SmartScreen reputation warm-up + Windows runner). Folding both into the next minor would either delay v0.2.0 indefinitely or ship a half-done port. Better: keep Linux AppImage as the only packaged target until the framework's public API surface settles, then concentrate the cross-platform push into a single v0.9.0 release-candidate milestone right before v1.0.0. The §11 *Distribution UX* foot-guns (Gatekeeper, SmartScreen, AV pre-submissions, file-association docs) ride along with that milestone. v0.2.0 stays focused on the smaller deferred items (deferred makers, ReactiveObject pagination, qmltestrunner, end-to-end UI test, auto-backup, bridge:export, periodic auto-update, build-time cache warmup, native-dialog boundary doc) — all things a Linux-only contributor can deliver without operational blockers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ec8d25c585 |
release: use public Gitea URL for user-facing artefact links
`github.server_url` on Gitea Actions resolves to the runner's internal Gitea endpoint (e.g. http://gitea:3000) — fine for API calls the runner makes itself, broken for URLs end-user machines have to resolve. Both v0.1.0 user-facing places used it: - latest.json's `url` and `zsync` fields (read by AppImageUpdate on user machines). - The AppImage's embedded `--update-info` ELF section (also read by AppImageUpdate to find the appcast). Result: v0.1.0's latest.json shipped pointing at gitea:3000, which no end-user machine can reach. Fix: add a job-level `PUBLIC_REPO_URL` env var (single source of truth, easy to change if Gitea ever moves) and use it for both artefact-URL composition sites. The release-create + asset-upload API calls keep using `github.server_url`/`api/v1` — those are runner→Gitea internal traffic where the internal URL is correct. Note: v0.1.0's already-uploaded latest.json still has the broken URLs. Either leave it (no auto-update consumers yet) or PATCH the asset out of band; future tags will be correct once this lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b60227e2e1 | removed empty lines at EOF | ||
|
|
f7c1a3e771 |
release: mark v0.* tags as prerelease per SemVer convention
Pre-1.0 releases get the prerelease flag in Gitea — pre-1.0 means
public API may break between minors (SemVer permits this), so these
shouldn't display as stable releases.
Computed from $TAG via `case` so the flag auto-flips to false when
v1.0.0 lands; no further workflow change needed at that point.
case "$TAG" in
v0.*) prerelease=true ;;
*) prerelease=false ;;
esac
Passed to jq via --argjson (not --arg) so it stays a JSON boolean
rather than the string "true" / "false".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
936c1f7e15 |
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>
|
||
|
|
68ee6efefe |
bundled: write Symfony cache + log to user data dir (AppImage is read-only)
Symfony defaults Kernel::getCacheDir/getLogDir to <project>/var/cache
and <project>/var/log. In bundled mode those resolve inside the
AppImage's FUSE mount (/tmp/.mount_<hash>/usr/share/<app>/symfony/) —
read-only. Migrations fail at startup with:
Unable to create the "cache" directory
(/tmp/.mount_<hash>/usr/share/<app>/symfony/var/cache/prod).
…and frankenphp's worker can't warm a cache either, so even after the
binary spawns, the app is in a half-working state (which probably also
explains the persistent Reconnecting banner the user reported — once
migrations fail the supervisor sets Offline; even a successful
re-probe of /healthz wouldn't recover from a half-warm state).
Two-part fix, framework-side seam + app-side override:
1. BackendConnection.cpp (runMigrations + spawnChild): mkdir
<m_dataDir>/var/{cache,log} and pass them as APP_CACHE_DIR /
APP_LOG_DIR env vars. <m_dataDir> resolves to
~/.local/share/<app> via QStandardPaths::AppDataLocation, so
it's user-writable.
2. App Kernel.php (skeleton + todo): override getCacheDir /
getLogDir to honour the env vars. Falls back to parent
behaviour when unset (dev mode keeps writing to var/cache like
normal).
Database file already lives at <m_dataDir>/var/data.sqlite, so the DB
side was fine. Caddy autosaves to ~/.config/caddy and TLS storage to
~/.local/share/caddy — both user-writable. Mercure ran in-memory
mode in earlier logs so no extra storage redirect needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.0
|
||
|
|
43cb716006 |
release: delete existing assets before re-upload (don't accumulate dupes)
Each tag rotation re-runs release.yml. The create-release POST returns 4xx for the existing release, the script falls back to GET on the existing one — and then re-POSTs the same asset names to the upload endpoint. Gitea appends each upload as a new asset rather than replacing, so the release page accumulates Todo-x86_64.AppImage once per rotation, same for .zsync / latest.json / SHA256SUMS. Fix: between getting the release id and the upload loop, list all existing assets and DELETE them first. Single rotation = single set of assets, regardless of how many times release.yml has run for this tag. Release body stays as set on first creation (the GET returns the original). If a future rotation needs to refresh the body too, that would be a separate PATCH on the release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5e8db0980e |
appimage: copy the path-repo bundle into vendor/ instead of symlinking
The Makefile's appimage target ran composer install with the path repo configured as `"symlink": true`. Composer created a symlink at vendor/php-qml/bridge → <BUNDLE_ABS>. rsync into the AppDir preserved the symlink, whose target path doesn't exist on the user's machine. At runtime: Caddy + frankenphp boot fine, /healthz returns 200 (no bundle services touched), but every API request fails with: Warning: include(.../symfony/vendor/composer/../php-qml/bridge/src/ BridgeBundle.php): Failed to open stream: No such file or directory …and the migrations step fails identically on first launch. COMPOSER_MIRROR_PATH_REPOS=1 is the documented env-var lever, but explicit `"symlink": true` in composer.json takes precedence over it (verified the env var alone leaves the symlink in place). Dropping the env var; instead, sed the symlink option to `false` in the staging composer.json, alongside the existing URL rewrite. Composer.json source-of-truth keeps `symlink: true` so dev-mode installs are still hot-reloadable against framework/php source. Only the staging copy used for AppImage assembly is mirrored. Verified locally: `vendor/php-qml/bridge` is now a real directory after composer install; `BridgeBundle.php` exists as a regular file. Note for follow-up (out of scope here): perfsmoke didn't catch this because /healthz doesn't touch any BridgeBundle services. Worth extending perfsmoke to also exercise an actual API endpoint so packaging regressions of this shape fail loudly in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
58a6f7166e |
ci: raise perfsmoke idle-memory budget to 600 MB for xvfb llvmpipe
Observed idle memory on the Gitea act-runner was 434 MB vs the 200 MB
strict baseline. Two things inflate the number under CI:
1. Qt has no GPU under xvfb, so it falls back to Mesa llvmpipe; the
LLVM 20 libs + softpipe rasterizer add ~30-50 MB per process.
2. perfsmoke sums VmRSS across host + descendants, which
double-counts shared library pages (libllvm, libmesa) loaded into
both the Qt host and any frankenphp child workers.
Could fix #2 by switching to PSS (smaps_rollup) accounting, but that's
a bigger change than rotation can absorb here. For now: lift the
budget to 600 MB (3x baseline). Still catches order-of-magnitude
regressions; the strict 200 MB budget remains the bare-metal default
for `make perf`.
PERF_IDLE_MEM_MB: 200 (default) → 600 (CI override)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
76e738afaf |
ci: raise perfsmoke cold-start budget to 10s for shared act-runners
Observed cold start on the Gitea act-runner is ~6s — legitimately: AppImage extract (~0.5s) + xvfb startup (~0.5s) + Qt platform init (~1-2s) + frankenphp spawn + Symfony cold-cache bootstrap (~1.5-2s) + first /healthz roundtrip (~0.5-1s). The previous 4s budget (2x the strict PLAN.md §11 number) was too tight for that environment. PERF_COLD_START_MS: 4000 → 10000 (5x strict baseline) PERF_HEALTHZ_DEADLINE_MS: 8000 → 15000 (room for retry beyond budget) Bundle-size (200 MB) and idle-memory (200 MB) budgets stay strict — those are environment-independent. The strict 2s cold-start baseline also stays for `make perf` runs against bare metal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2f4766c7cb |
bridge: fix doubled bin/ in bundled-mode frankenphp path resolution
resolveFrankenphpBin() returned `applicationDirPath() + "/bin/frankenphp"`, but applicationDirPath() inside an AppImage is already `usr/bin/` — where the host binary itself runs from. The "/bin/" prefix produced the doubled path, e.g.: /tmp/appimage_extracted_<hash>/usr/bin/bin/frankenphp …which doesn't exist. The supervisor logged the lookup failure and kept retrying, eventually hitting the perfsmoke 8s cold-start deadline. build-appimage.sh:148 installs frankenphp at `usr/bin/frankenphp` (sibling of the host binary, per the layout comment at line 18). The fix is to drop the spurious `/bin/`. Other resolvers in the same file (resolveSymfonyDir, resolveCaddyfilePath) already use the correct `here + "/<file>"` pattern. Bug shipped since the bundled-mode supervisor was added — would have hit anyone running the AppImage. Local `make quality` only exercises dev mode (BRIDGE_URL set), so the integration test loop never reached this codepath; CI's perfsmoke against the actual AppImage is the only thing that catches it. Manual test would be: launch the AppImage with BRIDGE_FRANKENPHP_BIN unset and watch the phpqml.bridge.bundled log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fcf7dc26cf |
qml: silence skeleton + todo Main.qml qmllint warnings
Two warnings, two distinct kinds of fix:
1. `Item { width: 12 }` (skeleton:77) — explicit width on a
layout-managed Item is undefined behaviour per qmllint. Replaced
with `Item { Layout.preferredWidth: 12 }`, matching the pattern
already used at line 85 (`Item { Layout.fillWidth: true }`). Real
fix, not a suppression.
2. `target: SingleInstance` (skeleton:48, todo/Main.qml:220) — false
positive. SingleInstance is intentionally a context property set
by main() before the QML engine boots (see SingleInstance.h
doc comment), so qmllint can't see it via static analysis.
Disabled the `unqualified` warning at the call site with an inline
`// qmllint disable unqualified` directive plus a one-line
explanation comment above. (Note: the disable directive parses
every word after `disable` as a category name, so the prose has
to live on the previous line — found the hard way after qmllint
complained about the "unknown category" of every English word in
the explanation.)
Verified `make quality` from framework/skeleton green (qmllint clean
across both targets — `php_qml_bridge_qmllint` and `skeleton_qmllint`
both build with zero warnings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e89a1c77c8 |
release: include the tag's CHANGELOG section in the Gitea release body
Previously the create-release POST sent only `{tag_name, name, draft,
prerelease}` — Gitea created the release with an empty description, so
users hitting the release page saw the tag name and nothing else.
Extracts the relevant `## [<version>]` block from CHANGELOG.md (using
$TAG with the leading `v` stripped) via awk, stopping at the next
`## [<other>]` section header or the trailing `[link-ref]: url` block,
and passes it as the `body` field to the release creation API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8ed452495c |
qml: silence DevConsole qmllint warnings (pragma + required property)
Qt 6.5.3's qmllint exits 255 on these two warnings even though older
qmllints only warn:
- Outer-id access from a delegate (`width: view.width`) requires
`pragma ComponentBehavior: Bound` to make the binding explicit.
- Implicit role injection (`text: model.text`) should be a
`required property`. Renamed the model role from "text" to "line"
so the required property doesn't shadow Label's own `text`.
Behaviour unchanged. Verified `make quality` from framework/skeleton
green; the framework's `php_qml_bridge_qmllint` target now lints
clean (no more warnings on DevConsole.qml).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1389b92906 |
qml: pin OUTPUT_DIRECTORY of PhpQml.Bridge to match its URI path
qmllint resolves QML modules by walking the import path looking for a
directory layout that mirrors the URI (PhpQml.Bridge → PhpQml/Bridge/).
qt_add_qml_module's OUTPUT_DIRECTORY defaults to CMAKE_CURRENT_BINARY_DIR
— which, when consumers add_subdirectory() this with their own binary_dir
(skeleton: build/qml/php_qml_bridge, todo: build/qml/php_qml_bridge),
ends in `php_qml_bridge` instead of `PhpQml/Bridge`. cmake configure
warns about the mismatch:
The php_qml_bridge target is a QML module with target path
PhpQml/Bridge. It uses an OUTPUT_DIRECTORY of .../php_qml_bridge,
which should end in the same target path, but doesn't. Tooling
such as qmllint may not work correctly.
…and at lint time, qmllint can't find the module, so every file that
`import PhpQml.Bridge` (AppShell.qml, DevConsole.qml) fails with
"Failed to import PhpQml.Bridge", which cascades into bogus
"Unqualified access" warnings for every BackendConnection reference.
The cascade exits 255 in Qt 6.5.3's qmllint (CI), even when an older
local qmllint would only warn.
Fix: pin OUTPUT_DIRECTORY in the framework's own qt_add_qml_module so
the layout is correct regardless of how consumers wire up the
add_subdirectory binary_dir. Single source of truth in the framework,
no consumer-side change needed.
Verified locally: rebuild from scratch + `make quality` green
(qmllint clean of the cascade — only the pre-existing
DevConsole/Main.qml warnings remain, all non-fatal). PHPStan +
cs-fixer + 16 tests + maker snapshots also still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
64be713b97 |
ci: add rsync + AppImage host tools (file, libfuse2, desktop-file-utils)
After the QML/C++ build started succeeding, `make appimage` failed at
the staging-symfony step ("rsync: No such file or directory") — slim
runner image again. Adding:
- rsync — used by examples/todo/Makefile to stage a
--no-dev composer copy of the Symfony tree
into build/staging-symfony/.
- file — appimagetool/linuxdeploy invoke `file` to
detect ELF type (AppImage, AppDir contents).
- libfuse2 — AppImage runtime mounts the squashfs via
libfuse2; without it appimagetool refuses
to assemble. (Alternative is
APPIMAGE_EXTRACT_AND_RUN=1 but installing
libfuse2 keeps the script unchanged.)
- desktop-file-utils — appimagetool validates the bundled
.desktop file via desktop-file-validate.
ci.yml only needs cmake + ninja + rsync (the symfony staging happens
in `make build` which it runs too, after the QML build) — no AppImage
assembly there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
fca2378d63 |
ci: install cmake + ninja-build (act-runner image is slim)
GitHub-hosted ubuntu-latest preinstalls cmake and ninja. Gitea's act-runner uses a minimal Ubuntu image (catthehacker/ubuntu:act-*) which doesn't, so the build step fails with "cmake: command not found" (exit 127). apt-get update was already run by install-qt-action's deps step earlier in the job, so the lists are populated — just install. Same step added to ci.yml and release.yml. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6c1a3364c4 |
ci: drop modules:'qtquickcontrols2' (rolled into base Qt 6)
aqt's "modules" list is for *additional* modules beyond the base Qt
install (qt5compat, qtcharts, qtmultimedia, qtwebsockets, etc.).
QtQuick.Controls 2 was a separately-shipped Qt 5 module, but in Qt
6.0+ it was folded into qtdeclarative — part of every base install.
aqt 3.3 rejects the obsolete name with:
ERROR: The packages ['qtquickcontrols2'] were not found while
parsing XML of package information!
Project's CMakeLists request `Core Gui Quick QuickControls2 Network
Qml` — all in base Qt 6.5. No `modules:` line needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
badb5056c9 |
ci: pin Qt install dir for act-runner (runner.temp comes through empty)
install-qt-action defaults `dir:` to `${{ runner.temp }}` if unset.
GitHub-hosted runners populate runner.temp; Gitea/Forgejo's act-runner
leaves it as the empty string. The action then rejects the empty path
with `TypeError: "dir" input may not be empty`.
Hardcoding `dir: ${{ github.workspace }}/qt` works on both — workspace
is always populated and writable, and the path is inside the job's
working tree so it's auto-cleaned with the workspace.
Same change in ci.yml and release.yml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|