1c0a7cb850
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.
494 lines
12 KiB
TypeScript
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();
|
|
});
|