@jungjaehoon/mama-os 0.18.2 → 0.19.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 (171) hide show
  1. package/dist/agent/agent-loop.d.ts +25 -0
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +67 -14
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  6. package/dist/agent/code-act/host-bridge.js +98 -0
  7. package/dist/agent/code-act/host-bridge.js.map +1 -1
  8. package/dist/agent/code-act/type-definition-generator.d.ts.map +1 -1
  9. package/dist/agent/code-act/type-definition-generator.js +0 -1
  10. package/dist/agent/code-act/type-definition-generator.js.map +1 -1
  11. package/dist/agent/gateway-tool-executor.d.ts +36 -1
  12. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  13. package/dist/agent/gateway-tool-executor.js +938 -54
  14. package/dist/agent/gateway-tool-executor.js.map +1 -1
  15. package/dist/agent/gateway-tools.md +9 -0
  16. package/dist/agent/managed-agent-runtime-sync.d.ts +36 -0
  17. package/dist/agent/managed-agent-runtime-sync.d.ts.map +1 -0
  18. package/dist/agent/managed-agent-runtime-sync.js +207 -0
  19. package/dist/agent/managed-agent-runtime-sync.js.map +1 -0
  20. package/dist/agent/managed-agent-validation.d.ts +4 -0
  21. package/dist/agent/managed-agent-validation.d.ts.map +1 -0
  22. package/dist/agent/managed-agent-validation.js +84 -0
  23. package/dist/agent/managed-agent-validation.js.map +1 -0
  24. package/dist/agent/os-agent-capabilities.md +400 -0
  25. package/dist/agent/skill-loader.d.ts +2 -0
  26. package/dist/agent/skill-loader.d.ts.map +1 -1
  27. package/dist/agent/skill-loader.js +28 -0
  28. package/dist/agent/skill-loader.js.map +1 -1
  29. package/dist/agent/tool-registry.d.ts.map +1 -1
  30. package/dist/agent/tool-registry.js +66 -0
  31. package/dist/agent/tool-registry.js.map +1 -1
  32. package/dist/agent/types.d.ts +2 -1
  33. package/dist/agent/types.d.ts.map +1 -1
  34. package/dist/agent/types.js.map +1 -1
  35. package/dist/api/agent-handler.d.ts +34 -0
  36. package/dist/api/agent-handler.d.ts.map +1 -0
  37. package/dist/api/agent-handler.js +216 -0
  38. package/dist/api/agent-handler.js.map +1 -0
  39. package/dist/api/graph-api-types.d.ts +4 -0
  40. package/dist/api/graph-api-types.d.ts.map +1 -1
  41. package/dist/api/graph-api.d.ts +2 -2
  42. package/dist/api/graph-api.d.ts.map +1 -1
  43. package/dist/api/graph-api.js +480 -51
  44. package/dist/api/graph-api.js.map +1 -1
  45. package/dist/api/index.d.ts.map +1 -1
  46. package/dist/api/index.js +4 -0
  47. package/dist/api/index.js.map +1 -1
  48. package/dist/api/token-handler.d.ts +1 -0
  49. package/dist/api/token-handler.d.ts.map +1 -1
  50. package/dist/api/token-handler.js +4 -3
  51. package/dist/api/token-handler.js.map +1 -1
  52. package/dist/api/ui-command-handler.d.ts +48 -0
  53. package/dist/api/ui-command-handler.d.ts.map +1 -0
  54. package/dist/api/ui-command-handler.js +160 -0
  55. package/dist/api/ui-command-handler.js.map +1 -0
  56. package/dist/cli/commands/start.d.ts.map +1 -1
  57. package/dist/cli/commands/start.js +127 -1
  58. package/dist/cli/commands/start.js.map +1 -1
  59. package/dist/cli/config/config-manager.d.ts.map +1 -1
  60. package/dist/cli/config/config-manager.js +16 -31
  61. package/dist/cli/config/config-manager.js.map +1 -1
  62. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  63. package/dist/cli/runtime/agent-loop-init.js +31 -7
  64. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  65. package/dist/cli/runtime/api-routes-init.d.ts +3 -0
  66. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  67. package/dist/cli/runtime/api-routes-init.js +283 -34
  68. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  69. package/dist/cli/runtime/gateway-init.d.ts +2 -1
  70. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  71. package/dist/cli/runtime/gateway-init.js +5 -1
  72. package/dist/cli/runtime/gateway-init.js.map +1 -1
  73. package/dist/connectors/framework/raw-store.d.ts +4 -0
  74. package/dist/connectors/framework/raw-store.d.ts.map +1 -1
  75. package/dist/connectors/framework/raw-store.js +33 -10
  76. package/dist/connectors/framework/raw-store.js.map +1 -1
  77. package/dist/db/agent-store.d.ts +115 -0
  78. package/dist/db/agent-store.d.ts.map +1 -0
  79. package/dist/db/agent-store.js +248 -0
  80. package/dist/db/agent-store.js.map +1 -0
  81. package/dist/db/migrations/agent-activity-validation-columns.d.ts +3 -0
  82. package/dist/db/migrations/agent-activity-validation-columns.d.ts.map +1 -0
  83. package/dist/db/migrations/agent-activity-validation-columns.js +22 -0
  84. package/dist/db/migrations/agent-activity-validation-columns.js.map +1 -0
  85. package/dist/db/migrations/agent-metrics-response-avg.d.ts +3 -0
  86. package/dist/db/migrations/agent-metrics-response-avg.d.ts.map +1 -0
  87. package/dist/db/migrations/agent-metrics-response-avg.js +19 -0
  88. package/dist/db/migrations/agent-metrics-response-avg.js.map +1 -0
  89. package/dist/db/migrations/agent-store-tables.d.ts +3 -0
  90. package/dist/db/migrations/agent-store-tables.d.ts.map +1 -0
  91. package/dist/db/migrations/agent-store-tables.js +59 -0
  92. package/dist/db/migrations/agent-store-tables.js.map +1 -0
  93. package/dist/db/migrations/token-usage-agent-version.d.ts +3 -0
  94. package/dist/db/migrations/token-usage-agent-version.d.ts.map +1 -0
  95. package/dist/db/migrations/token-usage-agent-version.js +16 -0
  96. package/dist/db/migrations/token-usage-agent-version.js.map +1 -0
  97. package/dist/db/migrations/validation-session-tables.d.ts +3 -0
  98. package/dist/db/migrations/validation-session-tables.d.ts.map +1 -0
  99. package/dist/db/migrations/validation-session-tables.js +59 -0
  100. package/dist/db/migrations/validation-session-tables.js.map +1 -0
  101. package/dist/gateways/message-router.d.ts +10 -0
  102. package/dist/gateways/message-router.d.ts.map +1 -1
  103. package/dist/gateways/message-router.js +188 -14
  104. package/dist/gateways/message-router.js.map +1 -1
  105. package/dist/gateways/types.d.ts +1 -1
  106. package/dist/gateways/types.d.ts.map +1 -1
  107. package/dist/multi-agent/agent-process-manager.js +1 -1
  108. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  109. package/dist/multi-agent/conductor-persona.d.ts +13 -0
  110. package/dist/multi-agent/conductor-persona.d.ts.map +1 -0
  111. package/dist/multi-agent/conductor-persona.js +157 -0
  112. package/dist/multi-agent/conductor-persona.js.map +1 -0
  113. package/dist/multi-agent/dashboard-agent-persona.d.ts +1 -1
  114. package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
  115. package/dist/multi-agent/dashboard-agent-persona.js +7 -3
  116. package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
  117. package/dist/multi-agent/delegation-manager.d.ts +5 -0
  118. package/dist/multi-agent/delegation-manager.d.ts.map +1 -1
  119. package/dist/multi-agent/delegation-manager.js +37 -0
  120. package/dist/multi-agent/delegation-manager.js.map +1 -1
  121. package/dist/multi-agent/ultrawork.d.ts +3 -0
  122. package/dist/multi-agent/ultrawork.d.ts.map +1 -1
  123. package/dist/multi-agent/ultrawork.js +9 -0
  124. package/dist/multi-agent/ultrawork.js.map +1 -1
  125. package/dist/validation/session-service.d.ts +72 -0
  126. package/dist/validation/session-service.d.ts.map +1 -0
  127. package/dist/validation/session-service.js +298 -0
  128. package/dist/validation/session-service.js.map +1 -0
  129. package/dist/validation/store.d.ts +25 -0
  130. package/dist/validation/store.d.ts.map +1 -0
  131. package/dist/validation/store.js +200 -0
  132. package/dist/validation/store.js.map +1 -0
  133. package/dist/validation/types.d.ts +119 -0
  134. package/dist/validation/types.d.ts.map +1 -0
  135. package/dist/validation/types.js +57 -0
  136. package/dist/validation/types.js.map +1 -0
  137. package/package.json +3 -3
  138. package/public/viewer/js/modules/agents.js +1148 -0
  139. package/public/viewer/js/modules/chat.js +20 -11
  140. package/public/viewer/js/modules/connector-feed.js +35 -0
  141. package/public/viewer/js/modules/dashboard.js +49 -0
  142. package/public/viewer/js/modules/memory.js +32 -0
  143. package/public/viewer/js/modules/settings.js +34 -79
  144. package/public/viewer/js/modules/wiki.js +59 -4
  145. package/public/viewer/js/utils/api.js +70 -0
  146. package/public/viewer/js/utils/dom.js +3 -0
  147. package/public/viewer/js/utils/ui-commands.js +93 -0
  148. package/public/viewer/log-viewer.html +2 -2
  149. package/public/viewer/src/modules/agents.ts +1299 -0
  150. package/public/viewer/src/modules/chat.ts +23 -14
  151. package/public/viewer/src/modules/connector-feed.ts +35 -0
  152. package/public/viewer/src/modules/dashboard.ts +50 -0
  153. package/public/viewer/src/modules/memory.ts +31 -0
  154. package/public/viewer/src/modules/settings.ts +36 -96
  155. package/public/viewer/src/modules/wiki.ts +73 -6
  156. package/public/viewer/src/types/global.d.ts +0 -9
  157. package/public/viewer/src/utils/api.ts +156 -2
  158. package/public/viewer/src/utils/dom.ts +6 -1
  159. package/public/viewer/src/utils/ui-commands.ts +118 -0
  160. package/public/viewer/viewer.css +105 -10
  161. package/public/viewer/viewer.html +1868 -777
  162. package/scripts/generate-gateway-tools.ts +5 -1
  163. package/public/viewer/js/modules/playground.js +0 -148
  164. package/public/viewer/js/modules/skills.js +0 -451
  165. package/public/viewer/src/modules/playground.ts +0 -173
  166. package/public/viewer/src/modules/skills.ts +0 -491
  167. package/templates/playgrounds/cron-workflow-lab.html +0 -1601
  168. package/templates/playgrounds/mama-log-viewer.html +0 -1341
  169. package/templates/playgrounds/skill-lab-playground.html +0 -1625
  170. package/templates/playgrounds/wave-visualizer.html +0 -694
  171. package/templates/skills/playground.md +0 -197
