claude-session-continuity-mcp 1.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +307 -0
  3. package/dist/dashboard-v2.d.ts +11 -0
  4. package/dist/dashboard-v2.js +1321 -0
  5. package/dist/dashboard.d.ts +2 -0
  6. package/dist/dashboard.js +1196 -0
  7. package/dist/db/database.d.ts +8 -0
  8. package/dist/db/database.js +208 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +3426 -0
  11. package/dist/schemas.d.ts +381 -0
  12. package/dist/schemas.js +119 -0
  13. package/dist/tools/context.d.ts +6 -0
  14. package/dist/tools/context.js +227 -0
  15. package/dist/tools/embedding.d.ts +5 -0
  16. package/dist/tools/embedding.js +191 -0
  17. package/dist/tools/feedback.d.ts +5 -0
  18. package/dist/tools/feedback.js +200 -0
  19. package/dist/tools/filter.d.ts +5 -0
  20. package/dist/tools/filter.js +169 -0
  21. package/dist/tools/index.d.ts +12 -0
  22. package/dist/tools/index.js +38 -0
  23. package/dist/tools/learning.d.ts +8 -0
  24. package/dist/tools/learning.js +395 -0
  25. package/dist/tools/memory.d.ts +8 -0
  26. package/dist/tools/memory.js +356 -0
  27. package/dist/tools/project.d.ts +9 -0
  28. package/dist/tools/project.js +396 -0
  29. package/dist/tools/relation.d.ts +4 -0
  30. package/dist/tools/relation.js +148 -0
  31. package/dist/tools/session.d.ts +7 -0
  32. package/dist/tools/session.js +272 -0
  33. package/dist/tools/solution.d.ts +5 -0
  34. package/dist/tools/solution.js +182 -0
  35. package/dist/tools/task.d.ts +6 -0
  36. package/dist/tools/task.js +184 -0
  37. package/dist/tools-v2/auto-capture.d.ts +5 -0
  38. package/dist/tools-v2/auto-capture.js +252 -0
  39. package/dist/tools-v2/context.d.ts +4 -0
  40. package/dist/tools-v2/context.js +170 -0
  41. package/dist/tools-v2/embedding.d.ts +3 -0
  42. package/dist/tools-v2/embedding.js +115 -0
  43. package/dist/tools-v2/index.d.ts +13 -0
  44. package/dist/tools-v2/index.js +73 -0
  45. package/dist/tools-v2/learn.d.ts +4 -0
  46. package/dist/tools-v2/learn.js +233 -0
  47. package/dist/tools-v2/memory.d.ts +6 -0
  48. package/dist/tools-v2/memory.js +340 -0
  49. package/dist/tools-v2/projects.d.ts +3 -0
  50. package/dist/tools-v2/projects.js +218 -0
  51. package/dist/tools-v2/task.d.ts +3 -0
  52. package/dist/tools-v2/task.js +193 -0
  53. package/dist/tools-v2/verify.d.ts +3 -0
  54. package/dist/tools-v2/verify.js +164 -0
  55. package/dist/types.d.ts +51 -0
  56. package/dist/types.js +7 -0
  57. package/dist/utils/auto-context.d.ts +58 -0
  58. package/dist/utils/auto-context.js +234 -0
  59. package/dist/utils/cache.d.ts +60 -0
  60. package/dist/utils/cache.js +161 -0
  61. package/dist/utils/embedding.d.ts +7 -0
  62. package/dist/utils/embedding.js +67 -0
  63. package/dist/utils/helpers.d.ts +4 -0
  64. package/dist/utils/helpers.js +45 -0
  65. package/dist/utils/logger.d.ts +17 -0
  66. package/dist/utils/logger.js +111 -0
  67. package/package.json +64 -0
