memorix 1.0.6 → 1.0.7
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/CHANGELOG.md +335 -245
- package/README.md +31 -32
- package/README.zh-CN.md +20 -14
- package/dist/cli/index.js +21258 -13789
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +969 -197
- package/dist/dashboard/static/index.html +10 -3
- package/dist/dashboard/static/style.css +46 -0
- package/dist/index.js +5853 -2260
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +396 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/package.json +25 -4
|
@@ -43,6 +43,8 @@ const i18n = {
|
|
|
43
43
|
noObsTitle: 'No Observations',
|
|
44
44
|
noObsDesc: 'Use memorix_store to create observations',
|
|
45
45
|
untitled: 'Untitled',
|
|
46
|
+
unknown: 'unknown',
|
|
47
|
+
lowQuality: 'cleanup candidate',
|
|
46
48
|
exportData: 'Export',
|
|
47
49
|
deleteObs: 'Delete',
|
|
48
50
|
deleteConfirm: 'Delete observation #%id%?',
|
|
@@ -50,6 +52,7 @@ const i18n = {
|
|
|
50
52
|
selected: 'selected',
|
|
51
53
|
cancel: 'Cancel',
|
|
52
54
|
deleteSelected: 'Delete Selected',
|
|
55
|
+
deleteFailed: 'Delete failed',
|
|
53
56
|
batchDeleteConfirm: 'Delete %count% observations?',
|
|
54
57
|
deleted: 'Deleted',
|
|
55
58
|
narrative: 'Narrative',
|
|
@@ -93,11 +96,11 @@ const i18n = {
|
|
|
93
96
|
noRetentionData: 'No Retention Data',
|
|
94
97
|
noRetentionDesc: 'Store observations to see memory retention scores',
|
|
95
98
|
|
|
96
|
-
// Team
|
|
97
|
-
teamTitle: '
|
|
98
|
-
teamSubtitle: '
|
|
99
|
-
teamNoData: '
|
|
100
|
-
teamNoDataHint: '
|
|
99
|
+
// Team → Collaboration Space
|
|
100
|
+
teamTitle: 'Collaboration',
|
|
101
|
+
teamSubtitle: 'Project collaboration status — who\'s working, what\'s pending',
|
|
102
|
+
teamNoData: 'Collaboration requires HTTP transport',
|
|
103
|
+
teamNoDataHint: 'Project collaboration (agents, file locks, tasks) requires the HTTP transport. Start it with:',
|
|
101
104
|
teamActiveAgents: 'Active Agents',
|
|
102
105
|
teamLockedFiles: 'Locked Files',
|
|
103
106
|
teamTasks: 'Tasks',
|
|
@@ -105,6 +108,17 @@ const i18n = {
|
|
|
105
108
|
teamAgents: 'Agents',
|
|
106
109
|
teamLocks: 'File Locks',
|
|
107
110
|
teamTaskBoard: 'Task Board',
|
|
111
|
+
// Resume area
|
|
112
|
+
resumeTitle: 'Continue This Project',
|
|
113
|
+
resumeDesc: 'What needs attention right now',
|
|
114
|
+
resumeOpenTasks: 'Open tasks',
|
|
115
|
+
resumeAvailableTasks: 'Available to claim',
|
|
116
|
+
resumeOpenHandoffs: 'Pending handoffs',
|
|
117
|
+
resumeUnreadMessages: 'Unread messages',
|
|
118
|
+
resumeActiveLocks: 'Active locks',
|
|
119
|
+
resumeActiveAgents: 'Active agents',
|
|
120
|
+
resumeAllClear: 'All clear — nothing pending',
|
|
121
|
+
resumeAllClearDesc: 'No open tasks, handoffs, or unread messages',
|
|
108
122
|
|
|
109
123
|
// Overview (new)
|
|
110
124
|
memoryControlPlane: 'Memory Control Plane',
|
|
@@ -115,7 +129,7 @@ const i18n = {
|
|
|
115
129
|
thisWeek: 'this week',
|
|
116
130
|
hooksAndMcp: 'hooks + MCP',
|
|
117
131
|
memorySources: 'Memory Sources',
|
|
118
|
-
retentionHealth: 'Retention
|
|
132
|
+
retentionHealth: 'Retention Summary',
|
|
119
133
|
sourceGit: 'Git',
|
|
120
134
|
sourceAgent: 'Agent',
|
|
121
135
|
sourceManual: 'Manual',
|
|
@@ -154,8 +168,8 @@ const i18n = {
|
|
|
154
168
|
identityTitle: 'Project Identity Health',
|
|
155
169
|
identitySubtitle: 'Project ID stability, aliases, and cross-agent consistency',
|
|
156
170
|
healthStatus: 'Health Status',
|
|
157
|
-
healthy: '
|
|
158
|
-
unhealthy: '
|
|
171
|
+
healthy: 'Healthy',
|
|
172
|
+
unhealthy: 'Issues',
|
|
159
173
|
knownProjectIds: 'Known Project IDs',
|
|
160
174
|
aliasGroups: 'Alias Groups',
|
|
161
175
|
dirtyIds: 'Dirty IDs',
|
|
@@ -173,8 +187,8 @@ const i18n = {
|
|
|
173
187
|
identityUnavailable: 'Identity Unavailable',
|
|
174
188
|
identityUnavailableDesc: 'Could not load project identity data',
|
|
175
189
|
|
|
176
|
-
// System
|
|
177
|
-
|
|
190
|
+
// System Status
|
|
191
|
+
systemStatus: 'System Status',
|
|
178
192
|
searchMode: 'Search Mode',
|
|
179
193
|
embeddingProvider: 'Embedding Provider',
|
|
180
194
|
backfillPending: 'Backfill Pending',
|
|
@@ -185,7 +199,138 @@ const i18n = {
|
|
|
185
199
|
providerDisabled: 'Disabled (BM25 only)',
|
|
186
200
|
degradedHint: 'Search is degraded — no vector similarity',
|
|
187
201
|
|
|
188
|
-
//
|
|
202
|
+
// Project states
|
|
203
|
+
noProjects: 'No projects',
|
|
204
|
+
error: 'Error',
|
|
205
|
+
projectUnresolved: 'Unresolved',
|
|
206
|
+
projectUnresolvedDesc: 'No project bound — select a project from the switcher',
|
|
207
|
+
projectResolved: 'Resolved',
|
|
208
|
+
projectScopeProject: 'Project Collaboration',
|
|
209
|
+
projectScopeGlobal: 'All Projects',
|
|
210
|
+
projectScopeProjectDesc: 'Current project\'s collaboration space',
|
|
211
|
+
projectScopeGlobalDesc: 'All agents across projects',
|
|
212
|
+
|
|
213
|
+
// Team (additional)
|
|
214
|
+
teamMessages: 'Messages',
|
|
215
|
+
teamAllRead: 'All read',
|
|
216
|
+
teamUnread: 'unread',
|
|
217
|
+
teamNoAgentsProject: 'No agents in this session',
|
|
218
|
+
teamNoAgentsGlobal: 'No agents in any scope',
|
|
219
|
+
teamNoFilesLocked: 'No files locked',
|
|
220
|
+
teamNoTasksCreated: 'No tasks created',
|
|
221
|
+
teamPending: 'pending',
|
|
222
|
+
teamActive: 'active',
|
|
223
|
+
teamDone: 'done',
|
|
224
|
+
teamRefresh: 'Refresh',
|
|
225
|
+
teamProjectBtn: 'Project',
|
|
226
|
+
teamGlobalBtn: 'Global',
|
|
227
|
+
teamOffline: 'offline',
|
|
228
|
+
teamLive: 'live',
|
|
229
|
+
teamFile: 'file',
|
|
230
|
+
// H1/H2: Activity layering
|
|
231
|
+
teamTierActive: 'Active',
|
|
232
|
+
teamTierRecent: 'Recent',
|
|
233
|
+
teamTierHistorical: 'Historical',
|
|
234
|
+
teamTierAll: 'All',
|
|
235
|
+
teamRecentCount: 'recent',
|
|
236
|
+
teamHistoricalCount: 'historical',
|
|
237
|
+
teamHistoricalTotal: 'Historical total',
|
|
238
|
+
teamHistoricalHint: 'Inactive for more than 7 days. Not current collaborators.',
|
|
239
|
+
teamRecentHint: 'Inactive, last seen within 7 days.',
|
|
240
|
+
teamShowHistorical: 'Show historical',
|
|
241
|
+
teamHideHistorical: 'Hide historical',
|
|
242
|
+
teamNoActiveNow: 'No active agents right now',
|
|
243
|
+
teamNoRecent: 'No agents seen in the last 7 days',
|
|
244
|
+
teamSummaryHint: 'Headline shows currently active. Historical rows are collapsed by default.',
|
|
245
|
+
// Resume area
|
|
246
|
+
resumeTitle: 'Continue This Project',
|
|
247
|
+
resumeDesc: 'Pick up where you left off',
|
|
248
|
+
resumeOpenTasks: 'Open tasks',
|
|
249
|
+
resumeAvailableTasks: 'available to claim',
|
|
250
|
+
resumeOpenHandoffs: 'Open handoffs',
|
|
251
|
+
resumeUnreadMessages: 'Unread messages',
|
|
252
|
+
resumeActiveLocks: 'Active locks',
|
|
253
|
+
resumeActiveAgents: 'Active agents',
|
|
254
|
+
resumeAllClear: 'All clear',
|
|
255
|
+
resumeAllClearDesc: 'No pending tasks, handoffs, or locks',
|
|
256
|
+
// H3: Identity layering
|
|
257
|
+
identityHealthPrimary: 'Current project health',
|
|
258
|
+
identityRealProjects: 'Real projects',
|
|
259
|
+
identityTemporary: 'Temporary / smoke / demo',
|
|
260
|
+
identityPlaceholder: 'Placeholder / broken',
|
|
261
|
+
identityUnmergedFragments: 'Unmerged real fragments',
|
|
262
|
+
identityAliasGroupsReal: 'Alias groups (real projects only)',
|
|
263
|
+
identityAliasGroupsAll: 'Alias groups (all, including historical)',
|
|
264
|
+
identityHistoricalFold: 'Historical / temporary project IDs',
|
|
265
|
+
identityHistoricalNote: 'These include smoke/demo/test scratch projects and placeholder IDs. Not counted toward current health.',
|
|
266
|
+
identityShowHistorical: 'Show historical project IDs',
|
|
267
|
+
identityHideHistorical: 'Hide historical',
|
|
268
|
+
// H4/H5: Project switcher groups
|
|
269
|
+
switcherGroupCurrent: 'Current',
|
|
270
|
+
switcherGroupReal: 'Real projects',
|
|
271
|
+
switcherGroupTemporary: 'Temporary / smoke / demo',
|
|
272
|
+
switcherGroupPlaceholder: 'Placeholder',
|
|
273
|
+
switcherShowTemp: 'Show temporary',
|
|
274
|
+
switcherHideTemp: 'Hide temporary',
|
|
275
|
+
switcherTempCount: 'temp',
|
|
276
|
+
teamNoRole: 'no role',
|
|
277
|
+
teamAvailableToClaim: 'available to claim',
|
|
278
|
+
teamActiveCount: 'active',
|
|
279
|
+
teamRoles: 'Roles',
|
|
280
|
+
teamDefined: 'Defined',
|
|
281
|
+
teamHandoffs: 'Handoffs',
|
|
282
|
+
teamOpen: 'Open',
|
|
283
|
+
taskPending: 'Pending',
|
|
284
|
+
taskInProgress: 'In Progress',
|
|
285
|
+
taskCompleted: 'Done',
|
|
286
|
+
taskFailed: 'Failed',
|
|
287
|
+
|
|
288
|
+
// Graph (additional)
|
|
289
|
+
graphIsolatedEntities: 'Isolated Entities',
|
|
290
|
+
graphIsolatedDesc: 'entities with no relations — shown separately for readability',
|
|
291
|
+
graphConnected: 'Connected',
|
|
292
|
+
graphNeighborhood: 'Neighborhood',
|
|
293
|
+
graphFullGraph: 'Full Graph',
|
|
294
|
+
graphTopology: 'Topology',
|
|
295
|
+
graphTable: 'Table',
|
|
296
|
+
graphSearch: 'Search',
|
|
297
|
+
graphScope: 'Scope',
|
|
298
|
+
graphView: 'View',
|
|
299
|
+
graphLayout: 'Layout',
|
|
300
|
+
graphEntityType: 'Entity Type',
|
|
301
|
+
graphDepth: 'Depth',
|
|
302
|
+
graphConnections: 'Connections',
|
|
303
|
+
graphEvidence: 'Evidence',
|
|
304
|
+
graphObservations: 'Observations',
|
|
305
|
+
graphRelations: 'Relations',
|
|
306
|
+
graphSparseWarning: 'Sparse graph: %isolated% of %total% entities have no relations. Isolated entities shown in inventory below.',
|
|
307
|
+
graphNoRelations: 'No relations — isolated entities only',
|
|
308
|
+
graphNoRelationsDesc: 'This project has entities but no relations between them. All entities are shown in the inventory panel below.',
|
|
309
|
+
graphEmptyState: 'No Graph Data',
|
|
310
|
+
graphEmptyStateDesc: 'Create entities and relations to see your knowledge graph',
|
|
311
|
+
graphSelectNode: 'Select a node to inspect',
|
|
312
|
+
graphFindEntity: 'Find entity...',
|
|
313
|
+
graphLeftToRight: 'Left → Right',
|
|
314
|
+
graphTopToBottom: 'Top → Bottom',
|
|
315
|
+
graphMore: 'more',
|
|
316
|
+
|
|
317
|
+
// Identity (additional)
|
|
318
|
+
identityCurrentProject: 'Current Project',
|
|
319
|
+
identityHistoricalProjects: 'Historical Projects',
|
|
320
|
+
identityProjectBound: 'Project bound and healthy',
|
|
321
|
+
identityProjectUnbound: 'Project not bound — select a project',
|
|
322
|
+
identityDirtyCurrentWarning: 'Current project has dirty IDs that need resolution',
|
|
323
|
+
identityDirtyHistoricalNote: 'Dirty IDs from other projects (not affecting current project)',
|
|
324
|
+
identityTotalCount: 'total',
|
|
325
|
+
|
|
326
|
+
// Config (additional)
|
|
327
|
+
configProjectUnavailable: 'Project config unavailable',
|
|
328
|
+
configProjectUnavailableDesc: 'Project root path is unknown — project-scoped config files cannot be resolved',
|
|
329
|
+
|
|
330
|
+
// Git Memory (additional)
|
|
331
|
+
gitMemoryTotal: 'total',
|
|
332
|
+
|
|
333
|
+
// Nav tooltips + labels
|
|
189
334
|
navDashboard: 'Dashboard',
|
|
190
335
|
navGitMemory: 'Git Memory',
|
|
191
336
|
navGraph: 'Knowledge Graph',
|
|
@@ -194,7 +339,44 @@ const i18n = {
|
|
|
194
339
|
navConfig: 'Config',
|
|
195
340
|
navIdentity: 'Identity',
|
|
196
341
|
navSessions: 'Sessions',
|
|
197
|
-
navTeam: '
|
|
342
|
+
navTeam: 'Collaboration',
|
|
343
|
+
navLabelDashboard: 'Overview',
|
|
344
|
+
navLabelGitMemory: 'Git Memory',
|
|
345
|
+
navLabelGraph: 'Graph',
|
|
346
|
+
navLabelObservations: 'Observations',
|
|
347
|
+
navLabelRetention: 'Retention',
|
|
348
|
+
navLabelConfig: 'Config',
|
|
349
|
+
navLabelIdentity: 'Identity',
|
|
350
|
+
navLabelSessions: 'Sessions',
|
|
351
|
+
navLabelTeam: 'Collaboration',
|
|
352
|
+
sectionCore: 'CORE',
|
|
353
|
+
sectionHealth: 'HEALTH',
|
|
354
|
+
sectionCollaboration: 'COLLABORATION',
|
|
355
|
+
themeDark: 'Dark',
|
|
356
|
+
themeLight: 'Light',
|
|
357
|
+
loading: 'Loading...',
|
|
358
|
+
searchProjects: 'Search projects...',
|
|
359
|
+
|
|
360
|
+
// Mode banner
|
|
361
|
+
modeStandalone: 'Standalone',
|
|
362
|
+
modeControlPlane: 'Control Plane',
|
|
363
|
+
modeStandaloneHint: 'No live MCP — team features unavailable',
|
|
364
|
+
modeControlPlaneHint: 'Full MCP + team collaboration',
|
|
365
|
+
modeBannerProject: 'Project',
|
|
366
|
+
modeBannerMcp: 'MCP',
|
|
367
|
+
|
|
368
|
+
// Time ago
|
|
369
|
+
timeAgoS: 's ago',
|
|
370
|
+
timeAgoM: 'm ago',
|
|
371
|
+
timeAgoH: 'h ago',
|
|
372
|
+
timeAgoD: 'd ago',
|
|
373
|
+
timeExpired: 'expired',
|
|
374
|
+
timeLeft: 'm left',
|
|
375
|
+
teamJoined: 'joined',
|
|
376
|
+
teamSeen: 'seen',
|
|
377
|
+
teamLeft: 'left',
|
|
378
|
+
persistedAgentsAvailable: 'persisted agent(s) available in',
|
|
379
|
+
sessionsCount: 'session(s)',
|
|
198
380
|
},
|
|
199
381
|
zh: {
|
|
200
382
|
// Dashboard
|
|
@@ -231,13 +413,16 @@ const i18n = {
|
|
|
231
413
|
noObsTitle: '暂无观察记录',
|
|
232
414
|
noObsDesc: '使用 memorix_store 创建观察记录',
|
|
233
415
|
untitled: '无标题',
|
|
416
|
+
unknown: '未知',
|
|
417
|
+
lowQuality: '清理候选',
|
|
234
418
|
exportData: '导出',
|
|
235
419
|
deleteObs: '删除',
|
|
236
420
|
deleteConfirm: '确认删除观察 #%id%?',
|
|
237
421
|
batchCleanup: '清理',
|
|
238
422
|
selected: '已选中',
|
|
239
423
|
cancel: '取消',
|
|
240
|
-
deleteSelected: '
|
|
424
|
+
deleteSelected: '删除所选',
|
|
425
|
+
deleteFailed: '删除失败',
|
|
241
426
|
batchDeleteConfirm: '确认删除 %count% 条观察?',
|
|
242
427
|
deleted: '已删除',
|
|
243
428
|
narrative: '叙述',
|
|
@@ -281,11 +466,11 @@ const i18n = {
|
|
|
281
466
|
noRetentionData: '暂无衰减数据',
|
|
282
467
|
noRetentionDesc: '存储观察记录以查看记忆衰减分数',
|
|
283
468
|
|
|
284
|
-
// Team
|
|
285
|
-
teamTitle: '
|
|
286
|
-
teamSubtitle: '
|
|
287
|
-
teamNoData: '
|
|
288
|
-
teamNoDataHint: '
|
|
469
|
+
// Team → 协作空间
|
|
470
|
+
teamTitle: '协作',
|
|
471
|
+
teamSubtitle: '项目协作状态 — 谁在工作、什么待处理',
|
|
472
|
+
teamNoData: '协作功能需要 HTTP 传输',
|
|
473
|
+
teamNoDataHint: '项目协作(Agent 注册、文件锁、任务看板)需要 HTTP 传输模式。使用以下命令启动:',
|
|
289
474
|
teamActiveAgents: '活跃 Agent',
|
|
290
475
|
teamLockedFiles: '锁定文件',
|
|
291
476
|
teamTasks: '任务',
|
|
@@ -293,6 +478,17 @@ const i18n = {
|
|
|
293
478
|
teamAgents: 'Agent 列表',
|
|
294
479
|
teamLocks: '文件锁',
|
|
295
480
|
teamTaskBoard: '任务看板',
|
|
481
|
+
// Resume area
|
|
482
|
+
resumeTitle: '继续这个项目',
|
|
483
|
+
resumeDesc: '当前需要关注的事项',
|
|
484
|
+
resumeOpenTasks: '待处理任务',
|
|
485
|
+
resumeAvailableTasks: '可领取任务',
|
|
486
|
+
resumeOpenHandoffs: '待接交接',
|
|
487
|
+
resumeUnreadMessages: '未读消息',
|
|
488
|
+
resumeActiveLocks: '活跃锁',
|
|
489
|
+
resumeActiveAgents: '活跃 Agent',
|
|
490
|
+
resumeAllClear: '全部就绪 — 无待处理项',
|
|
491
|
+
resumeAllClearDesc: '无待处理任务、交接或未读消息',
|
|
296
492
|
|
|
297
493
|
// Overview (new)
|
|
298
494
|
memoryControlPlane: '记忆控制台',
|
|
@@ -303,7 +499,7 @@ const i18n = {
|
|
|
303
499
|
thisWeek: '本周新增',
|
|
304
500
|
hooksAndMcp: 'hooks + MCP',
|
|
305
501
|
memorySources: '记忆来源',
|
|
306
|
-
retentionHealth: '
|
|
502
|
+
retentionHealth: '保留摘要',
|
|
307
503
|
sourceGit: 'Git',
|
|
308
504
|
sourceAgent: 'Agent',
|
|
309
505
|
sourceManual: '手动',
|
|
@@ -342,8 +538,8 @@ const i18n = {
|
|
|
342
538
|
identityTitle: '项目身份健康度',
|
|
343
539
|
identitySubtitle: '项目 ID 稳定性、别名和跨 Agent 一致性',
|
|
344
540
|
healthStatus: '健康状态',
|
|
345
|
-
healthy: '
|
|
346
|
-
unhealthy: '
|
|
541
|
+
healthy: '健康',
|
|
542
|
+
unhealthy: '存在问题',
|
|
347
543
|
knownProjectIds: '已知项目 ID',
|
|
348
544
|
aliasGroups: '别名组',
|
|
349
545
|
dirtyIds: '脏 ID',
|
|
@@ -361,8 +557,8 @@ const i18n = {
|
|
|
361
557
|
identityUnavailable: '身份信息不可用',
|
|
362
558
|
identityUnavailableDesc: '无法加载项目身份数据',
|
|
363
559
|
|
|
364
|
-
// System
|
|
365
|
-
|
|
560
|
+
// System Status
|
|
561
|
+
systemStatus: '系统状态',
|
|
366
562
|
searchMode: '搜索模式',
|
|
367
563
|
embeddingProvider: '向量提供者',
|
|
368
564
|
backfillPending: '回填待处理',
|
|
@@ -373,6 +569,142 @@ const i18n = {
|
|
|
373
569
|
providerDisabled: '已禁用 (仅 BM25)',
|
|
374
570
|
degradedHint: '搜索已降级 — 无向量相似性',
|
|
375
571
|
|
|
572
|
+
// Project states
|
|
573
|
+
noProjects: '无项目',
|
|
574
|
+
error: '错误',
|
|
575
|
+
projectUnresolved: '未绑定',
|
|
576
|
+
projectUnresolvedDesc: '无项目绑定 — 请从切换器选择项目',
|
|
577
|
+
projectResolved: '已绑定',
|
|
578
|
+
projectScopeProject: '项目协作',
|
|
579
|
+
projectScopeGlobal: '所有项目',
|
|
580
|
+
projectScopeProjectDesc: '当前项目的协作空间',
|
|
581
|
+
projectScopeGlobalDesc: '所有项目中的 Agent',
|
|
582
|
+
|
|
583
|
+
// Team (additional)
|
|
584
|
+
teamMessages: '消息',
|
|
585
|
+
teamAllRead: '全部已读',
|
|
586
|
+
teamUnread: '未读',
|
|
587
|
+
teamNoAgentsProject: '当前会话无 Agent',
|
|
588
|
+
teamNoAgentsGlobal: '任何范围均无 Agent',
|
|
589
|
+
teamNoFilesLocked: '无文件锁定',
|
|
590
|
+
teamNoTasksCreated: '无已创建任务',
|
|
591
|
+
teamPending: '待处理',
|
|
592
|
+
teamActive: '进行中',
|
|
593
|
+
teamDone: '已完成',
|
|
594
|
+
teamRefresh: '刷新',
|
|
595
|
+
teamProjectBtn: '项目',
|
|
596
|
+
teamGlobalBtn: '全局',
|
|
597
|
+
teamOffline: '离线',
|
|
598
|
+
teamLive: '在线',
|
|
599
|
+
teamFile: '文件',
|
|
600
|
+
// H1/H2: 活跃度分层
|
|
601
|
+
teamTierActive: '活跃',
|
|
602
|
+
teamTierRecent: '近期',
|
|
603
|
+
teamTierHistorical: '历史',
|
|
604
|
+
teamTierAll: '全部',
|
|
605
|
+
teamRecentCount: '近期',
|
|
606
|
+
teamHistoricalCount: '历史',
|
|
607
|
+
teamHistoricalTotal: '历史累计',
|
|
608
|
+
teamHistoricalHint: '超过 7 天无活动,非当前协作成员',
|
|
609
|
+
teamRecentHint: '未活跃,最近 7 天内有过心跳',
|
|
610
|
+
teamShowHistorical: '显示历史',
|
|
611
|
+
teamHideHistorical: '隐藏历史',
|
|
612
|
+
teamNoActiveNow: '当前无活跃 Agent',
|
|
613
|
+
teamNoRecent: '最近 7 天无 Agent 活动',
|
|
614
|
+
teamSummaryHint: '标题显示当前活跃数。历史数据默认折叠。',
|
|
615
|
+
// Resume area
|
|
616
|
+
resumeTitle: '继续此项目',
|
|
617
|
+
resumeDesc: '从上次中断处继续',
|
|
618
|
+
resumeOpenTasks: '待办任务',
|
|
619
|
+
resumeAvailableTasks: '可认领',
|
|
620
|
+
resumeOpenHandoffs: '待接手',
|
|
621
|
+
resumeUnreadMessages: '未读消息',
|
|
622
|
+
resumeActiveLocks: '文件锁',
|
|
623
|
+
resumeActiveAgents: '活跃 Agent',
|
|
624
|
+
resumeAllClear: '一切就绪',
|
|
625
|
+
resumeAllClearDesc: '无待办任务、待接手或文件锁',
|
|
626
|
+
// H3: Identity 分层
|
|
627
|
+
identityHealthPrimary: '当前项目健康',
|
|
628
|
+
identityRealProjects: '真实项目',
|
|
629
|
+
identityTemporary: '临时 / smoke / 演示',
|
|
630
|
+
identityPlaceholder: 'Placeholder / 损坏',
|
|
631
|
+
identityUnmergedFragments: '未合并的真实项目碎片',
|
|
632
|
+
identityAliasGroupsReal: '别名组(仅真实项目)',
|
|
633
|
+
identityAliasGroupsAll: '别名组(全部,含历史)',
|
|
634
|
+
identityHistoricalFold: '历史 / 临时项目 ID',
|
|
635
|
+
identityHistoricalNote: '包含 smoke/demo/测试临时项目与 placeholder ID,不计入当前健康。',
|
|
636
|
+
identityShowHistorical: '显示历史项目 ID',
|
|
637
|
+
identityHideHistorical: '隐藏历史',
|
|
638
|
+
// H4/H5: 项目切换器分组
|
|
639
|
+
switcherGroupCurrent: '当前',
|
|
640
|
+
switcherGroupReal: '真实项目',
|
|
641
|
+
switcherGroupTemporary: '临时 / smoke / 演示',
|
|
642
|
+
switcherGroupPlaceholder: 'Placeholder',
|
|
643
|
+
switcherShowTemp: '显示临时项目',
|
|
644
|
+
switcherHideTemp: '隐藏临时项目',
|
|
645
|
+
switcherTempCount: '临时',
|
|
646
|
+
teamNoRole: '无角色',
|
|
647
|
+
teamAvailableToClaim: '可领取',
|
|
648
|
+
teamActiveCount: '活跃',
|
|
649
|
+
teamRoles: '角色',
|
|
650
|
+
teamDefined: '已定义',
|
|
651
|
+
teamHandoffs: '交接',
|
|
652
|
+
teamOpen: '待领取',
|
|
653
|
+
taskPending: '待处理',
|
|
654
|
+
taskInProgress: '进行中',
|
|
655
|
+
taskCompleted: '已完成',
|
|
656
|
+
taskFailed: '失败',
|
|
657
|
+
|
|
658
|
+
// Sessions (additional)
|
|
659
|
+
sessionsTimeline: '时间线',
|
|
660
|
+
sessionsTotal: '总计',
|
|
661
|
+
sessionsTotalLower: '总计',
|
|
662
|
+
|
|
663
|
+
// Graph (additional)
|
|
664
|
+
graphIsolatedEntities: '孤立实体',
|
|
665
|
+
graphIsolatedDesc: '无关系的实体 — 单独显示以提高可读性',
|
|
666
|
+
graphConnected: '已连接',
|
|
667
|
+
graphNeighborhood: '邻域',
|
|
668
|
+
graphFullGraph: '完整图谱',
|
|
669
|
+
graphTopology: '拓扑',
|
|
670
|
+
graphTable: '表格',
|
|
671
|
+
graphSearch: '搜索',
|
|
672
|
+
graphScope: '范围',
|
|
673
|
+
graphView: '视图',
|
|
674
|
+
graphLayout: '布局',
|
|
675
|
+
graphEntityType: '实体类型',
|
|
676
|
+
graphDepth: '深度',
|
|
677
|
+
graphConnections: '连接数',
|
|
678
|
+
graphEvidence: '证据',
|
|
679
|
+
graphObservations: '观察',
|
|
680
|
+
graphRelations: '关系',
|
|
681
|
+
graphSparseWarning: '稀疏图谱:%isolated% / %total% 个实体无关系。孤立实体显示在下方清单中。',
|
|
682
|
+
graphNoRelations: '无关系 — 仅有孤立实体',
|
|
683
|
+
graphNoRelationsDesc: '此项目有实体但无关系。所有实体显示在下方清单面板中。',
|
|
684
|
+
graphEmptyState: '暂无图谱数据',
|
|
685
|
+
graphEmptyStateDesc: '创建实体和关系来查看知识图谱',
|
|
686
|
+
graphSelectNode: '选择节点以查看详情',
|
|
687
|
+
graphFindEntity: '查找实体...',
|
|
688
|
+
graphLeftToRight: '从左到右',
|
|
689
|
+
graphTopToBottom: '从上到下',
|
|
690
|
+
graphMore: '更多',
|
|
691
|
+
|
|
692
|
+
// Identity (additional)
|
|
693
|
+
identityCurrentProject: '当前项目',
|
|
694
|
+
identityHistoricalProjects: '历史项目',
|
|
695
|
+
identityProjectBound: '项目已绑定且健康',
|
|
696
|
+
identityProjectUnbound: '项目未绑定 — 请选择项目',
|
|
697
|
+
identityDirtyCurrentWarning: '当前项目存在需要解决的脏 ID',
|
|
698
|
+
identityDirtyHistoricalNote: '来自其他项目的脏 ID(不影响当前项目)',
|
|
699
|
+
identityTotalCount: '总计',
|
|
700
|
+
|
|
701
|
+
// Config (additional)
|
|
702
|
+
configProjectUnavailable: '项目配置不可用',
|
|
703
|
+
configProjectUnavailableDesc: '项目根路径未知 — 无法解析项目级配置文件',
|
|
704
|
+
|
|
705
|
+
// Git Memory (additional)
|
|
706
|
+
gitMemoryTotal: '总计',
|
|
707
|
+
|
|
376
708
|
// Nav tooltips
|
|
377
709
|
navDashboard: '仪表盘',
|
|
378
710
|
navGitMemory: 'Git 记忆',
|
|
@@ -382,11 +714,51 @@ const i18n = {
|
|
|
382
714
|
navConfig: '配置溯源',
|
|
383
715
|
navIdentity: '身份健康',
|
|
384
716
|
navSessions: '会话',
|
|
385
|
-
navTeam: '
|
|
717
|
+
navTeam: '协作',
|
|
718
|
+
navLabelDashboard: '概览',
|
|
719
|
+
navLabelGitMemory: 'Git 记忆',
|
|
720
|
+
navLabelGraph: '图谱',
|
|
721
|
+
navLabelObservations: '观察',
|
|
722
|
+
navLabelRetention: '衰减',
|
|
723
|
+
navLabelConfig: '配置',
|
|
724
|
+
navLabelIdentity: '身份',
|
|
725
|
+
navLabelSessions: '会话',
|
|
726
|
+
navLabelTeam: '协作',
|
|
727
|
+
sectionCore: '核心',
|
|
728
|
+
sectionHealth: '健康',
|
|
729
|
+
sectionCollaboration: '协作',
|
|
730
|
+
themeDark: '深色',
|
|
731
|
+
themeLight: '浅色',
|
|
732
|
+
loading: '加载中...',
|
|
733
|
+
searchProjects: '搜索项目...',
|
|
734
|
+
|
|
735
|
+
// Mode banner
|
|
736
|
+
modeStandalone: '独立模式',
|
|
737
|
+
modeControlPlane: '控制平面',
|
|
738
|
+
modeStandaloneHint: '无实时 MCP — 团队功能不可用',
|
|
739
|
+
modeControlPlaneHint: '完整 MCP + 团队协作',
|
|
740
|
+
modeBannerProject: '项目',
|
|
741
|
+
modeBannerMcp: 'MCP',
|
|
742
|
+
|
|
743
|
+
// Time ago
|
|
744
|
+
timeAgoS: '秒前',
|
|
745
|
+
timeAgoM: '分钟前',
|
|
746
|
+
timeAgoH: '小时前',
|
|
747
|
+
timeAgoD: '天前',
|
|
748
|
+
timeExpired: '已过期',
|
|
749
|
+
timeLeft: '分钟剩余',
|
|
750
|
+
teamJoined: '加入',
|
|
751
|
+
teamSeen: '活跃',
|
|
752
|
+
teamLeft: '离开',
|
|
753
|
+
persistedAgentsAvailable: '个持久化代理可用,在',
|
|
754
|
+
sessionsCount: '个会话',
|
|
386
755
|
},
|
|
387
756
|
};
|
|
388
757
|
|
|
389
758
|
let currentLang = localStorage.getItem('memorix-lang') || 'en';
|
|
759
|
+
let dashboardMode = 'standalone'; // 'standalone' | 'control-plane'
|
|
760
|
+
let dashboardPort = 3210;
|
|
761
|
+
let mcpEndpoint = null;
|
|
390
762
|
|
|
391
763
|
function t(key) {
|
|
392
764
|
return (i18n[currentLang] && i18n[currentLang][key]) || i18n.en[key] || key;
|
|
@@ -395,18 +767,43 @@ function t(key) {
|
|
|
395
767
|
function setLang(lang) {
|
|
396
768
|
currentLang = lang;
|
|
397
769
|
localStorage.setItem('memorix-lang', lang);
|
|
770
|
+
document.documentElement.lang = lang;
|
|
398
771
|
|
|
399
|
-
// Update label
|
|
772
|
+
// Update lang toggle label
|
|
400
773
|
const label = document.getElementById('lang-label');
|
|
401
774
|
if (label) label.textContent = lang === 'en' ? '中文' : 'EN';
|
|
402
775
|
|
|
403
|
-
// Update nav tooltips
|
|
404
|
-
const tooltipMap = { dashboard: 'navDashboard', graph: 'navGraph', observations: 'navObservations', retention: 'navRetention', sessions: 'navSessions', team: 'navTeam' };
|
|
776
|
+
// Update nav tooltips + labels
|
|
777
|
+
const tooltipMap = { dashboard: 'navDashboard', 'git-memory': 'navGitMemory', graph: 'navGraph', observations: 'navObservations', retention: 'navRetention', config: 'navConfig', identity: 'navIdentity', sessions: 'navSessions', team: 'navTeam' };
|
|
778
|
+
const labelMap = { dashboard: 'navLabelDashboard', 'git-memory': 'navLabelGitMemory', graph: 'navLabelGraph', observations: 'navLabelObservations', retention: 'navLabelRetention', config: 'navLabelConfig', identity: 'navLabelIdentity', sessions: 'navLabelSessions', team: 'navLabelTeam' };
|
|
405
779
|
document.querySelectorAll('.nav-btn').forEach(b => {
|
|
406
780
|
const page = b.dataset.page;
|
|
407
781
|
if (page && tooltipMap[page]) b.title = t(tooltipMap[page]);
|
|
782
|
+
if (page && labelMap[page]) {
|
|
783
|
+
const span = b.querySelector('.nav-label');
|
|
784
|
+
if (span) span.textContent = t(labelMap[page]);
|
|
785
|
+
}
|
|
408
786
|
});
|
|
409
787
|
|
|
788
|
+
// Update section labels
|
|
789
|
+
const sectionMap = { core: 'sectionCore', health: 'sectionHealth', collaboration: 'sectionCollaboration' };
|
|
790
|
+
document.querySelectorAll('.sidebar-section-label').forEach(el => {
|
|
791
|
+
const key = sectionMap[el.dataset.section];
|
|
792
|
+
if (key) el.textContent = t(key);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Update theme label
|
|
796
|
+
const themeLabel = document.getElementById('theme-label');
|
|
797
|
+
if (themeLabel) themeLabel.textContent = currentTheme === 'dark' ? t('themeDark') : t('themeLight');
|
|
798
|
+
|
|
799
|
+
// Update project search placeholder
|
|
800
|
+
const projectSearch = document.getElementById('project-search');
|
|
801
|
+
if (projectSearch) projectSearch.placeholder = t('searchProjects');
|
|
802
|
+
|
|
803
|
+
// Update project name loading text
|
|
804
|
+
const projectName = document.getElementById('project-name');
|
|
805
|
+
if (projectName && projectName.textContent === 'Loading...') projectName.textContent = t('loading');
|
|
806
|
+
|
|
410
807
|
// Force reload all pages
|
|
411
808
|
Object.keys(loaded).forEach(k => delete loaded[k]);
|
|
412
809
|
loadPage(currentPage);
|
|
@@ -442,7 +839,7 @@ function applyTheme(theme) {
|
|
|
442
839
|
sunIcon.style.display = theme === 'dark' ? 'none' : 'block';
|
|
443
840
|
moonIcon.style.display = theme === 'dark' ? 'block' : 'none';
|
|
444
841
|
}
|
|
445
|
-
if (themeLabel) themeLabel.textContent = theme === 'dark' ? '
|
|
842
|
+
if (themeLabel) themeLabel.textContent = theme === 'dark' ? t('themeDark') : t('themeLight');
|
|
446
843
|
|
|
447
844
|
// Force reload current page so Canvas graph redraws with new colors
|
|
448
845
|
try {
|
|
@@ -501,6 +898,32 @@ document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
|
501
898
|
|
|
502
899
|
let selectedProject = ''; // empty = current project (default)
|
|
503
900
|
|
|
901
|
+
// Update sidebar mode badge — shows standalone/control-plane, port, MCP endpoint
|
|
902
|
+
function updateModeBadge() {
|
|
903
|
+
const badge = document.getElementById('mode-badge');
|
|
904
|
+
const label = document.getElementById('mode-badge-label');
|
|
905
|
+
const portEl = document.getElementById('mode-badge-port');
|
|
906
|
+
const mcpEl = document.getElementById('mode-badge-mcp');
|
|
907
|
+
if (!badge || !label || !portEl || !mcpEl) return;
|
|
908
|
+
|
|
909
|
+
const isCP = dashboardMode === 'control-plane';
|
|
910
|
+
label.textContent = isCP ? t('modeControlPlane') : t('modeStandalone');
|
|
911
|
+
label.className = 'mode-badge-label ' + (isCP ? 'control-plane' : 'standalone');
|
|
912
|
+
portEl.textContent = ':' + dashboardPort;
|
|
913
|
+
|
|
914
|
+
if (isCP && mcpEndpoint) {
|
|
915
|
+
mcpEl.style.display = '';
|
|
916
|
+
mcpEl.textContent = t('modeBannerMcp') + ' ' + mcpEndpoint.replace('http://127.0.0.1', '');
|
|
917
|
+
mcpEl.title = mcpEndpoint;
|
|
918
|
+
mcpEl.onclick = () => { navigator.clipboard.writeText(mcpEndpoint).catch(() => {}); };
|
|
919
|
+
} else {
|
|
920
|
+
mcpEl.style.display = 'none';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
badge.style.display = 'flex';
|
|
924
|
+
badge.title = isCP ? t('modeControlPlaneHint') : t('modeStandaloneHint');
|
|
925
|
+
}
|
|
926
|
+
|
|
504
927
|
async function api(endpoint) {
|
|
505
928
|
try {
|
|
506
929
|
const sep = endpoint.includes('?') ? '&' : '?';
|
|
@@ -521,6 +944,7 @@ async function api(endpoint) {
|
|
|
521
944
|
// ============================================================
|
|
522
945
|
|
|
523
946
|
let allProjects = [];
|
|
947
|
+
let projectShowTemp = false; // H4: collapse temp/placeholder by default
|
|
524
948
|
|
|
525
949
|
async function initProjectSwitcher() {
|
|
526
950
|
const switcher = document.getElementById('project-switcher');
|
|
@@ -541,7 +965,7 @@ async function initProjectSwitcher() {
|
|
|
541
965
|
const res = await fetch('/api/projects');
|
|
542
966
|
allProjects = await res.json();
|
|
543
967
|
if (!Array.isArray(allProjects) || allProjects.length === 0) {
|
|
544
|
-
nameEl.textContent = '
|
|
968
|
+
nameEl.textContent = t('noProjects');
|
|
545
969
|
return;
|
|
546
970
|
}
|
|
547
971
|
|
|
@@ -581,7 +1005,7 @@ async function initProjectSwitcher() {
|
|
|
581
1005
|
updateTrigger(active);
|
|
582
1006
|
renderProjectList(allProjects, active);
|
|
583
1007
|
} catch {
|
|
584
|
-
nameEl.textContent = '
|
|
1008
|
+
nameEl.textContent = t('error');
|
|
585
1009
|
}
|
|
586
1010
|
|
|
587
1011
|
// Toggle dropdown
|
|
@@ -617,25 +1041,87 @@ async function initProjectSwitcher() {
|
|
|
617
1041
|
});
|
|
618
1042
|
|
|
619
1043
|
function updateTrigger(project) {
|
|
1044
|
+
const isUnresolved = project.id === '__unresolved__';
|
|
620
1045
|
nameEl.textContent = project.name;
|
|
621
1046
|
nameEl.title = project.id;
|
|
622
1047
|
countEl.textContent = project.count || '';
|
|
1048
|
+
// Visual indicator for unresolved project
|
|
1049
|
+
const existingBadge = trigger.querySelector('.project-resolved-badge');
|
|
1050
|
+
if (existingBadge) existingBadge.remove();
|
|
1051
|
+
if (isUnresolved) {
|
|
1052
|
+
const badge = document.createElement('span');
|
|
1053
|
+
badge.className = 'project-resolved-badge';
|
|
1054
|
+
badge.style.cssText = 'font-size:9px;padding:1px 5px;border-radius:4px;background:rgba(245,158,11,0.12);color:var(--accent-amber);margin-left:6px;';
|
|
1055
|
+
badge.textContent = t('projectUnresolved');
|
|
1056
|
+
trigger.appendChild(badge);
|
|
1057
|
+
}
|
|
623
1058
|
}
|
|
624
1059
|
|
|
625
1060
|
function renderProjectList(projects, activeOverride) {
|
|
626
1061
|
const activeId = activeOverride ? activeOverride.id : (selectedProject || allProjects.find(p => p.isCurrent)?.id || '');
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1062
|
+
|
|
1063
|
+
// H4: Group by kind — current / real / temporary / placeholder
|
|
1064
|
+
const current = projects.find(p => p.id === activeId || (p.isCurrent && !activeId));
|
|
1065
|
+
const real = projects.filter(p => p.kind === 'real' && p !== current);
|
|
1066
|
+
const temporary = projects.filter(p => p.kind === 'temporary');
|
|
1067
|
+
const placeholder = projects.filter(p => p.kind === 'placeholder' && p.id !== '__unresolved__');
|
|
1068
|
+
// Legacy: items without a kind field (pre-1.0.7 API) → treat as real
|
|
1069
|
+
const untagged = projects.filter(p => !p.kind && p !== current);
|
|
1070
|
+
const realCombined = [...real, ...untagged];
|
|
1071
|
+
|
|
1072
|
+
const itemHtml = (p) => {
|
|
1073
|
+
const isUnresolved = p.id === '__unresolved__';
|
|
1074
|
+
const isDirty = p.dirty;
|
|
1075
|
+
const isTemp = p.kind === 'temporary';
|
|
1076
|
+
const isPlaceholder = p.kind === 'placeholder';
|
|
1077
|
+
let badge = '';
|
|
1078
|
+
if (isUnresolved) badge = `<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:rgba(245,158,11,0.12);color:var(--accent-amber);margin-left:4px;">${t('projectUnresolved')}</span>`;
|
|
1079
|
+
else if (isPlaceholder) badge = `<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:rgba(239,68,68,0.12);color:var(--accent-red);margin-left:4px;">${t('switcherGroupPlaceholder')}</span>`;
|
|
1080
|
+
else if (isTemp) badge = `<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:rgba(148,163,184,0.15);color:var(--text-muted);margin-left:4px;">${t('switcherTempCount')}</span>`;
|
|
1081
|
+
else if (isDirty) badge = `<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:rgba(239,68,68,0.12);color:var(--accent-red);margin-left:4px;">${t('tagDirty')}</span>`;
|
|
1082
|
+
const opacity = (isTemp || isPlaceholder) ? 'opacity:0.72;' : '';
|
|
1083
|
+
return `<button class="project-item${p.id === activeId || (p.isCurrent && !activeId) ? ' active' : ''}${isUnresolved ? ' unresolved' : ''}"
|
|
1084
|
+
data-id="${escapeHtml(p.id)}" title="${escapeHtml(p.id)}" style="${opacity}">
|
|
1085
|
+
<span class="project-item-dot"${isUnresolved ? ' style="background:var(--accent-amber);"' : ''}></span>
|
|
1086
|
+
<span class="project-item-name">${escapeHtml(p.name)}${badge}</span>
|
|
632
1087
|
<span class="project-item-count">${p.count}</span>
|
|
633
|
-
</button
|
|
634
|
-
|
|
1088
|
+
</button>`;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
const groupHeader = (label, count) =>
|
|
1092
|
+
`<div style="font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;padding:10px 12px 4px;">${label} <span style="color:var(--text-faint);">(${count})</span></div>`;
|
|
1093
|
+
|
|
1094
|
+
const hideCount = temporary.length + placeholder.length;
|
|
1095
|
+
const tempSectionHtml = (temporary.length + placeholder.length) > 0
|
|
1096
|
+
? `<div style="border-top:1px dashed var(--border);margin-top:6px;padding-top:4px;">
|
|
1097
|
+
<button class="project-item" style="width:100%;border:none;background:transparent;padding:8px 12px;cursor:pointer;color:var(--text-muted);font-size:11px;text-align:left;"
|
|
1098
|
+
data-toggle-temp="1">
|
|
1099
|
+
<span class="iconify" data-icon="${projectShowTemp ? 'lucide:chevron-down' : 'lucide:chevron-right'}" style="font-size:12px;vertical-align:middle;margin-right:4px;"></span>
|
|
1100
|
+
${projectShowTemp ? t('switcherHideTemp') : t('switcherShowTemp')} <span style="color:var(--text-faint);">(${hideCount})</span>
|
|
1101
|
+
</button>
|
|
1102
|
+
${projectShowTemp ? `
|
|
1103
|
+
${temporary.length > 0 ? groupHeader(t('switcherGroupTemporary'), temporary.length) + temporary.map(itemHtml).join('') : ''}
|
|
1104
|
+
${placeholder.length > 0 ? groupHeader(t('switcherGroupPlaceholder'), placeholder.length) + placeholder.map(itemHtml).join('') : ''}
|
|
1105
|
+
` : ''}
|
|
1106
|
+
</div>`
|
|
1107
|
+
: '';
|
|
1108
|
+
|
|
1109
|
+
listEl.innerHTML =
|
|
1110
|
+
(current ? groupHeader(t('switcherGroupCurrent'), 1) + itemHtml(current) : '') +
|
|
1111
|
+
(realCombined.length > 0 ? groupHeader(t('switcherGroupReal'), realCombined.length) + realCombined.map(itemHtml).join('') : '') +
|
|
1112
|
+
tempSectionHtml;
|
|
635
1113
|
|
|
636
1114
|
// Click handlers
|
|
637
|
-
listEl.
|
|
638
|
-
|
|
1115
|
+
const tempToggle = listEl.querySelector('[data-toggle-temp]');
|
|
1116
|
+
if (tempToggle) {
|
|
1117
|
+
tempToggle.addEventListener('click', (ev) => {
|
|
1118
|
+
ev.stopPropagation();
|
|
1119
|
+
projectShowTemp = !projectShowTemp;
|
|
1120
|
+
renderProjectList(projects, activeOverride);
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
listEl.querySelectorAll('.project-item[data-id]').forEach(item => {
|
|
1124
|
+
item.addEventListener('click', async () => {
|
|
639
1125
|
const id = item.dataset.id;
|
|
640
1126
|
const project = allProjects.find(p => p.id === id);
|
|
641
1127
|
if (!project) return;
|
|
@@ -648,6 +1134,15 @@ async function initProjectSwitcher() {
|
|
|
648
1134
|
listEl.querySelectorAll('.project-item').forEach(el => el.classList.remove('active'));
|
|
649
1135
|
item.classList.add('active');
|
|
650
1136
|
|
|
1137
|
+
// Sync to backend so all API endpoints use the new project
|
|
1138
|
+
try {
|
|
1139
|
+
await fetch('/api/set-current-project', {
|
|
1140
|
+
method: 'POST',
|
|
1141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1142
|
+
body: JSON.stringify({ projectId: project.id, projectName: project.name }),
|
|
1143
|
+
});
|
|
1144
|
+
} catch { /* best effort */ }
|
|
1145
|
+
|
|
651
1146
|
// Reload pages
|
|
652
1147
|
Object.keys(loaded).forEach(k => delete loaded[k]);
|
|
653
1148
|
loadPage(currentPage);
|
|
@@ -693,10 +1188,20 @@ async function loadDashboard() {
|
|
|
693
1188
|
|
|
694
1189
|
const [stats, project] = await Promise.all([api('stats'), api('project')]);
|
|
695
1190
|
if (!stats) {
|
|
696
|
-
container.innerHTML = emptyState('
|
|
1191
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:bar-chart-3" style="font-size:36px;"></span>', t('noData'), t('noDataDesc'));
|
|
697
1192
|
return;
|
|
698
1193
|
}
|
|
699
1194
|
|
|
1195
|
+
// Capture mode info from project API
|
|
1196
|
+
if (project) {
|
|
1197
|
+
dashboardMode = project.mode || 'standalone';
|
|
1198
|
+
dashboardPort = project.port || 3210;
|
|
1199
|
+
mcpEndpoint = project.mcpEndpoint || null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Update sidebar mode badge (4.2: mode/port/MCP must be visible)
|
|
1203
|
+
updateModeBadge();
|
|
1204
|
+
|
|
700
1205
|
const projectLabel = project ? project.name : '';
|
|
701
1206
|
const sc = stats.sourceCounts || { git: 0, agent: 0, manual: 0 };
|
|
702
1207
|
const totalObs = stats.observations || 0;
|
|
@@ -704,9 +1209,9 @@ async function loadDashboard() {
|
|
|
704
1209
|
const rs = stats.retentionSummary || { active: 0, stale: 0, archive: 0, immune: 0 };
|
|
705
1210
|
|
|
706
1211
|
const typeIcons = {
|
|
707
|
-
'session-request': '
|
|
708
|
-
'how-it-works': '
|
|
709
|
-
'why-it-exists': '
|
|
1212
|
+
'session-request': '<span class="iconify" data-icon="lucide:target" style="color:#f87171;"></span>', gotcha: '<span class="iconify" data-icon="lucide:alert-octagon" style="color:#ef4444;"></span>', 'problem-solution': '<span class="iconify" data-icon="lucide:lightbulb" style="color:#fbbf24;"></span>',
|
|
1213
|
+
'how-it-works': '<span class="iconify" data-icon="lucide:info" style="color:#38bdf8;"></span>', 'what-changed': '<span class="iconify" data-icon="lucide:git-branch" style="color:#4ade80;"></span>', discovery: '<span class="iconify" data-icon="lucide:sparkles" style="color:#a78bfa;"></span>',
|
|
1214
|
+
'why-it-exists': '<span class="iconify" data-icon="lucide:help-circle" style="color:#fb923c;"></span>', decision: '<span class="iconify" data-icon="lucide:scale" style="color:#a1887f;"></span>', 'trade-off': '<span class="iconify" data-icon="lucide:scale" style="color:#94a3b8;"></span>',
|
|
710
1215
|
};
|
|
711
1216
|
|
|
712
1217
|
const typeEntries = Object.entries(stats.typeCounts || {}).sort((a, b) => b[1] - a[1]);
|
|
@@ -718,10 +1223,11 @@ async function loadDashboard() {
|
|
|
718
1223
|
const agentPct = Math.round(sc.agent / srcTotal * 100);
|
|
719
1224
|
const manualPct = 100 - gitPct - agentPct;
|
|
720
1225
|
|
|
1226
|
+
const modeTitle = dashboardMode === 'control-plane' ? t('memoryControlPlane') : t('modeStandalone');
|
|
721
1227
|
container.innerHTML = `
|
|
722
1228
|
<div class="page-header">
|
|
723
|
-
<h1 class="page-title">${
|
|
724
|
-
<p class="page-subtitle">${totalObs} ${t('memoriesAcross')} ${stats.entities} ${t('entitiesUnit')}</p>
|
|
1229
|
+
<h1 class="page-title">${modeTitle} ${projectLabel ? `<span class="overview-project-badge">${escapeHtml(projectLabel)}</span>` : ''}</h1>
|
|
1230
|
+
<p class="page-subtitle">${totalObs} ${t('memoriesAcross')} ${stats.entities} ${t('entitiesUnit')}${mcpEndpoint ? ` · ${t('modeBannerMcp')}: <span style="color:var(--accent-blue);cursor:pointer;font-family:'JetBrains Mono',monospace;font-size:12px;" onclick="navigator.clipboard.writeText('${mcpEndpoint}').catch(()=>{})" title="${mcpEndpoint}">${mcpEndpoint.replace('http://127.0.0.1','')}</span>` : ''}</p>
|
|
725
1231
|
</div>
|
|
726
1232
|
|
|
727
1233
|
<div class="stats-grid">
|
|
@@ -742,15 +1248,15 @@ async function loadDashboard() {
|
|
|
742
1248
|
</div>
|
|
743
1249
|
<div class="stat-card" data-accent="${stats.embedding?.enabled ? 'blue' : 'amber'}">
|
|
744
1250
|
<div class="stat-label">${t('vectorSearch')}</div>
|
|
745
|
-
<div class="stat-value" style="font-size: 18px;">${stats.embedding?.enabled ? '
|
|
1251
|
+
<div class="stat-value" style="font-size: 18px;">${stats.embedding?.enabled ? '<span class="iconify" data-icon="lucide:circle-check" style="font-size:16px;vertical-align:middle;margin-right:3px;color:var(--accent-green);"></span> ' + t('enabled') : t('fulltextOnly')}</div>
|
|
746
1252
|
${stats.embedding?.provider ? `<div class="stat-sub">${stats.embedding.provider} (${stats.embedding.dimensions}d)</div>` : ''}
|
|
747
1253
|
</div>
|
|
748
1254
|
</div>
|
|
749
1255
|
|
|
750
|
-
<!-- System
|
|
1256
|
+
<!-- System Status -->
|
|
751
1257
|
<div class="overview-row">
|
|
752
1258
|
<div class="panel" style="flex:1;">
|
|
753
|
-
<div class="panel-header"><span class="panel-title">${t('
|
|
1259
|
+
<div class="panel-header"><span class="panel-title">${t('systemStatus')}</span></div>
|
|
754
1260
|
<div class="panel-body">
|
|
755
1261
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
|
|
756
1262
|
<div>
|
|
@@ -789,9 +1295,9 @@ async function loadDashboard() {
|
|
|
789
1295
|
<div class="panel-body">
|
|
790
1296
|
<div class="source-bar-container">
|
|
791
1297
|
<div class="source-bar">
|
|
792
|
-
${gitPct > 0 ? `<div class="source-bar-seg" style="width:${gitPct}%;background:var(--accent-green);" title="
|
|
793
|
-
${agentPct > 0 ? `<div class="source-bar-seg" style="width:${agentPct}%;background:var(--accent-purple);" title="
|
|
794
|
-
${manualPct > 0 ? `<div class="source-bar-seg" style="width:${manualPct}%;background:var(--accent-amber);" title="
|
|
1298
|
+
${gitPct > 0 ? `<div class="source-bar-seg" style="width:${gitPct}%;background:var(--accent-green);" title="${t('sourceGit')} ${gitPct}%"></div>` : ''}
|
|
1299
|
+
${agentPct > 0 ? `<div class="source-bar-seg" style="width:${agentPct}%;background:var(--accent-purple);" title="${t('sourceAgent')} ${agentPct}%"></div>` : ''}
|
|
1300
|
+
${manualPct > 0 ? `<div class="source-bar-seg" style="width:${manualPct}%;background:var(--accent-amber);" title="${t('sourceManual')} ${manualPct}%"></div>` : ''}
|
|
795
1301
|
</div>
|
|
796
1302
|
<div class="source-legend">
|
|
797
1303
|
<span class="source-legend-item"><span class="source-dot" style="background:var(--accent-green)"></span> ${t('sourceGit')} <strong>${sc.git}</strong></span>
|
|
@@ -924,15 +1430,30 @@ async function loadGraph() {
|
|
|
924
1430
|
|
|
925
1431
|
const graph = await api('graph');
|
|
926
1432
|
if (!graph || (graph.entities.length === 0 && graph.relations.length === 0)) {
|
|
927
|
-
container.innerHTML = emptyState('
|
|
1433
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:network" style="font-size:36px;"></span>', t('noGraphData'), t('noGraphDataDesc'));
|
|
928
1434
|
return;
|
|
929
1435
|
}
|
|
930
1436
|
|
|
1437
|
+
// Check for no-relations state (only isolated entities)
|
|
1438
|
+
const hasRelations = graph.relations.length > 0;
|
|
1439
|
+
|
|
931
1440
|
container.innerHTML = `
|
|
932
1441
|
<div class="page-header">
|
|
933
|
-
<h1 class="page-title"
|
|
934
|
-
<p class="page-subtitle">${graph.entities.length} entities · ${graph.relations.length} relations</p>
|
|
1442
|
+
<h1 class="page-title">${t('knowledgeGraph')}</h1>
|
|
1443
|
+
<p class="page-subtitle">${graph.entities.length} ${t('entities').toLowerCase()} · ${graph.relations.length} ${t('relations').toLowerCase()}</p>
|
|
935
1444
|
</div>
|
|
1445
|
+
${!hasRelations && graph.entities.length > 0 ? `
|
|
1446
|
+
<div class="panel" style="margin-bottom:16px;border-color:var(--accent-amber);">
|
|
1447
|
+
<div class="panel-body" style="display:flex;align-items:center;gap:12px;padding:12px 16px;">
|
|
1448
|
+
<span class="iconify" data-icon="lucide:network" style="font-size:20px;color:var(--accent-amber);"></span>
|
|
1449
|
+
<div>
|
|
1450
|
+
<div style="font-weight:600;color:var(--accent-amber);">${t('graphNoRelations')}</div>
|
|
1451
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:2px;">${t('graphNoRelationsDesc')}</div>
|
|
1452
|
+
</div>
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
` : ''}
|
|
1456
|
+
${hasRelations ? `
|
|
936
1457
|
<div class="graph-layout">
|
|
937
1458
|
<div class="graph-filter-panel" id="graph-filter-panel"></div>
|
|
938
1459
|
<div id="graph-container">
|
|
@@ -951,9 +1472,10 @@ async function loadGraph() {
|
|
|
951
1472
|
</div>
|
|
952
1473
|
<div class="graph-table-container" id="graph-table-container" style="display:none;"></div>
|
|
953
1474
|
<div class="graph-inspector" id="graph-inspector">
|
|
954
|
-
<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div
|
|
1475
|
+
<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>${t('graphSelectNode')}</div>
|
|
955
1476
|
</div>
|
|
956
1477
|
</div>
|
|
1478
|
+
` : ''}
|
|
957
1479
|
<div id="graph-isolated-panel" style="display:none;"></div>
|
|
958
1480
|
`;
|
|
959
1481
|
|
|
@@ -1333,8 +1855,8 @@ function renderGraph(graph) {
|
|
|
1333
1855
|
panel.innerHTML = `
|
|
1334
1856
|
<div class="panel" style="margin-top:16px;">
|
|
1335
1857
|
<div class="panel-header">
|
|
1336
|
-
<span class="panel-title"
|
|
1337
|
-
<span style="font-size:11px;color:var(--text-muted);">${isolated.length}
|
|
1858
|
+
<span class="panel-title">${t('graphIsolatedEntities')}</span>
|
|
1859
|
+
<span style="font-size:11px;color:var(--text-muted);">${isolated.length} ${t('graphIsolatedDesc')}</span>
|
|
1338
1860
|
</div>
|
|
1339
1861
|
<div class="panel-body" style="padding:12px 16px;">
|
|
1340
1862
|
${groupEntries.map(([type, entities]) => {
|
|
@@ -1355,7 +1877,7 @@ function renderGraph(graph) {
|
|
|
1355
1877
|
${e.observations.length > 0 ? '<span style="font-size:9px;color:var(--text-muted);">' + e.observations.length + '</span>' : ''}
|
|
1356
1878
|
</span>
|
|
1357
1879
|
`).join('')}
|
|
1358
|
-
${collapsed ? '<span style="font-size:11px;color:var(--text-muted);padding:3px 8px;">+' + (entities.length - 8) + '
|
|
1880
|
+
${collapsed ? '<span style="font-size:11px;color:var(--text-muted);padding:3px 8px;">+' + (entities.length - 8) + ' ' + t('graphMore') + '</span>' : ''}
|
|
1359
1881
|
</div>
|
|
1360
1882
|
</div>
|
|
1361
1883
|
`;
|
|
@@ -1381,7 +1903,7 @@ function renderGraph(graph) {
|
|
|
1381
1903
|
const inspector = document.getElementById('graph-inspector');
|
|
1382
1904
|
if (!inspector) return;
|
|
1383
1905
|
if (!nodeId || !entityMap[nodeId]) {
|
|
1384
|
-
inspector.innerHTML = '<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>
|
|
1906
|
+
inspector.innerHTML = '<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>' + t('graphSelectNode') + '</div>';
|
|
1385
1907
|
return;
|
|
1386
1908
|
}
|
|
1387
1909
|
const entity = entityMap[nodeId];
|
|
@@ -1391,7 +1913,7 @@ function renderGraph(graph) {
|
|
|
1391
1913
|
|
|
1392
1914
|
const obsHtml = entity.observations.length > 0
|
|
1393
1915
|
? entity.observations.map(o => `<div class="gi-obs-item">${escapeHtml(o)}</div>`).join('')
|
|
1394
|
-
: '<div style="font-size:12px;color:var(--text-muted);font-style:italic;">
|
|
1916
|
+
: '<div style="font-size:12px;color:var(--text-muted);font-style:italic;">' + t('noObservations') + '</div>';
|
|
1395
1917
|
const relHtml = related.length > 0
|
|
1396
1918
|
? related.map(r => {
|
|
1397
1919
|
const dir = r.from === nodeId;
|
|
@@ -1402,7 +1924,7 @@ function renderGraph(graph) {
|
|
|
1402
1924
|
<span class="gi-rel-target" data-inspector-nav="${escapeHtml(other)}">${escapeHtml(other)}</span>
|
|
1403
1925
|
</div>`;
|
|
1404
1926
|
}).join('')
|
|
1405
|
-
: '<div style="font-size:12px;color:var(--text-muted);font-style:italic;">
|
|
1927
|
+
: '<div style="font-size:12px;color:var(--text-muted);font-style:italic;">' + t('noRelations') + '</div>';
|
|
1406
1928
|
|
|
1407
1929
|
inspector.innerHTML = `
|
|
1408
1930
|
<div class="gi-header">
|
|
@@ -1413,15 +1935,15 @@ function renderGraph(graph) {
|
|
|
1413
1935
|
<div class="gi-type">${escapeHtml(entity.entityType)}</div>
|
|
1414
1936
|
</div>
|
|
1415
1937
|
<div class="gi-stats">
|
|
1416
|
-
<div class="gi-stat"><div class="gi-stat-value">${deg}</div><div class="gi-stat-label"
|
|
1417
|
-
<div class="gi-stat"><div class="gi-stat-value">${entity.observations.length}</div><div class="gi-stat-label"
|
|
1938
|
+
<div class="gi-stat"><div class="gi-stat-value">${deg}</div><div class="gi-stat-label">${t('graphConnections')}</div></div>
|
|
1939
|
+
<div class="gi-stat"><div class="gi-stat-value">${entity.observations.length}</div><div class="gi-stat-label">${t('graphEvidence')}</div></div>
|
|
1418
1940
|
</div>
|
|
1419
1941
|
<div class="gi-section">
|
|
1420
|
-
<div class="gi-section-title"
|
|
1942
|
+
<div class="gi-section-title">${t('graphObservations')} <span class="gi-section-count">${entity.observations.length}</span></div>
|
|
1421
1943
|
${obsHtml}
|
|
1422
1944
|
</div>
|
|
1423
1945
|
<div class="gi-section">
|
|
1424
|
-
<div class="gi-section-title"
|
|
1946
|
+
<div class="gi-section-title">${t('graphRelations')} <span class="gi-section-count">${related.length}</span></div>
|
|
1425
1947
|
${relHtml}
|
|
1426
1948
|
</div>
|
|
1427
1949
|
`;
|
|
@@ -1456,32 +1978,32 @@ function renderGraph(graph) {
|
|
|
1456
1978
|
|
|
1457
1979
|
const searchHtml = `
|
|
1458
1980
|
<div class="gfp-section">
|
|
1459
|
-
<div class="gfp-label"
|
|
1460
|
-
<input type="text" class="gfp-search" id="gfp-search" placeholder="
|
|
1981
|
+
<div class="gfp-label">${t('graphSearch')}</div>
|
|
1982
|
+
<input type="text" class="gfp-search" id="gfp-search" placeholder="${t('graphFindEntity')}" autocomplete="off" />
|
|
1461
1983
|
</div>
|
|
1462
1984
|
`;
|
|
1463
1985
|
|
|
1464
1986
|
const scopeHtml = `
|
|
1465
1987
|
<div class="gfp-section">
|
|
1466
|
-
<div class="gfp-label"
|
|
1988
|
+
<div class="gfp-label">${t('graphScope')}</div>
|
|
1467
1989
|
<div class="gfp-radio-group">
|
|
1468
1990
|
<button class="gfp-radio${scope === 'connected' ? ' active' : ''}" data-scope="connected">
|
|
1469
|
-
<span class="gfp-radio-dot"></span>
|
|
1991
|
+
<span class="gfp-radio-dot"></span> ${t('graphConnected')}
|
|
1470
1992
|
</button>
|
|
1471
1993
|
<button class="gfp-radio${scope === 'neighborhood' ? ' active' : ''}" data-scope="neighborhood">
|
|
1472
|
-
<span class="gfp-radio-dot"></span>
|
|
1994
|
+
<span class="gfp-radio-dot"></span> ${t('graphNeighborhood')}
|
|
1473
1995
|
</button>
|
|
1474
1996
|
<button class="gfp-radio${scope === 'full' ? ' active' : ''}" data-scope="full">
|
|
1475
|
-
<span class="gfp-radio-dot"></span>
|
|
1997
|
+
<span class="gfp-radio-dot"></span> ${t('graphFullGraph')}
|
|
1476
1998
|
</button>
|
|
1477
1999
|
</div>
|
|
1478
|
-
${isSparse ? `<div style="font-size:10px;color:var(--accent-amber);margin-top:6px;line-height:1.4;">\u26A0
|
|
2000
|
+
${isSparse ? `<div style="font-size:10px;color:var(--accent-amber);margin-top:6px;line-height:1.4;">\u26A0 ${t('graphSparseWarning').replace('%isolated%', isolatedCount).replace('%total%', graph.entities.length)}</div>` : ''}
|
|
1479
2001
|
</div>
|
|
1480
2002
|
`;
|
|
1481
2003
|
|
|
1482
2004
|
const depthHtml = `
|
|
1483
2005
|
<div class="gfp-section" id="gfp-depth-section"${scope !== 'neighborhood' ? ' style="display:none"' : ''}>
|
|
1484
|
-
<div class="gfp-label"
|
|
2006
|
+
<div class="gfp-label">${t('graphDepth')}</div>
|
|
1485
2007
|
<div class="gfp-depth-row">
|
|
1486
2008
|
<button class="gfp-depth-btn${depth === 1 ? ' active' : ''}" data-depth="1">1</button>
|
|
1487
2009
|
<button class="gfp-depth-btn${depth === 2 ? ' active' : ''}" data-depth="2">2</button>
|
|
@@ -1492,13 +2014,13 @@ function renderGraph(graph) {
|
|
|
1492
2014
|
|
|
1493
2015
|
const viewHtml = `
|
|
1494
2016
|
<div class="gfp-section">
|
|
1495
|
-
<div class="gfp-label"
|
|
2017
|
+
<div class="gfp-label">${t('graphView')}</div>
|
|
1496
2018
|
<div class="gfp-radio-group">
|
|
1497
2019
|
<button class="gfp-radio${currentView === 'topology' ? ' active' : ''}" data-view="topology">
|
|
1498
|
-
<span class="gfp-radio-dot"></span>
|
|
2020
|
+
<span class="gfp-radio-dot"></span> ${t('graphTopology')}
|
|
1499
2021
|
</button>
|
|
1500
2022
|
<button class="gfp-radio${currentView === 'table' ? ' active' : ''}" data-view="table">
|
|
1501
|
-
<span class="gfp-radio-dot"></span>
|
|
2023
|
+
<span class="gfp-radio-dot"></span> ${t('graphTable')}
|
|
1502
2024
|
</button>
|
|
1503
2025
|
</div>
|
|
1504
2026
|
</div>
|
|
@@ -1506,13 +2028,13 @@ function renderGraph(graph) {
|
|
|
1506
2028
|
|
|
1507
2029
|
const layoutHtml = `
|
|
1508
2030
|
<div class="gfp-section" id="gfp-layout-section"${currentView === 'table' ? ' style="display:none"' : ''}>
|
|
1509
|
-
<div class="gfp-label"
|
|
2031
|
+
<div class="gfp-label">${t('graphLayout')}</div>
|
|
1510
2032
|
<div class="gfp-radio-group">
|
|
1511
2033
|
<button class="gfp-radio${currentLayout === 'dagre-lr' ? ' active' : ''}" data-layout="dagre-lr">
|
|
1512
|
-
<span class="gfp-radio-dot"></span>
|
|
2034
|
+
<span class="gfp-radio-dot"></span> ${t('graphLeftToRight')}
|
|
1513
2035
|
</button>
|
|
1514
2036
|
<button class="gfp-radio${currentLayout === 'dagre-tb' ? ' active' : ''}" data-layout="dagre-tb">
|
|
1515
|
-
<span class="gfp-radio-dot"></span>
|
|
2037
|
+
<span class="gfp-radio-dot"></span> ${t('graphTopToBottom')}
|
|
1516
2038
|
</button>
|
|
1517
2039
|
</div>
|
|
1518
2040
|
</div>
|
|
@@ -1521,7 +2043,7 @@ function renderGraph(graph) {
|
|
|
1521
2043
|
const typeEntries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
|
|
1522
2044
|
const filterHtml = `
|
|
1523
2045
|
<div class="gfp-section">
|
|
1524
|
-
<div class="gfp-label"
|
|
2046
|
+
<div class="gfp-label">${t('graphEntityType')}</div>
|
|
1525
2047
|
<div class="gfp-radio-group">
|
|
1526
2048
|
${typeEntries.map(([type, count]) => `
|
|
1527
2049
|
<button class="gfp-check${activeTypes.has(type) ? ' active' : ''}" data-type-filter="${escapeHtml(type)}">
|
|
@@ -1680,12 +2202,12 @@ function renderGraph(graph) {
|
|
|
1680
2202
|
const gsEdges = document.getElementById('gs-edges');
|
|
1681
2203
|
const gsLayout = document.getElementById('gs-layout');
|
|
1682
2204
|
const gsScope = document.getElementById('gs-scope');
|
|
1683
|
-
if (gsNodes) gsNodes.textContent = `${nodeCount || 0} nodes`;
|
|
1684
|
-
if (gsEdges) gsEdges.textContent = `${edgeCount || 0} edges`;
|
|
2205
|
+
if (gsNodes) gsNodes.textContent = `${nodeCount || 0} ${t('nodes')}`;
|
|
2206
|
+
if (gsEdges) gsEdges.textContent = `${edgeCount || 0} ${t('edges')}`;
|
|
1685
2207
|
if (gsLayout) gsLayout.textContent = currentLayout === 'dagre-tb' ? 'TB' : 'LR';
|
|
1686
|
-
if (gsScope) gsScope.textContent = scope === 'full' ? '
|
|
2208
|
+
if (gsScope) gsScope.textContent = scope === 'full' ? t('graphFullGraph').toLowerCase() : scope === 'neighborhood' ? `${depth}-${t('graphDepth').toLowerCase().slice(0, 3)}` : t('graphConnected').toLowerCase();
|
|
1687
2209
|
if (isolatedCount > 0 && scope !== 'neighborhood') {
|
|
1688
|
-
if (gsScope) gsScope.textContent += ` · ${isolatedCount}
|
|
2210
|
+
if (gsScope) gsScope.textContent += ` · ${isolatedCount} ${t('graphIsolatedEntities').toLowerCase()}`;
|
|
1689
2211
|
}
|
|
1690
2212
|
}
|
|
1691
2213
|
|
|
@@ -1699,8 +2221,11 @@ function renderGraph(graph) {
|
|
|
1699
2221
|
|
|
1700
2222
|
// --- Initialize ---
|
|
1701
2223
|
_graphState = { graph, entityMap, degreeMap, typeColors, showInspector };
|
|
1702
|
-
|
|
1703
|
-
|
|
2224
|
+
const hasGraphCanvas = !!document.getElementById('cytoscape-mount');
|
|
2225
|
+
if (hasGraphCanvas) {
|
|
2226
|
+
initCytoscape();
|
|
2227
|
+
renderFilterPanel();
|
|
2228
|
+
}
|
|
1704
2229
|
renderIsolatedPanel();
|
|
1705
2230
|
}
|
|
1706
2231
|
|
|
@@ -1737,16 +2262,16 @@ function renderBatchToolbar() {
|
|
|
1737
2262
|
}
|
|
1738
2263
|
slot.innerHTML = `
|
|
1739
2264
|
<div class="batch-toolbar">
|
|
1740
|
-
<span class="batch-count">${selectedIds.size} ${t('selected')
|
|
1741
|
-
<button class="batch-cancel-btn" onclick="exitBatchMode()">${t('cancel')
|
|
1742
|
-
<button class="batch-delete-btn" onclick="batchDeleteSelected()"
|
|
2265
|
+
<span class="batch-count">${selectedIds.size} ${t('selected')}</span>
|
|
2266
|
+
<button class="batch-cancel-btn" onclick="exitBatchMode()">${t('cancel')}</button>
|
|
2267
|
+
<button class="batch-delete-btn" onclick="batchDeleteSelected()"><span class="iconify" data-icon="lucide:trash-2" style="font-size:13px;vertical-align:middle;margin-right:3px;"></span> ${t('deleteSelected')}</button>
|
|
1743
2268
|
</div>
|
|
1744
2269
|
`;
|
|
1745
2270
|
}
|
|
1746
2271
|
|
|
1747
2272
|
async function batchDeleteSelected() {
|
|
1748
2273
|
if (selectedIds.size === 0) return;
|
|
1749
|
-
const msg =
|
|
2274
|
+
const msg = t('batchDeleteConfirm').replace('%count%', selectedIds.size);
|
|
1750
2275
|
if (!confirm(msg)) return;
|
|
1751
2276
|
|
|
1752
2277
|
const sep = selectedProject ? `?project=${encodeURIComponent(selectedProject)}` : '';
|
|
@@ -1799,7 +2324,7 @@ async function loadObservations() {
|
|
|
1799
2324
|
allObservations = await api('observations') || [];
|
|
1800
2325
|
|
|
1801
2326
|
if (allObservations.length === 0) {
|
|
1802
|
-
container.innerHTML = emptyState('
|
|
2327
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:search" style="font-size:36px;"></span>', t('noObsTitle'), t('noObsDesc'));
|
|
1803
2328
|
return;
|
|
1804
2329
|
}
|
|
1805
2330
|
|
|
@@ -1814,8 +2339,8 @@ async function loadObservations() {
|
|
|
1814
2339
|
<p class="page-subtitle">${allObservations.length} ${t('observationsStored')}</p>
|
|
1815
2340
|
</div>
|
|
1816
2341
|
<div style="display:flex;gap:8px;">
|
|
1817
|
-
<button class="export-btn" id="btn-batch-cleanup" title="${t('batchCleanup')
|
|
1818
|
-
🧹 ${t('batchCleanup')
|
|
2342
|
+
<button class="export-btn" id="btn-batch-cleanup" title="${t('batchCleanup')}">
|
|
2343
|
+
🧹 ${t('batchCleanup')}
|
|
1819
2344
|
</button>
|
|
1820
2345
|
<button class="export-btn" id="btn-export" title="${t('exportData')}">
|
|
1821
2346
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 7l4 4 4-4M2 12v2h12v-2"/></svg>
|
|
@@ -1879,9 +2404,9 @@ function renderObsList() {
|
|
|
1879
2404
|
if (!list) return;
|
|
1880
2405
|
|
|
1881
2406
|
const typeIcons = {
|
|
1882
|
-
'session-request': '
|
|
1883
|
-
'how-it-works': '
|
|
1884
|
-
'why-it-exists': '
|
|
2407
|
+
'session-request': '<span class="iconify" data-icon="lucide:target" style="color:#f87171;"></span>', gotcha: '<span class="iconify" data-icon="lucide:alert-octagon" style="color:#ef4444;"></span>', 'problem-solution': '<span class="iconify" data-icon="lucide:lightbulb" style="color:#fbbf24;"></span>',
|
|
2408
|
+
'how-it-works': '<span class="iconify" data-icon="lucide:info" style="color:#38bdf8;"></span>', 'what-changed': '<span class="iconify" data-icon="lucide:git-branch" style="color:#4ade80;"></span>', discovery: '<span class="iconify" data-icon="lucide:sparkles" style="color:#a78bfa;"></span>',
|
|
2409
|
+
'why-it-exists': '<span class="iconify" data-icon="lucide:help-circle" style="color:#fb923c;"></span>', decision: '<span class="iconify" data-icon="lucide:scale" style="color:#a1887f;"></span>', 'trade-off': '<span class="iconify" data-icon="lucide:scale" style="color:#94a3b8;"></span>',
|
|
1885
2410
|
};
|
|
1886
2411
|
|
|
1887
2412
|
let filtered = allObservations;
|
|
@@ -1914,14 +2439,14 @@ function renderObsList() {
|
|
|
1914
2439
|
${batchMode ? `<input type="checkbox" class="obs-checkbox" ${isSelected ? 'checked' : ''} onclick="event.stopPropagation(); toggleObsSelect(${obs.id});" />` : ''}
|
|
1915
2440
|
<span class="obs-card-id">#${obs.id}</span>
|
|
1916
2441
|
<span class="type-badge" data-type="${obs.type || 'unknown'}">
|
|
1917
|
-
${typeIcons[obs.type] || '❓'} ${obs.type || 'unknown'}
|
|
2442
|
+
${typeIcons[obs.type] || '❓'} ${obs.type || t('unknown')}
|
|
1918
2443
|
</span>
|
|
1919
|
-
${isLow ? '<span class="low-quality-badge">
|
|
2444
|
+
${isLow ? '<span class="low-quality-badge">' + t('lowQuality') + '</span>' : ''}
|
|
1920
2445
|
<span class="obs-card-title">${hl(obs.title || t('untitled'))}</span>
|
|
1921
2446
|
<span class="obs-expand-icon">▼</span>
|
|
1922
2447
|
</div>
|
|
1923
2448
|
<div class="obs-card-meta">
|
|
1924
|
-
<span>📁 ${hl(obs.entityName || 'unknown')}</span>
|
|
2449
|
+
<span>📁 ${hl(obs.entityName || t('unknown'))}</span>
|
|
1925
2450
|
${obs.createdAt ? `<span>🕐 ${formatTime(obs.createdAt)}</span>` : ''}
|
|
1926
2451
|
${obs.accessCount ? `<span>👁 ${obs.accessCount}</span>` : ''}
|
|
1927
2452
|
</div>
|
|
@@ -2019,7 +2544,7 @@ async function loadRetention() {
|
|
|
2019
2544
|
</td>
|
|
2020
2545
|
<td style="font-family: var(--font-mono); color: var(--text-muted); font-size: 12px;">${item.ageHours}h</td>
|
|
2021
2546
|
<td style="font-family: var(--font-mono); color: var(--text-muted); font-size: 12px;">${item.accessCount}</td>
|
|
2022
|
-
<td>${item.isImmune ? `<span class="immune-badge"
|
|
2547
|
+
<td>${item.isImmune ? `<span class="immune-badge"><span class="iconify" data-icon="lucide:shield" style="font-size:12px;vertical-align:middle;margin-right:3px;"></span>${t('immune')}</span>` : ''}</td>
|
|
2023
2548
|
</tr>
|
|
2024
2549
|
`;
|
|
2025
2550
|
}).join('')}
|
|
@@ -2101,10 +2626,10 @@ async function deleteObs(id, event) {
|
|
|
2101
2626
|
const subtitle = document.querySelector('#page-observations .page-subtitle');
|
|
2102
2627
|
if (subtitle) subtitle.textContent = `${allObservations.length} ${t('observationsStored')}`;
|
|
2103
2628
|
} else {
|
|
2104
|
-
alert(data.error || '
|
|
2629
|
+
alert(data.error || t('deleteFailed'));
|
|
2105
2630
|
}
|
|
2106
2631
|
} catch (err) {
|
|
2107
|
-
alert('
|
|
2632
|
+
alert(t('deleteFailed') + ': ' + err.message);
|
|
2108
2633
|
}
|
|
2109
2634
|
}
|
|
2110
2635
|
|
|
@@ -2176,7 +2701,7 @@ async function loadGitMemory() {
|
|
|
2176
2701
|
<div class="panel">
|
|
2177
2702
|
<div class="panel-header">
|
|
2178
2703
|
<span class="panel-title">${t('recentGitMemories')}</span>
|
|
2179
|
-
<span style="font-size:11px;color:var(--text-muted);">${gitObs.length}
|
|
2704
|
+
<span style="font-size:11px;color:var(--text-muted);">${gitObs.length} ${t('gitMemoryTotal')}</span>
|
|
2180
2705
|
</div>
|
|
2181
2706
|
<div class="panel-body" style="padding:0;">
|
|
2182
2707
|
<table class="retention-table">
|
|
@@ -2196,8 +2721,8 @@ async function loadGitMemory() {
|
|
|
2196
2721
|
<tr>
|
|
2197
2722
|
<td style="font-family:var(--font-mono);color:var(--text-muted);">#${obs.id}</td>
|
|
2198
2723
|
<td><code class="git-hash">${obs.commitHash ? escapeHtml(obs.commitHash.slice(0, 7)) : '—'}</code></td>
|
|
2199
|
-
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(obs.title || '
|
|
2200
|
-
<td><span class="type-badge" data-type="${obs.type || 'unknown'}">${obs.type || 'unknown'}</span></td>
|
|
2724
|
+
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(obs.title || t('untitled'))}</td>
|
|
2725
|
+
<td><span class="type-badge" data-type="${obs.type || 'unknown'}">${obs.type || t('unknown')}</span></td>
|
|
2201
2726
|
<td style="font-family:var(--font-mono);font-size:12px;color:var(--text-muted);">${escapeHtml(obs.entityName || '')}</td>
|
|
2202
2727
|
<td style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);">${(obs.filesModified || []).length || '—'}</td>
|
|
2203
2728
|
<td style="font-size:11px;color:var(--text-muted);">${obs.createdAt ? formatTime(obs.createdAt) : '—'}</td>
|
|
@@ -2221,7 +2746,7 @@ async function loadConfig() {
|
|
|
2221
2746
|
|
|
2222
2747
|
const data = await api('config');
|
|
2223
2748
|
if (!data) {
|
|
2224
|
-
container.innerHTML = emptyState('
|
|
2749
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:settings" style="font-size:36px;"></span>', t('configUnavailable'), t('configUnavailableDesc'));
|
|
2225
2750
|
return;
|
|
2226
2751
|
}
|
|
2227
2752
|
|
|
@@ -2241,9 +2766,9 @@ async function loadConfig() {
|
|
|
2241
2766
|
<div class="config-matrix">
|
|
2242
2767
|
${fileEntries.map(([name, info]) => `
|
|
2243
2768
|
<div class="config-file-row">
|
|
2244
|
-
<span class="config-file-status ${info.exists ? 'exists' : 'missing'}">${info.exists ? '
|
|
2769
|
+
<span class="config-file-status ${info.unavailable ? 'unavailable' : info.exists ? 'exists' : 'missing'}">${info.unavailable ? '—' : info.exists ? '<span class="iconify" data-icon="lucide:check" style="font-size:13px;"></span>' : '<span class="iconify" data-icon="lucide:x" style="font-size:13px;"></span>'}</span>
|
|
2245
2770
|
<span class="config-file-name">${escapeHtml(name)}</span>
|
|
2246
|
-
<span class="config-file-path">${info.path ? escapeHtml(info.path) : ''}</span>
|
|
2771
|
+
<span class="config-file-path">${info.unavailable ? '<span style="color:var(--accent-amber);font-style:italic;">' + t('configProjectUnavailable') + '</span>' : info.path ? escapeHtml(info.path) : ''}</span>
|
|
2247
2772
|
</div>
|
|
2248
2773
|
`).join('')}
|
|
2249
2774
|
</div>
|
|
@@ -2278,7 +2803,7 @@ async function loadConfig() {
|
|
|
2278
2803
|
<td><code class="config-key">${escapeHtml(v.key)}</code></td>
|
|
2279
2804
|
<td style="font-family:var(--font-mono);font-size:12px;">${isSensitive ? '<span class="config-masked">' + escapeHtml(v.value) + '</span>' : escapeHtml(v.value)}</td>
|
|
2280
2805
|
<td><span class="config-source-badge ${isWarn ? 'warn' : ''}">${escapeHtml(v.source)}</span></td>
|
|
2281
|
-
<td>${isWarn ? '<span class="config-warn-badge"
|
|
2806
|
+
<td>${isWarn ? '<span class="config-warn-badge"><span class="iconify" data-icon="lucide:alert-triangle" style="font-size:11px;vertical-align:middle;margin-right:3px;"></span> ' + t('moveToEnv') + '</span>' : ''}</td>
|
|
2282
2807
|
</tr>
|
|
2283
2808
|
`;
|
|
2284
2809
|
}).join('')}
|
|
@@ -2293,51 +2818,88 @@ async function loadConfig() {
|
|
|
2293
2818
|
// Identity Health Page
|
|
2294
2819
|
// ============================================================
|
|
2295
2820
|
|
|
2821
|
+
let identityShowHistorical = false; // H3: historical IDs collapsed by default
|
|
2822
|
+
|
|
2296
2823
|
async function loadIdentity() {
|
|
2297
2824
|
const container = document.getElementById('page-identity');
|
|
2298
2825
|
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
2299
2826
|
|
|
2300
2827
|
const data = await api('identity');
|
|
2301
2828
|
if (!data) {
|
|
2302
|
-
container.innerHTML = emptyState('
|
|
2829
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:shield" style="font-size:36px;"></span>', t('identityUnavailable'), t('identityUnavailableDesc'));
|
|
2303
2830
|
return;
|
|
2304
2831
|
}
|
|
2305
2832
|
|
|
2833
|
+
// Get project resolved state
|
|
2834
|
+
const projectInfo = await api('project');
|
|
2835
|
+
const isResolved = projectInfo?.resolved !== false;
|
|
2836
|
+
|
|
2306
2837
|
const healthColor = data.isHealthy ? 'var(--accent-green)' : 'var(--accent-red)';
|
|
2307
2838
|
const healthIcon = data.isHealthy ? t('healthy') : t('unhealthy');
|
|
2308
2839
|
|
|
2840
|
+
// Layered identity data (H3)
|
|
2841
|
+
const realIds = data.realKnownIds || [];
|
|
2842
|
+
const temporaryIds = data.temporaryKnownIds || [];
|
|
2843
|
+
const placeholderIds = data.placeholderKnownIds || [];
|
|
2844
|
+
const historicalIds = [...temporaryIds, ...placeholderIds];
|
|
2845
|
+
const aliasGroupsReal = typeof data.aliasGroupsReal === 'number' ? data.aliasGroupsReal : (data.aliasGroups || 0);
|
|
2846
|
+
const aliasGroupsAll = data.aliasGroups || 0;
|
|
2847
|
+
|
|
2848
|
+
// Separate dirty IDs into current-project vs historical
|
|
2849
|
+
const currentDirtyIds = (data.dirtyIds || []).filter(id => id === data.currentProjectId || (data.aliases || []).includes(id));
|
|
2850
|
+
const historicalDirtyIds = (data.dirtyIds || []).filter(id => !currentDirtyIds.includes(id));
|
|
2851
|
+
|
|
2309
2852
|
container.innerHTML = `
|
|
2310
2853
|
<div class="page-header">
|
|
2311
2854
|
<h1 class="page-title">${t('identityTitle')}</h1>
|
|
2312
2855
|
<p class="page-subtitle">${t('identitySubtitle')}</p>
|
|
2313
2856
|
</div>
|
|
2314
2857
|
|
|
2858
|
+
${!isResolved ? `
|
|
2859
|
+
<div class="panel" style="margin-bottom:16px;border-color:var(--accent-amber);">
|
|
2860
|
+
<div class="panel-body" style="display:flex;align-items:center;gap:12px;padding:12px 16px;">
|
|
2861
|
+
<span class="iconify" data-icon="lucide:alert-triangle" style="font-size:20px;color:var(--accent-amber);"></span>
|
|
2862
|
+
<div>
|
|
2863
|
+
<div style="font-weight:600;color:var(--accent-amber);">${t('projectUnresolved')}</div>
|
|
2864
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:2px;">${t('projectUnresolvedDesc')}</div>
|
|
2865
|
+
</div>
|
|
2866
|
+
</div>
|
|
2867
|
+
</div>
|
|
2868
|
+
` : ''}
|
|
2869
|
+
|
|
2315
2870
|
<div class="stats-grid">
|
|
2316
2871
|
<div class="stat-card" data-accent="${data.isHealthy ? 'green' : 'red'}">
|
|
2317
|
-
<div class="stat-label">${t('
|
|
2872
|
+
<div class="stat-label">${t('identityHealthPrimary')}</div>
|
|
2318
2873
|
<div class="stat-value" style="font-size:20px;color:${healthColor}">${healthIcon}</div>
|
|
2319
2874
|
</div>
|
|
2320
2875
|
<div class="stat-card" data-accent="cyan">
|
|
2321
|
-
<div class="stat-label">${t('
|
|
2322
|
-
<div class="stat-value">${
|
|
2876
|
+
<div class="stat-label">${t('identityRealProjects')}</div>
|
|
2877
|
+
<div class="stat-value">${realIds.length}</div>
|
|
2878
|
+
<div class="team-stat-sub">${historicalIds.length} ${t('teamHistoricalCount')}</div>
|
|
2323
2879
|
</div>
|
|
2324
2880
|
<div class="stat-card" data-accent="purple">
|
|
2325
|
-
<div class="stat-label">${t('
|
|
2326
|
-
<div class="stat-value">${
|
|
2881
|
+
<div class="stat-label">${t('identityAliasGroupsReal')}</div>
|
|
2882
|
+
<div class="stat-value">${aliasGroupsReal}</div>
|
|
2883
|
+
<div class="team-stat-sub">${aliasGroupsAll} ${t('aliasGroups').toLowerCase()} ${t('identityTotalCount')}</div>
|
|
2327
2884
|
</div>
|
|
2328
|
-
<div class="stat-card" data-accent="amber">
|
|
2885
|
+
<div class="stat-card" data-accent="${currentDirtyIds.length > 0 ? 'red' : 'amber'}">
|
|
2329
2886
|
<div class="stat-label">${t('dirtyIds')}</div>
|
|
2330
|
-
<div class="stat-value">${
|
|
2887
|
+
<div class="stat-value">${currentDirtyIds.length}</div>
|
|
2888
|
+
<div class="team-stat-sub">${historicalDirtyIds.length} ${t('teamHistoricalCount')}</div>
|
|
2331
2889
|
</div>
|
|
2332
2890
|
</div>
|
|
2333
2891
|
|
|
2334
2892
|
<div class="overview-row">
|
|
2335
2893
|
<div class="panel" style="flex:1;">
|
|
2336
|
-
<div class="panel-header"><span class="panel-title">${t('
|
|
2894
|
+
<div class="panel-header"><span class="panel-title">${t('identityCurrentProject')}</span></div>
|
|
2337
2895
|
<div class="panel-body">
|
|
2338
2896
|
<div class="identity-row">
|
|
2339
2897
|
<span class="identity-label">${t('currentProjectId')}</span>
|
|
2340
2898
|
<code class="identity-value">${escapeHtml(data.currentProjectId || '—')}</code>
|
|
2899
|
+
${isResolved
|
|
2900
|
+
? '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(34,197,94,0.12);color:var(--accent-green);margin-left:6px;">' + t('projectResolved') + '</span>'
|
|
2901
|
+
: '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(245,158,11,0.12);color:var(--accent-amber);margin-left:6px;">' + t('projectUnresolved') + '</span>'
|
|
2902
|
+
}
|
|
2341
2903
|
</div>
|
|
2342
2904
|
<div class="identity-row">
|
|
2343
2905
|
<span class="identity-label">${t('canonicalId')}</span>
|
|
@@ -2347,6 +2909,12 @@ async function loadIdentity() {
|
|
|
2347
2909
|
<span class="identity-label">${t('aliases')}</span>
|
|
2348
2910
|
<div>${(data.aliases || []).map(a => `<code class="identity-alias">${escapeHtml(a)}</code>`).join(' ')}</div>
|
|
2349
2911
|
</div>
|
|
2912
|
+
${currentDirtyIds.length > 0 ? `
|
|
2913
|
+
<div style="margin-top:8px;padding:8px 12px;border-radius:6px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.15);">
|
|
2914
|
+
<div style="font-size:12px;font-weight:600;color:var(--accent-red);margin-bottom:4px;"><span class="iconify" data-icon="lucide:alert-triangle" style="font-size:12px;vertical-align:middle;margin-right:3px;"></span> ${t('identityDirtyCurrentWarning')}</div>
|
|
2915
|
+
${currentDirtyIds.map(id => `<code class="identity-dirty">${escapeHtml(id)}</code>`).join(' ')}
|
|
2916
|
+
</div>
|
|
2917
|
+
` : ''}
|
|
2350
2918
|
</div>
|
|
2351
2919
|
</div>
|
|
2352
2920
|
|
|
@@ -2354,10 +2922,10 @@ async function loadIdentity() {
|
|
|
2354
2922
|
<div class="panel-header"><span class="panel-title">${t('healthIssues')}</span></div>
|
|
2355
2923
|
<div class="panel-body">
|
|
2356
2924
|
${(data.healthIssues || []).length === 0
|
|
2357
|
-
? '<div style="color:var(--accent-green);font-size:13px;">' + t('
|
|
2925
|
+
? '<div style="color:var(--accent-green);font-size:13px;">' + (isResolved ? t('identityProjectBound') : t('identityProjectUnbound')) + '</div>'
|
|
2358
2926
|
: (data.healthIssues || []).map(issue => `
|
|
2359
2927
|
<div class="identity-issue">
|
|
2360
|
-
<span style="color:var(--accent-red);"
|
|
2928
|
+
<span class="iconify" data-icon="lucide:alert-triangle" style="font-size:13px;color:var(--accent-red);vertical-align:middle;"></span>
|
|
2361
2929
|
<span>${escapeHtml(issue)}</span>
|
|
2362
2930
|
</div>
|
|
2363
2931
|
`).join('')
|
|
@@ -2370,34 +2938,85 @@ async function loadIdentity() {
|
|
|
2370
2938
|
<div class="panel">
|
|
2371
2939
|
<div class="panel-header"><span class="panel-title">${t('dirtyProjectIds')}</span></div>
|
|
2372
2940
|
<div class="panel-body">
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2941
|
+
${currentDirtyIds.length > 0 ? `
|
|
2942
|
+
<div style="margin-bottom:12px;">
|
|
2943
|
+
<div style="font-size:12px;font-weight:600;color:var(--accent-red);margin-bottom:6px;"><span class="iconify" data-icon="lucide:alert-triangle" style="font-size:12px;vertical-align:middle;margin-right:3px;"></span> ${t('identityDirtyCurrentWarning')}</div>
|
|
2944
|
+
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
|
2945
|
+
${currentDirtyIds.map(id => `<code class="identity-dirty">${escapeHtml(id)}</code>`).join('')}
|
|
2946
|
+
</div>
|
|
2947
|
+
</div>
|
|
2948
|
+
` : ''}
|
|
2949
|
+
${historicalDirtyIds.length > 0 ? `
|
|
2950
|
+
<div>
|
|
2951
|
+
<div style="font-size:12px;font-weight:500;color:var(--text-muted);margin-bottom:6px;">${t('identityDirtyHistoricalNote')}</div>
|
|
2952
|
+
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
|
2953
|
+
${historicalDirtyIds.map(id => `<code class="identity-dirty" style="opacity:0.6;">${escapeHtml(id)}</code>`).join('')}
|
|
2954
|
+
</div>
|
|
2955
|
+
</div>
|
|
2956
|
+
` : ''}
|
|
2376
2957
|
</div>
|
|
2377
2958
|
</div>
|
|
2378
2959
|
` : ''}
|
|
2379
2960
|
|
|
2380
2961
|
<div class="panel">
|
|
2381
2962
|
<div class="panel-header">
|
|
2382
|
-
<span class="panel-title">${t('
|
|
2383
|
-
<span style="font-size:11px;color:var(--text-muted);">${
|
|
2963
|
+
<span class="panel-title">${t('identityRealProjects')}</span>
|
|
2964
|
+
<span style="font-size:11px;color:var(--text-muted);">${realIds.length}</span>
|
|
2384
2965
|
</div>
|
|
2385
2966
|
<div class="panel-body">
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2967
|
+
${realIds.length === 0
|
|
2968
|
+
? '<div style="color:var(--text-muted);font-size:12px;">—</div>'
|
|
2969
|
+
: `<div style="display:flex;flex-direction:column;gap:6px;">
|
|
2970
|
+
${realIds.map(id => {
|
|
2971
|
+
const isDirty = (data.dirtyIds || []).includes(id);
|
|
2972
|
+
const isCurrent = id === data.currentProjectId;
|
|
2973
|
+
const isCanonical = id === data.canonicalId;
|
|
2974
|
+
return `<div class="identity-id-row">
|
|
2975
|
+
<code class="identity-id ${isDirty ? 'dirty' : ''}">${escapeHtml(id)}</code>
|
|
2976
|
+
${isCurrent ? '<span class="identity-tag current">' + t('tagCurrent') + '</span>' : ''}
|
|
2977
|
+
${isCanonical ? '<span class="identity-tag canonical">' + t('tagCanonical') + '</span>' : ''}
|
|
2978
|
+
${isDirty ? '<span class="identity-tag dirty">' + t('tagDirty') + '</span>' : ''}
|
|
2979
|
+
</div>`;
|
|
2980
|
+
}).join('')}
|
|
2981
|
+
</div>`
|
|
2982
|
+
}
|
|
2399
2983
|
</div>
|
|
2400
2984
|
</div>
|
|
2985
|
+
|
|
2986
|
+
${historicalIds.length > 0 ? `
|
|
2987
|
+
<div class="panel">
|
|
2988
|
+
<div class="panel-header" style="cursor:pointer;" onclick="identityShowHistorical = !identityShowHistorical; delete loaded['identity']; loadIdentity();">
|
|
2989
|
+
<span class="panel-title">
|
|
2990
|
+
<span class="iconify" data-icon="${identityShowHistorical ? 'lucide:chevron-down' : 'lucide:chevron-right'}" style="font-size:14px;vertical-align:middle;"></span>
|
|
2991
|
+
${t('identityHistoricalFold')}
|
|
2992
|
+
</span>
|
|
2993
|
+
<span style="font-size:11px;color:var(--text-muted);">
|
|
2994
|
+
${temporaryIds.length} ${t('identityTemporary').toLowerCase()} · ${placeholderIds.length} ${t('identityPlaceholder').toLowerCase()}
|
|
2995
|
+
</span>
|
|
2996
|
+
</div>
|
|
2997
|
+
${identityShowHistorical ? `
|
|
2998
|
+
<div class="panel-body">
|
|
2999
|
+
<div style="font-size:11px;color:var(--text-muted);margin-bottom:10px;font-style:italic;">${t('identityHistoricalNote')}</div>
|
|
3000
|
+
${temporaryIds.length > 0 ? `
|
|
3001
|
+
<div style="margin-bottom:12px;">
|
|
3002
|
+
<div style="font-size:11px;font-weight:600;color:var(--text-muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">${t('identityTemporary')} (${temporaryIds.length})</div>
|
|
3003
|
+
<div style="display:flex;flex-direction:column;gap:4px;">
|
|
3004
|
+
${temporaryIds.map(id => `<div class="identity-id-row" style="opacity:0.65;"><code class="identity-id">${escapeHtml(id)}</code><span class="identity-tag" style="background:rgba(148,163,184,0.15);color:var(--text-muted);">temp</span></div>`).join('')}
|
|
3005
|
+
</div>
|
|
3006
|
+
</div>
|
|
3007
|
+
` : ''}
|
|
3008
|
+
${placeholderIds.length > 0 ? `
|
|
3009
|
+
<div>
|
|
3010
|
+
<div style="font-size:11px;font-weight:600;color:var(--text-muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">${t('identityPlaceholder')} (${placeholderIds.length})</div>
|
|
3011
|
+
<div style="display:flex;flex-direction:column;gap:4px;">
|
|
3012
|
+
${placeholderIds.map(id => `<div class="identity-id-row" style="opacity:0.5;"><code class="identity-id dirty">${escapeHtml(id)}</code><span class="identity-tag dirty">placeholder</span></div>`).join('')}
|
|
3013
|
+
</div>
|
|
3014
|
+
</div>
|
|
3015
|
+
` : ''}
|
|
3016
|
+
</div>
|
|
3017
|
+
` : ''}
|
|
3018
|
+
</div>
|
|
3019
|
+
` : ''}
|
|
2401
3020
|
`;
|
|
2402
3021
|
}
|
|
2403
3022
|
|
|
@@ -2441,7 +3060,7 @@ async function loadSessions() {
|
|
|
2441
3060
|
|
|
2442
3061
|
const sessions = await api('sessions');
|
|
2443
3062
|
if (!sessions || sessions.length === 0) {
|
|
2444
|
-
container.innerHTML = emptyState('
|
|
3063
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:clipboard-list" style="font-size:36px;"></span>', t('noSessions'), t('noSessionsDesc'));
|
|
2445
3064
|
return;
|
|
2446
3065
|
}
|
|
2447
3066
|
|
|
@@ -2467,13 +3086,13 @@ async function loadSessions() {
|
|
|
2467
3086
|
<div class="stat-value">${completedCount}</div>
|
|
2468
3087
|
</div>
|
|
2469
3088
|
<div class="stat-card" data-accent="purple">
|
|
2470
|
-
<div class="stat-label"
|
|
3089
|
+
<div class="stat-label">${t('sessionsTotal')}</div>
|
|
2471
3090
|
<div class="stat-value">${sessions.length}</div>
|
|
2472
3091
|
</div>
|
|
2473
3092
|
</div>
|
|
2474
3093
|
|
|
2475
3094
|
<div class="panel">
|
|
2476
|
-
<div class="panel-header"><span class="panel-title"
|
|
3095
|
+
<div class="panel-header"><span class="panel-title">${t('sessionsTimeline')}</span></div>
|
|
2477
3096
|
<div class="panel-body" style="padding: 0;">
|
|
2478
3097
|
<table class="retention-table">
|
|
2479
3098
|
<thead>
|
|
@@ -2491,8 +3110,8 @@ async function loadSessions() {
|
|
|
2491
3110
|
|
|
2492
3111
|
for (const s of sessions) {
|
|
2493
3112
|
const statusBadge = s.status === 'active'
|
|
2494
|
-
? '<span class="badge" style="background:var(--color-green);color:#fff"
|
|
2495
|
-
: '<span class="badge" style="background:var(--color-blue);color:#fff"
|
|
3113
|
+
? '<span class="badge" style="background:var(--color-green);color:#fff"><span class="iconify" data-icon="lucide:circle-dot" style="font-size:11px;vertical-align:middle;margin-right:3px;"></span> ' + t('sessionActive') + '</span>'
|
|
3114
|
+
: '<span class="badge" style="background:var(--color-blue);color:#fff"><span class="iconify" data-icon="lucide:circle-check" style="font-size:11px;vertical-align:middle;margin-right:3px;"></span> ' + t('sessionCompleted') + '</span>';
|
|
2496
3115
|
const agent = s.agent ? escapeHtml(s.agent) : '—';
|
|
2497
3116
|
const started = formatTime(s.startedAt);
|
|
2498
3117
|
const ended = s.endedAt ? formatTime(s.endedAt) : '—';
|
|
@@ -2526,23 +3145,25 @@ function teamTimeAgo(dateStr) {
|
|
|
2526
3145
|
if (!dateStr) return '';
|
|
2527
3146
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
2528
3147
|
const sec = Math.floor(diff / 1000);
|
|
2529
|
-
if (sec < 60) return sec + '
|
|
3148
|
+
if (sec < 60) return sec + t('timeAgoS');
|
|
2530
3149
|
const min = Math.floor(sec / 60);
|
|
2531
|
-
if (min < 60) return min + '
|
|
3150
|
+
if (min < 60) return min + t('timeAgoM');
|
|
2532
3151
|
const hr = Math.floor(min / 60);
|
|
2533
|
-
if (hr < 24) return hr + '
|
|
2534
|
-
return Math.floor(hr / 24) + '
|
|
3152
|
+
if (hr < 24) return hr + t('timeAgoH');
|
|
3153
|
+
return Math.floor(hr / 24) + t('timeAgoD');
|
|
2535
3154
|
}
|
|
2536
3155
|
|
|
2537
3156
|
function teamLockTTL(expiresAt) {
|
|
2538
3157
|
if (!expiresAt) return '';
|
|
2539
3158
|
const remaining = new Date(expiresAt).getTime() - Date.now();
|
|
2540
|
-
if (remaining <= 0) return '
|
|
3159
|
+
if (remaining <= 0) return t('timeExpired');
|
|
2541
3160
|
const min = Math.floor(remaining / 60000);
|
|
2542
|
-
return min + '
|
|
3161
|
+
return min + t('timeLeft');
|
|
2543
3162
|
}
|
|
2544
3163
|
|
|
2545
3164
|
let teamScope = 'project'; // 'project' | 'global'
|
|
3165
|
+
let teamTierFilter = 'active'; // 'active' | 'recent' | 'historical' | 'all'
|
|
3166
|
+
let teamShowHistorical = false; // collapsed by default
|
|
2546
3167
|
|
|
2547
3168
|
async function loadTeam() {
|
|
2548
3169
|
const container = document.getElementById('page-team');
|
|
@@ -2559,7 +3180,7 @@ async function loadTeam() {
|
|
|
2559
3180
|
</div>
|
|
2560
3181
|
<div class="panel">
|
|
2561
3182
|
<div class="panel-body" style="text-align:center;padding:48px;">
|
|
2562
|
-
<div style="font-size:36px;margin-bottom:12px;"
|
|
3183
|
+
<div style="font-size:36px;margin-bottom:12px;"><span class="iconify" data-icon="lucide:users" style="font-size:36px;"></span></div>
|
|
2563
3184
|
<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('teamNoData')}</div>
|
|
2564
3185
|
<div style="font-size:13px;color:var(--text-muted);max-width:480px;margin:0 auto;line-height:1.6;">
|
|
2565
3186
|
${t('teamNoDataHint')}<br>
|
|
@@ -2577,18 +3198,48 @@ async function loadTeam() {
|
|
|
2577
3198
|
completed: 'lucide:circle-check',
|
|
2578
3199
|
failed: 'lucide:circle-x',
|
|
2579
3200
|
};
|
|
2580
|
-
const statusLabels = { pending: '
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
const
|
|
2584
|
-
const
|
|
3201
|
+
const statusLabels = { pending: t('taskPending'), in_progress: t('taskInProgress'), completed: t('taskCompleted'), failed: t('taskFailed') };
|
|
3202
|
+
|
|
3203
|
+
// Ensure arrays exist and fields are safe
|
|
3204
|
+
const safeAgents = (data.agents || []).map(a => ({ ...a, id: a.id || a.agent_id || '', name: a.name || t('unknown'), status: a.status || 'inactive', unread: a.unread || 0, activityTier: a.activityTier || (a.status === 'active' ? 'active' : 'historical') }));
|
|
3205
|
+
const safeLocks = (data.locks || []).map(l => ({ ...l, file: l.file || '', lockedBy: l.lockedBy || l.locked_by || '', lockedAt: l.lockedAt || l.locked_at, expiresAt: l.expiresAt || l.expires_at }));
|
|
3206
|
+
const safeTasks = (data.tasks || []).map(tk => ({ ...tk, id: tk.id || tk.task_id || '', status: tk.status || 'pending', assignee: tk.assignee || tk.assignee_agent_id, deps: tk.deps || [], required_role: tk.required_role || tk.requiredRole || null, preferred_role: tk.preferred_role || tk.preferredRole || null }));
|
|
3207
|
+
const safeRoles = (data.roles || []).map(r => ({ ...r, roleId: r.role_id || r.roleId || '', label: r.label || '', description: r.description || '', preferredAgentTypes: r.preferred_agent_types || r.preferredAgentTypes || '[]', maxConcurrent: r.max_concurrent || r.maxConcurrent || 1 }));
|
|
3208
|
+
const safeOccupancy = (data.roleOccupancy || []).map(o => ({ ...o, role: { ...o.role, roleId: o.role.role_id || o.role.roleId || '', label: o.role.label || '', maxConcurrent: o.role.max_concurrent || o.role.maxConcurrent || 1, preferredAgentTypes: o.role.preferred_agent_types || o.role.preferredAgentTypes || '[]' }, activeAgents: (o.activeAgents || []).map(a => ({ ...a, name: a.name || 'unknown', role: a.role || '' })), vacant: o.vacant || 0 }));
|
|
3209
|
+
const safeHandoffs = (data.handoffs || []).map(h => ({ ...h, id: h.id || '', sender_agent_id: h.sender_agent_id || '', to_role: h.to_role || '', handoff_status: h.handoff_status || h.handoffStatus || 'open', content: h.content || '', created_at: h.created_at || h.createdAt || 0 }));
|
|
3210
|
+
|
|
3211
|
+
// H1/H2: headline semantics — active is primary, historical is secondary
|
|
3212
|
+
const totalAgents = typeof data.totalAgents === 'number' ? data.totalAgents : safeAgents.length;
|
|
3213
|
+
const activeCount = typeof data.activeCount === 'number' ? data.activeCount : safeAgents.filter(a => a.status === 'active').length;
|
|
3214
|
+
const recentCount = typeof data.recentCount === 'number' ? data.recentCount : safeAgents.filter(a => a.activityTier === 'recent').length;
|
|
3215
|
+
const historicalCount = typeof data.historicalCount === 'number' ? data.historicalCount : safeAgents.filter(a => a.activityTier === 'historical').length;
|
|
3216
|
+
const totalUnread = data.totalUnread || safeAgents.reduce((sum, a) => sum + (a.unread || 0), 0);
|
|
2585
3217
|
const tasksByStatus = { pending: 0, in_progress: 0, completed: 0, failed: 0 };
|
|
2586
|
-
|
|
3218
|
+
safeTasks.forEach(tk => { tasksByStatus[tk.status] = (tasksByStatus[tk.status] || 0) + 1; });
|
|
3219
|
+
|
|
3220
|
+
// Apply tier filter for the Agents panel
|
|
3221
|
+
const tierMatches = (tier) => {
|
|
3222
|
+
if (teamTierFilter === 'all') return true;
|
|
3223
|
+
if (teamTierFilter === 'active') return tier === 'active';
|
|
3224
|
+
if (teamTierFilter === 'recent') return tier === 'recent';
|
|
3225
|
+
if (teamTierFilter === 'historical') return tier === 'historical';
|
|
3226
|
+
return true;
|
|
3227
|
+
};
|
|
3228
|
+
const filteredAgents = safeAgents.filter(a => {
|
|
3229
|
+
if (!tierMatches(a.activityTier)) return false;
|
|
3230
|
+
if (!teamShowHistorical && a.activityTier === 'historical' && teamTierFilter !== 'historical') return false;
|
|
3231
|
+
return true;
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
const scopeLabel = teamScope === 'global' ? t('projectScopeGlobal') : t('projectScopeProject');
|
|
3235
|
+
const scopeDesc = teamScope === 'global' ? t('projectScopeGlobalDesc') : t('projectScopeProjectDesc');
|
|
2587
3236
|
|
|
2588
|
-
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
3237
|
+
// Resume data from API
|
|
3238
|
+
const openTasks = data.openTasks || 0;
|
|
3239
|
+
const availableTasks = data.availableTasks || 0;
|
|
3240
|
+
const openHandoffs = data.openHandoffs || 0;
|
|
3241
|
+
const activeSessions = data.activeSessions || 0;
|
|
3242
|
+
const hasPending = openTasks > 0 || openHandoffs > 0 || totalUnread > 0 || safeLocks.length > 0;
|
|
2592
3243
|
|
|
2593
3244
|
let html = `
|
|
2594
3245
|
<div class="team-header">
|
|
@@ -2598,92 +3249,187 @@ async function loadTeam() {
|
|
|
2598
3249
|
</div>
|
|
2599
3250
|
<div>
|
|
2600
3251
|
<h1 class="page-title">${scopeLabel}</h1>
|
|
2601
|
-
<p class="page-subtitle">${scopeDesc}
|
|
3252
|
+
<p class="page-subtitle">${scopeDesc}</p>
|
|
2602
3253
|
</div>
|
|
2603
3254
|
</div>
|
|
2604
3255
|
<div class="team-header-right">
|
|
2605
3256
|
<div style="display:flex;gap:2px;margin-right:12px;">
|
|
2606
|
-
<button class="filter-btn${teamScope === 'project' ? ' active' : ''}" onclick="teamScope='project';delete loaded['team'];loadTeam();" style="padding:6px 14px;font-size:12px;"
|
|
2607
|
-
<button class="filter-btn${teamScope === 'global' ? ' active' : ''}" onclick="teamScope='global';delete loaded['team'];loadTeam();" style="padding:6px 14px;font-size:12px;"
|
|
3257
|
+
<button class="filter-btn${teamScope === 'project' ? ' active' : ''}" onclick="teamScope='project';delete loaded['team'];loadTeam();" style="padding:6px 14px;font-size:12px;">${t('teamProjectBtn')}</button>
|
|
3258
|
+
<button class="filter-btn${teamScope === 'global' ? ' active' : ''}" onclick="teamScope='global';delete loaded['team'];loadTeam();" style="padding:6px 14px;font-size:12px;">${t('teamGlobalBtn')}</button>
|
|
2608
3259
|
</div>
|
|
2609
3260
|
<span class="team-refresh-time" id="team-refresh-indicator"></span>
|
|
2610
3261
|
<button class="team-refresh-btn" onclick="loadTeam()">
|
|
2611
3262
|
<span class="iconify" data-icon="lucide:refresh-cw" style="font-size:14px;"></span>
|
|
2612
|
-
|
|
3263
|
+
${t('teamRefresh')}
|
|
2613
3264
|
</button>
|
|
2614
3265
|
</div>
|
|
2615
3266
|
</div>
|
|
2616
3267
|
|
|
3268
|
+
<!-- Resume: Continue This Project -->
|
|
3269
|
+
<div class="panel" style="margin-bottom:16px;border-left:3px solid ${hasPending ? 'var(--accent-amber)' : 'var(--accent-green)'};">
|
|
3270
|
+
<div class="panel-header">
|
|
3271
|
+
<span class="panel-title"><span class="iconify" data-icon="lucide:play-circle" style="font-size:15px;vertical-align:middle;margin-right:6px;"></span>${t('resumeTitle')}</span>
|
|
3272
|
+
<span class="team-panel-count" style="color:var(--text-muted);font-size:11px;">${t('resumeDesc')}</span>
|
|
3273
|
+
</div>
|
|
3274
|
+
<div class="panel-body" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;padding:16px;">
|
|
3275
|
+
${hasPending ? `
|
|
3276
|
+
${openTasks > 0 ? `<div style="text-align:center;padding:12px;background:var(--bg-surface);border-radius:8px;border:1px solid var(--border);">
|
|
3277
|
+
<div style="font-size:24px;font-weight:700;color:var(--accent-amber);">${openTasks}</div>
|
|
3278
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${t('resumeOpenTasks')}</div>
|
|
3279
|
+
${availableTasks > 0 ? `<div style="font-size:10px;color:var(--accent-green);margin-top:2px;">${availableTasks} ${t('resumeAvailableTasks')}</div>` : ''}
|
|
3280
|
+
</div>` : ''}
|
|
3281
|
+
${openHandoffs > 0 ? `<div style="text-align:center;padding:12px;background:var(--bg-surface);border-radius:8px;border:1px solid var(--border);">
|
|
3282
|
+
<div style="font-size:24px;font-weight:700;color:var(--accent-cyan);">${openHandoffs}</div>
|
|
3283
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${t('resumeOpenHandoffs')}</div>
|
|
3284
|
+
</div>` : ''}
|
|
3285
|
+
${totalUnread > 0 ? `<div style="text-align:center;padding:12px;background:var(--bg-surface);border-radius:8px;border:1px solid var(--border);">
|
|
3286
|
+
<div style="font-size:24px;font-weight:700;color:var(--accent-green);">${totalUnread}</div>
|
|
3287
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${t('resumeUnreadMessages')}</div>
|
|
3288
|
+
</div>` : ''}
|
|
3289
|
+
${safeLocks.length > 0 ? `<div style="text-align:center;padding:12px;background:var(--bg-surface);border-radius:8px;border:1px solid var(--border);">
|
|
3290
|
+
<div style="font-size:24px;font-weight:700;color:var(--accent-red);">${safeLocks.length}</div>
|
|
3291
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${t('resumeActiveLocks')}</div>
|
|
3292
|
+
</div>` : ''}
|
|
3293
|
+
${activeSessions > 0 ? `<div style="text-align:center;padding:12px;background:var(--bg-surface);border-radius:8px;border:1px solid var(--border);">
|
|
3294
|
+
<div style="font-size:24px;font-weight:700;color:var(--accent-cyan);">${activeSessions}</div>
|
|
3295
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${t('resumeActiveAgents')}</div>
|
|
3296
|
+
</div>` : ''}
|
|
3297
|
+
` : `
|
|
3298
|
+
<div style="grid-column:1/-1;text-align:center;padding:24px;">
|
|
3299
|
+
<div style="font-size:28px;margin-bottom:8px;"><span class="iconify" data-icon="lucide:check-circle-2" style="font-size:28px;color:var(--accent-green);"></span></div>
|
|
3300
|
+
<div style="font-size:14px;font-weight:600;color:var(--accent-green);">${t('resumeAllClear')}</div>
|
|
3301
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px;">${t('resumeAllClearDesc')}</div>
|
|
3302
|
+
</div>
|
|
3303
|
+
`}
|
|
3304
|
+
</div>
|
|
3305
|
+
</div>
|
|
3306
|
+
|
|
3307
|
+
<!-- Stats: clearly labeled counts -->
|
|
2617
3308
|
<div class="stats-grid">
|
|
2618
3309
|
<div class="stat-card" data-accent="cyan">
|
|
2619
3310
|
<div class="team-stat-icon"><span class="iconify" data-icon="lucide:bot"></span></div>
|
|
2620
3311
|
<div class="stat-label">${t('teamActiveAgents')}</div>
|
|
2621
|
-
<div class="stat-value">${
|
|
3312
|
+
<div class="stat-value">${activeCount}</div>
|
|
3313
|
+
<div class="team-stat-sub">${activeSessions} ${t('sessionsCount')} · ${historicalCount} ${t('teamHistoricalTotal')}</div>
|
|
2622
3314
|
</div>
|
|
2623
3315
|
<div class="stat-card" data-accent="amber">
|
|
2624
3316
|
<div class="team-stat-icon"><span class="iconify" data-icon="lucide:lock"></span></div>
|
|
2625
3317
|
<div class="stat-label">${t('teamLockedFiles')}</div>
|
|
2626
|
-
<div class="stat-value">${
|
|
3318
|
+
<div class="stat-value">${safeLocks.length}</div>
|
|
2627
3319
|
</div>
|
|
2628
3320
|
<div class="stat-card" data-accent="purple">
|
|
2629
3321
|
<div class="team-stat-icon"><span class="iconify" data-icon="lucide:list-checks"></span></div>
|
|
2630
3322
|
<div class="stat-label">${t('teamTasks')}</div>
|
|
2631
|
-
<div class="stat-value">${
|
|
2632
|
-
<div class="team-stat-sub">${tasksByStatus.pending}
|
|
3323
|
+
<div class="stat-value">${safeTasks.length}</div>
|
|
3324
|
+
<div class="team-stat-sub">${tasksByStatus.pending} ${t('teamPending')} · ${tasksByStatus.in_progress} ${t('teamActive')} · ${tasksByStatus.completed} ${t('teamDone')}</div>
|
|
2633
3325
|
</div>
|
|
2634
3326
|
<div class="stat-card" data-accent="green">
|
|
2635
3327
|
<div class="team-stat-icon"><span class="iconify" data-icon="lucide:mail"></span></div>
|
|
2636
|
-
<div class="stat-label"
|
|
3328
|
+
<div class="stat-label">${t('teamMessages')}</div>
|
|
2637
3329
|
<div class="stat-value">${totalUnread}</div>
|
|
2638
|
-
<div class="team-stat-sub">${totalUnread > 0 ? totalUnread + '
|
|
3330
|
+
<div class="team-stat-sub">${totalUnread > 0 ? totalUnread + ' ' + t('teamUnread') : t('teamAllRead')}</div>
|
|
2639
3331
|
</div>
|
|
2640
3332
|
</div>
|
|
2641
3333
|
|
|
3334
|
+
${safeRoles.length > 0 ? `
|
|
3335
|
+
<div class="panel" style="margin-bottom:16px;">
|
|
3336
|
+
<div class="panel-header">
|
|
3337
|
+
<span class="panel-title"><span class="iconify" data-icon="lucide:shield" style="font-size:15px;vertical-align:middle;margin-right:6px;"></span>${t('teamRoles') || 'Roles'}</span>
|
|
3338
|
+
<span class="team-panel-count">${safeRoles.length} ${t('teamDefined') || 'defined'}</span>
|
|
3339
|
+
</div>
|
|
3340
|
+
<div class="panel-body" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;padding:16px;">
|
|
3341
|
+
${safeOccupancy.map(({ role, activeAgents, vacant }) => {
|
|
3342
|
+
const agentTypes = JSON.parse(role.preferredAgentTypes || '[]');
|
|
3343
|
+
const fillPct = role.maxConcurrent > 0 ? Math.min(100, Math.round(activeAgents.length / role.maxConcurrent * 100)) : 0;
|
|
3344
|
+
const barColor = fillPct >= 100 ? 'var(--accent-red)' : fillPct > 50 ? 'var(--accent-amber)' : 'var(--accent-green)';
|
|
3345
|
+
return `
|
|
3346
|
+
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:14px;">
|
|
3347
|
+
<div style="font-weight:600;font-size:13px;margin-bottom:4px;">${escapeHtml(role.label)}</div>
|
|
3348
|
+
<div style="font-size:11px;color:var(--text-muted);margin-bottom:8px;">${role.description ? escapeHtml(role.description) : ''}</div>
|
|
3349
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
|
3350
|
+
<div style="flex:1;height:6px;background:var(--border);border-radius:3px;overflow:hidden;">
|
|
3351
|
+
<div style="width:${fillPct}%;height:100%;background:${barColor};border-radius:3px;"></div>
|
|
3352
|
+
</div>
|
|
3353
|
+
<span style="font-size:11px;color:var(--text-muted);">${activeAgents.length}/${role.maxConcurrent}</span>
|
|
3354
|
+
</div>
|
|
3355
|
+
<div style="font-size:11px;color:var(--text-secondary);">
|
|
3356
|
+
${activeAgents.length > 0 ? activeAgents.map(a => escapeHtml(a.name)).join(', ') : '<span style="color:var(--text-muted);">vacant</span>'}
|
|
3357
|
+
</div>
|
|
3358
|
+
${vacant > 0 ? '<div style="font-size:10px;color:var(--accent-green);margin-top:4px;">' + vacant + ' slot' + (vacant > 1 ? 's' : '') + ' open</div>' : ''}
|
|
3359
|
+
</div>`;
|
|
3360
|
+
}).join('')}
|
|
3361
|
+
</div>
|
|
3362
|
+
</div>` : ''}
|
|
3363
|
+
|
|
2642
3364
|
<div class="team-grid">
|
|
2643
3365
|
<div class="panel">
|
|
2644
|
-
<div class="panel-header">
|
|
3366
|
+
<div class="panel-header" style="flex-wrap:wrap;gap:8px;">
|
|
2645
3367
|
<span class="panel-title">${t('teamAgents')}</span>
|
|
2646
|
-
<
|
|
3368
|
+
<div style="display:flex;gap:2px;margin-left:auto;">
|
|
3369
|
+
<button class="filter-btn${teamTierFilter === 'active' ? ' active' : ''}" onclick="teamTierFilter='active';delete loaded['team'];loadTeam();" style="padding:4px 10px;font-size:11px;">${t('teamTierActive')} (${activeCount})</button>
|
|
3370
|
+
<button class="filter-btn${teamTierFilter === 'recent' ? ' active' : ''}" onclick="teamTierFilter='recent';delete loaded['team'];loadTeam();" style="padding:4px 10px;font-size:11px;">${t('teamTierRecent')} (${recentCount})</button>
|
|
3371
|
+
<button class="filter-btn${teamTierFilter === 'historical' ? ' active' : ''}" onclick="teamTierFilter='historical';delete loaded['team'];loadTeam();" style="padding:4px 10px;font-size:11px;">${t('teamTierHistorical')} (${historicalCount})</button>
|
|
3372
|
+
<button class="filter-btn${teamTierFilter === 'all' ? ' active' : ''}" onclick="teamTierFilter='all';delete loaded['team'];loadTeam();" style="padding:4px 10px;font-size:11px;">${t('teamTierAll')} (${totalAgents})</button>
|
|
3373
|
+
</div>
|
|
2647
3374
|
</div>
|
|
2648
3375
|
<div class="panel-body team-scrollable">
|
|
2649
|
-
${
|
|
2650
|
-
? '<div class="team-empty"><span class="team-empty-icon"><span class="iconify" data-icon="lucide:user-x"></span></span><span class="team-empty-text">
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
3376
|
+
${filteredAgents.length === 0
|
|
3377
|
+
? '<div class="team-empty"><span class="team-empty-icon"><span class="iconify" data-icon="lucide:user-x"></span></span><span class="team-empty-text">' +
|
|
3378
|
+
(teamTierFilter === 'active' ? t('teamNoActiveNow') :
|
|
3379
|
+
teamTierFilter === 'recent' ? t('teamNoRecent') :
|
|
3380
|
+
teamScope === 'project' ? t('teamNoAgentsProject') : t('teamNoAgentsGlobal')) + '</span>' +
|
|
3381
|
+
(teamTierFilter === 'active' && historicalCount > 0 ? '<div style="font-size:11px;color:var(--text-muted);margin-top:6px;">' + historicalCount + ' ' + t('teamHistoricalHint') + '</div>' : '') +
|
|
3382
|
+
'</div>'
|
|
3383
|
+
: filteredAgents.map(a => {
|
|
3384
|
+
const tier = a.activityTier;
|
|
3385
|
+
const isHistorical = tier === 'historical';
|
|
3386
|
+
const tierColor = tier === 'active' ? 'var(--accent-green)' : tier === 'recent' ? 'var(--accent-amber)' : 'var(--text-muted)';
|
|
3387
|
+
const tierBg = tier === 'active' ? 'rgba(105,240,174,0.12)' : tier === 'recent' ? 'rgba(255,171,64,0.12)' : 'rgba(148,163,184,0.08)';
|
|
3388
|
+
const tierLabel = tier === 'active' ? t('teamTierActive') : tier === 'recent' ? t('teamTierRecent') : t('teamTierHistorical');
|
|
3389
|
+
return `
|
|
3390
|
+
<div class="team-agent-row${tier !== 'active' ? ' inactive' : ''}"${isHistorical ? ' style="opacity:0.55;"' : ''}>
|
|
3391
|
+
<div class="team-agent-status ${tier === 'active' ? 'active' : 'offline'}"></div>
|
|
2654
3392
|
<div class="team-agent-info">
|
|
2655
|
-
<div class="team-agent-name">${escapeHtml(a.name)} <span style="font-size:9px;padding:1px 5px;border-radius:4px;font-weight:500;margin-left:4px
|
|
3393
|
+
<div class="team-agent-name">${escapeHtml(a.name)} <span style="font-size:9px;padding:1px 5px;border-radius:4px;font-weight:500;margin-left:4px;background:${tierBg};color:${tierColor};">${tierLabel}</span></div>
|
|
2656
3394
|
<div class="team-agent-meta">
|
|
2657
|
-
<span>${a.role ? escapeHtml(a.role) : '
|
|
3395
|
+
<span>${a.role ? escapeHtml(a.role) : t('teamNoRole')}</span>
|
|
2658
3396
|
${a.capabilities && a.capabilities.length ? a.capabilities.map(c => '<span class="team-cap-tag">' + escapeHtml(c) + '</span>').join('') : ''}
|
|
2659
3397
|
</div>
|
|
2660
|
-
<div class="team-agent-time"
|
|
3398
|
+
<div class="team-agent-time">${t('teamJoined')} ${teamTimeAgo(a.joinedAt)} · ${t('teamSeen')} ${teamTimeAgo(a.lastSeenAt)}${a.leftAt ? ' · ' + t('teamLeft') + ' ' + teamTimeAgo(a.leftAt) : ''}</div>
|
|
2661
3399
|
</div>
|
|
2662
3400
|
${a.unread > 0 ? '<span class="team-unread-badge">' + a.unread + '</span>' : ''}
|
|
2663
|
-
<span class="team-agent-id">${a.id.slice(0, 8)}</span>
|
|
2664
|
-
</div
|
|
2665
|
-
|
|
3401
|
+
<span class="team-agent-id">${(a.id || '').slice(0, 8)}</span>
|
|
3402
|
+
</div>`;
|
|
3403
|
+
}).join('')
|
|
2666
3404
|
}
|
|
3405
|
+
${teamTierFilter === 'active' && !teamShowHistorical && historicalCount > 0 ? `
|
|
3406
|
+
<div style="padding:12px;text-align:center;border-top:1px dashed var(--border);margin-top:8px;opacity:0.6;">
|
|
3407
|
+
<button class="filter-btn" onclick="teamShowHistorical=true;delete loaded['team'];loadTeam();" style="font-size:11px;color:var(--text-muted);">
|
|
3408
|
+
<span class="iconify" data-icon="lucide:chevron-down" style="font-size:12px;vertical-align:middle;"></span>
|
|
3409
|
+
${t('teamShowHistorical')} (${historicalCount})
|
|
3410
|
+
</button>
|
|
3411
|
+
</div>
|
|
3412
|
+
` : ''}
|
|
2667
3413
|
</div>
|
|
2668
3414
|
</div>
|
|
2669
3415
|
|
|
2670
3416
|
<div class="panel">
|
|
2671
3417
|
<div class="panel-header">
|
|
2672
3418
|
<span class="panel-title">${t('teamLocks')}</span>
|
|
2673
|
-
<span class="team-panel-count">${
|
|
3419
|
+
<span class="team-panel-count">${safeLocks.length} ${t('teamActiveCount')}</span>
|
|
2674
3420
|
</div>
|
|
2675
3421
|
<div class="panel-body team-scrollable">
|
|
2676
|
-
${
|
|
2677
|
-
? '<div class="team-empty"><span class="team-empty-icon"><span class="iconify" data-icon="lucide:lock-open"></span></span><span class="team-empty-text">
|
|
2678
|
-
:
|
|
2679
|
-
const owner =
|
|
3422
|
+
${safeLocks.length === 0
|
|
3423
|
+
? '<div class="team-empty"><span class="team-empty-icon"><span class="iconify" data-icon="lucide:lock-open"></span></span><span class="team-empty-text">' + t('teamNoFilesLocked') + '</span></div>'
|
|
3424
|
+
: safeLocks.map(l => {
|
|
3425
|
+
const owner = safeAgents.find(a => a.id === l.lockedBy);
|
|
2680
3426
|
const ttl = teamLockTTL(l.expiresAt);
|
|
2681
3427
|
return '<div class="team-lock-row">' +
|
|
2682
3428
|
'<div class="team-lock-icon"><span class="iconify" data-icon="lucide:file-lock-2"></span></div>' +
|
|
2683
3429
|
'<div class="team-lock-info">' +
|
|
2684
3430
|
'<div class="team-lock-file">' + escapeHtml(l.file) + '</div>' +
|
|
2685
3431
|
'<div class="team-lock-meta">' +
|
|
2686
|
-
'<span>' + (owner ? escapeHtml(owner.name) : l.lockedBy.slice(0, 8)) + '</span>' +
|
|
3432
|
+
'<span>' + (owner ? escapeHtml(owner.name) : (l.lockedBy || '').slice(0, 8)) + '</span>' +
|
|
2687
3433
|
'<span>' + teamTimeAgo(l.lockedAt) + '</span>' +
|
|
2688
3434
|
(ttl ? '<span class="team-lock-ttl">' + ttl + '</span>' : '') +
|
|
2689
3435
|
'</div>' +
|
|
@@ -2698,20 +3444,22 @@ async function loadTeam() {
|
|
|
2698
3444
|
<div class="panel">
|
|
2699
3445
|
<div class="panel-header">
|
|
2700
3446
|
<span class="panel-title">${t('teamTaskBoard')}</span>
|
|
2701
|
-
<span class="team-panel-count">${data.availableTasks
|
|
3447
|
+
<span class="team-panel-count">${data.availableTasks || 0} ${t('teamAvailableToClaim')}</span>
|
|
2702
3448
|
</div>
|
|
2703
3449
|
<div class="panel-body">
|
|
2704
|
-
${
|
|
2705
|
-
? '<div class="team-empty"><span class="team-empty-icon"><span class="iconify" data-icon="lucide:clipboard-list"></span></span><span class="team-empty-text">
|
|
2706
|
-
: '<table class="team-task-table"><thead><tr><th>Status</th><th>ID</th><th>Description</th><th>Assignee</th><th>Deps</th><th>Updated</th></tr></thead><tbody>' +
|
|
2707
|
-
|
|
2708
|
-
const assignee = tk.assignee ? (
|
|
3450
|
+
${safeTasks.length === 0
|
|
3451
|
+
? '<div class="team-empty"><span class="team-empty-icon"><span class="iconify" data-icon="lucide:clipboard-list"></span></span><span class="team-empty-text">' + t('teamNoTasksCreated') + '</span></div>'
|
|
3452
|
+
: '<table class="team-task-table"><thead><tr><th>Status</th><th>ID</th><th>Description</th><th>Assignee</th><th>Role</th><th>Deps</th><th>Updated</th></tr></thead><tbody>' +
|
|
3453
|
+
safeTasks.map(tk => {
|
|
3454
|
+
const assignee = tk.assignee ? (safeAgents.find(a => a.id === tk.assignee)?.name || (tk.assignee || '').slice(0, 8)) : '<span style="color:var(--text-muted);">—</span>';
|
|
3455
|
+
const roleTag = tk.required_role ? '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(139,92,246,0.15);color:#a78bfa;font-weight:500;">' + escapeHtml(tk.required_role) + '</span>' : (tk.preferred_role ? '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(56,189,248,0.12);color:#38bdf8;font-weight:500;">' + escapeHtml(tk.preferred_role) + '</span>' : '—');
|
|
2709
3456
|
return '<tr>' +
|
|
2710
3457
|
'<td><span class="team-task-status" data-status="' + tk.status + '"><span class="iconify" data-icon="' + (statusIcons[tk.status] || 'lucide:circle') + '" style="font-size:13px;"></span> ' + (statusLabels[tk.status] || tk.status) + '</span></td>' +
|
|
2711
|
-
'<td><span class="team-task-id">' + tk.id.slice(0, 8) + '</span></td>' +
|
|
3458
|
+
'<td><span class="team-task-id">' + (tk.id || '').slice(0, 8) + '</span></td>' +
|
|
2712
3459
|
'<td>' + escapeHtml(tk.description) + (tk.result ? '<div class="team-task-result"><span class="iconify" data-icon="lucide:corner-down-right" style="font-size:11px;"></span> ' + escapeHtml(tk.result.slice(0, 80)) + '</div>' : '') + '</td>' +
|
|
2713
3460
|
'<td style="font-size:12px;">' + assignee + '</td>' +
|
|
2714
|
-
'<td
|
|
3461
|
+
'<td>' + roleTag + '</td>' +
|
|
3462
|
+
'<td style="text-align:center;color:var(--text-muted);">' + ((tk.deps || []).length > 0 ? (tk.deps || []).length : '—') + '</td>' +
|
|
2715
3463
|
'<td style="font-size:11px;color:var(--text-muted);">' + teamTimeAgo(tk.updatedAt) + '</td>' +
|
|
2716
3464
|
'</tr>';
|
|
2717
3465
|
}).join('') +
|
|
@@ -2719,6 +3467,30 @@ async function loadTeam() {
|
|
|
2719
3467
|
}
|
|
2720
3468
|
</div>
|
|
2721
3469
|
</div>
|
|
3470
|
+
|
|
3471
|
+
${safeHandoffs.length > 0 ? `
|
|
3472
|
+
<div class="panel" style="margin-top:16px;">
|
|
3473
|
+
<div class="panel-header">
|
|
3474
|
+
<span class="panel-title"><span class="iconify" data-icon="lucide:arrow-right-left" style="font-size:15px;vertical-align:middle;margin-right:6px;"></span>${t('teamHandoffs') || 'Handoffs'}</span>
|
|
3475
|
+
<span class="team-panel-count">${safeHandoffs.filter(h => h.handoff_status === 'open').length} ${t('teamOpen') || 'open'}</span>
|
|
3476
|
+
</div>
|
|
3477
|
+
<div class="panel-body team-scrollable">
|
|
3478
|
+
${safeHandoffs.map(h => {
|
|
3479
|
+
const sender = safeAgents.find(a => a.id === h.sender_agent_id);
|
|
3480
|
+
const statusColor = h.handoff_status === 'open' ? 'var(--accent-amber)' : h.handoff_status === 'claimed' ? 'var(--accent-cyan)' : h.handoff_status === 'completed' ? 'var(--accent-green)' : 'var(--text-muted)';
|
|
3481
|
+
return '<div class="team-lock-row">' +
|
|
3482
|
+
'<div style="display:flex;align-items:center;gap:6px;">' +
|
|
3483
|
+
'<span style="font-size:10px;padding:2px 8px;border-radius:4px;background:' + statusColor + '22;color:' + statusColor + ';font-weight:500;text-transform:uppercase;">' + escapeHtml(h.handoff_status) + '</span>' +
|
|
3484
|
+
'<span style="font-size:12px;font-weight:500;">' + (sender ? escapeHtml(sender.name) : (h.sender_agent_id || '').slice(0, 8)) + '</span>' +
|
|
3485
|
+
'<span style="font-size:11px;color:var(--text-muted);">→</span>' +
|
|
3486
|
+
'<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(139,92,246,0.15);color:#a78bfa;font-weight:500;">' + escapeHtml(h.to_role) + '</span>' +
|
|
3487
|
+
'</div>' +
|
|
3488
|
+
'<div style="font-size:12px;color:var(--text-secondary);margin-top:4px;">' + escapeHtml((h.content || '').slice(0, 120)) + '</div>' +
|
|
3489
|
+
'<div style="font-size:10px;color:var(--text-muted);margin-top:2px;">' + teamTimeAgo(h.created_at) + '</div>' +
|
|
3490
|
+
'</div>';
|
|
3491
|
+
}).join('')}
|
|
3492
|
+
</div>
|
|
3493
|
+
</div>` : ''}
|
|
2722
3494
|
`;
|
|
2723
3495
|
|
|
2724
3496
|
container.innerHTML = html;
|