diff --git a/DOCKER.md b/DOCKER.md index 88b6de3d6..c286aa44d 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -126,7 +126,7 @@ docker run --rm --network host \ ``` This creates: -- 3 workspaces (`love-openstatus`, `test2`, `test3`) +- 3 workspaces (`love-openstatus` "Acme Inc.", `test2`, `test3`) - Sample monitors and 2 status pages (slugs `status` and `acme`) - Test user account: `ping@openstatus.dev` - Sample incidents, status reports, and maintenance windows diff --git a/packages/api/src/router/workspace.test.ts b/packages/api/src/router/workspace.test.ts index 4b62c5ffa..be81046bd 100644 --- a/packages/api/src/router/workspace.test.ts +++ b/packages/api/src/router/workspace.test.ts @@ -23,7 +23,7 @@ test("Get Test Workspace", async () => { expect(result).toMatchObject({ id: 1, slug: "love-openstatus", - name: "test", + name: "Acme Inc.", plan: "team", paidUntil: null, stripeId: "stripeId1", @@ -52,7 +52,7 @@ test("by default we get the first workspace", async () => { expect(result).toMatchObject({ id: 1, slug: "love-openstatus", - name: "test", + name: "Acme Inc.", plan: "team", paidUntil: null, stripeId: "stripeId1", diff --git a/packages/db/src/seed.mts b/packages/db/src/seed.mts index 24048af9e..536081f7a 100644 --- a/packages/db/src/seed.mts +++ b/packages/db/src/seed.mts @@ -66,7 +66,7 @@ async function main() { id: 1, slug: "love-openstatus", stripeId: "stripeId1", - name: "test", + name: "Acme Inc.", subscriptionId: "subscriptionId", plan: "team", endsAt: null, @@ -77,7 +77,7 @@ async function main() { id: 2, slug: "test2", stripeId: "stripeId2", - name: "test2", + name: "Side Project", subscriptionId: "subscriptionId2", plan: "free", endsAt: null, @@ -87,7 +87,7 @@ async function main() { id: 3, slug: "test3", stripeId: "stripeId3", - name: "test3", + name: "Acme Labs", subscriptionId: "subscriptionId3", plan: "team", endsAt: null, @@ -105,8 +105,8 @@ async function main() { workspaceId: 1, active: true, url: "https://www.openstatus.dev", - name: "OpenStatus", - description: "OpenStatus website", + name: "API", + description: "Public REST API", method: "POST", periodicity: "1m", regions: "ams", @@ -117,6 +117,8 @@ async function main() { id: 2, active: false, workspaceId: 1, + name: "Legacy Dashboard", + description: "Paused until the migration is done", periodicity: "10m", url: "https://www.google.com", method: "GET", @@ -128,8 +130,8 @@ async function main() { workspaceId: 1, active: true, url: "https://www.openstatus.dev", - name: "OpenStatus", - description: "OpenStatus website", + name: "Website", + description: "Marketing site and docs", method: "GET", periodicity: "1m", regions: "ams", @@ -140,6 +142,8 @@ async function main() { id: 4, active: true, workspaceId: 1, + name: "Search", + description: "Search backend, traces exported to OpenTelemetry", periodicity: "10m", url: "https://www.google.com", method: "GET", @@ -152,6 +156,8 @@ async function main() { id: 5, active: true, workspaceId: 3, + name: "Edge Proxy", + description: "Checked from a private location", periodicity: "10m", url: "https://openstat.us", method: "GET", @@ -168,7 +174,7 @@ async function main() { id: 1, workspaceId: 1, title: "Acme Inc.", - description: "Get informed about our services.", + description: "Live status of the Acme API, website and search.", icon: "https://www.openstatus.dev/favicon.ico", slug: "status", customDomain: "", @@ -182,8 +188,8 @@ async function main() { .values({ id: 2, workspaceId: 1, - title: "Acme Inc.", - description: "Get informed about our services.", + title: "Acme Enterprise", + description: "Status of the dedicated Enterprise infrastructure.", icon: "https://www.openstatus.dev/favicon.ico", slug: "acme", customDomain: "status.acme.com", @@ -268,8 +274,8 @@ async function main() { pageId: 1, type: "monitor", monitorId: 1, - name: "OpenStatus Monitor", - description: "Main website monitoring", + name: "API", + description: "Public REST API", order: 0, }, { @@ -278,8 +284,8 @@ async function main() { pageId: 1, type: "monitor", monitorId: 2, - name: "Google Monitor", - description: "Google.com monitoring", + name: "Legacy Dashboard", + description: "Being retired, read-only", order: 1, }, ]) @@ -291,7 +297,7 @@ async function main() { .values({ id: 1, provider: "email", - name: "sample test notification", + name: "On-call email", data: '{"email":"ping@openstatus.dev"}', workspaceId: 1, }) diff --git a/scripts/record.mjs b/scripts/record.mjs index a727e6506..be4132f0a 100644 --- a/scripts/record.mjs +++ b/scripts/record.mjs @@ -138,7 +138,13 @@ const cursorScript = () => { }; const { chromium } = await loadPlaywright(); -const browser = await chromium.launch(); +// Chromium ignores HTTPS_PROXY; external images (avatars, favicons) need it. +const proxy = process.env.HTTPS_PROXY ?? process.env.https_proxy; +const browser = await chromium.launch( + proxy + ? { proxy: { server: proxy, bypass: "localhost,127.0.0.1" } } + : undefined, +); const tmp = join(out, `.tmp-${process.pid}`); mkdirSync(tmp, { recursive: true }); const context = await browser.newContext({ @@ -181,13 +187,33 @@ async function moveTo(locator) { mouse = target; } +// A broken image in a tutorial is worse than no tutorial: fail the run. +async function settle() { + await page.waitForFunction( + () => [...document.images].every((img) => img.complete), + null, + { timeout: 15000 }, + ); + const broken = await page.evaluate(() => + [...document.images] + .filter((img) => img.naturalWidth === 0 && img.getClientRects().length) + .map((img) => img.currentSrc || img.src), + ); + if (broken.length) + throw new Error( + `images failed to load:\n ${[...new Set(broken)].join("\n ")}`, + ); +} + const rec = { page, sql, wait, moveTo, + settle, async goto(path) { await page.goto(path, { waitUntil: "networkidle" }); + await settle(); }, async click(locator) { await moveTo(locator); @@ -207,7 +233,8 @@ const rec = { for (const path of paths) await context.request.get(path); }, // Mark where the video starts; everything before (login, first load) is cut. - start() { + async start() { + await settle(); rec.startAt = (Date.now() - recordingStarted) / 1000; }, }; @@ -224,6 +251,12 @@ await context.close(); await browser.close(); const webm = await video.path(); +if (failed) { + rmSync(tmp, { recursive: true, force: true }); + console.error(`tutorial ${name} failed, no video written: ${failed.message}`); + process.exit(1); +} + const file = join(out, `${name}${theme === "dark" ? ".dark" : ""}`); const ffmpeg = spawnSync("ffmpeg", [ "-y", @@ -249,8 +282,3 @@ if (ffmpeg.error || ffmpeg.status !== 0) { console.log(`${file}.webm`); } else console.log(`${file}.mp4`); rmSync(tmp, { recursive: true, force: true }); - -if (failed) { - console.error(`tutorial ${name} failed: ${failed.message}`); - process.exit(1); -} diff --git a/scripts/tutorials/create-status-page.mjs b/scripts/tutorials/create-status-page.mjs index 74a7c376a..50f820b99 100644 --- a/scripts/tutorials/create-status-page.mjs +++ b/scripts/tutorials/create-status-page.mjs @@ -1,6 +1,6 @@ // How to create a status page: list → create form → edit page. -const SLUG = "acme-status"; +const SLUG = "acme-payments"; export default async function ({ page, @@ -11,6 +11,7 @@ export default async function ({ click, type, wait, + settle, }) { // Re-runs would otherwise hit the slug-uniqueness check. await sql([{ q: "delete from page where slug = ?", params: [SLUG] }]); @@ -18,23 +19,25 @@ export default async function ({ await warm(["/status-pages/create", "/status-pages/1/edit"]); await goto("/status-pages"); - start(); + await start(); await wait(1200); await click(page.getByRole("link", { name: "Create Status Page" }).first()); await page.waitForURL("**/status-pages/create"); + await settle(); await wait(800); - await type(page.getByPlaceholder("My Status Page"), "Acme Status"); + await type(page.getByPlaceholder("My Status Page"), "Acme Payments"); await wait(600); await type( page.getByLabel("Description"), - "Live status of the Acme API and dashboard.", + "Live status of Acme checkout and payouts.", ); await wait(600); await click(page.getByRole("button", { name: "Submit" })); await page.waitForURL(/\/status-pages\/\d+\/edit/); await page.waitForLoadState("networkidle"); + await settle(); await wait(2500); }