@@ -0,0 +1,1196 @@
1
+ #!/usr/bin/env node
2
+ import * as http from 'http';
3
+ import * as url from 'url';
4
+ import Database from 'better-sqlite3';
5
+ import * as path from 'path';
6
+ const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/Users/ibyeongchang/Documents/dev/ai-service-generator';
7
+ const DB_PATH = path.join(WORKSPACE_ROOT, '.claude', 'sessions.db');
8
+ const PORT = parseInt(process.env.PORT || '8000');
9
+ const db = new Database(DB_PATH);
10
+ // ===== 다국어 지원 =====
11
+ const i18n = {
12
+ en: {
13
+ title: 'Project Manager MCP Dashboard',
14
+ memories: 'Memories',
15
+ sessions: 'Sessions',
16
+ relations: 'Relations',
17
+ embeddings: 'Embeddings',
18
+ totalMemories: 'Total Memories',
19
+ totalSessions: 'Sessions',
20
+ totalRelations: 'Relations',
21
+ totalPatterns: 'Patterns',
22
+ search: 'Search memories...',
23
+ allTypes: 'All Types',
24
+ allProjects: 'All Projects',
25
+ id: 'ID',
26
+ type: 'Type',
27
+ content: 'Content',
28
+ tags: 'Tags',
29
+ project: 'Project',
30
+ importance: 'Importance',
31
+ created: 'Created',
32
+ actions: 'Actions',
33
+ view: 'View',
34
+ edit: 'Edit',
35
+ delete: 'Delete',
36
+ close: 'Close',
37
+ cancel: 'Cancel',
38
+ update: 'Update Memory',
39
+ noMemories: 'No memories found',
40
+ noSessions: 'No sessions found',
41
+ noRelations: 'No relations found',
42
+ confirmDelete: 'Are you sure you want to delete this memory?',
43
+ updated: 'Memory updated successfully!',
44
+ deleted: 'Memory deleted',
45
+ lastWork: 'Last Work',
46
+ status: 'Status',
47
+ verification: 'Verification',
48
+ timestamp: 'Timestamp',
49
+ source: 'Source',
50
+ relation: 'Relation',
51
+ target: 'Target',
52
+ strength: 'Strength',
53
+ modelStatus: 'Model Status',
54
+ model: 'Model',
55
+ dimensions: 'Dimensions',
56
+ coverage: 'Coverage',
57
+ ready: 'Ready',
58
+ loading: 'Loading',
59
+ language: 'Language',
60
+ observation: 'observation',
61
+ decision: 'decision',
62
+ learning: 'learning',
63
+ error: 'error',
64
+ pattern: 'pattern',
65
+ preference: 'preference'
66
+ },
67
+ ko: {
68
+ title: 'Project Manager MCP 대시보드',
69
+ memories: '메모리',
70
+ sessions: '세션',
71
+ relations: '관계',
72
+ embeddings: '임베딩',
73
+ totalMemories: '총 메모리',
74
+ totalSessions: '세션',
75
+ totalRelations: '관계',
76
+ totalPatterns: '패턴',
77
+ search: '메모리 검색...',
78
+ allTypes: '전체 유형',
79
+ allProjects: '전체 프로젝트',
80
+ id: 'ID',
81
+ type: '유형',
82
+ content: '내용',
83
+ tags: '태그',
84
+ project: '프로젝트',
85
+ importance: '중요도',
86
+ created: '생성일',
87
+ actions: '작업',
88
+ view: '보기',
89
+ edit: '수정',
90
+ delete: '삭제',
91
+ close: '닫기',
92
+ cancel: '취소',
93
+ update: '메모리 업데이트',
94
+ noMemories: '메모리가 없습니다',
95
+ noSessions: '세션이 없습니다',
96
+ noRelations: '관계가 없습니다',
97
+ confirmDelete: '이 메모리를 삭제하시겠습니까?',
98
+ updated: '메모리가 업데이트되었습니다!',
99
+ deleted: '메모리가 삭제되었습니다',
100
+ lastWork: '마지막 작업',
101
+ status: '상태',
102
+ verification: '검증',
103
+ timestamp: '시간',
104
+ source: '출발',
105
+ relation: '관계',
106
+ target: '도착',
107
+ strength: '강도',
108
+ modelStatus: '모델 상태',
109
+ model: '모델',
110
+ dimensions: '차원',
111
+ coverage: '커버리지',
112
+ ready: '준비됨',
113
+ loading: '로딩 중',
114
+ language: '언어',
115
+ observation: '관찰',
116
+ decision: '결정',
117
+ learning: '학습',
118
+ error: '에러',
119
+ pattern: '패턴',
120
+ preference: '선호'
121
+ },
122
+ ja: {
123
+ title: 'Project Manager MCP ダッシュボード',
124
+ memories: 'メモリ',
125
+ sessions: 'セッション',
126
+ relations: '関係',
127
+ embeddings: '埋め込み',
128
+ totalMemories: '総メモリ',
129
+ totalSessions: 'セッション',
130
+ totalRelations: '関係',
131
+ totalPatterns: 'パターン',
132
+ search: 'メモリを検索...',
133
+ allTypes: 'すべてのタイプ',
134
+ allProjects: 'すべてのプロジェクト',
135
+ id: 'ID',
136
+ type: 'タイプ',
137
+ content: '内容',
138
+ tags: 'タグ',
139
+ project: 'プロジェクト',
140
+ importance: '重要度',
141
+ created: '作成日',
142
+ actions: 'アクション',
143
+ view: '表示',
144
+ edit: '編集',
145
+ delete: '削除',
146
+ close: '閉じる',
147
+ cancel: 'キャンセル',
148
+ update: 'メモリを更新',
149
+ noMemories: 'メモリが見つかりません',
150
+ noSessions: 'セッションが見つかりません',
151
+ noRelations: '関係が見つかりません',
152
+ confirmDelete: 'このメモリを削除しますか?',
153
+ updated: 'メモリが更新されました!',
154
+ deleted: 'メモリが削除されました',
155
+ lastWork: '最後の作業',
156
+ status: 'ステータス',
157
+ verification: '検証',
158
+ timestamp: 'タイムスタンプ',
159
+ source: 'ソース',
160
+ relation: '関係',
161
+ target: 'ターゲット',
162
+ strength: '強度',
163
+ modelStatus: 'モデル状態',
164
+ model: 'モデル',
165
+ dimensions: '次元',
166
+ coverage: 'カバレッジ',
167
+ ready: '準備完了',
168
+ loading: '読み込み中',
169
+ language: '言語',
170
+ observation: '観察',
171
+ decision: '決定',
172
+ learning: '学習',
173
+ error: 'エラー',
174
+ pattern: 'パターン',
175
+ preference: '好み'
176
+ },
177
+ zh: {
178
+ title: 'Project Manager MCP 仪表板',
179
+ memories: '记忆',
180
+ sessions: '会话',
181
+ relations: '关系',
182
+ embeddings: '嵌入',
183
+ totalMemories: '总记忆',
184
+ totalSessions: '会话',
185
+ totalRelations: '关系',
186
+ totalPatterns: '模式',
187
+ search: '搜索记忆...',
188
+ allTypes: '所有类型',
189
+ allProjects: '所有项目',
190
+ id: 'ID',
191
+ type: '类型',
192
+ content: '内容',
193
+ tags: '标签',
194
+ project: '项目',
195
+ importance: '重要性',
196
+ created: '创建时间',
197
+ actions: '操作',
198
+ view: '查看',
199
+ edit: '编辑',
200
+ delete: '删除',
201
+ close: '关闭',
202
+ cancel: '取消',
203
+ update: '更新记忆',
204
+ noMemories: '没有找到记忆',
205
+ noSessions: '没有找到会话',
206
+ noRelations: '没有找到关系',
207
+ confirmDelete: '确定要删除这条记忆吗?',
208
+ updated: '记忆更新成功!',
209
+ deleted: '记忆已删除',
210
+ lastWork: '最后工作',
211
+ status: '状态',
212
+ verification: '验证',
213
+ timestamp: '时间戳',
214
+ source: '来源',
215
+ relation: '关系',
216
+ target: '目标',
217
+ strength: '强度',
218
+ modelStatus: '模型状态',
219
+ model: '模型',
220
+ dimensions: '维度',
221
+ coverage: '覆盖率',
222
+ ready: '就绪',
223
+ loading: '加载中',
224
+ language: '语言',
225
+ observation: '观察',
226
+ decision: '决定',
227
+ learning: '学习',
228
+ error: '错误',
229
+ pattern: '模式',
230
+ preference: '偏好'
231
+ },
232
+ de: {
233
+ title: 'Project Manager MCP Dashboard',
234
+ memories: 'Erinnerungen',
235
+ sessions: 'Sitzungen',
236
+ relations: 'Beziehungen',
237
+ embeddings: 'Einbettungen',
238
+ totalMemories: 'Gesamt',
239
+ totalSessions: 'Sitzungen',
240
+ totalRelations: 'Beziehungen',
241
+ totalPatterns: 'Muster',
242
+ search: 'Erinnerungen suchen...',
243
+ allTypes: 'Alle Typen',
244
+ allProjects: 'Alle Projekte',
245
+ id: 'ID',
246
+ type: 'Typ',
247
+ content: 'Inhalt',
248
+ tags: 'Tags',
249
+ project: 'Projekt',
250
+ importance: 'Wichtigkeit',
251
+ created: 'Erstellt',
252
+ actions: 'Aktionen',
253
+ view: 'Ansehen',
254
+ edit: 'Bearbeiten',
255
+ delete: 'Löschen',
256
+ close: 'Schließen',
257
+ cancel: 'Abbrechen',
258
+ update: 'Aktualisieren',
259
+ noMemories: 'Keine Erinnerungen gefunden',
260
+ noSessions: 'Keine Sitzungen gefunden',
261
+ noRelations: 'Keine Beziehungen gefunden',
262
+ confirmDelete: 'Möchten Sie diese Erinnerung wirklich löschen?',
263
+ updated: 'Erinnerung erfolgreich aktualisiert!',
264
+ deleted: 'Erinnerung gelöscht',
265
+ lastWork: 'Letzte Arbeit',
266
+ status: 'Status',
267
+ verification: 'Verifizierung',
268
+ timestamp: 'Zeitstempel',
269
+ source: 'Quelle',
270
+ relation: 'Beziehung',
271
+ target: 'Ziel',
272
+ strength: 'Stärke',
273
+ modelStatus: 'Modellstatus',
274
+ model: 'Modell',
275
+ dimensions: 'Dimensionen',
276
+ coverage: 'Abdeckung',
277
+ ready: 'Bereit',
278
+ loading: 'Laden',
279
+ language: 'Sprache',
280
+ observation: 'Beobachtung',
281
+ decision: 'Entscheidung',
282
+ learning: 'Lernen',
283
+ error: 'Fehler',
284
+ pattern: 'Muster',
285
+ preference: 'Präferenz'
286
+ },
287
+ fr: {
288
+ title: 'Tableau de bord Project Manager MCP',
289
+ memories: 'Mémoires',
290
+ sessions: 'Sessions',
291
+ relations: 'Relations',
292
+ embeddings: 'Incorporations',
293
+ totalMemories: 'Total',
294
+ totalSessions: 'Sessions',
295
+ totalRelations: 'Relations',
296
+ totalPatterns: 'Modèles',
297
+ search: 'Rechercher...',
298
+ allTypes: 'Tous les types',
299
+ allProjects: 'Tous les projets',
300
+ id: 'ID',
301
+ type: 'Type',
302
+ content: 'Contenu',
303
+ tags: 'Tags',
304
+ project: 'Projet',
305
+ importance: 'Importance',
306
+ created: 'Créé',
307
+ actions: 'Actions',
308
+ view: 'Voir',
309
+ edit: 'Modifier',
310
+ delete: 'Supprimer',
311
+ close: 'Fermer',
312
+ cancel: 'Annuler',
313
+ update: 'Mettre à jour',
314
+ noMemories: 'Aucune mémoire trouvée',
315
+ noSessions: 'Aucune session trouvée',
316
+ noRelations: 'Aucune relation trouvée',
317
+ confirmDelete: 'Voulez-vous vraiment supprimer cette mémoire?',
318
+ updated: 'Mémoire mise à jour avec succès!',
319
+ deleted: 'Mémoire supprimée',
320
+ lastWork: 'Dernier travail',
321
+ status: 'Statut',
322
+ verification: 'Vérification',
323
+ timestamp: 'Horodatage',
324
+ source: 'Source',
325
+ relation: 'Relation',
326
+ target: 'Cible',
327
+ strength: 'Force',
328
+ modelStatus: 'État du modèle',
329
+ model: 'Modèle',
330
+ dimensions: 'Dimensions',
331
+ coverage: 'Couverture',
332
+ ready: 'Prêt',
333
+ loading: 'Chargement',
334
+ language: 'Langue',
335
+ observation: 'observation',
336
+ decision: 'décision',
337
+ learning: 'apprentissage',
338
+ error: 'erreur',
339
+ pattern: 'modèle',
340
+ preference: 'préférence'
341
+ },
342
+ es: {
343
+ title: 'Panel de Project Manager MCP',
344
+ memories: 'Memorias',
345
+ sessions: 'Sesiones',
346
+ relations: 'Relaciones',
347
+ embeddings: 'Incrustaciones',
348
+ totalMemories: 'Total',
349
+ totalSessions: 'Sesiones',
350
+ totalRelations: 'Relaciones',
351
+ totalPatterns: 'Patrones',
352
+ search: 'Buscar memorias...',
353
+ allTypes: 'Todos los tipos',
354
+ allProjects: 'Todos los proyectos',
355
+ id: 'ID',
356
+ type: 'Tipo',
357
+ content: 'Contenido',
358
+ tags: 'Etiquetas',
359
+ project: 'Proyecto',
360
+ importance: 'Importancia',
361
+ created: 'Creado',
362
+ actions: 'Acciones',
363
+ view: 'Ver',
364
+ edit: 'Editar',
365
+ delete: 'Eliminar',
366
+ close: 'Cerrar',
367
+ cancel: 'Cancelar',
368
+ update: 'Actualizar',
369
+ noMemories: 'No se encontraron memorias',
370
+ noSessions: 'No se encontraron sesiones',
371
+ noRelations: 'No se encontraron relaciones',
372
+ confirmDelete: '¿Está seguro de que desea eliminar esta memoria?',
373
+ updated: '¡Memoria actualizada con éxito!',
374
+ deleted: 'Memoria eliminada',
375
+ lastWork: 'Último trabajo',
376
+ status: 'Estado',
377
+ verification: 'Verificación',
378
+ timestamp: 'Marca de tiempo',
379
+ source: 'Origen',
380
+ relation: 'Relación',
381
+ target: 'Destino',
382
+ strength: 'Fuerza',
383
+ modelStatus: 'Estado del modelo',
384
+ model: 'Modelo',
385
+ dimensions: 'Dimensiones',
386
+ coverage: 'Cobertura',
387
+ ready: 'Listo',
388
+ loading: 'Cargando',
389
+ language: 'Idioma',
390
+ observation: 'observación',
391
+ decision: 'decisión',
392
+ learning: 'aprendizaje',
393
+ error: 'error',
394
+ pattern: 'patrón',
395
+ preference: 'preferencia'
396
+ }
397
+ };
398
+ // ===== API 핸들러 =====
399
+ function getMemories(params) {
400
+ const type = params.get('type');
401
+ const project = params.get('project');
402
+ const search = params.get('search');
403
+ const limit = parseInt(params.get('limit') || '50');
404
+ let sql = 'SELECT * FROM memories WHERE 1=1';
405
+ const sqlParams = [];
406
+ if (type) {
407
+ sql += ' AND memory_type = ?';
408
+ sqlParams.push(type);
409
+ }
410
+ if (project) {
411
+ sql += ' AND project = ?';
412
+ sqlParams.push(project);
413
+ }
414
+ if (search) {
415
+ sql += ' AND (content LIKE ? OR tags LIKE ?)';
416
+ sqlParams.push(`%${search}%`, `%${search}%`);
417
+ }
418
+ sql += ' ORDER BY created_at DESC LIMIT ?';
419
+ sqlParams.push(limit);
420
+ const stmt = db.prepare(sql);
421
+ return stmt.all(...sqlParams);
422
+ }
423
+ function getMemory(id) {
424
+ const stmt = db.prepare('SELECT * FROM memories WHERE id = ?');
425
+ return stmt.get(id);
426
+ }
427
+ function updateMemory(id, data) {
428
+ const updates = [];
429
+ const params = [];
430
+ if (data.content !== undefined) {
431
+ updates.push('content = ?');
432
+ params.push(data.content);
433
+ }
434
+ if (data.tags !== undefined) {
435
+ updates.push('tags = ?');
436
+ params.push(JSON.stringify(data.tags));
437
+ }
438
+ if (data.importance !== undefined) {
439
+ updates.push('importance = ?');
440
+ params.push(data.importance);
441
+ }
442
+ if (data.memory_type !== undefined) {
443
+ updates.push('memory_type = ?');
444
+ params.push(data.memory_type);
445
+ }
446
+ if (updates.length === 0)
447
+ return { success: false, message: 'No updates' };
448
+ params.push(id);
449
+ const sql = `UPDATE memories SET ${updates.join(', ')} WHERE id = ?`;
450
+ const stmt = db.prepare(sql);
451
+ const result = stmt.run(...params);
452
+ return { success: result.changes > 0 };
453
+ }
454
+ function deleteMemoryById(id) {
455
+ const stmt = db.prepare('DELETE FROM memories WHERE id = ?');
456
+ const result = stmt.run(id);
457
+ return { success: result.changes > 0 };
458
+ }
459
+ function getSessions(params) {
460
+ const project = params.get('project');
461
+ const limit = parseInt(params.get('limit') || '50');
462
+ let sql = 'SELECT * FROM sessions WHERE 1=1';
463
+ const sqlParams = [];
464
+ if (project) {
465
+ sql += ' AND project = ?';
466
+ sqlParams.push(project);
467
+ }
468
+ sql += ' ORDER BY timestamp DESC LIMIT ?';
469
+ sqlParams.push(limit);
470
+ const stmt = db.prepare(sql);
471
+ return stmt.all(...sqlParams);
472
+ }
473
+ function getStats() {
474
+ const memoriesCount = db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
475
+ const sessionsCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get().count;
476
+ const relationsCount = db.prepare('SELECT COUNT(*) as count FROM memory_relations').get().count;
477
+ const patternsCount = db.prepare('SELECT COUNT(*) as count FROM work_patterns').get().count;
478
+ // 임베딩 통계
479
+ let embeddingsCount = 0;
480
+ try {
481
+ embeddingsCount = db.prepare('SELECT COUNT(*) as count FROM embeddings').get().count;
482
+ }
483
+ catch {
484
+ // embeddings 테이블이 없을 수 있음
485
+ }
486
+ const memoryTypes = db.prepare('SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type').all();
487
+ const projects = db.prepare('SELECT DISTINCT project FROM memories WHERE project IS NOT NULL UNION SELECT DISTINCT project FROM sessions').all();
488
+ return {
489
+ memories: memoriesCount,
490
+ sessions: sessionsCount,
491
+ relations: relationsCount,
492
+ patterns: patternsCount,
493
+ embeddings: embeddingsCount,
494
+ embeddingCoverage: memoriesCount > 0 ? Math.round((embeddingsCount / memoriesCount) * 100) : 100,
495
+ memoryTypes,
496
+ projects
497
+ };
498
+ }
499
+ function getRelations(memoryId) {
500
+ if (memoryId) {
501
+ const stmt = db.prepare(`
502
+ SELECT r.*,
503
+ s.content as source_content, s.memory_type as source_type,
504
+ t.content as target_content, t.memory_type as target_type
505
+ FROM memory_relations r
506
+ JOIN memories s ON r.source_id = s.id
507
+ JOIN memories t ON r.target_id = t.id
508
+ WHERE r.source_id = ? OR r.target_id = ?
509
+ `);
510
+ return stmt.all(memoryId, memoryId);
511
+ }
512
+ const stmt = db.prepare(`
513
+ SELECT r.*,
514
+ s.content as source_content, s.memory_type as source_type,
515
+ t.content as target_content, t.memory_type as target_type
516
+ FROM memory_relations r
517
+ JOIN memories s ON r.source_id = s.id
518
+ JOIN memories t ON r.target_id = t.id
519
+ LIMIT 100
520
+ `);
521
+ return stmt.all();
522
+ }
523
+ // ===== HTML 템플릿 =====
524
+ const HTML_TEMPLATE = `<!DOCTYPE html>
525
+ <html lang="en">
526
+ <head>
527
+ <meta charset="UTF-8">
528
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
529
+ <title>Project Manager MCP Dashboard</title>
530
+ <style>
531
+ * { box-sizing: border-box; margin: 0; padding: 0; }
532
+ body {
533
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
534
+ background: #0f172a; color: #e2e8f0; line-height: 1.6;
535
+ }
536
+ .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
537
+
538
+ header {
539
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
540
+ padding: 24px; border-radius: 12px; margin-bottom: 24px;
541
+ display: flex; justify-content: space-between; align-items: center;
542
+ }
543
+ header h1 { font-size: 1.5rem; display: flex; align-items: center; gap: 12px; }
544
+ header h1::before { content: '🧠'; font-size: 1.8rem; }
545
+
546
+ .header-right { display: flex; align-items: center; gap: 16px; }
547
+ .lang-select {
548
+ padding: 8px 12px; background: #0f172a; border: 1px solid #334155;
549
+ border-radius: 8px; color: #e2e8f0; font-size: 0.875rem; cursor: pointer;
550
+ }
551
+
552
+ .stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 24px; }
553
+ .stat-card {
554
+ background: #1e293b; padding: 20px; border-radius: 12px;
555
+ border: 1px solid #334155; transition: transform 0.2s;
556
+ }
557
+ .stat-card:hover { transform: translateY(-2px); }
558
+ .stat-card h3 { color: #94a3b8; font-size: 0.875rem; margin-bottom: 8px; }
559
+ .stat-card .value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
560
+ .stat-card .sub { font-size: 0.75rem; color: #64748b; margin-top: 4px; }
561
+
562
+ .tabs { display: flex; gap: 8px; margin-bottom: 20px; }
563
+ .tab {
564
+ padding: 12px 24px; background: #1e293b; border: none; color: #94a3b8;
565
+ border-radius: 8px; cursor: pointer; font-size: 0.875rem; transition: all 0.2s;
566
+ }
567
+ .tab:hover { background: #334155; }
568
+ .tab.active { background: #3b82f6; color: white; }
569
+
570
+ .filters {
571
+ display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap;
572
+ background: #1e293b; padding: 16px; border-radius: 12px;
573
+ }
574
+ .filters input, .filters select {
575
+ padding: 10px 16px; background: #0f172a; border: 1px solid #334155;
576
+ border-radius: 8px; color: #e2e8f0; font-size: 0.875rem;
577
+ }
578
+ .filters input:focus, .filters select:focus {
579
+ outline: none; border-color: #3b82f6;
580
+ }
581
+ .filters input { flex: 1; min-width: 200px; }
582
+
583
+ .content { background: #1e293b; border-radius: 12px; overflow: hidden; }
584
+
585
+ table { width: 100%; border-collapse: collapse; }
586
+ th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid #334155; }
587
+ th { background: #0f172a; color: #94a3b8; font-weight: 500; font-size: 0.75rem; text-transform: uppercase; }
588
+ tr:hover { background: #334155; }
589
+
590
+ .tag {
591
+ display: inline-block; padding: 4px 10px; background: #3b82f6;
592
+ border-radius: 12px; font-size: 0.75rem; margin: 2px;
593
+ }
594
+ .tag.learning { background: #8b5cf6; }
595
+ .tag.decision { background: #f59e0b; }
596
+ .tag.error { background: #ef4444; }
597
+ .tag.pattern { background: #10b981; }
598
+ .tag.observation { background: #6366f1; }
599
+ .tag.preference { background: #ec4899; }
600
+
601
+ .importance {
602
+ display: inline-flex; align-items: center; gap: 4px;
603
+ padding: 4px 8px; background: #334155; border-radius: 6px; font-size: 0.75rem;
604
+ }
605
+ .importance.high { background: #ef4444; }
606
+ .importance.medium { background: #f59e0b; }
607
+ .importance.low { background: #6b7280; }
608
+
609
+ .actions { display: flex; gap: 8px; }
610
+ .btn {
611
+ padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer;
612
+ font-size: 0.75rem; transition: all 0.2s;
613
+ }
614
+ .btn-edit { background: #3b82f6; color: white; }
615
+ .btn-edit:hover { background: #2563eb; }
616
+ .btn-delete { background: #ef4444; color: white; }
617
+ .btn-delete:hover { background: #dc2626; }
618
+ .btn-view { background: #334155; color: #e2e8f0; }
619
+ .btn-view:hover { background: #475569; }
620
+
621
+ .modal {
622
+ display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
623
+ background: rgba(0,0,0,0.8); justify-content: center; align-items: center; z-index: 1000;
624
+ }
625
+ .modal.active { display: flex; }
626
+ .modal-content {
627
+ background: #1e293b; padding: 32px; border-radius: 16px;
628
+ width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto;
629
+ }
630
+ .modal-content h2 { margin-bottom: 24px; display: flex; align-items: center; gap: 12px; }
631
+ .modal-content label { display: block; margin-bottom: 8px; color: #94a3b8; font-size: 0.875rem; }
632
+ .modal-content input, .modal-content textarea, .modal-content select {
633
+ width: 100%; padding: 12px; background: #0f172a; border: 1px solid #334155;
634
+ border-radius: 8px; color: #e2e8f0; margin-bottom: 16px; font-size: 0.875rem;
635
+ }
636
+ .modal-content textarea { min-height: 120px; resize: vertical; font-family: inherit; }
637
+ .modal-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
638
+ .btn-primary { background: #3b82f6; color: white; padding: 12px 24px; }
639
+ .btn-secondary { background: #334155; color: #e2e8f0; padding: 12px 24px; }
640
+
641
+ .truncate { max-width: 400px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
642
+ .empty { text-align: center; padding: 60px; color: #64748b; }
643
+ .empty::before { content: '📭'; font-size: 3rem; display: block; margin-bottom: 16px; }
644
+
645
+ .toast {
646
+ position: fixed; bottom: 20px; right: 20px; padding: 16px 24px;
647
+ background: #10b981; color: white; border-radius: 8px;
648
+ transform: translateY(100px); opacity: 0; transition: all 0.3s;
649
+ }
650
+ .toast.show { transform: translateY(0); opacity: 1; }
651
+ .toast.error { background: #ef4444; }
652
+
653
+ .embedding-bar {
654
+ height: 8px; background: #334155; border-radius: 4px; overflow: hidden; margin-top: 8px;
655
+ }
656
+ .embedding-bar .fill {
657
+ height: 100%; background: linear-gradient(90deg, #3b82f6, #10b981);
658
+ transition: width 0.3s;
659
+ }
660
+
661
+ @media (max-width: 768px) {
662
+ .stats { grid-template-columns: repeat(2, 1fr); }
663
+ .header-right { flex-direction: column; gap: 8px; }
664
+ }
665
+ </style>
666
+ </head>
667
+ <body>
668
+ <div class="container">
669
+ <header>
670
+ <h1 id="page-title">Project Manager MCP Dashboard</h1>
671
+ <div class="header-right">
672
+ <select class="lang-select" id="lang-select" onchange="changeLanguage(this.value)">
673
+ <option value="en">English</option>
674
+ <option value="ko">한국어</option>
675
+ <option value="ja">日本語</option>
676
+ <option value="zh">中文</option>
677
+ <option value="de">Deutsch</option>
678
+ <option value="fr">Français</option>
679
+ <option value="es">Español</option>
680
+ </select>
681
+ </div>
682
+ </header>
683
+
684
+ <div class="stats" id="stats"></div>
685
+
686
+ <div class="tabs" id="tabs"></div>
687
+
688
+ <div class="filters" id="filters"></div>
689
+
690
+ <div class="content" id="content"></div>
691
+ </div>
692
+
693
+ <div class="modal" id="modal">
694
+ <div class="modal-content" id="modal-content"></div>
695
+ </div>
696
+
697
+ <div class="toast" id="toast"></div>
698
+
699
+ <script>
700
+ // 다국어 데이터
701
+ const i18n = ${JSON.stringify(i18n)};
702
+
703
+ let currentLang = localStorage.getItem('mcp-lang') || 'en';
704
+ let currentTab = 'memories';
705
+ let stats = {};
706
+
707
+ function t(key) {
708
+ return i18n[currentLang]?.[key] || i18n['en'][key] || key;
709
+ }
710
+
711
+ function changeLanguage(lang) {
712
+ currentLang = lang;
713
+ localStorage.setItem('mcp-lang', lang);
714
+ document.getElementById('lang-select').value = lang;
715
+ updateUI();
716
+ }
717
+
718
+ function updateUI() {
719
+ document.getElementById('page-title').textContent = t('title');
720
+ document.title = t('title');
721
+ renderTabs();
722
+ loadStats();
723
+ }
724
+
725
+ function renderTabs() {
726
+ document.getElementById('tabs').innerHTML = \`
727
+ <button class="tab \${currentTab === 'memories' ? 'active' : ''}" data-tab="memories">🧠 \${t('memories')}</button>
728
+ <button class="tab \${currentTab === 'sessions' ? 'active' : ''}" data-tab="sessions">📝 \${t('sessions')}</button>
729
+ <button class="tab \${currentTab === 'relations' ? 'active' : ''}" data-tab="relations">🔗 \${t('relations')}</button>
730
+ <button class="tab \${currentTab === 'embeddings' ? 'active' : ''}" data-tab="embeddings">🔮 \${t('embeddings')}</button>
731
+ \`;
732
+ document.querySelectorAll('.tab').forEach(tab => {
733
+ tab.addEventListener('click', () => {
734
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
735
+ tab.classList.add('active');
736
+ currentTab = tab.dataset.tab;
737
+ renderFilters();
738
+ loadContent();
739
+ });
740
+ });
741
+ }
742
+
743
+ // API 호출
744
+ async function api(endpoint, options = {}) {
745
+ const res = await fetch('/api' + endpoint, {
746
+ ...options,
747
+ headers: { 'Content-Type': 'application/json', ...options.headers }
748
+ });
749
+ return res.json();
750
+ }
751
+
752
+ // 토스트 메시지
753
+ function showToast(message, isError = false) {
754
+ const toast = document.getElementById('toast');
755
+ toast.textContent = message;
756
+ toast.className = 'toast show' + (isError ? ' error' : '');
757
+ setTimeout(() => toast.className = 'toast', 3000);
758
+ }
759
+
760
+ // 통계 로드
761
+ async function loadStats() {
762
+ stats = await api('/stats');
763
+ document.getElementById('stats').innerHTML = \`
764
+ <div class="stat-card">
765
+ <h3>\${t('totalMemories')}</h3>
766
+ <div class="value">\${stats.memories}</div>
767
+ </div>
768
+ <div class="stat-card">
769
+ <h3>\${t('totalSessions')}</h3>
770
+ <div class="value">\${stats.sessions}</div>
771
+ </div>
772
+ <div class="stat-card">
773
+ <h3>\${t('totalRelations')}</h3>
774
+ <div class="value">\${stats.relations}</div>
775
+ </div>
776
+ <div class="stat-card">
777
+ <h3>\${t('totalPatterns')}</h3>
778
+ <div class="value">\${stats.patterns}</div>
779
+ </div>
780
+ <div class="stat-card">
781
+ <h3>\${t('embeddings')}</h3>
782
+ <div class="value">\${stats.embeddings || 0}</div>
783
+ <div class="sub">\${t('coverage')}: \${stats.embeddingCoverage || 0}%</div>
784
+ <div class="embedding-bar"><div class="fill" style="width: \${stats.embeddingCoverage || 0}%"></div></div>
785
+ </div>
786
+ \`;
787
+ renderFilters();
788
+ loadContent();
789
+ }
790
+
791
+ // 필터 렌더링
792
+ function renderFilters() {
793
+ const projects = stats.projects?.map(p => p.project).filter(Boolean) || [];
794
+ const types = ['observation', 'decision', 'learning', 'error', 'pattern', 'preference'];
795
+
796
+ if (currentTab === 'memories') {
797
+ document.getElementById('filters').innerHTML = \`
798
+ <input type="text" id="search" placeholder="\${t('search')}" onkeyup="debounce(loadContent, 300)()">
799
+ <select id="type-filter" onchange="loadContent()">
800
+ <option value="">\${t('allTypes')}</option>
801
+ \${types.map(tp => \`<option value="\${tp}">\${t(tp)}</option>\`).join('')}
802
+ </select>
803
+ <select id="project-filter" onchange="loadContent()">
804
+ <option value="">\${t('allProjects')}</option>
805
+ \${projects.map(p => \`<option value="\${p}">\${p}</option>\`).join('')}
806
+ </select>
807
+ \`;
808
+ } else if (currentTab === 'sessions') {
809
+ document.getElementById('filters').innerHTML = \`
810
+ <select id="project-filter" onchange="loadContent()">
811
+ <option value="">\${t('allProjects')}</option>
812
+ \${projects.map(p => \`<option value="\${p}">\${p}</option>\`).join('')}
813
+ </select>
814
+ \`;
815
+ } else {
816
+ document.getElementById('filters').innerHTML = '';
817
+ }
818
+ }
819
+
820
+ // 디바운스
821
+ function debounce(fn, delay) {
822
+ let timeout;
823
+ return function() {
824
+ clearTimeout(timeout);
825
+ timeout = setTimeout(fn, delay);
826
+ };
827
+ }
828
+
829
+ // 컨텐츠 로드
830
+ async function loadContent() {
831
+ const content = document.getElementById('content');
832
+
833
+ if (currentTab === 'memories') {
834
+ const search = document.getElementById('search')?.value || '';
835
+ const type = document.getElementById('type-filter')?.value || '';
836
+ const project = document.getElementById('project-filter')?.value || '';
837
+
838
+ const params = new URLSearchParams();
839
+ if (search) params.set('search', search);
840
+ if (type) params.set('type', type);
841
+ if (project) params.set('project', project);
842
+
843
+ const memories = await api('/memories?' + params);
844
+
845
+ if (memories.length === 0) {
846
+ content.innerHTML = '<div class="empty">' + t('noMemories') + '</div>';
847
+ return;
848
+ }
849
+
850
+ content.innerHTML = \`
851
+ <table>
852
+ <thead>
853
+ <tr>
854
+ <th>\${t('id')}</th>
855
+ <th>\${t('type')}</th>
856
+ <th>\${t('content')}</th>
857
+ <th>\${t('tags')}</th>
858
+ <th>\${t('project')}</th>
859
+ <th>\${t('importance')}</th>
860
+ <th>\${t('created')}</th>
861
+ <th>\${t('actions')}</th>
862
+ </tr>
863
+ </thead>
864
+ <tbody>
865
+ \${memories.map(m => \`
866
+ <tr>
867
+ <td>\${m.id}</td>
868
+ <td><span class="tag \${m.memory_type}">\${t(m.memory_type)}</span></td>
869
+ <td class="truncate" title="\${m.content.replace(/"/g, '&quot;')}">\${m.content}</td>
870
+ <td>\${(JSON.parse(m.tags || '[]')).map(tg => \`<span class="tag">\${tg}</span>\`).join('')}</td>
871
+ <td>\${m.project || '-'}</td>
872
+ <td>
873
+ <span class="importance \${m.importance >= 8 ? 'high' : m.importance >= 5 ? 'medium' : 'low'}">
874
+ ⭐ \${m.importance}
875
+ </span>
876
+ </td>
877
+ <td>\${new Date(m.created_at).toLocaleDateString(currentLang)}</td>
878
+ <td class="actions">
879
+ <button class="btn btn-view" onclick="viewMemory(\${m.id})">\${t('view')}</button>
880
+ <button class="btn btn-edit" onclick="editMemory(\${m.id})">\${t('edit')}</button>
881
+ <button class="btn btn-delete" onclick="deleteMemory(\${m.id})">\${t('delete')}</button>
882
+ </td>
883
+ </tr>
884
+ \`).join('')}
885
+ </tbody>
886
+ </table>
887
+ \`;
888
+ } else if (currentTab === 'sessions') {
889
+ const project = document.getElementById('project-filter')?.value || '';
890
+ const params = new URLSearchParams();
891
+ if (project) params.set('project', project);
892
+
893
+ const sessions = await api('/sessions?' + params);
894
+
895
+ if (sessions.length === 0) {
896
+ content.innerHTML = '<div class="empty">' + t('noSessions') + '</div>';
897
+ return;
898
+ }
899
+
900
+ content.innerHTML = \`
901
+ <table>
902
+ <thead>
903
+ <tr>
904
+ <th>\${t('id')}</th>
905
+ <th>\${t('project')}</th>
906
+ <th>\${t('lastWork')}</th>
907
+ <th>\${t('status')}</th>
908
+ <th>\${t('verification')}</th>
909
+ <th>\${t('timestamp')}</th>
910
+ </tr>
911
+ </thead>
912
+ <tbody>
913
+ \${sessions.map(s => \`
914
+ <tr>
915
+ <td>\${s.id}</td>
916
+ <td>\${s.project}</td>
917
+ <td class="truncate">\${s.last_work}</td>
918
+ <td>\${s.current_status || '-'}</td>
919
+ <td>
920
+ <span class="tag \${s.verification_result === 'passed' ? 'pattern' : s.verification_result === 'failed' ? 'error' : ''}">\${s.verification_result || '-'}</span>
921
+ </td>
922
+ <td>\${new Date(s.timestamp).toLocaleString(currentLang)}</td>
923
+ </tr>
924
+ \`).join('')}
925
+ </tbody>
926
+ </table>
927
+ \`;
928
+ } else if (currentTab === 'relations') {
929
+ const relations = await api('/relations');
930
+
931
+ if (relations.length === 0) {
932
+ content.innerHTML = '<div class="empty">' + t('noRelations') + '</div>';
933
+ return;
934
+ }
935
+
936
+ content.innerHTML = \`
937
+ <table>
938
+ <thead>
939
+ <tr>
940
+ <th>\${t('source')}</th>
941
+ <th>\${t('relation')}</th>
942
+ <th>\${t('target')}</th>
943
+ <th>\${t('strength')}</th>
944
+ </tr>
945
+ </thead>
946
+ <tbody>
947
+ \${relations.map(r => \`
948
+ <tr>
949
+ <td>
950
+ <span class="tag \${r.source_type}">\${t(r.source_type)}</span>
951
+ <span class="truncate" style="display: block; max-width: 300px;">\${r.source_content}</span>
952
+ </td>
953
+ <td><strong>\${r.relation_type}</strong></td>
954
+ <td>
955
+ <span class="tag \${r.target_type}">\${t(r.target_type)}</span>
956
+ <span class="truncate" style="display: block; max-width: 300px;">\${r.target_content}</span>
957
+ </td>
958
+ <td>\${r.strength}</td>
959
+ </tr>
960
+ \`).join('')}
961
+ </tbody>
962
+ </table>
963
+ \`;
964
+ } else if (currentTab === 'embeddings') {
965
+ content.innerHTML = \`
966
+ <div style="padding: 40px; text-align: center;">
967
+ <h2 style="margin-bottom: 24px;">🔮 \${t('embeddings')} \${t('status')}</h2>
968
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; max-width: 600px; margin: 0 auto;">
969
+ <div class="stat-card">
970
+ <h3>\${t('modelStatus')}</h3>
971
+ <div class="value" style="font-size: 1.2rem; color: #10b981;">all-MiniLM-L6-v2</div>
972
+ </div>
973
+ <div class="stat-card">
974
+ <h3>\${t('dimensions')}</h3>
975
+ <div class="value">384</div>
976
+ </div>
977
+ <div class="stat-card">
978
+ <h3>\${t('coverage')}</h3>
979
+ <div class="value">\${stats.embeddingCoverage || 0}%</div>
980
+ </div>
981
+ </div>
982
+ <div style="margin-top: 32px; color: #94a3b8;">
983
+ <p>\${stats.embeddings || 0} / \${stats.memories} memories embedded</p>
984
+ <div class="embedding-bar" style="max-width: 400px; margin: 16px auto; height: 12px;">
985
+ <div class="fill" style="width: \${stats.embeddingCoverage || 0}%"></div>
986
+ </div>
987
+ </div>
988
+ </div>
989
+ \`;
990
+ }
991
+ }
992
+
993
+ // 메모리 보기
994
+ async function viewMemory(id) {
995
+ const m = await api('/memories/' + id);
996
+ const tags = JSON.parse(m.tags || '[]');
997
+
998
+ document.getElementById('modal-content').innerHTML = \`
999
+ <h2>🧠 Memory #\${m.id}</h2>
1000
+ <div style="margin-bottom: 16px;">
1001
+ <span class="tag \${m.memory_type}">\${t(m.memory_type)}</span>
1002
+ <span class="importance \${m.importance >= 8 ? 'high' : m.importance >= 5 ? 'medium' : 'low'}">⭐ \${m.importance}</span>
1003
+ </div>
1004
+ <label>\${t('content')}</label>
1005
+ <div style="background: #0f172a; padding: 16px; border-radius: 8px; margin-bottom: 16px; white-space: pre-wrap;">\${m.content}</div>
1006
+ <label>\${t('tags')}</label>
1007
+ <div style="margin-bottom: 16px;">\${tags.map(tg => \`<span class="tag">\${tg}</span>\`).join(' ') || 'No tags'}</div>
1008
+ <label>\${t('project')}</label>
1009
+ <div style="margin-bottom: 16px;">\${m.project || 'None'}</div>
1010
+ <label>\${t('created')}</label>
1011
+ <div style="margin-bottom: 16px;">\${new Date(m.created_at).toLocaleString(currentLang)}</div>
1012
+ <div class="modal-actions">
1013
+ <button class="btn btn-secondary" onclick="closeModal()">\${t('close')}</button>
1014
+ <button class="btn btn-primary" onclick="editMemory(\${m.id})">\${t('edit')}</button>
1015
+ </div>
1016
+ \`;
1017
+ document.getElementById('modal').classList.add('active');
1018
+ }
1019
+
1020
+ // 메모리 편집
1021
+ async function editMemory(id) {
1022
+ const m = await api('/memories/' + id);
1023
+ const tags = JSON.parse(m.tags || '[]');
1024
+ const types = ['observation', 'decision', 'learning', 'error', 'pattern', 'preference'];
1025
+
1026
+ document.getElementById('modal-content').innerHTML = \`
1027
+ <h2>✏️ \${t('edit')} Memory #\${m.id}</h2>
1028
+ <label>\${t('content')}</label>
1029
+ <textarea id="edit-content">\${m.content}</textarea>
1030
+ <label>\${t('type')}</label>
1031
+ <select id="edit-type">
1032
+ \${types.map(tp => \`<option value="\${tp}" \${tp === m.memory_type ? 'selected' : ''}>\${t(tp)}</option>\`).join('')}
1033
+ </select>
1034
+ <label>\${t('tags')} (comma-separated)</label>
1035
+ <input type="text" id="edit-tags" value="\${tags.join(', ')}">
1036
+ <label>\${t('importance')} (1-10)</label>
1037
+ <input type="number" id="edit-importance" value="\${m.importance}" min="1" max="10">
1038
+ <div class="modal-actions">
1039
+ <button class="btn btn-secondary" onclick="closeModal()">\${t('cancel')}</button>
1040
+ <button class="btn btn-primary" onclick="saveMemory(\${m.id})">\${t('update')}</button>
1041
+ </div>
1042
+ \`;
1043
+ document.getElementById('modal').classList.add('active');
1044
+ }
1045
+
1046
+ // 메모리 저장
1047
+ async function saveMemory(id) {
1048
+ const content = document.getElementById('edit-content').value;
1049
+ const memory_type = document.getElementById('edit-type').value;
1050
+ const tags = document.getElementById('edit-tags').value.split(',').map(tg => tg.trim()).filter(Boolean);
1051
+ const importance = parseInt(document.getElementById('edit-importance').value);
1052
+
1053
+ await api('/memories/' + id, {
1054
+ method: 'PUT',
1055
+ body: JSON.stringify({ content, memory_type, tags, importance })
1056
+ });
1057
+
1058
+ closeModal();
1059
+ showToast(t('updated'));
1060
+ loadContent();
1061
+ }
1062
+
1063
+ // 메모리 삭제
1064
+ async function deleteMemory(id) {
1065
+ if (!confirm(t('confirmDelete'))) return;
1066
+
1067
+ await api('/memories/' + id, { method: 'DELETE' });
1068
+ showToast(t('deleted'));
1069
+ loadStats();
1070
+ loadContent();
1071
+ }
1072
+
1073
+ // 모달 닫기
1074
+ function closeModal() {
1075
+ document.getElementById('modal').classList.remove('active');
1076
+ }
1077
+
1078
+ // ESC로 모달 닫기
1079
+ document.addEventListener('keydown', e => {
1080
+ if (e.key === 'Escape') closeModal();
1081
+ });
1082
+
1083
+ // 모달 외부 클릭으로 닫기
1084
+ document.getElementById('modal').addEventListener('click', e => {
1085
+ if (e.target.id === 'modal') closeModal();
1086
+ });
1087
+
1088
+ // 초기화
1089
+ document.getElementById('lang-select').value = currentLang;
1090
+ updateUI();
1091
+ </script>
1092
+ </body>
1093
+ </html>`;
1094
+ // ===== HTTP 서버 =====
1095
+ const server = http.createServer(async (req, res) => {
1096
+ const parsedUrl = url.parse(req.url || '', true);
1097
+ const pathname = parsedUrl.pathname || '/';
1098
+ // CORS
1099
+ res.setHeader('Access-Control-Allow-Origin', '*');
1100
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1101
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1102
+ if (req.method === 'OPTIONS') {
1103
+ res.writeHead(200);
1104
+ res.end();
1105
+ return;
1106
+ }
1107
+ // JSON 응답 헬퍼
1108
+ const json = (data, status = 200) => {
1109
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1110
+ res.end(JSON.stringify(data));
1111
+ };
1112
+ // Body 파싱
1113
+ const parseBody = () => {
1114
+ return new Promise((resolve) => {
1115
+ let body = '';
1116
+ req.on('data', chunk => body += chunk);
1117
+ req.on('end', () => {
1118
+ try {
1119
+ resolve(JSON.parse(body));
1120
+ }
1121
+ catch {
1122
+ resolve({});
1123
+ }
1124
+ });
1125
+ });
1126
+ };
1127
+ try {
1128
+ // API 라우팅
1129
+ if (pathname.startsWith('/api')) {
1130
+ const apiPath = pathname.slice(4);
1131
+ const params = new URLSearchParams(parsedUrl.search || '');
1132
+ // GET /api/stats
1133
+ if (apiPath === '/stats' && req.method === 'GET') {
1134
+ return json(getStats());
1135
+ }
1136
+ // GET /api/memories
1137
+ if (apiPath === '/memories' && req.method === 'GET') {
1138
+ return json(getMemories(params));
1139
+ }
1140
+ // GET /api/memories/:id
1141
+ const memoryMatch = apiPath.match(/^\/memories\/(\d+)$/);
1142
+ if (memoryMatch) {
1143
+ const id = parseInt(memoryMatch[1]);
1144
+ if (req.method === 'GET') {
1145
+ return json(getMemory(id));
1146
+ }
1147
+ if (req.method === 'PUT') {
1148
+ const body = await parseBody();
1149
+ return json(updateMemory(id, body));
1150
+ }
1151
+ if (req.method === 'DELETE') {
1152
+ return json(deleteMemoryById(id));
1153
+ }
1154
+ }
1155
+ // GET /api/sessions
1156
+ if (apiPath === '/sessions' && req.method === 'GET') {
1157
+ return json(getSessions(params));
1158
+ }
1159
+ // GET /api/relations
1160
+ if (apiPath === '/relations' && req.method === 'GET') {
1161
+ const memoryId = params.get('memoryId');
1162
+ return json(getRelations(memoryId ? parseInt(memoryId) : undefined));
1163
+ }
1164
+ return json({ error: 'Not found' }, 404);
1165
+ }
1166
+ // HTML 페이지
1167
+ if (pathname === '/' || pathname === '/index.html') {
1168
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1169
+ res.end(HTML_TEMPLATE);
1170
+ return;
1171
+ }
1172
+ // 404
1173
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1174
+ res.end('Not Found');
1175
+ }
1176
+ catch (error) {
1177
+ console.error('Error:', error);
1178
+ json({ error: String(error) }, 500);
1179
+ }
1180
+ });
1181
+ server.listen(PORT, '127.0.0.1', () => {
1182
+ console.log(`
1183
+ ╔══════════════════════════════════════════════════════════════╗
1184
+ ║ ║
1185
+ ║ 🧠 Project Manager MCP Dashboard ║
1186
+ ║ ║
1187
+ ║ Open: http://127.0.0.1:${PORT} ║
1188
+ ║ DB: ${DB_PATH}
1189
+ ║ ║
1190
+ ║ Languages: EN | KO | JA | ZH | DE | FR | ES ║
1191
+ ║ ║
1192
+ ║ Press Ctrl+C to stop ║
1193
+ ║ ║
1194
+ ╚══════════════════════════════════════════════════════════════╝
1195
+ `);
1196
+ });