memorix 1.0.3 → 1.0.4
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 +30 -0
- package/README.md +119 -237
- package/README.zh-CN.md +119 -239
- package/dist/cli/index.js +12546 -8801
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +554 -43
- package/dist/dashboard/static/index.html +39 -8
- package/dist/dashboard/static/style.css +2403 -2064
- package/dist/index.js +3697 -2172
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
|
@@ -96,7 +96,8 @@ const i18n = {
|
|
|
96
96
|
// Team
|
|
97
97
|
teamTitle: 'Team',
|
|
98
98
|
teamSubtitle: 'Multi-agent collaboration overview',
|
|
99
|
-
teamNoData: 'Team features
|
|
99
|
+
teamNoData: 'Team features require HTTP transport',
|
|
100
|
+
teamNoDataHint: 'Team collaboration (agents, file locks, tasks) requires the HTTP transport. Start it with:',
|
|
100
101
|
teamActiveAgents: 'Active Agents',
|
|
101
102
|
teamLockedFiles: 'Locked Files',
|
|
102
103
|
teamTasks: 'Tasks',
|
|
@@ -105,11 +106,81 @@ const i18n = {
|
|
|
105
106
|
teamLocks: 'File Locks',
|
|
106
107
|
teamTaskBoard: 'Task Board',
|
|
107
108
|
|
|
109
|
+
// Overview (new)
|
|
110
|
+
memoryControlPlane: 'Memory Control Plane',
|
|
111
|
+
memoriesAcross: 'memories across',
|
|
112
|
+
entitiesUnit: 'entities',
|
|
113
|
+
gitMemories: 'Git Memories',
|
|
114
|
+
agentMemories: 'Agent Memories',
|
|
115
|
+
thisWeek: 'this week',
|
|
116
|
+
hooksAndMcp: 'hooks + MCP',
|
|
117
|
+
memorySources: 'Memory Sources',
|
|
118
|
+
retentionHealth: 'Retention Health',
|
|
119
|
+
sourceGit: 'Git',
|
|
120
|
+
sourceAgent: 'Agent',
|
|
121
|
+
sourceManual: 'Manual',
|
|
122
|
+
|
|
123
|
+
// Git Memory
|
|
124
|
+
gitMemoryTitle: 'Git Memory',
|
|
125
|
+
gitMemorySubtitle: 'memories from git commits — ground truth, immutable',
|
|
126
|
+
totalGitMemories: 'Total Git Memories',
|
|
127
|
+
uniqueCommits: 'Unique Commits',
|
|
128
|
+
typeCoverage: 'Type Coverage',
|
|
129
|
+
noGitMemory: 'No Git Memory',
|
|
130
|
+
noGitMemoryDesc: 'Install the post-commit hook with: memorix git-hook-install',
|
|
131
|
+
noGitMemoriesYet: 'No Git Memories Yet',
|
|
132
|
+
noGitMemoriesHint: 'Install the post-commit hook to automatically capture git memories:',
|
|
133
|
+
recentGitMemories: 'Recent Git Memories',
|
|
134
|
+
commit: 'Commit',
|
|
135
|
+
created: 'Created',
|
|
136
|
+
|
|
137
|
+
// Config
|
|
138
|
+
configTitle: 'Config Provenance',
|
|
139
|
+
configSubtitle: 'Where every configuration value comes from — two files, two roles',
|
|
140
|
+
configSourceMatrix: 'Config Source Matrix',
|
|
141
|
+
configHint: '= behavior config',
|
|
142
|
+
configHintEnv: '= secrets only',
|
|
143
|
+
valueProvenance: 'Value Provenance',
|
|
144
|
+
trackedValues: 'tracked values',
|
|
145
|
+
configKey: 'Key',
|
|
146
|
+
configValue: 'Value',
|
|
147
|
+
configSource: 'Source',
|
|
148
|
+
configStatus: 'Status',
|
|
149
|
+
moveToEnv: 'Move to .env',
|
|
150
|
+
configUnavailable: 'Config Unavailable',
|
|
151
|
+
configUnavailableDesc: 'Could not load configuration data',
|
|
152
|
+
|
|
153
|
+
// Identity
|
|
154
|
+
identityTitle: 'Project Identity Health',
|
|
155
|
+
identitySubtitle: 'Project ID stability, aliases, and cross-agent consistency',
|
|
156
|
+
healthStatus: 'Health Status',
|
|
157
|
+
healthy: '✓ Healthy',
|
|
158
|
+
unhealthy: '⚠ Issues',
|
|
159
|
+
knownProjectIds: 'Known Project IDs',
|
|
160
|
+
aliasGroups: 'Alias Groups',
|
|
161
|
+
dirtyIds: 'Dirty IDs',
|
|
162
|
+
currentIdentity: 'Current Identity',
|
|
163
|
+
currentProjectId: 'Current Project ID',
|
|
164
|
+
canonicalId: 'Canonical ID',
|
|
165
|
+
aliases: 'Aliases',
|
|
166
|
+
healthIssues: 'Health Issues',
|
|
167
|
+
noIssues: 'No issues detected. Project identity is clean.',
|
|
168
|
+
dirtyProjectIds: 'Dirty Project IDs',
|
|
169
|
+
allKnownProjectIds: 'All Known Project IDs',
|
|
170
|
+
tagCurrent: 'current',
|
|
171
|
+
tagCanonical: 'canonical',
|
|
172
|
+
tagDirty: 'dirty',
|
|
173
|
+
identityUnavailable: 'Identity Unavailable',
|
|
174
|
+
identityUnavailableDesc: 'Could not load project identity data',
|
|
175
|
+
|
|
108
176
|
// Nav tooltips
|
|
109
177
|
navDashboard: 'Dashboard',
|
|
178
|
+
navGitMemory: 'Git Memory',
|
|
110
179
|
navGraph: 'Knowledge Graph',
|
|
111
180
|
navObservations: 'Observations',
|
|
112
181
|
navRetention: 'Retention',
|
|
182
|
+
navConfig: 'Config',
|
|
183
|
+
navIdentity: 'Identity',
|
|
113
184
|
navSessions: 'Sessions',
|
|
114
185
|
navTeam: 'Team',
|
|
115
186
|
},
|
|
@@ -201,7 +272,8 @@ const i18n = {
|
|
|
201
272
|
// Team
|
|
202
273
|
teamTitle: '团队',
|
|
203
274
|
teamSubtitle: '多 Agent 协作概览',
|
|
204
|
-
teamNoData: '
|
|
275
|
+
teamNoData: '团队功能需要 HTTP 传输',
|
|
276
|
+
teamNoDataHint: '团队协作(Agent 注册、文件锁、任务看板)需要 HTTP 传输模式。使用以下命令启动:',
|
|
205
277
|
teamActiveAgents: '活跃 Agent',
|
|
206
278
|
teamLockedFiles: '锁定文件',
|
|
207
279
|
teamTasks: '任务',
|
|
@@ -210,11 +282,81 @@ const i18n = {
|
|
|
210
282
|
teamLocks: '文件锁',
|
|
211
283
|
teamTaskBoard: '任务看板',
|
|
212
284
|
|
|
285
|
+
// Overview (new)
|
|
286
|
+
memoryControlPlane: '记忆控制台',
|
|
287
|
+
memoriesAcross: '条记忆,分布于',
|
|
288
|
+
entitiesUnit: '个实体',
|
|
289
|
+
gitMemories: 'Git 记忆',
|
|
290
|
+
agentMemories: 'Agent 记忆',
|
|
291
|
+
thisWeek: '本周新增',
|
|
292
|
+
hooksAndMcp: 'hooks + MCP',
|
|
293
|
+
memorySources: '记忆来源',
|
|
294
|
+
retentionHealth: '衰减健康度',
|
|
295
|
+
sourceGit: 'Git',
|
|
296
|
+
sourceAgent: 'Agent',
|
|
297
|
+
sourceManual: '手动',
|
|
298
|
+
|
|
299
|
+
// Git Memory
|
|
300
|
+
gitMemoryTitle: 'Git 记忆',
|
|
301
|
+
gitMemorySubtitle: '来自 git 提交的记忆 — 不可变的事实源',
|
|
302
|
+
totalGitMemories: 'Git 记忆总数',
|
|
303
|
+
uniqueCommits: '唯一提交数',
|
|
304
|
+
typeCoverage: '类型覆盖',
|
|
305
|
+
noGitMemory: '暂无 Git 记忆',
|
|
306
|
+
noGitMemoryDesc: '使用以下命令安装 post-commit hook: memorix git-hook-install',
|
|
307
|
+
noGitMemoriesYet: '暂无 Git 记忆',
|
|
308
|
+
noGitMemoriesHint: '安装 post-commit hook 以自动捕获 git 记忆:',
|
|
309
|
+
recentGitMemories: '最近的 Git 记忆',
|
|
310
|
+
commit: '提交',
|
|
311
|
+
created: '创建时间',
|
|
312
|
+
|
|
313
|
+
// Config
|
|
314
|
+
configTitle: '配置溯源',
|
|
315
|
+
configSubtitle: '每个配置值的来源 — 两个文件,两种角色',
|
|
316
|
+
configSourceMatrix: '配置源矩阵',
|
|
317
|
+
configHint: '= 行为配置',
|
|
318
|
+
configHintEnv: '= 仅存放密钥',
|
|
319
|
+
valueProvenance: '值的溯源',
|
|
320
|
+
trackedValues: '个追踪值',
|
|
321
|
+
configKey: '键',
|
|
322
|
+
configValue: '值',
|
|
323
|
+
configSource: '来源',
|
|
324
|
+
configStatus: '状态',
|
|
325
|
+
moveToEnv: '应移至 .env',
|
|
326
|
+
configUnavailable: '配置不可用',
|
|
327
|
+
configUnavailableDesc: '无法加载配置数据',
|
|
328
|
+
|
|
329
|
+
// Identity
|
|
330
|
+
identityTitle: '项目身份健康度',
|
|
331
|
+
identitySubtitle: '项目 ID 稳定性、别名和跨 Agent 一致性',
|
|
332
|
+
healthStatus: '健康状态',
|
|
333
|
+
healthy: '✓ 健康',
|
|
334
|
+
unhealthy: '⚠ 存在问题',
|
|
335
|
+
knownProjectIds: '已知项目 ID',
|
|
336
|
+
aliasGroups: '别名组',
|
|
337
|
+
dirtyIds: '脏 ID',
|
|
338
|
+
currentIdentity: '当前身份',
|
|
339
|
+
currentProjectId: '当前项目 ID',
|
|
340
|
+
canonicalId: '标准 ID',
|
|
341
|
+
aliases: '别名',
|
|
342
|
+
healthIssues: '健康问题',
|
|
343
|
+
noIssues: '未检测到问题。项目身份状态良好。',
|
|
344
|
+
dirtyProjectIds: '脏项目 ID',
|
|
345
|
+
allKnownProjectIds: '所有已知项目 ID',
|
|
346
|
+
tagCurrent: '当前',
|
|
347
|
+
tagCanonical: '标准',
|
|
348
|
+
tagDirty: '脏',
|
|
349
|
+
identityUnavailable: '身份信息不可用',
|
|
350
|
+
identityUnavailableDesc: '无法加载项目身份数据',
|
|
351
|
+
|
|
213
352
|
// Nav tooltips
|
|
214
353
|
navDashboard: '仪表盘',
|
|
354
|
+
navGitMemory: 'Git 记忆',
|
|
215
355
|
navGraph: '知识图谱',
|
|
216
356
|
navObservations: '观察记录',
|
|
217
357
|
navRetention: '记忆衰减',
|
|
358
|
+
navConfig: '配置溯源',
|
|
359
|
+
navIdentity: '身份健康',
|
|
218
360
|
navSessions: '会话',
|
|
219
361
|
navTeam: '团队',
|
|
220
362
|
},
|
|
@@ -303,7 +445,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
303
445
|
// Router & Navigation
|
|
304
446
|
// ============================================================
|
|
305
447
|
|
|
306
|
-
const pages = ['dashboard', 'graph', 'observations', 'retention', 'sessions', 'team'];
|
|
448
|
+
const pages = ['dashboard', 'git-memory', 'graph', 'observations', 'retention', 'config', 'identity', 'sessions', 'team'];
|
|
307
449
|
let currentPage = 'dashboard';
|
|
308
450
|
|
|
309
451
|
function navigate(page) {
|
|
@@ -485,9 +627,12 @@ async function loadPage(page) {
|
|
|
485
627
|
|
|
486
628
|
switch (page) {
|
|
487
629
|
case 'dashboard': await loadDashboard(); break;
|
|
630
|
+
case 'git-memory': await loadGitMemory(); break;
|
|
488
631
|
case 'graph': await loadGraph(); break;
|
|
489
632
|
case 'observations': await loadObservations(); break;
|
|
490
633
|
case 'retention': await loadRetention(); break;
|
|
634
|
+
case 'config': await loadConfig(); break;
|
|
635
|
+
case 'identity': await loadIdentity(); break;
|
|
491
636
|
case 'sessions': await loadSessions(); break;
|
|
492
637
|
case 'team': await loadTeam(); break;
|
|
493
638
|
}
|
|
@@ -509,6 +654,10 @@ async function loadDashboard() {
|
|
|
509
654
|
}
|
|
510
655
|
|
|
511
656
|
const projectLabel = project ? project.name : '';
|
|
657
|
+
const sc = stats.sourceCounts || { git: 0, agent: 0, manual: 0 };
|
|
658
|
+
const totalObs = stats.observations || 0;
|
|
659
|
+
const gs = stats.gitSummary || { total: 0, recentWeek: 0, recentMemories: [] };
|
|
660
|
+
const rs = stats.retentionSummary || { active: 0, stale: 0, archive: 0, immune: 0 };
|
|
512
661
|
|
|
513
662
|
const typeIcons = {
|
|
514
663
|
'session-request': '🎯', gotcha: '🔴', 'problem-solution': '🟡',
|
|
@@ -516,45 +665,81 @@ async function loadDashboard() {
|
|
|
516
665
|
'why-it-exists': '🟠', decision: '🟤', 'trade-off': '⚖️',
|
|
517
666
|
};
|
|
518
667
|
|
|
519
|
-
// Type distribution
|
|
520
668
|
const typeEntries = Object.entries(stats.typeCounts || {}).sort((a, b) => b[1] - a[1]);
|
|
521
669
|
const maxTypeCount = Math.max(...typeEntries.map(e => e[1]), 1);
|
|
522
670
|
|
|
671
|
+
// Source bar percentages
|
|
672
|
+
const srcTotal = Math.max(sc.git + sc.agent + sc.manual, 1);
|
|
673
|
+
const gitPct = Math.round(sc.git / srcTotal * 100);
|
|
674
|
+
const agentPct = Math.round(sc.agent / srcTotal * 100);
|
|
675
|
+
const manualPct = 100 - gitPct - agentPct;
|
|
676
|
+
|
|
523
677
|
container.innerHTML = `
|
|
524
678
|
<div class="page-header">
|
|
525
|
-
<h1 class="page-title">${t('
|
|
526
|
-
<p class="page-subtitle">${t('
|
|
679
|
+
<h1 class="page-title">${t('memoryControlPlane')} ${projectLabel ? `<span class="overview-project-badge">${escapeHtml(projectLabel)}</span>` : ''}</h1>
|
|
680
|
+
<p class="page-subtitle">${totalObs} ${t('memoriesAcross')} ${stats.entities} ${t('entitiesUnit')}</p>
|
|
527
681
|
</div>
|
|
528
682
|
|
|
529
683
|
<div class="stats-grid">
|
|
530
|
-
<div class="stat-card" data-accent="
|
|
531
|
-
<div class="stat-label">${t('
|
|
532
|
-
<div class="stat-value">${
|
|
684
|
+
<div class="stat-card" data-accent="green">
|
|
685
|
+
<div class="stat-label">${t('gitMemories')}</div>
|
|
686
|
+
<div class="stat-value">${sc.git}</div>
|
|
687
|
+
<div class="stat-sub">${gs.recentWeek} ${t('thisWeek')}</div>
|
|
533
688
|
</div>
|
|
534
689
|
<div class="stat-card" data-accent="purple">
|
|
535
|
-
<div class="stat-label">${t('
|
|
536
|
-
<div class="stat-value">${
|
|
537
|
-
|
|
538
|
-
<div class="stat-card" data-accent="amber">
|
|
539
|
-
<div class="stat-label">${t('observations')}</div>
|
|
540
|
-
<div class="stat-value">${stats.observations}</div>
|
|
690
|
+
<div class="stat-label">${t('agentMemories')}</div>
|
|
691
|
+
<div class="stat-value">${sc.agent}</div>
|
|
692
|
+
<div class="stat-sub">${t('hooksAndMcp')}</div>
|
|
541
693
|
</div>
|
|
542
|
-
<div class="stat-card" data-accent="
|
|
543
|
-
<div class="stat-label">${t('
|
|
544
|
-
<div class="stat-value"
|
|
694
|
+
<div class="stat-card" data-accent="cyan">
|
|
695
|
+
<div class="stat-label">${t('entities')}</div>
|
|
696
|
+
<div class="stat-value">${stats.entities}</div>
|
|
697
|
+
<div class="stat-sub">${stats.relations} ${t('relations')}</div>
|
|
545
698
|
</div>
|
|
546
|
-
<div class="stat-card" data-accent="${stats.embedding?.enabled ? '
|
|
699
|
+
<div class="stat-card" data-accent="${stats.embedding?.enabled ? 'blue' : 'amber'}">
|
|
547
700
|
<div class="stat-label">${t('vectorSearch')}</div>
|
|
548
701
|
<div class="stat-value" style="font-size: 18px;">${stats.embedding?.enabled ? '✓ ' + t('enabled') : t('fulltextOnly')}</div>
|
|
549
|
-
${stats.embedding?.provider ? `<div
|
|
702
|
+
${stats.embedding?.provider ? `<div class="stat-sub">${stats.embedding.provider} (${stats.embedding.dimensions}d)</div>` : ''}
|
|
550
703
|
</div>
|
|
551
704
|
</div>
|
|
552
705
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
706
|
+
<!-- Source Breakdown -->
|
|
707
|
+
<div class="overview-row">
|
|
708
|
+
<div class="panel" style="flex:1;">
|
|
709
|
+
<div class="panel-header"><span class="panel-title">${t('memorySources')}</span></div>
|
|
710
|
+
<div class="panel-body">
|
|
711
|
+
<div class="source-bar-container">
|
|
712
|
+
<div class="source-bar">
|
|
713
|
+
${gitPct > 0 ? `<div class="source-bar-seg" style="width:${gitPct}%;background:var(--accent-green);" title="Git ${gitPct}%"></div>` : ''}
|
|
714
|
+
${agentPct > 0 ? `<div class="source-bar-seg" style="width:${agentPct}%;background:var(--accent-purple);" title="Agent ${agentPct}%"></div>` : ''}
|
|
715
|
+
${manualPct > 0 ? `<div class="source-bar-seg" style="width:${manualPct}%;background:var(--accent-amber);" title="Manual ${manualPct}%"></div>` : ''}
|
|
716
|
+
</div>
|
|
717
|
+
<div class="source-legend">
|
|
718
|
+
<span class="source-legend-item"><span class="source-dot" style="background:var(--accent-green)"></span> ${t('sourceGit')} <strong>${sc.git}</strong></span>
|
|
719
|
+
<span class="source-legend-item"><span class="source-dot" style="background:var(--accent-purple)"></span> ${t('sourceAgent')} <strong>${sc.agent}</strong></span>
|
|
720
|
+
<span class="source-legend-item"><span class="source-dot" style="background:var(--accent-amber)"></span> ${t('sourceManual')} <strong>${sc.manual}</strong></span>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
557
723
|
</div>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
<div class="panel" style="flex:1;">
|
|
727
|
+
<div class="panel-header"><span class="panel-title">${t('retentionHealth')}</span></div>
|
|
728
|
+
<div class="panel-body">
|
|
729
|
+
<div class="retention-mini-grid">
|
|
730
|
+
<div class="retention-mini-item"><span class="retention-mini-value" style="color:var(--accent-green)">${rs.active}</span><span class="retention-mini-label">${t('active')}</span></div>
|
|
731
|
+
<div class="retention-mini-item"><span class="retention-mini-value" style="color:var(--accent-amber)">${rs.stale}</span><span class="retention-mini-label">${t('stale')}</span></div>
|
|
732
|
+
<div class="retention-mini-item"><span class="retention-mini-value" style="color:var(--accent-red)">${rs.archive}</span><span class="retention-mini-label">${t('archiveCandidates')}</span></div>
|
|
733
|
+
<div class="retention-mini-item"><span class="retention-mini-value" style="color:var(--accent-purple)">${rs.immune}</span><span class="retention-mini-label">${t('immune')}</span></div>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<!-- Type Distribution + Recent Activity -->
|
|
740
|
+
<div class="overview-row">
|
|
741
|
+
<div class="panel" style="flex:1;">
|
|
742
|
+
<div class="panel-header"><span class="panel-title">${t('observationTypes')}</span></div>
|
|
558
743
|
<div class="panel-body">
|
|
559
744
|
${typeEntries.length > 0 ? `
|
|
560
745
|
<div style="display: flex; gap: 20px; align-items: flex-start;">
|
|
@@ -576,10 +761,8 @@ async function loadDashboard() {
|
|
|
576
761
|
</div>
|
|
577
762
|
</div>
|
|
578
763
|
|
|
579
|
-
<div class="panel">
|
|
580
|
-
<div class="panel-header">
|
|
581
|
-
<span class="panel-title">${t('recentActivity')}</span>
|
|
582
|
-
</div>
|
|
764
|
+
<div class="panel" style="flex:1;">
|
|
765
|
+
<div class="panel-header"><span class="panel-title">${t('recentActivity')}</span></div>
|
|
583
766
|
<div class="panel-body">
|
|
584
767
|
<ul class="activity-list">
|
|
585
768
|
${(stats.recentObservations || []).map(obs => `
|
|
@@ -600,7 +783,6 @@ async function loadDashboard() {
|
|
|
600
783
|
</div>
|
|
601
784
|
`;
|
|
602
785
|
|
|
603
|
-
// Render pie chart if data exists
|
|
604
786
|
if (typeEntries.length > 0) {
|
|
605
787
|
requestAnimationFrame(() => renderPieChart('type-pie-chart', typeEntries, typeIcons));
|
|
606
788
|
}
|
|
@@ -807,6 +989,10 @@ function renderGraph(graph) {
|
|
|
807
989
|
let dragNode = null;
|
|
808
990
|
let panStart = null;
|
|
809
991
|
let simTick = 0;
|
|
992
|
+
let isSettled = false;
|
|
993
|
+
let stableMode = nodes.length > 15; // Auto-stable for larger graphs
|
|
994
|
+
const SETTLE_THRESHOLD = 0.15;
|
|
995
|
+
let settleCountdown = 0; // frames below threshold before declaring settled
|
|
810
996
|
|
|
811
997
|
// Group nodes by color → separate galaxies
|
|
812
998
|
const colorGroups = {};
|
|
@@ -855,13 +1041,17 @@ function renderGraph(graph) {
|
|
|
855
1041
|
}
|
|
856
1042
|
}
|
|
857
1043
|
|
|
858
|
-
// ---
|
|
1044
|
+
// --- Warmup factor (ramp all forces gradually to prevent explosive start) ---
|
|
1045
|
+
const warmup = Math.min(1, simTick / 100);
|
|
1046
|
+
|
|
1047
|
+
// --- Node-to-node repulsion (also ramped) ---
|
|
1048
|
+
const curRepulsion = REPULSION * (0.1 + 0.9 * warmup);
|
|
859
1049
|
for (let i = 0; i < nodes.length; i++) {
|
|
860
1050
|
for (let j = i + 1; j < nodes.length; j++) {
|
|
861
1051
|
const a = nodes[i], b = nodes[j];
|
|
862
1052
|
let dx = b.x - a.x, dy = b.y - a.y;
|
|
863
1053
|
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
864
|
-
let force =
|
|
1054
|
+
let force = curRepulsion / (dist * dist);
|
|
865
1055
|
// Same-color nodes repel less (stay in galaxy)
|
|
866
1056
|
if (a.color === b.color) force *= 0.4;
|
|
867
1057
|
let fx = (dx / dist) * force, fy = (dy / dist) * force;
|
|
@@ -870,8 +1060,7 @@ function renderGraph(graph) {
|
|
|
870
1060
|
}
|
|
871
1061
|
}
|
|
872
1062
|
|
|
873
|
-
// --- Edge attraction (
|
|
874
|
-
const warmup = Math.min(1, simTick / 120);
|
|
1063
|
+
// --- Edge attraction (also ramped) ---
|
|
875
1064
|
const curAttraction = ATTRACTION * warmup;
|
|
876
1065
|
for (const edge of edges) {
|
|
877
1066
|
let dx = edge.target.x - edge.source.x, dy = edge.target.y - edge.source.y;
|
|
@@ -890,21 +1079,34 @@ function renderGraph(graph) {
|
|
|
890
1079
|
node.vy += (cc.y - node.y) * CLUSTER_PULL;
|
|
891
1080
|
}
|
|
892
1081
|
|
|
893
|
-
// ---
|
|
894
|
-
const jitter = simTick <
|
|
895
|
-
const maxV = 3.0; //
|
|
1082
|
+
// --- Gentle jitter only during warmup, then pure physics ---
|
|
1083
|
+
const jitter = simTick < 60 ? 0.06 * (1 - simTick / 60) : 0;
|
|
1084
|
+
const maxV = simTick < 40 ? 1.2 : 3.0; // Strict speed limit during early frames
|
|
896
1085
|
let totalMovement = 0;
|
|
897
1086
|
for (const node of nodes) {
|
|
898
1087
|
if (node === dragNode) continue;
|
|
899
1088
|
node.vx *= DAMPING; node.vy *= DAMPING;
|
|
900
|
-
|
|
901
|
-
|
|
1089
|
+
if (jitter > 0) {
|
|
1090
|
+
node.vx += (Math.random() - 0.5) * jitter;
|
|
1091
|
+
node.vy += (Math.random() - 0.5) * jitter;
|
|
1092
|
+
}
|
|
902
1093
|
// Clamp velocity
|
|
903
1094
|
const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy);
|
|
904
1095
|
if (speed > maxV) { node.vx *= maxV / speed; node.vy *= maxV / speed; }
|
|
905
1096
|
node.x += node.vx; node.y += node.vy;
|
|
906
1097
|
totalMovement += Math.abs(node.vx) + Math.abs(node.vy);
|
|
907
1098
|
}
|
|
1099
|
+
|
|
1100
|
+
// Settle detection — stop physics when stable
|
|
1101
|
+
if (stableMode && simTick > 120) {
|
|
1102
|
+
const avgMovement = totalMovement / Math.max(1, nodes.length);
|
|
1103
|
+
if (avgMovement < SETTLE_THRESHOLD) {
|
|
1104
|
+
settleCountdown++;
|
|
1105
|
+
if (settleCountdown > 30) isSettled = true;
|
|
1106
|
+
} else {
|
|
1107
|
+
settleCountdown = 0;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
908
1110
|
return totalMovement;
|
|
909
1111
|
}
|
|
910
1112
|
|
|
@@ -1292,14 +1494,19 @@ function renderGraph(graph) {
|
|
|
1292
1494
|
drawer.classList.add('open');
|
|
1293
1495
|
}
|
|
1294
1496
|
|
|
1295
|
-
// --- Animation loop
|
|
1497
|
+
// --- Animation loop — stops physics when settled, breathing is visual-only ---
|
|
1498
|
+
let animFrame = null;
|
|
1296
1499
|
function tick() {
|
|
1297
|
-
|
|
1500
|
+
if (!isSettled) {
|
|
1501
|
+
simulate();
|
|
1502
|
+
}
|
|
1298
1503
|
draw();
|
|
1299
|
-
requestAnimationFrame(tick);
|
|
1504
|
+
animFrame = requestAnimationFrame(tick);
|
|
1300
1505
|
}
|
|
1301
1506
|
|
|
1302
1507
|
function wakeUp() {
|
|
1508
|
+
isSettled = false;
|
|
1509
|
+
settleCountdown = 0;
|
|
1303
1510
|
nodes.forEach(n => {
|
|
1304
1511
|
n.vx += (Math.random() - 0.5) * 0.5;
|
|
1305
1512
|
n.vy += (Math.random() - 0.5) * 0.5;
|
|
@@ -1824,6 +2031,295 @@ async function deleteObs(id, event) {
|
|
|
1824
2031
|
window.toggleObsDetail = toggleObsDetail;
|
|
1825
2032
|
window.deleteObs = deleteObs;
|
|
1826
2033
|
|
|
2034
|
+
// ============================================================
|
|
2035
|
+
// Git Memory Page
|
|
2036
|
+
// ============================================================
|
|
2037
|
+
|
|
2038
|
+
async function loadGitMemory() {
|
|
2039
|
+
const container = document.getElementById('page-git-memory');
|
|
2040
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
2041
|
+
|
|
2042
|
+
const [stats, allObs] = await Promise.all([api('stats'), api('observations')]);
|
|
2043
|
+
if (!stats || !allObs) {
|
|
2044
|
+
container.innerHTML = emptyState('🔀', t('noGitMemory'), t('noGitMemoryDesc'));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const gitObs = (allObs || []).filter(o => o.source === 'git').sort((a, b) => (b.id || 0) - (a.id || 0));
|
|
2049
|
+
const gs = stats.gitSummary || { total: 0, recentWeek: 0, recentMemories: [] };
|
|
2050
|
+
const sc = stats.sourceCounts || {};
|
|
2051
|
+
|
|
2052
|
+
// Type breakdown of git memories
|
|
2053
|
+
const gitTypes = {};
|
|
2054
|
+
gitObs.forEach(o => { gitTypes[o.type || 'unknown'] = (gitTypes[o.type || 'unknown'] || 0) + 1; });
|
|
2055
|
+
const gitTypeEntries = Object.entries(gitTypes).sort((a, b) => b[1] - a[1]);
|
|
2056
|
+
|
|
2057
|
+
container.innerHTML = `
|
|
2058
|
+
<div class="page-header">
|
|
2059
|
+
<h1 class="page-title">${t('gitMemoryTitle')}</h1>
|
|
2060
|
+
<p class="page-subtitle">${gitObs.length} ${t('gitMemorySubtitle')}</p>
|
|
2061
|
+
</div>
|
|
2062
|
+
|
|
2063
|
+
<div class="stats-grid">
|
|
2064
|
+
<div class="stat-card" data-accent="green">
|
|
2065
|
+
<div class="stat-label">${t('totalGitMemories')}</div>
|
|
2066
|
+
<div class="stat-value">${gitObs.length}</div>
|
|
2067
|
+
</div>
|
|
2068
|
+
<div class="stat-card" data-accent="cyan">
|
|
2069
|
+
<div class="stat-label">${t('thisWeek')}</div>
|
|
2070
|
+
<div class="stat-value">${gs.recentWeek}</div>
|
|
2071
|
+
</div>
|
|
2072
|
+
<div class="stat-card" data-accent="purple">
|
|
2073
|
+
<div class="stat-label">${t('uniqueCommits')}</div>
|
|
2074
|
+
<div class="stat-value">${new Set(gitObs.map(o => o.commitHash).filter(Boolean)).size}</div>
|
|
2075
|
+
</div>
|
|
2076
|
+
<div class="stat-card" data-accent="amber">
|
|
2077
|
+
<div class="stat-label">${t('typeCoverage')}</div>
|
|
2078
|
+
<div class="stat-value">${gitTypeEntries.length}</div>
|
|
2079
|
+
<div class="stat-sub">${gitTypeEntries.slice(0, 3).map(([t]) => t).join(', ')}</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
</div>
|
|
2082
|
+
|
|
2083
|
+
${gitObs.length === 0 ? `
|
|
2084
|
+
<div class="panel">
|
|
2085
|
+
<div class="panel-body" style="text-align:center;padding:48px;">
|
|
2086
|
+
<div style="font-size:36px;margin-bottom:12px;">🔀</div>
|
|
2087
|
+
<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('noGitMemoriesYet')}</div>
|
|
2088
|
+
<div style="font-size:13px;color:var(--text-muted);max-width:400px;margin:0 auto;">
|
|
2089
|
+
${t('noGitMemoriesHint')}<br>
|
|
2090
|
+
<code style="background:var(--bg-surface);padding:4px 10px;border-radius:6px;margin-top:8px;display:inline-block;font-size:12px;">memorix git-hook-install</code>
|
|
2091
|
+
</div>
|
|
2092
|
+
</div>
|
|
2093
|
+
</div>
|
|
2094
|
+
` : `
|
|
2095
|
+
<div class="panel">
|
|
2096
|
+
<div class="panel-header">
|
|
2097
|
+
<span class="panel-title">${t('recentGitMemories')}</span>
|
|
2098
|
+
<span style="font-size:11px;color:var(--text-muted);">${gitObs.length} total</span>
|
|
2099
|
+
</div>
|
|
2100
|
+
<div class="panel-body" style="padding:0;">
|
|
2101
|
+
<table class="retention-table">
|
|
2102
|
+
<thead>
|
|
2103
|
+
<tr>
|
|
2104
|
+
<th>${t('id')}</th>
|
|
2105
|
+
<th>${t('commit')}</th>
|
|
2106
|
+
<th>${t('title')}</th>
|
|
2107
|
+
<th>${t('type')}</th>
|
|
2108
|
+
<th>${t('entity')}</th>
|
|
2109
|
+
<th>${t('files')}</th>
|
|
2110
|
+
<th>${t('created')}</th>
|
|
2111
|
+
</tr>
|
|
2112
|
+
</thead>
|
|
2113
|
+
<tbody>
|
|
2114
|
+
${gitObs.slice(0, 50).map(obs => `
|
|
2115
|
+
<tr>
|
|
2116
|
+
<td style="font-family:var(--font-mono);color:var(--text-muted);">#${obs.id}</td>
|
|
2117
|
+
<td><code class="git-hash">${obs.commitHash ? escapeHtml(obs.commitHash.slice(0, 7)) : '—'}</code></td>
|
|
2118
|
+
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(obs.title || 'Untitled')}</td>
|
|
2119
|
+
<td><span class="type-badge" data-type="${obs.type || 'unknown'}">${obs.type || 'unknown'}</span></td>
|
|
2120
|
+
<td style="font-family:var(--font-mono);font-size:12px;color:var(--text-muted);">${escapeHtml(obs.entityName || '')}</td>
|
|
2121
|
+
<td style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);">${(obs.filesModified || []).length || '—'}</td>
|
|
2122
|
+
<td style="font-size:11px;color:var(--text-muted);">${obs.createdAt ? formatTime(obs.createdAt) : '—'}</td>
|
|
2123
|
+
</tr>
|
|
2124
|
+
`).join('')}
|
|
2125
|
+
</tbody>
|
|
2126
|
+
</table>
|
|
2127
|
+
</div>
|
|
2128
|
+
</div>
|
|
2129
|
+
`}
|
|
2130
|
+
`;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// ============================================================
|
|
2134
|
+
// Config Provenance Page
|
|
2135
|
+
// ============================================================
|
|
2136
|
+
|
|
2137
|
+
async function loadConfig() {
|
|
2138
|
+
const container = document.getElementById('page-config');
|
|
2139
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
2140
|
+
|
|
2141
|
+
const data = await api('config');
|
|
2142
|
+
if (!data) {
|
|
2143
|
+
container.innerHTML = emptyState('⚙️', t('configUnavailable'), t('configUnavailableDesc'));
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
const fileEntries = Object.entries(data.files || {});
|
|
2148
|
+
const values = data.values || [];
|
|
2149
|
+
|
|
2150
|
+
container.innerHTML = `
|
|
2151
|
+
<div class="page-header">
|
|
2152
|
+
<h1 class="page-title">${t('configTitle')}</h1>
|
|
2153
|
+
<p class="page-subtitle">${t('configSubtitle')}</p>
|
|
2154
|
+
</div>
|
|
2155
|
+
|
|
2156
|
+
<div class="overview-row">
|
|
2157
|
+
<div class="panel" style="flex:1;">
|
|
2158
|
+
<div class="panel-header"><span class="panel-title">${t('configSourceMatrix')}</span></div>
|
|
2159
|
+
<div class="panel-body">
|
|
2160
|
+
<div class="config-matrix">
|
|
2161
|
+
${fileEntries.map(([name, info]) => `
|
|
2162
|
+
<div class="config-file-row">
|
|
2163
|
+
<span class="config-file-status ${info.exists ? 'exists' : 'missing'}">${info.exists ? '✓' : '✗'}</span>
|
|
2164
|
+
<span class="config-file-name">${escapeHtml(name)}</span>
|
|
2165
|
+
<span class="config-file-path">${info.path ? escapeHtml(info.path) : ''}</span>
|
|
2166
|
+
</div>
|
|
2167
|
+
`).join('')}
|
|
2168
|
+
</div>
|
|
2169
|
+
<div class="config-hint">
|
|
2170
|
+
<strong>memorix.yml</strong> ${t('configHint')} | <strong>.env</strong> ${t('configHintEnv')}
|
|
2171
|
+
</div>
|
|
2172
|
+
</div>
|
|
2173
|
+
</div>
|
|
2174
|
+
</div>
|
|
2175
|
+
|
|
2176
|
+
<div class="panel">
|
|
2177
|
+
<div class="panel-header">
|
|
2178
|
+
<span class="panel-title">${t('valueProvenance')}</span>
|
|
2179
|
+
<span style="font-size:11px;color:var(--text-muted);">${values.length} ${t('trackedValues')}</span>
|
|
2180
|
+
</div>
|
|
2181
|
+
<div class="panel-body" style="padding:0;">
|
|
2182
|
+
<table class="retention-table">
|
|
2183
|
+
<thead>
|
|
2184
|
+
<tr>
|
|
2185
|
+
<th>${t('configKey')}</th>
|
|
2186
|
+
<th>${t('configValue')}</th>
|
|
2187
|
+
<th>${t('configSource')}</th>
|
|
2188
|
+
<th>${t('configStatus')}</th>
|
|
2189
|
+
</tr>
|
|
2190
|
+
</thead>
|
|
2191
|
+
<tbody>
|
|
2192
|
+
${values.map(v => {
|
|
2193
|
+
const isWarn = v.source && v.source.includes('move to .env');
|
|
2194
|
+
const isSensitive = v.sensitive;
|
|
2195
|
+
return `
|
|
2196
|
+
<tr>
|
|
2197
|
+
<td><code class="config-key">${escapeHtml(v.key)}</code></td>
|
|
2198
|
+
<td style="font-family:var(--font-mono);font-size:12px;">${isSensitive ? '<span class="config-masked">' + escapeHtml(v.value) + '</span>' : escapeHtml(v.value)}</td>
|
|
2199
|
+
<td><span class="config-source-badge ${isWarn ? 'warn' : ''}">${escapeHtml(v.source)}</span></td>
|
|
2200
|
+
<td>${isWarn ? '<span class="config-warn-badge">⚠ ' + t('moveToEnv') + '</span>' : ''}</td>
|
|
2201
|
+
</tr>
|
|
2202
|
+
`;
|
|
2203
|
+
}).join('')}
|
|
2204
|
+
</tbody>
|
|
2205
|
+
</table>
|
|
2206
|
+
</div>
|
|
2207
|
+
</div>
|
|
2208
|
+
`;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// ============================================================
|
|
2212
|
+
// Identity Health Page
|
|
2213
|
+
// ============================================================
|
|
2214
|
+
|
|
2215
|
+
async function loadIdentity() {
|
|
2216
|
+
const container = document.getElementById('page-identity');
|
|
2217
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
2218
|
+
|
|
2219
|
+
const data = await api('identity');
|
|
2220
|
+
if (!data) {
|
|
2221
|
+
container.innerHTML = emptyState('🛡️', t('identityUnavailable'), t('identityUnavailableDesc'));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
const healthColor = data.isHealthy ? 'var(--accent-green)' : 'var(--accent-red)';
|
|
2226
|
+
const healthIcon = data.isHealthy ? t('healthy') : t('unhealthy');
|
|
2227
|
+
|
|
2228
|
+
container.innerHTML = `
|
|
2229
|
+
<div class="page-header">
|
|
2230
|
+
<h1 class="page-title">${t('identityTitle')}</h1>
|
|
2231
|
+
<p class="page-subtitle">${t('identitySubtitle')}</p>
|
|
2232
|
+
</div>
|
|
2233
|
+
|
|
2234
|
+
<div class="stats-grid">
|
|
2235
|
+
<div class="stat-card" data-accent="${data.isHealthy ? 'green' : 'red'}">
|
|
2236
|
+
<div class="stat-label">${t('healthStatus')}</div>
|
|
2237
|
+
<div class="stat-value" style="font-size:20px;color:${healthColor}">${healthIcon}</div>
|
|
2238
|
+
</div>
|
|
2239
|
+
<div class="stat-card" data-accent="cyan">
|
|
2240
|
+
<div class="stat-label">${t('knownProjectIds')}</div>
|
|
2241
|
+
<div class="stat-value">${data.allProjectIds?.length || 0}</div>
|
|
2242
|
+
</div>
|
|
2243
|
+
<div class="stat-card" data-accent="purple">
|
|
2244
|
+
<div class="stat-label">${t('aliasGroups')}</div>
|
|
2245
|
+
<div class="stat-value">${data.aliasGroups || 0}</div>
|
|
2246
|
+
</div>
|
|
2247
|
+
<div class="stat-card" data-accent="amber">
|
|
2248
|
+
<div class="stat-label">${t('dirtyIds')}</div>
|
|
2249
|
+
<div class="stat-value">${data.dirtyIds?.length || 0}</div>
|
|
2250
|
+
</div>
|
|
2251
|
+
</div>
|
|
2252
|
+
|
|
2253
|
+
<div class="overview-row">
|
|
2254
|
+
<div class="panel" style="flex:1;">
|
|
2255
|
+
<div class="panel-header"><span class="panel-title">${t('currentIdentity')}</span></div>
|
|
2256
|
+
<div class="panel-body">
|
|
2257
|
+
<div class="identity-row">
|
|
2258
|
+
<span class="identity-label">${t('currentProjectId')}</span>
|
|
2259
|
+
<code class="identity-value">${escapeHtml(data.currentProjectId || '—')}</code>
|
|
2260
|
+
</div>
|
|
2261
|
+
<div class="identity-row">
|
|
2262
|
+
<span class="identity-label">${t('canonicalId')}</span>
|
|
2263
|
+
<code class="identity-value">${escapeHtml(data.canonicalId || '—')}</code>
|
|
2264
|
+
</div>
|
|
2265
|
+
<div class="identity-row">
|
|
2266
|
+
<span class="identity-label">${t('aliases')}</span>
|
|
2267
|
+
<div>${(data.aliases || []).map(a => `<code class="identity-alias">${escapeHtml(a)}</code>`).join(' ')}</div>
|
|
2268
|
+
</div>
|
|
2269
|
+
</div>
|
|
2270
|
+
</div>
|
|
2271
|
+
|
|
2272
|
+
<div class="panel" style="flex:1;">
|
|
2273
|
+
<div class="panel-header"><span class="panel-title">${t('healthIssues')}</span></div>
|
|
2274
|
+
<div class="panel-body">
|
|
2275
|
+
${(data.healthIssues || []).length === 0
|
|
2276
|
+
? '<div style="color:var(--accent-green);font-size:13px;">' + t('noIssues') + '</div>'
|
|
2277
|
+
: (data.healthIssues || []).map(issue => `
|
|
2278
|
+
<div class="identity-issue">
|
|
2279
|
+
<span style="color:var(--accent-red);">⚠</span>
|
|
2280
|
+
<span>${escapeHtml(issue)}</span>
|
|
2281
|
+
</div>
|
|
2282
|
+
`).join('')
|
|
2283
|
+
}
|
|
2284
|
+
</div>
|
|
2285
|
+
</div>
|
|
2286
|
+
</div>
|
|
2287
|
+
|
|
2288
|
+
${(data.dirtyIds || []).length > 0 ? `
|
|
2289
|
+
<div class="panel">
|
|
2290
|
+
<div class="panel-header"><span class="panel-title">${t('dirtyProjectIds')}</span></div>
|
|
2291
|
+
<div class="panel-body">
|
|
2292
|
+
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
|
2293
|
+
${data.dirtyIds.map(id => `<code class="identity-dirty">${escapeHtml(id)}</code>`).join('')}
|
|
2294
|
+
</div>
|
|
2295
|
+
</div>
|
|
2296
|
+
</div>
|
|
2297
|
+
` : ''}
|
|
2298
|
+
|
|
2299
|
+
<div class="panel">
|
|
2300
|
+
<div class="panel-header">
|
|
2301
|
+
<span class="panel-title">${t('allKnownProjectIds')}</span>
|
|
2302
|
+
<span style="font-size:11px;color:var(--text-muted);">${data.allProjectIds?.length || 0} total</span>
|
|
2303
|
+
</div>
|
|
2304
|
+
<div class="panel-body">
|
|
2305
|
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
2306
|
+
${(data.allProjectIds || []).map(id => {
|
|
2307
|
+
const isDirty = (data.dirtyIds || []).includes(id);
|
|
2308
|
+
const isCurrent = id === data.currentProjectId;
|
|
2309
|
+
const isCanonical = id === data.canonicalId;
|
|
2310
|
+
return `<div class="identity-id-row">
|
|
2311
|
+
<code class="identity-id ${isDirty ? 'dirty' : ''}">${escapeHtml(id)}</code>
|
|
2312
|
+
${isCurrent ? '<span class="identity-tag current">' + t('tagCurrent') + '</span>' : ''}
|
|
2313
|
+
${isCanonical ? '<span class="identity-tag canonical">' + t('tagCanonical') + '</span>' : ''}
|
|
2314
|
+
${isDirty ? '<span class="identity-tag dirty">' + t('tagDirty') + '</span>' : ''}
|
|
2315
|
+
</div>`;
|
|
2316
|
+
}).join('')}
|
|
2317
|
+
</div>
|
|
2318
|
+
</div>
|
|
2319
|
+
</div>
|
|
2320
|
+
`;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
1827
2323
|
// ============================================================
|
|
1828
2324
|
// Utilities
|
|
1829
2325
|
// ============================================================
|
|
@@ -1972,8 +2468,23 @@ async function loadTeam() {
|
|
|
1972
2468
|
}
|
|
1973
2469
|
|
|
1974
2470
|
const data = await api('team');
|
|
1975
|
-
if (!data) {
|
|
1976
|
-
container.innerHTML =
|
|
2471
|
+
if (!data || data.unavailable) {
|
|
2472
|
+
container.innerHTML = `
|
|
2473
|
+
<div class="page-header">
|
|
2474
|
+
<h1 class="page-title">${t('teamTitle')}</h1>
|
|
2475
|
+
<p class="page-subtitle">${t('teamSubtitle')}</p>
|
|
2476
|
+
</div>
|
|
2477
|
+
<div class="panel">
|
|
2478
|
+
<div class="panel-body" style="text-align:center;padding:48px;">
|
|
2479
|
+
<div style="font-size:36px;margin-bottom:12px;">👥</div>
|
|
2480
|
+
<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('teamNoData')}</div>
|
|
2481
|
+
<div style="font-size:13px;color:var(--text-muted);max-width:480px;margin:0 auto;line-height:1.6;">
|
|
2482
|
+
${t('teamNoDataHint')}<br>
|
|
2483
|
+
<code style="background:var(--bg-surface);padding:4px 10px;border-radius:6px;margin-top:8px;display:inline-block;font-size:12px;">memorix serve-http --port 3211</code>
|
|
2484
|
+
</div>
|
|
2485
|
+
</div>
|
|
2486
|
+
</div>
|
|
2487
|
+
`;
|
|
1977
2488
|
return;
|
|
1978
2489
|
}
|
|
1979
2490
|
|