@superdangerous/app-framework 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/dist/api/logsRouter.d.ts +20 -0
- package/dist/api/logsRouter.d.ts.map +1 -0
- package/dist/api/logsRouter.js +515 -0
- package/dist/api/logsRouter.js.map +1 -0
- package/dist/cli/dev-server.d.ts +7 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +640 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/StandardServer.d.ts +129 -0
- package/dist/core/StandardServer.d.ts.map +1 -0
- package/dist/core/StandardServer.js +453 -0
- package/dist/core/StandardServer.js.map +1 -0
- package/dist/core/apiResponse.d.ts +69 -0
- package/dist/core/apiResponse.d.ts.map +1 -0
- package/dist/core/apiResponse.js +127 -0
- package/dist/core/apiResponse.js.map +1 -0
- package/dist/core/healthCheck.d.ts +160 -0
- package/dist/core/healthCheck.d.ts.map +1 -0
- package/dist/core/healthCheck.js +398 -0
- package/dist/core/healthCheck.js.map +1 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +40 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +117 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +826 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/portUtils.d.ts +71 -0
- package/dist/core/portUtils.d.ts.map +1 -0
- package/dist/core/portUtils.js +240 -0
- package/dist/core/portUtils.js.map +1 -0
- package/dist/core/storageService.d.ts +119 -0
- package/dist/core/storageService.d.ts.map +1 -0
- package/dist/core/storageService.js +405 -0
- package/dist/core/storageService.js.map +1 -0
- package/dist/desktop/bundler.d.ts +40 -0
- package/dist/desktop/bundler.d.ts.map +1 -0
- package/dist/desktop/bundler.js +176 -0
- package/dist/desktop/bundler.js.map +1 -0
- package/dist/desktop/index.d.ts +25 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/index.js +15 -0
- package/dist/desktop/index.js.map +1 -0
- package/dist/desktop/native-modules.d.ts +66 -0
- package/dist/desktop/native-modules.d.ts.map +1 -0
- package/dist/desktop/native-modules.js +200 -0
- package/dist/desktop/native-modules.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/LogCategories.d.ts +87 -0
- package/dist/logging/LogCategories.d.ts.map +1 -0
- package/dist/logging/LogCategories.js +205 -0
- package/dist/logging/LogCategories.js.map +1 -0
- package/dist/middleware/aiErrorHandler.d.ts +31 -0
- package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
- package/dist/middleware/aiErrorHandler.js +181 -0
- package/dist/middleware/aiErrorHandler.js.map +1 -0
- package/dist/middleware/auth.d.ts +101 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +230 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +56 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +123 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +13 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +85 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/fileUpload.d.ts +62 -0
- package/dist/middleware/fileUpload.d.ts.map +1 -0
- package/dist/middleware/fileUpload.js +175 -0
- package/dist/middleware/fileUpload.js.map +1 -0
- package/dist/middleware/health.d.ts +48 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +143 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/index.d.ts +20 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/openapi.d.ts +64 -0
- package/dist/middleware/openapi.d.ts.map +1 -0
- package/dist/middleware/openapi.js +258 -0
- package/dist/middleware/openapi.js.map +1 -0
- package/dist/middleware/requestLogging.d.ts +22 -0
- package/dist/middleware/requestLogging.d.ts.map +1 -0
- package/dist/middleware/requestLogging.js +61 -0
- package/dist/middleware/requestLogging.js.map +1 -0
- package/dist/middleware/session.d.ts +84 -0
- package/dist/middleware/session.d.ts.map +1 -0
- package/dist/middleware/session.js +189 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/validation.d.ts +1337 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +483 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/services/aiService.d.ts +180 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +547 -0
- package/dist/services/aiService.js.map +1 -0
- package/dist/services/conversationStorage.d.ts +38 -0
- package/dist/services/conversationStorage.d.ts.map +1 -0
- package/dist/services/conversationStorage.js +158 -0
- package/dist/services/conversationStorage.js.map +1 -0
- package/dist/services/crossPlatformBuffer.d.ts +84 -0
- package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
- package/dist/services/crossPlatformBuffer.js +246 -0
- package/dist/services/crossPlatformBuffer.js.map +1 -0
- package/dist/services/index.d.ts +17 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/networkService.d.ts +81 -0
- package/dist/services/networkService.d.ts.map +1 -0
- package/dist/services/networkService.js +268 -0
- package/dist/services/networkService.js.map +1 -0
- package/dist/services/queueService.d.ts +112 -0
- package/dist/services/queueService.d.ts.map +1 -0
- package/dist/services/queueService.js +338 -0
- package/dist/services/queueService.js.map +1 -0
- package/dist/services/settingsService.d.ts +135 -0
- package/dist/services/settingsService.d.ts.map +1 -0
- package/dist/services/settingsService.js +425 -0
- package/dist/services/settingsService.js.map +1 -0
- package/dist/services/systemMonitor.d.ts +208 -0
- package/dist/services/systemMonitor.d.ts.map +1 -0
- package/dist/services/systemMonitor.js +693 -0
- package/dist/services/systemMonitor.js.map +1 -0
- package/dist/services/updateService.d.ts +78 -0
- package/dist/services/updateService.d.ts.map +1 -0
- package/dist/services/updateService.js +252 -0
- package/dist/services/updateService.js.map +1 -0
- package/dist/services/websocketEvents.d.ts +372 -0
- package/dist/services/websocketEvents.d.ts.map +1 -0
- package/dist/services/websocketEvents.js +338 -0
- package/dist/services/websocketEvents.js.map +1 -0
- package/dist/services/websocketServer.d.ts +80 -0
- package/dist/services/websocketServer.d.ts.map +1 -0
- package/dist/services/websocketServer.js +299 -0
- package/dist/services/websocketServer.js.map +1 -0
- package/dist/settings/SettingsSchema.d.ts +151 -0
- package/dist/settings/SettingsSchema.d.ts.map +1 -0
- package/dist/settings/SettingsSchema.js +424 -0
- package/dist/settings/SettingsSchema.js.map +1 -0
- package/dist/testing/TestServer.d.ts +69 -0
- package/dist/testing/TestServer.d.ts.map +1 -0
- package/dist/testing/TestServer.js +250 -0
- package/dist/testing/TestServer.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/appPaths.d.ts +74 -0
- package/dist/utils/appPaths.d.ts.map +1 -0
- package/dist/utils/appPaths.js +162 -0
- package/dist/utils/appPaths.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +50 -0
- package/dist/utils/fs-utils.d.ts.map +1 -0
- package/dist/utils/fs-utils.js +114 -0
- package/dist/utils/fs-utils.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/standardConfig.d.ts +61 -0
- package/dist/utils/standardConfig.d.ts.map +1 -0
- package/dist/utils/standardConfig.js +109 -0
- package/dist/utils/standardConfig.js.map +1 -0
- package/dist/utils/startupBanner.d.ts +34 -0
- package/dist/utils/startupBanner.d.ts.map +1 -0
- package/dist/utils/startupBanner.js +169 -0
- package/dist/utils/startupBanner.js.map +1 -0
- package/dist/utils/startupLogger.d.ts +45 -0
- package/dist/utils/startupLogger.d.ts.map +1 -0
- package/dist/utils/startupLogger.js +200 -0
- package/dist/utils/startupLogger.js.map +1 -0
- package/package.json +151 -0
- package/src/api/logsRouter.ts +600 -0
- package/src/cli/dev-server.ts +803 -0
- package/src/cli/index.ts +31 -0
- package/src/core/StandardServer.ts +587 -0
- package/src/core/apiResponse.ts +202 -0
- package/src/core/healthCheck.ts +565 -0
- package/src/core/index.ts +80 -0
- package/src/core/logger.ts +1092 -0
- package/src/core/portUtils.ts +319 -0
- package/src/core/storageService.ts +595 -0
- package/src/desktop/bundler.ts +271 -0
- package/src/desktop/index.ts +18 -0
- package/src/desktop/native-modules.ts +289 -0
- package/src/index.ts +142 -0
- package/src/logging/LogCategories.ts +302 -0
- package/src/middleware/aiErrorHandler.ts +278 -0
- package/src/middleware/auth.ts +329 -0
- package/src/middleware/cors.ts +187 -0
- package/src/middleware/errorHandler.ts +103 -0
- package/src/middleware/fileUpload.ts +252 -0
- package/src/middleware/health.ts +206 -0
- package/src/middleware/index.ts +71 -0
- package/src/middleware/openapi.ts +305 -0
- package/src/middleware/requestLogging.ts +92 -0
- package/src/middleware/session.ts +238 -0
- package/src/middleware/validation.ts +603 -0
- package/src/services/aiService.ts +789 -0
- package/src/services/conversationStorage.ts +232 -0
- package/src/services/crossPlatformBuffer.ts +341 -0
- package/src/services/index.ts +47 -0
- package/src/services/networkService.ts +351 -0
- package/src/services/queueService.ts +446 -0
- package/src/services/settingsService.ts +549 -0
- package/src/services/systemMonitor.ts +936 -0
- package/src/services/updateService.ts +334 -0
- package/src/services/websocketEvents.ts +409 -0
- package/src/services/websocketServer.ts +394 -0
- package/src/settings/SettingsSchema.ts +664 -0
- package/src/testing/TestServer.ts +312 -0
- package/src/types/index.ts +154 -0
- package/src/utils/appPaths.ts +196 -0
- package/src/utils/fs-utils.ts +130 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/standardConfig.ts +178 -0
- package/src/utils/startupBanner.ts +287 -0
- package/src/utils/startupLogger.ts +268 -0
- package/ui/dist/index.d.mts +1221 -0
- package/ui/dist/index.d.ts +1221 -0
- package/ui/dist/index.js +73 -0
- package/ui/dist/index.js.map +1 -0
- package/ui/dist/index.mjs +73 -0
- package/ui/dist/index.mjs.map +1 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Schema System
|
|
3
|
+
* Consolidated settings system with all features from both implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SettingOption {
|
|
7
|
+
value: any; // Changed from string to any to support more types
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SettingDefinition {
|
|
13
|
+
// Core fields
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
defaultValue?: any;
|
|
18
|
+
|
|
19
|
+
// Type system - comprehensive list
|
|
20
|
+
type:
|
|
21
|
+
| "string"
|
|
22
|
+
| "number"
|
|
23
|
+
| "boolean"
|
|
24
|
+
| "select"
|
|
25
|
+
| "multiselect"
|
|
26
|
+
| "password"
|
|
27
|
+
| "textarea"
|
|
28
|
+
| "email"
|
|
29
|
+
| "url"
|
|
30
|
+
| "color"
|
|
31
|
+
| "date"
|
|
32
|
+
| "time"
|
|
33
|
+
| "datetime"
|
|
34
|
+
| "json"
|
|
35
|
+
| "ipaddress"
|
|
36
|
+
| "network-interface";
|
|
37
|
+
|
|
38
|
+
// Options for select/multiselect
|
|
39
|
+
options?: SettingOption[];
|
|
40
|
+
|
|
41
|
+
// Validation
|
|
42
|
+
required?: boolean;
|
|
43
|
+
validation?: {
|
|
44
|
+
min?: number;
|
|
45
|
+
max?: number;
|
|
46
|
+
minLength?: number;
|
|
47
|
+
maxLength?: number;
|
|
48
|
+
pattern?: string;
|
|
49
|
+
custom?: (value: any) => string | null;
|
|
50
|
+
};
|
|
51
|
+
validationMessage?: string; // Custom validation message
|
|
52
|
+
|
|
53
|
+
// Transforms
|
|
54
|
+
transform?: {
|
|
55
|
+
fromStorage?: (value: any) => any;
|
|
56
|
+
toStorage?: (value: any) => any;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// UI hints
|
|
60
|
+
help?: string; // Detailed help text for tooltips
|
|
61
|
+
hint?: string; // Hint text below input
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
prefix?: string; // Text before input
|
|
64
|
+
suffix?: string; // Text after input
|
|
65
|
+
inputWidth?: "small" | "medium" | "large" | "full";
|
|
66
|
+
rows?: number; // For textarea
|
|
67
|
+
|
|
68
|
+
// Behavior
|
|
69
|
+
readOnly?: boolean;
|
|
70
|
+
hidden?: boolean; // For config-only settings
|
|
71
|
+
sensitive?: boolean; // For passwords, API keys, etc.
|
|
72
|
+
requiresRestart?: boolean;
|
|
73
|
+
|
|
74
|
+
// Organization
|
|
75
|
+
category: string;
|
|
76
|
+
subcategory?: string;
|
|
77
|
+
group?: string; // Group related fields together
|
|
78
|
+
order?: number; // Display order within category
|
|
79
|
+
icon?: string; // Icon to display with field
|
|
80
|
+
|
|
81
|
+
// Conditional logic
|
|
82
|
+
showIf?: (settings: Record<string, any>) => boolean;
|
|
83
|
+
confirmMessage?: string; // Confirmation before applying
|
|
84
|
+
|
|
85
|
+
// Number-specific
|
|
86
|
+
min?: number;
|
|
87
|
+
max?: number;
|
|
88
|
+
step?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SettingsCategory {
|
|
92
|
+
id: string;
|
|
93
|
+
label: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
icon?: string;
|
|
96
|
+
settings: SettingDefinition[];
|
|
97
|
+
order?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SettingsSchema {
|
|
101
|
+
categories: SettingsCategory[];
|
|
102
|
+
version: string;
|
|
103
|
+
onSettingChange?: (
|
|
104
|
+
key: string,
|
|
105
|
+
value: any,
|
|
106
|
+
oldValue: any,
|
|
107
|
+
) => void | Promise<void>;
|
|
108
|
+
onValidate?: (settings: Record<string, any>) => Record<string, string> | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SettingsFormState {
|
|
112
|
+
values: Record<string, any>;
|
|
113
|
+
errors: Record<string, string>;
|
|
114
|
+
touched: Record<string, boolean>;
|
|
115
|
+
dirty: Record<string, boolean>;
|
|
116
|
+
isValid: boolean;
|
|
117
|
+
isSubmitting: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SettingsValidationResult {
|
|
121
|
+
isValid: boolean;
|
|
122
|
+
errors: Record<string, string>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Built-in validation functions
|
|
127
|
+
*/
|
|
128
|
+
export const Validators = {
|
|
129
|
+
required:
|
|
130
|
+
(message = "This field is required") =>
|
|
131
|
+
(value: any) => {
|
|
132
|
+
if (!value || (typeof value === "string" && !value.trim())) {
|
|
133
|
+
return message;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
email:
|
|
139
|
+
(message = "Invalid email address") =>
|
|
140
|
+
(value: string) => {
|
|
141
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
142
|
+
return message;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
url:
|
|
148
|
+
(message = "Invalid URL") =>
|
|
149
|
+
(value: string) => {
|
|
150
|
+
if (value) {
|
|
151
|
+
try {
|
|
152
|
+
new URL(value);
|
|
153
|
+
} catch {
|
|
154
|
+
return message;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
ipAddress:
|
|
161
|
+
(message = "Invalid IP address") =>
|
|
162
|
+
(value: string) => {
|
|
163
|
+
if (value) {
|
|
164
|
+
const parts = value.split(".");
|
|
165
|
+
if (parts.length !== 4) return message;
|
|
166
|
+
for (const part of parts) {
|
|
167
|
+
const num = parseInt(part, 10);
|
|
168
|
+
if (isNaN(num) || num < 0 || num > 255) return message;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
port:
|
|
175
|
+
(message = "Invalid port number") =>
|
|
176
|
+
(value: number) => {
|
|
177
|
+
if (value < 1 || value > 65535) {
|
|
178
|
+
return message;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
range: (min: number, max: number, message?: string) => (value: number) => {
|
|
184
|
+
if (value < min || value > max) {
|
|
185
|
+
return message || `Value must be between ${min} and ${max}`;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
pattern:
|
|
191
|
+
(pattern: RegExp, message = "Invalid format") =>
|
|
192
|
+
(value: string) => {
|
|
193
|
+
if (value && !pattern.test(value)) {
|
|
194
|
+
return message;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
minLength: (min: number, message?: string) => (value: string) => {
|
|
200
|
+
if (value && value.length < min) {
|
|
201
|
+
return message || `Must be at least ${min} characters`;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
maxLength: (max: number, message?: string) => (value: string) => {
|
|
207
|
+
if (value && value.length > max) {
|
|
208
|
+
return message || `Must be at most ${max} characters`;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Helper to create a settings schema
|
|
216
|
+
*/
|
|
217
|
+
export function createSettingsSchema(schema: SettingsSchema): SettingsSchema {
|
|
218
|
+
// Sort categories by order
|
|
219
|
+
schema.categories.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
220
|
+
return schema;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get setting by key from categories
|
|
225
|
+
*/
|
|
226
|
+
export function getSettingByKey(
|
|
227
|
+
categories: SettingsCategory[],
|
|
228
|
+
key: string,
|
|
229
|
+
): SettingDefinition | undefined {
|
|
230
|
+
for (const category of categories) {
|
|
231
|
+
const setting = category.settings.find((s) => s.key === key);
|
|
232
|
+
if (setting) return setting;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate a single setting
|
|
239
|
+
*/
|
|
240
|
+
export function validateSetting(
|
|
241
|
+
setting: SettingDefinition,
|
|
242
|
+
value: any,
|
|
243
|
+
): string | null {
|
|
244
|
+
// Check required
|
|
245
|
+
if (
|
|
246
|
+
setting.required &&
|
|
247
|
+
(!value || (typeof value === "string" && !value.trim()))
|
|
248
|
+
) {
|
|
249
|
+
return setting.validationMessage || "This field is required";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Type-specific validation
|
|
253
|
+
switch (setting.type) {
|
|
254
|
+
case "string":
|
|
255
|
+
case "password":
|
|
256
|
+
case "textarea":
|
|
257
|
+
if (value && typeof value !== "string") {
|
|
258
|
+
return setting.validationMessage || "Must be a string";
|
|
259
|
+
}
|
|
260
|
+
if (setting.validation) {
|
|
261
|
+
if (
|
|
262
|
+
setting.validation.minLength !== undefined &&
|
|
263
|
+
value.length < setting.validation.minLength
|
|
264
|
+
) {
|
|
265
|
+
return (
|
|
266
|
+
setting.validationMessage ||
|
|
267
|
+
`Must be at least ${setting.validation.minLength} characters`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
setting.validation.maxLength !== undefined &&
|
|
272
|
+
value.length > setting.validation.maxLength
|
|
273
|
+
) {
|
|
274
|
+
return (
|
|
275
|
+
setting.validationMessage ||
|
|
276
|
+
`Must be at most ${setting.validation.maxLength} characters`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (
|
|
280
|
+
setting.validation.pattern &&
|
|
281
|
+
!new RegExp(setting.validation.pattern).test(value)
|
|
282
|
+
) {
|
|
283
|
+
return setting.validationMessage || "Invalid format";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case "email":
|
|
289
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
290
|
+
return setting.validationMessage || "Invalid email address";
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case "url":
|
|
295
|
+
try {
|
|
296
|
+
if (value) new URL(value);
|
|
297
|
+
} catch {
|
|
298
|
+
return setting.validationMessage || "Invalid URL";
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case "ipaddress":
|
|
303
|
+
if (value) {
|
|
304
|
+
const parts = value.split(".");
|
|
305
|
+
if (parts.length !== 4) {
|
|
306
|
+
return setting.validationMessage || "Invalid IP address";
|
|
307
|
+
}
|
|
308
|
+
for (const part of parts) {
|
|
309
|
+
const num = parseInt(part, 10);
|
|
310
|
+
if (isNaN(num) || num < 0 || num > 255) {
|
|
311
|
+
return setting.validationMessage || "Invalid IP address";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case "number":
|
|
318
|
+
if (typeof value !== "number" || isNaN(value)) {
|
|
319
|
+
return setting.validationMessage || "Must be a valid number";
|
|
320
|
+
}
|
|
321
|
+
if (setting.validation) {
|
|
322
|
+
if (
|
|
323
|
+
setting.validation.min !== undefined &&
|
|
324
|
+
value < setting.validation.min
|
|
325
|
+
) {
|
|
326
|
+
return (
|
|
327
|
+
setting.validationMessage ||
|
|
328
|
+
`Must be at least ${setting.validation.min}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (
|
|
332
|
+
setting.validation.max !== undefined &&
|
|
333
|
+
value > setting.validation.max
|
|
334
|
+
) {
|
|
335
|
+
return (
|
|
336
|
+
setting.validationMessage ||
|
|
337
|
+
`Must be at most ${setting.validation.max}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Also check min/max at root level for backward compatibility
|
|
342
|
+
if (setting.min !== undefined && value < setting.min) {
|
|
343
|
+
return setting.validationMessage || `Must be at least ${setting.min}`;
|
|
344
|
+
}
|
|
345
|
+
if (setting.max !== undefined && value > setting.max) {
|
|
346
|
+
return setting.validationMessage || `Must be at most ${setting.max}`;
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case "boolean":
|
|
351
|
+
if (typeof value !== "boolean") {
|
|
352
|
+
return setting.validationMessage || "Must be true or false";
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
case "select":
|
|
357
|
+
case "multiselect":
|
|
358
|
+
if (setting.options) {
|
|
359
|
+
if (
|
|
360
|
+
setting.type === "select" &&
|
|
361
|
+
!setting.options.some((opt) => opt.value === value)
|
|
362
|
+
) {
|
|
363
|
+
return setting.validationMessage || "Must select a valid option";
|
|
364
|
+
}
|
|
365
|
+
if (setting.type === "multiselect" && Array.isArray(value)) {
|
|
366
|
+
const validValues = setting.options.map((opt) => opt.value);
|
|
367
|
+
if (!value.every((v) => validValues.includes(v))) {
|
|
368
|
+
return setting.validationMessage || "Invalid selection";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case "date":
|
|
375
|
+
case "time":
|
|
376
|
+
case "datetime":
|
|
377
|
+
if (value && !Date.parse(value)) {
|
|
378
|
+
return setting.validationMessage || "Invalid date/time";
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case "color":
|
|
383
|
+
if (value && !/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
384
|
+
return (
|
|
385
|
+
setting.validationMessage || "Invalid color (must be hex format)"
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Custom validation
|
|
392
|
+
if (setting.validation?.custom) {
|
|
393
|
+
const error = setting.validation.custom(value);
|
|
394
|
+
if (error) return error;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Validate all settings against schema
|
|
402
|
+
*/
|
|
403
|
+
export function validateSettings(
|
|
404
|
+
schema: SettingsSchema,
|
|
405
|
+
settings: Record<string, any>,
|
|
406
|
+
): Record<string, string> {
|
|
407
|
+
const errors: Record<string, string> = {};
|
|
408
|
+
|
|
409
|
+
for (const category of schema.categories) {
|
|
410
|
+
for (const setting of category.settings) {
|
|
411
|
+
if (setting.hidden) continue;
|
|
412
|
+
|
|
413
|
+
const value = settings[setting.key];
|
|
414
|
+
const error = validateSetting(setting, value);
|
|
415
|
+
if (error) {
|
|
416
|
+
errors[setting.key] = error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Run custom validation
|
|
422
|
+
if (schema.onValidate) {
|
|
423
|
+
const customErrors = schema.onValidate(settings);
|
|
424
|
+
if (customErrors) {
|
|
425
|
+
Object.assign(errors, customErrors);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return errors;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Validate all settings (returns result object)
|
|
434
|
+
*/
|
|
435
|
+
export function validateAllSettings(
|
|
436
|
+
categories: SettingsCategory[],
|
|
437
|
+
values: Record<string, any>,
|
|
438
|
+
): SettingsValidationResult {
|
|
439
|
+
const errors: Record<string, string> = {};
|
|
440
|
+
|
|
441
|
+
for (const category of categories) {
|
|
442
|
+
for (const setting of category.settings) {
|
|
443
|
+
if (setting.hidden) continue;
|
|
444
|
+
|
|
445
|
+
const value = values[setting.key];
|
|
446
|
+
const error = validateSetting(setting, value);
|
|
447
|
+
if (error) {
|
|
448
|
+
errors[setting.key] = error;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
isValid: Object.keys(errors).length === 0,
|
|
455
|
+
errors,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Helper to flatten nested settings for storage
|
|
461
|
+
*/
|
|
462
|
+
export function flattenSettings(
|
|
463
|
+
settings: any,
|
|
464
|
+
prefix = "",
|
|
465
|
+
): Record<string, any> {
|
|
466
|
+
const flattened: Record<string, any> = {};
|
|
467
|
+
|
|
468
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
469
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
470
|
+
|
|
471
|
+
if (
|
|
472
|
+
value &&
|
|
473
|
+
typeof value === "object" &&
|
|
474
|
+
!Array.isArray(value) &&
|
|
475
|
+
!(value instanceof Date)
|
|
476
|
+
) {
|
|
477
|
+
Object.assign(flattened, flattenSettings(value, fullKey));
|
|
478
|
+
} else {
|
|
479
|
+
flattened[fullKey] = value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return flattened;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Alias for consistency
|
|
487
|
+
export const flattenSettingsValues = flattenSettings;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Helper to unflatten settings from storage
|
|
491
|
+
*/
|
|
492
|
+
export function unflattenSettings(flattened: Record<string, any>): any {
|
|
493
|
+
const settings: any = {};
|
|
494
|
+
|
|
495
|
+
for (const [key, value] of Object.entries(flattened)) {
|
|
496
|
+
const parts = key.split(".");
|
|
497
|
+
let current = settings;
|
|
498
|
+
|
|
499
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
500
|
+
const part = parts[i];
|
|
501
|
+
if (!part) continue;
|
|
502
|
+
if (!current[part]) {
|
|
503
|
+
current[part] = {};
|
|
504
|
+
}
|
|
505
|
+
current = current[part];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const lastPart = parts[parts.length - 1];
|
|
509
|
+
if (lastPart) {
|
|
510
|
+
current[lastPart] = value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return settings;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Alias for consistency
|
|
518
|
+
export const unflattenSettingsValues = unflattenSettings;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get all settings that require restart
|
|
522
|
+
*/
|
|
523
|
+
export function getRestartRequiredSettings(
|
|
524
|
+
schema: SettingsSchema | SettingsCategory[],
|
|
525
|
+
changedSettings: Record<string, any> | string[],
|
|
526
|
+
): string[] {
|
|
527
|
+
const restartRequired: string[] = [];
|
|
528
|
+
|
|
529
|
+
// Handle both schema and categories array
|
|
530
|
+
const categories = Array.isArray(schema) ? schema : schema.categories;
|
|
531
|
+
|
|
532
|
+
// Handle both changed settings object and array of keys
|
|
533
|
+
const changedKeys = Array.isArray(changedSettings)
|
|
534
|
+
? changedSettings
|
|
535
|
+
: Object.keys(changedSettings);
|
|
536
|
+
|
|
537
|
+
for (const category of categories) {
|
|
538
|
+
for (const setting of category.settings) {
|
|
539
|
+
if (setting.requiresRestart && changedKeys.includes(setting.key)) {
|
|
540
|
+
restartRequired.push(setting.key);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return restartRequired;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get default values from settings
|
|
550
|
+
*/
|
|
551
|
+
export function getDefaultSettingsValues(
|
|
552
|
+
categories: SettingsCategory[],
|
|
553
|
+
): Record<string, any> {
|
|
554
|
+
const defaults: Record<string, any> = {};
|
|
555
|
+
|
|
556
|
+
for (const category of categories) {
|
|
557
|
+
for (const setting of category.settings) {
|
|
558
|
+
if (!setting.hidden && setting.defaultValue !== undefined) {
|
|
559
|
+
defaults[setting.key] = setting.defaultValue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return defaults;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Evaluate conditional visibility for a field
|
|
569
|
+
*/
|
|
570
|
+
export function evaluateFieldVisibility(
|
|
571
|
+
field: SettingDefinition,
|
|
572
|
+
currentValues: Record<string, any>,
|
|
573
|
+
): boolean {
|
|
574
|
+
if (!field.showIf) {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
return field.showIf(currentValues);
|
|
580
|
+
} catch (_error) {
|
|
581
|
+
console.error(`Error evaluating showIf for field ${field.key}:`, _error);
|
|
582
|
+
return true; // Show field if evaluation fails
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Group fields by their group property
|
|
588
|
+
*/
|
|
589
|
+
export function groupFields(
|
|
590
|
+
fields: SettingDefinition[],
|
|
591
|
+
): Map<string, SettingDefinition[]> {
|
|
592
|
+
const groups = new Map<string, SettingDefinition[]>();
|
|
593
|
+
|
|
594
|
+
// First, add ungrouped fields
|
|
595
|
+
const ungrouped = fields.filter((f) => !f.group);
|
|
596
|
+
if (ungrouped.length > 0) {
|
|
597
|
+
groups.set("_default", ungrouped);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Then, group the rest
|
|
601
|
+
fields
|
|
602
|
+
.filter((f) => f.group)
|
|
603
|
+
.forEach((field) => {
|
|
604
|
+
const group = field.group!;
|
|
605
|
+
if (!groups.has(group)) {
|
|
606
|
+
groups.set(group, []);
|
|
607
|
+
}
|
|
608
|
+
groups.get(group)!.push(field);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Sort fields within each group by order
|
|
612
|
+
groups.forEach((groupFields) => {
|
|
613
|
+
groupFields.sort((a, b) => (a.order || 999) - (b.order || 999));
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return groups;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Apply toStorage transform
|
|
621
|
+
*/
|
|
622
|
+
export function applyToStorageTransform(
|
|
623
|
+
field: SettingDefinition,
|
|
624
|
+
value: any,
|
|
625
|
+
): any {
|
|
626
|
+
if (field.transform?.toStorage) {
|
|
627
|
+
return field.transform.toStorage(value);
|
|
628
|
+
}
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Apply fromStorage transform
|
|
634
|
+
*/
|
|
635
|
+
export function applyFromStorageTransform(
|
|
636
|
+
field: SettingDefinition,
|
|
637
|
+
value: any,
|
|
638
|
+
): any {
|
|
639
|
+
if (field.transform?.fromStorage) {
|
|
640
|
+
return field.transform.fromStorage(value);
|
|
641
|
+
}
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Create settings form state
|
|
647
|
+
*/
|
|
648
|
+
export function createSettingsFormState(
|
|
649
|
+
categories: SettingsCategory[],
|
|
650
|
+
initialValues?: Record<string, any>,
|
|
651
|
+
): SettingsFormState {
|
|
652
|
+
const defaults = getDefaultSettingsValues(categories);
|
|
653
|
+
const values = { ...defaults, ...initialValues };
|
|
654
|
+
const validation = validateAllSettings(categories, values);
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
values,
|
|
658
|
+
errors: validation.errors,
|
|
659
|
+
touched: {},
|
|
660
|
+
dirty: {},
|
|
661
|
+
isValid: validation.isValid,
|
|
662
|
+
isSubmitting: false,
|
|
663
|
+
};
|
|
664
|
+
}
|