Commit Graph

18 Commits

Author SHA1 Message Date
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>
2026-05-03 19:50:01 +02:00
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>
2026-05-03 13:43:48 +02:00
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>
2026-05-03 13:14:22 +02:00
68ee6efefe bundled: write Symfony cache + log to user data dir (AppImage is read-only)
All checks were successful
CI / Quality (push) Successful in 4m40s
Release / Linux AppImage (push) Successful in 4m48s
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>
2026-05-03 12:21:10 +02:00
fcf7dc26cf qml: silence skeleton + todo Main.qml qmllint warnings
Some checks failed
CI / Quality (push) Successful in 5m3s
Release / Linux AppImage (push) Failing after 4m50s
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>
2026-05-03 11:05:17 +02:00
b925774eea Phase 5 sub-commit 3: hot-reload docs + .vscode/.idea editor configs
Skeleton README documents the hot-reload story end-to-end:
- PHP-side: frankenphp run --watch (already what `make dev` uses).
- QML-side: Qt Creator Reload, qmlls live preview, run-from-source.
- Dev console: Ctrl+` toggle from sub-commit 1.

Both skeleton and todo example ship .vscode/ (launch.json with Xdebug
attach + Qt-host gdb launch + a compound config, tasks.json for the
make targets, settings.json) and .idea/runConfigurations/ shell run
configs for `make dev`, `make doctor`, `make quality` (and `make
appimage` in the todo example). PhpStorm's Xdebug listener is global
so we don't ship a project-level run config for it; the README
points users at the toolbar toggle.

php-qml-init also rewrites .vscode/launch.json's binary path and
config label so a fresh scaffold's debugger configs point at the
new project's binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:28:02 +02:00
4c15ac281c Phase 5 sub-commit 1: DevConsole + child-output capture + Ctrl+` toggle
BackendConnection now captures the bundled FrankenPHP child's merged
stdout+stderr into a 500-line ring buffer, mirrors each line through
qCInfo(lcBundled) so terminal users still see logs, and exposes
childLogTail() / childLogLine for QML.