@@ -1,1341 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ko">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>MAMA OS Log Viewer</title>
7
- <style>
8
- *{margin:0;padding:0;box-sizing:border-box}
9
- :root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#8b949e;--accent:#58a6ff;--error:#f85149;--warn:#d29922;--info:#58a6ff;--success:#3fb950;--debug:#8b949e;--pin:#f0883e}
10
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;overflow:hidden}
11
- .header{display:flex;align-items:center;gap:10px;padding:8px 14px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}
12
- .header h1{font-size:13px;font-weight:600;color:var(--accent);white-space:nowrap}
13
- .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
14
- .status-dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
15
- .status-dot.disconnected{background:var(--error)}
16
- .status-dot.connecting{background:var(--warn);animation:pulse 1s infinite}
17
- .status-dot.ws-connected{background:#39d353;box-shadow:0 0 8px #39d353}
18
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
19
- @keyframes fadeNew{from{background:rgba(59,185,80,.18)}to{background:transparent}}
20
- .status-text{font-size:11px;color:var(--muted);min-width:100px}
21
- .rate-badge{font-size:10px;color:var(--muted);background:var(--bg);padding:2px 7px;border-radius:10px;border:1px solid var(--border)}
22
- .hdr-right{margin-left:auto;display:flex;gap:5px;align-items:center}
23
- .btn{padding:3px 10px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:5px;color:var(--text);cursor:pointer;transition:all .15s;font-family:inherit;white-space:nowrap}
24
- .btn:hover{border-color:var(--accent);color:var(--accent)}
25
- .btn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
26
- .btn.paused{background:var(--warn);border-color:var(--warn);color:#000}
27
- .btn.ws-mode{background:#39d353;border-color:#39d353;color:#000}
28
- .sel-ctrl{background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:3px 5px;color:var(--text);font-size:11px;font-family:inherit;outline:none}
29
- .toolbar{display:flex;align-items:center;gap:6px;padding:5px 14px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap}
30
- .search-box{flex:1;min-width:120px;display:flex;align-items:center;gap:5px;background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:3px 7px}
31
- .search-box input{flex:1;background:none;border:none;color:var(--text);font-size:11px;font-family:'SF Mono','Cascadia Code',Consolas,monospace;outline:none}
32
- .search-box input::placeholder{color:var(--muted)}
33
- .search-box .count{font-size:10px;color:var(--muted);white-space:nowrap}
34
- .search-mode-btn{padding:1px 5px;font-size:9px;border:1px solid var(--border);background:transparent;color:var(--muted);border-radius:3px;cursor:pointer;font-family:monospace}
35
- .search-mode-btn.active{color:var(--accent);border-color:var(--accent);background:rgba(88,166,255,.1)}
36
- .levels{display:flex;gap:2px}
37
- .lvl{padding:2px 6px;font-size:10px;font-weight:700;border-radius:3px;cursor:pointer;border:1px solid transparent;user-select:none;transition:all .12s}
38
- .lvl.off{opacity:.3}
39
- .lvl-error{color:var(--error);border-color:var(--error)}.lvl-error.on{background:var(--error);color:#fff}
40
- .lvl-warn{color:var(--warn);border-color:var(--warn)}.lvl-warn.on{background:var(--warn);color:#000}
41
- .lvl-info{color:var(--accent);border-color:var(--accent)}.lvl-info.on{background:var(--accent);color:#fff}
42
- .lvl-debug{color:var(--muted);border-color:var(--muted)}.lvl-debug.on{background:var(--muted);color:#000}
43
- .source-panel{display:none;padding:6px 14px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;overflow-x:auto}
44
- .source-panel.active{display:block}
45
- .source-panel-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}
46
- .source-panel-header span{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
47
- .source-panel-header button{font-size:10px;color:var(--accent);background:none;border:none;cursor:pointer}
48
- .source-panel-header button:hover{text-decoration:underline}
49
- .source-group{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px}
50
- .source-group-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;width:100%;margin-top:4px;margin-bottom:1px}
51
- .source-chip{padding:2px 8px;font-size:10px;border:1px solid var(--border);background:transparent;color:var(--muted);border-radius:10px;cursor:pointer;transition:all .15s;white-space:nowrap}
52
- .source-chip:hover{border-color:var(--accent);color:var(--text)}
53
- .source-chip.active{background:rgba(88,166,255,.15);border-color:var(--accent);color:var(--accent)}
54
- .source-chip.excluded{background:rgba(248,81,73,.1);border-color:var(--error);color:var(--error);text-decoration:line-through;opacity:.6}
55
- .source-chip .chip-count{font-size:9px;opacity:.6;margin-left:3px}
56
- .src-toggle{font-size:10px;padding:2px 8px;border:1px solid var(--border);background:transparent;color:var(--muted);border-radius:5px;cursor:pointer}
57
- .src-toggle:hover{color:var(--text);border-color:var(--accent)}
58
- .src-toggle.open{color:var(--accent);border-color:var(--accent)}
59
-
60
- /* Stats dashboard */
61
- .stats-dashboard{display:none;padding:10px 14px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;overflow:hidden;transition:all .2s}
62
- .stats-dashboard.active{display:block}
63
- .stats-dash-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
64
- .stats-dash-header h3{font-size:11px;color:var(--accent);font-weight:600}
65
- .stats-dash-header button{font-size:10px;color:var(--muted);background:none;border:none;cursor:pointer}
66
- .stats-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
67
- .stats-card{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px}
68
- .stats-card h4{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
69
- .stats-card-counts{display:flex;gap:10px;margin-bottom:6px}
70
- .stats-count{display:flex;flex-direction:column;align-items:center;gap:2px}
71
- .stats-count .num{font-size:16px;font-weight:700}
72
- .stats-count .lbl{font-size:9px;color:var(--muted)}
73
- .bar-chart{display:flex;flex-direction:column;gap:2px}
74
- .bar-row{display:flex;align-items:center;gap:5px;font-size:9px}
75
- .bar-label{min-width:70px;color:var(--muted);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
76
- .bar-fill{height:10px;border-radius:2px;transition:width .3s;min-width:2px}
77
- .bar-value{color:var(--muted);min-width:25px}
78
- .histogram{display:flex;align-items:flex-end;gap:1px;height:40px}
79
- .histogram .hist-bar{flex:1;background:var(--accent);border-radius:1px 1px 0 0;min-width:2px;transition:height .3s;cursor:pointer;position:relative}
80
- .histogram .hist-bar:hover{opacity:.8}
81
- .histogram .hist-bar .hist-tip{display:none;position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--surface);border:1px solid var(--border);border-radius:3px;padding:2px 5px;font-size:9px;white-space:nowrap;color:var(--text);z-index:10}
82
- .histogram .hist-bar:hover .hist-tip{display:block}
83
-
84
- /* Pins panel */
85
- .pins-panel{display:none;max-height:120px;overflow-y:auto;background:rgba(240,136,62,.05);border-bottom:1px solid var(--pin);flex-shrink:0}
86
- .pins-panel.active{display:block}
87
- .pins-header{display:flex;align-items:center;gap:6px;padding:4px 14px;font-size:10px;color:var(--pin)}
88
- .pins-header span{font-weight:600}
89
- .pins-header button{font-size:9px;color:var(--muted);background:none;border:none;cursor:pointer;margin-left:auto}
90
- .pins-header button:hover{color:var(--pin)}
91
- .pin-line{padding:2px 14px 2px 28px;font-family:'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;font-size:10.5px;line-height:1.4;cursor:pointer;display:flex;gap:6px;border-bottom:1px solid rgba(240,136,62,.1)}
92
- .pin-line:hover{background:rgba(240,136,62,.08)}
93
- .pin-line .pin-src{color:var(--pin);font-weight:700;min-width:80px}
94
- .pin-line .pin-msg{color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
95
- .pin-line .pin-rm{color:var(--muted);font-size:9px;cursor:pointer;opacity:0;transition:opacity .15s}
96
- .pin-line:hover .pin-rm{opacity:1}
97
-
98
- .stats-bar{display:flex;align-items:center;gap:12px;padding:3px 14px;font-size:10px;color:var(--muted);background:var(--bg);border-bottom:1px solid var(--border);flex-shrink:0;overflow-x:auto}
99
- .stats-bar strong{color:var(--text)}
100
- .mod-stat{display:flex;align-items:center;gap:3px}
101
- .mod-stat .dot-sm{width:5px;height:5px;border-radius:50%;flex-shrink:0}
102
- .selection-bar{display:none;align-items:center;gap:8px;padding:6px 14px;background:var(--accent);color:#fff;font-size:12px;flex-shrink:0}
103
- .selection-bar.active{display:flex}
104
- .selection-bar .sel-count{font-weight:600}
105
- .selection-bar button{padding:3px 10px;font-size:11px;border:1px solid rgba(255,255,255,.3);background:rgba(255,255,255,.1);color:#fff;border-radius:6px;cursor:pointer}
106
- .selection-bar button:hover{background:rgba(255,255,255,.25)}
107
- .selection-bar .send-btn{background:#fff;color:var(--accent);font-weight:600;border:none}
108
- .selection-bar .send-btn:hover{background:#e6edf3}
109
-
110
- /* Log area + minimap */
111
- .log-wrap{flex:1;overflow:hidden;display:flex;flex-direction:column}
112
- .log-container{flex:1;display:flex;overflow:hidden;position:relative}
113
- .log-area{flex:1;overflow-y:auto;padding:2px 0;font-family:'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;font-size:11.5px;line-height:1.5}
114
- .log-area::-webkit-scrollbar{width:7px}
115
- .log-area::-webkit-scrollbar-track{background:var(--bg)}
116
- .log-area::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
117
-
118
- /* Minimap */
119
- .minimap{width:18px;background:var(--surface);border-left:1px solid var(--border);flex-shrink:0;position:relative;cursor:pointer;overflow:hidden}
120
- .minimap-marker{position:absolute;width:100%;min-height:2px;border-radius:1px;opacity:.7;transition:opacity .1s}
121
- .minimap-marker:hover{opacity:1}
122
- .minimap-marker.error{background:var(--error)}
123
- .minimap-marker.warn{background:var(--warn)}
124
- .minimap-marker.pin{background:var(--pin)}
125
- .minimap-viewport{position:absolute;width:100%;background:rgba(88,166,255,.15);border:1px solid rgba(88,166,255,.3);border-radius:1px;pointer-events:none;transition:top .1s,height .1s}
126
-
127
- .ll{padding:1px 14px;display:flex;gap:6px;cursor:pointer;user-select:none;transition:background .08s;border-bottom:1px solid rgba(48,54,61,.2)}
128
- .ll:hover{background:rgba(88,166,255,.05)}
129
- .ll.selected{background:rgba(88,166,255,.12);border-left:3px solid var(--accent);padding-left:11px}
130
- .ll.new-line{animation:fadeNew .8s}
131
- .ll.level-error{background:rgba(248,81,73,.06);border-left:2px solid var(--error)}
132
- .ll.level-error.selected{background:rgba(248,81,73,.15);border-left-color:var(--error)}
133
- .ll.level-warn{background:rgba(210,153,34,.05);border-left:2px solid var(--warn)}
134
- .ll.level-warn.selected{background:rgba(210,153,34,.12);border-left-color:var(--warn)}
135
- .ll.hidden{display:none}
136
- .ll.graphhandler{opacity:.35}.ll.graphhandler:hover{opacity:.7}
137
- .ll.pinned .pin-icon{display:inline}
138
- .pin-icon{display:none;color:var(--pin);font-size:10px;cursor:pointer;flex-shrink:0}
139
- .ln{color:var(--muted);min-width:36px;text-align:right;user-select:none;opacity:.4;font-size:10px}
140
- .ltime{color:var(--muted);min-width:70px;font-size:10px;opacity:.6}
141
- .lmod{min-width:90px;font-weight:700;font-size:10.5px;white-space:nowrap}
142
- .llvl{min-width:32px;font-size:9px;font-weight:700;text-transform:uppercase;opacity:.8}
143
- .llvl.error{color:var(--error)}.llvl.warn{color:var(--warn)}.llvl.info{color:var(--accent)}.llvl.debug{color:var(--muted)}.llvl.success{color:var(--success)}
144
- .lmsg{color:var(--text);flex:1;word-break:break-all;white-space:pre-wrap}
145
- .lmsg mark{background:var(--warn);color:#000;border-radius:2px;padding:0 1px}
146
- .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--muted);gap:8px}
147
- .empty .ico{font-size:36px;opacity:.25}
148
- .empty p{font-size:11px}
149
- .empty kbd{background:var(--surface);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-size:10px}
150
- .prompt-panel{border-top:1px solid var(--border);background:var(--surface);flex-shrink:0}
151
- .prompt-hdr{display:flex;align-items:center;justify-content:space-between;padding:6px 14px;cursor:pointer;user-select:none}
152
- .prompt-hdr h3{font-size:11px;color:var(--accent);font-weight:600}
153
- .prompt-hdr .arrow{font-size:10px;color:var(--muted);transition:transform .2s}
154
- .prompt-hdr .arrow.open{transform:rotate(180deg)}
155
- .prompt-body{padding:0 14px 8px;display:none}
156
- .prompt-body.open{display:block}
157
- .presets{display:flex;gap:3px;flex-wrap:wrap;margin-bottom:6px}
158
- .preset{padding:2px 8px;font-size:10px;background:var(--bg);border:1px solid var(--border);border-radius:10px;color:var(--muted);cursor:pointer;transition:all .12s;font-family:inherit}
159
- .preset:hover{border-color:var(--accent);color:var(--accent)}
160
- .prompt-row{display:flex;gap:6px}
161
- .prompt-input{flex:1;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:6px 10px;color:var(--text);font-size:11px;font-family:inherit;resize:none;outline:none;min-height:40px;max-height:80px}
162
- .prompt-input:focus{border-color:var(--accent)}
163
- .prompt-input::placeholder{color:var(--muted)}
164
- .prompt-actions{display:flex;flex-direction:column;gap:3px}
165
- .send-btn-prompt{padding:5px 12px;background:var(--accent);color:#fff;border:none;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
166
- .send-btn-prompt:hover{opacity:.9}
167
- .copy-btn-prompt{padding:5px 12px;background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:5px;font-size:11px;cursor:pointer;font-family:inherit}
168
- .copy-btn-prompt:hover{color:var(--text);border-color:var(--muted)}
169
-
170
- /* Export dropdown */
171
- .export-dropdown{position:relative;display:inline-block}
172
- .export-menu{display:none;position:absolute;top:100%;right:0;margin-top:4px;background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:4px 0;z-index:100;min-width:160px;box-shadow:0 4px 12px rgba(0,0,0,.4)}
173
- .export-menu.open{display:block}
174
- .export-item{display:block;width:100%;padding:5px 12px;font-size:11px;color:var(--text);background:none;border:none;cursor:pointer;text-align:left;font-family:inherit}
175
- .export-item:hover{background:rgba(88,166,255,.1);color:var(--accent)}
176
- .export-item .exp-desc{display:block;font-size:9px;color:var(--muted)}
177
- </style>
178
- </head>
179
- <body>
180
- <div class="header">
181
- <div class="status-dot disconnected" id="statusDot"></div>
182
- <h1>MAMA OS Log Viewer</h1>
183
- <span class="status-text" id="statusText">Starting...</span>
184
- <span class="rate-badge" id="rateBadge" title="Lines per second">0 l/s</span>
185
- <div class="hdr-right">
186
- <button class="btn" id="liveBtn" title="Toggle live (L)">&#9654; Live</button>
187
- <button class="btn" id="wsBtn" title="WebSocket mode (W)">WS</button>
188
- <select class="sel-ctrl" id="intervalSel" title="Poll interval">
189
- <option value="1000">1s</option>
190
- <option value="2000" selected>2s</option>
191
- <option value="3000">3s</option>
192
- <option value="5000">5s</option>
193
- </select>
194
- <input type="number" class="sel-ctrl" id="tailN" value="500" min="50" max="5000" style="width:50px;text-align:center" title="Tail lines">
195
- <button class="btn active" id="autoBtn" title="Auto-scroll (A)">&#11015;</button>
196
- <button class="btn" id="statsBtn" title="Stats dashboard (D)">&#128202;</button>
197
- <div class="export-dropdown">
198
- <button class="btn" id="exportBtn" title="Export logs (E)">&#128229;</button>
199
- <div class="export-menu" id="exportMenu">
200
- <button class="export-item" id="exportLog">Export as .log<span class="exp-desc">All visible lines (plain text)</span></button>
201
- <button class="export-item" id="exportSelLog">Export selected as .log<span class="exp-desc">Selected lines only</span></button>
202
- <button class="export-item" id="exportJson">Export as .json<span class="exp-desc">Structured data (all visible)</span></button>
203
- <button class="export-item" id="exportSelJson">Export selected as .json<span class="exp-desc">Structured data (selected)</span></button>
204
- </div>
205
- </div>
206
- <button class="btn" id="clearBtn" title="Clear (C)">&#128465;</button>
207
- </div>
208
- </div>
209
- <div class="toolbar">
210
- <div class="search-box">
211
- <span style="opacity:.5">&#128269;</span>
212
- <input id="searchInput" placeholder="Filter... (/ or Ctrl+F)" />
213
- <button class="search-mode-btn" id="regexToggle" title="Toggle regex mode">.*</button>
214
- <span class="count" id="searchCount"></span>
215
- </div>
216
- <div class="levels">
217
- <span class="lvl lvl-error on" data-l="error">ERR</span>
218
- <span class="lvl lvl-warn on" data-l="warn">WRN</span>
219
- <span class="lvl lvl-info on" data-l="info">INF</span>
220
- <span class="lvl lvl-debug on" data-l="debug">DBG</span>
221
- </div>
222
- <button class="btn active" id="graphToggle" style="font-size:10px" title="GraphHandler toggle">GH</button>
223
- <button class="btn" id="pinToggle" style="font-size:10px" title="Show pinned lines">&#128204;</button>
224
- <button class="src-toggle" id="srcToggle" title="Source/Channel panel">Sources</button>
225
- </div>
226
- <div class="source-panel" id="sourcePanel">
227
- <div class="source-panel-header">
228
- <span>Sources</span>
229
- <div style="flex:1"></div>
230
- <button id="showAllBtn">Show All</button>
231
- <button id="soloBtn">Solo Selected</button>
232
- </div>
233
- <div class="source-group" id="sourceChips"></div>
234
- <div class="source-group-label">Channels / Lanes</div>
235
- <div class="source-group" id="channelChips"></div>
236
- </div>
237
- <div class="stats-dashboard" id="statsDashboard">
238
- <div class="stats-dash-header">
239
- <h3>&#128202; Log Statistics</h3>
240
- <button id="closeStats">&#10005; Close</button>
241
- </div>
242
- <div class="stats-grid">
243
- <div class="stats-card">
244
- <h4>Level Counts</h4>
245
- <div class="stats-card-counts" id="levelCounts"></div>
246
- </div>
247
- <div class="stats-card">
248
- <h4>Top Sources</h4>
249
- <div class="bar-chart" id="sourceChart"></div>
250
- </div>
251
- <div class="stats-card">
252
- <h4>Volume (time)</h4>
253
- <div class="histogram" id="volumeHistogram"></div>
254
- </div>
255
- </div>
256
- </div>
257
- <div class="pins-panel" id="pinsPanel">
258
- <div class="pins-header">
259
- <span>&#128204; Pinned Lines</span>
260
- <span id="pinCount">0</span>
261
- <button id="clearPins">Clear All</button>
262
- </div>
263
- <div id="pinsList"></div>
264
- </div>
265
- <div class="stats-bar">
266
- <span>Total: <strong id="stTotal">0</strong></span>
267
- <span>Shown: <strong id="stShown">0</strong></span>
268
- <span id="modStats"></span>
269
- <span id="fileInfo" style="margin-left:auto"></span>
270
- </div>
271
- <div class="selection-bar" id="selectionBar">
272
- <span class="sel-count" id="selBarCount">0</span> lines selected
273
- <div style="flex:1"></div>
274
- <button id="selPin" title="Pin selected lines">&#128204; Pin</button>
275
- <button id="selCopy">Copy</button>
276
- <button class="send-btn" id="selSend">Send to Chat</button>
277
- <button id="selClear">Clear</button>
278
- </div>
279
- <div class="log-wrap">
280
- <div class="log-container">
281
- <div class="log-area" id="logArea">
282
- <div class="empty" id="emptyState">
283
- <div class="ico">&#128203;</div>
284
- <p>daemon.log</p>
285
- <p><kbd>L</kbd> Live <kbd>W</kbd> WebSocket <kbd>/</kbd> Search <kbd>A</kbd> Auto-scroll <kbd>P</kbd> Pause <kbd>D</kbd> Stats</p>
286
- </div>
287
- </div>
288
- <div class="minimap" id="minimap">
289
- <div class="minimap-viewport" id="minimapViewport"></div>
290
- </div>
291
- </div>
292
- </div>
293
- <div class="prompt-panel">
294
- <div class="prompt-hdr" id="promptHdr">
295
- <h3>&#128172; Send to Chat</h3>
296
- <span class="arrow open" id="promptArrow">&#9660;</span>
297
- </div>
298
- <div class="prompt-body open" id="promptBody">
299
- <div class="presets">
300
- <button class="preset" data-p="errors">&#128308; 에러 분석</button>
301
- <button class="preset" data-p="status">&#128202; 시스템 상태</button>
302
- <button class="preset" data-p="slow">&#128012; 느린 작업</button>
303
- <button class="preset" data-p="recent">&#128221; 최근 요약</button>
304
- </div>
305
- <div class="prompt-row">
306
- <textarea class="prompt-input" id="promptInput" placeholder="루나에게 요청..."></textarea>
307
- <div class="prompt-actions">
308
- <button class="send-btn-prompt" id="promptSend">&#128228; Send</button>
309
- <button class="copy-btn-prompt" id="promptCopy">&#128203; Copy</button>
310
- </div>
311
- </div>
312
- </div>
313
- </div>
314
- <script>
315
- (function(){
316
- 'use strict';
317
-
318
- const MAX_LINES = 5000;
319
- const INITIAL_TAIL = 500;
320
- const RATE_HISTORY_SIZE = 10;
321
- const NEW_LINE_ANIM_MS = 900;
322
- const AUTO_SCROLL_THRESHOLD = 50;
323
- const MAX_INCREMENTAL_LINES = 200;
324
- const PINS_STORAGE_KEY = 'mama-log-viewer-pins';
325
- const WS_RECONNECT_DELAY = 3000;
326
- const WS_MAX_RETRIES = 5;
327
- const HISTOGRAM_BUCKETS = 30;
328
-
329
- // Use same-origin by default so custom host/IP + non-default port keeps working.
330
- const API_BASE = location.protocol === 'file:' ? 'http://localhost:3847' : '';
331
- const WS_URL = location.protocol === 'file:'
332
- ? 'ws://localhost:3847/ws'
333
- : (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws';
334
- const POST_MSG_ORIGIN = location.protocol === 'file:' ? '*' : location.origin;
335
-
336
- const S = {
337
- logs: [], autoScroll: true, selected: new Set(), lastClickIdx: -1,
338
- live: false, paused: false, pollTimer: null, lastMtime: 0, lastTotal: 0, lastHadTotal: false,
339
- initialized: false, prevCount: 0, rateHistory: [],
340
- levels: {error: true, warn: true, info: true, debug: true}, showGraph: true,
341
- sourceCounts: {}, channelCounts: {},
342
- activeSources: null, excludedSources: new Set(),
343
- activeChannels: null, excludedChannels: new Set(), sourcePanelOpen: false,
344
- regexMode: false,
345
- wsMode: false, ws: null, wsRetries: 0,
346
- pins: new Set(),
347
- pinsPanelOpen: false,
348
- statsDashOpen: false,
349
- exportOpen: false
350
- };
351
-
352
- const $ = (id) => document.getElementById(id);
353
- const logArea = $('logArea');
354
- let emptyState = $('emptyState');
355
-
356
- function esc(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
357
- function sourceClass(src) { return src ? 'src-' + src.toLowerCase().replace(/[^a-z]/g, '') : ''; }
358
- function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
359
-
360
- const SRC_COLORS = {
361
- agentloop: '#d2a8ff', lane: '#7ee787', contextinjector: '#79c0ff', sessionpool: '#ffa657',
362
- mama: '#ff7b72', graphhandler: '#484f58', slackgateway: '#e9b0ff', slackratelimiter: '#e9b0ff',
363
- slackmultibotmanager: '#e9b0ff', multiagentslack: '#e9b0ff', discord: '#58a6ff',
364
- pluginloader: '#d29922', embeddinghttp: '#3fb950', orchestrator: '#f778ba',
365
- systemreminder: '#a5d6ff', backgroundtaskmanager: '#ffd700', messagerouter: '#d29922',
366
- cron: '#f0883e', daemon: '#c9d1d9', gateway: '#f778ba', api: '#58a6ff',
367
- wsproxy: '#39d353', persistentcliprocess: '#bc8cff', agentprocessmanager: '#bc8cff',
368
- swarmcoordinator: '#f778ba', waveengine: '#f778ba'
369
- };
370
-
371
- function srcColor(source) {
372
- if (!source) return '#8b949e';
373
- return SRC_COLORS[source.toLowerCase().replace(/[^a-z]/g, '')] || '#8b949e';
374
- }
375
-
376
- function classifyLevel(text) {
377
- if (/\[ERROR\]|\[ERR\]/i.test(text)) return 'error';
378
- if (/\[WARN\]|\[WRN\]/i.test(text)) return 'warn';
379
- if (/\[DEBUG\]|\[DBG\]/i.test(text)) return 'debug';
380
- const u = text.toUpperCase();
381
- if (/\bERROR\b|\bFAIL(ED|URE)?\b|\bECONNREFUSED\b|\bEXCEPTION\b|\bFATAL\b|\bENOENT\b/.test(u)) return 'error';
382
- if (/\bWARN(ING)?\b|\bDEPRECATED\b|\bTIMEOUT\b|\bRETRY\b/.test(u)) return 'warn';
383
- if (/\u2713|\bSUCCESS\b|\bCONNECTED\b|\bREADY\b|\bSTARTED\b/.test(u)) return 'success';
384
- if (/\bDEBUG\b|\bTRACE\b|\bVERBOSE\b/.test(u)) return 'debug';
385
- return 'info';
386
- }
387
-
388
- function extractSource(text) {
389
- const m = text.match(/\[([^\]]{2,40})\]/);
390
- return m ? m[1] : null;
391
- }
392
-
393
- function extractChannel(text) {
394
- const m = text.match(/lane=session:([^\s]+)/);
395
- if (m) { const p = m[1].split(':'); return p.length >= 2 ? p[0] + ':' + p[1] : m[1]; }
396
- const ch = text.match(/channel=([A-Z0-9]+)/);
397
- if (ch) return 'ch:' + ch[1];
398
- if (text.indexOf('viewer:') >= 0) {
399
- const vm = text.match(/(viewer:[^\s\u2192]+)/);
400
- if (vm) return vm[1];
401
- }
402
- return null;
403
- }
404
-
405
- function extractTime(text) {
406
- const m = text.match(/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\s]*)/);
407
- if (m) return m[1].replace(/^.*T/, '').replace(/\.\d+.*/, '');
408
- return '';
409
- }
410
-
411
- function parseLine(raw, idx) {
412
- const source = extractSource(raw);
413
- const level = classifyLevel(raw);
414
- const time = extractTime(raw);
415
- const channel = extractChannel(raw);
416
- let msg = raw;
417
- if (source) msg = raw.replace('[' + source + ']', '').trim();
418
- const timePrefix = raw.match(/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\s]*\s*/);
419
- if (timePrefix) msg = raw.substring(timePrefix[0].length);
420
- if (source && msg.indexOf('[' + source + ']') === 0) msg = msg.replace('[' + source + ']', '').trim();
421
-
422
- if (source) S.sourceCounts[source] = (S.sourceCounts[source] || 0) + 1;
423
- if (channel) S.channelCounts[channel] = (S.channelCounts[channel] || 0) + 1;
424
-
425
- return { n: idx, raw, source, srcCls: sourceClass(source), level, time, channel, msg, isNew: false };
426
- }
427
-
428
- function shouldShow(entry) {
429
- if (!S.showGraph && entry.source === 'GraphHandler') return false;
430
- if (!S.levels[entry.level] && entry.level !== 'success') return false;
431
- if (S.excludedSources.has(entry.source)) return false;
432
- if (S.activeSources && !S.activeSources.has(entry.source)) return false;
433
- if (S.excludedChannels.size > 0 && entry.channel) {
434
- for (const ec of S.excludedChannels) { if (entry.channel.indexOf(ec) >= 0) return false; }
435
- }
436
- if (S.activeChannels) {
437
- if (!entry.channel) return false;
438
- let match = false;
439
- for (const ac of S.activeChannels) { if (entry.raw.indexOf(ac) >= 0) { match = true; break; } }
440
- if (!match) return false;
441
- }
442
- return true;
443
- }
444
-
445
- function buildSearchRegex(q) {
446
- if (!q) return null;
447
- try {
448
- if (S.regexMode) return new RegExp(q, 'gi');
449
- return new RegExp(escapeRegex(q), 'gi');
450
- } catch (e) { return null; }
451
- }
452
-
453
- function highlightMsg(msgHtml, searchRe) {
454
- if (!searchRe) return msgHtml;
455
- try {
456
- const q = $('searchInput').value;
457
- let pattern;
458
- if (S.regexMode) {
459
- pattern = q;
460
- } else {
461
- pattern = escapeRegex(esc(q));
462
- }
463
- return msgHtml.replace(new RegExp('(' + pattern + ')', 'gi'), '<mark>$1</mark>');
464
- } catch (e) { return msgHtml; }
465
- }
466
-
467
- function renderLine(entry, searchRe) {
468
- const div = document.createElement('div');
469
- let cls = 'll';
470
- if (S.selected.has(entry.n)) cls += ' selected';
471
- if (entry.isNew) cls += ' new-line';
472
- if (entry.level === 'error' || entry.level === 'warn') cls += ' level-' + entry.level;
473
- if (entry.source === 'GraphHandler') cls += ' graphhandler';
474
- if (entry.srcCls) cls += ' ' + entry.srcCls;
475
- if (S.pins.has(entry.n)) cls += ' pinned';
476
- div.className = cls;
477
- div.setAttribute('data-idx', entry.n);
478
-
479
- const msgHtml = highlightMsg(esc(entry.msg), searchRe);
480
-
481
- div.innerHTML =
482
- '<span class="pin-icon" title="Pinned">&#128204;</span>' +
483
- '<span class="ln">' + entry.n + '</span>' +
484
- (entry.time ? '<span class="ltime">' + esc(entry.time) + '</span>' : '') +
485
- (entry.source ? '<span class="lmod" style="color:' + srcColor(entry.source) + '">' + esc(entry.source) + '</span>' : '<span class="lmod" style="color:#484f58">\u2014</span>') +
486
- '<span class="llvl ' + entry.level + '">' + entry.level.substring(0, 3) + '</span>' +
487
- '<span class="lmsg">' + msgHtml + '</span>';
488
-
489
- if (entry.isNew) setTimeout(() => { entry.isNew = false; }, NEW_LINE_ANIM_MS);
490
- return div;
491
- }
492
-
493
- function applyFilters() {
494
- const q = $('searchInput').value.trim();
495
- const searchRe = buildSearchRegex(q);
496
- let visible = 0;
497
- const rows = logArea.querySelectorAll('.ll');
498
- for (let i = 0; i < rows.length; i++) {
499
- const idx = parseInt(rows[i].getAttribute('data-idx'));
500
- const entry = S.logs[idx];
501
- if (!entry) { rows[i].classList.add('hidden'); continue; }
502
- let show = shouldShow(entry);
503
- if (show && searchRe) { searchRe.lastIndex = 0; show = searchRe.test(entry.raw); }
504
- rows[i].classList.toggle('hidden', !show);
505
- if (show) visible++;
506
- }
507
- $('stShown').textContent = visible;
508
- $('searchCount').textContent = q ? visible + ' matches' : '';
509
- updateMinimap();
510
- }
511
-
512
- function updateModStats() {
513
- const counts = {};
514
- S.logs.forEach((l) => { const k = l.source || 'other'; counts[k] = (counts[k] || 0) + 1; });
515
- const parts = [];
516
- const sorted = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
517
- sorted.slice(0, 8).forEach((k) => {
518
- parts.push('<span class="mod-stat"><span class="dot-sm" style="background:' + srcColor(k) + '"></span>' + k + ':' + counts[k] + '</span>');
519
- });
520
- $('modStats').innerHTML = parts.join(' ');
521
- }
522
-
523
- function addLines(rawLines, isInitial) {
524
- if (!rawLines.length) return;
525
- if (emptyState && emptyState.parentNode) emptyState.remove();
526
-
527
- const q = $('searchInput').value.trim();
528
- const searchRe = buildSearchRegex(q);
529
-
530
- const base = S.logs.length;
531
- const frag = document.createDocumentFragment();
532
-
533
- for (let i = 0; i < rawLines.length; i++) {
534
- if (!rawLines[i].trim()) continue;
535
- const entry = parseLine(rawLines[i], base + i);
536
- if (!isInitial) entry.isNew = true;
537
- S.logs.push(entry);
538
- const div = renderLine(entry, searchRe);
539
- let show = shouldShow(entry);
540
- if (show && searchRe) { searchRe.lastIndex = 0; show = searchRe.test(entry.raw); }
541
- if (!show) div.classList.add('hidden');
542
- frag.appendChild(div);
543
- }
544
-
545
- logArea.appendChild(frag);
546
-
547
- while (logArea.children.length > MAX_LINES) logArea.removeChild(logArea.firstChild);
548
- const excess = S.logs.length - MAX_LINES;
549
- if (excess > 0) {
550
- S.logs.splice(0, excess);
551
- const shiftSet = (s) => {
552
- const updated = new Set();
553
- s.forEach((idx) => { const ni = idx - excess; if (ni >= 0) updated.add(ni); });
554
- return updated;
555
- };
556
- S.pins = shiftSet(S.pins);
557
- S.selected = shiftSet(S.selected);
558
- if (S.lastClickIdx >= 0) {
559
- S.lastClickIdx = Math.max(-1, S.lastClickIdx - excess);
560
- }
561
- }
562
-
563
- for (let j = 0; j < S.logs.length; j++) S.logs[j].n = j;
564
- const rows = logArea.querySelectorAll('.ll');
565
- for (let k = 0; k < rows.length; k++) rows[k].setAttribute('data-idx', k);
566
-
567
- $('stTotal').textContent = S.logs.length;
568
- const vis = logArea.querySelectorAll('.ll:not(.hidden)').length;
569
- $('stShown').textContent = vis;
570
- updateModStats();
571
- if (S.sourcePanelOpen) renderSourceChips();
572
- if (S.autoScroll) logArea.scrollTop = logArea.scrollHeight;
573
- updateMinimap();
574
- if (S.statsDashOpen) updateStatsDashboard();
575
- }
576
-
577
- function diffTailLines(snapshotLines) {
578
- if (!Array.isArray(snapshotLines) || snapshotLines.length === 0) return [];
579
- const existingLen = S.logs.length;
580
- const maxOverlap = Math.min(existingLen, snapshotLines.length);
581
- for (let overlap = maxOverlap; overlap > 0; overlap--) {
582
- // Fast pre-check: first candidate line
583
- if (S.logs[existingLen - overlap].raw !== snapshotLines[0]) continue;
584
- let same = true;
585
- for (let i = 1; i < overlap; i++) {
586
- if (S.logs[existingLen - overlap + i].raw !== snapshotLines[i]) {
587
- same = false;
588
- break;
589
- }
590
- }
591
- if (same) return snapshotLines.slice(overlap);
592
- }
593
- return snapshotLines;
594
- }
595
-
596
- function updateRate() {
597
- const now = S.logs.length;
598
- const diff = now - S.prevCount;
599
- S.prevCount = now;
600
- S.rateHistory.push(diff);
601
- if (S.rateHistory.length > RATE_HISTORY_SIZE) S.rateHistory.shift();
602
- const avg = S.rateHistory.reduce((a, b) => a + b, 0) / S.rateHistory.length;
603
- const interval = parseInt($('intervalSel').value) / 1000;
604
- const perSec = (avg / interval).toFixed(1);
605
- $('rateBadge').textContent = perSec + ' l/s';
606
- }
607
-
608
- function setStatus(state, text) {
609
- $('statusDot').className = 'status-dot ' + state;
610
- $('statusText').textContent = text;
611
- }
612
-
613
- /* ===== Polling ===== */
614
- function fetchLogs() {
615
- if (S.paused) return;
616
- const tail = parseInt($('tailN').value) || 500;
617
- let url = API_BASE + '/api/logs/daemon?tail=' + (S.initialized ? tail : INITIAL_TAIL);
618
- if (S.lastMtime > 0) url += '&since=' + S.lastMtime;
619
-
620
- fetch(url).then((r) => {
621
- if (!r.ok) throw new Error('HTTP ' + r.status);
622
- return r.json();
623
- }).then((data) => {
624
- const hasTotal = Number.isFinite(data.total);
625
- const displayTotal = hasTotal ? data.total : (data.truncated ? '~' + data.lines.length : data.lines.length);
626
- const modeLabel = S.wsMode ? 'WS+Poll' : 'Polling';
627
- setStatus('connected', modeLabel + ' (' + (parseInt($('intervalSel').value) / 1000) + 's) \u2022 ' + displayTotal + ' total');
628
- const bytes = data.totalBytes || data.fileSize;
629
- if (bytes) $('fileInfo').textContent = (bytes / 1024 / 1024).toFixed(1) + 'MB';
630
-
631
- if (!S.initialized) {
632
- addLines(data.lines, true);
633
- S.lastMtime = data.mtime;
634
- S.lastTotal = hasTotal ? data.total : null;
635
- S.lastHadTotal = hasTotal;
636
- S.initialized = true;
637
- } else if (data.mtime > S.lastMtime) {
638
- if (hasTotal && S.lastHadTotal && Number.isFinite(S.lastTotal)) {
639
- const newCount = data.total - S.lastTotal;
640
- if (newCount > 0 && newCount <= MAX_INCREMENTAL_LINES) {
641
- addLines(data.lines.slice(-newCount), false);
642
- } else if (newCount > MAX_INCREMENTAL_LINES) {
643
- S.logs = []; S.sourceCounts = {}; S.channelCounts = {};
644
- logArea.innerHTML = '';
645
- addLines(data.lines, true);
646
- }
647
- } else {
648
- const incremental = diffTailLines(data.lines);
649
- if (incremental.length > 0 && incremental.length <= MAX_INCREMENTAL_LINES) {
650
- addLines(incremental, false);
651
- } else if (incremental.length > MAX_INCREMENTAL_LINES) {
652
- S.logs = []; S.sourceCounts = {}; S.channelCounts = {};
653
- logArea.innerHTML = '';
654
- addLines(data.lines, true);
655
- }
656
- }
657
- S.lastMtime = data.mtime;
658
- S.lastTotal = hasTotal ? data.total : null;
659
- S.lastHadTotal = hasTotal;
660
- }
661
- updateRate();
662
- }).catch((e) => {
663
- setStatus('disconnected', 'Error: ' + e.message);
664
- });
665
- }
666
-
667
- function startPolling() {
668
- S.live = true;
669
- $('liveBtn').className = 'btn active';
670
- $('liveBtn').innerHTML = '&#9208; Pause';
671
- setStatus('connecting', 'Connecting...');
672
- fetchLogs();
673
- S.pollTimer = setInterval(fetchLogs, parseInt($('intervalSel').value) || 2000);
674
- }
675
-
676
- function stopPolling() {
677
- S.live = false;
678
- if (S.pollTimer) { clearInterval(S.pollTimer); S.pollTimer = null; }
679
- $('liveBtn').className = 'btn';
680
- $('liveBtn').innerHTML = '&#9654; Live';
681
- setStatus('disconnected', 'Stopped \u2014 ' + S.logs.length + ' lines');
682
- }
683
-
684
- /* ===== WebSocket ===== */
685
- function connectWS() {
686
- if (S.ws && (S.ws.readyState === WebSocket.OPEN || S.ws.readyState === WebSocket.CONNECTING)) return;
687
-
688
- S.wsMode = true;
689
- $('wsBtn').classList.add('ws-mode');
690
- setStatus('connecting', 'WS connecting...');
691
-
692
- try {
693
- S.ws = new WebSocket(WS_URL);
694
- } catch (e) {
695
- fallbackToPolling('WS init failed');
696
- return;
697
- }
698
-
699
- S.ws.onopen = () => {
700
- S.wsRetries = 0;
701
- setStatus('ws-connected', 'WebSocket connected');
702
- S.ws.send(JSON.stringify({ type: 'subscribe', channel: 'logs' }));
703
- };
704
-
705
- S.ws.onmessage = (evt) => {
706
- try {
707
- const data = JSON.parse(evt.data);
708
- if (data.type === 'log' && data.line) {
709
- if (emptyState && emptyState.parentNode) emptyState.remove();
710
- addLines([data.line], false);
711
- } else if (data.type === 'logs' && data.lines) {
712
- addLines(data.lines, !S.initialized);
713
- S.initialized = true;
714
- }
715
- } catch (e) {
716
- if (typeof evt.data === 'string' && evt.data.trim()) {
717
- addLines([evt.data], false);
718
- }
719
- }
720
- };
721
-
722
- S.ws.onclose = () => {
723
- if (!S.wsMode) return;
724
- S.wsRetries++;
725
- if (S.wsRetries <= WS_MAX_RETRIES) {
726
- setStatus('connecting', 'WS reconnecting (' + S.wsRetries + '/' + WS_MAX_RETRIES + ')...');
727
- setTimeout(connectWS, WS_RECONNECT_DELAY);
728
- } else {
729
- fallbackToPolling('WS max retries reached');
730
- }
731
- };
732
-
733
- S.ws.onerror = () => {
734
- // onclose will fire after onerror
735
- };
736
- }
737
-
738
- function disconnectWS() {
739
- S.wsMode = false;
740
- $('wsBtn').classList.remove('ws-mode');
741
- if (S.ws) {
742
- S.ws.onclose = null;
743
- S.ws.close();
744
- S.ws = null;
745
- }
746
- S.wsRetries = 0;
747
- }
748
-
749
- function fallbackToPolling(reason) {
750
- disconnectWS();
751
- setStatus('disconnected', 'WS failed: ' + reason + ' \u2014 switching to polling');
752
- if (!S.live) startPolling();
753
- }
754
-
755
- /* ===== Export ===== */
756
- function downloadFile(content, filename) {
757
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
758
- const url = URL.createObjectURL(blob);
759
- const a = document.createElement('a');
760
- a.href = url;
761
- a.download = filename;
762
- a.click();
763
- URL.revokeObjectURL(url);
764
- }
765
-
766
- function getVisibleLogs() {
767
- const result = [];
768
- const rows = logArea.querySelectorAll('.ll:not(.hidden)');
769
- rows.forEach((row) => {
770
- const idx = parseInt(row.getAttribute('data-idx'));
771
- if (S.logs[idx]) result.push(S.logs[idx]);
772
- });
773
- return result;
774
- }
775
-
776
- function getSelectedLogs() {
777
- const result = [];
778
- S.selected.forEach((idx) => { if (S.logs[idx]) result.push(S.logs[idx]); });
779
- return result;
780
- }
781
-
782
- function exportAsLog(entries) {
783
- const lines = entries.map((e) => e.raw).join('\n');
784
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
785
- downloadFile(lines, 'mama-logs-' + ts + '.log');
786
- }
787
-
788
- function exportAsJson(entries) {
789
- const data = entries.map((e) => ({
790
- line: e.n, time: e.time, source: e.source, level: e.level,
791
- channel: e.channel, message: e.msg, raw: e.raw
792
- }));
793
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
794
- downloadFile(JSON.stringify(data, null, 2), 'mama-logs-' + ts + '.json');
795
- }
796
-
797
- $('exportBtn').addEventListener('click', (e) => {
798
- e.stopPropagation();
799
- S.exportOpen = !S.exportOpen;
800
- $('exportMenu').classList.toggle('open', S.exportOpen);
801
- });
802
- document.addEventListener('click', () => { S.exportOpen = false; $('exportMenu').classList.remove('open'); });
803
- $('exportLog').addEventListener('click', () => { exportAsLog(getVisibleLogs()); S.exportOpen = false; $('exportMenu').classList.remove('open'); });
804
- $('exportSelLog').addEventListener('click', () => { exportAsLog(getSelectedLogs()); S.exportOpen = false; $('exportMenu').classList.remove('open'); });
805
- $('exportJson').addEventListener('click', () => { exportAsJson(getVisibleLogs()); S.exportOpen = false; $('exportMenu').classList.remove('open'); });
806
- $('exportSelJson').addEventListener('click', () => { exportAsJson(getSelectedLogs()); S.exportOpen = false; $('exportMenu').classList.remove('open'); });
807
-
808
- /* ===== Bookmarks/Pins ===== */
809
- function loadPins() {
810
- try {
811
- const stored = localStorage.getItem(PINS_STORAGE_KEY);
812
- if (stored) {
813
- const arr = JSON.parse(stored);
814
- arr.forEach((p) => S.pins.add(p.line));
815
- }
816
- } catch (e) { /* ignore */ }
817
- }
818
-
819
- function savePins() {
820
- try {
821
- const pinData = [];
822
- S.pins.forEach((idx) => {
823
- if (S.logs[idx]) {
824
- pinData.push({ line: idx, raw: S.logs[idx].raw, source: S.logs[idx].source, time: S.logs[idx].time });
825
- }
826
- });
827
- localStorage.setItem(PINS_STORAGE_KEY, JSON.stringify(pinData));
828
- } catch (e) { /* ignore */ }
829
- }
830
-
831
- function togglePin(idx) {
832
- if (S.pins.has(idx)) S.pins.delete(idx);
833
- else S.pins.add(idx);
834
- savePins();
835
- updatePinUI();
836
- updateMinimap();
837
- }
838
-
839
- function updatePinUI() {
840
- logArea.querySelectorAll('.ll').forEach((el) => {
841
- const idx = parseInt(el.getAttribute('data-idx'));
842
- el.classList.toggle('pinned', S.pins.has(idx));
843
- });
844
- renderPinsPanel();
845
- }
846
-
847
- function renderPinsPanel() {
848
- const list = $('pinsList');
849
- $('pinCount').textContent = S.pins.size;
850
- if (S.pins.size === 0) {
851
- list.innerHTML = '';
852
- return;
853
- }
854
- const sorted = [...S.pins].sort((a, b) => a - b);
855
- list.innerHTML = sorted.map((idx) => {
856
- const entry = S.logs[idx];
857
- if (!entry) return '';
858
- return '<div class="pin-line" data-pin-idx="' + idx + '">' +
859
- '<span class="pin-src" style="color:' + srcColor(entry.source) + '">' + esc(entry.source || '\u2014') + '</span>' +
860
- '<span class="pin-msg">' + esc(entry.msg).substring(0, 200) + '</span>' +
861
- '<span class="pin-rm" data-rm="' + idx + '">&#10005;</span></div>';
862
- }).join('');
863
- }
864
-
865
- $('pinToggle').addEventListener('click', () => {
866
- S.pinsPanelOpen = !S.pinsPanelOpen;
867
- $('pinsPanel').classList.toggle('active', S.pinsPanelOpen);
868
- $('pinToggle').classList.toggle('active', S.pinsPanelOpen);
869
- if (S.pinsPanelOpen) renderPinsPanel();
870
- });
871
-
872
- $('clearPins').addEventListener('click', () => {
873
- S.pins.clear();
874
- savePins();
875
- updatePinUI();
876
- updateMinimap();
877
- });
878
-
879
- $('pinsList').addEventListener('click', (e) => {
880
- const rm = e.target.closest('[data-rm]');
881
- if (rm) {
882
- const idx = parseInt(rm.getAttribute('data-rm'));
883
- S.pins.delete(idx);
884
- savePins();
885
- updatePinUI();
886
- updateMinimap();
887
- return;
888
- }
889
- const line = e.target.closest('.pin-line');
890
- if (line) {
891
- const idx = parseInt(line.getAttribute('data-pin-idx'));
892
- const row = logArea.querySelector('[data-idx="' + idx + '"]');
893
- if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
894
- }
895
- });
896
-
897
- $('selPin').addEventListener('click', () => {
898
- S.selected.forEach((idx) => S.pins.add(idx));
899
- savePins();
900
- updatePinUI();
901
- updateMinimap();
902
- });
903
-
904
- logArea.addEventListener('dblclick', (e) => {
905
- const row = e.target.closest('.ll');
906
- if (!row) return;
907
- const idx = parseInt(row.getAttribute('data-idx'));
908
- togglePin(idx);
909
- });
910
-
911
- /* ===== Minimap ===== */
912
- function updateMinimap() {
913
- const minimap = $('minimap');
914
- const viewport = $('minimapViewport');
915
- const markers = minimap.querySelectorAll('.minimap-marker');
916
- markers.forEach((m) => m.remove());
917
-
918
- const total = S.logs.length;
919
- if (total === 0) { viewport.style.display = 'none'; return; }
920
-
921
- const mapHeight = minimap.clientHeight;
922
- viewport.style.display = 'block';
923
-
924
- for (let i = 0; i < total; i++) {
925
- const entry = S.logs[i];
926
- if (entry.level !== 'error' && entry.level !== 'warn' && !S.pins.has(i)) continue;
927
- const y = (i / total) * mapHeight;
928
- const marker = document.createElement('div');
929
- marker.className = 'minimap-marker';
930
- if (S.pins.has(i)) marker.classList.add('pin');
931
- else marker.classList.add(entry.level);
932
- marker.style.top = y + 'px';
933
- marker.setAttribute('data-idx', i);
934
- minimap.appendChild(marker);
935
- }
936
-
937
- updateMinimapViewport();
938
- }
939
-
940
- function updateMinimapViewport() {
941
- const viewport = $('minimapViewport');
942
- const mapHeight = $('minimap').clientHeight;
943
- if (!logArea.scrollHeight || logArea.scrollHeight <= logArea.clientHeight) {
944
- viewport.style.top = '0';
945
- viewport.style.height = mapHeight + 'px';
946
- return;
947
- }
948
- const ratio = logArea.clientHeight / logArea.scrollHeight;
949
- const vpHeight = Math.max(ratio * mapHeight, 10);
950
- const scrollRatio = logArea.scrollTop / (logArea.scrollHeight - logArea.clientHeight);
951
- const vpTop = scrollRatio * (mapHeight - vpHeight);
952
- viewport.style.top = vpTop + 'px';
953
- viewport.style.height = vpHeight + 'px';
954
- }
955
-
956
- $('minimap').addEventListener('click', (e) => {
957
- if (e.target.classList.contains('minimap-marker')) {
958
- const idx = parseInt(e.target.getAttribute('data-idx'));
959
- const row = logArea.querySelector('[data-idx="' + idx + '"]');
960
- if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
961
- return;
962
- }
963
- const rect = $('minimap').getBoundingClientRect();
964
- const clickY = e.clientY - rect.top;
965
- const ratio = clickY / rect.height;
966
- logArea.scrollTop = ratio * (logArea.scrollHeight - logArea.clientHeight);
967
- });
968
-
969
- logArea.addEventListener('scroll', () => {
970
- S.autoScroll = logArea.scrollHeight - logArea.scrollTop - logArea.clientHeight < AUTO_SCROLL_THRESHOLD;
971
- $('autoBtn').classList.toggle('active', S.autoScroll);
972
- updateMinimapViewport();
973
- });
974
-
975
- /* ===== Stats Dashboard ===== */
976
- function updateStatsDashboard() {
977
- const levelMap = { error: 0, warn: 0, info: 0, debug: 0, success: 0 };
978
- const sourceMap = {};
979
- const timeSlots = {};
980
-
981
- S.logs.forEach((entry) => {
982
- if (levelMap[entry.level] !== undefined) levelMap[entry.level]++;
983
- else levelMap.info++;
984
-
985
- const src = entry.source || 'other';
986
- sourceMap[src] = (sourceMap[src] || 0) + 1;
987
-
988
- if (entry.time) {
989
- const hour = entry.time.substring(0, 5);
990
- timeSlots[hour] = (timeSlots[hour] || 0) + 1;
991
- }
992
- });
993
-
994
- // Level counts
995
- $('levelCounts').innerHTML = [
996
- { lbl: 'ERR', val: levelMap.error, color: 'var(--error)' },
997
- { lbl: 'WRN', val: levelMap.warn, color: 'var(--warn)' },
998
- { lbl: 'INF', val: levelMap.info, color: 'var(--accent)' },
999
- { lbl: 'DBG', val: levelMap.debug, color: 'var(--debug)' }
1000
- ].map((c) =>
1001
- '<span class="stats-count"><span class="num" style="color:' + c.color + '">' + c.val + '</span><span class="lbl">' + c.lbl + '</span></span>'
1002
- ).join('');
1003
-
1004
- // Source bar chart (top 6)
1005
- const sortedSrc = Object.entries(sourceMap).sort((a, b) => b[1] - a[1]).slice(0, 6);
1006
- const maxSrc = sortedSrc.length > 0 ? sortedSrc[0][1] : 1;
1007
- $('sourceChart').innerHTML = sortedSrc.map(([name, count]) => {
1008
- const w = Math.max((count / maxSrc) * 100, 2);
1009
- return '<div class="bar-row"><span class="bar-label">' + esc(name) + '</span>' +
1010
- '<div class="bar-fill" style="width:' + w + '%;background:' + srcColor(name) + '"></div>' +
1011
- '<span class="bar-value">' + count + '</span></div>';
1012
- }).join('');
1013
-
1014
- // Time histogram
1015
- const sortedTimes = Object.keys(timeSlots).sort();
1016
- if (sortedTimes.length === 0) {
1017
- $('volumeHistogram').innerHTML = '<span style="color:var(--muted);font-size:10px">No time data</span>';
1018
- return;
1019
- }
1020
-
1021
- let buckets;
1022
- if (sortedTimes.length <= HISTOGRAM_BUCKETS) {
1023
- buckets = sortedTimes.map((t) => ({ label: t, count: timeSlots[t] }));
1024
- } else {
1025
- const step = Math.ceil(sortedTimes.length / HISTOGRAM_BUCKETS);
1026
- buckets = [];
1027
- for (let i = 0; i < sortedTimes.length; i += step) {
1028
- const slice = sortedTimes.slice(i, i + step);
1029
- const total = slice.reduce((s, t) => s + timeSlots[t], 0);
1030
- buckets.push({ label: slice[0] + '-' + slice[slice.length - 1], count: total });
1031
- }
1032
- }
1033
-
1034
- const maxBucket = Math.max(...buckets.map((b) => b.count), 1);
1035
- $('volumeHistogram').innerHTML = buckets.map((b) => {
1036
- const h = Math.max((b.count / maxBucket) * 40, 1);
1037
- return '<div class="hist-bar" style="height:' + h + 'px"><span class="hist-tip">' + b.label + ': ' + b.count + '</span></div>';
1038
- }).join('');
1039
- }
1040
-
1041
- $('statsBtn').addEventListener('click', () => {
1042
- S.statsDashOpen = !S.statsDashOpen;
1043
- $('statsDashboard').classList.toggle('active', S.statsDashOpen);
1044
- $('statsBtn').classList.toggle('active', S.statsDashOpen);
1045
- if (S.statsDashOpen) updateStatsDashboard();
1046
- });
1047
- $('closeStats').addEventListener('click', () => {
1048
- S.statsDashOpen = false;
1049
- $('statsDashboard').classList.remove('active');
1050
- $('statsBtn').classList.remove('active');
1051
- });
1052
-
1053
- /* ===== Source panel ===== */
1054
- function renderSourceChips() {
1055
- const sorted = Object.entries(S.sourceCounts).sort((a, b) => b[1] - a[1]);
1056
- $('sourceChips').innerHTML = sorted.map(([name, count]) => {
1057
- let cls = 'source-chip';
1058
- if (S.activeSources && S.activeSources.has(name)) cls += ' active';
1059
- if (S.excludedSources.has(name)) cls += ' excluded';
1060
- return '<button class="' + cls + '" data-src="' + esc(name) + '" style="border-color:' + srcColor(name) + '">' + esc(name) + '<span class="chip-count">' + count + '</span></button>';
1061
- }).join('');
1062
-
1063
- const chSorted = Object.entries(S.channelCounts).sort((a, b) => b[1] - a[1]);
1064
- $('channelChips').innerHTML = chSorted.map(([name, count]) => {
1065
- let cls = 'source-chip';
1066
- if (S.activeChannels && S.activeChannels.has(name)) cls += ' active';
1067
- if (S.excludedChannels.has(name)) cls += ' excluded';
1068
- return '<button class="' + cls + '" data-ch="' + esc(name) + '">' + esc(name) + '<span class="chip-count">' + count + '</span></button>';
1069
- }).join('');
1070
- }
1071
-
1072
- $('sourceChips').addEventListener('click', (e) => {
1073
- const btn = e.target.closest('[data-src]'); if (!btn) return;
1074
- const name = btn.getAttribute('data-src');
1075
- if (e.shiftKey) {
1076
- if (!S.activeSources) S.activeSources = new Set();
1077
- if (S.activeSources.has(name)) S.activeSources.delete(name); else S.activeSources.add(name);
1078
- if (S.activeSources.size === 0) S.activeSources = null;
1079
- } else {
1080
- if (S.activeSources && S.activeSources.size === 1 && S.activeSources.has(name)) S.activeSources = null;
1081
- else S.activeSources = new Set([name]);
1082
- }
1083
- S.excludedSources.delete(name);
1084
- renderSourceChips(); applyFilters();
1085
- });
1086
- $('sourceChips').addEventListener('contextmenu', (e) => {
1087
- const btn = e.target.closest('[data-src]'); if (!btn) return;
1088
- e.preventDefault();
1089
- const name = btn.getAttribute('data-src');
1090
- if (S.excludedSources.has(name)) S.excludedSources.delete(name); else S.excludedSources.add(name);
1091
- if (S.activeSources) S.activeSources.delete(name);
1092
- renderSourceChips(); applyFilters();
1093
- });
1094
- $('channelChips').addEventListener('click', (e) => {
1095
- const btn = e.target.closest('[data-ch]'); if (!btn) return;
1096
- const name = btn.getAttribute('data-ch');
1097
- if (e.shiftKey) {
1098
- if (!S.activeChannels) S.activeChannels = new Set();
1099
- if (S.activeChannels.has(name)) S.activeChannels.delete(name); else S.activeChannels.add(name);
1100
- if (S.activeChannels.size === 0) S.activeChannels = null;
1101
- } else {
1102
- if (S.activeChannels && S.activeChannels.size === 1 && S.activeChannels.has(name)) S.activeChannels = null;
1103
- else S.activeChannels = new Set([name]);
1104
- }
1105
- S.excludedChannels.delete(name);
1106
- renderSourceChips(); applyFilters();
1107
- });
1108
- $('channelChips').addEventListener('contextmenu', (e) => {
1109
- const btn = e.target.closest('[data-ch]'); if (!btn) return;
1110
- e.preventDefault();
1111
- const name = btn.getAttribute('data-ch');
1112
- if (S.excludedChannels.has(name)) S.excludedChannels.delete(name); else S.excludedChannels.add(name);
1113
- if (S.activeChannels) S.activeChannels.delete(name);
1114
- renderSourceChips(); applyFilters();
1115
- });
1116
- $('showAllBtn').addEventListener('click', () => {
1117
- S.activeSources = null; S.excludedSources.clear();
1118
- S.activeChannels = null; S.excludedChannels.clear();
1119
- renderSourceChips(); applyFilters();
1120
- });
1121
- $('soloBtn').addEventListener('click', () => {
1122
- if (S.selected.size > 0) {
1123
- S.activeSources = new Set();
1124
- S.selected.forEach((idx) => { if (S.logs[idx] && S.logs[idx].source) S.activeSources.add(S.logs[idx].source); });
1125
- renderSourceChips(); applyFilters();
1126
- }
1127
- });
1128
-
1129
- /* ===== Selection ===== */
1130
- logArea.addEventListener('click', (e) => {
1131
- const row = e.target.closest('.ll'); if (!row) return;
1132
- const idx = parseInt(row.getAttribute('data-idx'));
1133
- if (e.shiftKey && S.lastClickIdx >= 0) {
1134
- const start = Math.min(S.lastClickIdx, idx), end = Math.max(S.lastClickIdx, idx);
1135
- for (let i = start; i <= end; i++) S.selected.add(i);
1136
- } else if (e.ctrlKey || e.metaKey) {
1137
- if (S.selected.has(idx)) S.selected.delete(idx); else S.selected.add(idx);
1138
- } else {
1139
- if (S.selected.size === 1 && S.selected.has(idx)) S.selected.clear();
1140
- else { S.selected.clear(); S.selected.add(idx); }
1141
- }
1142
- S.lastClickIdx = idx;
1143
- logArea.querySelectorAll('.ll').forEach((el) => {
1144
- el.classList.toggle('selected', S.selected.has(parseInt(el.getAttribute('data-idx'))));
1145
- });
1146
- updateSelectionBar();
1147
- });
1148
-
1149
- function updateSelectionBar() {
1150
- $('selBarCount').textContent = S.selected.size;
1151
- $('selectionBar').classList.toggle('active', S.selected.size > 0);
1152
- }
1153
-
1154
- function getSelectedText() {
1155
- const texts = [];
1156
- S.selected.forEach((idx) => { if (S.logs[idx]) texts.push(S.logs[idx].raw); });
1157
- return texts.join('\n');
1158
- }
1159
-
1160
- $('selCopy').addEventListener('click', () => {
1161
- navigator.clipboard.writeText(getSelectedText()).then(() => {
1162
- $('selCopy').textContent = '\u2705'; setTimeout(() => { $('selCopy').textContent = 'Copy'; }, 1200);
1163
- });
1164
- });
1165
- $('selSend').addEventListener('click', () => {
1166
- const text = getSelectedText();
1167
- const msg = '[\uc120\ud0dd\ub41c \ub85c\uadf8 ' + S.selected.size + '\uc904]:\n```\n' + text + '\n```';
1168
- window.parent.postMessage({ type: 'playground:sendToChat', message: msg }, POST_MSG_ORIGIN);
1169
- S.selected.clear();
1170
- logArea.querySelectorAll('.ll.selected').forEach((el) => { el.classList.remove('selected'); });
1171
- updateSelectionBar();
1172
- });
1173
- $('selClear').addEventListener('click', () => {
1174
- S.selected.clear();
1175
- logArea.querySelectorAll('.ll.selected').forEach((el) => { el.classList.remove('selected'); });
1176
- updateSelectionBar();
1177
- });
1178
-
1179
- /* ===== Events ===== */
1180
- $('liveBtn').addEventListener('click', () => { S.live ? stopPolling() : startPolling(); });
1181
- $('intervalSel').addEventListener('change', () => { if (S.live) { stopPolling(); startPolling(); } });
1182
- $('searchInput').addEventListener('input', () => { applyFilters(); });
1183
- $('regexToggle').addEventListener('click', () => {
1184
- S.regexMode = !S.regexMode;
1185
- $('regexToggle').classList.toggle('active', S.regexMode);
1186
- $('searchInput').placeholder = S.regexMode ? 'Filter regex... (/ or Ctrl+F)' : 'Filter... (/ or Ctrl+F)';
1187
- applyFilters();
1188
- });
1189
- $('autoBtn').addEventListener('click', function() {
1190
- S.autoScroll = !S.autoScroll;
1191
- this.classList.toggle('active', S.autoScroll);
1192
- if (S.autoScroll) logArea.scrollTop = logArea.scrollHeight;
1193
- });
1194
- $('clearBtn').addEventListener('click', () => {
1195
- S.logs = []; S.selected.clear(); S.pins.clear(); savePins(); S.lastClickIdx = -1;
1196
- S.lastMtime = 0; S.lastTotal = 0; S.lastHadTotal = false; S.initialized = false;
1197
- S.prevCount = 0; S.rateHistory = []; S.sourceCounts = {}; S.channelCounts = {};
1198
- logArea.innerHTML = '<div class="empty" id="emptyState"><div class="ico">&#128203;</div><p>Cleared</p></div>';
1199
- emptyState = $('emptyState');
1200
- $('stTotal').textContent = '0'; $('stShown').textContent = '0';
1201
- $('rateBadge').textContent = '0 l/s'; $('modStats').innerHTML = '';
1202
- updateSelectionBar();
1203
- updateMinimap();
1204
- if (S.statsDashOpen) updateStatsDashboard();
1205
- });
1206
-
1207
- $('wsBtn').addEventListener('click', () => {
1208
- if (S.wsMode) {
1209
- disconnectWS();
1210
- setStatus(S.live ? 'connected' : 'disconnected', S.live ? 'Polling mode' : 'WS disconnected');
1211
- } else {
1212
- connectWS();
1213
- if (!S.live) startPolling();
1214
- }
1215
- });
1216
-
1217
- document.querySelectorAll('.lvl').forEach((el) => {
1218
- el.addEventListener('click', () => {
1219
- const lv = el.getAttribute('data-l');
1220
- S.levels[lv] = !S.levels[lv];
1221
- el.classList.toggle('on', S.levels[lv]);
1222
- el.classList.toggle('off', !S.levels[lv]);
1223
- applyFilters();
1224
- });
1225
- });
1226
-
1227
- $('graphToggle').addEventListener('click', function() {
1228
- S.showGraph = !S.showGraph;
1229
- this.classList.toggle('active', S.showGraph);
1230
- applyFilters();
1231
- });
1232
-
1233
- $('srcToggle').addEventListener('click', function() {
1234
- S.sourcePanelOpen = !S.sourcePanelOpen;
1235
- $('sourcePanel').classList.toggle('active', S.sourcePanelOpen);
1236
- this.classList.toggle('open', S.sourcePanelOpen);
1237
- if (S.sourcePanelOpen) renderSourceChips();
1238
- });
1239
-
1240
- /* ===== Keyboard ===== */
1241
- document.addEventListener('keydown', (e) => {
1242
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
1243
- if (e.key === 'Escape') {
1244
- e.target.blur();
1245
- if (e.target === $('searchInput')) { $('searchInput').value = ''; applyFilters(); }
1246
- }
1247
- return;
1248
- }
1249
- if (e.ctrlKey && e.key === 'f') { e.preventDefault(); $('searchInput').focus(); return; }
1250
- if (e.key === '/') { e.preventDefault(); $('searchInput').focus(); return; }
1251
- if (e.key === 'l' || e.key === 'L') { S.live ? stopPolling() : startPolling(); }
1252
- if (e.key === 'w' || e.key === 'W') { $('wsBtn').click(); }
1253
- if (e.key === 'a' && !e.ctrlKey && !e.metaKey) {
1254
- S.autoScroll = !S.autoScroll;
1255
- $('autoBtn').classList.toggle('active', S.autoScroll);
1256
- if (S.autoScroll) logArea.scrollTop = logArea.scrollHeight;
1257
- }
1258
- if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
1259
- e.preventDefault();
1260
- S.logs.forEach((l, i) => { S.selected.add(i); });
1261
- logArea.querySelectorAll('.ll:not(.hidden)').forEach((el) => { el.classList.add('selected'); });
1262
- updateSelectionBar();
1263
- }
1264
- if (e.key === 'p' || e.key === 'P') {
1265
- if (S.live) {
1266
- S.paused = !S.paused;
1267
- if (S.paused) { $('liveBtn').innerHTML = '&#9654; Resume'; $('liveBtn').classList.add('paused'); }
1268
- else { $('liveBtn').innerHTML = '&#9208; Pause'; $('liveBtn').classList.remove('paused'); fetchLogs(); }
1269
- }
1270
- }
1271
- if (e.key === 'c' && !e.ctrlKey && !e.metaKey) { $('clearBtn').click(); }
1272
- if (e.key === 'd' || e.key === 'D') { $('statsBtn').click(); }
1273
- if (e.key === 'e' && !e.ctrlKey && !e.metaKey) { $('exportBtn').click(); }
1274
- if (e.key === 'Escape') {
1275
- if (S.selected.size > 0) {
1276
- S.selected.clear();
1277
- logArea.querySelectorAll('.ll.selected').forEach((el) => { el.classList.remove('selected'); });
1278
- updateSelectionBar();
1279
- }
1280
- if (S.exportOpen) { S.exportOpen = false; $('exportMenu').classList.remove('open'); }
1281
- }
1282
- });
1283
-
1284
- /* ===== Prompt panel ===== */
1285
- $('promptHdr').addEventListener('click', () => {
1286
- $('promptBody').classList.toggle('open');
1287
- $('promptArrow').classList.toggle('open');
1288
- });
1289
-
1290
- const PRESETS = {
1291
- errors: 'daemon.log\uc5d0\uc11c ERROR, FAIL, ECONNREFUSED \uad00\ub828 \ub85c\uadf8\ub97c \ubd84\uc11d\ud558\uace0 \uc6d0\uc778\uacfc \ud574\uacb0\ubc29\ubc95\uc744 \uc54c\ub824\uc918',
1292
- status: 'MAMA OS \ud604\uc7ac \uc0c1\ud0dc\ub97c \ud655\uc778\ud574\uc918 (\ud504\ub85c\uc138\uc2a4, \uba54\ubaa8\ub9ac, \uc5c5\ud0c0\uc784, \uc5f0\uacb0 \uc0c1\ud0dc)',
1293
- slow: 'daemon.log\uc5d0\uc11c \ub290\ub9b0 \uc791\uc5c5(timeout, slow, 1000ms \uc774\uc0c1)\uc744 \ucc3e\uc544\uc11c \uc131\ub2a5 \ubcd1\ubaa9\uc744 \ubd84\uc11d\ud574\uc918',
1294
- recent: '\ucd5c\uadfc \ub85c\uadf8 50\uc904\uc744 \uc694\uc57d\ud574\uc918 \u2014 \ubb34\uc2a8 \uc77c\uc774 \uc788\uc5c8\ub294\uc9c0, \uc5d0\ub7ec\uac00 \uc788\uc5c8\ub294\uc9c0'
1295
- };
1296
- document.querySelectorAll('.preset').forEach((el) => {
1297
- el.addEventListener('click', () => {
1298
- $('promptInput').value = PRESETS[el.getAttribute('data-p')];
1299
- $('promptInput').focus();
1300
- });
1301
- });
1302
-
1303
- $('promptSend').addEventListener('click', () => {
1304
- const text = $('promptInput').value.trim();
1305
- if (!text) return;
1306
- let msg = text;
1307
- if (S.selected.size > 0) {
1308
- const selLogs = [];
1309
- S.selected.forEach((idx) => { if (S.logs[idx]) selLogs.push(S.logs[idx].raw); });
1310
- msg += '\n\n[\uc120\ud0dd\ub41c \ub85c\uadf8 ' + selLogs.length + '\uc904]:\n```\n' + selLogs.join('\n') + '\n```';
1311
- }
1312
- window.parent.postMessage({ type: 'playground:sendToChat', message: msg }, POST_MSG_ORIGIN);
1313
- $('promptInput').value = '';
1314
- });
1315
- $('promptCopy').addEventListener('click', () => {
1316
- let text = $('promptInput').value;
1317
- if (S.selected.size > 0) {
1318
- const selLogs = [];
1319
- S.selected.forEach((idx) => { if (S.logs[idx]) selLogs.push(S.logs[idx].raw); });
1320
- text += '\n\n```\n' + selLogs.join('\n') + '\n```';
1321
- }
1322
- navigator.clipboard.writeText(text).then(() => {
1323
- $('promptCopy').textContent = '\u2705'; setTimeout(() => { $('promptCopy').innerHTML = '&#128203; Copy'; }, 1200);
1324
- });
1325
- });
1326
-
1327
- window.addEventListener('message', (e) => {
1328
- if (e.data && e.data.type === 'mama:logs') {
1329
- const lines = e.data.content.split('\n').filter((l) => l.trim());
1330
- addLines(lines, true);
1331
- setStatus('connected', 'Loaded ' + lines.length + ' lines');
1332
- }
1333
- });
1334
-
1335
- /* ===== Init ===== */
1336
- loadPins();
1337
- startPolling();
1338
- })();
1339
- </script>
1340
- </body>
1341
- </html>