From f6aa17b222890994c4920b025376d26d1bdddfa5 Mon Sep 17 00:00:00 2001 From: murugu21 Date: Sun, 14 Jun 2026 14:45:32 +0530 Subject: [PATCH 1/2] feat(cdk): timestamped, tail-able userdata boot log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tee all userdata output to /var/log/eventform-userdata.log and timestamp every traced command (PS4='+ [HH:MM:SS]'), so `tail -f` shows how long each boot step (dnf, image pull, compose up) takes — the duration of a step is the gap to the next line's timestamp. Start/done markers bracket the run (a missing "done" = boot failed there, since set -e exits on first error). Also wrap the SSM .env materialization in `set +x` so the secret values (Neon URLs, enc key, tunnel token) are never traced into the boot log — closes a pre-existing leak from the old `set -euxo`. Co-Authored-By: Claude Opus 4.8 (1M context) --- infra/cdk/lib/compute-stack.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/infra/cdk/lib/compute-stack.ts b/infra/cdk/lib/compute-stack.ts index e91e4c1..3af80dc 100644 --- a/infra/cdk/lib/compute-stack.ts +++ b/infra/cdk/lib/compute-stack.ts @@ -73,7 +73,15 @@ export class ComputeStack extends cdk.Stack { const userData = ec2.UserData.forLinux(); userData.addCommands( - "set -euxo pipefail", + "set -euo pipefail", + // Tee all userdata output to a tail-able log, and timestamp every traced + // command (PS4) so you can `tail -f /var/log/eventform-userdata.log` and + // see how long each boot step (dnf, image pull, compose up) takes. The + // duration of a step = the gap to the next line's timestamp. + `export PS4='+ [$(date "+%T")] '`, + "exec > >(tee -a /var/log/eventform-userdata.log) 2>&1", + `echo "=== eventform userdata start: $(date -u) ==="`, + "set -x", "dnf update -y", "dnf install -y docker git awscli", // Cap container logs daemon-wide (json-file is unbounded by default, and @@ -92,6 +100,10 @@ export class ComputeStack extends cdk.Stack { // Materialize .env from SSM SecureString params (region from the stack) `REGION=${this.region}`, "get() { aws ssm get-parameter --region \"$REGION\" --name \"$1\" --with-decryption --query Parameter.Value --output text; }", + // Disable xtrace around secret materialization — with set -x, the trace + // would print the expanded $(get ...) (the actual Neon URLs, enc key, + // tunnel token) into the userdata log. Re-enable after .env is written. + "set +x", "{", ' echo "DATABASE_URL=$(get /eventform/database-url)"', ' echo "DATABASE_URL_API=$(get /eventform/database-url-api)"', @@ -102,6 +114,7 @@ export class ComputeStack extends cdk.Stack { ' echo "COGNITO_CLIENT_ID=$(get /eventform/cognito-client-id)"', "} > .env", "chmod 600 .env", + "set -x", // Scale-to-zero: the API (non-root `node`, uid 1000) stamps its // last-activity file into this bind-mounted dir; pre-create it owned by // 1000 so the container can write and the idle-check (root) can read. @@ -118,6 +131,9 @@ export class ComputeStack extends cdk.Stack { "cp /opt/eventform/infra/systemd/eventform-idle.timer /etc/systemd/system/", "systemctl daemon-reload", "systemctl enable --now eventform-idle.timer", + // Completion marker — if this line isn't in the log, boot failed earlier + // (set -e exits on the first error), and the last timestamp shows where. + `echo "=== eventform userdata done: $(date -u) ==="`, ); const launchTemplate = new ec2.LaunchTemplate(this, "LaunchTemplate", { From 2524128dc91e7ddedc379f938e6ba2a35b1d3caa Mon Sep 17 00:00:00 2001 From: murugu21 Date: Sun, 14 Jun 2026 15:05:26 +0530 Subject: [PATCH 2/2] feat(web): descriptive, interactive cold-start screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "waking up" screen now explains *why* it takes 2–3 min (the box powers down when idle to stay cheap to host, and the first load pulls ~3 GB of container images), and shows progress instead of a bare spinner: - an elapsed-time counter (mm:ss), - a 4-step stepper (wake server → pull images → start pipeline → reconnect) that advances on elapsed time, with done/active/pending states, - an "taking longer than usual" message past ~3.5 min. Heuristic (the box can't report real boot status to a page that can't reach it), keyed to the actual sequence; the gate still flips to the app the instant /health 200s. Keeps the Sign-in/Retry/Back actions. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/components/api-health-gate.tsx | 107 ++++++++++++++++---- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/apps/web/src/components/api-health-gate.tsx b/apps/web/src/components/api-health-gate.tsx index 0d51df3..17cc940 100644 --- a/apps/web/src/components/api-health-gate.tsx +++ b/apps/web/src/components/api-health-gate.tsx @@ -1,11 +1,22 @@ import { useCallback, useEffect, useState } from "react"; import { Link, Outlet, useLocation } from "react-router"; +import { CheckIcon } from "lucide-react"; import { Button, buttonVariants } from "@/components/ui/button"; import { checkApiHealth, requestWake } from "@/lib/api"; const CONTACT_EMAIL = "murugu2001@gmail.com"; const POLL_INTERVAL_MS = 8000; +// Heuristic cold-start phases (keyed to elapsed seconds). The box can't report +// real boot progress to a page that can't reach it, so this is an honest estimate +// matching the actual sequence; the gate flips to the app the instant /health 200s. +const BOOT_PHASES = [ + { at: 0, label: "Waking up a server" }, + { at: 20, label: "Pulling container images (~3 GB)" }, + { at: 110, label: "Starting the pipeline — Postgres, Redpanda, Debezium, API" }, + { at: 165, label: "Reconnecting" }, +] as const; + type Status = "checking" | "up" | "down"; /** @@ -91,29 +102,80 @@ function InstanceStarting({ retrying: boolean; }) { const isPublicForm = variant === "publicForm"; + // Count elapsed seconds so the screen shows motion + an honest "how long" cue. + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + const id = window.setInterval(() => setElapsed((e) => e + 1), 1000); + return () => window.clearInterval(id); + }, []); + const phaseIdx = BOOT_PHASES.reduce((acc, p, i) => (elapsed >= p.at ? i : acc), 0); + const overtime = elapsed > 210; + const mmss = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, "0")}`; + return (
-
-
-
-
-

