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>
This commit is contained in:
@@ -20,6 +20,7 @@ This section tracks work landing on `dev` toward **v0.2.0** (next minor; pre-1.0
|
|||||||
- **`make:bridge:read-model <Name>` maker.** Generates a query-only projection: `src/ReadModel/<Name>ReadModel.php` (query service stub injecting `EntityManagerInterface`), `src/Controller/<Name>Controller.php` (single GET handler at `/api/<kebab-plural>`), and `{qml_path}/<Name>List.qml` (`ReactiveListModel` bound to the route, deliberately *no* Mercure topic — read-models aren't auto-reactive; invalidation is event-driven via `make:bridge:event`). Closes the fourth row of PLAN.md §8's makers table.
|
- **`make:bridge:read-model <Name>` maker.** Generates a query-only projection: `src/ReadModel/<Name>ReadModel.php` (query service stub injecting `EntityManagerInterface`), `src/Controller/<Name>Controller.php` (single GET handler at `/api/<kebab-plural>`), and `{qml_path}/<Name>List.qml` (`ReactiveListModel` bound to the route, deliberately *no* Mercure topic — read-models aren't auto-reactive; invalidation is event-driven via `make:bridge:event`). Closes the fourth row of PLAN.md §8's makers table.
|
||||||
- **Pre-migration auto-backup of `var/data.sqlite`.** Bundled-mode supervisor copies the SQLite file to `var/data.sqlite.<unix-timestamp>.bak` before invoking `doctrine:migrations:migrate`; trims to the 5 most recent. SQLite's lack of transactional DDL means a half-applied migration can corrupt the database with no rollback path; cheap insurance against that. Skipped on first launch (no DB to back up); failure to copy logs a warning and continues (a missing safety-net is not a reason to refuse to boot). Backup runs only in bundled mode — dev mode users own their `var/data.sqlite` lifecycle. Bundled-supervisor integration test gained an assertion that a `.bak` file appears under the user data dir on second launch.
|
- **Pre-migration auto-backup of `var/data.sqlite`.** Bundled-mode supervisor copies the SQLite file to `var/data.sqlite.<unix-timestamp>.bak` before invoking `doctrine:migrations:migrate`; trims to the 5 most recent. SQLite's lack of transactional DDL means a half-applied migration can corrupt the database with no rollback path; cheap insurance against that. Skipped on first launch (no DB to back up); failure to copy logs a warning and continues (a missing safety-net is not a reason to refuse to boot). Backup runs only in bundled mode — dev mode users own their `var/data.sqlite` lifecycle. Bundled-supervisor integration test gained an assertion that a `.bak` file appears under the user data dir on second launch.
|
||||||
- **`bridge:export` console command + QML hook.** New `bin/console bridge:export <destination>` copies the active SQLite database to a user-chosen path (overwrites if the destination exists; reads the source path from `DATABASE_URL` so it works in both dev and bundled mode). Mirrored on the QML side as `BackendConnection.exportDatabase(path)` (`Q_INVOKABLE bool`) returning success synchronously and emitting `databaseExported(path)` / `databaseExportFailed(reason)` for async UX. QML callers typically pair it with `Qt.labs.platform.FileDialog` (see `docs/native-dialogs.md`). 4 unit tests cover the command's success / non-SQLite-URL / missing-source / overwrite paths.
|
- **`bridge:export` console command + QML hook.** New `bin/console bridge:export <destination>` copies the active SQLite database to a user-chosen path (overwrites if the destination exists; reads the source path from `DATABASE_URL` so it works in both dev and bundled mode). Mirrored on the QML side as `BackendConnection.exportDatabase(path)` (`Q_INVOKABLE bool`) returning success synchronously and emitting `databaseExported(path)` / `databaseExportFailed(reason)` for async UX. QML callers typically pair it with `Qt.labs.platform.FileDialog` (see `docs/native-dialogs.md`). 4 unit tests cover the command's success / non-SQLite-URL / missing-source / overwrite paths.
|
||||||
|
- **Periodic auto-update check.** Bundled-mode supervisor arms an `AppImageUpdate` poll on the first `Online` transition: a launch-time check 10 s after backend ready, then a recurring check every 6 hours. PLAN.md §11 *Auto-update* called for "check on launch and once per N hours; offer install on next restart, never auto-restart" — the existing `checkForUpdates()` Q_INVOKABLE remains the install trigger, this just automates the polling. Disable with `BRIDGE_AUTO_UPDATE_DISABLE=1`; override the period with `BRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes>`. Dev mode skips entirely.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -615,6 +615,45 @@ void BackendConnection::setState(ConnectionState s)
|
|||||||
if (m_state == s) return;
|
if (m_state == s) return;
|
||||||
m_state = s;
|
m_state = s;
|
||||||
emit connectionStateChanged();
|
emit connectionStateChanged();
|
||||||
|
if (s == ConnectionState::Online) {
|
||||||
|
armAutoUpdateOnFirstOnline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendConnection::armAutoUpdateOnFirstOnline()
|
||||||
|
{
|
||||||
|
if (m_autoUpdateArmed) return;
|
||||||
|
if (m_mode != Mode::Bundled) return;
|
||||||
|
if (qEnvironmentVariableIsSet("BRIDGE_AUTO_UPDATE_DISABLE")
|
||||||
|
&& qgetenv("BRIDGE_AUTO_UPDATE_DISABLE") == "1") {
|
||||||
|
qCInfo(lcBundled) << "auto-update disabled via BRIDGE_AUTO_UPDATE_DISABLE";
|
||||||
|
m_autoUpdateArmed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_autoUpdateArmed = true;
|
||||||
|
|
||||||
|
int periodMs = kAutoUpdateDefaultPeriodMs;
|
||||||
|
bool ok = false;
|
||||||
|
const int overrideMin = qgetenv("BRIDGE_AUTO_UPDATE_PERIOD_MIN").toInt(&ok);
|
||||||
|
if (ok && overrideMin > 0) {
|
||||||
|
periodMs = overrideMin * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check: a few seconds after first Online so a fresh launch
|
||||||
|
// doesn't fight for bandwidth/CPU with the cold boot. Subsequent
|
||||||
|
// checks: every periodMs (default 6h).
|
||||||
|
QTimer::singleShot(kAutoUpdateLaunchDelayMs, this, &BackendConnection::checkForUpdates);
|
||||||
|
|
||||||
|
if (!m_autoUpdateTimer) {
|
||||||
|
m_autoUpdateTimer = new QTimer(this);
|
||||||
|
m_autoUpdateTimer->setSingleShot(false);
|
||||||
|
connect(m_autoUpdateTimer, &QTimer::timeout,
|
||||||
|
this, &BackendConnection::checkForUpdates);
|
||||||
|
}
|
||||||
|
m_autoUpdateTimer->start(periodMs);
|
||||||
|
qCInfo(lcBundled) << "auto-update armed: launch check in"
|
||||||
|
<< kAutoUpdateLaunchDelayMs << "ms, period"
|
||||||
|
<< (periodMs / 60000) << "min";
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendConnection::setError(const QString& msg)
|
void BackendConnection::setError(const QString& msg)
|
||||||
|
|||||||
@@ -175,6 +175,17 @@ private:
|
|||||||
QProcess* m_updateCheck = nullptr;
|
QProcess* m_updateCheck = nullptr;
|
||||||
QProcess* m_updateApply = nullptr;
|
QProcess* m_updateApply = nullptr;
|
||||||
|
|
||||||
|
/// Periodic AppImageUpdate poll. Armed on first Online transition
|
||||||
|
/// in bundled mode; PLAN.md §11 *Auto-update*. Disabled by env
|
||||||
|
/// `BRIDGE_AUTO_UPDATE_DISABLE=1`; period override via
|
||||||
|
/// `BRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes>`.
|
||||||
|
QTimer* m_autoUpdateTimer = nullptr;
|
||||||
|
bool m_autoUpdateArmed = false;
|
||||||
|
static constexpr int kAutoUpdateLaunchDelayMs = 10'000; // 10s after first Online
|
||||||
|
static constexpr int kAutoUpdateDefaultPeriodMs = 6 * 60 * 60 * 1000; // 6h
|
||||||
|
|
||||||
|
void armAutoUpdateOnFirstOnline();
|
||||||
|
|
||||||
QString resolveSidecarUpdater() const;
|
QString resolveSidecarUpdater() const;
|
||||||
QString currentAppImagePath() const;
|
QString currentAppImagePath() const;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user