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
+212 -9
View File
@@ -7,7 +7,11 @@ import {
PermissionLevel,
EntityType,
TargetType,
HealthcheckMethod
HealthcheckMethod,
CardSize,
NotificationType,
ApiTokenScope,
AuditAction
} from './constants.js';
// --- Auth ---
@@ -37,7 +41,9 @@ export const createUserSchema = z.object({
export const updateUserSchema = z.object({
displayName: z.string().min(1).max(100).optional(),
avatarUrl: z.string().url().nullable().optional(),
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional()
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional(),
onboardingComplete: z.boolean().optional(),
trackRecentApps: z.boolean().optional()
});
// --- Group ---
@@ -103,7 +109,18 @@ export const updateBoardSchema = z.object({
description: z.string().max(1000).nullable().optional(),
isDefault: z.boolean().optional(),
isGuestAccessible: z.boolean().optional(),
backgroundConfig: z.string().nullable().optional()
backgroundConfig: z.string().nullable().optional(),
themeHue: z.number().int().min(0).max(360).nullable().optional(),
themeSaturation: z.number().int().min(0).max(100).nullable().optional(),
backgroundType: z
.enum(['mesh', 'particles', 'aurora', 'wallpaper', 'none'])
.nullable()
.optional(),
cardSize: z.enum([CardSize.COMPACT, CardSize.MEDIUM, CardSize.LARGE]).nullable().optional(),
wallpaperUrl: z.string().url().max(2000).nullable().optional(),
wallpaperBlur: z.number().int().min(0).max(50).nullable().optional(),
wallpaperOverlay: z.number().min(0).max(1).nullable().optional(),
customCss: z.string().max(10000).nullable().optional()
});
// --- Section ---
@@ -120,7 +137,8 @@ export const updateSectionSchema = z.object({
title: z.string().min(1).max(200).optional(),
icon: z.string().max(500).nullable().optional(),
order: z.number().int().min(0).optional(),
isExpandedByDefault: z.boolean().optional()
isExpandedByDefault: z.boolean().optional(),
cardSize: z.enum([CardSize.COMPACT, CardSize.MEDIUM, CardSize.LARGE]).nullable().optional()
});
// --- Widget Config Schemas ---
@@ -154,18 +172,32 @@ export const statusWidgetConfigSchema = z.object({
// --- Widget ---
const allWidgetTypes = [
WidgetType.APP,
WidgetType.BOOKMARK,
WidgetType.NOTE,
WidgetType.EMBED,
WidgetType.STATUS,
WidgetType.CLOCK,
WidgetType.SYSTEM_STATS,
WidgetType.RSS,
WidgetType.CALENDAR,
WidgetType.MARKDOWN,
WidgetType.METRIC,
WidgetType.LINK_GROUP,
WidgetType.CAMERA
] as const;
export const createWidgetSchema = z.object({
sectionId: z.string().cuid(),
type: z.enum([WidgetType.APP, WidgetType.BOOKMARK, WidgetType.NOTE, WidgetType.EMBED, WidgetType.STATUS]),
type: z.enum(allWidgetTypes),
order: z.number().int().min(0).optional(),
config: z.string().optional(),
appId: z.string().cuid().optional()
});
export const updateWidgetSchema = z.object({
type: z
.enum([WidgetType.APP, WidgetType.BOOKMARK, WidgetType.NOTE, WidgetType.EMBED, WidgetType.STATUS])
.optional(),
type: z.enum(allWidgetTypes).optional(),
order: z.number().int().min(0).optional(),
config: z.string().optional(),
appId: z.string().cuid().nullable().optional()
@@ -259,5 +291,176 @@ export const updateSystemSettingsSchema = z.object({
.string()
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
.optional(),
healthcheckDefaults: z.string().optional()
healthcheckDefaults: z.string().optional(),
customCss: z.string().max(10000).nullable().optional(),
onboardingComplete: z.boolean().optional()
});
// --- New widget config schemas for Phases 4-7 ---
export const clockWeatherWidgetConfigSchema = z.object({
timezone: z.string().max(100).optional(),
showWeather: z.boolean().optional(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
clockStyle: z.enum(['analog', 'digital', '24h']).optional()
});
export const systemStatsWidgetConfigSchema = z.object({
sourceUrl: z.string().url('Invalid source URL'),
sourceType: z.enum(['glances', 'prometheus', 'custom']),
metrics: z.array(z.string().min(1)).min(1, 'At least one metric is required'),
refreshInterval: z.number().int().min(5).max(3600).optional()
});
export const rssWidgetConfigSchema = z.object({
feedUrl: z.string().url('Invalid feed URL'),
maxItems: z.number().int().min(1).max(50).optional(),
showSummary: z.boolean().optional()
});
export const calendarWidgetConfigSchema = z.object({
icalUrls: z
.array(
z.object({
url: z.string().url('Invalid iCal URL'),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
.optional(),
label: z.string().max(100).optional()
})
)
.min(1, 'At least one calendar URL is required'),
daysAhead: z.number().int().min(1).max(90).optional()
});
export const markdownWidgetConfigSchema = z.object({
content: z.string().max(50000, 'Content too long'),
syntaxTheme: z.string().max(50).optional()
});
export const metricWidgetConfigSchema = z.object({
label: z.string().min(1, 'Label is required').max(200),
source: z.enum(['static', 'json', 'prometheus']),
value: z.string().max(200).optional(),
url: z.string().url().optional(),
jsonPath: z.string().max(200).optional(),
query: z.string().max(500).optional(),
unit: z.string().max(50).optional(),
refreshInterval: z.number().int().min(5).max(3600).optional()
});
export const linkGroupWidgetConfigSchema = z.object({
links: z
.array(
z.object({
label: z.string().min(1).max(200),
url: z.string().url('Invalid URL'),
icon: z.string().max(100).optional()
})
)
.min(1, 'At least one link is required'),
collapsible: z.boolean().optional()
});
export const cameraWidgetConfigSchema = z.object({
streamUrl: z.string().url('Invalid stream URL'),
type: z.enum(['mjpeg', 'hls', 'image']),
refreshInterval: z.number().int().min(1).max(3600).optional(),
aspectRatio: z.string().max(20).optional()
});
// --- New entity schemas for Phases 4-7 ---
export const createTagSchema = z.object({
name: z.string().min(1, 'Tag name is required').max(50),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
.optional()
});
export const updateTagSchema = z.object({
name: z.string().min(1).max(50).optional(),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
.nullable()
.optional()
});
export const createAppLinkSchema = z.object({
appId: z.string().cuid(),
label: z.string().min(1, 'Label is required').max(200),
url: z.string().url('Invalid URL'),
icon: z.string().max(100).optional(),
order: z.number().int().min(0).optional()
});
export const updateAppLinkSchema = z.object({
label: z.string().min(1).max(200).optional(),
url: z.string().url().optional(),
icon: z.string().max(100).nullable().optional(),
order: z.number().int().min(0).optional()
});
export const createNotificationChannelSchema = z.object({
type: z.enum([
NotificationType.DISCORD,
NotificationType.SLACK,
NotificationType.TELEGRAM,
NotificationType.HTTP
]),
config: z.string().min(1, 'Config is required'),
enabled: z.boolean().optional()
});
export const updateNotificationChannelSchema = z.object({
type: z
.enum([
NotificationType.DISCORD,
NotificationType.SLACK,
NotificationType.TELEGRAM,
NotificationType.HTTP
])
.optional(),
config: z.string().min(1).optional(),
enabled: z.boolean().optional()
});
export const createApiTokenSchema = z.object({
name: z.string().min(1, 'Token name is required').max(100),
scope: z.enum([ApiTokenScope.READ, ApiTokenScope.WRITE, ApiTokenScope.ADMIN]),
expiresAt: z.string().datetime().optional()
});
export const createBoardTemplateSchema = z.object({
name: z.string().min(1, 'Template name is required').max(200),
description: z.string().max(1000).optional(),
icon: z.string().max(500).optional(),
config: z.string().min(1, 'Config is required'),
isBuiltin: z.boolean().optional()
});
export const auditLogQuerySchema = z.object({
action: z
.enum([
AuditAction.USER_CREATED,
AuditAction.USER_DELETED,
AuditAction.USER_UPDATED,
AuditAction.BOARD_CREATED,
AuditAction.BOARD_DELETED,
AuditAction.APP_CREATED,
AuditAction.APP_DELETED,
AuditAction.SETTINGS_UPDATED,
AuditAction.IMPORT,
AuditAction.EXPORT
])
.optional(),
entityType: z.string().max(50).optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional(),
page: z.number().int().min(1).optional(),
limit: z.number().int().min(1).max(100).optional()
});