feat(desktop/account-health): treat active/expiring_soon probes as live and dedupe in-flight checks
Why: avoid showing accounts as not-live during stale-window probes; keep publish gating consistent with runtime view. - collapse FIRST_PROBE window so initial probe runs immediately - skip due-probe selection when a probe is already queued/in-flight - mirror auth-state→health projection on both client report and tenant ingest paths Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import type { DesktopAccountInfo } from "@geo/shared-types";
|
import type { DesktopAccountInfo } from "@geo/shared-types";
|
||||||
|
|
||||||
export function resolveAccountHealth(account: DesktopAccountInfo): DesktopAccountInfo["health"] {
|
export function resolveAccountHealth(account: DesktopAccountInfo): DesktopAccountInfo["health"] {
|
||||||
|
if (
|
||||||
|
account.runtime_auth_state === "active" ||
|
||||||
|
(account.runtime_auth_state === "expiring_soon" && account.runtime_probe_state !== "network_error")
|
||||||
|
) {
|
||||||
|
return "live";
|
||||||
|
}
|
||||||
return account.runtime_health ?? account.health;
|
return account.runtime_health ?? account.health;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,11 @@ import {
|
|||||||
import type { PublishAccountIdentity } from "./account-binder";
|
import type { PublishAccountIdentity } from "./account-binder";
|
||||||
|
|
||||||
const PROBE_TICK_MS = 10_000;
|
const PROBE_TICK_MS = 10_000;
|
||||||
const FIRST_PROBE_MIN_MS = 5_000;
|
const STALE_AFTER_MS = 45 * 60_000;
|
||||||
const FIRST_PROBE_MAX_MS = 15_000;
|
|
||||||
const REGULAR_PROBE_MIN_MS = 30 * 60_000;
|
const REGULAR_PROBE_MIN_MS = 30 * 60_000;
|
||||||
const REGULAR_PROBE_MAX_MS = 60 * 60_000;
|
const REGULAR_PROBE_MAX_MS = STALE_AFTER_MS;
|
||||||
const CONFIRM_BACKOFF_STEPS_MS = [30_000, 120_000] as const;
|
const CONFIRM_BACKOFF_STEPS_MS = [30_000, 120_000] as const;
|
||||||
const NETWORK_RETRY_MS = 2 * 60_000;
|
const NETWORK_RETRY_MS = 2 * 60_000;
|
||||||
const STALE_AFTER_MS = 45 * 60_000;
|
|
||||||
const PREFLIGHT_MAX_AGE_MS = 15 * 60_000;
|
const PREFLIGHT_MAX_AGE_MS = 15 * 60_000;
|
||||||
const MAX_CONCURRENT_ACCOUNT_PROBES = 3;
|
const MAX_CONCURRENT_ACCOUNT_PROBES = 3;
|
||||||
const MANUAL_PROBE_CACHE_TTL_MS = 5 * 60_000;
|
const MANUAL_PROBE_CACHE_TTL_MS = 5 * 60_000;
|
||||||
@@ -100,7 +98,7 @@ function resolvePartition(accountId: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextInitialProbeAt(now = Date.now()): number {
|
function nextInitialProbeAt(now = Date.now()): number {
|
||||||
return now + randomInt(FIRST_PROBE_MIN_MS, FIRST_PROBE_MAX_MS);
|
return now;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextRegularProbeAt(now = Date.now()): number {
|
function nextRegularProbeAt(now = Date.now()): number {
|
||||||
@@ -113,9 +111,9 @@ function normalizePersisted(
|
|||||||
return {
|
return {
|
||||||
accountId: record.accountId,
|
accountId: record.accountId,
|
||||||
platform: record.platform,
|
platform: record.platform,
|
||||||
authState: "unknown",
|
authState: record.lastVerifiedAt ? "active" : "unknown",
|
||||||
probeState: "idle",
|
probeState: "idle",
|
||||||
authReason: null,
|
authReason: record.lastVerifiedAt ? "probe_success" : null,
|
||||||
lastVerifiedAt: record.lastVerifiedAt ?? null,
|
lastVerifiedAt: record.lastVerifiedAt ?? null,
|
||||||
lastProbeAt: null,
|
lastProbeAt: null,
|
||||||
nextProbeAt: nextInitialProbeAt(),
|
nextProbeAt: nextInitialProbeAt(),
|
||||||
@@ -176,15 +174,7 @@ function ensureProbeTimer(): void {
|
|||||||
}, PROBE_TICK_MS);
|
}, PROBE_TICK_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceDerivedAuthState(record: AccountHealthRecord, now = Date.now()): AccountAuthState {
|
function coerceDerivedAuthState(record: AccountHealthRecord): AccountAuthState {
|
||||||
if (
|
|
||||||
record.authState === "active"
|
|
||||||
&& record.lastVerifiedAt
|
|
||||||
&& now - record.lastVerifiedAt >= STALE_AFTER_MS
|
|
||||||
) {
|
|
||||||
return "expiring_soon";
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.authState;
|
return record.authState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +432,18 @@ function markProbeQueued(record: AccountHealthRecord): AccountHealthSnapshot {
|
|||||||
return commitRecord(record, previousSignature);
|
return commitRecord(record, previousSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isProbeInFlight(record: AccountHealthRecord): boolean {
|
||||||
|
return record.probeState === "queued" || record.probeState === "probing";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStaleActiveRecord(record: AccountHealthRecord, now = Date.now()): boolean {
|
||||||
|
return (
|
||||||
|
record.authState === "active"
|
||||||
|
&& Boolean(record.lastVerifiedAt)
|
||||||
|
&& now - (record.lastVerifiedAt ?? 0) >= STALE_AFTER_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function maxAttemptsForTrigger(trigger: ProbeTrigger): number {
|
function maxAttemptsForTrigger(trigger: ProbeTrigger): number {
|
||||||
return trigger === "preflight" ? 1 : ACCOUNT_PROBE_RETRY_BACKOFF_MS.length + 1;
|
return trigger === "preflight" ? 1 : ACCOUNT_PROBE_RETRY_BACKOFF_MS.length + 1;
|
||||||
}
|
}
|
||||||
@@ -569,7 +571,10 @@ async function runDueProbes(): Promise<void> {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const dueAccounts = [...trackedAccounts.values()].filter((account) => {
|
const dueAccounts = [...trackedAccounts.values()].filter((account) => {
|
||||||
const record = records.get(account.id);
|
const record = records.get(account.id);
|
||||||
return Boolean(record?.nextProbeAt && record.nextProbeAt <= now);
|
if (!record || isProbeInFlight(record)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean((record.nextProbeAt && record.nextProbeAt <= now) || isStaleActiveRecord(record, now));
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const account of dueAccounts) {
|
for (const account of dueAccounts) {
|
||||||
@@ -653,6 +658,11 @@ function syncTrackedAccountsInternal(
|
|||||||
if (removed) {
|
if (removed) {
|
||||||
emitRuntimeInvalidated("account-health");
|
emitRuntimeInvalidated("account-health");
|
||||||
}
|
}
|
||||||
|
if (pruneMissing) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
void runDueProbes();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forgetTrackedAccountHealth(
|
export function forgetTrackedAccountHealth(
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ function registerBridgeHandlers(): void {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
safeHandle("desktop:probe-runtime-account", async (_event, accountId: string) => {
|
safeHandle("desktop:probe-runtime-account", async (_event, accountId: string) => {
|
||||||
requestRuntimeAccountProbe(accountId);
|
await requestRuntimeAccountProbe(accountId);
|
||||||
return captureRuntimeAccountSnapshot(accountId);
|
return captureRuntimeAccountSnapshot(accountId);
|
||||||
});
|
});
|
||||||
safeHandle(
|
safeHandle(
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import {
|
|||||||
startHiddenPlaywrightReaper,
|
startHiddenPlaywrightReaper,
|
||||||
} from "./playwright-cdp";
|
} from "./playwright-cdp";
|
||||||
import { getProcessMetricsSnapshot } from "./process-metrics";
|
import { getProcessMetricsSnapshot } from "./process-metrics";
|
||||||
|
import { onRuntimeInvalidated } from "./runtime-events";
|
||||||
import {
|
import {
|
||||||
initScheduler,
|
initScheduler,
|
||||||
noteSchedulerAccountsSync,
|
noteSchedulerAccountsSync,
|
||||||
@@ -97,7 +98,7 @@ import {
|
|||||||
setSchedulerPhase,
|
setSchedulerPhase,
|
||||||
setSchedulerQueueDepth,
|
setSchedulerQueueDepth,
|
||||||
} from "./scheduler";
|
} from "./scheduler";
|
||||||
import { clearSessionHandle, createSessionHandle } from "./session-registry";
|
import { clearSessionHandle, createSessionHandle, getPersistedPartition, getSessionHandle } from "./session-registry";
|
||||||
import {
|
import {
|
||||||
cancelDesktopTask,
|
cancelDesktopTask,
|
||||||
completeDesktopTask,
|
completeDesktopTask,
|
||||||
@@ -239,6 +240,8 @@ interface RuntimeState {
|
|||||||
pullTimer: ReturnType<typeof setInterval> | null;
|
pullTimer: ReturnType<typeof setInterval> | null;
|
||||||
accountHealthReportTimer: ReturnType<typeof setTimeout> | null;
|
accountHealthReportTimer: ReturnType<typeof setTimeout> | null;
|
||||||
accountHealthReportQueue: Map<string, DesktopAccountHealthReport>;
|
accountHealthReportQueue: Map<string, DesktopAccountHealthReport>;
|
||||||
|
accountHealthReportBadAccounts: Set<string>;
|
||||||
|
accountRemoteHealth: Map<string, DesktopAccountInfo["health"]>;
|
||||||
dispatchWsClient: DispatchWsClient | null;
|
dispatchWsClient: DispatchWsClient | null;
|
||||||
dispatchWsConnected: boolean;
|
dispatchWsConnected: boolean;
|
||||||
lastHeartbeatAt: number;
|
lastHeartbeatAt: number;
|
||||||
@@ -266,6 +269,8 @@ const state: RuntimeState = {
|
|||||||
pullTimer: null,
|
pullTimer: null,
|
||||||
accountHealthReportTimer: null,
|
accountHealthReportTimer: null,
|
||||||
accountHealthReportQueue: new Map<string, DesktopAccountHealthReport>(),
|
accountHealthReportQueue: new Map<string, DesktopAccountHealthReport>(),
|
||||||
|
accountHealthReportBadAccounts: new Set<string>(),
|
||||||
|
accountRemoteHealth: new Map<string, DesktopAccountInfo["health"]>(),
|
||||||
dispatchWsClient: null,
|
dispatchWsClient: null,
|
||||||
dispatchWsConnected: false,
|
dispatchWsConnected: false,
|
||||||
lastHeartbeatAt: 0,
|
lastHeartbeatAt: 0,
|
||||||
@@ -277,6 +282,7 @@ const state: RuntimeState = {
|
|||||||
lastError: null,
|
lastError: null,
|
||||||
activitySeq: 0,
|
activitySeq: 0,
|
||||||
};
|
};
|
||||||
|
let accountHealthReportBridgeUnsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
function accountIdentityFromDesktopAccount(account: DesktopAccountInfo): PublishAccountIdentity {
|
function accountIdentityFromDesktopAccount(account: DesktopAccountInfo): PublishAccountIdentity {
|
||||||
return {
|
return {
|
||||||
@@ -309,6 +315,14 @@ function isDefinitiveAuthState(authState: string): boolean {
|
|||||||
return ["active", "expired", "challenge_required", "revoked"].includes(authState);
|
return ["active", "expired", "challenge_required", "revoked"].includes(authState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasLocalAccountSession(accountId: string): boolean {
|
||||||
|
return Boolean(getSessionHandle(accountId) || getPersistedPartition(accountId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldReportAccountPresence(account: DesktopAccountInfo): boolean {
|
||||||
|
return hasLocalAccountSession(account.id) || account.client_id === state.client?.id;
|
||||||
|
}
|
||||||
|
|
||||||
function runtimePlatformLabel(platform: string): string {
|
function runtimePlatformLabel(platform: string): string {
|
||||||
return getAIPlatformCatalogItem(platform)?.label ?? platform;
|
return getAIPlatformCatalogItem(platform)?.label ?? platform;
|
||||||
}
|
}
|
||||||
@@ -473,6 +487,7 @@ function startRuntime(): void {
|
|||||||
initMonitorScheduler();
|
initMonitorScheduler();
|
||||||
initPublishScheduler();
|
initPublishScheduler();
|
||||||
startHiddenPlaywrightReaper();
|
startHiddenPlaywrightReaper();
|
||||||
|
ensureAccountHealthReportBridge();
|
||||||
state.running = true;
|
state.running = true;
|
||||||
setSchedulerPhase("running");
|
setSchedulerPhase("running");
|
||||||
setSchedulerError(null);
|
setSchedulerError(null);
|
||||||
@@ -488,6 +503,19 @@ function startRuntime(): void {
|
|||||||
pumpExecutionLoop();
|
pumpExecutionLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureAccountHealthReportBridge(): void {
|
||||||
|
if (accountHealthReportBridgeUnsubscribe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountHealthReportBridgeUnsubscribe = onRuntimeInvalidated((event) => {
|
||||||
|
if (event.reason !== "account-health" || !event.accountId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enqueueAccountHealthReport(event.accountId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function stopRuntime(): void {
|
function stopRuntime(): void {
|
||||||
state.running = false;
|
state.running = false;
|
||||||
state.leaseInFlightKinds.clear();
|
state.leaseInFlightKinds.clear();
|
||||||
@@ -532,6 +560,8 @@ function clearRuntimeState(): void {
|
|||||||
state.accounts = [];
|
state.accounts = [];
|
||||||
state.accountProfiles.clear();
|
state.accountProfiles.clear();
|
||||||
state.accountHealthReportQueue.clear();
|
state.accountHealthReportQueue.clear();
|
||||||
|
state.accountHealthReportBadAccounts.clear();
|
||||||
|
state.accountRemoteHealth.clear();
|
||||||
state.activeExecutions.clear();
|
state.activeExecutions.clear();
|
||||||
state.lastHeartbeatAt = 0;
|
state.lastHeartbeatAt = 0;
|
||||||
state.lastHeartbeatStatus = "idle";
|
state.lastHeartbeatStatus = "idle";
|
||||||
@@ -860,14 +890,7 @@ async function sendHeartbeat(source: "startup" | "timer"): Promise<void> {
|
|||||||
channel: state.client?.channel ?? "dev",
|
channel: state.client?.channel ?? "dev",
|
||||||
startup: source === "startup",
|
startup: source === "startup",
|
||||||
account_ids: state.accounts
|
account_ids: state.accounts
|
||||||
.filter((account) =>
|
.filter((account) => shouldReportAccountPresence(account))
|
||||||
getProjectedAccountHealth({
|
|
||||||
accountId: account.id,
|
|
||||||
platform: account.platform,
|
|
||||||
health: account.health,
|
|
||||||
verifiedAt: account.verified_at,
|
|
||||||
}).health === "live",
|
|
||||||
)
|
|
||||||
.map((account) => account.id),
|
.map((account) => account.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -918,6 +941,7 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
|
|||||||
|
|
||||||
const identities = accounts.map((account) => accountIdentityFromDesktopAccount(account));
|
const identities = accounts.map((account) => accountIdentityFromDesktopAccount(account));
|
||||||
syncTrackedAccounts(identities);
|
syncTrackedAccounts(identities);
|
||||||
|
state.accountRemoteHealth = new Map(accounts.map((account) => [account.id, account.health]));
|
||||||
|
|
||||||
state.accountProfiles.clear();
|
state.accountProfiles.clear();
|
||||||
const syncedAccounts: Array<DesktopAccountInfo | null> = accounts.map((account) => {
|
const syncedAccounts: Array<DesktopAccountInfo | null> = accounts.map((account) => {
|
||||||
@@ -1036,11 +1060,15 @@ function buildAccountHealthReport(account: DesktopAccountInfo): DesktopAccountHe
|
|||||||
health: account.health,
|
health: account.health,
|
||||||
verifiedAt: account.verified_at,
|
verifiedAt: account.verified_at,
|
||||||
});
|
});
|
||||||
|
const health =
|
||||||
|
projected.authState === "expiring_soon" && projected.probeState !== "network_error"
|
||||||
|
? "live"
|
||||||
|
: projected.health;
|
||||||
return {
|
return {
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
platform: account.platform,
|
platform: account.platform,
|
||||||
platform_uid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
|
platform_uid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
|
||||||
health: projected.health,
|
health,
|
||||||
auth_state: projected.authState,
|
auth_state: projected.authState,
|
||||||
probe_state: projected.probeState,
|
probe_state: projected.probeState,
|
||||||
auth_reason: projected.authReason,
|
auth_reason: projected.authReason,
|
||||||
@@ -1051,13 +1079,47 @@ function buildAccountHealthReport(account: DesktopAccountInfo): DesktopAccountHe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAccountHealthFailureReport(report: DesktopAccountHealthReport): boolean {
|
||||||
|
return (
|
||||||
|
report.health !== "live"
|
||||||
|
|| report.auth_state === "expired"
|
||||||
|
|| report.auth_state === "challenge_required"
|
||||||
|
|| report.auth_state === "revoked"
|
||||||
|
|| report.probe_state === "network_error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldQueueAccountHealthReport(
|
||||||
|
account: DesktopAccountInfo,
|
||||||
|
report: DesktopAccountHealthReport,
|
||||||
|
): boolean {
|
||||||
|
if (isAccountHealthFailureReport(report)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.accountHealthReportBadAccounts.has(account.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (state.accountRemoteHealth.get(account.id) ?? account.health) !== "live";
|
||||||
|
}
|
||||||
|
|
||||||
function enqueueAccountHealthReport(accountId: string): void {
|
function enqueueAccountHealthReport(accountId: string): void {
|
||||||
const account = state.accounts.find((item) => item.id === accountId) ?? null;
|
const account = state.accounts.find((item) => item.id === accountId) ?? null;
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.accountHealthReportQueue.set(account.id, buildAccountHealthReport(account));
|
const report = buildAccountHealthReport(account);
|
||||||
|
if (!shouldQueueAccountHealthReport(account, report)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccountHealthFailureReport(report)) {
|
||||||
|
state.accountHealthReportBadAccounts.add(account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.accountHealthReportQueue.set(account.id, report);
|
||||||
if (state.accountHealthReportTimer) {
|
if (state.accountHealthReportTimer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1080,6 +1142,14 @@ async function flushAccountHealthReports(): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await reportDesktopAccountHealth({ reports });
|
await reportDesktopAccountHealth({ reports });
|
||||||
|
for (const report of reports) {
|
||||||
|
state.accountRemoteHealth.set(report.account_id, report.health);
|
||||||
|
if (isAccountHealthFailureReport(report)) {
|
||||||
|
state.accountHealthReportBadAccounts.add(report.account_id);
|
||||||
|
} else {
|
||||||
|
state.accountHealthReportBadAccounts.delete(report.account_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[desktop-runtime] account health report failed", {
|
console.warn("[desktop-runtime] account health report failed", {
|
||||||
count: reports.length,
|
count: reports.length,
|
||||||
@@ -1156,32 +1226,32 @@ export async function refreshRuntimeAccounts(): Promise<void> {
|
|||||||
await syncAccounts("manual");
|
await syncAccounts("manual");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestRuntimeAccountProbe(
|
export async function requestRuntimeAccountProbe(
|
||||||
accountId: string,
|
accountId: string,
|
||||||
options: { account?: PublishAccountIdentity; force?: boolean } = {},
|
options: { account?: PublishAccountIdentity; force?: boolean } = {},
|
||||||
): void {
|
): Promise<void> {
|
||||||
const existingAccount = state.accounts.find((item) => item.id === accountId) ?? null;
|
const existingAccount = state.accounts.find((item) => item.id === accountId) ?? null;
|
||||||
const account = options.account ?? (existingAccount ? accountIdentityFromDesktopAccount(existingAccount) : null);
|
const account = options.account ?? (existingAccount ? accountIdentityFromDesktopAccount(existingAccount) : null);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error("runtime_account_not_found");
|
throw new Error("runtime_account_not_found");
|
||||||
}
|
}
|
||||||
|
|
||||||
void probeTrackedAccount(account, {
|
try {
|
||||||
trigger: "manual",
|
await probeTrackedAccount(account, {
|
||||||
allowSilentRefresh: true,
|
trigger: "manual",
|
||||||
force: options.force ?? true,
|
allowSilentRefresh: true,
|
||||||
})
|
force: options.force ?? true,
|
||||||
.then(async () => {
|
|
||||||
enqueueAccountHealthReport(account.id);
|
|
||||||
await mirrorProjectedAccountProfile(account.id);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn("[desktop-runtime] account probe failed", {
|
|
||||||
accountId: account.id,
|
|
||||||
platform: account.platform,
|
|
||||||
message: errorMessage(error),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
enqueueAccountHealthReport(account.id);
|
||||||
|
await mirrorProjectedAccountProfile(account.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[desktop-runtime] account probe failed", {
|
||||||
|
accountId: account.id,
|
||||||
|
platform: account.platform,
|
||||||
|
message: errorMessage(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function noteRuntimeAccountBound(account: DesktopAccountInfo): void {
|
export function noteRuntimeAccountBound(account: DesktopAccountInfo): void {
|
||||||
|
|||||||
@@ -505,7 +505,7 @@ func (s *DesktopAccountService) applyRuntimeHealth(ctx context.Context, workspac
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
health := report.Health
|
health := effectiveDesktopAccountRuntimeHealth(report)
|
||||||
authState := report.AuthState
|
authState := report.AuthState
|
||||||
probeState := report.ProbeState
|
probeState := report.ProbeState
|
||||||
checkedAt := report.CheckedAt
|
checkedAt := report.CheckedAt
|
||||||
@@ -523,6 +523,20 @@ func (s *DesktopAccountService) applyRuntimeHealth(ctx context.Context, workspac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func effectiveDesktopAccountRuntimeHealth(report bufferedDesktopAccountHealthReport) string {
|
||||||
|
authState := strings.TrimSpace(report.AuthState)
|
||||||
|
probeState := strings.TrimSpace(report.ProbeState)
|
||||||
|
switch authState {
|
||||||
|
case "active":
|
||||||
|
return "live"
|
||||||
|
case "expiring_soon":
|
||||||
|
if probeState != "network_error" {
|
||||||
|
return "live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return report.Health
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DesktopAccountService) buildDesktopAccountView(
|
func (s *DesktopAccountService) buildDesktopAccountView(
|
||||||
account *repository.DesktopAccount,
|
account *repository.DesktopAccount,
|
||||||
clientMap map[uuid.UUID]*repository.DesktopClient,
|
clientMap map[uuid.UUID]*repository.DesktopClient,
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (s *PublishJobService) Create(ctx context.Context, actor auth.Actor, req Cr
|
|||||||
}
|
}
|
||||||
accountHealth := account.Health
|
accountHealth := account.Health
|
||||||
if report, ok := runtimeHealth[account.DesktopID]; ok && strings.TrimSpace(report.Health) != "" {
|
if report, ok := runtimeHealth[account.DesktopID]; ok && strings.TrimSpace(report.Health) != "" {
|
||||||
accountHealth = report.Health
|
accountHealth = effectiveDesktopAccountRuntimeHealth(report)
|
||||||
}
|
}
|
||||||
if accountHealth != "live" {
|
if accountHealth != "live" {
|
||||||
return nil, response.ErrConflict(40993, "desktop_account_not_publishable", "one or more selected desktop accounts require re-authorization before publishing")
|
return nil, response.ErrConflict(40993, "desktop_account_not_publishable", "one or more selected desktop accounts require re-authorization before publishing")
|
||||||
|
|||||||
Reference in New Issue
Block a user