import SuperJSON from "superjson"; import { createId, generateResponsiveGridFor, objectKeys } from "@homarr/common"; import { BUNDLED_CUSTOM_WIDGETS, customWidgetDefinitionSchema } from "@homarr/custom-widgets/core"; import { createDocumentationLink, credentialsAdminGroup, defaultBookmarkApps, everyoneGroup, getIntegrationDefaultUrl, getIntegrationName, integrationDefs, integrationKinds, normalizeBoardLayoutRoles, } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions"; import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings"; import type { Database, InferInsertModel } from ".."; import { eq, inArray } from ".."; import { getMaxGroupPositionAsync, placeAllWidgetsAsync } from "../queries"; import { getServerSettingByKeyAsync, insertServerSettingByKeyAsync, updateServerSettingByKeyAsync, } from "../queries/server-setting"; import { apps, boards, customWidgetDefinitions, groupMembers, groupPermissions, groups, integrationItems, integrations, itemLayouts, items, layouts, onboarding, searchEngines, sections, sectionLayouts, users, } from "../schema"; import type { Integration } from "../schema"; const isTruthyEnv = (value: string | undefined) => ["1", "yes", "t", "true"].includes((value ?? "").toLowerCase()); export const seedDataAsync = async (db: Database) => { if (isTruthyEnv(process.env.UNSAFE_ENABLE_MOCK_INTEGRATION)) { console.warn( "UNSAFE_ENABLE_MOCK_INTEGRATION is enabled: mock integration is available in the UI. Disable by setting UNSAFE_ENABLE_MOCK_INTEGRATION=false.", ); } await seedEveryoneGroupAsync(db); await seedOnboardingAsync(db); await seedServerSettingsAsync(db); await seedDefaultSearchEnginesAsync(db); await seedDefaultIntegrationsAsync(db); await seedDefaultAppsAsync(db); await seedDefaultBoardAsync(db); await seedDefaultCustomWidgetsAsync(db); await seedBoardWidgetsAsync(db); if (isTruthyEnv(process.env.DEMO_MODE)) { await seedDemoUserAsync(db); } await seedProtectedBoardLayoutsAsync(db); }; export const seedProtectedBoardLayoutsAsync = async (db: Database, boardId?: string) => { const dbBoards = await db.query.boards.findMany({ where: boardId ? eq(boards.id, boardId) : undefined, with: { layouts: true, items: { with: { layouts: true } }, sections: { with: { layouts: true } }, }, }); for (const board of dbBoards) { if (board.layouts.length === 0) { await db.insert(layouts).values([ { id: createId(), name: "Mobile", columnCount: 3, breakpoint: 0, role: "mobile", boardId: board.id, }, { id: createId(), name: "Base", columnCount: 10, breakpoint: 768, role: "base", boardId: board.id, }, ]); continue; } if (board.layouts.length === 1) { const [baseLayout] = board.layouts; if (!baseLayout) continue; const mobileLayout = { id: createId(), name: "Mobile", columnCount: 3, breakpoint: 0, role: "mobile" as const, boardId: board.id, }; await db.update(layouts).set({ role: "base", breakpoint: 768 }).where(eq(layouts.id, baseLayout.id)); await db.insert(layouts).values(mobileLayout); await insertMissingProjectedPositionsAsync( db, board, { ...baseLayout, role: "base", breakpoint: 768 }, mobileLayout, ); continue; } const normalizedLayouts = normalizeBoardLayoutRoles(board.layouts); const alreadyNormalized = normalizedLayouts.every((layout) => { const previousLayout = board.layouts.find((candidate) => candidate.id === layout.id); return layout.breakpoint === previousLayout?.breakpoint && previousLayout.role === layout.role; }); if (!alreadyNormalized) { for (const layout of normalizedLayouts) { await db .update(layouts) .set({ breakpoint: layout.breakpoint, role: layout.role }) .where(eq(layouts.id, layout.id)); } } const mobileLayout = normalizedLayouts.find((layout) => layout.role === "mobile"); const baseLayout = normalizedLayouts.find((layout) => layout.role === "base"); if (mobileLayout && baseLayout) { await insertMissingProjectedPositionsAsync( db, board, { ...baseLayout, role: "base" }, { ...mobileLayout, role: "mobile" }, ); } } }; interface BoardWithLayoutPositions { items: Array<{ id: string; layouts: Array<{ layoutId: string; sectionId: string; width: number; height: number; xOffset: number; yOffset: number; }>; }>; sections: Array<{ id: string; kind: string; layouts: Array<{ layoutId: string; parentSectionId: string | null; width: number; height: number; xOffset: number; yOffset: number; }>; }>; } const insertMissingProjectedPositionsAsync = async ( db: Database, board: BoardWithLayoutPositions, sourceLayout: InferInsertModel, targetLayout: InferInsertModel, ) => { const elements = [ ...board.items.flatMap((item) => { const layout = item.layouts.find((itemLayout) => itemLayout.layoutId === sourceLayout.id); return layout ? [ { id: item.id, type: "item" as const, width: layout.width, height: layout.height, xOffset: layout.xOffset, yOffset: layout.yOffset, sectionId: layout.sectionId, }, ] : []; }), ...board.sections.flatMap((section) => { if (section.kind !== "dynamic") return []; const layout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === sourceLayout.id); return layout?.parentSectionId ? [ { id: section.id, type: "section" as const, width: layout.width, height: layout.height, xOffset: layout.xOffset, yOffset: layout.yOffset, sectionId: layout.parentSectionId, }, ] : []; }), ]; const projectedElements = board.sections .filter((section) => section.kind !== "dynamic") .flatMap( (section) => generateResponsiveGridFor({ items: elements, previousWidth: sourceLayout.columnCount, width: targetLayout.columnCount, sectionId: section.id, }).items, ); const existingItemIds = new Set( board.items .filter((item) => item.layouts.some((layout) => layout.layoutId === targetLayout.id)) .map((item) => item.id), ); const itemPositions = projectedElements .filter((element) => element.type === "item" && !existingItemIds.has(element.id)) .map( (element): InferInsertModel => ({ itemId: element.id, layoutId: targetLayout.id, sectionId: element.sectionId, width: element.width, height: element.height, xOffset: element.xOffset, yOffset: element.yOffset, }), ); const existingSectionIds = new Set( board.sections .filter((section) => section.layouts.some((layout) => layout.layoutId === targetLayout.id)) .map((section) => section.id), ); const sectionPositions = projectedElements .filter((element) => element.type === "section" && !existingSectionIds.has(element.id)) .map( (element): InferInsertModel => ({ sectionId: element.id, layoutId: targetLayout.id, parentSectionId: element.sectionId, width: element.width, height: element.height, xOffset: element.xOffset, yOffset: element.yOffset, }), ); if (itemPositions.length > 0) await db.insert(itemLayouts).values(itemPositions); if (sectionPositions.length > 0) await db.insert(sectionLayouts).values(sectionPositions); }; const seedEveryoneGroupAsync = async (db: Database) => { const group = await db.query.groups.findFirst({ where: eq(groups.name, everyoneGroup), }); if (group) { console.log("Skipping seeding of group 'everyone' as it already exists"); return; } await db.insert(groups).values({ id: createId(), name: everyoneGroup, position: -1, }); console.log("Created group 'everyone' through seed"); }; const seedOnboardingAsync = async (db: Database) => { const existing = await db.query.onboarding.findFirst(); if (existing) { console.log("Skipping seeding of onboarding as it already exists"); return; } await db.insert(onboarding).values({ id: createId(), step: "start", }); console.log("Created onboarding step through seed"); }; const seedDefaultSearchEnginesAsync = async (db: Database) => { const existingSearchEngines = await db.$count(searchEngines); if (existingSearchEngines > 0) { console.log("Skipping seeding of default search engines as some already exists"); return; } const homarrId = createId(); const defaultSearchEngines = [ { id: createId(), name: "Google", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google.svg", short: "g", description: "Search the web with Google", urlTemplate: "https://www.google.com/search?q=%s", type: "generic" as const, integrationId: null, }, { id: createId(), name: "YouTube", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/youtube.svg", short: "yt", description: "Search for videos on YouTube", urlTemplate: "https://www.youtube.com/results?search_query=%s", type: "generic" as const, integrationId: null, }, { id: homarrId, name: "Homarr Docs", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", short: "docs", description: "Search the Homarr documentation", urlTemplate: createDocumentationLink("/search", undefined, { q: "%s" }), type: "generic" as const, integrationId: null, }, ]; await db.insert(searchEngines).values(defaultSearchEngines); console.log(`Created ${defaultSearchEngines.length} default search engines through seeding process`); // Set Homarr docs as the default search engine in server settings const searchSettings = await getServerSettingByKeyAsync(db, "search"); await updateServerSettingByKeyAsync(db, "search", { ...searchSettings, defaultSearchEngineId: homarrId, }); console.log("Set Homarr docs as the default search engine"); }; const seedServerSettingsAsync = async (db: Database) => { const serverSettingsData = await db.query.serverSettings.findMany(); for (const settingsKey of defaultServerSettingsKeys) { const currentDbEntry = serverSettingsData.find((setting) => setting.settingKey === settingsKey); if (!currentDbEntry) { await insertServerSettingByKeyAsync(db, settingsKey, defaultServerSettings[settingsKey]); console.log(`Created serverSetting through seed key=${settingsKey}`); continue; } const currentSettings = await getServerSettingByKeyAsync(db, settingsKey); const defaultSettings = defaultServerSettings[settingsKey]; const missingKeys = objectKeys(defaultSettings).filter((key) => !(key in currentSettings)); if (missingKeys.length === 0) { console.info(`Skipping seeding for serverSetting as it already exists key=${settingsKey}`); continue; } await updateServerSettingByKeyAsync(db, settingsKey, { ...defaultSettings, ...currentSettings }); console.log(`Updated serverSetting through seed key=${settingsKey}`); } }; const seedDefaultIntegrationsAsync = async (db: Database) => { const defaultIntegrations = integrationKinds.reduce((acc, kind) => { const name = getIntegrationName(kind); const defaultUrl = getIntegrationDefaultUrl(kind); const hasNoAuthOption = integrationDefs[kind].secretKinds.some((kinds) => kinds.length === 0); if (defaultUrl !== undefined && hasNoAuthOption) { acc.push({ id: "new", name: `${name} Default`, url: defaultUrl, kind, appId: null, }); } return acc; }, []); if (defaultIntegrations.length === 0) { console.warn("No default integrations found to seed"); return; } let createdCount = 0; await Promise.all( defaultIntegrations.map(async (integration) => { const existingKind = await db.$count(integrations, eq(integrations.kind, integration.kind)); if (existingKind > 0) { console.log(`Skipping seeding of default ${integration.kind} integration as one already exists`); return; } const newIntegration = { ...integration, id: createId(), }; await db.insert(integrations).values(newIntegration); createdCount++; }), ); if (createdCount === 0) { console.log("No default integrations were created as they already exist"); return; } console.log(`Created ${createdCount} default integrations through seeding process`); }; const seedDefaultAppsAsync = async (db: Database) => { const existingApps = await db.$count(apps); if (existingApps > 0) { console.log("Skipping seeding of default apps as some already exist"); return; } for (const app of defaultBookmarkApps) { await db.insert(apps).values({ id: createId(), name: app.name, iconUrl: app.iconUrl, href: app.href, }); } console.log(`Created ${defaultBookmarkApps.length} default apps through seeding process`); }; const seedDefaultBoardAsync = async (db: Database) => { const existingBoard = await db.query.boards.findFirst(); if (existingBoard) { console.log("Skipping seeding of default board as one already exists"); return; } const boardId = createId(); await db.insert(boards).values({ id: boardId, name: "dashboard", isPublic: false, }); await db.insert(sections).values({ id: createId(), kind: "empty", xOffset: 0, yOffset: 0, boardId, }); await db.insert(layouts).values([ { id: createId(), name: "Mobile", columnCount: 3, breakpoint: 0, role: "mobile", boardId, }, { id: createId(), name: "Base", columnCount: 10, breakpoint: 768, role: "base", boardId, }, ]); const everyoneGroupRow = await db.query.groups.findFirst({ where: eq(groups.name, everyoneGroup), }); if (everyoneGroupRow) { await db.update(groups).set({ homeBoardId: boardId }).where(eq(groups.id, everyoneGroupRow.id)); console.log("Set default board as home board for everyone group"); } console.log("Created default board 'dashboard' through seed"); }; interface DemoWidget { kind: WidgetKind; section?: "right"; xOffset: number; yOffset: number; width: number; height: number; needsIntegration: boolean; options?: Record; } const demoApps = [ { name: "Sonarr", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg", href: "https://sonarr.tv", }, { name: "Radarr", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg", href: "https://radarr.video", }, { name: "Plex", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg", href: "https://plex.tv", }, { name: "Jellyfin", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg", href: "https://jellyfin.org", }, { name: "Proxmox", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/proxmox.svg", href: "https://proxmox.com", }, { name: "Pi-hole", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pi-hole.svg", href: "https://pi-hole.net", }, { name: "Grafana", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg", href: "https://grafana.com", }, { name: "Portainer", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/portainer.svg", href: "https://portainer.io", }, { name: "Uptime Kuma", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/uptime-kuma.svg", href: "https://uptime.kuma.pet", }, { name: "Nextcloud", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg", href: "https://nextcloud.com", }, { name: "qBittorrent", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg", href: "https://qbittorrent.org", }, { name: "Overseerr", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg", href: "https://overseerr.dev", }, ] as const; const buildDemoWidgets = (appIds: string[], customWidgetDefinitionId: string): DemoWidget[] => [ // Daily focus { kind: "calendar", xOffset: 0, yOffset: 0, width: 2, height: 2, needsIntegration: true }, { kind: "weather", xOffset: 2, yOffset: 0, width: 2, height: 2, needsIntegration: false, options: { location: { name: "Paris", latitude: 48.85341, longitude: 2.3488 }, hasForecast: false, showHumidity: false, showCurrentWindSpeed: false, showCity: true, animateIcons: false, }, }, { kind: "clock", xOffset: 4, yOffset: 0, width: 1, height: 2, needsIntegration: false, options: { customTitleToggle: false, showDate: false, customTimeFormat: "HH:mm" }, }, { kind: "timer", xOffset: 5, yOffset: 0, width: 2, height: 1, needsIntegration: false, options: { mode: "pomodoro", focusMinutes: 25, shortBreakMinutes: 5, longBreakMinutes: 15, sessionsBeforeLongBreak: 4, autoStartBreaks: false, autoStartFocus: false, }, }, { kind: "airQuality", xOffset: 5, yOffset: 1, width: 2, height: 1, needsIntegration: false, options: { location: { name: "Paris", latitude: 48.85341, longitude: 2.3488 }, aqiStandard: "european", showUv: false, showPollutants: false, showPollen: false, }, }, { kind: "downloads", xOffset: 7, yOffset: 0, width: 5, height: 2, needsIntegration: true }, // Homarr workspace { kind: "notebook", xOffset: 0, yOffset: 2, width: 5, height: 3, needsIntegration: false, options: { showToolbar: false, allowReadOnlyCheck: true, content: `

Quick runbook

Keep today close: signals first, controls beside them, distractions out.

  • Backups verified

  • Review active alerts

  • Connect your everyday services

  • Tune this board to your routine

Make it yours. Keep it useful.

`, }, }, { kind: "beszelSystemGrid", xOffset: 5, yOffset: 2, width: 4, height: 3, needsIntegration: true }, { kind: "assistant", xOffset: 9, yOffset: 2, width: 3, height: 3, needsIntegration: false }, // Operations center { kind: "mediaRequests-requestList", xOffset: 0, yOffset: 5, width: 3, height: 2, needsIntegration: true }, { kind: "mediaMissing", xOffset: 9, yOffset: 5, width: 3, height: 2, needsIntegration: true }, { kind: "mediaServer", xOffset: 0, yOffset: 7, width: 3, height: 2, needsIntegration: true }, { kind: "mediaRequests-requestStats", xOffset: 9, yOffset: 7, width: 3, height: 2, needsIntegration: true }, { kind: "rssFeed", xOffset: 0, yOffset: 9, width: 3, height: 2, needsIntegration: false, options: { feedUrls: ["https://selfh.st/rss/", "https://hnrss.org/newest?q=self-hosted"], maximumAmountPosts: 12, textLinesClamp: 2, hideDescription: true, }, }, { kind: "indexerManager", xOffset: 9, yOffset: 9, width: 3, height: 2, needsIntegration: true }, // Infrastructure and activity { kind: "dockerContainers", xOffset: 0, yOffset: 11, width: 4, height: 3, needsIntegration: false }, { kind: "mediaReleases", xOffset: 4, yOffset: 11, width: 4, height: 3, needsIntegration: true }, // Community Workshop { kind: "customApi", xOffset: 8, yOffset: 11, width: 4, height: 3, needsIntegration: false, options: { definitionId: customWidgetDefinitionId, refreshInterval: 300 }, }, // Network and health { kind: "healthMonitoring", xOffset: 3, yOffset: 5, width: 6, height: 2, needsIntegration: true, }, { kind: "dnsHoleSummary", xOffset: 3, yOffset: 7, width: 3, height: 2, needsIntegration: true, options: { layout: "grid", usePiHoleColors: false }, }, { kind: "beszelSystemStats", xOffset: 6, yOffset: 7, width: 3, height: 2, needsIntegration: true, }, { kind: "notifications", xOffset: 3, yOffset: 9, width: 2, height: 2, needsIntegration: true }, { kind: "beszelAlerts", xOffset: 5, yOffset: 9, width: 4, height: 2, needsIntegration: true }, // Extended integrations { kind: "mediaTranscoding", xOffset: 0, yOffset: 14, width: 4, height: 3, needsIntegration: true }, { kind: "beszelSystemTable", xOffset: 4, yOffset: 14, width: 4, height: 3, needsIntegration: true }, { kind: "networkControllerSummary", xOffset: 8, yOffset: 14, width: 4, height: 3, needsIntegration: true }, { kind: "networkControllerStatus", xOffset: 0, yOffset: 17, width: 3, height: 2, needsIntegration: true, options: { content: "wifi" }, }, { kind: "ups", xOffset: 3, yOffset: 17, width: 3, height: 2, needsIntegration: true }, { kind: "smartHome-entityState", xOffset: 6, yOffset: 17, width: 3, height: 2, needsIntegration: true, options: { entityId: "sensor.homarr_demo", displayName: "Demo environment", entityUnit: "online" }, }, { kind: "smartHome-executeAutomation", xOffset: 9, yOffset: 17, width: 3, height: 2, needsIntegration: true, options: { automationId: "automation.homarr_demo", displayName: "Run demo routine" }, }, // Everyday tools { kind: "dnsHoleControls", xOffset: 0, yOffset: 19, width: 2, height: 2, needsIntegration: true }, { kind: "bookmarks", xOffset: 2, yOffset: 19, width: 4, height: 2, needsIntegration: false, options: { title: "Launchpad", layout: "grid", items: appIds.slice(0, 6), openNewTab: true }, }, { kind: "countdown", xOffset: 6, yOffset: 19, width: 2, height: 2, needsIntegration: false, options: { events: [ { id: "demo-new-year", label: "New year 2030", targetUtc: "2030-01-01T00:00:00.000Z", startUtc: "2026-01-01T00:00:00.000Z", timeZone: "Europe/Paris", recurrence: "none", }, ], showProgress: true, showSeconds: false, }, }, { kind: "stockPrice", xOffset: 8, yOffset: 19, width: 2, height: 2, needsIntegration: false }, { kind: "releases", xOffset: 10, yOffset: 19, width: 2, height: 2, needsIntegration: false }, { kind: "iframe", xOffset: 0, yOffset: 21, width: 4, height: 3, needsIntegration: false, options: { embedUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", allowScrolling: false, }, }, { kind: "video", xOffset: 4, yOffset: 21, width: 4, height: 3, needsIntegration: false, options: { feedUrl: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", hasAutoPlay: false, isMuted: true, hasControls: true, }, }, { kind: "minecraftServerStatus", xOffset: 8, yOffset: 21, width: 2, height: 3, needsIntegration: false }, { kind: "timetable", xOffset: 10, yOffset: 21, width: 2, height: 3, needsIntegration: false, options: { baseUrl: "https://search.ch", station: { value: "8507000", label: "Bern" } }, }, // Complete mock integration gallery { kind: "immich-albumCarousel", xOffset: 0, yOffset: 24, width: 3, height: 3, needsIntegration: true, options: { albumId: "demo-paris", rotationIntervalSeconds: 8, showPhotoInfo: true, randomizePhotos: false }, }, { kind: "coolify", xOffset: 3, yOffset: 24, width: 3, height: 3, needsIntegration: true }, { kind: "systemResources", xOffset: 6, yOffset: 24, width: 3, height: 3, needsIntegration: true }, { kind: "systemDisks", xOffset: 9, yOffset: 24, width: 3, height: 3, needsIntegration: true }, { kind: "immich-serverStats", xOffset: 0, yOffset: 27, width: 2, height: 3, needsIntegration: true }, { kind: "paperlessNgx", xOffset: 2, yOffset: 27, width: 2, height: 3, needsIntegration: true }, { kind: "patchmon", xOffset: 4, yOffset: 27, width: 2, height: 3, needsIntegration: true }, { kind: "speedtestTracker", xOffset: 6, yOffset: 27, width: 4, height: 3, needsIntegration: true, options: { showLatestResult: true, showStats: true, showRecentResults: false, showPingGraph: false }, }, { kind: "audioStats", xOffset: 10, yOffset: 27, width: 2, height: 3, needsIntegration: true }, { kind: "uptimeKuma", xOffset: 0, yOffset: 30, width: 3, height: 2, needsIntegration: true }, { kind: "firewall", xOffset: 3, yOffset: 30, width: 3, height: 2, needsIntegration: true }, { kind: "vpn", xOffset: 6, yOffset: 30, width: 3, height: 2, needsIntegration: true }, { kind: "bazarr", xOffset: 9, yOffset: 30, width: 3, height: 2, needsIntegration: true }, { kind: "archiveTeamWarrior", xOffset: 0, yOffset: 32, width: 3, height: 2, needsIntegration: true }, { kind: "wud", xOffset: 3, yOffset: 32, width: 3, height: 2, needsIntegration: true }, { kind: "anchorNote", xOffset: 6, yOffset: 32, width: 2, height: 2, needsIntegration: true, options: { noteId: "homarr-demo-note", showTitle: true, showUpdatedAt: true }, }, { kind: "tracearr", xOffset: 8, yOffset: 32, width: 4, height: 2, needsIntegration: true }, { kind: "traefik", xOffset: 0, yOffset: 34, width: 6, height: 2, needsIntegration: true }, { kind: "umami", xOffset: 6, yOffset: 34, width: 6, height: 2, needsIntegration: true, options: { websiteId: "homarr-demo", timeFrame: "24h", viewMode: "chart", chartType: "bar" }, }, // Right app rail ...appIds.map( (appId, index): DemoWidget => ({ kind: "app", section: "right", xOffset: 0, yOffset: index, width: 1, height: 1, needsIntegration: false, options: { appId, openInNewTab: true, showTitle: true, pingEnabled: false }, }), ), ]; const seedDemoUserAsync = async (db: Database) => { const existingUser = await db.query.users.findFirst({ where: eq(users.name, "demo"), }); if (existingUser) { console.log("Skipping seeding of demo user as it already exists"); return; } const userId = createId(); await db.insert(users).values({ id: userId, name: "demo", email: "demo@example.com", password: "$2b$10$odRXt5e95kSQV5Axmk/FeO6GVOxuRQQ8NnRcBA78Wg4V3kZxPY68u", }); const maxPosition = await getMaxGroupPositionAsync(db); const groupId = createId(); await db.insert(groups).values({ id: groupId, name: credentialsAdminGroup, ownerId: userId, position: maxPosition + 1, }); await db.insert(groupPermissions).values({ groupId, permission: "admin", }); await db.insert(groupMembers).values({ groupId, userId, }); await db.update(onboarding).set({ step: "finish", previousStep: "setup", }); const integrationId = createId(); await db.insert(integrations).values({ id: integrationId, name: "Demo Integration", url: "https://demo.homarr.dev", kind: "mock", appId: null, }); const appIds: string[] = []; for (const app of demoApps) { const appId = createId(); appIds.push(appId); await db.insert(apps).values({ id: appId, name: app.name, iconUrl: app.iconUrl, href: app.href, }); } const customWidgetDefinitionId = createId(); const customWidgetDefinition = customWidgetDefinitionSchema.parse({ $schema: "homarr-custom-widget-v2", name: "Community Workshop", description: "Live Custom Widget and Custom CSS submissions shared by the Homarr community.", sources: { default: { name: "Homarr Workshop", baseUrl: "https://v2.preview.homarr.dev", networkScope: "public", auth: "none", }, }, requests: { workshop: { path: "/api/collections/workshop_listings/records", query: { perPage: 50 }, cacheSeconds: 300, }, }, options: {}, template: ` COMMUNITY WORKSHOPMade by Homarr users {data.workshop?.totalItems ?? "—"} shared Build Custom Widgets or dashboard CSS, then share it with the community. {status.workshop?.loading ? : status.workshop?.error ? {status.workshop.error} : (data.workshop?.items ?? []).length === 0 ? The first community submission could be yours. : {(data.workshop?.items ?? []).slice(0, 4).map(submission => {submission.title}@{submission.authorName ?? "community"}{submission.upvotes ?? 0} ↑)}{data.workshop?.totalItems > 4 ? and {data.workshop.totalItems - 4} more in the Workshop : null}} Install Homarr now → `, }); await db.insert(customWidgetDefinitions).values({ id: customWidgetDefinitionId, name: customWidgetDefinition.name, description: customWidgetDefinition.description ?? null, iconUrl: null, sources: SuperJSON.stringify(customWidgetDefinition.sources), requests: SuperJSON.stringify(customWidgetDefinition.requests), options: SuperJSON.stringify(customWidgetDefinition.options), template: customWidgetDefinition.template, enabled: true, creatorId: userId, }); const boardId = createId(); await db.insert(boards).values({ id: boardId, name: "default", isPublic: false, creatorId: userId, pageTitle: "Homarr demo", backgroundImageUrl: "/images/demo-dashboard-background.svg", primaryColor: "#748FFC", secondaryColor: "#3BC9DB", opacity: 90, itemRadius: "xl", }); const mainSectionId = createId(); await db.insert(sections).values({ id: mainSectionId, kind: "empty", xOffset: 0, yOffset: 0, boardId, }); const rightSectionId = createId(); await db.insert(sections).values({ id: rightSectionId, kind: "empty", xOffset: 1, yOffset: 0, boardId, }); const layoutId = createId(); await db.insert(layouts).values({ id: layoutId, name: "Base", columnCount: 13, rightGutterColumnCount: 1, breakpoint: 768, role: "base", boardId, }); const demoWidgets = buildDemoWidgets(appIds, customWidgetDefinitionId); for (const widget of demoWidgets) { let sectionId = mainSectionId; if (widget.section === "right") { sectionId = rightSectionId; } const itemId = createId(); await db.insert(items).values({ id: itemId, boardId, kind: widget.kind, ...(widget.options ? { options: SuperJSON.stringify(widget.options) } : {}), }); await db.insert(itemLayouts).values({ itemId, sectionId, layoutId, xOffset: widget.xOffset, yOffset: widget.yOffset, width: widget.width, height: widget.height, }); if (widget.needsIntegration) { await db.insert(integrationItems).values({ itemId, integrationId, }); } } await db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, userId)); console.log( "Demo mode enabled: created demo user, mock integration, and sample board with widgets. Disable by setting DEMO_MODE=false.", ); }; const seedDefaultCustomWidgetsAsync = async (db: Database) => { const seedIds = BUNDLED_CUSTOM_WIDGETS.map(({ id }) => id); const existing = await db.query.customWidgetDefinitions.findMany({ columns: { id: true }, where: inArray(customWidgetDefinitions.id, seedIds), }); const existingIds = new Set(existing.map(({ id }) => id)); const values = BUNDLED_CUSTOM_WIDGETS.filter(({ id }) => !existingIds.has(id)).map(({ id, widget }) => { const definition = customWidgetDefinitionSchema.parse(widget); return { id, name: definition.name, description: definition.description ?? null, iconUrl: definition.iconUrl ?? null, sources: SuperJSON.stringify(definition.sources), requests: SuperJSON.stringify(definition.requests), options: SuperJSON.stringify(definition.options), template: definition.template, enabled: false, creatorId: null, }; }); if (values.length === 0) { console.log("Skipping seeding of bundled custom widgets because they already exist"); return; } await db.insert(customWidgetDefinitions).values(values); console.log(`Created ${values.length} bundled custom widgets through seeding process`); }; const seedBoardWidgetsAsync = async (db: Database) => { const existingItems = await db.$count(items); if (existingItems > 0) { console.log("Skipping seeding of board widgets as some already exist"); return; } const board = await db.query.boards.findFirst({ with: { sections: true, layouts: true }, }); if (!board) return; const section = board.sections.find((sec) => sec.kind === "empty"); const layout = board.layouts.find((candidate) => candidate.role === "base") ?? board.layouts[0]; if (!section || !layout) return; const allIntegrations = await db.query.integrations.findMany(); const allApps = await db.query.apps.findMany(); const count = await placeAllWidgetsAsync( db, { boardId: board.id, sectionId: section.id, layoutId: layout.id, columnCount: layout.columnCount }, allIntegrations, allApps, ); console.log(`Placed ${count} widgets on board`); };