Files
nekonest-cloud/db/device-control-plane.ts

192 lines
6.1 KiB
TypeScript

export const CONTROL_PLANE_CONTACT_FRESH_MS = 10 * 60 * 1_000;
export const CONTROL_PLANE_CONTACT_DELAYED_MS = 30 * 60 * 1_000;
const CONTROL_PLANE_CONTACT_FUTURE_SKEW_MS = 5 * 60 * 1_000;
const ADMIN_HOST_CONTACT_CTE = `
WITH latest_contact AS (
SELECT hosts.id, hosts.account_id, hosts.name, hosts.os,
hosts.daemon_version, accounts.email,
MAX(credentials.last_used_at) AS control_plane_last_seen_at
FROM hosts
INNER JOIN accounts ON accounts.id = hosts.account_id
LEFT JOIN device_credentials AS credentials ON credentials.host_id = hosts.id
WHERE hosts.lifecycle = 'active' AND hosts.slot_state = 'active'
GROUP BY hosts.id, hosts.account_id, hosts.name, hosts.os,
hosts.daemon_version, accounts.email
), categorized AS (
SELECT *, CASE
WHEN control_plane_last_seen_at IS NULL THEN 'never'
WHEN julianday(control_plane_last_seen_at) IS NULL
OR julianday(control_plane_last_seen_at) > julianday(?1) THEN 'invalid'
WHEN julianday(control_plane_last_seen_at) >= julianday(?2) THEN 'fresh'
WHEN julianday(control_plane_last_seen_at) >= julianday(?3) THEN 'delayed'
ELSE 'stale'
END AS contact_state
FROM latest_contact
)
`;
export const ADMIN_HOST_CONTROL_PLANE_SUMMARY_SQL = `${ADMIN_HOST_CONTACT_CTE}
SELECT COUNT(*) AS total_active,
SUM(CASE WHEN contact_state = 'fresh' THEN 1 ELSE 0 END) AS fresh,
SUM(CASE WHEN contact_state = 'delayed' THEN 1 ELSE 0 END) AS delayed,
SUM(CASE WHEN contact_state = 'stale' THEN 1 ELSE 0 END) AS stale,
SUM(CASE WHEN contact_state = 'never' THEN 1 ELSE 0 END) AS never,
SUM(CASE WHEN contact_state = 'invalid' THEN 1 ELSE 0 END) AS invalid,
SUM(CASE WHEN daemon_version IS NULL THEN 1 ELSE 0 END) AS version_unknown
FROM categorized
`;
export const ADMIN_HOST_CONTROL_PLANE_ATTENTION_SQL = `${ADMIN_HOST_CONTACT_CTE}
SELECT id, account_id, name, os, daemon_version, email,
control_plane_last_seen_at, contact_state
FROM categorized
WHERE contact_state <> 'fresh'
ORDER BY CASE contact_state
WHEN 'invalid' THEN 0
WHEN 'never' THEN 1
WHEN 'stale' THEN 2
WHEN 'delayed' THEN 3
ELSE 4
END,
control_plane_last_seen_at ASC, id ASC
LIMIT 25
`;
export const OWNED_HOSTS_WITH_CONTROL_PLANE_CONTACT_SQL = `
SELECT hosts.*,
(SELECT MAX(credentials.last_used_at)
FROM device_credentials AS credentials
WHERE credentials.host_id = hosts.id)
AS control_plane_last_seen_at
FROM hosts
WHERE hosts.account_id = ?
ORDER BY hosts.lifecycle = 'active' DESC, hosts.created_at DESC
`;
export type ControlPlaneContactState =
| "never"
| "fresh"
| "delayed"
| "stale"
| "invalid";
export type ControlPlaneContact = {
state: ControlPlaneContactState;
label: string;
detail: string;
tone: "good" | "warn" | "danger" | "neutral";
};
type AdminHostControlPlaneAggregateRow = Record<string, number | null>;
export type AdminHostControlPlaneAttentionRow = {
id: string;
account_id: string;
name: string;
os: string;
daemon_version: string | null;
email: string;
control_plane_last_seen_at: string | null;
contact_state: Exclude<ControlPlaneContactState, "fresh">;
};
export type AdminHostControlPlaneSnapshot = {
generatedAt: string;
totalActive: number;
fresh: number;
delayed: number;
stale: number;
never: number;
invalid: number;
versionUnknown: number;
attentionHosts: AdminHostControlPlaneAttentionRow[];
};
function aggregateCount(
row: AdminHostControlPlaneAggregateRow,
key: string,
): number {
const value = Number(row[key] ?? 0);
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
}
export function controlPlaneContactCutoffs(generatedAt: string) {
const nowMs = Date.parse(generatedAt);
if (!Number.isFinite(nowMs)) throw new Error("invalid_control_plane_contact_time");
return {
futureLimitAt: new Date(nowMs + CONTROL_PLANE_CONTACT_FUTURE_SKEW_MS).toISOString(),
freshCutoff: new Date(nowMs - CONTROL_PLANE_CONTACT_FRESH_MS).toISOString(),
delayedCutoff: new Date(nowMs - CONTROL_PLANE_CONTACT_DELAYED_MS).toISOString(),
};
}
export function deriveAdminHostControlPlaneSnapshot(input: {
generatedAt: string;
summary: AdminHostControlPlaneAggregateRow;
attentionHosts: AdminHostControlPlaneAttentionRow[];
}): AdminHostControlPlaneSnapshot {
return {
generatedAt: input.generatedAt,
totalActive: aggregateCount(input.summary, "total_active"),
fresh: aggregateCount(input.summary, "fresh"),
delayed: aggregateCount(input.summary, "delayed"),
stale: aggregateCount(input.summary, "stale"),
never: aggregateCount(input.summary, "never"),
invalid: aggregateCount(input.summary, "invalid"),
versionUnknown: aggregateCount(input.summary, "version_unknown"),
attentionHosts: input.attentionHosts,
};
}
export function deriveControlPlaneContact(
lastSeenAt: string | null,
nowMs = Date.now(),
): ControlPlaneContact {
if (!lastSeenAt) {
return {
state: "never",
label: "尚未联系",
detail: "daemon 还没有成功查询 Cloud 开通状态。",
tone: "neutral",
};
}
const seenAtMs = Date.parse(lastSeenAt);
if (
!Number.isFinite(seenAtMs) ||
seenAtMs > nowMs + CONTROL_PLANE_CONTACT_FUTURE_SKEW_MS
) {
return {
state: "invalid",
label: "时间待核实",
detail: "最近一次控制面签到时间无效或明显超前。",
tone: "warn",
};
}
const ageMs = Math.max(0, nowMs - seenAtMs);
if (ageMs <= CONTROL_PLANE_CONTACT_FRESH_MS) {
return {
state: "fresh",
label: "控制面正常",
detail: "daemon 最近成功查询了 Cloud 开通状态。",
tone: "good",
};
}
if (ageMs <= CONTROL_PLANE_CONTACT_DELAYED_MS) {
return {
state: "delayed",
label: "联系延迟",
detail: "daemon 一段时间没有再次查询 Cloud 开通状态。",
tone: "warn",
};
}
return {
state: "stale",
label: "长时间未联系",
detail: "daemon 已超过半小时没有查询 Cloud 开通状态。",
tone: "danger",
};
}