Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,13 @@ The UI has two views of the same runs, and each has a button that opens the
other. The classic view at `/` explains each stage in a tooltip. The table
view at `/table` shows the sites in a table, with each URL, the time that each
run took, and a delete button. It shows the stages in a table that tells what
each stage does and where it runs, with links to the workflow run and the
sandbox in the Render Dashboard. `public/runs.js` has what the views share;
each stage does, where it runs, and how long it took, with links to the
workflow run and the sandbox in the Render Dashboard. A timer counts the
seconds of the stage that runs now. `setRunStage()` adds each stage that a run
goes into, with the time, to `stage_history`. The gateway gives each item the
time that it stopped: when the next item started, or when the run stopped. A
stage that the run goes into again, as building after a failed verification,
shows the sum of its times. `public/runs.js` has what the views share;
`app.js` and `table.js` render only what differs. The gateway builds the links
from IDs in Postgres and gives null for a link that it cannot make: the SDK
does not give the ID of a subtask run, so each stage links to the run of
Expand Down Expand Up @@ -437,9 +442,9 @@ 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. It also has a row in the stage table of
`public/table.html`, which tells the same and names the Dashboard links of
the stage. `tests/gateway.test.ts` checks both, so a new stage needs an item
and a row.
`public/table.html`, which tells the same, has a cell for the time of the
stage, and names the Dashboard links of the stage. `tests/gateway.test.ts`
checks both, so a new stage needs an item and a row.

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
Expand Down
16 changes: 16 additions & 0 deletions app/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,16 @@ export interface RunResponse {
status: string;
stage: string | null;
progress: string | null;
/**
* Each time that the run went into a stage, in order. A stage that the run
* went into again, as building after a failed verification, has one more
* item. `finishedAt` is null for the stage that runs now.
*/
stageHistory: {
stage: string;
startedAt: string;
finishedAt: string | null;
}[];
prompt: string;
user: string;
appName: string | null;
Expand Down Expand Up @@ -320,6 +330,12 @@ export function runResponse(
status: run.status,
stage: run.stage,
progress: run.progress ? redactSecrets(run.progress) : null,
// A stage stops when the next stage starts, and the last stage stops
// when the run stops.
stageHistory: run.stageHistory.map((item, index, history) => ({
...item,
finishedAt: history[index + 1]?.startedAt ?? run.finishedAt,
})),
prompt: run.prompt,
user: run.user,
appName: run.appName,
Expand Down
7 changes: 7 additions & 0 deletions app/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ create table if not exists runs (
-- Where the run is right now, for GET /v1/apps/:runId.
stage text,
progress text,
-- When the run went into each stage, in order, for the time of each stage
-- in the UI: [{"stage": "designing", "started_at": "..."}]. A stage that
-- the run goes into again, as building after a failed verification, gets
-- one more item. A stage stops when the next one starts, and the last one
-- stops at finished_at.
stage_history jsonb not null default '[]',
-- The Workflows task run that owns the status: prompt-to-app while the
-- run is running, and delete-app while its app is deleting.
workflow_run_id text,
Expand Down Expand Up @@ -38,6 +44,7 @@ alter table runs add column if not exists workflow_checked_at timestamptz;
alter table runs add column if not exists sandbox_id text;
alter table runs add column if not exists sandbox_group_id text;
alter table runs add column if not exists finished_at timestamptz;
alter table runs add column if not exists stage_history jsonb not null default '[]';

-- A run that stopped before finished_at existed stopped when it was last
-- updated. A run that a delete changed after that gets no time.
Expand Down
26 changes: 24 additions & 2 deletions app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export interface RunRecord {
status: RunStatus;
stage: RunStage | null;
progress: string | null;
/**
* When the run went into each stage, in order. A stage that the run went
* into again has one more item.
*/
stageHistory: { stage: RunStage; startedAt: string }[];
workflowRunId: string | null;
appName: string | null;
webUrl: string | null;
Expand Down Expand Up @@ -79,7 +84,7 @@ export type DeleteClaim =
| { claimed: false; reason: "missing" };

const COLUMNS = `id, idempotency_key, prompt, user_name, status, stage, progress,
workflow_run_id,
stage_history, workflow_run_id,
app_name, web_url, api_url, blueprint_path, summary,
sandbox_id, sandbox_group_id,
created_at, updated_at, finished_at`;
Expand Down Expand Up @@ -173,13 +178,22 @@ export async function claimRun(input: {
: { claimed: false, reason: "at_capacity" };
}

/**
* Each change of stage adds the stage and the time to stage_history, so that
* the UI can show how long each stage took.
*/
export async function setRunStage(
id: string,
stage: RunStage,
progress: string | null = null,
): Promise<void> {
await db().query(
"update runs set stage = $2, progress = $3, updated_at = now() where id = $1",
`update runs
set stage = $2, progress = $3,
stage_history = stage_history ||
jsonb_build_array(jsonb_build_object('stage', $2::text, 'started_at', now())),
updated_at = now()
where id = $1`,
[id, stage, progress],
);
}
Expand Down Expand Up @@ -406,6 +420,14 @@ function rowToRun(row: Record<string, unknown>): RunRecord {
status: row.status as RunStatus,
stage: (row.stage as RunStage | null) ?? null,
progress: (row.progress as string | null) ?? null,
// Postgres writes each time in the JSON with the offset of its session.
// Give it in UTC, as the other times of the run are.
stageHistory: (
row.stage_history as { stage: RunStage; started_at: string }[]
).map((item) => ({
stage: item.stage,
startedAt: new Date(item.started_at).toISOString(),
})),
workflowRunId: (row.workflow_run_id as string | null) ?? null,
appName: (row.app_name as string | null) ?? null,
webUrl: (row.web_url as string | null) ?? null,
Expand Down
10 changes: 6 additions & 4 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ Architecture overview and how Render products fit together:
- 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.
- For a talk track, click **Table view**. It shows the stages as a table: what
each stage does, where it runs, and links to the workflow run and the sandbox
in the Render Dashboard. The row of the stage that runs now has a tint. The
sites table shows each URL and how long each run took.
each stage does, where it runs, how long it took, and links to the workflow
run and the sandbox in the Render Dashboard. The row of the stage that runs
now has a tint and a timer that counts its seconds. The sites table shows
each URL and how long each run took.
- 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`.
- If a run looks stuck, `GET /v1/apps/:runId` shows the current `stage` and `progress`,
and `stageHistory` shows when each stage started and stopped.
- If your network stops, the run continues on Render. Reload the page, and
the UI shows the same run again.

Expand Down
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,10 @@ it does not emulate the Render data plane.
Open `http://localhost:3000` and sign in with `UI_USERNAME` and `UI_PASSWORD`.
The **Table view** button opens `/table`, which shows the same runs as tables:
the sites with their URLs, the time that each run took, and a delete button,
and the stages with what each one does, where it runs, and links to the
workflow run and the sandbox in the Render Dashboard. Local task runs are not
in the Dashboard, so local development shows no workflow links.
and the stages with what each one does, where it runs, how long it took, and
links to the workflow run and the sandbox in the Render Dashboard. A timer
counts the seconds of the stage that runs now. Local task runs are not in the
Dashboard, so local development shows no workflow links.
The UI calls same-origin `/ui` endpoints; `FACTORY_API_KEY` stays on the
gateway and is never delivered to browser JavaScript. `UI_USERNAME` is also
the generated-app namespace: a user named `jacob` creates apps under
Expand Down
33 changes: 29 additions & 4 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -803,11 +803,16 @@ input {
width: 18%;
}

.stage-table thead th:nth-child(3) {
width: 24%;
/* The time column is wide enough for "1h 12m 34s". */
.stage-table thead th:nth-child(2) {
width: 104px;
}

.stage-table thead th:nth-child(4) {
width: 24%;
}

.stage-table thead th:nth-child(5) {
width: 14%;
}

Expand Down Expand Up @@ -839,6 +844,15 @@ input {
line-height: 20px;
}

/* All digits have the same width, so the timer does not move as it counts. */
.stage-table .stage-time {
font-family: var(--font-mono);
font-size: 13px;
font-variant-numeric: tabular-nums;
letter-spacing: 0;
white-space: nowrap;
}

.stage-table .stage-where {
color: var(--text);
}
Expand Down Expand Up @@ -983,10 +997,12 @@ input {
min-height: 44px;
}

/* Each stage is a block: its name, what it does, where it runs, and links. */
/*
* Each stage is a block: its name and its time on one line, and then what
* it does, where it runs, and links.
*/
.stage-table,
.stage-table tbody,
.stage-table tbody tr,
.stage-table th,
.stage-table td {
display: block;
Expand All @@ -1001,6 +1017,8 @@ input {
}

.stage-table tbody tr {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
padding: 10px 0;
border-bottom: 1px solid var(--border);
}
Expand All @@ -1012,9 +1030,16 @@ input {
}

.stage-table td {
grid-column: 1 / -1;
margin-top: 4px;
}

.stage-table .stage-time {
grid-column: 2;
margin-top: 0;
padding-left: 12px;
}

.stage-table tbody th::before {
top: 5px;
}
Expand Down
16 changes: 14 additions & 2 deletions public/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ <h3 id="result-name">Your website</h3>
<pre id="run-details" class="run-details" hidden></pre>
<!--
RUN_STAGES in app/store.ts lists the same stages in the same order.
The table is static, so aria-live="off" keeps the live region of the
run panel from reading it. table.js fills the Dashboard cells.
table.js fills the Time and Dashboard cells, and a timer changes the
time of the stage that runs now each second. So aria-live="off" keeps
the live region of the run panel from reading the table.
-->
<table id="stages" class="stage-table" aria-live="off">
<thead>
<tr>
<th scope="col">Stage</th>
<th scope="col">Time</th>
<th scope="col">What it does</th>
<th scope="col">Where it runs</th>
<th scope="col">Dashboard</th>
Expand All @@ -115,60 +117,70 @@ <h3 id="result-name">Your website</h3>
<tbody>
<tr data-stage="designing">
<th scope="row">Designing</th>
<td class="stage-time"></td>
<td>The architect agent plans the app and its Render resources. It cannot create anything.</td>
<td class="stage-where">Render Workflows <span>architect task</span></td>
<td class="stage-links" data-links="workflowRun"></td>
</tr>
<tr data-stage="provisioning">
<th scope="row">Provisioning</th>
<td class="stage-time"></td>
<td>Starts a real Postgres for the builder and the checks. An app without a database skips this stage.</td>
<td class="stage-where">Render Sandbox <span>started by the prompt-to-app task</span></td>
<td class="stage-links" data-links="workflowRun sandbox"></td>
</tr>
<tr data-stage="curating">
<th scope="row">Curating</th>
<td class="stage-time"></td>
<td>Downloads openly licensed photographs from Wikimedia Commons into the app. No model picks a URL.</td>
<td class="stage-where">Render Workflows <span>prompt-to-app task</span></td>
<td class="stage-links" data-links="workflowRun"></td>
</tr>
<tr data-stage="building">
<th scope="row">Building</th>
<td class="stage-time"></td>
<td>The builder agent writes the app and runs commands. It also fixes what verification finds.</td>
<td class="stage-where">Render Workflows + Sandbox <span>builder task; its tools act in the sandbox</span></td>
<td class="stage-links" data-links="workflowRun sandbox"></td>
</tr>
<tr data-stage="verifying">
<th scope="row">Verifying</th>
<td class="stage-time"></td>
<td>Builds, boots, and queries the app as Render will, from only the files that a commit holds.</td>
<td class="stage-where">Render Workflows + Sandbox <span>verify-app task; the builds run in the sandbox</span></td>
<td class="stage-links" data-links="workflowRun sandbox"></td>
</tr>
<tr data-stage="publishing">
<th scope="row">Publishing</th>
<td class="stage-time"></td>
<td>Copies the app into a clean clone of the apps repository, commits it with its Blueprint, and pushes to GitHub.</td>
<td class="stage-where">Render Workflows + Sandbox <span>publish-app task, in a second sandbox</span></td>
<td class="stage-links" data-links="workflowRun"></td>
</tr>
<tr data-stage="waiting_for_services">
<th scope="row">Waiting For Services</th>
<td class="stage-time"></td>
<td>The push starts a Blueprint sync. Render creates the app&rsquo;s project and services from render.yaml.</td>
<td class="stage-where">Render Blueprints <span>the prompt-to-app task waits on the API</span></td>
<td class="stage-links" data-links="workflowRun"></td>
</tr>
<tr data-stage="waiting_for_deploys">
<th scope="row">Waiting For Deploys</th>
<td class="stage-time"></td>
<td>Render builds and deploys each service. If a deploy fails, the deploy manager diagnoses it and the builder fixes it.</td>
<td class="stage-where">Render <span>repairs run in the deploy-manager and builder tasks</span></td>
<td class="stage-links" data-links="workflowRun"></td>
</tr>
<tr data-stage="smoke_testing">
<th scope="row">Smoke Testing</th>
<td class="stage-time"></td>
<td>Render says that the app is live. Now the workflow checks its public URLs, its data, and CORS.</td>
<td class="stage-where">Render Workflows <span>prompt-to-app task</span></td>
<td class="stage-links" data-links="workflowRun"></td>
</tr>
<tr data-stage="done">
<th scope="row">Done</th>
<td class="stage-time"></td>
<td>The app passed every check and is live.</td>
<td class="stage-where">Render <span>the app&rsquo;s own project</span></td>
<td class="stage-links" data-links="workflowRun"></td>
Expand Down
Loading
Loading