Skip to content

Commit e107d41

Browse files
committed
Broadcast endpoint + deploy scripts, exit codes in discord, friendly messages for rollout DO kills, webhook reliability, polish
1 parent 97478b3 commit e107d41

12 files changed

Lines changed: 273 additions & 74 deletions

File tree

web/.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copy to .env (gitignored) and fill in real values
2+
3+
# Bearer token used by prod-alert / deploy-prod to authenticate against
4+
# the /api/broadcast endpoint. Must match the BROADCAST_TOKEN secret set
5+
# on the worker (via: npx wrangler secret put BROADCAST_TOKEN)
6+
GLUA_BROADCAST_TOKEN=
7+
8+
# Base URL for the deployed worker
9+
GLUA_PROD_URL=https://glua.dev

web/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules
2+
.env

web/frontend/src/components/Editor.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ hello()
6666
history(),
6767
keymap.of([
6868
{ key: "Mod-Enter", run: () => { runScript(); return true; } },
69+
{ key: "Ctrl-Enter", run: () => { runScript(); return true; } },
6970
...defaultKeymap,
7071
...historyKeymap,
7172
]),

web/frontend/src/components/StatusPanel.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
<span class="font-bold text-sm {inactive ? 'text-gray-400' : 'text-white'}">{inactive ? "Session Ended" : "Session Status"}</span>
3737
</button>
3838
<div class="flex items-center gap-2">
39-
<a href={DISCORD_URL} target="_blank" rel="noopener noreferrer" title="Questions, bugs, or feedback? Join the CFC dev Discord" class="text-gray-500 hover:text-indigo-300 transition-colors" aria-label="Join the CFC developer Discord">
39+
<a href={DISCORD_URL} target="_blank" rel="noopener noreferrer" title="Questions, bugs, or feedback? Join the CFC dev Discord" class="text-[#5865F2] hover:text-[#7983f5] hover:drop-shadow-[0_0_6px_rgba(255,255,255,0.5)] transition-all" aria-label="Join the CFC developer Discord">
4040
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.317 4.369A19.79 19.79 0 0 0 16.558 3.2a.074.074 0 0 0-.079.037c-.34.607-.719 1.4-.984 2.024a18.302 18.302 0 0 0-5.487 0 12.64 12.64 0 0 0-.998-2.024.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.058a.082.082 0 0 0 .031.056 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.105 13.1 13.1 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .077-.01c3.927 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .078.01c.12.099.246.197.373.29a.077.077 0 0 1-.006.129 12.3 12.3 0 0 1-1.873.891.077.077 0 0 0-.041.106c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.03ZM8.02 15.331c-1.183 0-2.157-1.086-2.157-2.42 0-1.334.955-2.42 2.157-2.42 1.211 0 2.176 1.095 2.157 2.42 0 1.334-.955 2.42-2.157 2.42Zm7.974 0c-1.183 0-2.157-1.086-2.157-2.42 0-1.334.955-2.42 2.157-2.42 1.211 0 2.176 1.095 2.157 2.42 0 1.334-.946 2.42-2.157 2.42Z"/></svg>
4141
</a>
4242
<button on:click={() => collapsed = !collapsed} class="focus:outline-none" aria-label="Toggle panel">

