a6b09aae9c
Replace the disconnected board edit page with inline editing directly on the board view. Toggle with Ctrl+E or the Edit button. Features: - Edit mode store with changeset accumulation and batch save - Floating toolbar (save, discard, add section, board settings, exit) - Widget hover overlays with edit/delete/drag controls - Type-specific widget config panels for all 14 widget types - Section inline editing (title, icon picker, delete) - "+" buttons for adding widgets and sections inline - Section-level drag-and-drop reordering via svelte-dnd-action - Batch save API endpoint (single Prisma transaction) - Board properties side panel with live theme/wallpaper preview - Modal widget type picker with search filtering - Icon picker component with visual grid and search - Confirmation dialog modal for all destructive actions - HTML format support for Note widget (in addition to markdown/text) - Full i18n support (en + ru) for all new UI strings - Legacy edit page banner linking to new inline mode
426 lines
13 KiB
TypeScript
426 lines
13 KiB
TypeScript
import { z } from 'zod';
|
|
import {
|
|
UserRole,
|
|
AuthMode,
|
|
WidgetType,
|
|
IconType,
|
|
PermissionLevel,
|
|
EntityType,
|
|
TargetType,
|
|
HealthcheckMethod,
|
|
CardSize,
|
|
NotificationType,
|
|
ApiTokenScope,
|
|
AuditAction
|
|
} from './constants.js';
|
|
|
|
// --- Auth ---
|
|
|
|
export const loginSchema = z.object({
|
|
email: z.string().email('Invalid email address'),
|
|
password: z.string().min(1, 'Password is required')
|
|
});
|
|
|
|
export const registerSchema = z.object({
|
|
email: z.string().email('Invalid email address'),
|
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
displayName: z.string().min(1, 'Display name is required').max(100)
|
|
});
|
|
|
|
// --- User ---
|
|
|
|
export const createUserSchema = z.object({
|
|
email: z.string().email('Invalid email address'),
|
|
password: z.string().min(6).optional(),
|
|
displayName: z.string().min(1).max(100),
|
|
avatarUrl: z.string().url().optional(),
|
|
authProvider: z.enum([AuthMode.LOCAL, AuthMode.OAUTH]).optional(),
|
|
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional()
|
|
});
|
|
|
|
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(),
|
|
onboardingComplete: z.boolean().optional(),
|
|
trackRecentApps: z.boolean().optional()
|
|
});
|
|
|
|
// --- Group ---
|
|
|
|
export const createGroupSchema = z.object({
|
|
name: z.string().min(1, 'Group name is required').max(100),
|
|
description: z.string().max(500).optional(),
|
|
isDefault: z.boolean().optional()
|
|
});
|
|
|
|
export const updateGroupSchema = z.object({
|
|
name: z.string().min(1).max(100).optional(),
|
|
description: z.string().max(500).nullable().optional(),
|
|
isDefault: z.boolean().optional()
|
|
});
|
|
|
|
// --- App ---
|
|
|
|
export const createAppSchema = z.object({
|
|
name: z.string().min(1, 'App name is required').max(200),
|
|
url: z.string().url('Invalid URL'),
|
|
icon: z.string().max(500).optional(),
|
|
iconType: z.enum([IconType.LUCIDE, IconType.SIMPLE, IconType.URL, IconType.EMOJI]).optional(),
|
|
description: z.string().max(1000).optional(),
|
|
category: z.string().max(100).optional(),
|
|
tags: z.string().max(500).optional(),
|
|
healthcheckEnabled: z.boolean().optional(),
|
|
healthcheckInterval: z.number().int().min(30).max(86400).optional(),
|
|
healthcheckMethod: z.enum([HealthcheckMethod.GET, HealthcheckMethod.HEAD]).optional(),
|
|
healthcheckExpectedStatus: z.number().int().min(100).max(599).optional(),
|
|
healthcheckTimeout: z.number().int().min(1000).max(30000).optional(),
|
|
integrationType: z.string().max(50).nullable().optional(),
|
|
integrationConfig: z.string().max(10000).nullable().optional(),
|
|
integrationEnabled: z.boolean().optional()
|
|
});
|
|
|
|
export const updateAppSchema = z.object({
|
|
name: z.string().min(1).max(200).optional(),
|
|
url: z.string().url().optional(),
|
|
icon: z.string().max(500).nullable().optional(),
|
|
iconType: z.enum([IconType.LUCIDE, IconType.SIMPLE, IconType.URL, IconType.EMOJI]).optional(),
|
|
description: z.string().max(1000).nullable().optional(),
|
|
category: z.string().max(100).nullable().optional(),
|
|
tags: z.string().max(500).optional(),
|
|
healthcheckEnabled: z.boolean().optional(),
|
|
healthcheckInterval: z.number().int().min(30).max(86400).optional(),
|
|
healthcheckMethod: z.enum([HealthcheckMethod.GET, HealthcheckMethod.HEAD]).optional(),
|
|
healthcheckExpectedStatus: z.number().int().min(100).max(599).optional(),
|
|
healthcheckTimeout: z.number().int().min(1000).max(30000).optional(),
|
|
integrationType: z.string().max(50).nullable().optional(),
|
|
integrationConfig: z.string().max(10000).nullable().optional(),
|
|
integrationEnabled: z.boolean().optional()
|
|
});
|
|
|
|
// --- Board ---
|
|
|
|
export const createBoardSchema = z.object({
|
|
name: z.string().min(1, 'Board name is required').max(200),
|
|
icon: z.string().max(500).optional(),
|
|
description: z.string().max(1000).optional(),
|
|
isDefault: z.boolean().optional(),
|
|
isGuestAccessible: z.boolean().optional(),
|
|
backgroundConfig: z.string().optional()
|
|
});
|
|
|
|
export const updateBoardSchema = z.object({
|
|
name: z.string().min(1).max(200).optional(),
|
|
icon: z.string().max(500).nullable().optional(),
|
|
description: z.string().max(1000).nullable().optional(),
|
|
isDefault: z.boolean().optional(),
|
|
isGuestAccessible: z.boolean().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 ---
|
|
|
|
export const createSectionSchema = z.object({
|
|
boardId: z.string().cuid(),
|
|
title: z.string().min(1, 'Section title is required').max(200),
|
|
icon: z.string().max(500).optional(),
|
|
order: z.number().int().min(0).optional(),
|
|
isExpandedByDefault: z.boolean().optional()
|
|
});
|
|
|
|
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(),
|
|
cardSize: z.enum([CardSize.COMPACT, CardSize.MEDIUM, CardSize.LARGE]).nullable().optional()
|
|
});
|
|
|
|
// --- Widget Config Schemas ---
|
|
|
|
export const appWidgetConfigSchema = z.object({
|
|
appId: z.string().min(1, 'App ID is required')
|
|
});
|
|
|
|
export const bookmarkWidgetConfigSchema = z.object({
|
|
url: z.string().url('Invalid URL'),
|
|
label: z.string().min(1, 'Label is required').max(200),
|
|
icon: z.string().max(100).optional(),
|
|
description: z.string().max(500).optional()
|
|
});
|
|
|
|
export const noteWidgetConfigSchema = z.object({
|
|
content: z.string().max(10000, 'Content too long'),
|
|
format: z.enum(['markdown', 'text', 'html']).default('markdown')
|
|
});
|
|
|
|
export const embedWidgetConfigSchema = z.object({
|
|
url: z.string().url('Invalid URL'),
|
|
height: z.number().int().min(100).max(2000).default(300),
|
|
sandbox: z.string().max(200).optional()
|
|
});
|
|
|
|
export const statusWidgetConfigSchema = z.object({
|
|
appIds: z.array(z.string().min(1)).min(1, 'At least one app is required'),
|
|
label: z.string().max(200).optional()
|
|
});
|
|
|
|
// --- 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,
|
|
WidgetType.INTEGRATION
|
|
] as const;
|
|
|
|
export const createWidgetSchema = z.object({
|
|
sectionId: z.string().cuid(),
|
|
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(allWidgetTypes).optional(),
|
|
order: z.number().int().min(0).optional(),
|
|
config: z.string().optional(),
|
|
appId: z.string().cuid().nullable().optional()
|
|
});
|
|
|
|
// --- Permission ---
|
|
|
|
export const createPermissionSchema = z.object({
|
|
entityType: z.enum([EntityType.BOARD, EntityType.APP]),
|
|
entityId: z.string().cuid(),
|
|
targetType: z.enum([TargetType.USER, TargetType.GROUP]),
|
|
targetId: z.string().cuid(),
|
|
level: z.enum([PermissionLevel.VIEW, PermissionLevel.EDIT, PermissionLevel.ADMIN])
|
|
});
|
|
|
|
// --- Backup Schedule ---
|
|
|
|
export const updateBackupScheduleSchema = z.object({
|
|
backupEnabled: z.boolean().optional(),
|
|
backupCronExpression: z.string().min(1).max(100).optional(),
|
|
backupMaxCount: z.number().int().min(1).max(100).optional()
|
|
});
|
|
|
|
// --- System Settings ---
|
|
|
|
export const updateSystemSettingsSchema = z.object({
|
|
authMode: z.enum([AuthMode.LOCAL, AuthMode.OAUTH, AuthMode.BOTH]).optional(),
|
|
registrationEnabled: z.boolean().optional(),
|
|
oauthClientId: z.string().nullable().optional(),
|
|
oauthClientSecret: z.string().nullable().optional(),
|
|
oauthDiscoveryUrl: z.string().url().nullable().optional(),
|
|
defaultTheme: z.enum(['dark', 'light']).optional(),
|
|
defaultPrimaryColor: z
|
|
.string()
|
|
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
|
|
.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()
|
|
});
|
|
|
|
export const integrationWidgetConfigSchema = z.object({
|
|
appId: z.string().min(1, 'App ID is required'),
|
|
endpointId: z.string().min(1, 'Endpoint ID is required'),
|
|
refreshInterval: z.number().int().min(5).max(3600).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,
|
|
AuditAction.BACKUP_CREATED,
|
|
AuditAction.BACKUP_RESTORED,
|
|
AuditAction.BACKUP_DELETED
|
|
])
|
|
.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()
|
|
});
|