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?{" "} >(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", {