web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
},
88
"scripts": {
99
"deploy": "wrangler deploy",
10+
"deploy-prod": "node --env-file=.env scripts/deploy-prod.mjs",
11+
"prod-alert": "node --env-file=.env scripts/prod-alert.mjs",
1012
"dev": "wrangler dev",
1113
"start": "wrangler dev",
1214
"dev:frontend": "npm run dev --prefix frontend",

web/scripts/deploy-prod.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env node
2+
// Warns active sessions that a deploy is incoming, waits, then deploys.
3+
// Usage: npm run deploy-prod
4+
5+
import { spawn } from "node:child_process";
6+
import { fileURLToPath } from "node:url";
7+
import { dirname, join } from "node:path";
8+
9+
const __dirname = dirname(fileURLToPath(import.meta.url));
10+
const GRACE_SECONDS = 15;
11+
const ALERT_MESSAGE = `\u001b[33m*** glua.dev is deploying an update in ${GRACE_SECONDS}s — your session will briefly disconnect. Start a new one if it doesn't come back. ***\u001b[0m`;
12+
13+
function run(command, args) {
14+
return new Promise((resolve, reject) => {
15+
const child = spawn(command, args, { stdio: "inherit", env: process.env });
16+
child.on("exit", (code) => {
17+
if (code === 0) resolve();
18+
else reject(new Error(`${command} exited with code ${code}`));
19+
});
20+
child.on("error", reject);
21+
});
22+
}
23+
24+
console.log(`[deploy-prod] broadcasting heads-up to active sessions…`);
25+
try {
26+
await run("node", [join(__dirname, "prod-alert.mjs"), ALERT_MESSAGE]);
27+
} catch (e) {
28+
console.warn(`[deploy-prod] alert failed (${e.message}) — continuing anyway`);
29+
}
30+
31+
console.log(`[deploy-prod] waiting ${GRACE_SECONDS}s so users can read the message…`);
32+
await new Promise((r) => setTimeout(r, GRACE_SECONDS * 1000));
33+
34+
console.log(`[deploy-prod] running wrangler deploy`);
35+
await run("npx", ["wrangler", "deploy"]);

web/scripts/prod-alert.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env node
2+
// Sends a message that the worker will broadcast to every active session.
3+
// Usage: npm run prod-alert -- "Your message (ANSI escapes allowed)"
4+
5+
const message = process.argv.slice(2).join(" ");
6+
if (!message) {
7+
console.error("Usage: npm run prod-alert -- \"message to broadcast\"");
8+
process.exit(1);
9+
}
10+
11+
const token = process.env.GLUA_BROADCAST_TOKEN;
12+
const base = process.env.GLUA_PROD_URL ?? "https://glua.dev";
13+
if (!token) {
14+
console.error("GLUA_BROADCAST_TOKEN is not set — add it to web/.env");
15+
process.exit(1);
16+
}
17+
18+
const res = await fetch(`${base}/api/broadcast`, {
19+
method: "POST",
20+
headers: {
21+
"Authorization": `Bearer ${token}`,
22+
"Content-Type": "application/json",
23+
},
24+
body: JSON.stringify({ message }),
25+
});
26+
27+
if (!res.ok) {
28+
console.error(`Broadcast failed: ${res.status} ${res.statusText}`);
29+
console.error(await res.text());
30+
process.exit(1);
31+
}
32+
33+
const result = await res.json();
34+
console.log(`Broadcast delivered to ${result.delivered}/${result.sessions} active sessions`);

web/src/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ export interface Env {
1616

1717
/** Optional — when set, fires Discord webhook notifications for observability */
1818
DISCORD_WEBHOOK_URL?: string;
19+
20+
/** Bearer token required to call /api/broadcast — matches GLUA_BROADCAST_TOKEN in .env */
21+
BROADCAST_TOKEN?: string;
1922
}

web/src/observability/embeds.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const CLOSE_REASON_DISPLAY: Record<CloseReason, { label: string; icon: string; a
129129
container_start_failed: { label: "container failed to start", icon: "✖", ansiCode: "2;31", color: COLORS.error },
130130
agent_ws_close: { label: "agent ws closed", icon: "⚠", ansiCode: "2;33", color: COLORS.warning },
131131
agent_ws_error: { label: "agent ws errored", icon: "⚠", ansiCode: "2;33", color: COLORS.warning },
132+
deploy_rollout: { label: "deploy rollout", icon: "🚀", ansiCode: "2;36", color: COLORS.info },
132133
};
133134

134135
export function buildSessionStartedEmbed(e: SessionStartedEvent): DiscordEmbed {
@@ -189,6 +190,11 @@ export function buildSessionEndedEmbed(e: SessionEndedEvent): DiscordEmbed {
189190
{ name: "Extended", value: e.extensionGranted ? "✅" : "❌", inline: true },
190191
);
191192

193+
if (e.exitCode !== undefined) {
194+
const exitValue = e.exitReason ? `${code(String(e.exitCode))} (${e.exitReason})` : code(String(e.exitCode));
195+
fields.push({ name: "Exit code", value: exitValue, inline: true });
196+
}
197+
192198
return {
193199
title: "🏁 Session ended",
194200
url: sessionHistoryUrl(e.sessionId),
@@ -206,7 +212,9 @@ export function buildErrorEmbed(e: ErrorEvent): DiscordEmbed {
206212
const msg = truncate(rawMsg, LIMIT_ERROR_MESSAGE);
207213
const stack = err instanceof Error ? err.stack : undefined;
208214

209-
const lines: string[] = [`### ✖ ${truncate(e.where, 120)}`, ansi("2;31", msg)];
215+
const lines: string[] = [];
216+
if (e.sessionId) lines.push(code(truncate(e.sessionId, 100)));
217+
lines.push(`### ✖ ${truncate(e.where, 120)}`, ansi("2;31", msg));
210218

211219
if (stack) {
212220
lines.push("```ts\n" + truncate(stack, LIMIT_STACK) + "\n```");
@@ -216,9 +224,6 @@ export function buildErrorEmbed(e: ErrorEvent): DiscordEmbed {
216224
if (ctxSub) lines.push(ctxSub);
217225

218226
const fields: DiscordEmbedField[] = [];
219-
if (e.sessionId) {
220-
fields.push({ name: "Session", value: code(truncate(e.sessionId, 100)), inline: true });
221-
}
222227
if (e.branch) {
223228
const { emoji, label } = branchMeta(e.branch);
224229
fields.push({ name: "Branch", value: `${emoji} ${label}`, inline: true });

web/src/observability/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export type CloseReason =
66
| "container_error"
77
| "container_start_failed"
88
| "agent_ws_close"
9-
| "agent_ws_error";
9+
| "agent_ws_error"
10+
| "deploy_rollout";
1011

1112
export interface RequestContext {
1213
ip: string;
@@ -44,6 +45,10 @@ export interface SessionEndedEvent {
4445
scriptCount: number;
4546
logLineCount: number;
4647
extensionGranted: boolean;
48+
/** Container process exit code, when the session ended because the container stopped */
49+
exitCode?: number;
50+
/** "exit" for a clean stop, "runtime_signal" for a killed process */
51+
exitReason?: string;
4752
context?: RequestContext;
4853
capacity?: CapacitySnapshot;
4954
}

0 commit comments

Comments
 (0)