fix(shell): daemon-start and health-poll auth handling; policy panel freshness
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Review follow-ups on the auth-status work: - Both daemon-start paths classified an auth-rejected hub as "daemon dead" via healthy() and showed a start-failure dialog while the shell banner above correctly blamed the token. They now share daemonAnswers(): only an unreachable probe counts as down. - An auth-rejected poll now re-reads ~/.chain/hub-auth-token and reconnects when the file changed, so a token fixed outside Studio (CLI, editor) heals the connection without a restart — previously the client kept the stale in-memory token forever and the banner's own advice could not work. - An endpoint switch resets the failure streak, so a stale in-flight probe can no longer let the unreachable banner blame the new endpoint for the old one's misses. - The auth-policy panel re-queries when the hub token is saved or cleared in the panel above (reloadTick), instead of keeping a stale admin-denied hint; it also renders the hub's new reload_required flag as a pending-reload warning (DE+EN). - today-pipeline.md still documented ~/.fai/today after the rename; the FAB theme comment now states the both-themes intent. flutter analyze clean; 71 tests green including four new ones. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
2a1cbc82c3
commit
7cc8bab9b9
13 changed files with 242 additions and 16 deletions
|
|
@ -270,6 +270,11 @@ class StudioShellState extends State<StudioShell> {
|
|||
int _failedPolls = 0;
|
||||
static const int _unreachableThreshold = 3;
|
||||
|
||||
/// Endpoint the previous poll ran against; a change resets the
|
||||
/// failure streak so stale in-flight results never blame the new
|
||||
/// endpoint (see `_checkHealth`).
|
||||
String _polledEndpoint = '';
|
||||
|
||||
/// Whether to render the unreachable banner. Distinct from
|
||||
/// `_connected == false` so a single transient miss doesn't
|
||||
/// flash the banner — only sustained failure does.
|
||||
|
|
@ -287,6 +292,44 @@ class StudioShellState extends State<StudioShell> {
|
|||
@visibleForTesting
|
||||
static Future<HubProbeResult> Function()? debugProbeOverride;
|
||||
|
||||
/// Test-only: replaces the token-file self-heal check (see
|
||||
/// [_reloadTokenIfChanged]) so widget tests can assert the
|
||||
/// auth-rejected poll re-reads the token without touching disk.
|
||||
@visibleForTesting
|
||||
static Future<bool> Function()? debugReloadTokenOverride;
|
||||
|
||||
/// Whether the hub daemon answers at all — an auth rejection or a
|
||||
/// not-serving health state still means the process is alive, just
|
||||
/// not usable yet; only [HubProbeResult.unreachable] is "down".
|
||||
/// Used by the daemon-start paths so a wrong token doesn't
|
||||
/// misreport as "daemon start failed" (the shell banner already
|
||||
/// explains the token problem).
|
||||
static Future<bool> daemonAnswers() async {
|
||||
try {
|
||||
final probe = debugProbeOverride != null
|
||||
? await debugProbeOverride!()
|
||||
: await HubService.instance.probeHealth();
|
||||
return probe != HubProbeResult.unreachable;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Self-heal after an auth-rejected poll: the operator may have
|
||||
/// fixed `~/.chain/hub-auth-token` outside Studio (CLI, editor) —
|
||||
/// the client caches the token, so re-read the file and reconnect
|
||||
/// when it changed. Returns whether a reconnect happened. Inert
|
||||
/// when the probe is test-overridden, unless the reload override
|
||||
/// is set too.
|
||||
static Future<bool> _reloadTokenIfChanged() {
|
||||
final override = debugReloadTokenOverride;
|
||||
if (override != null) return override();
|
||||
if (debugProbeOverride != null) return Future.value(false);
|
||||
return HubService.instance
|
||||
.reloadAuthTokenIfChanged()
|
||||
.catchError((_) => false);
|
||||
}
|
||||
|
||||
/// Current connection state, for descendants (e.g. WelcomePage)
|
||||
/// that adapt their content to hub availability.
|
||||
bool? get connected => _connected;
|
||||
|
|
@ -307,9 +350,9 @@ class StudioShellState extends State<StudioShell> {
|
|||
}
|
||||
// The start command reported failure — but very often the daemon is
|
||||
// simply already running (port in use). Probe before crying error,
|
||||
// so "tap to start" on an already-up hub just connects.
|
||||
final alive =
|
||||
await HubService.instance.healthy().catchError((_) => false);
|
||||
// so "tap to start" on an already-up hub just connects. Alive but
|
||||
// auth-rejected counts as running — the banner explains the token.
|
||||
final alive = await daemonAnswers();
|
||||
if (!mounted) return;
|
||||
if (alive) {
|
||||
await _checkHealth();
|
||||
|
|
@ -422,15 +465,22 @@ class StudioShellState extends State<StudioShell> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _checkHealth() async {
|
||||
Future<void> _checkHealth({bool retriedAfterTokenReload = false}) async {
|
||||
final probe = debugProbeOverride != null
|
||||
? await debugProbeOverride!()
|
||||
: await HubService.instance.probeHealth();
|
||||
if (!mounted) return;
|
||||
// A failure that raced an endpoint switch (Settings save, channel
|
||||
// switch) must not inherit the old endpoint's failure streak —
|
||||
// restart the count so the banner never blames the NEW endpoint
|
||||
// for the OLD one's misses.
|
||||
final endpoint = HubService.instance.endpointLabel;
|
||||
final endpointChanged = endpoint != _polledEndpoint;
|
||||
_polledEndpoint = endpoint;
|
||||
final ok = probe == HubProbeResult.serving;
|
||||
final authRejected = probe == HubProbeResult.authRejected;
|
||||
final wasUnreachable = _hubUnreachable;
|
||||
final nextFailed = ok ? 0 : _failedPolls + 1;
|
||||
final nextFailed = ok ? 0 : (endpointChanged ? 1 : _failedPolls + 1);
|
||||
final connectionChanged = _connected != ok;
|
||||
final bannerChanged =
|
||||
wasUnreachable != (nextFailed >= _unreachableThreshold);
|
||||
|
|
@ -444,6 +494,16 @@ class StudioShellState extends State<StudioShell> {
|
|||
_authRejected = authRejected;
|
||||
});
|
||||
}
|
||||
if (authRejected && !retriedAfterTokenReload) {
|
||||
// Self-heal: re-read the token file in case it was fixed
|
||||
// outside Studio; on a real change reconnect + re-probe now
|
||||
// instead of waiting out the poll interval. One retry per
|
||||
// poll tick at most.
|
||||
final changed = await _reloadTokenIfChanged();
|
||||
if (changed && mounted) {
|
||||
return _checkHealth(retriedAfterTokenReload: true);
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
try {
|
||||
final snap = await HubService.instance.channelStatus();
|
||||
|
|
@ -779,8 +839,9 @@ class _SidebarState extends State<_Sidebar>
|
|||
}
|
||||
// The daemon may already be running (port in use) — probe before
|
||||
// showing an error, so this just connects on an already-up hub.
|
||||
final alive =
|
||||
await HubService.instance.healthy().catchError((_) => false);
|
||||
// Alive but auth-rejected counts as running — the shell banner
|
||||
// explains the token problem.
|
||||
final alive = await StudioShellState.daemonAnswers();
|
||||
if (!context.mounted) return;
|
||||
if (alive) return;
|
||||
// Genuinely down: persistent, copyable dialog (a SnackBar flashes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue