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:
+212
-9
@@ -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()
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user