create-fluxstack 1.12.1 โ 1.14.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/LLMD/INDEX.md +8 -1
- package/LLMD/agent.md +867 -0
- package/LLMD/config/environment-vars.md +30 -0
- package/LLMD/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +465 -0
- package/LLMD/resources/live-components.md +168 -26
- package/LLMD/resources/live-logging.md +220 -0
- package/LLMD/resources/live-upload.md +59 -8
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +13 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +139 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +334 -0
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- package/app/server/auth/AuthManager.ts +213 -0
- package/app/server/auth/DevAuthProvider.ts +66 -0
- package/app/server/auth/HashManager.ts +123 -0
- package/app/server/auth/JWTAuthProvider.example.ts +101 -0
- package/app/server/auth/RateLimiter.ts +106 -0
- package/app/server/auth/contracts.ts +192 -0
- package/app/server/auth/guards/SessionGuard.ts +167 -0
- package/app/server/auth/guards/TokenGuard.ts +202 -0
- package/app/server/auth/index.ts +174 -0
- package/app/server/auth/middleware.ts +163 -0
- package/app/server/auth/providers/InMemoryProvider.ts +162 -0
- package/app/server/auth/sessions/SessionManager.ts +164 -0
- package/app/server/cache/CacheManager.ts +81 -0
- package/app/server/cache/MemoryDriver.ts +112 -0
- package/app/server/cache/contracts.ts +49 -0
- package/app/server/cache/index.ts +42 -0
- package/app/server/index.ts +14 -0
- package/app/server/live/LiveAdminPanel.ts +174 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -32
- package/app/server/live/LiveProtectedChat.ts +151 -0
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/app/server/routes/auth.routes.ts +278 -0
- package/app/server/routes/index.ts +2 -0
- package/config/index.ts +8 -0
- package/config/system/auth.config.ts +49 -0
- package/config/system/runtime.config.ts +4 -0
- package/config/system/session.config.ts +33 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +17 -10
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +58 -5
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +16 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +19 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +151 -20
- package/core/plugins/config.ts +5 -4
- package/core/plugins/discovery.ts +11 -2
- package/core/plugins/manager.ts +11 -5
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +53 -25
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +134 -50
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +144 -0
- package/core/server/live/LiveRoomManager.ts +22 -5
- package/core/server/live/StateSignature.ts +704 -643
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auth/LiveAuthContext.ts +71 -0
- package/core/server/live/auth/LiveAuthManager.ts +304 -0
- package/core/server/live/auth/index.ts +19 -0
- package/core/server/live/auth/types.ts +179 -0
- package/core/server/live/auto-generated-components.ts +8 -2
- package/core/server/live/index.ts +16 -0
- package/core/server/live/websocket-plugin.ts +323 -22
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/templates/create-project.ts +0 -3
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +278 -22
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -8
- package/plugins/crypto-auth/index.ts +6 -0
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
- package/plugins/crypto-auth/server/index.ts +24 -21
- package/rest-tests/README.md +57 -0
- package/rest-tests/auth-token.http +113 -0
- package/rest-tests/auth.http +112 -0
- package/rest-tests/rooms-token.http +69 -0
- package/rest-tests/users-token.http +62 -0
- package/.dockerignore +0 -81
- package/Dockerfile +0 -70
- package/LIVE_COMPONENTS_REVIEW.md +0 -781
- package/app/client/src/assets/react.svg +0 -1
|
@@ -1,35 +1,115 @@
|
|
|
1
1
|
import { writeFile, mkdir, unlink } from 'fs/promises'
|
|
2
2
|
import { existsSync } from 'fs'
|
|
3
|
-
import { join, extname } from 'path'
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { join, extname, basename } from 'path'
|
|
4
|
+
import { liveLog, liveWarn } from './LiveLogger'
|
|
5
|
+
import type {
|
|
6
|
+
ActiveUpload,
|
|
7
|
+
FileUploadStartMessage,
|
|
7
8
|
FileUploadChunkMessage,
|
|
8
9
|
FileUploadCompleteMessage,
|
|
9
10
|
FileUploadProgressResponse,
|
|
10
11
|
FileUploadCompleteResponse
|
|
11
12
|
} from '@core/types/types'
|
|
12
13
|
|
|
14
|
+
// ๐ Magic bytes mapping for content validation
|
|
15
|
+
// Validates actual file content, not just the MIME type header
|
|
16
|
+
const MAGIC_BYTES: Record<string, { bytes: number[]; offset?: number }[]> = {
|
|
17
|
+
'image/jpeg': [{ bytes: [0xFF, 0xD8, 0xFF] }],
|
|
18
|
+
'image/png': [{ bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] }],
|
|
19
|
+
'image/gif': [
|
|
20
|
+
{ bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] }, // GIF87a
|
|
21
|
+
{ bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] }, // GIF89a
|
|
22
|
+
],
|
|
23
|
+
'image/webp': [
|
|
24
|
+
{ bytes: [0x52, 0x49, 0x46, 0x46], offset: 0 }, // RIFF header
|
|
25
|
+
// Byte 8-11 should be WEBP, checked separately
|
|
26
|
+
],
|
|
27
|
+
'application/pdf': [{ bytes: [0x25, 0x50, 0x44, 0x46] }], // %PDF
|
|
28
|
+
'application/zip': [
|
|
29
|
+
{ bytes: [0x50, 0x4B, 0x03, 0x04] }, // PK\x03\x04
|
|
30
|
+
{ bytes: [0x50, 0x4B, 0x05, 0x06] }, // Empty archive
|
|
31
|
+
],
|
|
32
|
+
'application/gzip': [{ bytes: [0x1F, 0x8B] }],
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
export class FileUploadManager {
|
|
14
36
|
private activeUploads = new Map<string, ActiveUpload>()
|
|
15
|
-
private readonly maxUploadSize =
|
|
37
|
+
private readonly maxUploadSize = 50 * 1024 * 1024 // ๐ 50MB max (reduced from 500MB)
|
|
16
38
|
private readonly chunkTimeout = 30000 // 30 seconds timeout per chunk
|
|
17
|
-
|
|
39
|
+
// ๐ Per-user upload quota tracking
|
|
40
|
+
private userUploadBytes = new Map<string, number>() // userId -> total bytes uploaded
|
|
41
|
+
private readonly maxBytesPerUser = 500 * 1024 * 1024 // ๐ 500MB per user total
|
|
42
|
+
private readonly quotaResetInterval = 24 * 60 * 60 * 1000 // Reset quotas daily
|
|
43
|
+
// ๐ Default allowed MIME types - safe file types only
|
|
44
|
+
private readonly allowedTypes: string[] = [
|
|
45
|
+
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
|
46
|
+
'application/pdf',
|
|
47
|
+
'text/plain', 'text/csv', 'text/markdown',
|
|
48
|
+
'application/json',
|
|
49
|
+
'application/zip', 'application/gzip',
|
|
50
|
+
]
|
|
51
|
+
// ๐ Blocked file extensions that could be dangerous
|
|
52
|
+
private readonly blockedExtensions: Set<string> = new Set([
|
|
53
|
+
'.exe', '.bat', '.cmd', '.com', '.msi', '.scr', '.pif',
|
|
54
|
+
'.sh', '.bash', '.zsh', '.csh',
|
|
55
|
+
'.ps1', '.psm1', '.psd1',
|
|
56
|
+
'.vbs', '.vbe', '.js', '.jse', '.wsf', '.wsh',
|
|
57
|
+
'.dll', '.sys', '.drv', '.so', '.dylib',
|
|
58
|
+
])
|
|
18
59
|
|
|
19
60
|
constructor() {
|
|
20
61
|
// Cleanup stale uploads every 5 minutes
|
|
21
62
|
setInterval(() => this.cleanupStaleUploads(), 5 * 60 * 1000)
|
|
63
|
+
// ๐ Reset per-user upload quotas daily
|
|
64
|
+
setInterval(() => this.resetUploadQuotas(), this.quotaResetInterval)
|
|
22
65
|
}
|
|
23
66
|
|
|
24
|
-
async startUpload(message: FileUploadStartMessage): Promise<{ success: boolean; error?: string }> {
|
|
67
|
+
async startUpload(message: FileUploadStartMessage, userId?: string): Promise<{ success: boolean; error?: string }> {
|
|
25
68
|
try {
|
|
26
69
|
const { uploadId, componentId, filename, fileType, fileSize, chunkSize = 64 * 1024 } = message
|
|
27
70
|
|
|
28
|
-
// Validate file size
|
|
71
|
+
// ๐ Validate file size
|
|
29
72
|
if (fileSize > this.maxUploadSize) {
|
|
30
73
|
throw new Error(`File too large: ${fileSize} bytes. Max: ${this.maxUploadSize} bytes`)
|
|
31
74
|
}
|
|
32
75
|
|
|
76
|
+
// ๐ Per-user upload quota check
|
|
77
|
+
if (userId) {
|
|
78
|
+
const currentUsage = this.userUploadBytes.get(userId) || 0
|
|
79
|
+
if (currentUsage + fileSize > this.maxBytesPerUser) {
|
|
80
|
+
throw new Error(`Upload quota exceeded for user. Used: ${currentUsage} bytes, limit: ${this.maxBytesPerUser} bytes`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ๐ Validate MIME type against allowlist
|
|
85
|
+
if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(fileType)) {
|
|
86
|
+
throw new Error(`File type not allowed: ${fileType}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ๐ Validate filename - sanitize and check extension
|
|
90
|
+
const safeBase = basename(filename) // Strip any path traversal
|
|
91
|
+
const ext = extname(safeBase).toLowerCase()
|
|
92
|
+
if (this.blockedExtensions.has(ext)) {
|
|
93
|
+
throw new Error(`File extension not allowed: ${ext}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ๐ Double extension bypass prevention (e.g., malware.exe.jpg)
|
|
97
|
+
const parts = safeBase.split('.')
|
|
98
|
+
if (parts.length > 2) {
|
|
99
|
+
// Check all intermediate extensions
|
|
100
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
101
|
+
const intermediateExt = '.' + parts[i].toLowerCase()
|
|
102
|
+
if (this.blockedExtensions.has(intermediateExt)) {
|
|
103
|
+
throw new Error(`Suspicious double extension detected: ${intermediateExt} in ${safeBase}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ๐ Validate filename length
|
|
109
|
+
if (safeBase.length > 255) {
|
|
110
|
+
throw new Error('Filename too long')
|
|
111
|
+
}
|
|
112
|
+
|
|
33
113
|
// Check if upload already exists
|
|
34
114
|
if (this.activeUploads.has(uploadId)) {
|
|
35
115
|
throw new Error(`Upload ${uploadId} already in progress`)
|
|
@@ -54,13 +134,20 @@ export class FileUploadManager {
|
|
|
54
134
|
|
|
55
135
|
this.activeUploads.set(uploadId, upload)
|
|
56
136
|
|
|
57
|
-
|
|
137
|
+
// ๐ Reserve quota for this upload
|
|
138
|
+
if (userId) {
|
|
139
|
+
const currentUsage = this.userUploadBytes.get(userId) || 0
|
|
140
|
+
this.userUploadBytes.set(userId, currentUsage + fileSize)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
liveLog('messages', componentId, '๐ค Upload started:', {
|
|
58
144
|
uploadId,
|
|
59
145
|
componentId,
|
|
60
146
|
filename,
|
|
61
147
|
fileType,
|
|
62
148
|
fileSize,
|
|
63
|
-
totalChunks
|
|
149
|
+
totalChunks,
|
|
150
|
+
userId: userId || 'anonymous'
|
|
64
151
|
})
|
|
65
152
|
|
|
66
153
|
return { success: true }
|
|
@@ -87,7 +174,7 @@ export class FileUploadManager {
|
|
|
87
174
|
|
|
88
175
|
// Check if chunk already received
|
|
89
176
|
if (upload.receivedChunks.has(chunkIndex)) {
|
|
90
|
-
|
|
177
|
+
liveLog('messages', upload.componentId, `๐ฆ Chunk ${chunkIndex} already received for upload ${uploadId}`)
|
|
91
178
|
} else {
|
|
92
179
|
// Store chunk data - use binary data if available, otherwise use base64 string
|
|
93
180
|
let chunkBytes: number
|
|
@@ -105,7 +192,7 @@ export class FileUploadManager {
|
|
|
105
192
|
upload.lastChunkTime = Date.now()
|
|
106
193
|
upload.bytesReceived += chunkBytes
|
|
107
194
|
|
|
108
|
-
|
|
195
|
+
liveLog('messages', upload.componentId, `๐ฆ Received chunk ${chunkIndex + 1}/${totalChunks} for upload ${uploadId} (${chunkBytes} bytes, total: ${upload.bytesReceived}/${upload.fileSize})${binaryData ? ' [binary]' : ' [base64]'}`)
|
|
109
196
|
}
|
|
110
197
|
|
|
111
198
|
// Calculate progress based on actual bytes received (supports adaptive chunking)
|
|
@@ -114,7 +201,7 @@ export class FileUploadManager {
|
|
|
114
201
|
|
|
115
202
|
// Log completion status (but don't finalize until COMPLETE message)
|
|
116
203
|
if (upload.bytesReceived >= upload.fileSize) {
|
|
117
|
-
|
|
204
|
+
liveLog('messages', upload.componentId, `โ
All bytes received for upload ${uploadId} (${upload.bytesReceived}/${upload.fileSize}), waiting for COMPLETE message`)
|
|
118
205
|
}
|
|
119
206
|
|
|
120
207
|
return {
|
|
@@ -137,7 +224,7 @@ export class FileUploadManager {
|
|
|
137
224
|
|
|
138
225
|
private async finalizeUpload(upload: ActiveUpload): Promise<void> {
|
|
139
226
|
try {
|
|
140
|
-
|
|
227
|
+
liveLog('messages', upload.componentId, `โ
Upload completed: ${upload.uploadId}`)
|
|
141
228
|
|
|
142
229
|
// Assemble file from chunks
|
|
143
230
|
const fileUrl = await this.assembleFile(upload)
|
|
@@ -160,7 +247,7 @@ export class FileUploadManager {
|
|
|
160
247
|
throw new Error(`Upload ${uploadId} not found`)
|
|
161
248
|
}
|
|
162
249
|
|
|
163
|
-
|
|
250
|
+
liveLog('messages', upload.componentId, `โ
Upload completion requested: ${uploadId}`)
|
|
164
251
|
|
|
165
252
|
// Validate bytes received (supports adaptive chunking)
|
|
166
253
|
if (upload.bytesReceived !== upload.fileSize) {
|
|
@@ -168,8 +255,10 @@ export class FileUploadManager {
|
|
|
168
255
|
throw new Error(`Incomplete upload: received ${upload.bytesReceived}/${upload.fileSize} bytes (${bytesShort} bytes short)`)
|
|
169
256
|
}
|
|
170
257
|
|
|
171
|
-
|
|
258
|
+
// ๐ Content validation: verify file magic bytes match claimed MIME type
|
|
259
|
+
this.validateContentMagicBytes(upload)
|
|
172
260
|
|
|
261
|
+
liveLog('messages', upload.componentId, `โ
Upload validation passed: ${uploadId} (${upload.bytesReceived} bytes)`)
|
|
173
262
|
|
|
174
263
|
// Assemble file from chunks
|
|
175
264
|
const fileUrl = await this.assembleFile(upload)
|
|
@@ -209,11 +298,9 @@ export class FileUploadManager {
|
|
|
209
298
|
await mkdir(uploadsDir, { recursive: true })
|
|
210
299
|
}
|
|
211
300
|
|
|
212
|
-
// Generate unique filename
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const baseName = upload.filename.replace(extension, '')
|
|
216
|
-
const safeFilename = `${baseName}_${timestamp}${extension}`
|
|
301
|
+
// ๐ Generate secure unique filename using UUID (prevents path traversal and name collisions)
|
|
302
|
+
const extension = extname(basename(upload.filename)).toLowerCase()
|
|
303
|
+
const safeFilename = `${crypto.randomUUID()}${extension}`
|
|
217
304
|
const filePath = join(uploadsDir, safeFilename)
|
|
218
305
|
|
|
219
306
|
// Assemble chunks in order
|
|
@@ -234,7 +321,7 @@ export class FileUploadManager {
|
|
|
234
321
|
const fileBuffer = Buffer.concat(chunks)
|
|
235
322
|
await writeFile(filePath, fileBuffer)
|
|
236
323
|
|
|
237
|
-
|
|
324
|
+
liveLog('messages', upload.componentId, `๐ File assembled: ${filePath}`)
|
|
238
325
|
return `/uploads/${safeFilename}`
|
|
239
326
|
|
|
240
327
|
} catch (error) {
|
|
@@ -257,11 +344,88 @@ export class FileUploadManager {
|
|
|
257
344
|
|
|
258
345
|
for (const uploadId of staleUploads) {
|
|
259
346
|
this.activeUploads.delete(uploadId)
|
|
260
|
-
|
|
347
|
+
liveLog('messages', null, `๐งน Cleaned up stale upload: ${uploadId}`)
|
|
261
348
|
}
|
|
262
349
|
|
|
263
350
|
if (staleUploads.length > 0) {
|
|
264
|
-
|
|
351
|
+
liveLog('messages', null, `๐งน Cleaned up ${staleUploads.length} stale uploads`)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* ๐ Validate that the first bytes of the uploaded file match the claimed MIME type.
|
|
357
|
+
* Prevents attacks where a malicious file is uploaded with a fake MIME type header.
|
|
358
|
+
*/
|
|
359
|
+
private validateContentMagicBytes(upload: ActiveUpload): void {
|
|
360
|
+
const expectedSignatures = MAGIC_BYTES[upload.fileType]
|
|
361
|
+
if (!expectedSignatures) {
|
|
362
|
+
// No magic bytes defined for this type (text types, SVG, JSON, etc.) - skip binary check
|
|
363
|
+
// For text types, we could add content sniffing but it's less critical
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Get the first chunk to read magic bytes
|
|
368
|
+
const firstChunk = upload.receivedChunks.get(0)
|
|
369
|
+
if (!firstChunk) {
|
|
370
|
+
throw new Error('Cannot validate file content: first chunk missing')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const headerBuffer = Buffer.isBuffer(firstChunk)
|
|
374
|
+
? firstChunk
|
|
375
|
+
: Buffer.from(firstChunk, 'base64')
|
|
376
|
+
|
|
377
|
+
// Check if any of the expected signatures match
|
|
378
|
+
let matched = false
|
|
379
|
+
for (const sig of expectedSignatures) {
|
|
380
|
+
const offset = sig.offset ?? 0
|
|
381
|
+
if (headerBuffer.length < offset + sig.bytes.length) {
|
|
382
|
+
continue // File too small for this signature
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let sigMatches = true
|
|
386
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
387
|
+
if (headerBuffer[offset + i] !== sig.bytes[i]) {
|
|
388
|
+
sigMatches = false
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (sigMatches) {
|
|
394
|
+
matched = true
|
|
395
|
+
break
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!matched) {
|
|
400
|
+
liveWarn('messages', upload.componentId, `๐ Content validation failed for upload ${upload.uploadId}: ` +
|
|
401
|
+
`claimed type ${upload.fileType} does not match file magic bytes`)
|
|
402
|
+
throw new Error(
|
|
403
|
+
`File content does not match claimed type '${upload.fileType}'. ` +
|
|
404
|
+
`The file may be disguised as a different format.`
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* ๐ Reset per-user upload quotas (called periodically)
|
|
411
|
+
*/
|
|
412
|
+
private resetUploadQuotas(): void {
|
|
413
|
+
const userCount = this.userUploadBytes.size
|
|
414
|
+
this.userUploadBytes.clear()
|
|
415
|
+
if (userCount > 0) {
|
|
416
|
+
liveLog('messages', null, `๐ Reset upload quotas for ${userCount} users`)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get per-user upload usage
|
|
422
|
+
*/
|
|
423
|
+
getUserUploadUsage(userId: string): { used: number; limit: number; remaining: number } {
|
|
424
|
+
const used = this.userUploadBytes.get(userId) || 0
|
|
425
|
+
return {
|
|
426
|
+
used,
|
|
427
|
+
limit: this.maxBytesPerUser,
|
|
428
|
+
remaining: Math.max(0, this.maxBytesPerUser - used)
|
|
265
429
|
}
|
|
266
430
|
}
|
|
267
431
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Advanced performance monitoring, metrics collection, and optimization suggestions
|
|
3
3
|
|
|
4
4
|
import { EventEmitter } from 'events'
|
|
5
|
+
import { liveLog, liveWarn } from './LiveLogger'
|
|
5
6
|
|
|
6
7
|
export interface PerformanceMetrics {
|
|
7
8
|
componentId: string
|
|
@@ -279,7 +280,7 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
279
280
|
this.alerts.set(componentId, [])
|
|
280
281
|
this.suggestions.set(componentId, [])
|
|
281
282
|
|
|
282
|
-
|
|
283
|
+
liveLog('performance', componentId, `๐ Performance monitoring initialized for component: ${componentId}`)
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
/**
|
|
@@ -562,7 +563,7 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
562
563
|
|
|
563
564
|
this.alertCooldowns.set(alertKey, now)
|
|
564
565
|
|
|
565
|
-
|
|
566
|
+
liveWarn('performance', componentId, `โ ๏ธ Performance alert [${type}]: ${message}`)
|
|
566
567
|
this.emit('performanceAlert', alert)
|
|
567
568
|
}
|
|
568
569
|
|
|
@@ -695,7 +696,7 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
695
696
|
suggestions.push(suggestion)
|
|
696
697
|
this.suggestions.set(componentId, suggestions)
|
|
697
698
|
|
|
698
|
-
|
|
699
|
+
liveLog('performance', componentId, `๐ก Optimization suggestion for ${componentId}: ${title}`)
|
|
699
700
|
this.emit('optimizationSuggestion', suggestion)
|
|
700
701
|
}
|
|
701
702
|
|
|
@@ -863,7 +864,7 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
863
864
|
const alert = alerts.find(a => a.id === alertId)
|
|
864
865
|
if (alert) {
|
|
865
866
|
alert.resolved = true
|
|
866
|
-
|
|
867
|
+
liveLog('performance', null, `โ
Alert resolved: ${alertId}`)
|
|
867
868
|
return true
|
|
868
869
|
}
|
|
869
870
|
}
|
|
@@ -889,7 +890,7 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
889
890
|
this.suggestions.set(componentId, validSuggestions)
|
|
890
891
|
}
|
|
891
892
|
|
|
892
|
-
|
|
893
|
+
liveLog('performance', null, '๐งน Performance monitoring data cleanup completed')
|
|
893
894
|
}
|
|
894
895
|
|
|
895
896
|
/**
|
|
@@ -900,14 +901,14 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
900
901
|
this.alerts.delete(componentId)
|
|
901
902
|
this.suggestions.delete(componentId)
|
|
902
903
|
|
|
903
|
-
|
|
904
|
+
liveLog('performance', componentId, `๐ Performance monitoring removed for component: ${componentId}`)
|
|
904
905
|
}
|
|
905
906
|
|
|
906
907
|
/**
|
|
907
908
|
* Shutdown performance monitor
|
|
908
909
|
*/
|
|
909
910
|
shutdown(): void {
|
|
910
|
-
|
|
911
|
+
liveLog('performance', null, '๐ Shutting down Performance Monitor...')
|
|
911
912
|
|
|
912
913
|
if (this.dashboardUpdateInterval) {
|
|
913
914
|
clearInterval(this.dashboardUpdateInterval)
|
|
@@ -922,7 +923,7 @@ export class LiveComponentPerformanceMonitor extends EventEmitter {
|
|
|
922
923
|
this.suggestions.clear()
|
|
923
924
|
this.alertCooldowns.clear()
|
|
924
925
|
|
|
925
|
-
|
|
926
|
+
liveLog('performance', null, 'โ
Performance Monitor shutdown complete')
|
|
926
927
|
}
|
|
927
928
|
}
|
|
928
929
|
|