#pragma once #include #include #include #include #include class QNetworkAccessManager; class QNetworkReply; class QTimer; class QQmlEngine; class QJSEngine; namespace PhpQml::Bridge { /// Owns the backend lifecycle and exposes its health to QML. /// /// Mode is auto-detected on construction: /// - `BRIDGE_URL` env set → **dev mode**: connect to a developer-managed /// backend at that URL. /// - `BRIDGE_URL` unset → **bundled mode** (Phase 4a): spawn the embedded /// `frankenphp` next to the host binary, generate a per-session bearer /// token, run first-launch migrations, and supervise the child. /// /// See PLAN.md §3 (Run modes), §7 (BackendConnection), §13 Phase 4a. class BackendConnection : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON Q_PROPERTY(Mode mode READ mode CONSTANT) Q_PROPERTY(QString url READ url NOTIFY urlChanged) Q_PROPERTY(QString token READ token NOTIFY tokenChanged) Q_PROPERTY(ConnectionState connectionState READ connectionState NOTIFY connectionStateChanged) Q_PROPERTY(QString error READ error NOTIFY errorChanged) public: enum class Mode { Dev, Bundled, }; Q_ENUM(Mode) /// Full Update Semantics enum (PLAN.md §5). enum class ConnectionState { Connecting, Online, Reconnecting, Offline, }; Q_ENUM(ConnectionState) explicit BackendConnection(QObject* parent = nullptr); ~BackendConnection() override; static BackendConnection* create(QQmlEngine* engine, QJSEngine*); Mode mode() const noexcept { return m_mode; } QString url() const { return m_url; } QString token() const { return m_token; } ConnectionState connectionState() const noexcept { return m_state; } QString error() const { return m_error; } /// Bundled mode: re-spawn the FrankenPHP child (e.g. after the user /// hits Retry on the Offline overlay). Dev mode: re-probe. Q_INVOKABLE void restart(); signals: void urlChanged(); void tokenChanged(); void connectionStateChanged(); void errorChanged(); /// Emitted in bundled mode when the supervisor restarts the FrankenPHP /// child and a fresh per-session secret is generated. RestClient and /// MercureClient pick the new value up on next request (§3 *Edge cases*). void tokenRotated(const QString& newToken); private slots: void probe(); void onProbeFinished(); void onChildFinished(int exitCode, QProcess::ExitStatus status); private: void initDevMode(); void initBundledMode(); bool runMigrations(); bool spawnChild(QString* errorOut = nullptr); void teardownChild(); QString resolveFrankenphpBin() const; QString resolveSymfonyDir() const; QString resolveCaddyfilePath() const; QString userDataDir() const; QString databaseUrl() const; void setState(ConnectionState s); void setError(const QString& msg); void setUrl(const QString& url); void setToken(const QString& token); static QString randomSecret(int bytes); Mode m_mode = Mode::Dev; QString m_url; QString m_token; QString m_jwtSecret; ConnectionState m_state = ConnectionState::Connecting; QString m_error; QString m_appName; QString m_dataDir; quint16 m_port = 8765; QNetworkAccessManager* m_nam = nullptr; QNetworkReply* m_pendingReply = nullptr; QTimer* m_retryTimer = nullptr; QElapsedTimer m_firstFailureSinceOnline; int m_offlineThresholdMs = 30000; QProcess* m_child = nullptr; int m_supervisorRetries = 0; static constexpr int kMaxSupervisorRetries = 5; }; } // namespace PhpQml::Bridge