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.
This commit is contained in:
2026-03-25 14:18:10 +03:00
parent 8d7847889e
commit 1c0a7cb850
212 changed files with 15642 additions and 981 deletions
@@ -0,0 +1,243 @@
-- AlterTable
ALTER TABLE "Board" ADD COLUMN "backgroundType" TEXT;
ALTER TABLE "Board" ADD COLUMN "cardSize" TEXT;
ALTER TABLE "Board" ADD COLUMN "customCss" TEXT;
ALTER TABLE "Board" ADD COLUMN "themeHue" INTEGER;
ALTER TABLE "Board" ADD COLUMN "themeSaturation" INTEGER;
ALTER TABLE "Board" ADD COLUMN "wallpaperBlur" INTEGER;
ALTER TABLE "Board" ADD COLUMN "wallpaperOverlay" REAL;
ALTER TABLE "Board" ADD COLUMN "wallpaperUrl" TEXT;
-- AlterTable
ALTER TABLE "Section" ADD COLUMN "cardSize" TEXT;
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"color" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "AppTag" (
"id" TEXT NOT NULL PRIMARY KEY,
"appId" TEXT NOT NULL,
"tagId" TEXT NOT NULL,
CONSTRAINT "AppTag_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "AppTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AppLink" (
"id" TEXT NOT NULL PRIMARY KEY,
"appId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"url" TEXT NOT NULL,
"icon" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "AppLink_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "UserFavorite" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"appId" TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "UserFavorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "UserFavorite_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AppClick" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"appId" TEXT NOT NULL,
"clickedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AppClick_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "AppClick_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "NotificationChannel" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"config" TEXT NOT NULL DEFAULT '{}',
"enabled" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NotificationChannel_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"appId" TEXT,
"event" TEXT NOT NULL,
"message" TEXT NOT NULL,
"sentAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"readAt" DATETIME,
CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "Notification_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ApiToken" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"lastUsedAt" DATETIME,
"expiresAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ApiToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT,
"action" TEXT NOT NULL,
"entityType" TEXT NOT NULL,
"entityId" TEXT NOT NULL,
"details" TEXT NOT NULL DEFAULT '{}',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "BoardTemplate" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"icon" TEXT,
"config" TEXT NOT NULL DEFAULT '{}',
"isBuiltin" BOOLEAN NOT NULL DEFAULT false,
"createdById" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BoardTemplate_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_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 '{}',
"customCss" TEXT,
"onboardingComplete" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_SystemSettings" ("authMode", "createdAt", "defaultPrimaryColor", "defaultTheme", "healthcheckDefaults", "id", "oauthClientId", "oauthClientSecret", "oauthDiscoveryUrl", "registrationEnabled", "updatedAt") SELECT "authMode", "createdAt", "defaultPrimaryColor", "defaultTheme", "healthcheckDefaults", "id", "oauthClientId", "oauthClientSecret", "oauthDiscoveryUrl", "registrationEnabled", "updatedAt" FROM "SystemSettings";
DROP TABLE "SystemSettings";
ALTER TABLE "new_SystemSettings" RENAME TO "SystemSettings";
CREATE TABLE "new_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,
"onboardingComplete" BOOLEAN NOT NULL DEFAULT false,
"trackRecentApps" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"themeMode" TEXT,
"primaryHue" INTEGER,
"primarySaturation" INTEGER,
"backgroundType" TEXT,
"locale" TEXT
);
INSERT INTO "new_User" ("authProvider", "avatarUrl", "backgroundType", "createdAt", "displayName", "email", "id", "locale", "password", "primaryHue", "primarySaturation", "refreshToken", "refreshTokenExpiresAt", "role", "themeMode", "updatedAt") SELECT "authProvider", "avatarUrl", "backgroundType", "createdAt", "displayName", "email", "id", "locale", "password", "primaryHue", "primarySaturation", "refreshToken", "refreshTokenExpiresAt", "role", "themeMode", "updatedAt" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
CREATE INDEX "User_email_idx" ON "User"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
-- CreateIndex
CREATE INDEX "Tag_name_idx" ON "Tag"("name");
-- CreateIndex
CREATE INDEX "AppTag_appId_idx" ON "AppTag"("appId");
-- CreateIndex
CREATE INDEX "AppTag_tagId_idx" ON "AppTag"("tagId");
-- CreateIndex
CREATE UNIQUE INDEX "AppTag_appId_tagId_key" ON "AppTag"("appId", "tagId");
-- CreateIndex
CREATE INDEX "AppLink_appId_idx" ON "AppLink"("appId");
-- CreateIndex
CREATE INDEX "UserFavorite_userId_idx" ON "UserFavorite"("userId");
-- CreateIndex
CREATE INDEX "UserFavorite_appId_idx" ON "UserFavorite"("appId");
-- CreateIndex
CREATE UNIQUE INDEX "UserFavorite_userId_appId_key" ON "UserFavorite"("userId", "appId");
-- CreateIndex
CREATE INDEX "AppClick_userId_idx" ON "AppClick"("userId");
-- CreateIndex
CREATE INDEX "AppClick_appId_idx" ON "AppClick"("appId");
-- CreateIndex
CREATE INDEX "AppClick_clickedAt_idx" ON "AppClick"("clickedAt");
-- CreateIndex
CREATE INDEX "NotificationChannel_userId_idx" ON "NotificationChannel"("userId");
-- CreateIndex
CREATE INDEX "Notification_userId_idx" ON "Notification"("userId");
-- CreateIndex
CREATE INDEX "Notification_appId_idx" ON "Notification"("appId");
-- CreateIndex
CREATE INDEX "Notification_sentAt_idx" ON "Notification"("sentAt");
-- CreateIndex
CREATE UNIQUE INDEX "ApiToken_tokenHash_key" ON "ApiToken"("tokenHash");
-- CreateIndex
CREATE INDEX "ApiToken_userId_idx" ON "ApiToken"("userId");
-- CreateIndex
CREATE INDEX "ApiToken_tokenHash_idx" ON "ApiToken"("tokenHash");
-- CreateIndex
CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId");
-- CreateIndex
CREATE INDEX "AuditLog_action_idx" ON "AuditLog"("action");
-- CreateIndex
CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId");
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
-- CreateIndex
CREATE INDEX "BoardTemplate_createdById_idx" ON "BoardTemplate"("createdById");
+176 -6
View File
@@ -17,6 +17,8 @@ model User {
role String @default("user") // admin | user
refreshToken String?
refreshTokenExpiresAt DateTime?
onboardingComplete Boolean @default(false)
trackRecentApps Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -26,9 +28,16 @@ model User {
backgroundType String?
locale String?
groups UserGroup[]
createdApps App[]
boards Board[]
groups UserGroup[]
createdApps App[]
boards Board[]
favorites UserFavorite[]
clicks AppClick[]
notificationChannels NotificationChannel[]
notifications Notification[]
apiTokens ApiToken[]
auditLogs AuditLog[]
boardTemplates BoardTemplate[]
@@index([email])
}
@@ -75,9 +84,14 @@ model App {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
statuses AppStatus[]
widgets Widget[]
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
statuses AppStatus[]
widgets Widget[]
appTags AppTag[]
links AppLink[]
clicks AppClick[]
notifications Notification[]
favorites UserFavorite[]
@@index([name])
@@index([category])
@@ -105,6 +119,14 @@ model Board {
isDefault Boolean @default(false)
isGuestAccessible Boolean @default(false)
backgroundConfig String? // JSON stored as string for SQLite
themeHue Int?
themeSaturation Int?
backgroundType String?
cardSize String?
wallpaperUrl String?
wallpaperBlur Int?
wallpaperOverlay Float?
customCss String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -122,6 +144,7 @@ model Section {
icon String?
order Int @default(0)
isExpandedByDefault Boolean @default(true)
cardSize String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -173,6 +196,153 @@ model SystemSettings {
defaultTheme String @default("dark")
defaultPrimaryColor String @default("#6366f1")
healthcheckDefaults String @default("{}") // JSON stored as string for SQLite
customCss String?
onboardingComplete Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// --- New models for Phases 4-7 ---
model Tag {
id String @id @default(cuid())
name String @unique
color String?
createdAt DateTime @default(now())
appTags AppTag[]
@@index([name])
}
model AppTag {
id String @id @default(cuid())
appId String
tagId String
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([appId, tagId])
@@index([appId])
@@index([tagId])
}
model AppLink {
id String @id @default(cuid())
appId String
label String
url String
icon String?
order Int @default(0)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@index([appId])
}
model UserFavorite {
id String @id @default(cuid())
userId String
appId String
order Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@unique([userId, appId])
@@index([userId])
@@index([appId])
}
model AppClick {
id String @id @default(cuid())
userId String
appId String
clickedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([appId])
@@index([clickedAt])
}
model NotificationChannel {
id String @id @default(cuid())
userId String
type String // discord | slack | telegram | http
config String @default("{}") // JSON stored as string for SQLite
enabled Boolean @default(true)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model Notification {
id String @id @default(cuid())
userId String
appId String?
event String // app_online | app_offline | app_degraded
message String
sentAt DateTime @default(now())
readAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([appId])
@@index([sentAt])
}
model ApiToken {
id String @id @default(cuid())
userId String
name String
tokenHash String @unique
scope String // read | write | admin
lastUsedAt DateTime?
expiresAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([tokenHash])
}
model AuditLog {
id String @id @default(cuid())
userId String?
action String // user_created | user_deleted | etc.
entityType String
entityId String
details String @default("{}") // JSON stored as string for SQLite
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@index([entityType, entityId])
@@index([createdAt])
}
model BoardTemplate {
id String @id @default(cuid())
name String
description String?
icon String?
config String @default("{}") // JSON stored as string for SQLite
isBuiltin Boolean @default(false)
createdById String?
createdAt DateTime @default(now())
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
@@index([createdById])
}
+2 -1
View File
@@ -386,7 +386,8 @@ async function main() {
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',
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'
})
}