Initial commit: Personal Dashboard

Next.js 16 dashboard with configurable widgets (favorites, notes, calendar,
clock, calculator, search, domain-check), multi-tab support, user auth,
dark mode, and Docker deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-18 10:02:05 +02:00
commit a4051ae132
74 changed files with 18317 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
displayName String?
profileImageUrl String?
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
favorites Favorite[]
settings Settings?
tabs DashboardTab[]
widgets Widget[]
notes NoteBoardItem[]
calendarSources CalendarSource[]
calendarWidgetSources CalendarWidgetSource[]
}
model Session {
id String @id @default(cuid())
tokenHash String @unique
userId String
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
}
model DashboardTab {
id String @id @default(cuid())
userId String
title String @default("Dashboard")
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
widgets Widget[]
@@index([userId])
@@index([userId, position])
}
model Favorite {
id String @id @default(cuid())
userId String
widgetId String?
title String
url String
iconUrl String?
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
widget Widget? @relation(fields: [widgetId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([widgetId])
@@index([userId, widgetId])
}
model Settings {
id String @id @default(cuid())
userId String @unique
darkMode Boolean @default(false)
calendarIcsUrl String?
calendarMaxEvents Int @default(8)
calendarLookaheadDays Int @default(60)
dashboardTitle String @default("Personal Dashboard")
dashboardSubtitle String?
logoUrl String? @default("/logo.svg")
faviconUrl String? @default("/favicon.ico")
backgroundImageUrl String?
backgroundImageOpacity Int @default(0)
primaryColor String @default("#2563eb")
secondaryColor String @default("#dbeafe")
customCss String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Widget {
id String @id @default(cuid())
userId String
tabId String?
type String
title String
position Int @default(0)
x Int @default(0)
y Int @default(0)
w Int @default(4)
h Int @default(4)
opacity Int @default(100)
viewMode String @default("list")
fontSize Int @default(100)
calendarNextEventsCount Int @default(3)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tab DashboardTab? @relation(fields: [tabId], references: [id], onDelete: Cascade)
favorites Favorite[]
calendarWidgetSources CalendarWidgetSource[]
@@index([userId])
@@index([tabId])
@@index([userId, tabId])
@@index([userId, position])
}
model NoteBoardItem {
id String @id @default(cuid())
userId String
type String @default("note")
title String
content String @default("")
x Int @default(0)
y Int @default(0)
w Int @default(3)
h Int @default(3)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model CalendarSource {
id String @id @default(cuid())
userId String
widgetId String?
type String @default("ICS")
name String @default("Kalender")
color String @default("#2563eb")
nextEventsCount Int @default(3)
icsUrl String?
exchangeEwsUrl String?
exchangeMailbox String?
exchangeUsername String?
exchangeDomain String?
exchangePasswordEnc String?
exchangePasswordIv String?
exchangePasswordTag String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
widgetLinks CalendarWidgetSource[]
@@index([userId])
@@index([widgetId])
}
model CalendarWidgetSource {
id String @id @default(cuid())
userId String
widgetId String
sourceId String
position Int @default(0)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
widget Widget @relation(fields: [widgetId], references: [id], onDelete: Cascade)
source CalendarSource @relation(fields: [sourceId], references: [id], onDelete: Cascade)
@@unique([widgetId, sourceId])
@@index([userId])
@@index([widgetId])
@@index([sourceId])
}
enum UserRole {
ADMIN
USER
}
+106
View File
@@ -0,0 +1,106 @@
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
function normalizeEmail(value) {
const email = String(value ?? "").trim().toLowerCase();
if (!email) {
return "admin@example.local";
}
return email;
}
function normalizePassword(value) {
const password = String(value ?? "").trim();
if (!password) {
return "BitteEinLangesSicheresPasswortSetzen";
}
return password;
}
async function ensureUserSettings(userId, email) {
const existingSettings = await prisma.settings.findUnique({
where: {
userId
}
});
if (existingSettings) {
return existingSettings;
}
return prisma.settings.create({
data: {
userId,
darkMode: false,
calendarIcsUrl: null,
calendarMaxEvents: 8,
calendarLookaheadDays: 60,
dashboardTitle: "Personal Dashboard",
dashboardSubtitle: email,
logoUrl: "/logo.svg",
backgroundImageUrl: "/background-fancy.svg",
backgroundImageOpacity: 32,
primaryColor: "#2563eb",
secondaryColor: "#dbeafe"
}
});
}
async function main() {
const initialAdminEmail = normalizeEmail(process.env.INITIAL_ADMIN_EMAIL);
const initialAdminPassword = normalizePassword(process.env.INITIAL_ADMIN_PASSWORD);
const existingAdmin = await prisma.user.findUnique({
where: {
email: initialAdminEmail
},
select: {
id: true,
email: true,
role: true
}
});
if (existingAdmin) {
await ensureUserSettings(existingAdmin.id, existingAdmin.email);
console.log("Initialer Admin existiert bereits. Seed übersprungen.");
return;
}
const passwordHash = await bcrypt.hash(initialAdminPassword, 12);
const admin = await prisma.user.create({
data: {
email: initialAdminEmail,
passwordHash,
displayName: "Admin",
profileImageUrl: null,
role: "ADMIN"
},
select: {
id: true,
email: true
}
});
await ensureUserSettings(admin.id, admin.email);
console.log("Initialer Admin wurde erstellt.");
console.log(`E-Mail: ${admin.email}`);
console.log("Dashboard startet leer. Es wurden keine Widgets und keine Favoriten angelegt.");
}
main()
.catch((error) => {
console.error(error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});