e6b50fb4f1
Fix all build/type/lint errors (zod 3.25 compat wrapper, Svelte 5 fixes), write 115 unit tests across 10 test files, expand seed script with demo data, update Docker config with migration on startup.
353 lines
8.4 KiB
TypeScript
353 lines
8.4 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
async function main() {
|
|
console.log('Seeding database...');
|
|
|
|
// --- System Settings ---
|
|
const settings = await prisma.systemSettings.upsert({
|
|
where: { id: 'singleton' },
|
|
update: {},
|
|
create: {
|
|
id: 'singleton',
|
|
authMode: 'local',
|
|
registrationEnabled: true,
|
|
defaultTheme: 'dark',
|
|
defaultPrimaryColor: '#6366f1',
|
|
healthcheckDefaults: JSON.stringify({
|
|
interval: 300,
|
|
timeout: 5000,
|
|
method: 'GET',
|
|
expectedStatus: 200
|
|
})
|
|
}
|
|
});
|
|
console.log(' Created system settings:', settings.id);
|
|
|
|
// --- Admin User ---
|
|
const adminPassword = await bcrypt.hash('admin123', 12);
|
|
const admin = await prisma.user.upsert({
|
|
where: { email: 'admin@localhost' },
|
|
update: {},
|
|
create: {
|
|
email: 'admin@localhost',
|
|
password: adminPassword,
|
|
displayName: 'Administrator',
|
|
role: 'admin',
|
|
authProvider: 'local'
|
|
}
|
|
});
|
|
console.log(' Created admin user:', admin.email);
|
|
|
|
// --- Regular User ---
|
|
const userPassword = await bcrypt.hash('user123', 12);
|
|
const regularUser = await prisma.user.upsert({
|
|
where: { email: 'user@localhost' },
|
|
update: {},
|
|
create: {
|
|
email: 'user@localhost',
|
|
password: userPassword,
|
|
displayName: 'Demo User',
|
|
role: 'user',
|
|
authProvider: 'local'
|
|
}
|
|
});
|
|
console.log(' Created regular user:', regularUser.email);
|
|
|
|
// --- Groups ---
|
|
const adminGroup = await prisma.group.upsert({
|
|
where: { name: 'admin' },
|
|
update: {},
|
|
create: {
|
|
name: 'admin',
|
|
description: 'Administrators with full system access',
|
|
isDefault: false
|
|
}
|
|
});
|
|
console.log(' Created group:', adminGroup.name);
|
|
|
|
const userGroup = await prisma.group.upsert({
|
|
where: { name: 'user' },
|
|
update: {},
|
|
create: {
|
|
name: 'user',
|
|
description: 'Default group for all registered users',
|
|
isDefault: true
|
|
}
|
|
});
|
|
console.log(' Created group:', userGroup.name);
|
|
|
|
// --- User-Group memberships ---
|
|
await prisma.userGroup.upsert({
|
|
where: { userId_groupId: { userId: admin.id, groupId: adminGroup.id } },
|
|
update: {},
|
|
create: { userId: admin.id, groupId: adminGroup.id }
|
|
});
|
|
await prisma.userGroup.upsert({
|
|
where: { userId_groupId: { userId: admin.id, groupId: userGroup.id } },
|
|
update: {},
|
|
create: { userId: admin.id, groupId: userGroup.id }
|
|
});
|
|
await prisma.userGroup.upsert({
|
|
where: { userId_groupId: { userId: regularUser.id, groupId: userGroup.id } },
|
|
update: {},
|
|
create: { userId: regularUser.id, groupId: userGroup.id }
|
|
});
|
|
console.log(' Added users to groups');
|
|
|
|
// --- Sample Apps ---
|
|
const appDefinitions = [
|
|
{
|
|
name: 'Plex',
|
|
url: 'http://plex.local:32400',
|
|
icon: 'plex',
|
|
iconType: 'simple',
|
|
description: 'Media server for streaming movies, TV shows, and music',
|
|
category: 'Media',
|
|
tags: 'media,streaming,movies,tv',
|
|
healthcheckEnabled: true
|
|
},
|
|
{
|
|
name: 'Nextcloud',
|
|
url: 'http://nextcloud.local',
|
|
icon: 'nextcloud',
|
|
iconType: 'simple',
|
|
description: 'Self-hosted file sync, sharing, and collaboration platform',
|
|
category: 'Productivity',
|
|
tags: 'files,sync,cloud,office',
|
|
healthcheckEnabled: true
|
|
},
|
|
{
|
|
name: 'Gitea',
|
|
url: 'http://gitea.local:3000',
|
|
icon: 'gitea',
|
|
iconType: 'simple',
|
|
description: 'Lightweight self-hosted Git service',
|
|
category: 'Development',
|
|
tags: 'git,code,development,ci',
|
|
healthcheckEnabled: true
|
|
},
|
|
{
|
|
name: 'Home Assistant',
|
|
url: 'http://homeassistant.local:8123',
|
|
icon: 'homeassistant',
|
|
iconType: 'simple',
|
|
description: 'Open-source home automation platform',
|
|
category: 'Home Automation',
|
|
tags: 'home,automation,iot,smart-home',
|
|
healthcheckEnabled: true
|
|
},
|
|
{
|
|
name: 'Grafana',
|
|
url: 'http://grafana.local:3000',
|
|
icon: 'grafana',
|
|
iconType: 'simple',
|
|
description: 'Analytics and monitoring dashboards',
|
|
category: 'Monitoring',
|
|
tags: 'monitoring,analytics,dashboards,metrics',
|
|
healthcheckEnabled: true
|
|
},
|
|
{
|
|
name: 'Portainer',
|
|
url: 'http://portainer.local:9000',
|
|
icon: 'portainer',
|
|
iconType: 'simple',
|
|
description: 'Container management UI for Docker and Kubernetes',
|
|
category: 'Infrastructure',
|
|
tags: 'docker,containers,kubernetes,management',
|
|
healthcheckEnabled: true
|
|
},
|
|
{
|
|
name: 'Pi-hole',
|
|
url: 'http://pihole.local/admin',
|
|
icon: 'pihole',
|
|
iconType: 'simple',
|
|
description: 'Network-wide ad blocking DNS sinkhole',
|
|
category: 'Network',
|
|
tags: 'dns,adblock,network,privacy',
|
|
healthcheckEnabled: true
|
|
}
|
|
];
|
|
|
|
// Create apps using create (delete existing first for idempotency)
|
|
const createdApps = [];
|
|
for (const appData of appDefinitions) {
|
|
// Delete existing app with same name if present (for re-seeding)
|
|
await prisma.app.deleteMany({ where: { name: appData.name } });
|
|
const app = await prisma.app.create({
|
|
data: {
|
|
...appData,
|
|
createdById: admin.id
|
|
}
|
|
});
|
|
createdApps.push(app);
|
|
console.log(' Created app:', app.name);
|
|
}
|
|
|
|
// --- Default Board ---
|
|
const board = await prisma.board.upsert({
|
|
where: { id: 'default-board' },
|
|
update: {},
|
|
create: {
|
|
id: 'default-board',
|
|
name: 'Dashboard',
|
|
icon: 'layout-dashboard',
|
|
description: 'Default application dashboard',
|
|
isDefault: true,
|
|
isGuestAccessible: true,
|
|
createdById: admin.id
|
|
}
|
|
});
|
|
console.log(' Created board:', board.name);
|
|
|
|
// --- Sections ---
|
|
const mediaSection = await prisma.section.upsert({
|
|
where: { id: 'section-media' },
|
|
update: {},
|
|
create: {
|
|
id: 'section-media',
|
|
boardId: board.id,
|
|
title: 'Media & Entertainment',
|
|
icon: 'tv',
|
|
order: 0,
|
|
isExpandedByDefault: true
|
|
}
|
|
});
|
|
console.log(' Created section:', mediaSection.title);
|
|
|
|
const infraSection = await prisma.section.upsert({
|
|
where: { id: 'section-infra' },
|
|
update: {},
|
|
create: {
|
|
id: 'section-infra',
|
|
boardId: board.id,
|
|
title: 'Infrastructure & Tools',
|
|
icon: 'server',
|
|
order: 1,
|
|
isExpandedByDefault: true
|
|
}
|
|
});
|
|
console.log(' Created section:', infraSection.title);
|
|
|
|
const networkSection = await prisma.section.upsert({
|
|
where: { id: 'section-network' },
|
|
update: {},
|
|
create: {
|
|
id: 'section-network',
|
|
boardId: board.id,
|
|
title: 'Network & Security',
|
|
icon: 'shield',
|
|
order: 2,
|
|
isExpandedByDefault: true
|
|
}
|
|
});
|
|
console.log(' Created section:', networkSection.title);
|
|
|
|
// --- Widgets ---
|
|
// Delete existing seed widgets for idempotency
|
|
const seedWidgetIds = [
|
|
'widget-plex',
|
|
'widget-nextcloud',
|
|
'widget-gitea',
|
|
'widget-homeassistant',
|
|
'widget-grafana',
|
|
'widget-portainer',
|
|
'widget-pihole'
|
|
];
|
|
await prisma.widget.deleteMany({ where: { id: { in: seedWidgetIds } } });
|
|
|
|
// Media section widgets
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-plex',
|
|
sectionId: mediaSection.id,
|
|
type: 'app',
|
|
order: 0,
|
|
appId: createdApps[0].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
// Infrastructure section widgets
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-nextcloud',
|
|
sectionId: infraSection.id,
|
|
type: 'app',
|
|
order: 0,
|
|
appId: createdApps[1].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-gitea',
|
|
sectionId: infraSection.id,
|
|
type: 'app',
|
|
order: 1,
|
|
appId: createdApps[2].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-homeassistant',
|
|
sectionId: infraSection.id,
|
|
type: 'app',
|
|
order: 2,
|
|
appId: createdApps[3].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-grafana',
|
|
sectionId: infraSection.id,
|
|
type: 'app',
|
|
order: 3,
|
|
appId: createdApps[4].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-portainer',
|
|
sectionId: infraSection.id,
|
|
type: 'app',
|
|
order: 4,
|
|
appId: createdApps[5].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
// Network section widgets
|
|
await prisma.widget.create({
|
|
data: {
|
|
id: 'widget-pihole',
|
|
sectionId: networkSection.id,
|
|
type: 'app',
|
|
order: 0,
|
|
appId: createdApps[6].id,
|
|
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
|
}
|
|
});
|
|
|
|
console.log(' Created widgets for all apps');
|
|
console.log('Seeding complete!');
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error('Seed error:', e);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|