Files
homarr/scripts/release-v2-qa/seed.mts
T

1301 lines
47 KiB
TypeScript

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<void>) => {
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 <TRow extends { id: string }>(
query: { findFirst: (input: unknown) => Promise<TRow | undefined> },
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: `<Stack p="md" gap="sm" h="100%"><Group justify="space-between"><Title order={3}>{data.fixture?.title ?? "Release v2 QA"}</Title><Badge color={status.fixture?.ok ? "green" : "gray"}>{data.fixture?.status ?? "loading"}</Badge></Group>{status.fixture?.loading ? <Skeleton height={80} /> : status.fixture?.error ? <Alert color="red" title="Fixture failed">{status.fixture.error}<RefreshButton /></Alert> : <Stack gap="xs">{(data.fixture?.items ?? []).map(item => <Paper key={item.id} withBorder p="xs"><Group justify="space-between"><Text>{item.label}</Text><Text fw={700}>{item.value}</Text></Group></Paper>)}</Stack>}</Stack>`,
});
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<WidgetKind, string>();
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<string, { xOffset: number; yOffset: number; rowHeight: number }>();
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<WidgetKind, string>,
): 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<WidgetKind, string>) => {
const result = new Map<string, ItemDefinition[]>();
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) ?? "<unknown>",
})),
});
if (geometryErrors.length > 0) {
throw new Error(`Generated QA fixture geometry is invalid: ${geometryErrors.join("; ")}`);
}
};
const seedBoardsAsync = async (optionsByKind: Map<WidgetKind, string>) => {
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> | 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);