@memtensor/memos-local-openclaw-plugin 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.env.example +6 -0
  2. package/README.md +90 -25
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/embedding/local.d.ts.map +1 -1
  7. package/dist/embedding/local.js +3 -2
  8. package/dist/embedding/local.js.map +1 -1
  9. package/dist/skill/evaluator.js +1 -1
  10. package/dist/skill/evaluator.js.map +1 -1
  11. package/dist/skill/generator.js +1 -1
  12. package/dist/skill/generator.js.map +1 -1
  13. package/dist/skill/upgrader.js +1 -1
  14. package/dist/skill/upgrader.js.map +1 -1
  15. package/dist/skill/validator.js +1 -1
  16. package/dist/skill/validator.js.map +1 -1
  17. package/dist/storage/sqlite.d.ts +4 -0
  18. package/dist/storage/sqlite.d.ts.map +1 -1
  19. package/dist/storage/sqlite.js +19 -0
  20. package/dist/storage/sqlite.js.map +1 -1
  21. package/dist/telemetry.d.ts +37 -0
  22. package/dist/telemetry.d.ts.map +1 -0
  23. package/dist/telemetry.js +179 -0
  24. package/dist/telemetry.js.map +1 -0
  25. package/dist/types.d.ts +6 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js.map +1 -1
  28. package/dist/viewer/html.d.ts +1 -1
  29. package/dist/viewer/html.d.ts.map +1 -1
  30. package/dist/viewer/html.js +828 -52
  31. package/dist/viewer/html.js.map +1 -1
  32. package/dist/viewer/server.d.ts +25 -1
  33. package/dist/viewer/server.d.ts.map +1 -1
  34. package/dist/viewer/server.js +807 -0
  35. package/dist/viewer/server.js.map +1 -1
  36. package/index.ts +31 -3
  37. package/openclaw.plugin.json +3 -3
  38. package/package.json +4 -3
  39. package/src/config.ts +11 -0
  40. package/src/embedding/local.ts +3 -2
  41. package/src/skill/evaluator.ts +1 -1
  42. package/src/skill/generator.ts +1 -1
  43. package/src/skill/upgrader.ts +1 -1
  44. package/src/skill/validator.ts +1 -1
  45. package/src/storage/sqlite.ts +29 -0
  46. package/src/telemetry.ts +160 -0
  47. package/src/types.ts +7 -1
  48. package/src/viewer/html.ts +828 -52
  49. package/src/viewer/server.ts +818 -1
@@ -181,6 +181,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
181
181
  .dedup-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
182
182
  .dedup-badge.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}
