Mitra ping: decouple stale-after from app cadence

Splits the single mitra_ping_interval_seconds config (which conflated
"how often the app pings" with "how long until offline" through a
hidden ×3 multiplier) into two orthogonal knobs:

- mitra_stale_after_seconds (CC-tunable, app_config DB row): the
  operator-facing offline threshold. What you set is what you get —
  no multiplier. Default 45s (preserves today's effective grace at
  the legacy 15s ping default).
- MITRA_HEARTBEAT_CADENCE_SECONDS (env var, default 30s): how often
  the mitra app sends a heartbeat. Backend-fixed per deployment;
  surfaced to the mitra app via /api/mitra/status.

Backend:
- config.service: getMitraPingConfig returns the new tuple
  {require_ping, stale_after_seconds, heartbeat_cadence_seconds}.
  Env parser handles blank/non-numeric → 30 fallback.
- mitra-status.service::autoOfflineStaleMitras drops the *3 and uses
  stale_after_seconds directly.
- mitra-status.service::getStatus returns heartbeat_cadence_seconds
  instead of ping_interval_seconds.
- /internal/config/mitra-ping PATCH validates
  stale_after_seconds >= cadence, returns 422 with a clear message
  ("stale_after_seconds must be a number >= heartbeat cadence (30s)").
- migrate.js: adds mitra_stale_after_seconds default 45. The old
  mitra_ping_interval_seconds key is left in place (vestigial) —
  no live code reads it; safe to drop after one release.

Mitra app:
- status_notifier reads heartbeat_cadence_seconds, uses it directly
  as the Timer.periodic interval. Defaults to 30s if missing (older
  backend safety).

Control center:
- SettingsPage: renames "Interval Ping" → "Ambang offline", input
  min={heartbeat_cadence_seconds}, shows the cadence as a read-only
  value with explanation that it's env-controlled.

Verified end-to-end on dev backend:
- GET /api/mitra/status returns {…, heartbeat_cadence_seconds: 30}
- GET /internal/config/mitra-ping returns {require_ping,
  stale_after_seconds: 45, heartbeat_cadence_seconds: 30}
- PATCH with stale_after_seconds=20 → 422 with cadence message
- PATCH with stale_after_seconds=120 → 200, persisted
- Env override (=60, blank, "foo") parses correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 20:39:59 +08:00
parent 1653482d54
commit a8c20d929e
6 changed files with 80 additions and 22 deletions

View File

@@ -31,7 +31,11 @@ class StatusErrorData extends OnlineStatusData {
class OnlineStatus extends _$OnlineStatus {
Timer? _heartbeatTimer;
bool _requirePing = true;
int _pingIntervalSeconds = 15;
// Heartbeat cadence is backend-fixed (MITRA_HEARTBEAT_CADENCE_SECONDS env,
// default 30s). Surfaced via /api/mitra/status. The CC-tunable
// mitra_stale_after_seconds is the offline threshold and lives entirely
// server-side — the app doesn't care, it just pings on a steady cadence.
int _heartbeatCadenceSeconds = 30;
@override
OnlineStatusData build() => const StatusInitialData();
@@ -41,7 +45,7 @@ class OnlineStatus extends _$OnlineStatus {
final response = await ref.read(apiClientProvider).get('/api/mitra/status');
final data = response['data'] as Map<String, dynamic>;
_requirePing = data['require_ping'] as bool? ?? true;
_pingIntervalSeconds = data['ping_interval_seconds'] as int? ?? 15;
_heartbeatCadenceSeconds = data['heartbeat_cadence_seconds'] as int? ?? 30;
state = StatusLoadedData(isOnline: data['is_online'] as bool);
} catch (e) {
state = const StatusLoadedData(isOnline: false);
@@ -83,7 +87,7 @@ class OnlineStatus extends _$OnlineStatus {
void _startHeartbeat() {
_stopHeartbeat();
_heartbeatTimer = Timer.periodic(Duration(seconds: _pingIntervalSeconds), (_) {
_heartbeatTimer = Timer.periodic(Duration(seconds: _heartbeatCadenceSeconds), (_) {
_heartbeatTick();
});
}