DevConsole.qml is an opt-in monospaced viewer with auto-scroll + clear
that the skeleton and the todo example bind to Ctrl+`. Dev mode (when
BRIDGE_URL is set, no bundled child) renders an explanatory hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 20:58:53 +02:00
d8726bac94 Fix CI: bump PHP requirement to ^8.4 (Symfony 8 enforces it)
CI was failing on the Install-bundle-dependencies step because
shivammathur/setup-php was installing 8.3 while Symfony 8.x dependencies
declare php >= 8.4. Local composer install worked because the dev box
runs PHP 8.5.5; CI doesn't.

Bumps:
  - framework/php/composer.json
  - framework/skeleton/symfony/composer.json
  - examples/todo/symfony/composer.json
  - .gitea/workflows/ci.yml         php-version: '8.3' → '8.4'
  - .gitea/workflows/release.yml    same
  - PLAN.md §13 Phase 1 *Detailed scope* PHP minimum row

PHPStan / cs-fixer / PHPUnit stay green locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:53:00 +02:00
a1cc06abbb Phase 4a sub-commit 1: bundled-mode startup in BackendConnection
Auto-detected on construction:

  - BRIDGE_URL env set → dev mode (today's behaviour, unchanged).
  - BRIDGE_URL unset → bundled mode: BackendConnection now
    1. Resolves the user app data dir (QStandardPaths::AppDataLocation,
       ~/.local/share/<org>/<app> on Linux) and ensures var/, var/log/,
       var/cache/ exist there.
    2. Generates a per-session 32-byte URL-safe token and a 48-byte
       Mercure JWT secret.
    3. Runs `frankenphp php-cli bin/console doctrine:migrations:migrate -n`
       against the user's DATABASE_URL with a 60s timeout.
    4. Spawns FrankenPHP via QProcess with BRIDGE_TOKEN/MERCURE_*/PORT
       in the env, prctl(PR_SET_PDEATHSIG, SIGTERM) on the child, and
       a supervisor that re-spawns up to 5 times on unexpected exit.
       Each restart fires tokenRotated(newToken).

Path resolution defaults to applicationDirPath() + bin/frankenphp,
applicationDirPath() + symfony, applicationDirPath() + Caddyfile, with
both `/../share/<app>/...` and `/../usr/share/<app>/...` fallbacks for
AppImage-style layouts. All three are overridable via
BRIDGE_FRANKENPHP_BIN / BRIDGE_SYMFONY_DIR / BRIDGE_CADDYFILE env vars.

Caddyfiles in skeleton + example now use {$VAR:default} substitution
for PORT and the Mercure JWT keys, so the same Caddyfile works in both
modes. Dev defaults match symfony/.env.

restart() in bundled mode re-spawns the child (resets the supervisor
counter); in dev mode it stays a probe-only no-op.

Smoke-tested locally with `BRIDGE_FRANKENPHP_BIN=…  BRIDGE_SYMFONY_DIR=…
BRIDGE_CADDYFILE=…  ./build/qml/todo` (no BRIDGE_URL): bundled mode
created ~/.local/share/php-qml/todo/var/data.sqlite, ran the migration,
spawned FrankenPHP, served /healthz, accepted a POST /api/todos with
the per-session bearer. Dev mode (`make dev`) still works unchanged.

Includes a `phpqml.bridge.bundled` Q_LOGGING_CATEGORY so failures
surface to the user; enable with QT_LOGGING_RULES='phpqml.bridge.bundled.*=true'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:00:13 +02:00
adc0cdc11d Phase 3 sub-commit 5: maker-output snapshot test + phase closure
framework/php/tests/snapshot/ holds reference output for every shipped
maker (resource Todo, command MarkAllDone, window Todo). The
run.sh script:

  - git-archives the skeleton into a temp dir
  - composer-installs against the bundle's real path
  - removes the existing maker outputs so the regenerators don't bail
  - runs the three makers
  - diffs each generated file against the matching baseline

CI / make quality fail on any drift; if a template change is intended,
the baselines must be regenerated in the same commit. Wired into:

  - framework/skeleton/Makefile's `quality` target (local/dev runs)
  - .gitea/workflows/ci.yml (CI runs after qmllint)

Plus a few hardenings discovered while wiring this up:

  - The resource maker template now injects NormalizerInterface
    (not SerializerInterface — that interface lacks ::normalize()).
    All Todo controllers re-rendered to match.
  - The command maker template emits a $this->em->flush() so the
    injected EntityManager isn't a property.onlyWritten violation
    in PHPStan after the user fills in the body.
  - phpstan.neon and php-cs-fixer's Finder both exclude tests/snapshot
    so the baselines aren't auto-rewritten or analysed as live code.

CI workflow now also installs FrankenPHP, builds the todo example, and
runs the bridge-integration test from Phase 3 sub-commit 4.

Phase 3 done. Outstanding follow-ups (deferred per spec): the
qmltestrunner-driven QML unit tests, make:bridge:event,
make:bridge:read-model, ReactiveObject pagination.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:03:41 +02:00
1964a52f99 Phase 2 sub-commit 5: convention test passes, skeleton walkthrough, phase 2 closed
Some checks failed
CI / Quality (push) Failing after 1m50s
Runs `make:bridge:resource Todo` against the skeleton, then `make:migration`
+ `doctrine:migrations:migrate`, and verifies the round-trip end-to-end:

  - POST /api/todos creates a row with a UUIDv7 id
  - GET /api/todos returns the row
  - Mercure dual-publishes:
    - app://model/todo (collection topic)
    - app://model/todo/{uuid} (entity topic)
  - The published envelope shape matches PLAN.md §4 exactly:
    {op:"upsert", id:..., version:..., data:{...}, correlationKey:"..."}
  - correlationKey echoes the request's Idempotency-Key, ready to be
    matched by ReactiveListModel's pending state on the QML side.

Generated files committed as the regression baseline (Phase 3 will add
a CI check that re-running the maker reproduces these byte-for-byte):

  - framework/skeleton/symfony/src/Entity/Todo.php
  - framework/skeleton/symfony/src/Controller/TodoController.php
  - framework/skeleton/symfony/migrations/Version20260502004612.php
  - framework/skeleton/qml/TodoList.qml

framework/skeleton/README.md captures the three-command flow plus a
curl walkthrough so future readers can reproduce. Phase 2 done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:49:23 +02:00
4a42de702b Phase 2 sub-commit 4: make:bridge:resource maker
Some checks failed
CI / Quality (push) Failing after 1m55s
Bundle picks up symfony/maker-bundle as require-dev. New BridgeResourceMaker
under PhpQml\Bridge\Maker generates three files for a named resource:

  - src/Entity/<Name>.php  — Doctrine entity with #[BridgeResource]
                             and a UUIDv7 id by default. --int-id flips
                             to auto-incrementing int IDs.
  - src/Controller/<Name>Controller.php — CRUD on /api/{plural} (list,
                             create, update, delete) with serializer-
                             normalised JSON responses.
  - {qml_path}/<Name>List.qml — starter ListView wrapped around a
                             ReactiveListModel bound to the right topic
                             and source URL.

The Doctrine subscriber from sub-commit 2 picks the entity up
automatically — no per-resource listener generated. The QML snippet
target defaults to '../qml/' (relative to the Symfony project root)
and is overridable via the maker's $qmlPath constructor arg.

Templates live under src/Maker/templates/ as .tpl.php files using
short-echo and alternative-syntax control structures by convention.
PHPStan and php-cs-fixer skip them — the maker's Generator binds the
template variables at render time.

Skeleton picks up MakerBundle as a `dev` bundle and require-dev'd
symfony/maker-bundle, so `bin/console make:bridge:resource Todo`
works out-of-the-box.

Verified: maker runs end-to-end against `Todo` and emits readable,
syntactically valid output. composer quality (16 tests, 45 assertions,
PHPStan clean, cs-fixer clean) stays green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:45:42 +02:00
030502ca38 Phase 2 sub-commit 3: full Update Semantics + ReactiveListModel + AppShell
Some checks failed
CI / Quality (push) Failing after 1m45s
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
1c5a5761f6 Phase 2 sub-commit 2: ModelPublisher + #[BridgeResource] + Doctrine listener
Some checks failed
CI / Quality (push) Failing after 1m54s
Bundle gains the model layer that bridges Doctrine entities to Mercure
without per-resource glue. Three new pieces:

- `#[BridgeResource(name: ?string)]` attribute marks an entity as a
  reactive bridge model. Topic name defaults to the lowercased class
  basename and can be overridden per resource.