- {isPublicForm ? "This form is waking up" : "Waking up the backend"} -

- {isPublicForm ? ( -

- To save resources, EventForm’s backend powers down when idle. - Ask whoever shared this link to sign in — that starts - it back up (a couple of minutes). If that’s you, sign in below; - this page loads the form on its own once it’s running. -

- ) : ( +
+
+
+
+
+

+ {isPublicForm ? "This form is waking up" : "Waking up EventForm"} +

- To save resources, EventForm’s backend powers down when idle, so - the first visit takes a minute. Sign in to wake it — - this page reconnects automatically once it’s up. + To stay (nearly) free to host, EventForm runs on a single small server + that powers down when idle. Your visit is waking it — + the first load takes about 2–3 minutes while it + boots and pulls ~3 GB of container images, then it’s instant + until it idles again.

- )} + {isPublicForm && ( +

+ Only a signed-in user can start it — if it doesn’t come up, ask + whoever shared this link to sign in. +

+ )} +
+ + {/* Heuristic progress — advances on elapsed time; gives a sense of motion + and explains where the 2–3 min goes (the image pull is the long part). */} +
    + {BOOT_PHASES.map((p, i) => { + const done = i < phaseIdx; + const active = i === phaseIdx; + return ( +
  1. + + {done ? ( + + ) : active ? ( + + ) : ( + + )} + + + {p.label} + +
  2. + ); + })} +
+ +

+ {overtime + ? "Taking longer than usual — it should still come up; you can retry below." + : `${mmss} elapsed · usually ~2–3 min · reconnects on its own`} +

+
{/* Signing in is what fires the wake (the wake endpoint is Cognito-only), so the sign-in CTA shows on every down screen — a logged-out visitor @@ -126,13 +188,14 @@ function InstanceStarting({ Back to home
-

- Still not loading after a few minutes?{" "} + +

+ Still nothing after a few minutes?{" "}