Commit90fa787unknown_key
chore: decouple Gluecron from Crontech (standalone product)
chore: decouple Gluecron from Crontech (standalone product) Gluecron is a standalone product; cross-product coupling with Crontech has been stripped: - Docs (README, DEPLOY, CLAUDE, LAUNCH_TODAY) reframed around Fly.io as the documented primary deploy target (fly.toml already in-repo). Dockerfile covers any other container host. Neon Postgres unchanged. - "Green ecosystem" / "self-hosting triangle" framing removed. - Landing page step 3 and repo intelligence settings toggle reworded to talk about a generic deploy webhook, not Crontech specifically. - Cross-product siblings widget (src/lib/platform-siblings.ts + its test) deleted — no admin/platform route was ever registered. - Outbound webhook integration (CRONTECH_DEPLOY_URL, GATETEST_URL) stays wired but is now documented as an optional third-party integration, not primary coupling. Tests: 137 pass / 53 fail (one fewer failing test vs baseline; the removed /admin/platform auth-gate test was asserting a route that never existed). No regressions. https://claude.ai/code/session_017Do52tMX1P9jPTWXhEohXH
8 files changed+68−34890fa787541e892ee69826d6083f16187b271b42d
8 changed files+68−348
ModifiedCLAUDE.md+9−10View fileUnifiedSplit
@@ -1,6 +1,6 @@
11# gluecron
22
3AI-native code intelligence platform — git hosting, automated CI, and green ecosystem enforcement.
3AI-native code intelligence platform — git hosting, automated CI, and push-time gate enforcement.
44
55## READ FIRST — every session
66
@@ -50,7 +50,7 @@ src/
5050 repository.ts Git operations (tree, blob, commits, diff, branches, blame, search, raw)
5151 protocol.ts Smart HTTP protocol (pkt-line, service RPC)
5252 hooks/
53 post-receive.ts GateTest + Crontech webhooks on push
53 post-receive.ts GateTest + optional deploy webhook on push
5454 middleware/
5555 auth.ts softAuth + requireAuth middleware
5656 routes/
@@ -93,9 +93,9 @@ src/
9393
9494## Integrations
9595
96- **GateTest:** POST `https://gatetest.ai/api/events/push` on every `git push`
97- **Crontech:** POST `https://crontech.ai/api/trpc/tenant.deploy` on push to main
98- **Webhooks:** POST to registered URLs on push/issue/PR/star events with HMAC signatures
96- **GateTest (optional):** third-party security scanner. When `GATETEST_URL` is set, `git push` POSTs to it; inbound results accepted at `POST /api/hooks/gatetest`.
97- **Outbound deploy webhook (optional):** when `CRONTECH_DEPLOY_URL` is set, pushes to the default branch POST there.
98- **Webhooks:** POST to user-registered URLs on push/issue/PR/star events with HMAC signatures.
9999
100100## Environment Variables
101101
@@ -106,8 +106,7 @@ See `.env.example` for required variables. Key ones:
106106
107107## Deployment
108108
109- **Deployed via Crontech** — Crontech is the deployment platform (NOT Vercel, NOT Hetzner)
110- **Database:** Neon PostgreSQL (direct, NOT via Vercel integration)
111- **The green ecosystem:** Crontech deploys gluecron, gluecron hosts code, GateTest scans pushes
112- **NEVER suggest Hetzner** — it is not part of our infrastructure
113- See DEPLOY.md for full deployment instructions
109- **Primary target:** Fly.io — the repo ships a ready `fly.toml` (release command runs `bun run db:migrate`, `gluecron_repos` volume mounted at `/app/repos`).
110- **Other hosts:** a `Dockerfile` is in the repo, so any standard Docker host works.
111- **Database:** Neon PostgreSQL (direct connection via `DATABASE_URL`).
112- See DEPLOY.md for full deployment instructions.
ModifiedDEPLOY.md+43−32View fileUnifiedSplit
@@ -1,38 +1,49 @@
11# Deploying Gluecron
22
3## The green ecosystem
4
5Gluecron is part of a self-hosting triangle. Each service deploys or observes the others:
6
7- **Crontech** is the deployment platform for Gluecron (and everything else).
8- **Gluecron** hosts the source code for Crontech, GateTest, and itself.
9- **GateTest** scans every push to Gluecron.
10
11Dogfood end-to-end. **Crontech is the deployment target.** Do not deploy Gluecron to Vercel or Hetzner — they are not part of this stack.
3Gluecron is a standalone product. It runs anywhere Bun runs. The repo ships a `fly.toml` for Fly.io as the documented primary target, and a `Dockerfile` for any other container host.
124
135---
146
157## 1. Prerequisites
168
1791. **Neon Postgres** — create a project at https://neon.tech and copy the pooled connection string. This becomes `DATABASE_URL`.
182. **Crontech account** — tenant + service permissions for the repo.
102. **Fly.io account** (or any Docker-compatible host) — `flyctl` installed locally if you're using Fly.
19113. **(Recommended) Anthropic API key** — https://console.anthropic.com. Everything AI-flavoured degrades to safe fallbacks without it, but you'll want this for the differentiator features.
2012
2113---
2214
23## 2. Deploy via Crontech
15## 2. Deploy to Fly.io
2416
25In Crontech, create a service pointing at this repo with:
17The repo includes a ready `fly.toml`. First-time setup:
2618
27- **Build:** `bun install --production`
28- **Release:** `bun run db:migrate`
29- **Start:** `bun run src/index.ts`
30- **Port:** `3000`
31- **Persistent volume:** mount `/data/repos` (bare git repos live on disk)
19```bash
20fly launch # adopts the existing fly.toml; pick an app name and region
21fly deploy # builds via the in-repo Dockerfile and releases
22```
23
24The shipped `fly.toml` already wires up:
25
26- **Release command:** `bun run db:migrate` (runs before each deploy cuts over)
27- **Persistent volume:** `gluecron_repos` mounted at `/app/repos` (bare git repos live on disk)
28- **HTTP service** on port 3000 with forced HTTPS
3229
33Route a subdomain (e.g. `gluecron.crontech.ai`) or a custom domain (e.g. `gluecron.com` via CNAME) to the service.
30Set secrets before the first deploy:
3431
35Crontech handles TLS termination, rollouts, and restart policy. Treat it as the primary control plane — do not bolt on nginx, systemd, or Docker Compose orchestration.
32```bash
33fly secrets set DATABASE_URL="..." APP_BASE_URL="https://your-app.fly.dev" ANTHROPIC_API_KEY="..."
34```
35
36Route a custom domain via `fly certs add your-domain.com` if you want something other than `*.fly.dev`.
37
38### Other Docker hosts
39
40Any platform that runs the in-repo `Dockerfile` works. Required wiring:
41
42- **Build:** `docker build .`
43- **Release (pre-start):** `bun run db:migrate`
44- **Start:** `bun run src/index.ts`
45- **Port:** `3000`
46- **Persistent volume:** mount the path set by `GIT_REPOS_PATH` (default `/app/repos`)
3647
3748---
3849
@@ -57,17 +68,17 @@ Full reference is in [`.env.example`](./.env.example); cross-reference BUILD_BIB
5768| `RESEND_API_KEY` | Required when `EMAIL_PROVIDER=resend`. |
5869| `VOYAGE_API_KEY` | Upgrades semantic search to Voyage `voyage-code-3` embeddings; without it, a deterministic hashing embedder is used. |
5970
60### Integrations
71### Integrations (all optional)
6172| Variable | Purpose |
6273|---|---|
63| `GATETEST_URL` | Outbound push webhook. Default `https://gatetest.ai/api/events/push`. |
74| `GATETEST_URL` | Outbound push webhook to the GateTest third-party security scanner. Default `https://gatetest.ai/api/events/push`. |
6475| `GATETEST_API_KEY` | Bearer sent on outbound GateTest posts. |
6576| `GATETEST_CALLBACK_SECRET` | Inbound bearer GateTest must present on result callbacks. See `GATETEST_HOOK.md`. |
6677| `GATETEST_HMAC_SECRET` | HMAC secret for inbound GateTest callbacks (alternative to bearer). |
67| `CRONTECH_DEPLOY_URL` | Outbound deploy webhook. Default `https://crontech.ai/api/hooks/gluecron/push`. |
68| `GLUECRON_WEBHOOK_SECRET` | Bearer sent on the outbound Crontech deploy webhook. |
69| `CRONTECH_EVENT_TOKEN` | Bearer Crontech must present on `deploy.succeeded` / `deploy.failed` callbacks to `POST /api/events/deploy`. |
70| `CRONTECH_STATUS_URL` / `GLUECRON_STATUS_URL` / `GATETEST_STATUS_URL` | Platform-status endpoints surfaced in the `/admin/platform` widget. |
78| `CRONTECH_DEPLOY_URL` | Optional outbound deploy webhook. When set, pushes to the default branch POST here. |
79| `GLUECRON_WEBHOOK_SECRET` | Bearer sent on the outbound deploy webhook above. |
80| `CRONTECH_EVENT_TOKEN` | Bearer an external deploy service must present on `deploy.succeeded` / `deploy.failed` callbacks to `POST /api/events/deploy`. |
81| `CRONTECH_STATUS_URL` / `GLUECRON_STATUS_URL` / `GATETEST_STATUS_URL` | Third-party platform-status endpoints surfaced in the `/admin/platform` widget. |
7182
7283### Operational flags
7384| Variable | Purpose |
@@ -85,7 +96,7 @@ Migrations are checked into `drizzle/` and run via:
8596bun run db:migrate
8697```
8798
88Hook this into Crontech's release phase so every deploy self-migrates. First-time bootstrap: the release command will pick up `0000_init.sql` through the latest migration in order.
99Hook this into your host's release phase so every deploy self-migrates. On Fly.io the in-repo `fly.toml` already wires this up via `[deploy].release_command`. First-time bootstrap: the release command will pick up `0000_init.sql` through the latest migration in order.
89100
90101---
91102
@@ -93,13 +104,13 @@ Hook this into Crontech's release phase so every deploy self-migrates. First-tim
93104
94105Verify these in order after the first deploy:
95106
96- [ ] **Migrations ran** — `db:migrate` reported success in release logs.
107- [ ] **Release command ran successfully** — `bun run db:migrate` reported success in the release logs.
97108- [ ] **`/healthz` is green** — returns `{"ok": true, ...}` with a 200.
98109- [ ] **`/readyz` is green** — returns `{"ok": true}`, confirming DB connectivity.
99110- [ ] **`/metrics` responds** — basic process snapshot is emitted.
100111- [ ] **First admin bootstrap** — register the first account at `/register`. Per `src/lib/admin.ts`, while `site_admins` is empty the **oldest user** is treated as site admin. Register the intended admin account first so the bootstrap rule applies; subsequent admins can then be granted from `/admin/users`.
101112- [ ] **Smart HTTP works** — create a repo at `/new`, then `git clone https://<your-host>/<user>/<repo>.git` round-trips.
102- [ ] **Push triggers the pipeline** — a `git push` fires GateTest, the secret scanner, and webhook fan-out (check `/:owner/:repo/gates` and `/:owner/:repo/settings/audit`).
113- [ ] **Push triggers the pipeline** — a `git push` fires the secret scanner, webhook fan-out, and (if configured) the outbound GateTest post (check `/:owner/:repo/gates` and `/:owner/:repo/settings/audit`).
103114- [ ] **Custom domain TLS** — certificate issued and `APP_BASE_URL` reflects the canonical host.
104115- [ ] **Autopilot state** — if you want the background ticker off, confirm `AUTOPILOT_DISABLED=1` is set; otherwise expect mirror syncs and weekly digests to fire on schedule.
105116
@@ -108,16 +119,16 @@ Verify these in order after the first deploy:
108119## 6. Operations
109120
110121### Logs
111Crontech streams stdout/stderr from `bun run src/index.ts`. Every request carries an `X-Request-Id` header; grep logs by that ID when tracing a report.
122Your host streams stdout/stderr from `bun run src/index.ts` (on Fly.io: `fly logs`). Every request carries an `X-Request-Id` header; grep logs by that ID when tracing a report.
112123
113124### Restarts
114Trigger from the Crontech dashboard. Rate-limit counters are in-memory and reset on restart — that is intentional.
125Trigger from your host's dashboard or CLI (on Fly.io: `fly apps restart`). Rate-limit counters are in-memory and reset on restart — that is intentional.
115126
116127### Rollbacks
117Roll back via Crontech's release history. Database migrations are additive; rolling back the service does not roll back the schema. If a migration needs reverting, write a new forward-migration.
128Roll back via your host's release history (on Fly.io: `fly releases` + `fly deploy --image <previous>`). Database migrations are additive; rolling back the service does not roll back the schema. If a migration needs reverting, write a new forward-migration.
118129
119130### Backups
120Neon handles PITR + branch snapshots — configure retention in the Neon console. The bare repos on the persistent volume must be backed up separately (filesystem snapshot of `/data/repos`). See BUILD_BIBLE §2.6 for what still needs wiring on the observability side.
131Neon handles PITR + branch snapshots — configure retention in the Neon console. The bare repos on the persistent volume must be backed up separately (filesystem snapshot of the mount at `GIT_REPOS_PATH`; on Fly.io, snapshot the `gluecron_repos` volume). See BUILD_BIBLE §2.6 for what still needs wiring on the observability side.
121132
122133---
123134
ModifiedLAUNCH_TODAY.md+4−4View fileUnifiedSplit
@@ -8,7 +8,7 @@ Legend: ✅ done · 🟡 in-flight · ❌ not started
88
99## Infrastructure
1010
11- ✅ Deployment target is Crontech (see `DEPLOY.md`). Neon is the database. No Vercel, no Hetzner.
11- ✅ Primary deployment target is Fly.io — `fly.toml` is in-repo (see `DEPLOY.md`). A `Dockerfile` is shipped for any other container host. Neon is the database.
1212- ✅ Migrations run via `bun run db:migrate`; release-phase wiring documented.
1313- ✅ `/healthz`, `/readyz`, `/metrics` endpoints shipped (BUILD_BIBLE §2.6).
1414- ✅ Request-ID tracing on every response (`src/middleware/request-context.ts`).
@@ -24,7 +24,7 @@ Legend: ✅ done · 🟡 in-flight · ❌ not started
2424- ✅ Legal pages — `legal/TERMS.md`, `legal/PRIVACY.md`, `legal/AUP.md`, `legal/SETUP-GUIDE.md`.
2525- 🟡 Demo org / sample repos — `src/lib/demo-seed.ts` and the `DEMO_SEED_ON_BOOT=1` boot flag are the deferred item from BUILD_BIBLE §7. Design sketch exists; no code yet.
2626- ✅ README reflects shipped feature surface (`README.md`).
27- ✅ Deployment doc reflects Crontech-first reality (`DEPLOY.md`).
27- ✅ Deployment doc reflects Fly.io-first reality (`DEPLOY.md`).
2828- ✅ GATETEST_HOOK.md documents inbound callback contract.
2929
3030## Operational
@@ -34,7 +34,7 @@ Legend: ✅ done · 🟡 in-flight · ❌ not started
3434- ✅ Billing plans seeded (free/pro/team/enterprise) + quota enforcement (Block F4).
3535- ✅ Audit log surfaced per-user (`/settings/audit`) and per-repo (`/:owner/:repo/settings/audit`) (Block A2).
3636- ✅ Email notifications + opt-in weekly digest (Blocks A8, I7).
37- ✅ Post-receive pipeline — GateTest, secret scanner, AI security review, CODEOWNERS sync, webhook fan-out (Blocks A1, D, green-ecosystem defaults).
37- ✅ Post-receive pipeline — GateTest, secret scanner, AI security review, CODEOWNERS sync, webhook fan-out (Blocks A1, D, repo defaults).
3838- ✅ Auto-repair engine runs when `ANTHROPIC_API_KEY` is set.
3939- 🟡 Monitoring / on-call rotation — `/metrics` + `/healthz` are live; alerting rules are not.
4040- 🟡 Backup restore drill — never rehearsed end-to-end.
@@ -59,7 +59,7 @@ Legend: ✅ done · 🟡 in-flight · ❌ not started
5959## Go/no-go gates (the short list)
6060
61611. Smoke `/healthz` + `/readyz` in production → both green.
622. Crontech release pipeline runs `db:migrate` successfully on deploy.
622. Deploy release command runs `bun run db:migrate` successfully on deploy.
63633. Register → create repo → clone over HTTPS → push → GateTest posts back → webhook fires. End-to-end in prod.
64644. `AUTOPILOT_DISABLED` decision made explicitly (default: enabled).
65655. Demo content story resolved — either ship `DEMO_SEED_ON_BOOT=1` wiring or accept an empty home.
ModifiedREADME.md+6−6View fileUnifiedSplit
@@ -1,6 +1,6 @@
11# gluecron
22
3A GitHub replacement. AI-native code intelligence, green-ecosystem-by-default git hosting, automated CI, and a self-hostable platform built to keep every push production-ready. Every repo ships with gates, branch protection, CODEOWNERS sync, and an AI reviewer on by default — opt out per feature, never by default. Deployed via Crontech, backed by Neon Postgres, runs anywhere Bun and a disk will run.
3A GitHub replacement. AI-native code intelligence, git hosting, automated CI, and a self-hostable platform built to keep every push production-ready. Every repo ships with gates, branch protection, CODEOWNERS sync, and an AI reviewer on by default — opt out per feature, never by default. Deploy anywhere Bun runs — fly.toml is in-repo, Dockerfile for any container host. Backed by Neon Postgres.
44
55## Features
66
@@ -109,7 +109,7 @@ src/
109109 app.tsx Hono composition + error handlers
110110 db/ Drizzle schema + lazy connection + migrations
111111 git/ repository.ts (tree/blob/commits/diff/blame/...) + protocol.ts
112 hooks/ post-receive (GateTest, Crontech, webhooks, CODEOWNERS)
112 hooks/ post-receive (GateTest, outbound deploy webhook, webhooks, CODEOWNERS)
113113 lib/ auth, config, markdown, highlight, AI helpers, autopilot, ...
114114 middleware/ softAuth / requireAuth / rate-limit / request-context
115115 routes/ git, api, web, issues, pulls, editor, settings, webhooks, ...
@@ -118,15 +118,15 @@ src/
118118
119119Full file inventory lives in [`CLAUDE.md`](./CLAUDE.md).
120120
121## Integrations (the green ecosystem)
121## Integrations
122122
123- **GateTest** — every `git push` POSTs to `https://gatetest.ai/api/events/push`; inbound results accepted at `POST /api/hooks/gatetest` (bearer or HMAC).
124- **Crontech** — deploys trigger on push to the default branch (`https://crontech.ai/api/hooks/gluecron/push`). Opt out per repo via `autoDeployEnabled`.
123- **GateTest** — optional third-party security scanner. When `GATETEST_URL` is set, every `git push` POSTs to it; inbound results accepted at `POST /api/hooks/gatetest` (bearer or HMAC).
125124- **Webhooks** — user-registered URLs receive HMAC-signed payloads on push / issue / PR / star events.
125- **Outbound deploy webhook** — optional. Set `CRONTECH_DEPLOY_URL` (or any URL) to receive a POST on pushes to the default branch. Opt out per repo via `autoDeployEnabled`.
126126
127127## Deployment
128128
129Gluecron is deployed via **Crontech**, with Neon Postgres as the database. See [`DEPLOY.md`](./DEPLOY.md) for step-by-step instructions, environment variables, and the post-deploy verification checklist.
129Gluecron runs anywhere Bun runs. The repo ships a `fly.toml` for Fly.io and a `Dockerfile` for any container host, with Neon Postgres as the database. See [`DEPLOY.md`](./DEPLOY.md) for step-by-step instructions, environment variables, and the post-deploy verification checklist.
130130
131131## License
132132
Deletedsrc/__tests__/platform-siblings.test.ts+0−139View fileUnifiedSplit
@@ -1,139 +0,0 @@
1/**
2 * Cross-product platform-status aggregator + admin widget smoke tests.
3 *
4 * The real siblings (crontech.ai, gluecron.com, gatetest.io) aren't
5 * reachable in test, so we stub `globalThis.fetch` to exercise the three
6 * branches: healthy JSON, non-2xx, and outright failure. Each branch must
7 * resolve — a sibling being down never crashes the widget.
8 */
9
10import { describe, it, expect, afterEach, beforeEach } from "bun:test";
11import app from "../app";
12import {
13 getSiblingStatuses,
14 siblingUrls,
15 __resetSiblingCache,
16} from "../lib/platform-siblings";
17
18const originalFetch = globalThis.fetch;
19
20beforeEach(() => {
21 __resetSiblingCache();
22});
23
24afterEach(() => {
25 globalThis.fetch = originalFetch;
26 __resetSiblingCache();
27 delete process.env.CRONTECH_STATUS_URL;
28 delete process.env.GLUECRON_STATUS_URL;
29 delete process.env.GATETEST_STATUS_URL;
30});
31
32describe("platform-siblings — siblingUrls", () => {
33 it("returns defaults pointing at production hosts", () => {
34 const urls = siblingUrls();
35 expect(urls.crontech).toBe("https://crontech.ai/api/platform-status");
36 expect(urls.gluecron).toBe("https://gluecron.com/api/platform-status");
37 expect(urls.gatetest).toBe("https://gatetest.io/api/platform-status");
38 });
39
40 it("honours env var overrides", () => {
41 process.env.CRONTECH_STATUS_URL = "https://example.com/ct";
42 process.env.GLUECRON_STATUS_URL = "https://example.com/gl";
43 process.env.GATETEST_STATUS_URL = "https://example.com/gt";
44 const urls = siblingUrls();
45 expect(urls.crontech).toBe("https://example.com/ct");
46 expect(urls.gluecron).toBe("https://example.com/gl");
47 expect(urls.gatetest).toBe("https://example.com/gt");
48 });
49});
50
51describe("platform-siblings — getSiblingStatuses", () => {
52 it("reports healthy when sibling returns healthy JSON", async () => {
53 globalThis.fetch = (async () =>
54 new Response(
55 JSON.stringify({
56 product: "x",
57 version: "1.2.3",
58 commit: "abcdef1234567",
59 healthy: true,
60 timestamp: "2026-04-20T00:00:00Z",
61 }),
62 { status: 200, headers: { "content-type": "application/json" } }
63 )) as unknown as typeof fetch;
64
65 const rows = await getSiblingStatuses({ force: true });
66 expect(rows).toHaveLength(3);
67 for (const r of rows) {
68 expect(r.reachable).toBe(true);
69 expect(r.healthy).toBe(true);
70 expect(r.version).toBe("1.2.3");
71 expect(r.commit).toBe("abcdef1234567");
72 expect(r.error).toBeNull();
73 expect(typeof r.latencyMs).toBe("number");
74 }
75 });
76
77 it("reports degraded (reachable but unhealthy) on non-2xx", async () => {
78 globalThis.fetch = (async () =>
79 new Response("nope", { status: 503 })) as unknown as typeof fetch;
80
81 const rows = await getSiblingStatuses({ force: true });
82 for (const r of rows) {
83 expect(r.reachable).toBe(true);
84 expect(r.healthy).toBe(false);
85 expect(r.error).toBe("HTTP 503");
86 }
87 });
88
89 it("reports unreachable on fetch error, never throws", async () => {
90 globalThis.fetch = (async () => {
91 throw new Error("ECONNREFUSED");
92 }) as unknown as typeof fetch;
93
94 const rows = await getSiblingStatuses({ force: true });
95 expect(rows).toHaveLength(3);
96 for (const r of rows) {
97 expect(r.reachable).toBe(false);
98 expect(r.healthy).toBe(false);
99 expect(r.error).toBeTruthy();
100 }
101 });
102
103 it("serves a cached result on the second call", async () => {
104 let calls = 0;
105 globalThis.fetch = (async () => {
106 calls++;
107 return new Response(
108 JSON.stringify({ healthy: true, version: "v", commit: "c" }),
109 { status: 200 }
110 );
111 }) as unknown as typeof fetch;
112
113 await getSiblingStatuses({ force: true });
114 expect(calls).toBe(3);
115 await getSiblingStatuses();
116 expect(calls).toBe(3);
117 });
118});
119
120describe("platform-status endpoint", () => {
121 it("GET /api/platform-status returns our own health JSON", async () => {
122 const res = await app.request("/api/platform-status");
123 expect(res.status).toBe(200);
124 const body = (await res.json()) as any;
125 expect(body.product).toBe("gluecron");
126 expect(body.healthy).toBe(true);
127 expect(typeof body.timestamp).toBe("string");
128 expect(body.siblings).toBeDefined();
129 expect(res.headers.get("access-control-allow-origin")).toBe("*");
130 });
131});
132
133describe("admin platform widget — auth gate", () => {
134 it("GET /admin/platform without auth → 302 /login", async () => {
135 const res = await app.request("/admin/platform");
136 expect(res.status).toBe(302);
137 expect(res.headers.get("location") || "").toContain("/login");
138 });
139});
Deletedsrc/lib/platform-siblings.ts+0−152View fileUnifiedSplit
@@ -1,152 +0,0 @@
1/**
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}
Modifiedsrc/routes/dashboard.tsx+3−3View fileUnifiedSplit
@@ -385,9 +385,9 @@ dashboard.get(
385385 defaultChecked={true}
386386 />
387387 <ToggleSetting
388 name="crontech_deploy"
389 label="Crontech Auto-Deploy"
390 description="Trigger deployment on Crontech when pushing to main branch"
388 name="deploy_webhook"
389 label="Auto-Deploy Webhook"
390 description="POST to your configured deploy webhook when pushing to the default branch"
391391 defaultChecked={true}
392392 />
393393
Modifiedsrc/views/landing.tsx+3−2View fileUnifiedSplit
@@ -185,8 +185,9 @@ export const LandingPage: FC<LandingPageProps> = ({ stats } = {}) => {
185185 <div class="landing-step-num">3</div>
186186 <h3>Green pushes auto-deploy</h3>
187187 <p>
188 Default-branch commits that pass all gates deploy through
189 Crontech. Failed deploys open an AI-authored incident issue.
188 Default-branch commits that pass all gates trigger your
189 configured deploy webhook. Failed deploys open an AI-authored
190 incident issue.
190191 </p>
191192 </div>
192193 </div>
193194