Files
web-app-launcher/prisma/seed.ts
T
alexei.dolgolyov 1c0a7cb850 feat: Phases 4-7 — Full Feature Expansion (26 features)
Phase 4 — New Widget Types:
- Clock/Weather, System Stats, RSS/Feed, Calendar, Markdown,
  Metric/Counter, Link Group, Camera/Stream widgets
- Backend services with caching for each data source
- Full creation form with dynamic config fields per type

Phase 5 — Visual & Styling Enhancements:
- Glassmorphism card style (solid/glass/outline)
- Board-level themes with per-board hue/saturation
- Animated SVG status rings replacing static dots
- Card size options (compact/medium/large)
- Custom CSS injection (admin + per-board, sanitized)
- Wallpaper backgrounds with blur/overlay/parallax

Phase 6 — Functional Features:
- Favorites bar with drag-and-drop reordering
- Recent apps tracking with privacy toggle
- Uptime dashboard page (/status, guest-accessible)
- Notifications system (Discord/Slack/Telegram/HTTP webhooks)
- App tags with filtering in board view
- Multi-URL app cards with expandable sub-links
- Personal API tokens with scoped permissions
- Audit log with retention and admin viewer

Phase 7 — Quality of Life:
- Onboarding wizard (5-step first-launch setup)
- App URL health preview with favicon/title detection
- Board templates (4 built-in + custom import/export)
- Keyboard shortcut overlay (j/k nav, 1-9 boards, ? help)

212 files changed, 15641 insertions, 980 deletions.
Build, lint, type check, and 222 tests all pass.
2026-03-25 14:18:10 +03:00

494 lines
12 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@launcher.local' },
update: {},
create: {
email: 'admin@launcher.local',
password: adminPassword,
displayName: 'Administrator',
role: 'admin',
authProvider: 'local',
themeMode: 'dark',
primaryHue: 240,
primarySaturation: 80,
backgroundType: 'aurora',
locale: 'en'
}
});
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@launcher.local' },
update: {},
create: {
email: 'user@launcher.local',
password: userPassword,
displayName: 'Demo User',
role: 'user',
authProvider: 'local',
themeMode: 'light',
primaryHue: 150,
primarySaturation: 60,
backgroundType: 'mesh',
locale: 'ru'
}
});
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
},
{
name: 'Wiki.js',
url: 'http://wiki.local:3000',
icon: 'http://wiki.local:3000/favicon.ico',
iconType: 'url',
description: 'Quick-added wiki service (demonstrates favicon URL icon)',
category: 'Productivity',
tags: 'wiki,docs,knowledge',
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',
'widget-bookmark-docs',
'widget-note-welcome',
'widget-embed-grafana',
'widget-status-infra'
];
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 })
}
});
// --- Bookmark widget ---
await prisma.widget.create({
data: {
id: 'widget-bookmark-docs',
sectionId: mediaSection.id,
type: 'bookmark',
order: 1,
config: JSON.stringify({
url: 'https://docs.selfhosted.example.com',
label: 'Self-Hosted Docs',
icon: 'book-open',
description: 'Documentation for all self-hosted services'
})
}
});
// --- Note widget ---
await prisma.widget.create({
data: {
id: 'widget-note-welcome',
sectionId: mediaSection.id,
type: 'note',
order: 2,
config: JSON.stringify({
content:
'# Welcome\n\nThis is your **home dashboard**. Use sections to organize apps, bookmarks, notes, and more.\n\n- Drag to reorder\n- Click to launch\n- Edit to customize',
format: 'markdown'
})
}
});
// --- Embed widget ---
await prisma.widget.create({
data: {
id: 'widget-embed-grafana',
sectionId: infraSection.id,
type: 'embed',
order: 5,
config: JSON.stringify({
url: 'http://grafana.local:3000/d/server-stats/overview?orgId=1&kiosk',
height: 400
})
}
});
// --- Status widget ---
await prisma.widget.create({
data: {
id: 'widget-status-infra',
sectionId: networkSection.id,
type: 'status',
order: 1,
config: JSON.stringify({
appIds: [createdApps[4].id, createdApps[5].id, createdApps[6].id],
label: 'Infrastructure Status'
})
}
});
console.log(' Created widgets for all apps (including bookmark, note, embed, status)');
// --- Second Board with permissions ---
const teamBoard = await prisma.board.upsert({
where: { id: 'team-board' },
update: {},
create: {
id: 'team-board',
name: 'Team Board',
icon: 'users',
description: 'A board with permission controls for the team',
isDefault: false,
isGuestAccessible: false,
createdById: admin.id
}
});
console.log(' Created board:', teamBoard.name);
// Grant 'view' permission to the regular user on the team board
await prisma.permission.upsert({
where: {
entityType_entityId_targetType_targetId: {
entityType: 'board',
entityId: teamBoard.id,
targetType: 'user',
targetId: regularUser.id
}
},
update: { level: 'view' },
create: {
entityType: 'board',
entityId: teamBoard.id,
targetType: 'user',
targetId: regularUser.id,
level: 'view'
}
});
// Grant 'edit' permission to the 'user' group on the team board
await prisma.permission.upsert({
where: {
entityType_entityId_targetType_targetId: {
entityType: 'board',
entityId: teamBoard.id,
targetType: 'group',
targetId: userGroup.id
}
},
update: { level: 'edit' },
create: {
entityType: 'board',
entityId: teamBoard.id,
targetType: 'group',
targetId: userGroup.id,
level: 'edit'
}
});
console.log(' Set permissions on team board');
console.log('Seeding complete!');
}
main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});