@goscribe/server 1.0.11 → 1.1.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/ANALYSIS_PROGRESS_SPEC.md +463 -0
- package/PROGRESS_QUICK_REFERENCE.md +239 -0
- package/dist/lib/ai-session.d.ts +20 -9
- package/dist/lib/ai-session.js +316 -80
- package/dist/lib/auth.d.ts +35 -2
- package/dist/lib/auth.js +88 -15
- package/dist/lib/env.d.ts +32 -0
- package/dist/lib/env.js +46 -0
- package/dist/lib/errors.d.ts +33 -0
- package/dist/lib/errors.js +78 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +9 -11
- package/dist/lib/logger.d.ts +62 -0
- package/dist/lib/logger.js +342 -0
- package/dist/lib/podcast-prompts.d.ts +43 -0
- package/dist/lib/podcast-prompts.js +135 -0
- package/dist/lib/pusher.d.ts +1 -0
- package/dist/lib/pusher.js +14 -2
- package/dist/lib/storage.d.ts +3 -3
- package/dist/lib/storage.js +51 -47
- package/dist/lib/validation.d.ts +51 -0
- package/dist/lib/validation.js +64 -0
- package/dist/routers/_app.d.ts +697 -111
- package/dist/routers/_app.js +5 -0
- package/dist/routers/auth.d.ts +11 -1
- package/dist/routers/chat.d.ts +11 -1
- package/dist/routers/flashcards.d.ts +205 -6
- package/dist/routers/flashcards.js +144 -66
- package/dist/routers/members.d.ts +165 -0
- package/dist/routers/members.js +531 -0
- package/dist/routers/podcast.d.ts +78 -63
- package/dist/routers/podcast.js +330 -393
- package/dist/routers/studyguide.d.ts +11 -1
- package/dist/routers/worksheets.d.ts +124 -13
- package/dist/routers/worksheets.js +123 -50
- package/dist/routers/workspace.d.ts +213 -26
- package/dist/routers/workspace.js +303 -181
- package/dist/server.js +12 -4
- package/dist/services/flashcard-progress.service.d.ts +183 -0
- package/dist/services/flashcard-progress.service.js +383 -0
- package/dist/services/flashcard.service.d.ts +183 -0
- package/dist/services/flashcard.service.js +224 -0
- package/dist/services/podcast-segment-reorder.d.ts +0 -0
- package/dist/services/podcast-segment-reorder.js +107 -0
- package/dist/services/podcast.service.d.ts +0 -0
- package/dist/services/podcast.service.js +326 -0
- package/dist/services/worksheet.service.d.ts +0 -0
- package/dist/services/worksheet.service.js +295 -0
- package/dist/trpc.d.ts +13 -2
- package/dist/trpc.js +55 -6
- package/dist/types/index.d.ts +126 -0
- package/dist/types/index.js +1 -0
- package/package.json +3 -2
- package/prisma/schema.prisma +142 -4
- package/src/lib/ai-session.ts +356 -85
- package/src/lib/auth.ts +113 -19
- package/src/lib/env.ts +59 -0
- package/src/lib/errors.ts +92 -0
- package/src/lib/inference.ts +11 -11
- package/src/lib/logger.ts +405 -0
- package/src/lib/pusher.ts +15 -3
- package/src/lib/storage.ts +56 -51
- package/src/lib/validation.ts +75 -0
- package/src/routers/_app.ts +5 -0
- package/src/routers/chat.ts +2 -23
- package/src/routers/flashcards.ts +108 -24
- package/src/routers/members.ts +586 -0
- package/src/routers/podcast.ts +385 -420
- package/src/routers/worksheets.ts +117 -35
- package/src/routers/workspace.ts +328 -195
- package/src/server.ts +13 -4
- package/src/services/flashcard-progress.service.ts +541 -0
- package/src/trpc.ts +59 -6
- package/src/types/index.ts +165 -0
- package/AUTH_FRONTEND_SPEC.md +0 -21
- package/CHAT_FRONTEND_SPEC.md +0 -474
- package/DATABASE_SETUP.md +0 -165
- package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
- package/PODCAST_FRONTEND_SPEC.md +0 -595
- package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
- package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
- package/WORKSPACE_FRONTEND_SPEC.md +0 -47
- package/test-ai-integration.js +0 -134
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { createWriteStream, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export enum LogLevel {
|
|
5
|
+
ERROR = 0,
|
|
6
|
+
WARN = 1,
|
|
7
|
+
INFO = 2,
|
|
8
|
+
DEBUG = 3,
|
|
9
|
+
TRACE = 4,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Icons and colors for different log levels
|
|
13
|
+
const LOG_STYLES = {
|
|
14
|
+
[LogLevel.ERROR]: {
|
|
15
|
+
icon: '❌',
|
|
16
|
+
color: '\x1b[31m', // Red
|
|
17
|
+
bgColor: '\x1b[41m', // Red background
|
|
18
|
+
bold: '\x1b[1m',
|
|
19
|
+
},
|
|
20
|
+
[LogLevel.WARN]: {
|
|
21
|
+
icon: '⚠️ ',
|
|
22
|
+
color: '\x1b[33m', // Yellow
|
|
23
|
+
bgColor: '\x1b[43m', // Yellow background
|
|
24
|
+
bold: '\x1b[1m',
|
|
25
|
+
},
|
|
26
|
+
[LogLevel.INFO]: {
|
|
27
|
+
icon: 'ℹ️ ',
|
|
28
|
+
color: '\x1b[36m', // Cyan
|
|
29
|
+
bgColor: '\x1b[46m', // Cyan background
|
|
30
|
+
bold: '\x1b[1m',
|
|
31
|
+
},
|
|
32
|
+
[LogLevel.DEBUG]: {
|
|
33
|
+
icon: '🐛',
|
|
34
|
+
color: '\x1b[35m', // Magenta
|
|
35
|
+
bgColor: '\x1b[45m', // Magenta background
|
|
36
|
+
bold: '\x1b[1m',
|
|
37
|
+
},
|
|
38
|
+
[LogLevel.TRACE]: {
|
|
39
|
+
icon: '🔍',
|
|
40
|
+
color: '\x1b[37m', // White
|
|
41
|
+
bgColor: '\x1b[47m', // White background
|
|
42
|
+
bold: '\x1b[1m',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Context icons for common services
|
|
47
|
+
const CONTEXT_ICONS: Record<string, string> = {
|
|
48
|
+
'SERVER': '🖥️ ',
|
|
49
|
+
'HTTP': '🌐',
|
|
50
|
+
'API': '🔌',
|
|
51
|
+
'AUTH': '🔐',
|
|
52
|
+
'DATABASE': '🗄️ ',
|
|
53
|
+
'DB': '🗄️ ',
|
|
54
|
+
'TRPC': '⚡',
|
|
55
|
+
'WORKSPACE': '📁',
|
|
56
|
+
'WORKSHEET': '📝',
|
|
57
|
+
'FLASHCARD': '🃏',
|
|
58
|
+
'STUDYGUIDE': '📚',
|
|
59
|
+
'PODCAST': '🎙️ ',
|
|
60
|
+
'MEETING': '🤝',
|
|
61
|
+
'CHAT': '💬',
|
|
62
|
+
'FILE': '📄',
|
|
63
|
+
'STORAGE': '💾',
|
|
64
|
+
'CACHE': '⚡',
|
|
65
|
+
'MIDDLEWARE': '🔧',
|
|
66
|
+
'PERFORMANCE': '⚡',
|
|
67
|
+
'SECURITY': '🛡️ ',
|
|
68
|
+
'VALIDATION': '✅',
|
|
69
|
+
'ERROR': '❌',
|
|
70
|
+
'SUCCESS': '✅',
|
|
71
|
+
'LOGGER': '📋',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface LogEntry {
|
|
75
|
+
timestamp: string;
|
|
76
|
+
level: string;
|
|
77
|
+
message: string;
|
|
78
|
+
context?: string;
|
|
79
|
+
metadata?: Record<string, any>;
|
|
80
|
+
error?: {
|
|
81
|
+
name: string;
|
|
82
|
+
message: string;
|
|
83
|
+
stack?: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface LoggerConfig {
|
|
88
|
+
level: LogLevel;
|
|
89
|
+
enableConsole: boolean;
|
|
90
|
+
enableFile: boolean;
|
|
91
|
+
logDir?: string;
|
|
92
|
+
maxFileSize?: number;
|
|
93
|
+
maxFiles?: number;
|
|
94
|
+
format?: 'json' | 'pretty';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class Logger {
|
|
98
|
+
private config: LoggerConfig;
|
|
99
|
+
private logStream?: NodeJS.WritableStream;
|
|
100
|
+
|
|
101
|
+
constructor(config: Partial<LoggerConfig> = {}) {
|
|
102
|
+
this.config = {
|
|
103
|
+
level: LogLevel.INFO,
|
|
104
|
+
enableConsole: true,
|
|
105
|
+
enableFile: false,
|
|
106
|
+
logDir: './logs',
|
|
107
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
108
|
+
maxFiles: 5,
|
|
109
|
+
format: 'pretty',
|
|
110
|
+
...config,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (this.config.enableFile) {
|
|
114
|
+
this.setupFileLogging();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private setupFileLogging(): void {
|
|
119
|
+
if (!this.config.logDir) return;
|
|
120
|
+
|
|
121
|
+
// Ensure log directory exists
|
|
122
|
+
if (!existsSync(this.config.logDir)) {
|
|
123
|
+
mkdirSync(this.config.logDir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const logFile = join(this.config.logDir, `app-${new Date().toISOString().split('T')[0]}.log`);
|
|
127
|
+
this.logStream = createWriteStream(logFile, { flags: 'a' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private shouldLog(level: LogLevel): boolean {
|
|
131
|
+
return level <= this.config.level;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private formatLogEntry(entry: LogEntry): string {
|
|
135
|
+
if (this.config.format === 'json') {
|
|
136
|
+
return JSON.stringify(entry) + '\n';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Pretty format with enhanced styling
|
|
140
|
+
const timestamp = this.formatTimestamp(entry.timestamp);
|
|
141
|
+
const level = this.formatLevel(entry.level);
|
|
142
|
+
const context = entry.context ? this.formatContext(entry.context) : '';
|
|
143
|
+
const metadata = entry.metadata ? this.formatMetadata(entry.metadata) : '';
|
|
144
|
+
const error = entry.error ? this.formatError(entry.error) : '';
|
|
145
|
+
|
|
146
|
+
return `${timestamp} ${level} ${context}${entry.message}${metadata}${error}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private formatLevel(level: string): string {
|
|
150
|
+
const levelNum = LogLevel[level as keyof typeof LogLevel];
|
|
151
|
+
const style = LOG_STYLES[levelNum];
|
|
152
|
+
return `${style.color}${style.bold}${level.padEnd(5)}\x1b[0m`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private formatTimestamp(timestamp: string): string {
|
|
156
|
+
const date = new Date(timestamp);
|
|
157
|
+
const time = date.toLocaleTimeString('en-US', {
|
|
158
|
+
hour12: false,
|
|
159
|
+
hour: '2-digit',
|
|
160
|
+
minute: '2-digit',
|
|
161
|
+
second: '2-digit',
|
|
162
|
+
});
|
|
163
|
+
return `\x1b[90m${time}\x1b[0m`; // Gray color
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private formatContext(context: string): string {
|
|
167
|
+
const icon = CONTEXT_ICONS[context.toUpperCase()] || '📦';
|
|
168
|
+
return `\x1b[94m${icon}${context}\x1b[0m `; // Blue color
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private formatMetadata(metadata: Record<string, any>): string {
|
|
172
|
+
const entries = Object.entries(metadata)
|
|
173
|
+
.map(([key, value]) => {
|
|
174
|
+
const formattedValue = this.formatValue(value);
|
|
175
|
+
return `\x1b[93m${key}\x1b[0m=\x1b[96m${formattedValue}\x1b[0m`;
|
|
176
|
+
})
|
|
177
|
+
.join(' \x1b[90m|\x1b[0m ');
|
|
178
|
+
|
|
179
|
+
return entries ? `\n \x1b[90m└─\x1b[0m \x1b[90m{\x1b[0m ${entries} \x1b[90m}\x1b[0m` : '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private formatValue(value: any): string {
|
|
183
|
+
if (value === null) return '\x1b[90mnull\x1b[0m';
|
|
184
|
+
if (value === undefined) return '\x1b[90mundefined\x1b[0m';
|
|
185
|
+
if (typeof value === 'boolean') return value ? '\x1b[92mtrue\x1b[0m' : '\x1b[91mfalse\x1b[0m';
|
|
186
|
+
if (typeof value === 'number') return `\x1b[95m${value}\x1b[0m`;
|
|
187
|
+
if (typeof value === 'string') return `\x1b[96m"${value}"\x1b[0m`;
|
|
188
|
+
if (typeof value === 'object') {
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
const items = value.map(item => this.formatValue(item)).join('\x1b[90m, \x1b[0m');
|
|
191
|
+
return `\x1b[90m[\x1b[0m${items}\x1b[90m]\x1b[0m`;
|
|
192
|
+
}
|
|
193
|
+
const objEntries = Object.entries(value)
|
|
194
|
+
.map(([k, v]) => `\x1b[93m${k}\x1b[0m:\x1b[96m${this.formatValue(v)}\x1b[0m`)
|
|
195
|
+
.join('\x1b[90m, \x1b[0m');
|
|
196
|
+
return `\x1b[90m{\x1b[0m${objEntries}\x1b[90m}\x1b[0m`;
|
|
197
|
+
}
|
|
198
|
+
return `\x1b[96m${String(value)}\x1b[0m`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private formatError(error: { name: string; message: string; stack?: string }): string {
|
|
202
|
+
let errorStr = `\n \x1b[90m└─\x1b[0m \x1b[31m❌ \x1b[93m${error.name}\x1b[0m: \x1b[91m${error.message}\x1b[0m`;
|
|
203
|
+
|
|
204
|
+
if (error.stack) {
|
|
205
|
+
const stackLines = error.stack.split('\n').slice(1, 4); // Show first 3 stack lines
|
|
206
|
+
errorStr += `\n \x1b[90m└─ Stack:\x1b[0m`;
|
|
207
|
+
stackLines.forEach((line, index) => {
|
|
208
|
+
const isLast = index === stackLines.length - 1;
|
|
209
|
+
const connector = isLast ? '└─' : '├─';
|
|
210
|
+
errorStr += `\n \x1b[90m${connector} \x1b[0m\x1b[90m${line.trim()}\x1b[0m`;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return errorStr;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private log(level: LogLevel, message: string, context?: string, metadata?: Record<string, any>, error?: Error): void {
|
|
218
|
+
if (!this.shouldLog(level)) return;
|
|
219
|
+
|
|
220
|
+
const entry: LogEntry = {
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
level: LogLevel[level],
|
|
223
|
+
message,
|
|
224
|
+
context,
|
|
225
|
+
metadata,
|
|
226
|
+
error: error ? {
|
|
227
|
+
name: error.name,
|
|
228
|
+
message: error.message,
|
|
229
|
+
stack: error.stack,
|
|
230
|
+
} : undefined,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const formattedLog = this.formatLogEntry(entry);
|
|
234
|
+
|
|
235
|
+
if (this.config.enableConsole) {
|
|
236
|
+
// Enhanced console output with icons and colors
|
|
237
|
+
const style = LOG_STYLES[level];
|
|
238
|
+
const reset = '\x1b[0m';
|
|
239
|
+
|
|
240
|
+
// Create a beautiful log line with proper spacing and colors
|
|
241
|
+
const logLine = `${style.color}${style.icon} ${formattedLog}${reset}`;
|
|
242
|
+
|
|
243
|
+
console.log(logLine);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (this.config.enableFile && this.logStream) {
|
|
247
|
+
this.logStream.write(formattedLog + '\n');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
error(message: string, context?: string, metadata?: Record<string, any>, error?: Error): void {
|
|
252
|
+
this.log(LogLevel.ERROR, message, context, metadata, error);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
warn(message: string, context?: string, metadata?: Record<string, any>): void {
|
|
256
|
+
this.log(LogLevel.WARN, message, context, metadata);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
info(message: string, context?: string, metadata?: Record<string, any>): void {
|
|
260
|
+
this.log(LogLevel.INFO, message, context, metadata);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
debug(message: string, context?: string, metadata?: Record<string, any>): void {
|
|
264
|
+
this.log(LogLevel.DEBUG, message, context, metadata);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
trace(message: string, context?: string, metadata?: Record<string, any>): void {
|
|
268
|
+
this.log(LogLevel.TRACE, message, context, metadata);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Convenience methods for common use cases
|
|
272
|
+
http(method: string, url: string, statusCode: number, responseTime?: number, context?: string): void {
|
|
273
|
+
const statusIcon = this.getHttpStatusIcon(statusCode);
|
|
274
|
+
const responseTimeStr = responseTime ? `${responseTime}ms` : undefined;
|
|
275
|
+
|
|
276
|
+
const metadata = {
|
|
277
|
+
method,
|
|
278
|
+
url,
|
|
279
|
+
statusCode,
|
|
280
|
+
responseTime: responseTimeStr,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
this.info(`${statusIcon} ${method} ${url} - ${statusCode}`, context || 'HTTP', metadata);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private getHttpStatusIcon(statusCode: number): string {
|
|
287
|
+
if (statusCode >= 200 && statusCode < 300) return '✅';
|
|
288
|
+
if (statusCode >= 300 && statusCode < 400) return '↩️ ';
|
|
289
|
+
if (statusCode >= 400 && statusCode < 500) return '⚠️ ';
|
|
290
|
+
if (statusCode >= 500) return '❌';
|
|
291
|
+
return '❓';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
database(operation: string, table: string, duration?: number, context?: string): void {
|
|
295
|
+
const operationIcon = this.getDatabaseOperationIcon(operation);
|
|
296
|
+
const durationStr = duration ? `${duration}ms` : undefined;
|
|
297
|
+
|
|
298
|
+
const metadata = {
|
|
299
|
+
operation,
|
|
300
|
+
table,
|
|
301
|
+
duration: durationStr,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
this.debug(`${operationIcon} ${operation} on ${table}`, context || 'DATABASE', metadata);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private getDatabaseOperationIcon(operation: string): string {
|
|
308
|
+
const op = operation.toUpperCase();
|
|
309
|
+
if (op.includes('SELECT')) return '🔍';
|
|
310
|
+
if (op.includes('INSERT')) return '➕';
|
|
311
|
+
if (op.includes('UPDATE')) return '✏️ ';
|
|
312
|
+
if (op.includes('DELETE')) return '🗑️ ';
|
|
313
|
+
if (op.includes('CREATE')) return '🏗️ ';
|
|
314
|
+
if (op.includes('DROP')) return '💥';
|
|
315
|
+
return '🗄️ ';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
auth(action: string, userId?: string, context?: string): void {
|
|
319
|
+
const metadata = {
|
|
320
|
+
action,
|
|
321
|
+
userId,
|
|
322
|
+
};
|
|
323
|
+
this.info(`Auth ${action}`, context, metadata);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
trpc(procedure: string, input?: any, output?: any, duration?: number, context?: string): void {
|
|
327
|
+
const metadata = {
|
|
328
|
+
procedure,
|
|
329
|
+
input: input ? JSON.stringify(input) : undefined,
|
|
330
|
+
output: output ? JSON.stringify(output) : undefined,
|
|
331
|
+
duration: duration ? `${duration}ms` : undefined,
|
|
332
|
+
};
|
|
333
|
+
this.debug(`tRPC ${procedure}`, context, metadata);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Method to update configuration at runtime
|
|
337
|
+
updateConfig(newConfig: Partial<LoggerConfig>): void {
|
|
338
|
+
this.config = { ...this.config, ...newConfig };
|
|
339
|
+
|
|
340
|
+
if (newConfig.enableFile && !this.logStream) {
|
|
341
|
+
this.setupFileLogging();
|
|
342
|
+
} else if (!newConfig.enableFile && this.logStream) {
|
|
343
|
+
this.logStream.end();
|
|
344
|
+
this.logStream = undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Progress indicator for long-running operations
|
|
349
|
+
progress(message: string, current: number, total: number, context?: string): void {
|
|
350
|
+
const percentage = Math.round((current / total) * 100);
|
|
351
|
+
const progressBar = this.createProgressBar(percentage);
|
|
352
|
+
|
|
353
|
+
this.info(`${progressBar} ${message} (${current}/${total} - ${percentage}%)`, context || 'PROGRESS');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private createProgressBar(percentage: number, width: number = 20): string {
|
|
357
|
+
const filled = Math.round((percentage / 100) * width);
|
|
358
|
+
const empty = width - filled;
|
|
359
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
360
|
+
return `[${bar}]`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Success and failure helpers
|
|
364
|
+
success(message: string, context?: string, metadata?: Record<string, any>): void {
|
|
365
|
+
this.info(`✅ ${message}`, context, metadata);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
failure(message: string, context?: string, metadata?: Record<string, any>, error?: Error): void {
|
|
369
|
+
this.error(`❌ ${message}`, context, metadata, error);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Method to close file streams (useful for graceful shutdown)
|
|
373
|
+
close(): void {
|
|
374
|
+
if (this.logStream) {
|
|
375
|
+
this.logStream.end();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Create default logger instance
|
|
381
|
+
const defaultConfig: Partial<LoggerConfig> = {
|
|
382
|
+
level: process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG,
|
|
383
|
+
enableConsole: true,
|
|
384
|
+
enableFile: process.env.NODE_ENV === 'production',
|
|
385
|
+
logDir: './logs',
|
|
386
|
+
format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const logger = new Logger(defaultConfig);
|
|
390
|
+
|
|
391
|
+
// Export the Logger class for custom instances
|
|
392
|
+
export { Logger };
|
|
393
|
+
|
|
394
|
+
// Graceful shutdown handling
|
|
395
|
+
process.on('SIGINT', () => {
|
|
396
|
+
logger.info('Received SIGINT, closing logger...');
|
|
397
|
+
logger.close();
|
|
398
|
+
process.exit(0);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
process.on('SIGTERM', () => {
|
|
402
|
+
logger.info('Received SIGTERM, closing logger...');
|
|
403
|
+
logger.close();
|
|
404
|
+
process.exit(0);
|
|
405
|
+
});
|
package/src/lib/pusher.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Pusher from 'pusher';
|
|
2
|
+
import { logger } from './logger.js';
|
|
2
3
|
|
|
3
4
|
// Server-side Pusher instance
|
|
4
5
|
export const pusher = new Pusher({
|
|
@@ -15,9 +16,9 @@ export class PusherService {
|
|
|
15
16
|
static async emitTaskComplete(workspaceId: string, event: string, data: any) {
|
|
16
17
|
try {
|
|
17
18
|
const channel = `workspace_${workspaceId}`;
|
|
18
|
-
const eventName =
|
|
19
|
+
const eventName = event;
|
|
19
20
|
await pusher.trigger(channel, eventName, data);
|
|
20
|
-
|
|
21
|
+
logger.info(`📡 Pusher notification sent: ${eventName} to ${channel}`);
|
|
21
22
|
} catch (error) {
|
|
22
23
|
console.error('❌ Pusher notification error:', error);
|
|
23
24
|
}
|
|
@@ -88,11 +89,22 @@ export class PusherService {
|
|
|
88
89
|
});
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// Emit analysis progress update (single event for all progress updates)
|
|
93
|
+
static async emitAnalysisProgress(workspaceId: string, progress: any) {
|
|
94
|
+
try {
|
|
95
|
+
const channel = `workspace_${workspaceId}`;
|
|
96
|
+
await pusher.trigger(channel, 'analysis_progress', progress);
|
|
97
|
+
logger.info(`📡 Analysis progress sent to ${channel}: ${progress.status}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('❌ Pusher progress notification error:', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
// Emit channel-specific events (for chat messages)
|
|
92
104
|
static async emitChannelEvent(channelId: string, event: string, data: any) {
|
|
93
105
|
try {
|
|
94
106
|
const channel = channelId; // Use channelId directly as channel name
|
|
95
|
-
const eventName =
|
|
107
|
+
const eventName = event;
|
|
96
108
|
await pusher.trigger(channel, eventName, data);
|
|
97
109
|
console.log(`📡 Pusher notification sent: ${eventName} to ${channel}`);
|
|
98
110
|
} catch (error) {
|
package/src/lib/storage.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// src/server/lib/
|
|
2
|
-
import {
|
|
1
|
+
// src/server/lib/storage.ts
|
|
2
|
+
import { createClient } from '@supabase/supabase-js';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
|
-
// Initialize
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
credentials: process.env.GCP_CLIENT_EMAIL && process.env.GCP_PRIVATE_KEY ? {
|
|
9
|
-
client_email: process.env.GCP_CLIENT_EMAIL,
|
|
10
|
-
private_key: process.env.GCP_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
|
11
|
-
} : undefined,
|
|
12
|
-
keyFilename: process.env.GOOGLE_CLOUD_KEY_FILE || process.env.GCP_KEY_FILE,
|
|
13
|
-
});
|
|
5
|
+
// Initialize Supabase Storage
|
|
6
|
+
const supabaseUrl = process.env.SUPABASE_URL;
|
|
7
|
+
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
if (!supabaseUrl || !supabaseServiceKey) {
|
|
10
|
+
throw new Error('Missing required Supabase environment variables: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
14
|
+
|
|
15
|
+
const bucketName = process.env.SUPABASE_BUCKET || 'media';
|
|
16
16
|
|
|
17
17
|
export interface UploadResult {
|
|
18
18
|
url: string;
|
|
@@ -20,35 +20,39 @@ export interface UploadResult {
|
|
|
20
20
|
objectKey: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export async function
|
|
23
|
+
export async function uploadToSupabase(
|
|
24
24
|
fileBuffer: Buffer,
|
|
25
25
|
fileName: string,
|
|
26
26
|
contentType: string,
|
|
27
27
|
makePublic: boolean = false
|
|
28
28
|
): Promise<UploadResult> {
|
|
29
|
-
const bucket = storage.bucket(bucketName);
|
|
30
29
|
const objectKey = `podcasts/${uuidv4()}_${fileName}`;
|
|
31
|
-
const file = bucket.file(objectKey);
|
|
32
30
|
|
|
33
|
-
// Upload the file
|
|
34
|
-
await
|
|
35
|
-
|
|
31
|
+
// Upload the file to Supabase Storage
|
|
32
|
+
const { data, error } = await supabase.storage
|
|
33
|
+
.from(bucketName)
|
|
34
|
+
.upload(objectKey, fileBuffer, {
|
|
36
35
|
contentType,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
36
|
+
upsert: false,
|
|
37
|
+
});
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
if (error) {
|
|
40
|
+
throw new Error(`Failed to upload file to Supabase: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const url = `${supabaseUrl}/storage/v1/object/public/${bucketName}/${objectKey}`;
|
|
42
44
|
|
|
43
45
|
// Generate signed URL for private files
|
|
44
46
|
let signedUrl: string | undefined;
|
|
45
47
|
if (!makePublic) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const { data: signedUrlData, error: signedUrlError } = await supabase.storage
|
|
49
|
+
.from(bucketName)
|
|
50
|
+
.createSignedUrl(objectKey, 24 * 60 * 60); // 24 hours
|
|
51
|
+
|
|
52
|
+
if (signedUrlError) {
|
|
53
|
+
throw new Error(`Failed to generate signed URL: ${signedUrlError.message}`);
|
|
54
|
+
}
|
|
55
|
+
signedUrl = signedUrlData.signedUrl;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
return {
|
|
@@ -59,38 +63,39 @@ export async function uploadToGCS(
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
export async function generateSignedUrl(objectKey: string, expiresInHours: number = 24): Promise<string> {
|
|
62
|
-
const
|
|
63
|
-
|
|
66
|
+
const { data, error } = await supabase.storage
|
|
67
|
+
.from(bucketName)
|
|
68
|
+
.createSignedUrl(objectKey, expiresInHours * 60 * 60);
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expires: Date.now() + expiresInHours * 60 * 60 * 1000,
|
|
69
|
-
});
|
|
70
|
+
if (error) {
|
|
71
|
+
throw new Error(`Failed to generate signed URL: ${error.message}`);
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
return signedUrl;
|
|
74
|
+
return data.signedUrl;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
export async function
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
export async function deleteFromSupabase(objectKey: string): Promise<void> {
|
|
78
|
+
const { error } = await supabase.storage
|
|
79
|
+
.from(bucketName)
|
|
80
|
+
.remove([objectKey]);
|
|
81
|
+
|
|
82
|
+
if (error) {
|
|
83
|
+
throw new Error(`Failed to delete file from Supabase: ${error.message}`);
|
|
84
|
+
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
export async function makeFilePublic(objectKey: string): Promise<void> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
// In Supabase, files are public by default when uploaded to public buckets
|
|
89
|
+
// For private buckets, you would need to update the bucket policy
|
|
90
|
+
// This function is kept for compatibility but may not be needed
|
|
91
|
+
console.log(`File ${objectKey} is already public in Supabase Storage`);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export async function makeFilePrivate(objectKey: string): Promise<void> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await file.makePrivate();
|
|
95
|
+
// In Supabase, you would need to update the bucket policy to make files private
|
|
96
|
+
// This function is kept for compatibility but may not be needed
|
|
97
|
+
console.log(`File ${objectKey} privacy is controlled by bucket policy in Supabase Storage`);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
export const
|
|
100
|
+
// Export supabase client for direct access if needed
|
|
101
|
+
export const supabaseClient = supabase;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ValidationError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Common validation schemas
|
|
6
|
+
*/
|
|
7
|
+
export const commonSchemas = {
|
|
8
|
+
id: z.string().cuid(),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
url: z.string().url(),
|
|
11
|
+
pagination: z.object({
|
|
12
|
+
page: z.number().int().positive().default(1),
|
|
13
|
+
limit: z.number().int().positive().max(100).default(20),
|
|
14
|
+
}),
|
|
15
|
+
search: z.object({
|
|
16
|
+
query: z.string().min(1).max(200),
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enums for type safety
|
|
22
|
+
*/
|
|
23
|
+
export const ArtifactType = z.enum([
|
|
24
|
+
'STUDY_GUIDE',
|
|
25
|
+
'FLASHCARD_SET',
|
|
26
|
+
'WORKSHEET',
|
|
27
|
+
'MEETING_SUMMARY',
|
|
28
|
+
'PODCAST_EPISODE',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const Difficulty = z.enum(['EASY', 'MEDIUM', 'HARD']);
|
|
32
|
+
|
|
33
|
+
export const QuestionType = z.enum([
|
|
34
|
+
'MULTIPLE_CHOICE',
|
|
35
|
+
'TEXT',
|
|
36
|
+
'NUMERIC',
|
|
37
|
+
'TRUE_FALSE',
|
|
38
|
+
'MATCHING',
|
|
39
|
+
'FILL_IN_THE_BLANK',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validation helper that throws ValidationError
|
|
44
|
+
*/
|
|
45
|
+
export function validateSchema<T extends z.ZodType>(
|
|
46
|
+
schema: T,
|
|
47
|
+
data: unknown
|
|
48
|
+
): z.infer<T> {
|
|
49
|
+
const result = schema.safeParse(data);
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
const errors = result.error.message;
|
|
52
|
+
throw new ValidationError(`Validation failed: ${errors}`);
|
|
53
|
+
}
|
|
54
|
+
return result.data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sanitize string inputs
|
|
59
|
+
*/
|
|
60
|
+
export function sanitizeString(input: string, maxLength: number = 10000): string {
|
|
61
|
+
return input
|
|
62
|
+
.trim()
|
|
63
|
+
.slice(0, maxLength)
|
|
64
|
+
.replace(/[<>]/g, ''); // Basic XSS prevention
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate ownership
|
|
69
|
+
*/
|
|
70
|
+
export function validateOwnership(ownerId: string, userId: string): void {
|
|
71
|
+
if (ownerId !== userId) {
|
|
72
|
+
throw new ValidationError('You do not have permission to access this resource');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
package/src/routers/_app.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { worksheets } from './worksheets.js';
|
|
|
7
7
|
import { studyguide } from './studyguide.js';
|
|
8
8
|
import { podcast } from './podcast.js';
|
|
9
9
|
import { chat } from './chat.js';
|
|
10
|
+
import { members } from './members.js';
|
|
10
11
|
|
|
11
12
|
export const appRouter = router({
|
|
12
13
|
auth,
|
|
@@ -16,6 +17,10 @@ export const appRouter = router({
|
|
|
16
17
|
studyguide,
|
|
17
18
|
podcast,
|
|
18
19
|
chat,
|
|
20
|
+
// Public member endpoints (for invitation acceptance)
|
|
21
|
+
member: router({
|
|
22
|
+
acceptInvite: members.acceptInvite,
|
|
23
|
+
}),
|
|
19
24
|
});
|
|
20
25
|
|
|
21
26
|
// Export type for client inference
|