From bfdf512b76ef9bc56e251c4d135ab22f1f0001fa Mon Sep 17 00:00:00 2001 From: Anurag Goel Date: Thu, 24 Sep 2026 07:28:36 -0700 Subject: [PATCH] Explain each stage of a run in a tooltip The UI showed the name of each stage, but not what the stage does or where it runs. Now each stage has a tooltip. The tooltip tells what the stage does, and where it runs: a Render Workflows task, the Render Sandbox of the run, or Render itself. The tooltip opens above its stage when the pointer is on the stage, or when the keyboard focuses it. Thus a pointer that moves down the list does not go into an open tooltip. The tooltip touches its stage, so the pointer can move onto it, and Escape closes it (WCAG 1.4.13). The name of each stage is a button, so that the Tab key can go to it. Biome does not let a list item have a tabindex. The stage list is now static HTML. Before, each poll replaced all of the stages, in a live region. That closed an open tooltip, took the focus from a stage, and could make a screen reader read the list again every 5 seconds. Now a poll changes only the class of each stage. RUN_STAGES in app/store.ts now gives the RunStage type. A new gateway test checks that public/index.html lists these stages in this order, each with a tooltip that tells where it runs. Before, only a comment kept the two lists the same. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 5 +++ app/store.ts | 33 +++++++++++-------- docs/FAQ.md | 2 ++ public/app.js | 58 +++++++++++++++++--------------- public/index.html | 77 ++++++++++++++++++++++++++++++++++++++++++- public/style.css | 60 +++++++++++++++++++++++++++++++++ tests/gateway.test.ts | 18 ++++++++++ 7 files changed, 211 insertions(+), 42 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 23fd700..0209f90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -420,6 +420,11 @@ run becomes `deployed`. The storefront check must also pass: the HTML of the storefront, or a script that it loads, must contain the public hostname of the API. The API checks cannot see the hostname that a browser uses. +Each stage in `RUN_STAGES` has an item in the stage list of +`public/index.html`, in the same order, with a tooltip that tells what the +stage does and where it runs. `tests/gateway.test.ts` checks this, so a new +stage needs an item and a tooltip. + When a deploy fails, the deploy manager diagnoses it from the logs of that deploy, which workflow code gives it. `fetchDeployLogs()` reads them in the time range of the deploy, with no type filter. Thus it gets the build logs, diff --git a/app/store.ts b/app/store.ts index c165400..5562a74 100644 --- a/app/store.ts +++ b/app/store.ts @@ -21,21 +21,26 @@ export type FinishedStatus = Exclude< >; /** - * Coarse progress for GET /v1/apps/:runId. Cosmetic; never gates a run. - * stageOrder in public/app.js must list these stages in this order. If the - * stage of a run is not in that list, the UI shows no progress for the run. + * Coarse progress for GET /v1/apps/:runId, in pipeline order. Cosmetic; + * never gates a run. The stage list in public/index.html must list these + * stages in this order, each with a tooltip, and tests/gateway.test.ts checks + * it. If the stage of a run is not in that list, the UI shows no progress for + * the run. */ -export type RunStage = - | "designing" - | "provisioning" - | "curating" - | "building" - | "verifying" - | "publishing" - | "waiting_for_services" - | "waiting_for_deploys" - | "smoke_testing" - | "done"; +export const RUN_STAGES = [ + "designing", + "provisioning", + "curating", + "building", + "verifying", + "publishing", + "waiting_for_services", + "waiting_for_deploys", + "smoke_testing", + "done", +] as const; + +export type RunStage = (typeof RUN_STAGES)[number]; export interface RunRecord { id: string; diff --git a/docs/FAQ.md b/docs/FAQ.md index 6a93db4..60b0dcf 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -72,6 +72,8 @@ Architecture overview and how Render products fit together: - The UI shows these stages: Designing, Provisioning (only for an app with a database), Curating, Building, Verifying, Publishing, Waiting For Services, Waiting For Deploys, Smoke Testing, and Done. +- Each stage has a tooltip that tells what the stage does and where it runs. + Hover over the stage, or go to it with the Tab key. - A typical run takes **5–10 minutes**. The builder takes the largest part. - The CLI `npm run demo` follows the status endpoint and prints final URLs. - If a run looks stuck, `GET /v1/apps/:runId` shows the current `stage` and `progress`. diff --git a/public/app.js b/public/app.js index 5deefc7..375b4d8 100644 --- a/public/app.js +++ b/public/app.js @@ -28,18 +28,13 @@ const deleteCancel = document.querySelector("#delete-cancel"); const activeStatuses = ["running", "deleting"]; const healthyStatuses = [...activeStatuses, "deployed", "awaiting_blueprint"]; -const stageOrder = [ - "designing", - "provisioning", - "curating", - "building", - "verifying", - "publishing", - "waiting_for_services", - "waiting_for_deploys", - "smoke_testing", - "done", -]; +/** + * The stage list is static HTML, with the tooltip of each stage. A poll + * changes only the class of each stage. It does not replace the stages, so an + * open tooltip stays open and a focused stage keeps the focus. + */ +const stageItems = [...stages.querySelectorAll("li")]; +const stageOrder = stageItems.map((item) => item.dataset.stage); let runs = []; let selectedRunId = null; @@ -84,6 +79,18 @@ deleteConfirm.addEventListener("input", () => { deleteCancel.addEventListener("click", () => deleteDialog.close()); +/** + * Escape closes an open stage tooltip, and the pointer and the focus stay + * where they are (WCAG 1.4.13). The next stage that the pointer or the focus + * goes to opens its tooltip again. + */ +document.addEventListener("keydown", (event) => { + if (event.key === "Escape") stages.classList.add("tips-closed"); +}); +for (const type of ["pointerover", "focusin"]) { + stages.addEventListener(type, () => stages.classList.remove("tips-closed")); +} + deleteForm.addEventListener("submit", async (event) => { event.preventDefault(); deleteDialog.close(); @@ -236,21 +243,18 @@ function renderRun(run) { stages.hidden = ["deleting", "delete_failed"].includes(run.status); const current = stageOrder.indexOf(run.stage); - stages.replaceChildren( - ...stageOrder.map((stage, index) => { - const item = document.createElement("li"); - item.textContent = label(stage); - if (index < current || run.status === "deployed") item.className = "complete"; - if (index === current && run.status === "running") item.className = "active"; - if ( - index === current && - !["running", "deployed", "awaiting_blueprint"].includes(run.status) - ) { - item.className = "failed-stage"; - } - return item; - }), - ); + stageItems.forEach((item, index) => { + let state = ""; + if (index < current || run.status === "deployed") state = "complete"; + if (index === current && run.status === "running") state = "active"; + if ( + index === current && + !["running", "deployed", "awaiting_blueprint"].includes(run.status) + ) { + state = "failed-stage"; + } + item.className = state; + }); const deployed = run.status === "deployed" && Boolean(run.urls?.web); result.hidden = !deployed; diff --git a/public/index.html b/public/index.html index e95881a..d90f078 100644 --- a/public/index.html +++ b/public/index.html @@ -73,7 +73,82 @@

Starting

-
    + +
      +
    1. + + +
    2. +
    3. + + +
    4. +
    5. + + +
    6. +
    7. + + +
    8. +
    9. + + +
    10. +
    11. + + +
    12. +
    13. + + +
    14. +
    15. + + +
    16. +
    17. + + +
    18. +
    19. + + +
    20. +