Files
geo/apps/browser-extension/src/monitoring-runtime.ts
T
root b16e9f0bd1 feat(desktop): implement workspace foundation + desktop-client skeleton
Plan 0 (workspaces): add workspaces + workspace_memberships schema, extend
JWT/Actor/claims with primary_workspace_id, seed default workspace per tenant,
thread workspace_id through tenant monitoring quota.

Plan A (desktop skeleton): new Electron app (apps/desktop-client) with main/
preload/renderer, shared Vue component package (packages/ui-shared), and server
surface — desktop client registration + token rotation + heartbeat, SSE task
event stream, desktop accounts/tasks/content handlers, publish job endpoint,
and supporting repositories, services, sqlc queries, and migrations.

Hard cutover per plan: remove browser-extension monitoring callback endpoints,
stub legacy media API in admin-web, and delete monitoring_callback_handler.go.
2026-04-19 14:18:20 +08:00

709 lines
24 KiB
TypeScript

import type {
JsonValue,
MonitoringHeartbeatPlatformPayload,
MonitoringHeartbeatResponse,
MonitoringLeaseTask,
MonitoringLeaseTasksPayload,
MonitoringLeaseTasksResponse,
MonitoringLocalPlatformState,
MonitoringPluginPingResponse,
MonitoringResumeTasksPayload,
MonitoringResumeTasksResponse,
MonitoringSkipTaskPayload,
MonitoringSkipTaskResponse,
MonitoringTaskResultPayload,
MonitoringTaskResultResponse,
} from "@geo/shared-types";
import { browser } from "wxt/browser";
import { canQueryMonitoringPlatform, detectMonitoringPlatformState, queryMonitoringTask } from "./monitoring-adapters";
import type { MonitoringAdapterTaskContext } from "./monitoring-adapters/types";
import type { StoredMonitoringPlatformState } from "./monitoring-platforms";
import { getExtensionState, patchExtensionState, type ExtensionStorageState, type StoredMonitoringScheduledTask } from "./storage";
export type MonitoringAction = "ping" | "detectPlatforms" | "syncHeartbeat" | "runCycle" | "getState";
export type MonitoringCycleMode = "manual" | "automatic";
const monitoringCycleBatchLeaseLimit = 50;
const monitoringAutomaticDelayMinMs = 5 * 60 * 1000;
const monitoringAutomaticDelayMaxMs = 10 * 60 * 1000;
const monitoringRemoteHeartbeatFreshnessMs = 10 * 60 * 1000;
const monitoringBusinessTimeZone = "Asia/Shanghai";
export interface MonitoringHeartbeatSyncResult {
success: boolean;
source: "remote" | "local";
platform_states: MonitoringLocalPlatformState[];
heartbeat?: MonitoringHeartbeatResponse | null;
message?: string | null;
}
export interface MonitoringCycleResult {
success: boolean;
leased_task_count: number;
processed_task_count: number;
failed_task_count: number;
skipped_task_count: number;
message?: string | null;
}
export interface MonitoringRuntimeStateSnapshot {
installation_key: string;
plugin_installation_id: number | null;
api_base_url: string | null;
monitoring_last_heartbeat_at: string | null;
monitoring_last_heartbeat_status: string | null;
monitoring_last_cycle_at: string | null;
monitoring_last_cycle_status: string | null;
monitoring_platforms: StoredMonitoringPlatformState[];
}
function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
}
function normalizeText(value?: string | null): string | null {
const trimmed = value?.trim();
return trimmed ? trimmed : null;
}
function formatMonitoringBusinessDate(value: Date): string {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: monitoringBusinessTimeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(value);
const year = parts.find((part) => part.type === "year")?.value;
const month = parts.find((part) => part.type === "month")?.value;
const day = parts.find((part) => part.type === "day")?.value;
if (!year || !month || !day) {
throw new Error("monitoring_business_date_format_failed");
}
return `${year}-${month}-${day}`;
}
function currentMonitoringBusinessDate(): string {
return formatMonitoringBusinessDate(new Date());
}
function currentTimestampMs(): number {
return Date.now();
}
function toTimestampMs(value: string | null | undefined): number | null {
if (!value) {
return null;
}
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? null : timestamp;
}
function randomMonitoringDelayMs(): number {
return monitoringAutomaticDelayMinMs + Math.floor(Math.random() * (monitoringAutomaticDelayMaxMs - monitoringAutomaticDelayMinMs + 1));
}
function hasFreshSuccessfulMonitoringHeartbeat(state: ExtensionStorageState): boolean {
if (state.monitoring_last_heartbeat_status !== "success") {
return false;
}
const lastHeartbeatAt = toTimestampMs(state.monitoring_last_heartbeat_at);
if (lastHeartbeatAt == null) {
return false;
}
return currentTimestampMs() - lastHeartbeatAt < monitoringRemoteHeartbeatFreshnessMs;
}
function isImmediateMonitoringTask(task: MonitoringLeaseTask | StoredMonitoringScheduledTask): boolean {
return task.trigger_source === "manual";
}
function sortMonitoringTaskQueue(tasks: StoredMonitoringScheduledTask[]): StoredMonitoringScheduledTask[] {
return [...tasks].sort((left, right) => {
const leftTime = toTimestampMs(left.next_execute_at) ?? 0;
const rightTime = toTimestampMs(right.next_execute_at) ?? 0;
if (leftTime !== rightTime) {
return leftTime - rightTime;
}
return left.task_id - right.task_id;
});
}
function isMonitoringNotLoggedIn(platform: StoredMonitoringPlatformState): boolean {
return platform.reason_code === "not_logged_in" || (platform.access_requirement === "login_required" && !platform.connected);
}
function resolveAccessStatus(platform: StoredMonitoringPlatformState): string {
if (platform.accessible) {
return "accessible";
}
if (isMonitoringNotLoggedIn(platform)) {
return "not_logged_in";
}
return "unavailable";
}
function toMonitoringPlatformPayload(platform: StoredMonitoringPlatformState): MonitoringHeartbeatPlatformPayload {
return {
ai_platform_id: platform.ai_platform_id,
access_status: resolveAccessStatus(platform),
connected: platform.connected,
accessible: platform.accessible,
detected_at: platform.detected_at ?? null,
reason_code: platform.reason_code ?? null,
reason_message: platform.reason_message ?? null,
};
}
async function postMonitoringApi<T>(path: string, payload: unknown): Promise<T> {
void path;
void payload;
throw new Error("browser_extension_monitoring_removed_use_desktop_client");
}
async function detectAllMonitoringPlatforms(): Promise<StoredMonitoringPlatformState[]> {
const state = await getExtensionState();
return await Promise.all(state.monitoring_platforms.map((platform) => detectMonitoringPlatformState(platform)));
}
async function persistMonitoringPlatforms(platforms: StoredMonitoringPlatformState[]): Promise<StoredMonitoringPlatformState[]> {
const nextState = await patchExtensionState({
monitoring_platforms: platforms,
});
return nextState.monitoring_platforms;
}
function projectMonitoringPlatforms(platforms: StoredMonitoringPlatformState[]): MonitoringLocalPlatformState[] {
return platforms.map((platform) => ({
ai_platform_id: platform.ai_platform_id,
connected: platform.connected,
accessible: platform.accessible,
detected_at: platform.detected_at ?? null,
reason_code: platform.reason_code ?? null,
reason_message: platform.reason_message ?? null,
message: platform.message ?? null,
}));
}
function asMonitoringTaskContext(task: MonitoringLeaseTask): MonitoringAdapterTaskContext {
return {
task_id: task.task_id,
brand_id: task.brand_id,
question_id: task.question_id,
question_hash: task.question_hash,
question_text: task.question_text,
ai_platform_id: task.ai_platform_id,
platform_name: task.platform_name,
collector_type: task.collector_type,
run_mode: task.run_mode,
trigger_source: task.trigger_source,
business_date: task.business_date,
lease_token: task.lease_token,
lease_expires_at: task.lease_expires_at,
};
}
function asTaskResultPayload(task: MonitoringLeaseTask, result: Awaited<ReturnType<typeof queryMonitoringTask>>): MonitoringTaskResultPayload {
return {
lease_token: task.lease_token,
status: "succeeded",
provider_model: normalizeText(result.provider_model),
provider_request_id: normalizeText(result.provider_request_id),
request_id: normalizeText(result.request_id),
answer: normalizeText(result.answer),
raw_response_json: (result.raw_response_json as Record<string, JsonValue> | undefined) ?? {},
citations: result.citations ?? [],
search_results: result.search_results ?? [],
brand_mentioned: result.brand_mentioned ?? null,
brand_mention_position: normalizeText(result.brand_mention_position),
first_recommended: result.first_recommended ?? null,
sentiment_label: normalizeText(result.sentiment_label),
matched_brand_terms: result.matched_brand_terms ?? [],
};
}
async function markMonitoringTaskSkipped(task: MonitoringLeaseTask, reason: string, errorMessage?: string | null): Promise<MonitoringSkipTaskResponse> {
const payload: MonitoringSkipTaskPayload = {
lease_token: task.lease_token,
skip_reason: reason,
error_message: normalizeText(errorMessage),
completed_at: new Date().toISOString(),
};
return await postMonitoringApi<MonitoringSkipTaskResponse>("skip_task", payload);
}
async function submitMonitoringTaskResult(task: MonitoringLeaseTask, payload: MonitoringTaskResultPayload): Promise<MonitoringTaskResultResponse> {
void task;
return await postMonitoringApi<MonitoringTaskResultResponse>("submit_result", payload);
}
async function processMonitoringLeasedTask(
platform: StoredMonitoringPlatformState,
task: MonitoringLeaseTask,
): Promise<"processed" | "failed" | "skipped"> {
if (!(await canQueryMonitoringPlatform(platform))) {
await markMonitoringTaskSkipped(task, "adapter_not_ready", `monitoring adapter is not ready for ${platform.ai_platform_id}`);
return "skipped";
}
try {
const result = await queryMonitoringTask(platform, asMonitoringTaskContext(task));
await submitMonitoringTaskResult(task, asTaskResultPayload(task, result));
return "processed";
} catch (error) {
const message = error instanceof Error ? error.message : "monitoring_query_failed";
try {
await markMonitoringTaskSkipped(task, "query_failed", message);
} catch {
// Ignore secondary skip failures so the caller can continue draining the queue.
}
return "failed";
}
}
async function resumeMonitoringTasks(aiPlatformIDs: string[]): Promise<MonitoringLeaseTask[]> {
const payload: MonitoringResumeTasksPayload = {
ai_platform_ids: aiPlatformIDs,
};
const resumed = await postMonitoringApi<MonitoringResumeTasksResponse>("resume_tasks", payload);
return resumed.tasks;
}
async function leaseMonitoringTasks(aiPlatformIDs: string[]): Promise<MonitoringLeaseTasksResponse> {
const payload: MonitoringLeaseTasksPayload = {
limit: monitoringCycleBatchLeaseLimit,
ai_platform_ids: aiPlatformIDs,
};
return await postMonitoringApi<MonitoringLeaseTasksResponse>("lease_tasks", payload);
}
function mergeMonitoringQueueTasks(
existingQueue: StoredMonitoringScheduledTask[],
incomingTasks: MonitoringLeaseTask[],
): StoredMonitoringScheduledTask[] {
if (incomingTasks.length === 0) {
return sortMonitoringTaskQueue(existingQueue);
}
const nowMs = currentTimestampMs();
const queueByTaskID = new Map(existingQueue.map((task) => [task.task_id, task]));
let latestDelayedExecuteAt = existingQueue.reduce((latest, task) => {
if (task.pacing_mode !== "delayed") {
return latest;
}
const executeAt = toTimestampMs(task.next_execute_at);
return executeAt == null ? latest : Math.max(latest, executeAt);
}, nowMs);
for (const task of incomingTasks) {
if (task.business_date !== currentMonitoringBusinessDate()) {
continue;
}
const existing = queueByTaskID.get(task.task_id);
if (existing) {
queueByTaskID.set(task.task_id, {
...existing,
...task,
});
continue;
}
const immediate = isImmediateMonitoringTask(task);
if (!immediate) {
latestDelayedExecuteAt = Math.max(latestDelayedExecuteAt, nowMs) + randomMonitoringDelayMs();
}
queueByTaskID.set(task.task_id, {
...task,
pacing_mode: immediate ? "immediate" : "delayed",
scheduled_at: new Date(nowMs).toISOString(),
next_execute_at: immediate ? new Date(nowMs).toISOString() : new Date(latestDelayedExecuteAt).toISOString(),
});
}
return sortMonitoringTaskQueue(Array.from(queueByTaskID.values()));
}
async function persistMonitoringTaskQueue(queue: StoredMonitoringScheduledTask[]): Promise<StoredMonitoringScheduledTask[]> {
const nextState = await patchExtensionState({
monitoring_task_queue: sortMonitoringTaskQueue(queue),
});
return nextState.monitoring_task_queue;
}
async function pruneMonitoringTaskQueue(): Promise<StoredMonitoringScheduledTask[]> {
const state = await getExtensionState();
const today = currentMonitoringBusinessDate();
const retained: StoredMonitoringScheduledTask[] = [];
for (const task of state.monitoring_task_queue) {
if (task.business_date === today) {
retained.push(task);
continue;
}
try {
await markMonitoringTaskSkipped(
task,
"stale_business_date",
`monitoring task business_date ${task.business_date} is not the current business date`,
);
} catch {
// Best-effort cleanup for locally cached stale tasks.
}
}
return await persistMonitoringTaskQueue(retained);
}
async function syncMonitoringTaskQueue(aiPlatformIDs: string[]): Promise<StoredMonitoringScheduledTask[]> {
const localQueue = await pruneMonitoringTaskQueue();
const resumedTasks = await resumeMonitoringTasks(aiPlatformIDs);
const resumedTaskMap = new Map(resumedTasks.map((task) => [task.task_id, task]));
const matchedQueue: StoredMonitoringScheduledTask[] = [];
for (const task of localQueue) {
const resumedTask = resumedTaskMap.get(task.task_id);
if (!resumedTask) {
continue;
}
matchedQueue.push({
...task,
...resumedTask,
});
resumedTaskMap.delete(task.task_id);
}
const mergedQueue = mergeMonitoringQueueTasks(matchedQueue, Array.from(resumedTaskMap.values()));
return await persistMonitoringTaskQueue(mergedQueue);
}
async function appendMonitoringTasksToQueue(tasks: MonitoringLeaseTask[]): Promise<StoredMonitoringScheduledTask[]> {
const state = await getExtensionState();
const mergedQueue = mergeMonitoringQueueTasks(state.monitoring_task_queue, tasks);
return await persistMonitoringTaskQueue(mergedQueue);
}
async function removeMonitoringTaskFromQueue(taskID: number): Promise<StoredMonitoringScheduledTask[]> {
const state = await getExtensionState();
return await persistMonitoringTaskQueue(state.monitoring_task_queue.filter((task) => task.task_id !== taskID));
}
function isMonitoringTaskDue(task: StoredMonitoringScheduledTask, nowMs: number): boolean {
const executeAt = toTimestampMs(task.next_execute_at);
return executeAt == null || executeAt <= nowMs;
}
async function processQueuedMonitoringTask(
task: StoredMonitoringScheduledTask,
platformMap: Map<string, StoredMonitoringPlatformState>,
): Promise<"processed" | "failed" | "skipped"> {
if (task.business_date !== currentMonitoringBusinessDate()) {
await markMonitoringTaskSkipped(
task,
"stale_business_date",
`monitoring task business_date ${task.business_date} is not the current business date`,
);
return "skipped";
}
const platform = platformMap.get(task.ai_platform_id);
if (!platform) {
await markMonitoringTaskSkipped(task, "platform_not_configured", `monitoring platform ${task.ai_platform_id} is not configured locally`);
return "skipped";
}
return await processMonitoringLeasedTask(platform, task);
}
async function processImmediateMonitoringTasks(
platformMap: Map<string, StoredMonitoringPlatformState>,
): Promise<{ processedCount: number; failedCount: number; skippedCount: number }> {
let processedCount = 0;
let failedCount = 0;
let skippedCount = 0;
for (;;) {
const state = await getExtensionState();
const dueImmediateTask = state.monitoring_task_queue.find(
(task) => task.pacing_mode === "immediate" && isMonitoringTaskDue(task, currentTimestampMs()),
);
if (!dueImmediateTask) {
break;
}
const outcome = await processQueuedMonitoringTask(dueImmediateTask, platformMap);
await removeMonitoringTaskFromQueue(dueImmediateTask.task_id);
if (outcome === "processed") {
processedCount += 1;
continue;
}
if (outcome === "failed") {
failedCount += 1;
continue;
}
skippedCount += 1;
}
return {
processedCount,
failedCount,
skippedCount,
};
}
async function processNextDelayedMonitoringTask(
platformMap: Map<string, StoredMonitoringPlatformState>,
): Promise<{ processedCount: number; failedCount: number; skippedCount: number }> {
const state = await getExtensionState();
const delayedTask = state.monitoring_task_queue.find(
(task) => task.pacing_mode === "delayed" && isMonitoringTaskDue(task, currentTimestampMs()),
);
if (!delayedTask) {
return {
processedCount: 0,
failedCount: 0,
skippedCount: 0,
};
}
const outcome = await processQueuedMonitoringTask(delayedTask, platformMap);
await removeMonitoringTaskFromQueue(delayedTask.task_id);
if (outcome === "processed") {
return { processedCount: 1, failedCount: 0, skippedCount: 0 };
}
if (outcome === "failed") {
return { processedCount: 0, failedCount: 1, skippedCount: 0 };
}
return { processedCount: 0, failedCount: 0, skippedCount: 1 };
}
export async function handleMonitoringAction(
action: MonitoringAction,
): Promise<
MonitoringPluginPingResponse | MonitoringLocalPlatformState[] | MonitoringHeartbeatSyncResult | MonitoringCycleResult | MonitoringRuntimeStateSnapshot
> {
switch (action) {
case "ping":
return await handleMonitoringPing();
case "detectPlatforms":
return await handleMonitoringDetectPlatforms();
case "syncHeartbeat":
return await handleMonitoringSyncHeartbeat(false);
case "runCycle":
return await runMonitoringCycle("manual");
case "getState":
return await handleMonitoringGetState();
default:
throw new Error("monitoring_action_not_supported");
}
}
export async function runMonitoringCycle(mode: MonitoringCycleMode): Promise<MonitoringCycleResult> {
return await handleMonitoringRunCycle(mode);
}
async function handleMonitoringPing(): Promise<MonitoringPluginPingResponse> {
const state = await getExtensionState();
return {
installed: true,
version: browser.runtime.getManifest().version,
capabilities: ["ping", "detect", "heartbeat", "lease", "background"],
installation_key: state.installation_key,
plugin_installation_id: state.plugin_installation_id,
};
}
async function handleMonitoringDetectPlatforms(): Promise<MonitoringLocalPlatformState[]> {
const platforms = await detectAllMonitoringPlatforms();
const persisted = await persistMonitoringPlatforms(platforms);
return projectMonitoringPlatforms(persisted);
}
async function handleMonitoringSyncHeartbeat(allowCachedRemote: boolean): Promise<MonitoringHeartbeatSyncResult> {
const detectedPlatforms = await detectAllMonitoringPlatforms();
const persistedPlatforms = await persistMonitoringPlatforms(detectedPlatforms);
const projectedPlatforms = projectMonitoringPlatforms(persistedPlatforms);
const heartbeatAt = new Date().toISOString();
const state = await getExtensionState();
if (!state.plugin_installation_id || !state.installation_token || !state.api_base_url) {
await patchExtensionState({
monitoring_last_heartbeat_at: heartbeatAt,
monitoring_last_heartbeat_status: "local_only",
});
return {
success: true,
source: "local",
platform_states: projectedPlatforms,
heartbeat: null,
message: "monitoring installation is not registered yet",
};
}
if (allowCachedRemote && hasFreshSuccessfulMonitoringHeartbeat(state)) {
return {
success: true,
source: "local",
platform_states: projectedPlatforms,
heartbeat: null,
message: "reused recent monitoring heartbeat",
};
}
try {
const heartbeat = await postMonitoringApi<MonitoringHeartbeatResponse>("heartbeat", {
platforms: persistedPlatforms.map(toMonitoringPlatformPayload),
});
await patchExtensionState({
monitoring_last_heartbeat_at: heartbeatAt,
monitoring_last_heartbeat_status: "success",
});
return {
success: true,
source: "remote",
platform_states: projectedPlatforms,
heartbeat,
message: null,
};
} catch (error) {
await patchExtensionState({
monitoring_last_heartbeat_at: heartbeatAt,
monitoring_last_heartbeat_status: "failed",
});
throw error;
}
}
async function handleMonitoringRunCycle(mode: MonitoringCycleMode): Promise<MonitoringCycleResult> {
let heartbeatResult: MonitoringHeartbeatSyncResult;
try {
heartbeatResult = await handleMonitoringSyncHeartbeat(mode === "automatic");
} catch (error) {
await patchExtensionState({
monitoring_last_cycle_at: new Date().toISOString(),
monitoring_last_cycle_status: "heartbeat_failed",
});
throw error;
}
const state = await getExtensionState();
const accessiblePlatforms = state.monitoring_platforms.filter((platform) => platform.accessible);
const queryReadyPlatforms = (
await Promise.all(
accessiblePlatforms.map(async (platform) => ({
platform,
canQuery: await canQueryMonitoringPlatform(platform),
})),
)
)
.filter((item) => item.canQuery)
.map((item) => item.platform);
if (!state.plugin_installation_id || !state.installation_token || !state.api_base_url) {
await patchExtensionState({
monitoring_last_cycle_at: new Date().toISOString(),
monitoring_last_cycle_status: "not_configured",
});
return {
success: false,
leased_task_count: 0,
processed_task_count: 0,
failed_task_count: 0,
skipped_task_count: 0,
message: "monitoring installation is not registered yet",
};
}
if (queryReadyPlatforms.length === 0) {
await patchExtensionState({
monitoring_last_cycle_at: new Date().toISOString(),
monitoring_last_cycle_status: "no_query_capable_platforms",
});
return {
success: heartbeatResult.success,
leased_task_count: 0,
processed_task_count: 0,
failed_task_count: 0,
skipped_task_count: 0,
message: "monitoring adapters are not query-ready yet",
};
}
const queryReadyPlatformIDs = queryReadyPlatforms.map((platform) => platform.ai_platform_id);
const platformMap = new Map(queryReadyPlatforms.map((platform) => [platform.ai_platform_id, platform] as const));
let leasedTaskCount = 0;
let processedCount = 0;
let failedCount = 0;
let skippedCount = 0;
let queue = await syncMonitoringTaskQueue(queryReadyPlatformIDs);
if (mode === "manual" || queue.length === 0) {
const leased = await leaseMonitoringTasks(queryReadyPlatformIDs);
leasedTaskCount += leased.lease_count;
if (leased.tasks.length > 0) {
queue = await appendMonitoringTasksToQueue(leased.tasks);
}
}
const immediateResult = await processImmediateMonitoringTasks(platformMap);
processedCount += immediateResult.processedCount;
failedCount += immediateResult.failedCount;
skippedCount += immediateResult.skippedCount;
if (mode === "automatic" && immediateResult.processedCount === 0) {
const delayedResult = await processNextDelayedMonitoringTask(platformMap);
processedCount += delayedResult.processedCount;
failedCount += delayedResult.failedCount;
skippedCount += delayedResult.skippedCount;
}
const remainingQueue = (await getExtensionState()).monitoring_task_queue;
const cycleStatus =
failedCount > 0
? "partial_failed"
: processedCount > 0
? "success"
: skippedCount > 0
? "skipped"
: remainingQueue.length > 0
? "scheduled"
: "idle";
await patchExtensionState({
monitoring_last_cycle_at: new Date().toISOString(),
monitoring_last_cycle_status: cycleStatus,
});
return {
success: failedCount === 0,
leased_task_count: leasedTaskCount,
processed_task_count: processedCount,
failed_task_count: failedCount,
skipped_task_count: skippedCount,
message: remainingQueue.length > 0 ? `scheduled ${remainingQueue.length} monitoring tasks` : null,
};
}
async function handleMonitoringGetState(): Promise<MonitoringRuntimeStateSnapshot> {
const state = await getExtensionState();
return {
installation_key: state.installation_key,
plugin_installation_id: state.plugin_installation_id,
api_base_url: state.api_base_url,
monitoring_last_heartbeat_at: state.monitoring_last_heartbeat_at,
monitoring_last_heartbeat_status: state.monitoring_last_heartbeat_status,
monitoring_last_cycle_at: state.monitoring_last_cycle_at,
monitoring_last_cycle_status: state.monitoring_last_cycle_status,
monitoring_platforms: state.monitoring_platforms,
};
}