- `ModelPublisher` translates entity changes into PLAN.md §4 envelopes
  ({op, id, data, version, ?correlationKey}) and dual-publishes them
  on `app://model/{name}` (collection topic) and `app://model/{name}/{id}`
  (entity topic). Entity normalisation goes through Symfony's Serializer
  (ObjectNormalizer + DateTime + BackedEnum) for predictable JSON. The
  envelope `version` field is a per-process monotonic counter — fine for
  single-instance dev mode; production should back this with a Postgres
  SEQUENCE or equivalent (noted for Phase 4).

- `DoctrineBridgeListener` registers `postPersist`/`postUpdate`/
  `postRemove` via `#[AsDoctrineListener]` and routes events through
  ModelPublisher. Entities without `#[BridgeResource]` are silently
  skipped.

Plus the correlation-key plumbing the §5 Update Semantics layer needs:

- `CorrelationContext` is a per-request holder for the originating
  request's `Idempotency-Key`.
- `CorrelationKeyListener` reads the header on `KernelEvents::REQUEST`
  and clears the context on `KernelEvents::TERMINATE` (worker mode
  hygiene). CLI mutations see no key, which is correct.

Bundle composer.json picks up `doctrine/dbal`, `doctrine/orm`,
`doctrine/doctrine-bundle`, `symfony/serializer`, `symfony/property-*`,
`symfony/uid`. PHPStan extension `phpstan-doctrine` added so the listener's
event-args types resolve. Skeleton's framework.yaml enables `serializer`
and `property_info`.

Tests: 5 new for ModelPublisher (dual publish, correlation echo, delete
op omits data, untagged entities ignored, version increments). Total:
16 tests, 45 assertions, PHPStan clean, cs-fixer clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:32:51 +02:00
6bd4d13a77 Phase 2 sub-commit 1: Doctrine ORM 3 + Migrations + SQLite
Some checks failed
CI / Quality (push) Has been cancelled
Skeleton gains Doctrine ORM 3.6 (with DoctrineBundle 3.x and Migrations
4.x), pointed at a SQLite file under var/data.sqlite. Apps move to
Postgres/MySQL by overriding DATABASE_URL in .env.local.

config/packages/doctrine.yaml registers the symfony/uid UuidType so
Phase 2 sub-commit 4's UUIDv7 default works without per-app config,
and pre-wires the App\Entity attribute mapping under src/Entity/ for
the maker to drop entities into.

Bundle gains an optional doctrine/dbal Connection via Autowire; when
present, bridge:doctor adds a "Database reachable" SELECT-1 probe.
The bundle still installs cleanly without doctrine/dbal — apps that
opt out get a doctor table without the database row.

