feat(mvp): phase 2 - database schema & services layer
Define full Prisma schema (10 models), run initial migration, build core services (auth, user, group, app, board, permission), Zod validators, type definitions, API response envelope, constants, and seed script.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"avatarUrl" TEXT,
|
||||
"authProvider" TEXT NOT NULL DEFAULT 'local',
|
||||
"role" TEXT NOT NULL DEFAULT 'user',
|
||||
"refreshToken" TEXT,
|
||||
"refreshTokenExpiresAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Group" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserGroup" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"groupId" TEXT NOT NULL,
|
||||
CONSTRAINT "UserGroup_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "UserGroup_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "App" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"icon" TEXT,
|
||||
"iconType" TEXT NOT NULL DEFAULT 'lucide',
|
||||
"description" TEXT,
|
||||
"category" TEXT,
|
||||
"tags" TEXT NOT NULL DEFAULT '',
|
||||
"healthcheckEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"healthcheckInterval" INTEGER NOT NULL DEFAULT 300,
|
||||
"healthcheckMethod" TEXT NOT NULL DEFAULT 'GET',
|
||||
"healthcheckExpectedStatus" INTEGER NOT NULL DEFAULT 200,
|
||||
"healthcheckTimeout" INTEGER NOT NULL DEFAULT 5000,
|
||||
"createdById" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "App_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppStatus" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"appId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'unknown',
|
||||
"responseTime" INTEGER,
|
||||
"checkedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "AppStatus_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Board" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"icon" TEXT,
|
||||
"description" TEXT,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isGuestAccessible" BOOLEAN NOT NULL DEFAULT false,
|
||||
"backgroundConfig" TEXT,
|
||||
"createdById" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Board_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Section" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"boardId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"icon" TEXT,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"isExpandedByDefault" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Section_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Widget" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"sectionId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"config" TEXT NOT NULL DEFAULT '{}',
|
||||
"appId" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Widget_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "Section" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "Widget_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Permission" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"entityType" TEXT NOT NULL,
|
||||
"entityId" TEXT NOT NULL,
|
||||
"targetType" TEXT NOT NULL,
|
||||
"targetId" TEXT NOT NULL,
|
||||
"level" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SystemSettings" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'singleton',
|
||||
"authMode" TEXT NOT NULL DEFAULT 'local',
|
||||
"registrationEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"oauthClientId" TEXT,
|
||||
"oauthClientSecret" TEXT,
|
||||
"oauthDiscoveryUrl" TEXT,
|
||||
"defaultTheme" TEXT NOT NULL DEFAULT 'dark',
|
||||
"defaultPrimaryColor" TEXT NOT NULL DEFAULT '#6366f1',
|
||||
"healthcheckDefaults" TEXT NOT NULL DEFAULT '{}',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Group_name_key" ON "Group"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserGroup_userId_idx" ON "UserGroup"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserGroup_groupId_idx" ON "UserGroup"("groupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserGroup_userId_groupId_key" ON "UserGroup"("userId", "groupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "App_name_idx" ON "App"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "App_category_idx" ON "App"("category");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "App_createdById_idx" ON "App"("createdById");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AppStatus_appId_idx" ON "AppStatus"("appId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AppStatus_checkedAt_idx" ON "AppStatus"("checkedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Board_createdById_idx" ON "Board"("createdById");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Section_boardId_idx" ON "Section"("boardId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Widget_sectionId_idx" ON "Widget"("sectionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Widget_appId_idx" ON "Widget"("appId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Permission_entityType_entityId_idx" ON "Permission"("entityType", "entityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Permission_targetType_targetId_idx" ON "Permission"("targetType", "targetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Permission_entityType_entityId_targetType_targetId_key" ON "Permission"("entityType", "entityId", "targetType", "targetId");
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
+164
-2
@@ -1,5 +1,3 @@
|
||||
// Prisma schema — models added in Phase 2
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
@@ -8,3 +6,167 @@ datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
password String?
|
||||
displayName String
|
||||
avatarUrl String?
|
||||
authProvider String @default("local") // local | oauth
|
||||
role String @default("user") // admin | user
|
||||
refreshToken String?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
groups UserGroup[]
|
||||
createdApps App[]
|
||||
boards Board[]
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model Group {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
isDefault Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
users UserGroup[]
|
||||
}
|
||||
|
||||
model UserGroup {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
groupId String
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, groupId])
|
||||
@@index([userId])
|
||||
@@index([groupId])
|
||||
}
|
||||
|
||||
model App {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
url String
|
||||
icon String?
|
||||
iconType String @default("lucide") // lucide | simple | url | emoji
|
||||
description String?
|
||||
category String?
|
||||
tags String @default("") // comma-separated
|
||||
healthcheckEnabled Boolean @default(false)
|
||||
healthcheckInterval Int @default(300) // seconds
|
||||
healthcheckMethod String @default("GET")
|
||||
healthcheckExpectedStatus Int @default(200)
|
||||
healthcheckTimeout Int @default(5000) // milliseconds
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
|
||||
statuses AppStatus[]
|
||||
widgets Widget[]
|
||||
|
||||
@@index([name])
|
||||
@@index([category])
|
||||
@@index([createdById])
|
||||
}
|
||||
|
||||
model AppStatus {
|
||||
id String @id @default(cuid())
|
||||
appId String
|
||||
status String @default("unknown") // online | offline | degraded | unknown
|
||||
responseTime Int? // milliseconds
|
||||
checkedAt DateTime @default(now())
|
||||
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([appId])
|
||||
@@index([checkedAt])
|
||||
}
|
||||
|
||||
model Board {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
icon String?
|
||||
description String?
|
||||
isDefault Boolean @default(false)
|
||||
isGuestAccessible Boolean @default(false)
|
||||
backgroundConfig String? // JSON stored as string for SQLite
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
|
||||
sections Section[]
|
||||
|
||||
@@index([createdById])
|
||||
}
|
||||
|
||||
model Section {
|
||||
id String @id @default(cuid())
|
||||
boardId String
|
||||
title String
|
||||
icon String?
|
||||
order Int @default(0)
|
||||
isExpandedByDefault Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
widgets Widget[]
|
||||
|
||||
@@index([boardId])
|
||||
}
|
||||
|
||||
model Widget {
|
||||
id String @id @default(cuid())
|
||||
sectionId String
|
||||
type String // app | bookmark | note | embed | status
|
||||
order Int @default(0)
|
||||
config String @default("{}") // JSON stored as string for SQLite
|
||||
appId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
section Section @relation(fields: [sectionId], references: [id], onDelete: Cascade)
|
||||
app App? @relation(fields: [appId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([sectionId])
|
||||
@@index([appId])
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
entityType String // board | app
|
||||
entityId String
|
||||
targetType String // user | group
|
||||
targetId String
|
||||
level String // view | edit | admin
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([entityType, entityId, targetType, targetId])
|
||||
@@index([entityType, entityId])
|
||||
@@index([targetType, targetId])
|
||||
}
|
||||
|
||||
model SystemSettings {
|
||||
id String @id @default("singleton")
|
||||
authMode String @default("local") // local | oauth | both
|
||||
registrationEnabled Boolean @default(true)
|
||||
oauthClientId String?
|
||||
oauthClientSecret String?
|
||||
oauthDiscoveryUrl String?
|
||||
defaultTheme String @default("dark")
|
||||
defaultPrimaryColor String @default("#6366f1")
|
||||
healthcheckDefaults String @default("{}") // JSON stored as string for SQLite
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
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);
|
||||
|
||||
// --- 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 }
|
||||
});
|
||||
console.log(' Added admin to groups');
|
||||
|
||||
// --- Sample Apps ---
|
||||
const apps = [
|
||||
{
|
||||
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
|
||||
}
|
||||
];
|
||||
|
||||
const createdApps = [];
|
||||
for (const appData of apps) {
|
||||
const app = await prisma.app.upsert({
|
||||
where: { id: appData.name.toLowerCase().replace(/\s+/g, '-') },
|
||||
update: {},
|
||||
create: {
|
||||
...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);
|
||||
|
||||
// --- Widgets ---
|
||||
// Plex widget in media section
|
||||
await prisma.widget.upsert({
|
||||
where: { id: 'widget-plex' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'widget-plex',
|
||||
sectionId: mediaSection.id,
|
||||
type: 'app',
|
||||
order: 0,
|
||||
appId: createdApps[0].id,
|
||||
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
||||
}
|
||||
});
|
||||
|
||||
// Nextcloud widget in infra section
|
||||
await prisma.widget.upsert({
|
||||
where: { id: 'widget-nextcloud' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'widget-nextcloud',
|
||||
sectionId: infraSection.id,
|
||||
type: 'app',
|
||||
order: 0,
|
||||
appId: createdApps[1].id,
|
||||
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
||||
}
|
||||
});
|
||||
|
||||
// Gitea widget in infra section
|
||||
await prisma.widget.upsert({
|
||||
where: { id: 'widget-gitea' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'widget-gitea',
|
||||
sectionId: infraSection.id,
|
||||
type: 'app',
|
||||
order: 1,
|
||||
appId: createdApps[2].id,
|
||||
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
||||
}
|
||||
});
|
||||
|
||||
// Home Assistant widget in infra section
|
||||
await prisma.widget.upsert({
|
||||
where: { id: 'widget-homeassistant' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'widget-homeassistant',
|
||||
sectionId: infraSection.id,
|
||||
type: 'app',
|
||||
order: 2,
|
||||
appId: createdApps[3].id,
|
||||
config: JSON.stringify({ showStatus: true, openInNewTab: true })
|
||||
}
|
||||
});
|
||||
|
||||
// Grafana widget in infra section
|
||||
await prisma.widget.upsert({
|
||||
where: { id: 'widget-grafana' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'widget-grafana',
|
||||
sectionId: infraSection.id,
|
||||
type: 'app',
|
||||
order: 3,
|
||||
appId: createdApps[4].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();
|
||||
});
|
||||
Reference in New Issue
Block a user