import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { stringify } from "superjson"; import { comparePasswordsAsync, hashPasswordAsync } from "../../packages/auth/security.ts"; import { customWidgetDefinitionSchema } from "../../packages/custom-widgets/src/core/index.ts"; import { emptySuperJSON, getBoardLaneColumnCount, getDefaultWidgetConfig, getRootSectionLane, getWidgetKindsForIntegration, widgetKinds, } from "../../packages/definitions/index.ts"; import type { BoardPermission, GroupPermissionKey, LayoutRole, WidgetKind } from "../../packages/definitions/index.ts"; import { db as rootDb, eq, inArray, sql } from "../../packages/db/index.ts"; import { apps, boardGroupPermissions, boardUserPermissions, boards, customWidgetDefinitions, groupMembers, groupPermissions, groups, iconRepositories, icons, integrationItems, integrations, itemLayouts, items, layouts, onboarding, sectionLayouts, sections, users, } from "../../packages/db/schema/index.ts"; import { boardNameSchema } from "../../packages/validation/src/board.ts"; import { findFixtureGeometryErrors } from "./geometry.mts"; import { assertReleaseV2QaProfileEnvironment, releaseV2QaProfiles, resolveCheckoutCandidateSha, validateCandidateSha, } from "./provenance.mts"; import type { ReleaseV2QaProfile, ReleaseV2QaProfileFlags } from "./provenance.mts"; import { validateFixtureUrl } from "./safety.mts"; let db = rootDb; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const profiles = releaseV2QaProfiles; type Profile = ReleaseV2QaProfile; interface CliOptions { candidateSha?: string; manifestPath?: string; outputDirectory?: string; profile?: Profile; reset: boolean; } interface PersonaDefinition { handle: string; name: string; permissions: GroupPermissionKey[]; } interface LayoutDefinition { breakpoint: number; columnCount: number; handle: string; leftGutterColumnCount?: number; name: string; rightGutterColumnCount?: number; role: LayoutRole; } interface BoardDefinition { handle: string; isPublic?: boolean; label: string; layouts?: LayoutDefinition[]; ownerHandle: string; } interface ItemDefinition { advancedOptions?: string; boardHandle: string; handle: string; height: number; kind: WidgetKind; options: string; sectionHandle?: string; width: number; } interface Position { boardHandle: string; height: number; itemHandle: string; layoutHandle: string; sectionHandle: string; width: number; xOffset: number; yOffset: number; } const mobileLayout: LayoutDefinition = { handle: "mobile", name: "Mobile", columnCount: 3, breakpoint: 0, role: "mobile", }; const defaultLayouts: LayoutDefinition[] = [ mobileLayout, { handle: "base", name: "Base", columnCount: 12, breakpoint: 768, role: "base" }, ]; const personas: PersonaDefinition[] = [ { handle: "avery-admin", name: "Avery Admin", permissions: ["admin"] }, { handle: "rowan-owner", name: "Rowan Owner", permissions: ["board-create"] }, { handle: "eden-editor", name: "Eden Editor", permissions: [] }, { handle: "vivian-viewer", name: "Vivian Viewer", permissions: [] }, { handle: "nolan-outsider", name: "Nolan Outsider", permissions: [] }, { handle: "morgan-mobile", name: "Morgan Mobile", permissions: [] }, { handle: "casey-chaos", name: "Casey Chaos", permissions: [] }, { handle: "ingrid-infra", name: "Ingrid Infra", permissions: ["integration-full-all"] }, { handle: "maya-media", name: "Maya Media", permissions: ["media-full-all"] }, { handle: "brooke-minimalist", name: "Brooke Minimalist", permissions: [] }, { handle: "cora-creator", name: "Cora Creator", permissions: ["board-create", "app-create"] }, { handle: "ash-assistant", name: "Ash Assistant", permissions: [] }, { handle: "kira-keyboard", name: "Kira Keyboard", permissions: [] }, { handle: "nora-newcomer", name: "Nora Newcomer", permissions: [] }, ]; const widgetGalleryGroups: readonly (readonly WidgetKind[])[] = [ ["clock", "weather", "airQuality", "countdown", "timer"], ["app", "iframe", "video", "minecraftServerStatus", "stockPrice"], ["notebook", "anchorNote", "bookmarks", "rssFeed", "timetable"], ["downloads", "dockerContainers", "indexerManager", "dnsHoleSummary", "dnsHoleControls"], ["smartHome-entityState", "smartHome-executeAutomation", "healthMonitoring", "systemResources", "systemDisks"], ["firewall", "notifications", "networkControllerSummary", "networkControllerStatus", "uptimeKuma"], ["beszelSystemTable", "beszelSystemGrid", "beszelAlerts", "beszelSystemStats", "wud"], ["ups", "vpn", "speedtestTracker", "traefik", "umami"], ["calendar", "mediaServer", "mediaRequests-requestList", "mediaRequests-requestStats", "mediaMissing"], ["mediaReleases", "mediaTranscoding", "immich-serverStats", "immich-albumCarousel", "audioStats"], ["paperlessNgx", "patchmon", "bazarr", "tracearr", "releases"], ["coolify", "archiveTeamWarrior", "customApi", "assistant"], ]; const sharedBoards: BoardDefinition[] = [ { handle: "grid-24", label: "QA v2 · 24-column grid", ownerHandle: "avery-admin", layouts: [ mobileLayout, { handle: "six", name: "6 columns", columnCount: 6, breakpoint: 480, role: "custom", leftGutterColumnCount: 1, rightGutterColumnCount: 1, }, { handle: "twelve", name: "12 columns", columnCount: 12, breakpoint: 768, role: "custom", leftGutterColumnCount: 1, rightGutterColumnCount: 1, }, { handle: "eighteen", name: "18 columns", columnCount: 18, breakpoint: 1024, role: "custom", leftGutterColumnCount: 1, rightGutterColumnCount: 1, }, { handle: "width-coverage", name: "24-column width coverage", columnCount: 24, breakpoint: 1280, role: "custom", }, { handle: "base", name: "Base 24", columnCount: 24, breakpoint: 1440, role: "base", leftGutterColumnCount: 1, rightGutterColumnCount: 1, }, ], }, { handle: "scroll", label: "QA v2 · Scroll", ownerHandle: "avery-admin", layouts: [ mobileLayout, { handle: "base", name: "Base with rails", columnCount: 12, breakpoint: 768, role: "base", leftGutterColumnCount: 2, rightGutterColumnCount: 2, }, ], }, { handle: "dense-collisions", label: "QA v2 · Dense collisions", ownerHandle: "avery-admin" }, { handle: "nested-containers", label: "QA v2 · Nested containers", ownerHandle: "avery-admin" }, { handle: "layout-boundaries", label: "QA v2 · Layout boundaries", ownerHandle: "avery-admin", layouts: [ mobileLayout, { handle: "compact", name: "Compact", columnCount: 6, breakpoint: 480, role: "custom" }, { handle: "desktop", name: "Desktop", columnCount: 12, breakpoint: 1024, role: "custom" }, { handle: "base", name: "Base 24", columnCount: 24, breakpoint: 1440, role: "base" }, ], }, { handle: "icons-bookmarks", label: "QA v2 · Icons and bookmarks", ownerHandle: "avery-admin" }, ...Array.from({ length: 12 }, (_, index) => { const handle = `widgets-${String(index + 1).padStart(2, "0")}`; return { handle, label: `QA v2 · ${handle}`, ownerHandle: "avery-admin" }; }), { handle: "custom-widget-assistant", label: "QA v2 · Custom Widget and Assistant", ownerHandle: "avery-admin", }, { handle: "permissions-public", label: "QA v2 · Permissions and public access", ownerHandle: "rowan-owner", isPublic: true, }, { handle: "download-upload", label: "QA v2 · Download and upload", ownerHandle: "maya-media" }, ]; const homeBoards: BoardDefinition[] = personas.map((persona) => ({ handle: `home-${persona.handle}`, label: `QA v2 · Home · ${persona.handle}`, ownerHandle: persona.handle, })); const qaBoards = [...homeBoards, ...sharedBoards]; const boardName = (handle: string) => { if (handle === "scroll") return "qa-scroll-lab"; return `qa-${handle}`; }; const validateQaBoardDefinitions = () => { const names = qaBoards.map((board) => boardName(board.handle)); if (new Set(names).size !== names.length) throw new Error("QA board names must be unique"); for (const name of names) { if (!boardNameSchema.safeParse(name).success) throw new Error(`Invalid QA board name: ${name}`); } }; const userId = (handle: string) => `qa-v2-user-${handle}`; const groupId = (handle: string) => `qa-v2-group-${handle}`; const boardId = (handle: string) => `qa-v2-board-${handle}`; const layoutId = (boardHandle: string, handle: string) => `qa-v2-layout-${boardHandle}-${handle}`; const sectionId = (boardHandle: string, handle: string) => `qa-v2-section-${boardHandle}-${handle}`; const itemId = (boardHandle: string, handle: string) => `qa-v2-item-${boardHandle}-${handle}`; const appId = (index: number) => `qa-v2-app-${String(index).padStart(2, "0")}`; const iconId = (index: number) => `qa-v2-icon-${String(index).padStart(2, "0")}`; const qaUserIds = personas.map((persona) => userId(persona.handle)); const qaBoardIds = qaBoards.map((board) => boardId(board.handle)); const qaLayoutIds = qaBoards.flatMap((board) => (board.layouts ?? defaultLayouts).map((layout) => layoutId(board.handle, layout.handle)), ); const qaGroupIds = [groupId("fixture-access"), ...personas.map((persona) => groupId(persona.handle))]; const qaAppIds = Array.from({ length: 8 }, (_, index) => appId(index + 1)); const qaIconIds = Array.from({ length: 8 }, (_, index) => iconId(index + 1)); const mockIntegrationId = "qa-v2-integration-mock"; const customDefinitionId = "qa-v2-custom-widget-fixture"; const disabledCustomDefinitionId = "qa-v2-custom-widget-disabled"; const missingSecretCustomDefinitionId = "qa-v2-custom-widget-missing-secret"; const customDefinitionIds = [customDefinitionId, disabledCustomDefinitionId, missingSecretCustomDefinitionId]; const iconRepositoryId = "qa-v2-icon-repository"; const parseCli = (): CliOptions => { const options: CliOptions = { reset: false }; for (let index = 2; index < process.argv.length; index++) { const argument = process.argv[index]; if (argument === "--") continue; if (argument === "--reset") { options.reset = true; continue; } if (!["--candidate-sha", "--manifest", "--output", "--profile"].includes(argument ?? "")) { throw new Error(`Unknown argument: ${argument ?? ""}`); } const value = process.argv[index + 1]; if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}`); index++; if (argument === "--candidate-sha") options.candidateSha = value; if (argument === "--manifest") options.manifestPath = value; if (argument === "--output") options.outputDirectory = value; if (argument === "--profile") { if (!profiles.includes(value as Profile)) throw new Error(`Unsupported profile: ${value}`); options.profile = value as Profile; } } return options; }; const requiredEnv = (name: string) => { const value = process.env[name]?.trim(); if (!value) throw new Error(`${name} is required`); return value; }; const requiredQaPassword = async () => { const direct = process.env.QA_PASSWORD?.trim(); if (direct) return direct; const passwordFile = process.env.QA_PASSWORD_FILE?.trim(); if (passwordFile) { const fromFile = (await readFile(resolve(passwordFile), "utf8")).trim(); if (fromFile) return fromFile; } throw new Error("QA_PASSWORD or QA_PASSWORD_FILE is required"); }; const runInTransactionAsync = async (operation: () => Promise) => { if ((process.env.DB_DRIVER ?? "better-sqlite3") === "better-sqlite3") { rootDb.run(sql.raw("BEGIN IMMEDIATE")); try { await operation(); rootDb.run(sql.raw("COMMIT")); } catch (error) { rootDb.run(sql.raw("ROLLBACK")); throw error; } return; } try { await rootDb.transaction(async (transaction) => { db = transaction as typeof rootDb; await operation(); }); } finally { db = rootDb; } }; const resolveManifestPath = (options: CliOptions) => { const explicit = options.manifestPath ?? process.env.QA_FIXTURE_MANIFEST; if (explicit) return resolve(explicit); if (options.outputDirectory) return resolve(options.outputDirectory, "fixture-manifest.json"); const runRoot = process.env.QA_RUN_ROOT; const slot = process.env.QA_SLOT; if (runRoot && slot) return resolve(runRoot, "slots", slot, "fixture-manifest.json"); throw new Error("--manifest, --output, or QA_FIXTURE_MANIFEST is required"); }; const upsertById = async ( query: { findFirst: (input: unknown) => Promise }, table: { id: unknown }, row: TRow, ) => { const existing = await query.findFirst({ where: eq(table.id as never, row.id) }); if (existing) { await db .update(table as never) .set(row as never) .where(eq(table.id as never, row.id)); return; } await db.insert(table as never).values(row as never); }; const resetQaRowsAsync = async () => { if (qaBoardIds.length > 0) await db.delete(boards).where(inArray(boards.id, qaBoardIds)); if (qaUserIds.length > 0) await db.delete(users).where(inArray(users.id, qaUserIds)); if (qaGroupIds.length > 0) await db.delete(groups).where(inArray(groups.id, qaGroupIds)); await db.delete(customWidgetDefinitions).where(inArray(customWidgetDefinitions.id, customDefinitionIds)); await db.delete(integrations).where(eq(integrations.id, mockIntegrationId)); if (qaAppIds.length > 0) await db.delete(apps).where(inArray(apps.id, qaAppIds)); if (qaIconIds.length > 0) await db.delete(icons).where(inArray(icons.id, qaIconIds)); await db.delete(iconRepositories).where(eq(iconRepositories.id, iconRepositoryId)); }; const setOnboardingStepAsync = async (profile: Profile) => { const row = await db.query.onboarding.findFirst(); if (!row) throw new Error("Production seed did not create the onboarding row"); if (profile === "onboarding-fresh") { await db.update(onboarding).set({ step: "start", previousStep: null }).where(eq(onboarding.id, row.id)); return; } await db.update(onboarding).set({ step: "finish", previousStep: "setup" }).where(eq(onboarding.id, row.id)); }; const resolvePasswordHashAsync = async (password: string) => { const existing = await db.query.users.findMany({ where: inArray(users.id, qaUserIds), columns: { password: true } }); for (const row of existing) { if (row.password && (await comparePasswordsAsync(password, row.password))) return row.password; } return hashPasswordAsync(password); }; const seedPersonasAsync = async (passwordHash: string) => { for (const persona of personas) { const id = userId(persona.handle); await upsertById( db.query.users as never, users as never, { id, name: persona.handle, email: `qa-v2-${persona.handle}@example.test`, emailVerified: null, image: null, password: passwordHash, provider: "credentials", homeBoardId: null, mobileHomeBoardId: null, defaultSearchEngineId: null, openSearchInNewTab: true, ddgBangs: true, colorScheme: "dark", firstDayOfWeek: 1, pingIconsEnabled: true, enableRightClickOnWidgets: true, completedManageTour: true, completedBoardTour: true, } as typeof users.$inferSelect, ); } await upsertById( db.query.groups as never, groups as never, { id: groupId("fixture-access"), name: "QA v2 fixture access", ownerId: userId("avery-admin"), homeBoardId: null, mobileHomeBoardId: null, position: 10_000, } as typeof groups.$inferSelect, ); for (const [index, persona] of personas.entries()) { await upsertById( db.query.groups as never, groups as never, { id: groupId(persona.handle), name: `QA v2 role · ${persona.handle}`, ownerId: userId("avery-admin"), homeBoardId: null, mobileHomeBoardId: null, position: 10_001 + index, } as typeof groups.$inferSelect, ); } await db.delete(groupPermissions).where(inArray(groupPermissions.groupId, qaGroupIds)); await db.delete(groupMembers).where(inArray(groupMembers.userId, qaUserIds)); await db .insert(groupPermissions) .values([ { groupId: groupId("fixture-access"), permission: "app-use-all" }, { groupId: groupId("fixture-access"), permission: "integration-use-all" }, ...personas.flatMap((persona) => persona.permissions.map((permission) => ({ groupId: groupId(persona.handle), permission })), ), ]); await db .insert(groupMembers) .values([ ...personas.map((persona) => ({ groupId: groupId("fixture-access"), userId: userId(persona.handle) })), ...personas.map((persona) => ({ groupId: groupId(persona.handle), userId: userId(persona.handle) })), ]); }; const seedGlobalFixturesAsync = async (fixtureUrl: string) => { await upsertById( db.query.integrations as never, integrations as never, { id: mockIntegrationId, name: "QA v2 built-in mock integration", url: fixtureUrl, kind: "mock", appId: null, } as typeof integrations.$inferSelect, ); const appRows = qaAppIds.map((id, index) => ({ id, name: `QA Fixture ${String(index + 1).padStart(2, "0")}`, description: `Deterministic release v2 QA bookmark ${index + 1}`, iconUrl: `${fixtureUrl}/media/image.png?app=${index + 1}`, href: [ `${fixtureUrl}/api/qa/download/sample.txt`, null, "/relative/qa-v2", "https://example.com/qa-v2-external", `${fixtureUrl}/error?status=404`, `${fixtureUrl}/iframe?app=6`, `${fixtureUrl}/empty`, `${fixtureUrl}/slow?ms=200`, ][index] ?? null, pingUrl: index === 1 ? null : index === 4 ? `${fixtureUrl}/error?status=503` : `${fixtureUrl}/json`, })); for (const row of appRows) await upsertById(db.query.apps as never, apps as never, row); await upsertById(db.query.iconRepositories as never, iconRepositories as never, { id: iconRepositoryId, slug: "qa-v2/release-fixtures", }); for (const [index, id] of qaIconIds.entries()) { await upsertById(db.query.icons as never, icons as never, { id, name: `qa-v2-icon-${String(index + 1).padStart(2, "0")}`, url: `${fixtureUrl}/media/image.png?icon=${index + 1}`, checksum: `qa-v2-checksum-${String(index + 1).padStart(2, "0")}`, iconRepositoryId, }); } const definition = customWidgetDefinitionSchema.parse({ $schema: "homarr-custom-widget-v2", name: "Release v2 QA fixture", description: "Credential-free deterministic Custom JSX fixture for release v2 browser QA.", sources: { default: { name: "QA fixture", baseUrl: fixtureUrl, networkScope: "loopback", auth: "none" }, }, requests: { fixture: { path: "/api/qa/custom-widget", cacheSeconds: 0 } }, options: {}, template: `{data.fixture?.title ?? "Release v2 QA"}{data.fixture?.status ?? "loading"}{status.fixture?.loading ? : status.fixture?.error ? {status.fixture.error} : {(data.fixture?.items ?? []).map(item => {item.label}{item.value})}}`, }); const missingSecretDefinition = customWidgetDefinitionSchema.parse({ ...definition, name: "Release v2 QA missing secret", description: "Valid bearer-auth definition intentionally stored without a secret.", sources: { default: { name: "QA protected fixture", baseUrl: fixtureUrl, networkScope: "loopback", auth: "bearer" }, }, }); const definitions = [ { id: customDefinitionId, definition, enabled: true }, { id: disabledCustomDefinitionId, definition: { ...definition, name: "Release v2 QA disabled" }, enabled: false }, { id: missingSecretCustomDefinitionId, definition: missingSecretDefinition, enabled: true }, ]; for (const row of definitions) { await upsertById( db.query.customWidgetDefinitions as never, customWidgetDefinitions as never, { id: row.id, name: row.definition.name, description: row.definition.description ?? null, iconUrl: null, sources: stringify(row.definition.sources), requests: stringify(row.definition.requests), options: stringify(row.definition.options), template: row.definition.template, enabled: row.enabled, createdAt: new Date(0), updatedAt: new Date(0), creatorId: userId("avery-admin"), } as typeof customWidgetDefinitions.$inferSelect, ); } }; const sourceOptionsByKindAsync = async () => { let sourceBoard = await db.query.boards.findFirst({ where: eq(boards.name, "default"), with: { items: true } }); sourceBoard ??= await db.query.boards.findFirst({ where: eq(boards.name, "dashboard"), with: { items: true } }); if (!sourceBoard) throw new Error("Production seed did not create a widget gallery board"); const result = new Map(); for (const kind of widgetKinds) { const source = sourceBoard.items.find((item) => item.kind === kind); result.set(kind, source?.options ?? emptySuperJSON); } result.set("app", stringify({ appId: qaAppIds[0], openInNewTab: true, showTitle: true, pingEnabled: false })); result.set("bookmarks", stringify({ title: "QA bookmarks", layout: "grid", items: qaAppIds, openNewTab: true })); result.set( "customApi", stringify({ definitionId: customDefinitionId, configuration: {}, configurationVersion: 1, refreshInterval: 1 }), ); return result; }; const boardLayouts = (board: BoardDefinition) => board.layouts ?? defaultLayouts; const emptySectionOffset = (sectionHandle: string) => { if (sectionHandle === "left") return -1; if (sectionHandle === "right") return 1; return 0; }; const mainLaneColumnCount = (layout: LayoutDefinition) => getBoardLaneColumnCount(layout, "main"); const sectionColumnCount = (layout: LayoutDefinition, sectionHandle: string) => { if (["root", "left", "right"].includes(sectionHandle)) { return getBoardLaneColumnCount(layout, getRootSectionLane(emptySectionOffset(sectionHandle))); } const mainColumns = mainLaneColumnCount(layout); if (sectionHandle === "overflow") return Math.min(mainColumns, 6); if (sectionHandle === "level-1") return mainColumns; if (sectionHandle === "level-2") return Math.max(1, mainColumns - 1); if (sectionHandle === "level-3") return Math.max(1, mainColumns - 2); if (sectionHandle.startsWith("sibling-")) return Math.min(mainColumns, 2); throw new Error(`Unknown QA section handle: ${sectionHandle}`); }; const resolveItemSectionHandle = (layout: LayoutDefinition, requestedHandle: string) => { if (!["left", "right"].includes(requestedHandle)) return requestedHandle; if (sectionColumnCount(layout, requestedHandle) > 0) return requestedHandle; return "root"; }; const packPositions = (itemDefinitions: ItemDefinition[], board: BoardDefinition): Position[] => { return boardLayouts(board).flatMap((layout) => { const cursors = new Map(); return itemDefinitions.map((item) => { const sectionHandle = resolveItemSectionHandle(layout, item.sectionHandle ?? "root"); const columnCount = sectionColumnCount(layout, sectionHandle); const width = Math.min(item.width, columnCount); const cursor = cursors.get(sectionHandle) ?? { xOffset: 0, yOffset: 0, rowHeight: 0 }; if (cursor.xOffset + width > columnCount) { cursor.xOffset = 0; cursor.yOffset += cursor.rowHeight; cursor.rowHeight = 0; } const position = { boardHandle: item.boardHandle, itemHandle: item.handle, layoutHandle: layout.handle, sectionHandle, xOffset: cursor.xOffset, yOffset: cursor.yOffset, width, height: item.height, }; cursor.xOffset += width; cursor.rowHeight = Math.max(cursor.rowHeight, item.height); cursors.set(sectionHandle, cursor); return position; }); }); }; const itemForKind = ( boardHandle: string, handle: string, kind: WidgetKind, optionsByKind: Map, ): ItemDefinition => { const defaultConfig = getDefaultWidgetConfig(kind); let options = optionsByKind.get(kind) ?? emptySuperJSON; if (options === emptySuperJSON && defaultConfig.options) options = stringify(defaultConfig.options); return { boardHandle, handle, kind, options, width: defaultConfig.width, height: defaultConfig.height, }; }; const buildItems = (optionsByKind: Map) => { const result = new Map(); for (const board of homeBoards) { result.set( board.handle, (["clock", "weather", "bookmarks"] as WidgetKind[]).map((kind, index) => itemForKind(board.handle, `${kind}-${index + 1}`, kind, optionsByKind), ), ); } result.set( "grid-24", Array.from({ length: 24 }, (_, index) => ({ ...itemForKind("grid-24", `cell-${String(index + 1).padStart(2, "0")}`, "clock", optionsByKind), width: index + 1, height: (index % 6) + 1, })), ); result.set("scroll", [ ...Array.from({ length: 30 }, (_, index) => ({ ...itemForKind( "scroll", `row-${String(index + 1).padStart(2, "0")}`, index % 2 === 0 ? "notebook" : "weather", optionsByKind, ), sectionHandle: ["left", "root", "right"][index % 3], })), ...Array.from({ length: 12 }, (_, index) => ({ ...itemForKind("scroll", `overflow-${index + 1}`, index % 2 === 0 ? "clock" : "app", optionsByKind), sectionHandle: "overflow", })), ]); result.set( "dense-collisions", Array.from({ length: 12 }, (_, index) => itemForKind("dense-collisions", `collision-${index + 1}`, "clock", optionsByKind), ), ); result.set("nested-containers", [ ...(["clock", "weather", "bookmarks"] as WidgetKind[]).map((kind, index) => ({ ...itemForKind("nested-containers", `nested-${index + 1}`, kind, optionsByKind), sectionHandle: "level-3", })), ...(["notebook", "app", "countdown"] as WidgetKind[]).map((kind, index) => ({ ...itemForKind("nested-containers", `sibling-${index + 1}`, kind, optionsByKind), sectionHandle: `sibling-${index + 1}`, })), itemForKind("nested-containers", "root-zone", "clock", optionsByKind), ]); result.set( "layout-boundaries", widgetKinds .slice(0, 16) .map((kind, index) => itemForKind("layout-boundaries", `boundary-${index + 1}`, kind, optionsByKind)), ); result.set("icons-bookmarks", [ { ...itemForKind("icons-bookmarks", "bookmarks-grid", "bookmarks", optionsByKind), options: stringify({ title: "QA grid", layout: "grid", items: qaAppIds, openNewTab: true }), }, { ...itemForKind("icons-bookmarks", "bookmarks-list", "bookmarks", optionsByKind), options: stringify({ title: "QA list", layout: "column", items: qaAppIds, openNewTab: false }), }, { ...itemForKind("icons-bookmarks", "bookmarks-empty-title", "bookmarks", optionsByKind), options: stringify({ title: "", layout: "icons", items: qaAppIds.slice(0, 4), openNewTab: true }), }, { ...itemForKind("icons-bookmarks", "bookmarks-custom-title", "bookmarks", optionsByKind), options: stringify({ title: "External and relative", layout: "row", items: qaAppIds.slice(2, 6), openNewTab: false, }), }, ...qaAppIds.slice(0, 6).map((id, index) => ({ ...itemForKind("icons-bookmarks", `app-${index + 1}`, "app", optionsByKind), options: stringify({ appId: id, openInNewTab: index % 2 === 0, showTitle: index >= 3, pingEnabled: index !== 1, }), })), ]); widgetGalleryGroups.forEach((group, groupIndex) => { const boardHandle = `widgets-${String(groupIndex + 1).padStart(2, "0")}`; result.set( boardHandle, group.map((kind, kindIndex) => itemForKind(boardHandle, `${String(kindIndex + 1).padStart(2, "0")}-${kind}`, kind, optionsByKind), ), ); }); result.set("custom-widget-assistant", [ { ...itemForKind("custom-widget-assistant", "custom-compact", "customApi", optionsByKind), width: 3, height: 2, }, { ...itemForKind("custom-widget-assistant", "custom-standard-disabled", "customApi", optionsByKind), options: stringify({ definitionId: disabledCustomDefinitionId, configuration: {}, configurationVersion: 1, refreshInterval: 1, }), width: 6, height: 4, }, { ...itemForKind("custom-widget-assistant", "custom-wide-missing-secret", "customApi", optionsByKind), options: stringify({ definitionId: missingSecretCustomDefinitionId, configuration: {}, configurationVersion: 1, refreshInterval: 1, }), width: 12, height: 4, }, { ...itemForKind("custom-widget-assistant", "custom-invalid-option", "customApi", optionsByKind), options: stringify({ definitionId: customDefinitionId, configuration: { unsupportedOption: "qa-invalid" }, configurationVersion: 1, refreshInterval: 1, }), width: 6, height: 3, }, { ...itemForKind("custom-widget-assistant", "assistant-compact", "assistant", optionsByKind), width: 3, height: 2 }, { ...itemForKind("custom-widget-assistant", "assistant-standard", "assistant", optionsByKind), width: 6, height: 4, }, { ...itemForKind("custom-widget-assistant", "assistant-wide", "assistant", optionsByKind), width: 12, height: 4 }, ]); result.set("permissions-public", [ itemForKind("permissions-public", "clock", "clock", optionsByKind), itemForKind("permissions-public", "bookmarks", "bookmarks", optionsByKind), ]); result.set("download-upload", [ { ...itemForKind("download-upload", "downloads", "downloads", optionsByKind), width: 6, height: 3 }, { ...itemForKind("download-upload", "download-link", "app", optionsByKind), options: stringify({ appId: qaAppIds[0], openInNewTab: true, showTitle: true, pingEnabled: false }), }, ]); return result; }; const assertSeededGeometryAsync = async () => { const geometryLayouts = await db .select({ id: layouts.id, boardId: layouts.boardId, columnCount: layouts.columnCount, leftGutterColumnCount: layouts.leftGutterColumnCount, rightGutterColumnCount: layouts.rightGutterColumnCount, role: layouts.role, }) .from(layouts) .where(inArray(layouts.id, qaLayoutIds)); const geometrySections = await db .select({ id: sections.id, boardId: sections.boardId, kind: sections.kind, xOffset: sections.xOffset, }) .from(sections) .where(inArray(sections.boardId, qaBoardIds)); const geometrySectionLayouts = await db .select({ sectionId: sectionLayouts.sectionId, layoutId: sectionLayouts.layoutId, parentSectionId: sectionLayouts.parentSectionId, xOffset: sectionLayouts.xOffset, yOffset: sectionLayouts.yOffset, width: sectionLayouts.width, height: sectionLayouts.height, }) .from(sectionLayouts) .where(inArray(sectionLayouts.layoutId, qaLayoutIds)); const geometryItems = await db .select({ id: items.id, boardId: items.boardId }) .from(items) .where(inArray(items.boardId, qaBoardIds)); const itemBoardById = new Map(geometryItems.map((item) => [item.id, item.boardId])); const geometryItemLayouts = await db .select({ itemId: itemLayouts.itemId, sectionId: itemLayouts.sectionId, layoutId: itemLayouts.layoutId, xOffset: itemLayouts.xOffset, yOffset: itemLayouts.yOffset, width: itemLayouts.width, height: itemLayouts.height, }) .from(itemLayouts) .where(inArray(itemLayouts.layoutId, qaLayoutIds)); const geometryErrors = findFixtureGeometryErrors({ layouts: geometryLayouts, sections: geometrySections, sectionLayouts: geometrySectionLayouts, itemLayouts: geometryItemLayouts.map((itemLayout) => ({ ...itemLayout, boardId: itemBoardById.get(itemLayout.itemId) ?? "", })), }); if (geometryErrors.length > 0) { throw new Error(`Generated QA fixture geometry is invalid: ${geometryErrors.join("; ")}`); } }; const seedBoardsAsync = async (optionsByKind: Map) => { for (const board of qaBoards) { const name = boardName(board.handle); await upsertById( db.query.boards as never, boards as never, { id: boardId(board.handle), name, isPublic: board.isPublic ?? false, creatorId: userId(board.ownerHandle), pageTitle: board.label, metaTitle: board.label, logoImageUrl: null, faviconImageUrl: null, backgroundImageUrl: null, backgroundImageAttachment: "fixed", backgroundImageRepeat: "no-repeat", backgroundImageSize: "cover", primaryColor: "#748ffc", secondaryColor: "#22b8cf", opacity: 95, customCss: null, iconColor: null, itemRadius: "md", disableStatus: false, } as typeof boards.$inferSelect, ); } await db.delete(items).where(inArray(items.boardId, qaBoardIds)); await db.delete(sections).where(inArray(sections.boardId, qaBoardIds)); await db.delete(layouts).where(inArray(layouts.boardId, qaBoardIds)); const allItems = buildItems(optionsByKind); const positionRows: Position[] = []; for (const board of qaBoards) { await db.insert(layouts).values( boardLayouts(board).map((layout) => ({ id: layoutId(board.handle, layout.handle), name: layout.name, boardId: boardId(board.handle), columnCount: layout.columnCount, leftGutterColumnCount: layout.leftGutterColumnCount ?? 0, rightGutterColumnCount: layout.rightGutterColumnCount ?? 0, breakpoint: layout.breakpoint, role: layout.role, })), ); await db.insert(sections).values({ id: sectionId(board.handle, "root"), boardId: boardId(board.handle), kind: "empty", xOffset: 0, yOffset: 0, name: null, options: emptySuperJSON, }); if (board.handle === "scroll") { await db.insert(sections).values([ { id: sectionId(board.handle, "left"), boardId: boardId(board.handle), kind: "empty", xOffset: -1, yOffset: 0, name: "Left rail", options: emptySuperJSON, }, { id: sectionId(board.handle, "right"), boardId: boardId(board.handle), kind: "empty", xOffset: 1, yOffset: 0, name: "Right rail", options: emptySuperJSON, }, { id: sectionId(board.handle, "overflow"), boardId: boardId(board.handle), kind: "container", xOffset: null, yOffset: null, name: "Scrollable container", options: stringify({ showLabel: true, collapsible: true, scrollable: true, showOpenAll: true }), }, ]); await db.insert(sectionLayouts).values( boardLayouts(board).map((layout) => ({ sectionId: sectionId(board.handle, "overflow"), layoutId: layoutId(board.handle, layout.handle), parentSectionId: sectionId(board.handle, "root"), xOffset: 0, yOffset: 0, width: sectionColumnCount(layout, "overflow"), height: 4, })), ); } if (board.handle === "nested-containers") { const containerHandles = ["level-1", "level-2", "level-3", "sibling-1", "sibling-2", "sibling-3"]; await db.insert(sections).values( containerHandles.map((handle, index) => ({ id: sectionId(board.handle, handle), boardId: boardId(board.handle), kind: "container" as const, xOffset: null, yOffset: null, name: index < 3 ? `Nested level ${index + 1}` : `Sibling zone ${index - 2}`, options: stringify({ showLabel: index !== 4, collapsible: index !== 5, scrollable: index === 2 || index === 4, showOpenAll: index === 3, }), })), ); await db.insert(sectionLayouts).values( boardLayouts(board).flatMap((layout) => containerHandles.map((handle, index) => { const width = sectionColumnCount(layout, handle); const parentHandle = index === 0 ? "root" : index < 3 ? `level-${index}` : "root"; const parentColumns = sectionColumnCount(layout, parentHandle); return { sectionId: sectionId(board.handle, handle), layoutId: layoutId(board.handle, layout.handle), parentSectionId: sectionId(board.handle, parentHandle), xOffset: index < 3 ? 0 : Math.max(0, Math.min((index - 3) * 2, parentColumns - width)), yOffset: index < 3 ? 0 : 7, width, height: index < 3 ? 6 - index : 3, }; }), ), ); } const boardItems = allItems.get(board.handle) ?? []; if (boardItems.length > 0) { await db.insert(items).values( boardItems.map((item) => ({ id: itemId(board.handle, item.handle), boardId: boardId(board.handle), kind: item.kind, options: item.options, advancedOptions: item.advancedOptions ?? emptySuperJSON, })), ); } if (board.handle === "dense-collisions") { positionRows.push( ...boardLayouts(board).flatMap((layout) => boardItems.map((item, index) => { const columnCount = mainLaneColumnCount(layout); const width = Math.min(columnCount, 2 + (index % 3)); return { boardHandle: board.handle, itemHandle: item.handle, layoutHandle: layout.handle, sectionHandle: "root", xOffset: Math.min(index % 3, columnCount - width), yOffset: index % 2, width, height: 2 + (index % 2), }; }), ), ); } else { positionRows.push(...packPositions(boardItems, board)); } } if (positionRows.length > 0) { await db.insert(itemLayouts).values( positionRows.map((position) => { return { itemId: itemId(position.boardHandle, position.itemHandle), sectionId: sectionId(position.boardHandle, position.sectionHandle), layoutId: layoutId(position.boardHandle, position.layoutHandle), xOffset: position.xOffset, yOffset: position.yOffset, width: position.width, height: position.height, }; }), ); } await assertSeededGeometryAsync(); const mockKinds = new Set(getWidgetKindsForIntegration("mock")); const mockItemIds = [...allItems.entries()].flatMap(([boardHandle, definitions]) => definitions.filter((item) => mockKinds.has(item.kind)).map((item) => itemId(boardHandle, item.handle)), ); if (mockItemIds.length > 0) { await db .insert(integrationItems) .values(mockItemIds.map((currentItemId) => ({ itemId: currentItemId, integrationId: mockIntegrationId }))); } await db.delete(boardUserPermissions).where(inArray(boardUserPermissions.boardId, qaBoardIds)); await db.delete(boardGroupPermissions).where(inArray(boardGroupPermissions.boardId, qaBoardIds)); const permissionRows: Array<{ boardId: string; userId: string; permission: BoardPermission }> = []; for (const board of sharedBoards) { permissionRows.push( { boardId: boardId(board.handle), userId: userId("rowan-owner"), permission: "full" }, { boardId: boardId(board.handle), userId: userId("eden-editor"), permission: "modify" }, { boardId: boardId(board.handle), userId: userId("vivian-viewer"), permission: "view" }, ); for (const persona of personas) { if (["avery-admin", "rowan-owner", "eden-editor", "vivian-viewer", "nolan-outsider"].includes(persona.handle)) { continue; } permissionRows.push({ boardId: boardId(board.handle), userId: userId(persona.handle), permission: "view" }); } } await db.insert(boardUserPermissions).values(permissionRows); for (const persona of personas) { await db .update(users) .set({ homeBoardId: boardId(`home-${persona.handle}`), mobileHomeBoardId: boardId(`home-${persona.handle}`) }) .where(eq(users.id, userId(persona.handle))); } }; const createManifest = ( profile: Profile, candidateSha: string, fixtureUrl: string, seeded: boolean, profileFlags: ReleaseV2QaProfileFlags, ) => { const allItems = seeded ? buildItems(new Map(widgetKinds.map((kind) => [kind, emptySuperJSON]))) : new Map(); const boardEntries = seeded ? qaBoards.map((board) => ({ id: boardId(board.handle), handle: board.handle, name: boardName(board.handle), label: board.label, owner: board.ownerHandle, isPublic: board.isPublic ?? false, layouts: boardLayouts(board).map((layout) => layoutId(board.handle, layout.handle)), itemCount: allItems.get(board.handle)?.length ?? 0, flags: { home: board.handle.startsWith("home-"), shared: !board.handle.startsWith("home-"), collisionFixture: board.handle === "dense-collisions", nestedContainers: board.handle === "nested-containers", scrollableContainer: board.handle === "scroll", leftRightRails: board.handle === "scroll", }, })) : []; const layoutEntries = seeded ? qaBoards.flatMap((board) => boardLayouts(board).map((layout) => ({ id: layoutId(board.handle, layout.handle), boardId: boardId(board.handle), handle: layout.handle, role: layout.role, breakpoint: layout.breakpoint, columnCount: layout.columnCount, leftGutterColumnCount: layout.leftGutterColumnCount ?? 0, rightGutterColumnCount: layout.rightGutterColumnCount ?? 0, })), ) : []; return { schemaVersion: 1, candidateSha, profile, personas: seeded ? personas.map((persona) => ({ id: userId(persona.handle), handle: persona.handle, name: persona.name, loginUsername: persona.handle, email: `qa-v2-${persona.handle}@example.test`, homeBoardId: boardId(`home-${persona.handle}`), permissions: persona.permissions, })) : [], boards: boardEntries, layouts: layoutEntries, expectedBoardPermissions: { "rowan-owner": "full", "eden-editor": "modify", "vivian-viewer": "view", "nolan-outsider": "none", }, counts: { personas: seeded ? personas.length : 0, boards: boardEntries.length, homeBoards: seeded ? homeBoards.length : 0, sharedBoards: seeded ? sharedBoards.length : 0, layouts: layoutEntries.length, items: seeded ? [...allItems.values()].reduce((sum, rows) => sum + rows.length, 0) : 0, widgetKinds: seeded ? widgetKinds.length : 0, apps: seeded ? qaAppIds.length : 0, icons: seeded ? qaIconIds.length : 0, customWidgets: seeded ? customDefinitionIds.length : 0, }, customWidgetVariants: seeded ? { enabled: customDefinitionId, disabled: disabledCustomDefinitionId, missingSecret: missingSecretCustomDefinitionId, invalidOptionItem: itemId("custom-widget-assistant", "custom-invalid-option"), sizes: ["compact", "standard", "wide"], } : null, limitations: ["No plaintext Custom Widget secret is seeded; the missing-secret state is intentional."], flags: { credentialsCreated: seeded, readOnly: profile === "main-readonly", onboardingFresh: profile === "onboarding-fresh", degraded: profile === "degraded", mockIntegration: seeded, allWidgetKindsCovered: seeded, demoMode: profileFlags.demoMode, demoReadOnly: profileFlags.demoReadOnly, unsafeMockIntegration: profileFlags.unsafeMockIntegration, fixtureOrigin: new URL(fixtureUrl).origin, }, }; }; const main = async () => { const options = parseCli(); validateQaBoardDefinitions(); const galleryKinds = widgetGalleryGroups.flat(); if ( galleryKinds.length !== widgetKinds.length || new Set(galleryKinds).size !== widgetKinds.length || widgetKinds.some((kind) => !galleryKinds.includes(kind)) ) { throw new Error("Widget gallery groups must contain every canonical widget kind exactly once"); } const profile = options.profile ?? (process.env.QA_PROFILE as Profile | undefined); if (!profile || !profiles.includes(profile)) throw new Error(`QA_PROFILE must be one of: ${profiles.join(", ")}`); const profileFlags = assertReleaseV2QaProfileEnvironment(profile, process.env); const checkoutSha = resolveCheckoutCandidateSha(repoRoot); const suppliedCandidateSha = options.candidateSha ?? process.env.QA_CANDIDATE_SHA?.trim(); const candidateSha = suppliedCandidateSha ? validateCandidateSha(suppliedCandidateSha, checkoutSha) : checkoutSha; const fixtureUrl = validateFixtureUrl(requiredEnv("QA_FIXTURE_URL")); const manifestPath = resolveManifestPath(options); const populated = profile !== "onboarding-fresh"; await runInTransactionAsync(async () => { if (options.reset) await resetQaRowsAsync(); await setOnboardingStepAsync(profile); if (populated) { const password = await requiredQaPassword(); const passwordHash = await resolvePasswordHashAsync(password); await seedPersonasAsync(passwordHash); await seedGlobalFixturesAsync(fixtureUrl); const optionsByKind = await sourceOptionsByKindAsync(); await seedBoardsAsync(optionsByKind); return; } await resetQaRowsAsync(); }); const manifest = createManifest(profile, candidateSha, fixtureUrl, populated, profileFlags); await mkdir(dirname(manifestPath), { recursive: true }); await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); console.log(`Release v2 QA seed complete for profile '${profile}' (${manifest.counts.boards} QA boards)`); console.log(`Fixture manifest written to ${manifestPath}`); }; const closeDatabaseAsync = async () => { const client = rootDb.$client as unknown as { close?: () => void; end?: () => Promise | void; }; if (typeof client.close === "function") { client.close(); return; } if (typeof client.end === "function") await client.end(); }; try { await main(); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown seed failure"; console.error(`Release v2 QA seed failed: ${message}`); process.exitCode = 1; } finally { await closeDatabaseAsync(); } process.exit(process.exitCode ?? 0);