183
183
  .dedup-badge.merged{background:rgba(59,130,246,.12);color:#60a5fa}
184
+ .import-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(236,72,153,.1);color:#ec4899;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
185
+ [data-theme="light"] .import-badge{background:rgba(219,39,119,.08);color:#db2777}
184
186
  .memory-card.dedup-inactive{opacity:.55;border-style:dashed}
185
187
  .memory-card.dedup-inactive:hover{opacity:.85}
186
188
  .dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}
@@ -406,8 +408,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
406
408
  .nav-tabs .tab.active{color:var(--text);background:rgba(255,255,255,.1);border-color:var(--border);box-shadow:0 1px 4px rgba(0,0,0,.15)}
407
409
  [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
408
410
  [data-theme="light"] .nav-tabs .tab.active{background:#fff;border-color:rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.08);color:var(--text)}
409
- .analytics-view,.settings-view,.logs-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
410
- .analytics-view.show,.settings-view.show,.logs-view.show{display:flex}
411
+ .analytics-view,.settings-view,.logs-view,.migrate-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
412
+ .analytics-view.show,.settings-view.show,.logs-view.show,.migrate-view.show{display:flex}
411
413
 
412
414
  /* ─── Logs ─── */
413
415
  .logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
@@ -503,6 +505,24 @@ input,textarea,select{font-family:inherit;font-size:inherit}
503
505
  [data-theme="light"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}
504
506
  .settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}
505
507
  .settings-saved.show{opacity:1}
508
+ .migrate-log-item{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border);animation:migrateFadeIn .3s ease}
509
+ .migrate-log-item:last-child{border-bottom:none}
510
+ .migrate-log-item .log-icon{flex-shrink:0;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;margin-top:2px}
511
+ .migrate-log-item .log-icon.stored{background:rgba(34,197,94,.12);color:#22c55e}
512
+ .migrate-log-item .log-icon.skipped{background:rgba(245,158,11,.12);color:#f59e0b}
513
+ .migrate-log-item .log-icon.merged{background:rgba(59,130,246,.12);color:#3b82f6}
514
+ .migrate-log-item .log-icon.error{background:rgba(239,68,68,.12);color:#ef4444}
515
+ .migrate-log-item .log-icon.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}
516
+ .migrate-log-item .log-body{flex:1;min-width:0}
517
+ .migrate-log-item .log-preview{color:var(--text);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
518
+ .migrate-log-item .log-meta{display:flex;gap:8px;font-size:9px;color:var(--text-muted);margin-top:2px}
519
+ .migrate-log-item .log-meta .tag{padding:1px 6px;border-radius:4px;font-weight:600;letter-spacing:.02em}
520
+ .migrate-log-item .log-meta .tag.stored{background:rgba(34,197,94,.1);color:#22c55e}
521
+ .migrate-log-item .log-meta .tag.skipped{background:rgba(245,158,11,.1);color:#f59e0b}
522
+ .migrate-log-item .log-meta .tag.merged{background:rgba(59,130,246,.1);color:#3b82f6}
523
+ .migrate-log-item .log-meta .tag.error{background:rgba(239,68,68,.1);color:#ef4444}
524
+ .migrate-log-item .log-meta .tag.duplicate{background:rgba(245,158,11,.1);color:#f59e0b}
525
+ @keyframes migrateFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
506
526
  .feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}
507
527
  .feed-wrap.hide{display:none}
508
528
  .analytics-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
@@ -685,6 +705,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
685
705
  <button class="tab" data-view="skills" onclick="switchView('skills')" data-i18n="tab.skills">\u{1F9E0} Skills</button>
686
706
  <button class="tab" data-view="analytics" onclick="switchView('analytics')" data-i18n="tab.analytics">\u{1F4CA} Analytics</button>
687
707
  <button class="tab" data-view="logs" onclick="switchView('logs')" data-i18n="tab.logs">\u{1F4DD} Logs</button>
708
+ <button class="tab" data-view="import" onclick="switchView('import')" data-i18n="tab.import">\u{1F4E5} Import</button>
688
709
  <button class="tab" data-view="settings" onclick="switchView('settings')" data-i18n="tab.settings">\u2699 Settings</button>
689
710
  </nav>
690
711
  </div>
@@ -965,38 +986,17 @@ input,textarea,select{font-family:inherit;font-size:inherit}
965
986
  <input type="number" id="cfgSkillMinChunks" placeholder="6">
966
987
  </div>
967
988
  </div>
968
- <div style="margin-top:16px;padding-top:16px;border-top:1px dashed var(--border)">
969
- <h3 style="font-size:12px;color:var(--text-muted);margin-bottom:4px;display:flex;align-items:center;gap:8px"><span class="icon">\u{1F680}</span> <span data-i18n="settings.skill.model">Skill Dedicated Model</span><span style="font-size:10px;font-weight:500;color:var(--amber);background:var(--amber-bg);padding:2px 8px;border-radius:10px" data-i18n="settings.optional">Optional</span></h3>
970
- <div style="font-size:11px;color:var(--text-muted);margin-bottom:12px;line-height:1.5" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
971
- <div class="settings-grid">
972
- <div class="settings-field">
973
- <label data-i18n="settings.provider">Provider</label>
974
- <select id="cfgSkillProvider">
975
- <option value="" id="cfgSkillProviderDefault">\u2014</option>
976
- <option value="openai_compatible">OpenAI Compatible</option>
977
- <option value="openai">OpenAI</option>
978
- <option value="anthropic">Anthropic</option>
979
- <option value="gemini">Gemini</option>
980
- <option value="azure_openai">Azure OpenAI</option>
981
- <option value="bedrock">Bedrock</option>
982
- </select>
983
- </div>
984
- <div class="settings-field">
985
- <label data-i18n="settings.model">Model</label>
986
- <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
987
- </div>
988
- <div class="settings-field full-width">
989
- <label>Endpoint</label>
990
- <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
991
- </div>
992
- <div class="settings-field">
993
- <label>API Key</label>
994
- <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
995
- </div>
996
- <div class="settings-field">
997
- <label data-i18n="settings.temperature">Temperature</label>
998
- <input type="number" id="cfgSkillTemp" step="0.1" min="0" max="2" placeholder="0.2">
999
- </div>
989
+ </div>
990
+
991
+ <div class="settings-section">
992
+ <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="settings.telemetry">Telemetry</span></h3>
993
+ <div class="settings-grid">
994
+ <div class="settings-toggle">
995
+ <label class="toggle-switch"><input type="checkbox" id="cfgTelemetryEnabled" checked><span class="toggle-slider"></span></label>
996
+ <label data-i18n="settings.telemetry.enabled">Enable Anonymous Telemetry</label>
997
+ </div>
998
+ <div class="settings-field full-width">
999
+ <div class="field-hint" data-i18n="settings.telemetry.hint">Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.</div>
1000
1000
  </div>
1001
1001
  </div>
1002
1002
  </div>
@@ -1020,6 +1020,160 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1020
1020
  <div style="font-size:11px;color:var(--text-muted);text-align:right;margin-top:4px" data-i18n="settings.restart.hint">Some changes require restarting the OpenClaw gateway to take effect.</div>
1021
1021
  </div>
1022
1022
 
1023
+ <!-- ─── Import Page ─── -->
1024
+ <div class="migrate-view" id="migrateView">
1025
+ <div class="settings-section" style="border:1px solid rgba(99,102,241,.15)">
1026
+ <h3><span class="icon">\u{1F4E5}</span> <span data-i18n="migrate.title">Import OpenClaw Memory</span></h3>
1027
+ <p style="font-size:12px;color:var(--text-sec);margin-bottom:12px;line-height:1.6" data-i18n="migrate.desc">Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.</p>
1028
+
1029
+ <div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:12px;line-height:1.7;color:var(--text-sec)">
1030
+ <div style="font-weight:700;color:var(--text);margin-bottom:8px" data-i18n="migrate.modes.title">Three ways to use:</div>
1031
+ <div style="display:flex;flex-direction:column;gap:6px">
1032
+ <div><span style="font-weight:600;color:var(--accent)" data-i18n="migrate.mode1.label">\u2460 Import memories only (fast)</span><span data-i18n="migrate.mode1.desc"> — Click "Start Import" to quickly migrate all memory chunks and conversations. No task/skill generation. Suitable when you just need the raw data.</span></div>
1033
+ <div><span style="font-weight:600;color:var(--accent)" data-i18n="migrate.mode2.label">\u2461 Import + generate tasks & skills (slow, serial)</span><span data-i18n="migrate.mode2.desc"> — After importing memories, enable "Generate Tasks" and/or "Trigger Skill Evolution" below to analyze conversations one by one. This takes longer as each session is processed by LLM sequentially.</span></div>
1034
+ <div><span style="font-weight:600;color:var(--accent)" data-i18n="migrate.mode3.label">\u2462 Import first, generate later (flexible)</span><span data-i18n="migrate.mode3.desc"> — Import memories now, then come back anytime to start task/skill generation. You can pause the generation at any point and resume later — it will pick up where you left off, only processing sessions that haven't been handled yet.</span></div>
1035
+ </div>
1036
+ </div>
1037
+
1038
+ <div id="migrateConfigWarn" style="display:none;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);border-radius:10px;padding:14px 18px;margin-bottom:16px">
1039
+ <div style="font-size:12px;font-weight:600;color:#f59e0b;margin-bottom:6px">\u26A0 <span data-i18n="migrate.config.warn">Configuration Required</span></div>
1040
+ <div style="font-size:11px;color:var(--text-sec);line-height:1.5" data-i18n="migrate.config.warn.desc">Please configure both Embedding Model and Summarizer Model in Settings before importing. These are required for processing memories.</div>
1041
+ </div>
1042
+
1043
+ <div id="migrateScanResult" style="display:none;margin-bottom:16px">
1044
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
1045
+ <div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px">
1046
+ <div style="font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px" data-i18n="migrate.sqlite.label">Memory Index (SQLite)</div>
1047
+ <div style="font-size:22px;font-weight:700;color:var(--text)" id="migrateSqliteCount">0</div>
1048
+ <div style="font-size:10px;color:var(--text-muted);margin-top:2px" id="migrateSqliteFiles"></div>
1049
+ </div>
1050
+ <div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px">
1051
+ <div style="font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px" data-i18n="migrate.sessions.label">Conversation History</div>
1052
+ <div style="font-size:22px;font-weight:700;color:var(--text)" id="migrateSessionCount">0</div>
1053
+ <div style="font-size:10px;color:var(--text-muted);margin-top:2px" id="migrateSessionFiles"></div>
1054
+ </div>
1055
+ </div>
1056
+ </div>
1057
+
1058
+ <div id="migrateActions" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
1059
+ <button class="btn btn-ghost" onclick="migrateScan()" id="migrateScanBtn" data-i18n="migrate.scan">Scan Data Sources</button>
1060
+ <button class="btn btn-primary" onclick="migrateStart()" id="migrateStartBtn" style="display:none" data-i18n="migrate.start">Start Import</button>
1061
+ <span id="migrateStatus" style="font-size:11px;color:var(--text-muted)"></span>
1062
+ </div>
1063
+
1064
+ <!-- Post-process section: shown after import completes -->
1065
+ <div id="postprocessSection" style="display:none;margin-top:16px">
1066
+ <div class="settings-section" style="border:1px solid var(--border)">
1067
+ <div style="font-size:14px;font-weight:700;color:var(--text);margin-bottom:6px" data-i18n="pp.title">\u{1F9E0} Optional: Generate Tasks & Skills</div>
1068
+ <div style="font-size:12px;color:var(--text-sec);margin-bottom:14px;line-height:1.6" data-i18n="pp.desc">This step is completely optional. The import above has already stored raw memory data. Here you can further analyze imported conversations to generate structured task summaries and evolve reusable skills. Processing is serial (one session at a time) and may take a while. You can stop at any time and resume later — it will only process sessions not yet handled.</div>
1069
+ <div style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px">
1070
+ <label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
1071
+ <input type="checkbox" id="ppEnableTasks" checked style="accent-color:var(--accent);margin-top:2px">
1072
+ <div>
1073
+ <div style="font-size:12px;font-weight:600;color:var(--text)" data-i18n="pp.tasks.label">Generate task summaries</div>
1074
+ <div style="font-size:11px;color:var(--text-sec);line-height:1.4" data-i18n="pp.tasks.hint">Group imported messages into tasks and generate a structured summary (title, goal, steps, result) for each one. Makes it easier to search and recall past work.</div>
1075
+ </div>
1076
+ </label>
1077
+ <label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
1078
+ <input type="checkbox" id="ppEnableSkills" style="accent-color:var(--accent);margin-top:2px">
1079
+ <div>
1080
+ <div style="font-size:12px;font-weight:600;color:var(--text)" data-i18n="pp.skills.label">Trigger skill evolution</div>
1081
+ <div style="font-size:11px;color:var(--text-sec);line-height:1.4" data-i18n="pp.skills.hint">Analyze completed tasks and automatically create or upgrade reusable skills (SKILL.md). Requires task summaries to be enabled. May take longer due to LLM evaluation.</div>
1082
+ </div>
1083
+ </label>
1084
+ </div>
1085
+ <div style="display:flex;gap:10px;align-items:center">
1086
+ <button class="btn btn-primary" id="ppStartBtn" onclick="ppStart()" data-i18n="pp.start">Start Processing</button>
1087
+ <button class="btn btn-sm" id="ppStopBtn" onclick="ppStop()" style="display:none;background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600" data-i18n="migrate.stop">\u25A0 Stop</button>
1088
+ <span id="ppStatus" style="font-size:11px;color:var(--text-muted)"></span>
1089
+ </div>
1090
+ <div id="ppProgress" style="display:none;margin-top:12px">
1091
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
1092
+ <div style="font-size:12px;font-weight:600;color:var(--text)" id="ppPhaseLabel"></div>
1093
+ <div style="font-size:11px;color:var(--text-muted);flex:1" id="ppCounter"></div>
1094
+ </div>
1095
+ <div style="position:relative;height:5px;background:var(--bg);border-radius:3px;overflow:hidden;margin-bottom:12px">
1096
+ <div id="ppBar" style="position:absolute;left:0;top:0;height:100%;width:0%;background:linear-gradient(90deg,#f59e0b,#fbbf24);border-radius:3px;transition:width .3s ease"></div>
1097
+ </div>
1098
+ <div style="display:flex;gap:16px;margin-bottom:12px" id="ppStatsRow">
1099
+ <div style="display:flex;align-items:center;gap:5px;font-size:11px">
1100
+ <span style="width:7px;height:7px;border-radius:50%;background:#22c55e;display:inline-block"></span>
1101
+ <span style="color:var(--text-sec)" data-i18n="pp.stat.tasks">Tasks</span>
1102
+ <span style="font-weight:700;color:var(--text)" id="ppStatTasks">0</span>
1103
+ </div>
1104
+ <div style="display:flex;align-items:center;gap:5px;font-size:11px">
1105
+ <span style="width:7px;height:7px;border-radius:50%;background:#8b5cf6;display:inline-block"></span>
1106
+ <span style="color:var(--text-sec)" data-i18n="pp.stat.skills">Skills</span>
1107
+ <span style="font-weight:700;color:var(--text)" id="ppStatSkills">0</span>
1108
+ </div>
1109
+ <div style="display:flex;align-items:center;gap:5px;font-size:11px">
1110
+ <span style="width:7px;height:7px;border-radius:50%;background:#ef4444;display:inline-block"></span>
1111
+ <span style="color:var(--text-sec)" data-i18n="pp.stat.errors">Errors</span>
1112
+ <span style="font-weight:700;color:var(--text)" id="ppStatErrors">0</span>
1113
+ </div>
1114
+ <div style="display:flex;align-items:center;gap:5px;font-size:11px" id="ppSkippedInfo" style="display:none">
1115
+ <span style="width:7px;height:7px;border-radius:50%;background:#3b82f6;display:inline-block"></span>
1116
+ <span style="color:var(--text-sec)" data-i18n="pp.stat.skipped">Skipped</span>
1117
+ <span style="font-weight:700;color:var(--text)" id="ppStatSkipped">0</span>
1118
+ </div>
1119
+ </div>
1120
+ <div id="ppLiveLog" style="background:var(--bg);border:1px solid var(--border);border-radius:8px;max-height:320px;overflow-y:auto;font-family:'SF Mono','Fira Code',monospace;font-size:11px;line-height:1.7;padding:0"></div>
1121
+ </div>
1122
+ <div id="ppDone" style="display:none;margin-top:12px;padding:10px 14px;border-radius:8px;font-size:12px;color:var(--text-sec);line-height:1.5"></div>
1123
+ </div>
1124
+ </div>
1125
+ </div>
1126
+
1127
+ <!-- Progress Area -->
1128
+ <div id="migrateProgress" style="display:none">
1129
+ <div class="settings-section">
1130
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
1131
+ <div style="font-size:13px;font-weight:600;color:var(--text)" id="migratePhaseLabel"></div>
1132
+ <div style="font-size:12px;color:var(--text-muted);flex:1" id="migrateCounter"></div>
1133
+ <button class="btn btn-sm" id="migrateStopBtn" onclick="migrateStop()" style="background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600;cursor:pointer" data-i18n="migrate.stop">\u25A0 Stop</button>
1134
+ </div>
1135
+
1136
+ <div style="position:relative;height:6px;background:var(--bg);border-radius:3px;overflow:hidden;margin-bottom:16px">
1137
+ <div id="migrateBar" style="position:absolute;left:0;top:0;height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:3px;transition:width .3s ease"></div>
1138
+ </div>
1139
+
1140
+ <div style="display:flex;gap:20px;margin-bottom:16px" id="migrateStatsRow">
1141
+ <div style="display:flex;align-items:center;gap:6px;font-size:12px">
1142
+ <span style="width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block"></span>
1143
+ <span style="color:var(--text-sec)" data-i18n="migrate.stat.stored">Stored</span>
1144
+ <span style="font-weight:700;color:var(--text)" id="migrateStatStored">0</span>
1145
+ </div>
1146
+ <div style="display:flex;align-items:center;gap:6px;font-size:12px">
1147
+ <span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block"></span>
1148
+ <span style="color:var(--text-sec)" data-i18n="migrate.stat.skipped">Skipped</span>
1149
+ <span style="font-weight:700;color:var(--text)" id="migrateStatSkipped">0</span>
1150
+ </div>
1151
+ <div style="display:flex;align-items:center;gap:6px;font-size:12px">
1152
+ <span style="width:8px;height:8px;border-radius:50%;background:#3b82f6;display:inline-block"></span>
1153
+ <span style="color:var(--text-sec)" data-i18n="migrate.stat.merged">Merged</span>
1154
+ <span style="font-weight:700;color:var(--text)" id="migrateStatMerged">0</span>
1155
+ </div>
1156
+ <div style="display:flex;align-items:center;gap:6px;font-size:12px">
1157
+ <span style="width:8px;height:8px;border-radius:50%;background:#ef4444;display:inline-block"></span>
1158
+ <span style="color:var(--text-sec)" data-i18n="migrate.stat.errors">Errors</span>
1159
+ <span style="font-weight:700;color:var(--text)" id="migrateStatErrors">0</span>
1160
+ </div>
1161
+ </div>
1162
+
1163
+ <div id="migrateLiveLog" style="background:var(--bg);border:1px solid var(--border);border-radius:10px;max-height:480px;overflow-y:auto;font-family:'SF Mono','Fira Code',monospace;font-size:11px;line-height:1.7;padding:0">
1164
+ </div>
1165
+ </div>
1166
+ </div>
1167
+
1168
+ <!-- Done Summary -->
1169
+ <div id="migrateDone" style="display:none">
1170
+ <div class="settings-section" style="background:rgba(34,197,94,.04);border:1px solid rgba(34,197,94,.2)">
1171
+ <div style="font-size:14px;font-weight:700;color:#22c55e;margin-bottom:8px">\u2705 <span data-i18n="migrate.done">Import Complete</span></div>
1172
+ <div id="migrateDoneSummary" style="font-size:12px;color:var(--text-sec);line-height:1.6"></div>
1173
+ </div>
1174
+ </div>
1175
+ </div>
1176
+
1023
1177
  </div>
1024
1178
  </div>
1025
1179
 
@@ -1192,6 +1346,7 @@ const I18N={
1192
1346
  'logs.output':'OUTPUT',
1193
1347
  'logs.empty':'No logs yet. Logs will appear here when tools are called.',
1194
1348
  'logs.ago':'ago',
1349
+ 'tab.import':'\u{1F4E5} Import',
1195
1350
  'tab.settings':'\u2699 Settings',
1196
1351
  'settings.embedding':'Embedding Model',
1197
1352
  'settings.summarizer':'Summarizer Model',
@@ -1208,6 +1363,9 @@ const I18N={
1208
1363
  'settings.skill.model.hint':'If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.',
1209
1364
  'settings.optional':'Optional',
1210
1365
  'settings.skill.usemain':'Use Main Summarizer',
1366
+ 'settings.telemetry':'Telemetry',
1367
+ 'settings.telemetry.enabled':'Enable Anonymous Telemetry',
1368
+ 'settings.telemetry.hint':'Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.',
1211
1369
  'settings.viewerport':'Viewer Port',
1212
1370
  'settings.viewerport.hint':'Requires restart to take effect',
1213
1371
  'settings.save':'Save Settings',
@@ -1215,6 +1373,65 @@ const I18N={
1215
1373
  'settings.saved':'Saved',
1216
1374
  'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',
1217
1375
  'settings.save.fail':'Failed to save settings',
1376
+ 'migrate.title':'Import OpenClaw Memory',
1377
+ 'migrate.desc':'Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.',
1378
+ 'migrate.modes.title':'Three ways to use:',
1379
+ 'migrate.mode1.label':'\\u2460 Import memories only (fast)',
1380
+ 'migrate.mode1.desc':' — Click "Start Import" to quickly migrate all memory chunks and conversations. No task/skill generation. Suitable when you just need the raw data.',
1381
+ 'migrate.mode2.label':'\\u2461 Import + generate tasks & skills (slow, serial)',
1382
+ 'migrate.mode2.desc':' — After importing memories, enable "Generate Tasks" and/or "Trigger Skill Evolution" below to analyze conversations one by one. This takes longer as each session is processed by LLM sequentially.',
1383
+ 'migrate.mode3.label':'\\u2462 Import first, generate later (flexible)',
1384
+ 'migrate.mode3.desc':' — Import memories now, then come back anytime to start task/skill generation. You can pause the generation at any point and resume later — it will pick up where you left off, only processing sessions that haven\\'t been handled yet.',
1385
+ 'migrate.config.warn':'Configuration Required',
1386
+ 'migrate.config.warn.desc':'Please configure both Embedding Model and Summarizer Model above before importing. These are required for processing memories.',
1387
+ 'migrate.sqlite.label':'Memory Index (SQLite)',
1388
+ 'migrate.sessions.label':'Conversation History',
1389
+ 'migrate.scan':'Scan Data Sources',
1390
+ 'migrate.start':'Start Import',
1391
+ 'migrate.scanning':'Scanning...',
1392
+ 'migrate.stat.stored':'Stored',
1393
+ 'migrate.stat.skipped':'Skipped',
1394
+ 'migrate.stat.merged':'Merged',
1395
+ 'migrate.stat.errors':'Errors',
1396
+ 'migrate.done':'Import Complete',
1397
+ 'migrate.done.summary':'Processed {total} items: {stored} stored, {skipped} skipped, {merged} merged, {errors} errors.',
1398
+ 'migrate.phase.sqlite':'Importing memory index...',
1399
+ 'migrate.phase.sessions':'Importing conversation history...',
1400
+ 'migrate.chunks':'chunks',
1401
+ 'migrate.sessions.count':'sessions, {n} messages',
1402
+ 'migrate.nodata':'No OpenClaw data found to import.',
1403
+ 'migrate.running':'Import in progress...',
1404
+ 'migrate.error.running':'A migration is already in progress.',
1405
+ 'migrate.stop':'\\u25A0 Stop',
1406
+ 'migrate.stopping':'Stopping...',
1407
+ 'migrate.stopped':'Import Stopped',
1408
+ 'migrate.stopped.hint':'Import was stopped. You can resume anytime — already imported items will be skipped automatically.',
1409
+ 'migrate.resume':'Continue Import',
1410
+ 'migrate.already.hint':'All data has been imported previously. No new items to process.',
1411
+ 'pp.title':'\\u{1F9E0} Optional: Generate Tasks & Skills',
1412
+ 'pp.desc':'This step is completely optional. The import above has already stored raw memory data. Here you can further analyze imported conversations to generate structured task summaries and evolve reusable skills. Processing is serial (one session at a time) and may take a while. You can stop at any time and resume later — it will only process sessions not yet handled.',
1413
+ 'pp.tasks.label':'Generate task summaries',
1414
+ 'pp.tasks.hint':'Group imported messages into tasks and generate a structured summary (title, goal, steps, result) for each one. Makes it easier to search and recall past work.',
1415
+ 'pp.skills.label':'Trigger skill evolution',
1416
+ 'pp.skills.hint':'Analyze completed tasks and automatically create or upgrade reusable skills (SKILL.md). Requires task summaries to be enabled. May take longer due to LLM evaluation.',
1417
+ 'pp.start':'Start Processing',
1418
+ 'pp.resume':'Resume Processing',
1419
+ 'pp.running':'Processing',
1420
+ 'pp.stopped':'Processing stopped. You can resume anytime.',
1421
+ 'pp.failed':'Processing failed — see error message above.',
1422
+ 'pp.done':'Task & skill generation complete!',
1423
+ 'pp.select.warn':'Please select at least one option.',
1424
+ 'pp.skill.created':'Skill created',
1425
+ 'pp.stat.tasks':'Tasks',
1426
+ 'pp.stat.skills':'Skills',
1427
+ 'pp.stat.errors':'Errors',
1428
+ 'pp.stat.skipped':'Skipped',
1429
+ 'pp.info.skipped':'{n} sessions already processed, skipping.',
1430
+ 'pp.info.pending':'Processing {n} sessions...',
1431
+ 'pp.info.allDone':'All sessions have been processed already. Nothing to do.',
1432
+ 'pp.action.full':'Task+Skill',
1433
+ 'pp.action.skillOnly':'Skill only (task exists)',
1434
+ 'card.imported':'OpenClaw Native',
1218
1435
  'skills.draft':'Draft',
1219
1436
  'skills.filter.active':'Active',
1220
1437
  'skills.filter.draft':'Draft',
@@ -1393,6 +1610,7 @@ const I18N={
1393
1610
  'logs.output':'输出',
1394
1611
  'logs.empty':'暂无日志。当工具被调用时日志会显示在这里。',
1395
1612
  'logs.ago':'前',
1613
+ 'tab.import':'\u{1F4E5} 导入',
1396
1614
  'tab.settings':'\u2699 设置',
1397
1615
  'settings.embedding':'嵌入模型',
1398
1616
  'settings.summarizer':'摘要模型',
@@ -1409,6 +1627,9 @@ const I18N={
1409
1627
  'settings.skill.model.hint':'不配置时默认使用上方的摘要模型进行技能生成。如需更高质量的技能输出,可在此单独配置一个更强的模型。',
1410
1628
  'settings.optional':'可选',
1411
1629
  'settings.skill.usemain':'使用主摘要模型',
1630
+ 'settings.telemetry':'数据统计',
1631
+ 'settings.telemetry.enabled':'启用匿名数据统计',
1632
+ 'settings.telemetry.hint':'匿名使用统计,帮助改进插件。仅发送工具名称、响应时间和版本信息,不会发送任何记忆内容、搜索查询或个人数据。',
1412
1633
  'settings.viewerport':'Viewer 端口',
1413
1634
  'settings.viewerport.hint':'修改后需重启网关生效',
1414
1635
  'settings.save':'保存设置',
@@ -1416,6 +1637,65 @@ const I18N={
1416
1637
  'settings.saved':'已保存',
1417
1638
  'settings.restart.hint':'部分设置修改后需要重启 OpenClaw 网关才能生效。',
1418
1639
  'settings.save.fail':'保存设置失败',
1640
+ 'migrate.title':'导入 OpenClaw 记忆',
1641
+ 'migrate.desc':'将 OpenClaw 内置的记忆数据和对话历史迁移到本插件中。导入过程使用智能去重,避免重复导入。',
1642
+ 'migrate.modes.title':'三种使用方式:',
1643
+ 'migrate.mode1.label':'\u2460 仅导入记忆(快速)',
1644
+ 'migrate.mode1.desc':'——点击「开始导入」即可快速迁移所有记忆片段和对话历史,不进行任务/技能生成。适合只需要原始数据的场景。',
1645
+ 'migrate.mode2.label':'\u2461 导入 + 生成任务与技能(较慢,串行)',
1646
+ 'migrate.mode2.desc':'——导入记忆后,在下方勾选「生成任务摘要」和/或「触发技能进化」,系统会逐个会话分析。由于每个会话都需要 LLM 处理,耗时较长。',
1647
+ 'migrate.mode3.label':'\u2462 先导入,随时再生成(灵活)',
1648
+ 'migrate.mode3.desc':'——先导入记忆,之后随时可以回来开启任务/技能生成。生成过程可以随时暂停,下次继续时会从上次停下的地方接着处理,已处理的会话会自动跳过。',
1649
+ 'migrate.config.warn':'需要配置',
1650
+ 'migrate.config.warn.desc':'请先在上方配置好 Embedding 模型和 Summarizer 模型,这两项是处理记忆所必需的。',
1651
+ 'migrate.sqlite.label':'记忆索引 (SQLite)',
1652
+ 'migrate.sessions.label':'对话历史',
1653
+ 'migrate.scan':'扫描数据源',
1654
+ 'migrate.start':'开始导入',
1655
+ 'migrate.scanning':'扫描中...',
1656
+ 'migrate.stat.stored':'已存储',
1657
+ 'migrate.stat.skipped':'已跳过',
1658
+ 'migrate.stat.merged':'已合并',
1659
+ 'migrate.stat.errors':'错误',
1660
+ 'migrate.done':'导入完成',
1661
+ 'migrate.done.summary':'共处理 {total} 条:{stored} 条存储,{skipped} 条跳过,{merged} 条合并,{errors} 条错误。',
1662
+ 'migrate.phase.sqlite':'正在导入记忆索引...',
1663
+ 'migrate.phase.sessions':'正在导入对话历史...',
1664
+ 'migrate.chunks':'条记忆',
1665
+ 'migrate.sessions.count':'个会话,{n} 条消息',
1666
+ 'migrate.nodata':'未找到可导入的 OpenClaw 数据。',
1667
+ 'migrate.running':'导入进行中...',
1668
+ 'migrate.error.running':'已有迁移任务正在进行。',
1669
+ 'migrate.stop':'\\u25A0 停止',
1670
+ 'migrate.stopping':'正在停止...',
1671
+ 'migrate.stopped':'导入已停止',
1672
+ 'migrate.stopped.hint':'导入已停止。你可以随时继续——已导入的内容会自动跳过。',
1673
+ 'migrate.resume':'继续导入',
1674
+ 'migrate.already.hint':'所有数据已在之前导入过,没有新内容需要处理。',
1675
+ 'pp.title':'\\u{1F9E0} 可选:生成任务与技能',
1676
+ 'pp.desc':'此步骤完全可选。上面的导入已经存储了原始记忆数据。在这里可以进一步分析已导入的对话,生成结构化的任务摘要或进化可复用的技能。处理过程是串行的(逐个会话),可能需要较长时间。你可以随时停止,下次继续时只会处理尚未完成的会话。',
1677
+ 'pp.tasks.label':'生成任务摘要',
1678
+ 'pp.tasks.hint':'将导入的消息按任务分组,为每个任务生成结构化摘要(标题、目标、步骤、结果),方便日后搜索和回忆。',
1679
+ 'pp.skills.label':'触发技能进化',
1680
+ 'pp.skills.hint':'分析已完成的任务,自动创建或升级可复用的技能(SKILL.md)。需要先启用任务摘要。由于需要 LLM 评估,耗时较长。',
1681
+ 'pp.start':'开始处理',
1682
+ 'pp.resume':'继续处理',
1683
+ 'pp.running':'正在处理',
1684
+ 'pp.stopped':'处理已停止,你可以随时继续。',
1685
+ 'pp.failed':'处理失败,请查看上方的错误提示。',
1686
+ 'pp.done':'任务与技能生成完成!',
1687
+ 'pp.select.warn':'请至少选择一个选项。',
1688
+ 'pp.skill.created':'技能已创建',
1689
+ 'pp.stat.tasks':'任务',
1690
+ 'pp.stat.skills':'技能',
1691
+ 'pp.stat.errors':'错误',
1692
+ 'pp.stat.skipped':'已跳过',
1693
+ 'pp.info.skipped':'已有 {n} 个会话处理过,自动跳过。',
1694
+ 'pp.info.pending':'正在处理 {n} 个会话...',
1695
+ 'pp.info.allDone':'所有会话均已处理过,无需重复处理。',
1696
+ 'pp.action.full':'任务+技能',
1697
+ 'pp.action.skillOnly':'仅技能(任务已存在)',
1698
+ 'card.imported':'OpenClaw 原生记忆',
1419
1699
  'skills.draft':'草稿',
1420
1700
  'skills.filter.active':'生效中',
1421
1701
  'skills.filter.draft':'草稿',
@@ -1561,12 +1841,14 @@ function switchView(view){
1561
1841
  const skillsView=document.getElementById('skillsView');
1562
1842
  const logsView=document.getElementById('logsView');
1563
1843
  const settingsView=document.getElementById('settingsView');
1844
+ const migrateView=document.getElementById('migrateView');
1564
1845
  feedWrap.classList.add('hide');
1565
1846
  analyticsView.classList.remove('show');
1566
1847
  tasksView.classList.remove('show');
1567
1848
  skillsView.classList.remove('show');
1568
1849
  logsView.classList.remove('show');
1569
1850
  settingsView.classList.remove('show');
1851
+ migrateView.classList.remove('show');
1570
1852
  if(view==='analytics'){
1571
1853
  analyticsView.classList.add('show');
1572
1854
  loadMetrics();
@@ -1582,6 +1864,9 @@ function switchView(view){
1582
1864
  } else if(view==='settings'){
1583
1865
  settingsView.classList.add('show');
1584
1866
  loadConfig();
1867
+ } else if(view==='import'){
1868
+ migrateView.classList.add('show');
1869
+ if(!window._migrateRunning) migrateScan();
1585
1870
  } else {
1586
1871
  feedWrap.classList.remove('hide');
1587
1872
  }
@@ -2183,15 +2468,10 @@ async function loadConfig(){
2183
2468
  document.getElementById('cfgSkillConfidence').value=sk.minConfidence||'';
2184
2469
  document.getElementById('cfgSkillMinChunks').value=sk.minChunksForEval||'';
2185
2470
 
2186
- const skSum=sk.summarizer||{};
2187
- document.getElementById('cfgSkillProviderDefault').textContent='\u2014 '+t('settings.skill.usemain');
2188
- document.getElementById('cfgSkillProvider').value=skSum.provider||'';
2189
- document.getElementById('cfgSkillModel').value=skSum.model||'';
2190
- document.getElementById('cfgSkillEndpoint').value=skSum.endpoint||'';
2191
- document.getElementById('cfgSkillApiKey').value=skSum.apiKey||'';
2192
- document.getElementById('cfgSkillTemp').value=skSum.temperature!=null?skSum.temperature:'';
2193
-
2194
2471
  document.getElementById('cfgViewerPort').value=cfg.viewerPort||'';
2472
+
2473
+ const tel=cfg.telemetry||{};
2474
+ document.getElementById('cfgTelemetryEnabled').checked=tel.enabled!==false;
2195
2475
  }catch(e){
2196
2476
  console.error('loadConfig error',e);
2197
2477
  }
@@ -2221,18 +2501,13 @@ async function saveConfig(){
2221
2501
  const mc=document.getElementById('cfgSkillConfidence').value.trim();if(mc) cfg.skillEvolution.minConfidence=Number(mc);
2222
2502
  const mk=document.getElementById('cfgSkillMinChunks').value.trim();if(mk) cfg.skillEvolution.minChunksForEval=Number(mk);
2223
2503
 
2224
- const skP=document.getElementById('cfgSkillProvider').value;
2225
- if(skP){
2226
- cfg.skillEvolution.summarizer={provider:skP};
2227
- const v=document.getElementById('cfgSkillModel').value.trim();if(v) cfg.skillEvolution.summarizer.model=v;
2228
- const e=document.getElementById('cfgSkillEndpoint').value.trim();if(e) cfg.skillEvolution.summarizer.endpoint=e;
2229
- const k=document.getElementById('cfgSkillApiKey').value.trim();if(k) cfg.skillEvolution.summarizer.apiKey=k;
2230
- const tp=document.getElementById('cfgSkillTemp').value.trim();if(tp!=='') cfg.skillEvolution.summarizer.temperature=Number(tp);
2231
- }
2232
-
2233
2504
  const vp=document.getElementById('cfgViewerPort').value.trim();
2234
2505
  if(vp) cfg.viewerPort=Number(vp);
2235
2506
 
2507
+ cfg.telemetry={
2508
+ enabled:document.getElementById('cfgTelemetryEnabled').checked
2509
+ };
2510
+
2236
2511
  try{
2237
2512
  const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
2238
2513
  if(!r.ok) throw new Error(await r.text());
@@ -2553,6 +2828,8 @@ function renderBreakdown(obj,containerId){
2553
2828
  /* ─── Data loading ─── */
2554
2829
  async function loadAll(){
2555
2830
  await Promise.all([loadStats(),loadMemories()]);
2831
+ checkMigrateStatus();
2832
+ connectPPSSE();
2556
2833
  }
2557
2834
 
2558
2835
  async function loadStats(){
@@ -2692,6 +2969,8 @@ function renderMemories(items){
2692
2969
  const ds=m.dedup_status||'active';
2693
2970
  const isInactive=ds==='duplicate'||ds==='merged';
2694
2971
  const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
2972
+ const isImported=sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-');
2973
+ const importBadge=isImported?'<span class="import-badge">\u{1F990} '+t('card.imported')+'</span>':'';
2695
2974
  let dedupInfo='';
2696
2975
  if(isInactive){
2697
2976
  const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
@@ -2716,7 +2995,7 @@ function renderMemories(items){
2716
2995
  }catch(e){}
2717
2996
  }
2718
2997
  return '<div class="memory-card'+(isInactive?' dedup-inactive':'')+'">'+
2719
- '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span>'+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
2998
+ '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span>'+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
2720
2999
  '<div class="card-summary">'+summary+'</div>'+
2721
3000
  dedupInfo+
2722
3001
  '<div class="card-content" id="content-'+id+'"><pre>'+content+'</pre></div>'+
@@ -2922,6 +3201,503 @@ async function clearAll(){
2922
3201
  else{toast(t('toast.clearfail'),'error')}
2923
3202
  }
2924
3203
 
3204
+ /* ─── Migration ─── */
3205
+ let migrateScanData=null;
3206
+ let migrateStats={stored:0,skipped:0,merged:0,errors:0};
3207
+
3208
+ async function migrateScan(){
3209
+ const btn=document.getElementById('migrateScanBtn');
3210
+ btn.disabled=true;
3211
+ btn.textContent=t('migrate.scanning');
3212
+ document.getElementById('migrateStartBtn').style.display='none';
3213
+ document.getElementById('migrateScanResult').style.display='none';
3214
+ document.getElementById('migrateConfigWarn').style.display='none';
3215
+ document.getElementById('migrateProgress').style.display='none';
3216
+ document.getElementById('migrateDone').style.display='none';
3217
+
3218
+ try{
3219
+ const r=await fetch('/api/migrate/scan');
3220
+ const d=await r.json();
3221
+ migrateScanData=d;
3222
+
3223
+ const sqliteTotal=d.sqliteFiles.reduce((s,f)=>s+f.chunks,0);
3224
+ document.getElementById('migrateSqliteCount').textContent=sqliteTotal;
3225
+ document.getElementById('migrateSqliteFiles').textContent=d.sqliteFiles.map(f=>f.file+' ('+f.chunks+')').join(', ')||'—';
3226
+ document.getElementById('migrateSessionCount').textContent=d.sessions.messages;
3227
+ document.getElementById('migrateSessionFiles').textContent=d.sessions.count+' '+t('migrate.sessions.count').replace('{n}',d.sessions.messages);
3228
+ document.getElementById('migrateScanResult').style.display='block';
3229
+
3230
+ if(!d.configReady){
3231
+ document.getElementById('migrateConfigWarn').style.display='block';
3232
+ const parts=[];
3233
+ if(!d.hasEmbedding) parts.push('Embedding');
3234
+ if(!d.hasSummarizer) parts.push('Summarizer');
3235
+ document.getElementById('migrateConfigWarn').querySelector('div:last-child').textContent=
3236
+ t('migrate.config.warn.desc')+' ('+parts.join(', ')+')';
3237
+ }
3238
+
3239
+ if(d.totalItems>0 && d.configReady){
3240
+ document.getElementById('migrateStartBtn').style.display='inline-flex';
3241
+ }
3242
+
3243
+ if(d.totalItems===0){
3244
+ document.getElementById('migrateStatus').textContent=t('migrate.nodata');
3245
+ }
3246
+
3247
+ if(d.hasImportedData){
3248
+ document.getElementById('postprocessSection').style.display='block';
3249
+ }
3250
+ }catch(e){
3251
+ toast('Scan failed: '+e.message,'error');
3252
+ }finally{
3253
+ btn.disabled=false;
3254
+ btn.textContent=t('migrate.scan');
3255
+ }
3256
+ }
3257
+
3258
+ function migrateStart(){
3259
+ if(!migrateScanData||!migrateScanData.configReady)return;
3260
+ if(!confirm(t('migrate.start')+'?'))return;
3261
+
3262
+ window._migrateRunning=true;
3263
+ _migrateStatusChecked=false;
3264
+ document.getElementById('migrateStartBtn').style.display='none';
3265
+ document.getElementById('migrateScanBtn').disabled=true;
3266
+ document.getElementById('migrateProgress').style.display='block';
3267
+ document.getElementById('migrateDone').style.display='none';
3268
+ document.getElementById('migrateLiveLog').innerHTML='';
3269
+ migrateStats={stored:0,skipped:0,merged:0,errors:0};
3270
+ updateMigrateStats();
3271
+
3272
+ document.getElementById('migrateStopBtn').disabled=false;
3273
+ document.getElementById('migrateBar').style.background='linear-gradient(90deg,#6366f1,#8b5cf6)';
3274
+ const body=JSON.stringify({sources:['sqlite','sessions']});
3275
+ connectMigrateSSE('/api/migrate/start','POST',body);
3276
+ }
3277
+
3278
+ async function migrateStop(){
3279
+ const btn=document.getElementById('migrateStopBtn');
3280
+ btn.disabled=true;
3281
+ btn.textContent=t('migrate.stopping');
3282
+ try{
3283
+ await fetch('/api/migrate/stop',{method:'POST'});
3284
+ }catch(e){
3285
+ toast('Stop failed: '+e.message,'error');
3286
+ btn.disabled=false;
3287
+ btn.textContent=t('migrate.stop');
3288
+ }
3289
+ }
3290
+
3291
+ function connectMigrateSSE(url,method,body){
3292
+ const opts={method:method||'GET'};
3293
+ if(body){opts.headers={'Content-Type':'application/json'};opts.body=body;}
3294
+ fetch(url,opts)
3295
+ .then(r=>{
3296
+ if(!r.ok){toast('Migration request failed: '+r.status,'error');onMigrateDone(false);return;}
3297
+ readSSEStream(r);
3298
+ })
3299
+ .catch(e=>{toast('Migration failed: '+e.message,'error');onMigrateDone(false);});
3300
+ }
3301
+
3302
+ function readSSEStream(r){
3303
+ const reader=r.body.getReader();
3304
+ const decoder=new TextDecoder();
3305
+ let buf='';
3306
+ let migrateDoneCalled=false;
3307
+ const NL=String.fromCharCode(10);
3308
+ function pump(){
3309
+ reader.read().then(({done,value})=>{
3310
+ if(done){if(!migrateDoneCalled)onMigrateDone(false);return;}
3311
+ buf+=decoder.decode(value,{stream:true});
3312
+ const lines=buf.split(NL);
3313
+ buf=lines.pop()||'';
3314
+ let evtType='';
3315
+ for(const line of lines){
3316
+ if(line.startsWith('event: ')){evtType=line.slice(7).trim();}
3317
+ else if(line.startsWith('data: ')){
3318
+ try{
3319
+ const data=JSON.parse(line.slice(6));
3320
+ if(evtType==='done'||evtType==='stopped') migrateDoneCalled=true;
3321
+ handleMigrateEvent(evtType,data);
3322
+ }catch{}
3323
+ }
3324
+ }
3325
+ pump();
3326
+ });
3327
+ }
3328
+ pump();
3329
+ }
3330
+
3331
+ var _migrateStatusChecked=false;
3332
+ async function checkMigrateStatus(){
3333
+ if(_migrateStatusChecked) return;
3334
+ _migrateStatusChecked=true;
3335
+ try{
3336
+ const r=await fetch('/api/migrate/status');
3337
+ if(!r.ok)return;
3338
+ const s=await r.json();
3339
+ if(s.running){
3340
+ window._migrateRunning=true;
3341
+ switchView('import');
3342
+ migrateStats={stored:s.stored,skipped:s.skipped,merged:s.merged,errors:s.errors};
3343
+ updateMigrateStats();
3344
+ document.getElementById('migrateProgress').style.display='block';
3345
+ document.getElementById('migrateStartBtn').style.display='none';
3346
+ document.getElementById('migrateScanBtn').disabled=true;
3347
+ document.getElementById('migrateStopBtn').disabled=false;
3348
+ const pct=s.total>0?Math.round((s.processed/s.total)*100):0;
3349
+ document.getElementById('migrateBar').style.width=pct+'%';
3350
+ document.getElementById('migrateCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';
3351
+ const label=s.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');
3352
+ document.getElementById('migratePhaseLabel').textContent=label;
3353
+ connectMigrateSSE('/api/migrate/stream','GET',null);
3354
+ }else if(s.done&&(s.stored>0||s.skipped>0||s.stopped)){
3355
+ migrateStats={stored:s.stored,skipped:s.skipped,merged:s.merged,errors:s.errors};
3356
+ updateMigrateStats();
3357
+ document.getElementById('migrateProgress').style.display='block';
3358
+ const pct=s.total>0?Math.round((s.processed/s.total)*100):0;
3359
+ document.getElementById('migrateBar').style.width=pct+'%';
3360
+ document.getElementById('migrateCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';
3361
+ onMigrateDone(!!s.stopped,true);
3362
+ }
3363
+ }catch(e){console.log('checkMigrateStatus error',e);}
3364
+ }
3365
+
3366
+ function handleMigrateEvent(evtType,data){
3367
+ if(evtType==='phase'){
3368
+ const label=data.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');
3369
+ document.getElementById('migratePhaseLabel').textContent=label;
3370
+ }
3371
+ else if(evtType==='progress'){
3372
+ document.getElementById('migrateCounter').textContent=data.processed+' / '+data.total;
3373
+ }
3374
+ else if(evtType==='item'){
3375
+ if(data.status==='stored')migrateStats.stored++;
3376
+ else if(data.status==='skipped'||data.status==='duplicate')migrateStats.skipped++;
3377
+ else if(data.status==='merged')migrateStats.merged++;
3378
+ else if(data.status==='error')migrateStats.errors++;
3379
+ updateMigrateStats();
3380
+
3381
+ const pct=data.total>0?Math.round((data.index/data.total)*100):0;
3382
+ document.getElementById('migrateBar').style.width=pct+'%';
3383
+ document.getElementById('migrateCounter').textContent=data.index+' / '+data.total+' ('+pct+'%)';
3384
+
3385
+ appendMigrateLogItem(data);
3386
+ }
3387
+ else if(evtType==='error'){
3388
+ migrateStats.errors++;
3389
+ updateMigrateStats();
3390
+ appendMigrateLogItem({status:'error',preview:data.error||data.file,source:data.file});
3391
+ }
3392
+ else if(evtType==='summary'){
3393
+ document.getElementById('migrateBar').style.width='100%';
3394
+ }
3395
+ else if(evtType==='done'){
3396
+ onMigrateDone(false);
3397
+ }
3398
+ else if(evtType==='stopped'){
3399
+ onMigrateDone(true);
3400
+ }
3401
+ else if(evtType==='state'){
3402
+ migrateStats={stored:data.stored||0,skipped:data.skipped||0,merged:data.merged||0,errors:data.errors||0};
3403
+ updateMigrateStats();
3404
+ const pct=data.total>0?Math.round((data.processed/data.total)*100):0;
3405
+ document.getElementById('migrateBar').style.width=pct+'%';
3406
+ document.getElementById('migrateCounter').textContent=data.processed+' / '+data.total+' ('+pct+'%)';
3407
+ if(data.phase){
3408
+ const label=data.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');
3409
+ document.getElementById('migratePhaseLabel').textContent=label;
3410
+ }
3411
+ }
3412
+ }
3413
+
3414
+ function updateMigrateStats(){
3415
+ document.getElementById('migrateStatStored').textContent=migrateStats.stored;
3416
+ document.getElementById('migrateStatSkipped').textContent=migrateStats.skipped;
3417
+ document.getElementById('migrateStatMerged').textContent=migrateStats.merged;
3418
+ document.getElementById('migrateStatErrors').textContent=migrateStats.errors;
3419
+ }
3420
+
3421
+ function appendMigrateLogItem(data){
3422
+ const log=document.getElementById('migrateLiveLog');
3423
+ const icons={stored:'\\u2705',skipped:'\\u23ED',merged:'\\u{1F500}',error:'\\u274C',duplicate:'\\u23ED'};
3424
+ const statusClass=data.status==='duplicate'?'skipped':data.status;
3425
+ const el=document.createElement('div');
3426
+ el.className='migrate-log-item';
3427
+ el.innerHTML=
3428
+ '<div class="log-icon '+statusClass+'">'+( icons[data.status]||'\\u2022')+'</div>'+
3429
+ '<div class="log-body">'+
3430
+ '<div class="log-preview">'+esc(data.preview||'')+'</div>'+
3431
+ '<div class="log-meta">'+
3432
+ '<span class="tag '+statusClass+'">'+(data.status||'').toUpperCase()+'</span>'+
3433
+ (data.source?'<span>'+esc(data.source)+'</span>':'')+
3434
+ (data.role?'<span>'+data.role+'</span>':'')+
3435
+ (data.summary?'<span style="opacity:.7">'+esc(data.summary)+'</span>':'')+
3436
+ '</div>'+
3437
+ '</div>';
3438
+ log.appendChild(el);
3439
+ log.scrollTop=log.scrollHeight;
3440
+ }
3441
+
3442
+ function onMigrateDone(wasStopped,skipReload){
3443
+ window._migrateRunning=false;
3444
+ document.getElementById('migrateScanBtn').disabled=false;
3445
+ document.getElementById('migrateStopBtn').disabled=true;
3446
+ document.getElementById('migrateStopBtn').textContent=t('migrate.stop');
3447
+ const doneEl=document.getElementById('migrateDone');
3448
+ doneEl.style.display='block';
3449
+ const total=migrateStats.stored+migrateStats.skipped+migrateStats.merged+migrateStats.errors;
3450
+ const allSkipped=migrateStats.stored===0&&migrateStats.merged===0&&migrateStats.skipped>0&&migrateStats.errors===0;
3451
+ const tmpl=t('migrate.done.summary');
3452
+ let summaryText=tmpl
3453
+ .replace('{total}',total)
3454
+ .replace('{stored}',migrateStats.stored)
3455
+ .replace('{skipped}',migrateStats.skipped)
3456
+ .replace('{merged}',migrateStats.merged)
3457
+ .replace('{errors}',migrateStats.errors);
3458
+ if(wasStopped){
3459
+ summaryText+=' '+t('migrate.stopped.hint');
3460
+ doneEl.querySelector('.settings-section').style.background='rgba(245,158,11,.04)';
3461
+ doneEl.querySelector('.settings-section').style.borderColor='rgba(245,158,11,.2)';
3462
+ doneEl.querySelector('div div:first-child').innerHTML='\u23F8 <span>'+t('migrate.stopped')+'</span>';
3463
+ doneEl.querySelector('div div:first-child').style.color='#f59e0b';
3464
+ document.getElementById('migrateStartBtn').style.display='inline-flex';
3465
+ document.getElementById('migrateStartBtn').textContent=t('migrate.resume');
3466
+ }else if(allSkipped){
3467
+ summaryText+=' '+t('migrate.already.hint');
3468
+ doneEl.querySelector('.settings-section').style.background='rgba(59,130,246,.04)';
3469
+ doneEl.querySelector('.settings-section').style.borderColor='rgba(59,130,246,.2)';
3470
+ doneEl.querySelector('div div:first-child').innerHTML='\u{1F4AD} <span>'+t('migrate.done')+'</span>';
3471
+ doneEl.querySelector('div div:first-child').style.color='#3b82f6';
3472
+ }else{
3473
+ doneEl.querySelector('.settings-section').style.background='rgba(34,197,94,.04)';
3474
+ doneEl.querySelector('.settings-section').style.borderColor='rgba(34,197,94,.2)';
3475
+ doneEl.querySelector('div div:first-child').innerHTML='\u2705 <span>'+t('migrate.done')+'</span>';
3476
+ doneEl.querySelector('div div:first-child').style.color='#22c55e';
3477
+ }
3478
+ document.getElementById('migrateDoneSummary').textContent=summaryText;
3479
+ if(!wasStopped){
3480
+ document.getElementById('migrateBar').style.width='100%';
3481
+ document.getElementById('migrateBar').style.background=allSkipped?'linear-gradient(90deg,#3b82f6,#60a5fa)':'linear-gradient(90deg,#22c55e,#16a34a)';
3482
+ }else{
3483
+ document.getElementById('migrateBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';
3484
+ }
3485
+ fetch('/api/migrate/scan').then(r=>r.json()).then(d=>{
3486
+ if(d.hasImportedData) document.getElementById('postprocessSection').style.display='block';
3487
+ }).catch(()=>{});
3488
+ if(!skipReload) loadAll();
3489
+ }
3490
+
3491
+ /* ─── Post-processing: tasks & skills ─── */
3492
+
3493
+ var ppStats={tasks:0,skills:0,errors:0,skipped:0};
3494
+ window._ppRunning=false;
3495
+
3496
+ function ppStart(){
3497
+ var enableTasks=document.getElementById('ppEnableTasks').checked;
3498
+ var enableSkills=document.getElementById('ppEnableSkills').checked;
3499
+ if(!enableTasks&&!enableSkills){toast(t('pp.select.warn'),'error');return;}
3500
+
3501
+ window._ppRunning=true;
3502
+ _ppSSEConnected=false;
3503
+ ppStats={tasks:0,skills:0,errors:0,skipped:0};
3504
+ document.getElementById('ppStartBtn').style.display='none';
3505
+ document.getElementById('ppStopBtn').style.display='inline-flex';
3506
+ document.getElementById('ppStopBtn').disabled=false;
3507
+ document.getElementById('ppStopBtn').textContent=t('migrate.stop');
3508
+ document.getElementById('ppProgress').style.display='block';
3509
+ document.getElementById('ppDone').style.display='none';
3510
+ document.getElementById('ppBar').style.width='0%';
3511
+ document.getElementById('ppBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';
3512
+ document.getElementById('ppPhaseLabel').textContent=t('pp.running');
3513
+ document.getElementById('ppCounter').textContent='';
3514
+ document.getElementById('ppLiveLog').innerHTML='';
3515
+ updatePPStats();
3516
+
3517
+ var body=JSON.stringify({enableTasks:enableTasks,enableSkills:enableSkills});
3518
+ fetch('/api/migrate/postprocess',{method:'POST',headers:{'Content-Type':'application/json'},body:body})
3519
+ .then(function(r){
3520
+ if(!r.ok){
3521
+ r.json().then(function(j){toast(j.error||('Postprocess failed: '+r.status),'error');}).catch(function(){toast('Postprocess failed: '+r.status,'error');});
3522
+ ppDone(false,true);
3523
+ return;
3524
+ }
3525
+ readPPStream(r.body.getReader());
3526
+ })
3527
+ .catch(function(e){toast('Postprocess failed: '+e.message,'error');ppDone(false,true);});
3528
+ }
3529
+
3530
+ function updatePPStats(){
3531
+ document.getElementById('ppStatTasks').textContent=ppStats.tasks;
3532
+ document.getElementById('ppStatSkills').textContent=ppStats.skills;
3533
+ document.getElementById('ppStatErrors').textContent=ppStats.errors;
3534
+ document.getElementById('ppStatSkipped').textContent=ppStats.skipped;
3535
+ }
3536
+
3537
+ function appendPPLogItem(data){
3538
+ var log=document.getElementById('ppLiveLog');
3539
+ var el=document.createElement('div');
3540
+ el.style.cssText='display:flex;align-items:flex-start;gap:8px;padding:6px 12px;border-bottom:1px solid var(--border)';
3541
+ var icon='\\u2022';var color='var(--text-muted)';
3542
+ if(data.step==='done'){icon='\\u2705';color='#22c55e';}
3543
+ else if(data.step==='error'){icon='\\u274C';color='#ef4444';}
3544
+ else if(data.step==='processing'){icon='\\u23F3';color='#f59e0b';}
3545
+ else if(data.step==='skipped'){icon='\\u23ED';color='#3b82f6';}
3546
+ else if(data.step==='skill'){icon='\\u{1F9E0}';color='#8b5cf6';}
3547
+ var label=data.taskTitle||data.session||data.title||'';
3548
+ if(label.length>60)label=label.slice(0,57)+'...';
3549
+ el.innerHTML='<span style="color:'+color+';min-width:18px">'+icon+'</span>'+
3550
+ '<span style="flex:1;color:var(--text-sec)">'+esc(label)+'</span>'+
3551
+ '<span style="color:var(--text-muted);font-size:10px">'+(data.index||'')+' / '+(data.total||'')+'</span>';
3552
+ if(data.error) el.innerHTML+='<span style="color:#ef4444;font-size:10px">'+esc(data.error)+'</span>';
3553
+ log.appendChild(el);
3554
+ log.scrollTop=log.scrollHeight;
3555
+ }
3556
+
3557
+ function readPPStream(reader){
3558
+ var NL=String.fromCharCode(10);
3559
+ var dec=new TextDecoder();
3560
+ var buf='';
3561
+ var ppDoneCalled=false;
3562
+ function pump(){
3563
+ reader.read().then(function(result){
3564
+ if(result.done){if(!ppDoneCalled)ppDone(false);return;}
3565
+ buf+=dec.decode(result.value,{stream:true});
3566
+ var lines=buf.split(NL);
3567
+ buf=lines.pop()||'';
3568
+ var evtType='';
3569
+ for(var i=0;i<lines.length;i++){
3570
+ var line=lines[i];
3571
+ if(line.startsWith('event: '))evtType=line.slice(7).trim();
3572
+ else if(line.startsWith('data: ')&&evtType){
3573
+ try{
3574
+ if(evtType==='done'||evtType==='stopped')ppDoneCalled=true;
3575
+ handlePPEvent(evtType,JSON.parse(line.slice(6)));
3576
+ }catch(e){}
3577
+ evtType='';
3578
+ }
3579
+ }
3580
+ pump();
3581
+ }).catch(function(){if(!ppDoneCalled)ppDone(false);});
3582
+ }
3583
+ pump();
3584
+ }
3585
+
3586
+ var _ppSSEConnected=false;
3587
+ function connectPPSSE(){
3588
+ if(_ppSSEConnected) return;
3589
+ _ppSSEConnected=true;
3590
+ fetch('/api/migrate/postprocess/status').then(function(r){return r.json();}).then(function(s){
3591
+ if(s.running){
3592
+ window._ppRunning=true;
3593
+ document.getElementById('postprocessSection').style.display='block';
3594
+ document.getElementById('ppStartBtn').style.display='none';
3595
+ document.getElementById('ppStopBtn').style.display='inline-flex';
3596
+ document.getElementById('ppStopBtn').disabled=false;
3597
+ document.getElementById('ppStopBtn').textContent=t('migrate.stop');
3598
+ document.getElementById('ppProgress').style.display='block';
3599
+ document.getElementById('ppDone').style.display='none';
3600
+ ppStats={tasks:s.tasksCreated||0,skills:s.skillsCreated||0,errors:s.errors||0,skipped:0};
3601
+ updatePPStats();
3602
+ var pct=s.total>0?Math.round((s.processed/s.total)*100):0;
3603
+ document.getElementById('ppBar').style.width=pct+'%';
3604
+ document.getElementById('ppCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';
3605
+ document.getElementById('ppPhaseLabel').textContent=t('pp.running');
3606
+ fetch('/api/migrate/postprocess/stream',{method:'GET'}).then(function(r){
3607
+ if(r.ok&&r.body)readPPStream(r.body.getReader());
3608
+ }).catch(function(){});
3609
+ }else if(s.done){
3610
+ document.getElementById('postprocessSection').style.display='block';
3611
+ ppStats={tasks:s.tasksCreated||0,skills:s.skillsCreated||0,errors:s.errors||0,skipped:0};
3612
+ updatePPStats();
3613
+ document.getElementById('ppProgress').style.display='block';
3614
+ var pct2=s.total>0?Math.round((s.processed/s.total)*100):0;
3615
+ document.getElementById('ppBar').style.width=pct2+'%';
3616
+ document.getElementById('ppCounter').textContent=s.processed+' / '+s.total+' ('+pct2+'%)';
3617
+ ppDone(!!s.stopped,false,true);
3618
+ }
3619
+ }).catch(function(){});
3620
+ }
3621
+
3622
+ function handlePPEvent(evtType,data){
3623
+ if(evtType==='progress'){
3624
+ var pct=data.total>0?Math.round((data.processed/data.total)*100):0;
3625
+ document.getElementById('ppBar').style.width=pct+'%';
3626
+ document.getElementById('ppCounter').textContent=data.processed+' / '+data.total+' ('+pct+'%)';
3627
+ }else if(evtType==='info'){
3628
+ if(data.alreadyProcessed>0){
3629
+ ppStats.skipped=data.alreadyProcessed;
3630
+ updatePPStats();
3631
+ appendPPLogItem({step:'skipped',session:t('pp.info.skipped').replace('{n}',data.alreadyProcessed),index:'',total:''});
3632
+ }
3633
+ if(data.pending===0){
3634
+ appendPPLogItem({step:'done',session:t('pp.info.allDone'),index:'',total:''});
3635
+ }else{
3636
+ document.getElementById('ppPhaseLabel').textContent=t('pp.info.pending').replace('{n}',data.pending);
3637
+ }
3638
+ }else if(evtType==='item'){
3639
+ var label=data.session||'';
3640
+ if(label.length>40)label=label.slice(0,37)+'...';
3641
+ if(data.step==='processing'){
3642
+ var actionLabel=data.action==='skill-only'?t('pp.action.skillOnly'):t('pp.action.full');
3643
+ document.getElementById('ppPhaseLabel').textContent=t('pp.running')+' — '+actionLabel+' — '+label;
3644
+ }
3645
+ if(data.step==='done'){
3646
+ if(data.action==='skill-only'){
3647
+ ppStats.skills++;
3648
+ }else{
3649
+ ppStats.tasks++;
3650
+ }
3651
+ updatePPStats();
3652
+ }else if(data.step==='error'){
3653
+ ppStats.errors++;
3654
+ updatePPStats();
3655
+ }
3656
+ appendPPLogItem(data);
3657
+ }else if(evtType==='skill'){
3658
+ ppStats.skills++;
3659
+ updatePPStats();
3660
+ appendPPLogItem({step:'skill',title:data.title,index:'',total:''});
3661
+ }else if(evtType==='done'){
3662
+ ppDone(false);
3663
+ }else if(evtType==='stopped'){
3664
+ ppDone(true);
3665
+ }
3666
+ }
3667
+
3668
+ function ppStop(){
3669
+ document.getElementById('ppStopBtn').disabled=true;
3670
+ document.getElementById('ppStopBtn').textContent=t('migrate.stopping');
3671
+ fetch('/api/migrate/postprocess/stop',{method:'POST'}).catch(function(){});
3672
+ }
3673
+
3674
+ function ppDone(wasStopped,wasFailed,skipReload){
3675
+ window._ppRunning=false;
3676
+ document.getElementById('ppStopBtn').style.display='none';
3677
+ document.getElementById('ppStartBtn').style.display='inline-flex';
3678
+ document.getElementById('ppStartBtn').textContent=wasStopped?t('pp.resume'):t('pp.start');
3679
+ var doneEl=document.getElementById('ppDone');
3680
+ doneEl.style.display='block';
3681
+ if(wasFailed){
3682
+ doneEl.style.background='rgba(239,68,68,.06)';
3683
+ doneEl.style.color='#ef4444';
3684
+ doneEl.textContent=t('pp.failed')||'Processing failed — check error above';
3685
+ document.getElementById('ppBar').style.background='linear-gradient(90deg,#ef4444,#dc2626)';
3686
+ }else if(wasStopped){
3687
+ doneEl.style.background='rgba(245,158,11,.06)';
3688
+ doneEl.style.color='#f59e0b';
3689
+ doneEl.textContent=t('pp.stopped');
3690
+ document.getElementById('ppBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';
3691
+ }else{
3692
+ doneEl.style.background='rgba(34,197,94,.06)';
3693
+ doneEl.style.color='#22c55e';
3694
+ doneEl.textContent=t('pp.done')+' ('+t('pp.stat.tasks')+': '+ppStats.tasks+', '+t('pp.stat.skills')+': '+ppStats.skills+')';
3695
+ document.getElementById('ppBar').style.width='100%';
3696
+ document.getElementById('ppBar').style.background='linear-gradient(90deg,#22c55e,#16a34a)';
3697
+ }
3698
+ if(!skipReload) loadAll();
3699
+ }
3700
+
2925
3701
  /* ─── Toast ─── */
2926
3702
  function toast(msg,type='info'){
2927
3703
  const c=document.getElementById('toasts');