Verified: `bin/console bridge:doctor` is all green against a fresh
SQLite. composer quality (PHPStan + cs-fixer + PHPUnit) stays green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:25:48 +02:00
7323b9affe Phase 1 sub-commit 7: CI quality job
Some checks failed
CI / Quality (push) Has been cancelled
PHPStan (level 6 + symfony extension) and PHP CS Fixer (Symfony +
PHP83Migration ruleset) configs at framework/php/. composer.json
exposes phpstan / cs:check / cs:fix / phpunit / quality scripts.
PHPStan-clean across the bundle; cs:check is happy after auto-fix
applied @Symfony idioms (yoda, leading-backslash JSON_*, blank-line
before return). Test mocks consolidated into a HubSpy helper to keep
PHPStan happy about by-ref captures.

Skeleton's Makefile target `quality` chains `composer quality` (in
framework/php/) with cmake's all_qmllint target. Local run is green —
11 tests / 32 assertions, no PHPStan errors, cs-fixer clean, qmllint
emits advisory warnings only.

Layout fix in skeleton's Main.qml: status-dot Rectangles inside
RowLayout now use Layout.preferredWidth/Height instead of width/height
to satisfy Quick.layout-positioning checks.

.gitea/workflows/ci.yml replaces the placeholder with a real `quality`
job: setup-php, composer install (cached), the four PHP checks, Qt 6
via install-qt-action (cached), QML module build, qmllint via the
all_qmllint CMake target. Workflow exists from this commit onward
even if a runner isn't provisioned yet.

bridge:doctor lost the Publisher dependency since it was only used as
a "service is wired" marker — the command being injectable already
proves that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:15:06 +02:00
d671b26cac Phase 1 sub-commit 6: skeleton wiring — make dev runs end-to-end
All checks were successful
CI / Quality (push) Successful in 5s
Symfony app under framework/skeleton/symfony/: minimal bin/console,
public/index.php, MicroKernel-based src/Kernel.php, services.yaml,
framework/security/mercure config, and a demo App\Controller\PingController
that GETs /api/ping (returning JSON pong) and republishes the same
payload to the Mercure topic app://ping. composer.json uses a path
repository to symlink the bundle from ../../php so local edits are
picked up live.

QML app under framework/skeleton/qml/: top-level CMake that
add_subdirectory's framework/qml, a main.cpp that creates the Qt
process, runs SingleInstance.acquireOrForward before any QML loads,
exposes SingleInstance via context property, and loadFromModule's
Skeleton.Main. Main.qml uses BackendConnection / RestClient /
MercureClient from PhpQml.Bridge and renders status dots, a Ping
button, and an event log.

Caddyfile binds 127.0.0.1:8765, enables in-memory Mercure with a
256-bit dev JWT (matches symfony/.env, lcobucci/jwt requires this).
Makefile wraps build / dev / doctor / clean; scripts/dev.sh starts
FrankenPHP --watch and the Qt host together with explicit PID-based
teardown (process-group `kill 0` proved unreliable when frankenphp's
watch fork reparented).

Bug fixes uncovered in this sub-commit:
- SingleInstance.acquireOrForward: probe-first, then removeServer +
  retry-listen. The original loop-with-removeServer-after-failed-bind
  silently exited on stale sockets from prior runs.
- Main.qml: MercureClient does NOT inherit BackendConnection.token —
  Mercure subscribes anonymously in dev (Caddyfile), and forwarding
  the bridge bearer made it 401-loop.
- /api/ping was 500ing because the dev MERCURE_JWT_SECRET was 144 bits;
  bumped to 64-char (>=256 bit) to satisfy lcobucci/jwt.
- Linked the framework lib (php_qml_bridge) explicitly in addition to
  the QML plugin so SingleInstance.h resolves.
- Auto-generated config/reference.php gitignored.

Smoke verified offscreen: /healthz 200, /api/ping 200, 1 publish, 1
subscriber, zero 401s, clean shutdown with no zombies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:50:16 +02:00
9001386f92 Phase 1 sub-commit 1: scaffold framework, skeleton, CI
All checks were successful
CI / Quality (push) Successful in 48s
Stands up the directory structure Phase 1 fills in over subsequent
sub-commits: framework/php (Composer package php-qml/bridge),
framework/qml (Qt module placeholder), framework/skeleton (Caddyfile +
Makefile stubs), and .gitea/workflows/ci.yml. Root .gitignore covers
the build/composer/Symfony/Qt/CMake/IDE artefacts the rest of Phase 1
will produce. No bundle code, no Qt module sources, no working dev mode
yet — those land in sub-commits 2-7. Spike still in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:59:06 +02:00