Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

platform-siblings.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

platform-siblings.tsBlame152 lines · 1 contributor
f295f78Dictation App1/**
2 * Cross-product health aggregator — fetches the `platform-status` endpoint
3 * of each sibling product (Crontech, Gluecron, GateTest) so the admin
4 * dashboard can render a single cross-product status panel.
5 *
6 * Contract (defined in docs/PLATFORM_STATUS.md):
7 *
8 * GET <base>/api/platform-status → {
9 * product, version, commit, healthy, timestamp, siblings
10 * }
11 *
12 * The endpoint is public + CORS-open + non-sensitive so no auth is required.
13 * We fetch server-side anyway to centralise timeouts + caching and avoid
14 * mixed-content or CORS surprises from admin browsers.
15 */
16
17const DEFAULTS = {
18 crontech: "https://crontech.ai/api/platform-status",
19 gluecron: "https://gluecron.com/api/platform-status",
20 gatetest: "https://gatetest.io/api/platform-status",
21} as const;
22
23export type SiblingId = keyof typeof DEFAULTS;
24
25export interface SiblingStatus {
26 id: SiblingId;
27 name: string;
28 url: string;
29 reachable: boolean;
30 healthy: boolean;
31 latencyMs: number | null;
32 version: string | null;
33 commit: string | null;
34 timestamp: string | null;
35 checkedAt: string;
36 error: string | null;
37}
38
39const DISPLAY_NAMES: Record<SiblingId, string> = {
40 crontech: "Crontech",
41 gluecron: "Gluecron",
42 gatetest: "GateTest",
43};
44
45const TIMEOUT_MS = 3000;
46const CACHE_TTL_MS = 30_000;
47
48interface CacheEntry {
49 value: SiblingStatus[];
50 expiresAt: number;
51}
52let cache: CacheEntry | null = null;
53
54export function siblingUrls(): Record<SiblingId, string> {
55 return {
56 crontech: process.env.CRONTECH_STATUS_URL || DEFAULTS.crontech,
57 gluecron: process.env.GLUECRON_STATUS_URL || DEFAULTS.gluecron,
58 gatetest: process.env.GATETEST_STATUS_URL || DEFAULTS.gatetest,
59 };
60}
61
62async function fetchOne(id: SiblingId, url: string): Promise<SiblingStatus> {
63 const checkedAt = new Date().toISOString();
64 const started = Date.now();
65 try {
66 const res = await fetch(url, {
67 method: "GET",
68 headers: { accept: "application/json" },
69 signal: AbortSignal.timeout(TIMEOUT_MS),
70 });
71 const latencyMs = Date.now() - started;
72 if (!res.ok) {
73 return {
74 id,
75 name: DISPLAY_NAMES[id],
76 url,
77 reachable: true,
78 healthy: false,
79 latencyMs,
80 version: null,
81 commit: null,
82 timestamp: null,
83 checkedAt,
84 error: `HTTP ${res.status}`,
85 };
86 }
87 const body = (await res.json()) as Partial<{
88 healthy: boolean;
89 version: string;
90 commit: string;
91 timestamp: string;
92 }>;
93 return {
94 id,
95 name: DISPLAY_NAMES[id],
96 url,
97 reachable: true,
98 healthy: body.healthy === true,
99 latencyMs,
100 version: typeof body.version === "string" ? body.version : null,
101 commit: typeof body.commit === "string" ? body.commit : null,
102 timestamp: typeof body.timestamp === "string" ? body.timestamp : null,
103 checkedAt,
104 error: null,
105 };
106 } catch (err) {
107 const latencyMs = Date.now() - started;
108 const message =
109 err instanceof Error
110 ? err.name === "TimeoutError" || err.name === "AbortError"
111 ? "timeout"
112 : err.message
113 : "unreachable";
114 return {
115 id,
116 name: DISPLAY_NAMES[id],
117 url,
118 reachable: false,
119 healthy: false,
120 latencyMs: latencyMs >= TIMEOUT_MS ? TIMEOUT_MS : latencyMs,
121 version: null,
122 commit: null,
123 timestamp: null,
124 checkedAt,
125 error: message,
126 };
127 }
128}
129
130/**
131 * Returns the health of all three sibling products. Cached for 30s so
132 * admin page reloads don't hammer neighbours. Always resolves — a failed
133 * fetch is reported as `{ reachable: false, healthy: false }`.
134 */
135export async function getSiblingStatuses(options?: {
136 force?: boolean;
137}): Promise<SiblingStatus[]> {
138 const now = Date.now();
139 if (!options?.force && cache && cache.expiresAt > now) {
140 return cache.value;
141 }
142 const urls = siblingUrls();
143 const ids = Object.keys(urls) as SiblingId[];
144 const value = await Promise.all(ids.map((id) => fetchOne(id, urls[id])));
145 cache = { value, expiresAt: now + CACHE_TTL_MS };
146 return value;
147}
148
149/** Test-only — resets the in-memory cache. */
150export function __resetSiblingCache(): void {
151 cache = null;
152}