dsh-flash-mem-mon 0.1.5 → 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.
package/lib/client.js CHANGED
@@ -1,308 +1,497 @@
1
- // dsh-flash-mem-mon — Memory monitor client half
2
- // Single file, no build step. Loaded via window.__ModuleLoader__.
3
- //
4
- // Migrated from dock-flash/lib/client.js:
5
- // - Memory i18n keys (zh/en)
6
- // - createHostMemoryAlertProvider() → provider id dsh-flash-mem-mon:host-memory-alert
7
- // - Memory enable toggle → dsh-flash-mem-mon:monitor-memory
8
- // - MONITOR_SLIDER_FIELDS.memory (9 sliders)
9
- // - MonitorConfigModal (memory-only branch)
10
- // - _memPrefs preference system
11
- //
12
- // Route: /plugins/dsh-flash-mem-mon/memory-trend (own route)
13
- // Cross-plugin: /plugins/dock-flash/health (read-only, for live host memory data)
14
- //
15
- // Provider id: dsh-flash-mem-mon:host-memory-alert
16
- // Toggle key: dsh-flash-mem-mon:monitor-memory
17
- // Settings namespace: dsh-flash-mem-mon
18
-
19
- window.__ModuleLoader__.load({
20
- id: 'dsh-flash-mem-mon',
21
- factory: (require) => {
22
-
23
- var React = require('react')
24
- var h = React.createElement
25
- var useState = React.useState
26
- var useEffect = React.useEffect
27
- var useRef = React.useRef
28
- var Component = React.Component
29
- var ReactDOMClient = null
30
- try { ReactDOMClient = require('react-dom/client') } catch (_) {}
31
-
32
- // ═══════════════════════════════════════════════════════════════════════
33
- //#region i18n ────────────────────────────────────────────────────────────
34
-
35
- var zh = {
36
- alertMemCluster: '内存监控',
37
- monitorOn: '已开启',
38
- monitorOff: '已关闭',
39
- monitorConfig: '配置',
40
- // Memory thresholds
41
- memConfigGroupRss: 'RSS 阈值 / 轮询',
42
- memConfigGroupGc: 'Major GC 阈值',
43
- memThresholdInfo: 'RSS 信息阈值',
44
- memThresholdInfoTip: 'RSS(常驻内存集)超过此值时触发信息级告警。信息级仅提示,不影响行为。建议设为正常负载的 1.5–2 倍。',
45
- memThresholdWarning: 'RSS 警告阈值',
46
- memThresholdWarningTip: 'RSS 超过此值时触发警告级告警。此时内存压力较大,应关注是否有泄漏。必须大于信息阈值。',
47
- memThresholdError: 'RSS 错误阈值',
48
- memThresholdErrorTip: 'RSS 超过此值时触发错误级告警。内存已接近危险区,建议尽快排查。必须大于警告阈值。',
49
- memThresholdCritical: 'RSS 严重阈值',
50
- memThresholdCriticalTip: 'RSS 超过此值时触发严重级告警。进程可能即将被 OOM Killer 终止,需立即处理。必须大于错误阈值。',
51
- memPollBase: '内存轮询基础间隔',
52
- memPollBaseTip: '堆使用率较低时的采样间隔(天花板)。空闲时以这个频率采样,堆越满采样越密。默认 30 秒,一般不需要调低。',
53
- memPollMin: '内存轮询最小间隔',
54
- memPollMinTip: '堆使用率很高时的采样间隔(地板)。无论堆多满,两次采样不会比这更密,避免过度消耗 CPU。默认 2 秒。',
55
- // GC thresholds
56
- gcThresholdInfo: 'Major GC 信息阈值',
57
- gcThresholdInfoTip: '过去 5 分钟内 Major GC(MarkSweep/MarkCompact)频率超过此值时触发信息级告警。1/min 即每分钟一次 Full GC,正常负载下应远低于此。',
58
- gcThresholdWarning: 'Major GC 警告阈值',
59
- gcThresholdWarningTip: 'Major GC 频率超过此值时触发警告级告警。GC 已频繁到影响吞吐量,堆分配/回收振荡剧烈。必须大于信息阈值。',
60
- gcThresholdError: 'Major GC 错误阈值',
61
- gcThresholdErrorTip: 'Major GC 频率超过此值时触发错误级告警。GC 自身开销占比很高,接近或已进入泄漏状态。必须大于警告阈值。',
62
- // Alert preview
63
- alertPreviewTitle: '告警示例',
64
- alertPreviewToggle: '查看告警消息样例',
65
- alertPreviewCollapse: '收起',
66
- alertPreviewCriteriaRss: 'RSS 绝对值',
67
- alertPreviewCriteriaGrowth: 'RSS 增长率',
68
- alertPreviewCriteriaGc: 'Major GC 频率',
69
- alertPreviewTrigger: '触发',
70
- // Alert title (includes severity level and threshold value)
71
- alertHostMemInfo: 'RSS 超过信息级阈值 {threshold} MB',
72
- alertHostMemWarning: 'RSS 超过警告级阈值 {threshold} MB',
73
- alertHostMemError: 'RSS 超过错误级阈值 {threshold} MB',
74
- alertHostMemCritical: 'RSS 超过严重级阈值 {threshold} MB',
75
- alertHostMemMsg: 'RSS {v} MB | 堆 {heap}/{heapTotal} MB ({pct}%) | 外部 {ext} MB | ArrayBuffer {ab} MB | 趋势 {trend} | 增长 +{slope} MB/min{gcInfo}',
76
- alertHostMemGrowth: 'RSS 持续增长',
77
- alertHostMemGrowthMsg: '+{r} MB/min | RSS {rss} MB | 堆 {heap}/{heapTotal} MB ({pct}%) | ArrayBuffer {ab} MB | 趋势 {trend}',
78
- alertHostGcInfo: 'GC 频率较高',
79
- alertHostGcWarning: 'GC 频率异常',
80
- alertHostGcError: 'GC 频率极高',
81
- alertHostGcMsg: 'Major {v} 次/min | 暂停 {pause} ms/min | Minor {minor} 次 | RSS {rss} MB | 堆 {heap}/{heapTotal} MB ({pct}%)',
82
- alertSevInfo: '信息',
83
- alertSevWarning: '警告',
84
- alertSevError: '错误',
85
- alertSevCritical: '严重',
86
- // Memory live data
87
- memLiveData: '当前数据',
88
- // Host process memory
89
- memHostSection: 'Host 进程',
90
- memHostRSS: '常驻内存',
91
- memHostHeapTotal: '堆总大小',
92
- memHostHeapUsed: '堆已用',
93
- memHostExternal: 'C++ 外部',
94
- memHostArrayBuffers: 'ArrayBuffer',
95
- memHostHeapRatio: '堆使用率',
96
- memHostUnavailable: '无法获取 Host 内存数据',
97
- // GC stats
98
- memGcMajorPerMin: 'Major GC/min',
99
- memGcPausePerMin: 'GC 暂停/min',
100
- memGcMinorCount: 'Minor GC',
101
- memGcMajorCount: 'Major GC',
102
- memGcUnavailable: 'GC 监控不可用',
103
- memGcEventList: 'Full GC 记录',
104
- memGcEventDuration: '耗时',
105
- memGcEventTime: '时间',
106
- memGcEventClick: '点击定位',
107
- memGcNoEvents: '近期无 Full GC',
108
- memGcEventRssDrop: '内存释放',
109
- // Memory trend
110
- memTrend: '内存趋势',
111
- memTrendPeak: 'RSS 峰值',
112
- memTrendDirection: '趋势',
113
- memTrendUp: '↑ 上升',
114
- memTrendStable: '→ 稳定',
115
- memTrendDown: '↓ 下降',
116
- memTrendSamples: '采样数',
117
- memTrendSpan: '跨度',
118
- memTrendHeapRatio: '堆使用率',
119
- memTrendNoData: '暂无数据',
120
- memTrendHoverHeap: '堆',
121
- memTrendHoverRSS: 'RSS',
122
- memTrendHoverTime: '时间',
123
- memTrendGrowthRate: 'RSS 增长率',
124
- memTrendAxisHeap: '堆',
125
- memTrendAxisTime: '时间',
126
- memTrendInfo: '趋势信息',
127
- }
128
-
129
- var en = {
130
- alertMemCluster: 'Memory Monitor',
131
- monitorOn: 'Enabled',
132
- monitorOff: 'Disabled',
133
- monitorConfig: 'Configure',
134
- // Memory thresholds
135
- memConfigGroupRss: 'RSS / Polling',
136
- memConfigGroupGc: 'Major GC',
137
- memThresholdInfo: 'RSS Info Threshold',
138
- memThresholdInfoTip: 'Triggers an info-level alert when RSS (Resident Set Size) exceeds this value. Info is informational only. Recommended: 1.5–2× your normal workload RSS.',
139
- memThresholdWarning: 'RSS Warning Threshold',
140
- memThresholdWarningTip: 'Triggers a warning alert when RSS exceeds this value. Memory pressure is significant — watch for leaks. Must be greater than the Info threshold.',
141
- memThresholdError: 'RSS Error Threshold',
142
- memThresholdErrorTip: 'Triggers an error alert when RSS exceeds this value. Memory is in the danger zone — investigate promptly. Must be greater than the Warning threshold.',
143
- memThresholdCritical: 'RSS Critical Threshold',
144
- memThresholdCriticalTip: 'Triggers a critical alert when RSS exceeds this value. The process may be OOM-killed soon — act immediately. Must be greater than the Error threshold.',
145
- memPollBase: 'Memory Poll Base Interval',
146
- memPollBaseTip: 'Sampling interval when heap usage is low (the ceiling). At idle, sampling runs at this pace; as heap fills up, sampling gets denser automatically. Default 30s — rarely needs lowering.',
147
- memPollMin: 'Memory Poll Min Interval',
148
- memPollMinTip: 'Minimum sampling interval even when heap is full (the floor). Prevents excessive CPU use from too-frequent sampling. Default 2s.',
149
- // GC thresholds
150
- gcThresholdInfo: 'Major GC Info Threshold',
151
- gcThresholdInfoTip: 'Triggers an info alert when Major GC (MarkSweep/MarkCompact) frequency over the last 5 minutes exceeds this value. 1/min means one Full GC per minute — normal workloads should be well below this.',
152
- gcThresholdWarning: 'Major GC Warning Threshold',
153
- gcThresholdWarningTip: 'Triggers a warning when Major GC frequency exceeds this value. GC is frequent enough to hurt throughput — the heap is oscillating heavily. Must be greater than the Info threshold.',
154
- gcThresholdError: 'Major GC Error Threshold',
155
- gcThresholdErrorTip: 'Triggers an error when Major GC frequency exceeds this value. GC overhead is dominant — likely a leak. Must be greater than the Warning threshold.',
156
- // Alert preview
157
- alertPreviewTitle: 'Alert Preview',
158
- alertPreviewToggle: 'View alert message samples',
159
- alertPreviewCollapse: 'Collapse',
160
- alertPreviewCriteriaRss: 'RSS Absolute',
161
- alertPreviewCriteriaGrowth: 'RSS Growth',
162
- alertPreviewCriteriaGc: 'Major GC Freq',
163
- alertPreviewTrigger: 'trigger',
164
- // Alert title (includes severity level and threshold value)
165
- alertHostMemInfo: 'RSS exceeded info threshold {threshold} MB',
166
- alertHostMemWarning: 'RSS exceeded warning threshold {threshold} MB',
167
- alertHostMemError: 'RSS exceeded error threshold {threshold} MB',
168
- alertHostMemCritical: 'RSS exceeded critical threshold {threshold} MB',
169
- alertHostMemMsg: 'RSS {v} MB | Heap {heap}/{heapTotal} MB ({pct}%) | Ext {ext} MB | ArrayBuffer {ab} MB | Trend {trend} | Growth +{slope} MB/min{gcInfo}',
170
- alertHostMemGrowth: 'RSS steadily growing',
171
- alertHostMemGrowthMsg: '+{r} MB/min | RSS {rss} MB | Heap {heap}/{heapTotal} MB ({pct}%) | ArrayBuffer {ab} MB | Trend {trend}',
172
- alertHostGcInfo: 'GC frequency elevated',
173
- alertHostGcWarning: 'GC frequency abnormal',
174
- alertHostGcError: 'GC frequency critical',
175
- alertHostGcMsg: 'Major {v}/min | Pause {pause} ms/min | Minor {minor} | RSS {rss} MB | Heap {heap}/{heapTotal} MB ({pct}%)',
176
- alertSevInfo: 'Info',
177
- alertSevWarning: 'Warning',
178
- alertSevError: 'Error',
179
- alertSevCritical: 'Critical',
180
- // Memory live data
181
- memLiveData: 'Live Data',
182
- // Host process memory
183
- memHostSection: 'Host Process',
184
- memHostRSS: 'RSS',
185
- memHostHeapTotal: 'Heap Total',
186
- memHostHeapUsed: 'Heap Used',
187
- memHostExternal: 'C++ External',
188
- memHostArrayBuffers: 'ArrayBuffer',
189
- memHostHeapRatio: 'Heap Usage',
190
- memHostUnavailable: 'Host memory data unavailable',
191
- // GC stats
192
- memGcMajorPerMin: 'Major GC/min',
193
- memGcPausePerMin: 'GC Pause/min',
194
- memGcMinorCount: 'Minor GC',
195
- memGcMajorCount: 'Major GC',
196
- memGcUnavailable: 'GC monitoring unavailable',
197
- memGcEventList: 'Full GC Events',
198
- memGcEventDuration: 'Duration',
199
- memGcEventTime: 'Time',
200
- memGcEventClick: 'Click to locate',
201
- memGcNoEvents: 'No Full GC recently',
202
- memGcEventRssDrop: 'Mem Release',
203
- // Memory trend
204
- memTrend: 'Memory Trend',
205
- memTrendPeak: 'RSS Peak',
206
- memTrendDirection: 'Trend',
207
- memTrendUp: '↑ Rising',
208
- memTrendStable: '→ Stable',
209
- memTrendDown: '↓ Falling',
210
- memTrendSamples: 'Samples',
211
- memTrendSpan: 'Span',
212
- memTrendHeapRatio: 'Heap Usage',
213
- memTrendNoData: 'No data yet',
214
- memTrendHoverHeap: 'Heap',
215
- memTrendHoverRSS: 'RSS',
216
- memTrendHoverTime: 'Time',
217
- memTrendGrowthRate: 'RSS Growth Rate',
218
- memTrendAxisHeap: 'Heap',
219
- memTrendAxisTime: 'Time',
220
- memTrendInfo: 'Trend Info',
221
- }
222
-
223
- var LOCALES = { zh: zh, en: en }
224
-
225
- /** Read the current BCP-47 tag from <html lang> (or navigator fallback). */
226
- function detectLocaleTag() {
227
- try {
228
- var tag = document.documentElement.lang
229
- if (tag) return tag
230
- } catch (_) {}
231
- try { return navigator.language || 'en' } catch (_) { return 'en' }
232
- }
233
-
234
- /** Map a BCP-47 tag to one of our locale keys ('zh' | 'en'). */
235
- function resolveLocaleKey(tag) {
236
- var lower = (tag || '').toLowerCase()
237
- if (lower.startsWith('zh')) return 'zh'
238
- return 'en'
239
- }
240
-
241
- var _currentLocaleKey = resolveLocaleKey(detectLocaleTag())
242
-
243
- /** Observe <html lang> changes so the UI updates on language switch. */
244
- if (typeof MutationObserver !== 'undefined' && typeof document !== 'undefined') {
245
- try {
246
- var _langObserver = new MutationObserver(function () {
247
- var next = resolveLocaleKey(detectLocaleTag())
248
- if (next !== _currentLocaleKey) _currentLocaleKey = next
249
- })
250
- _langObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
251
- } catch (_) {}
252
- }
253
-
254
- /** i18n lookup — returns the string for the current locale. */
255
- function t(key) {
256
- var locale = LOCALES[_currentLocaleKey] || LOCALES.en
257
- return locale[key] !== undefined ? locale[key] : (LOCALES.en[key] !== undefined ? LOCALES.en[key] : key)
258
- }
259
-
260
- /** Functional label helper — returns a function that calls t() so the label updates on locale change. */
261
- function L(key) {
262
- return function () { return t(key) }
263
- }
264
-
265
- //#endregion ───────────────────────────────────────────────────────────────
266
-
267
- // ═══════════════════════════════════════════════════════════════════════
268
- //#region Preference system ──────────────────────────────────────────────
269
-
270
- var _MEM_ALERT_DEFAULTS = {
271
- memThresholdInfo: 256,
272
- memThresholdWarning: 512,
273
- memThresholdError: 1024,
274
- memThresholdCritical: 1536,
275
- memPollBase: 30000,
276
- memPollMin: 2000,
277
- gcThresholdInfo: 2,
278
- gcThresholdWarning: 5,
279
- gcThresholdError: 10,
280
- }
281
- var _memPrefs = Object.assign({}, _MEM_ALERT_DEFAULTS)
282
- var _prefCtx = null
283
- var MEM_MON_NS = 'dsh-flash-mem-mon'
284
- var _hostRevision = null
285
- var _prefWriteTail = Promise.resolve()
286
- var _pendingRevision = null
287
-
288
- function _fenceRevision() {
289
- if (_pendingRevision !== null) return _pendingRevision
290
- return _hostRevision === null ? undefined : _hostRevision
291
- }
292
-
293
- function _remoteSettings(ctx) {
294
- try {
295
- if (ctx && ctx.remote && ctx.remote.settings) return ctx.remote.settings
296
- } catch (e) {
297
- console.warn('[dsh-flash-mem-mon] ctx.remote.settings threw:', e && e.message)
298
- }
299
- try {
300
- var remote = ctx && ctx.get ? ctx.get('remote') : undefined
301
- if (remote && remote.settings) return remote.settings
302
- } catch (e) { console.warn('[dsh-flash-mem-mon] ctx.get("remote") threw:', e && e.message) }
303
- return undefined
304
- }
305
-
1
+ // dsh-flash-mem-mon — Memory monitor client half
2
+ // Single file, no build step. Loaded via window.__ModuleLoader__.
3
+ //
4
+ // Migrated from dock-flash/lib/client.js:
5
+ // - Memory i18n keys (zh/en)
6
+ // - createHostMemoryAlertProvider() → provider id dsh-flash-mem-mon:host-memory-alert
7
+ // - Memory enable toggle → dsh-flash-mem-mon:monitor-memory
8
+ // - MONITOR_SLIDER_FIELDS.memory (9 sliders)
9
+ // - MonitorConfigModal (memory-only branch)
10
+ // - _memPrefs preference system
11
+ //
12
+ // Route: /plugins/dsh-flash-mem-mon/memory-trend (own route)
13
+ // Cross-plugin: /plugins/dsh-flash/health (read-only, for live host memory data)
14
+ //
15
+ // Provider id: dsh-flash-mem-mon:host-memory-alert
16
+ // Toggle key: dsh-flash-mem-mon:monitor-memory
17
+ // Settings namespace: dsh-flash-mem-mon
18
+ //
19
+ // ───────────────────────────────────────────────────────────────────────────
20
+ // FILE MAP — region layout (collapsible, in file order; line numbers are not
21
+ // maintained, the markers are the source of truth):
22
+ // #region i18n zh/en message tables + the `locale`
23
+ // service proxy (t / L / bindLocaleService)
24
+ // #region Preference system _MEM_ALERT_DEFAULTS, _memPrefs,
25
+ // _alertPref / loadMemPrefs,
26
+ // remote settings namespace bridge
27
+ // #region Shared trend cache _summaryCache + _subscribeSummary
28
+ // #region AlertProvider createHostMemoryAlertProvider:
29
+ // poll + emitIfChanged hysteresis
30
+ // #region Icon helpers small inline SVG icons
31
+ // #region ErrorBoundary class error boundary for modal
32
+ // #region Styles inline stylesheet objects
33
+ // #region MonitorConfigModal memory-only config modal + trend
34
+ // #region Monitor toggle helpers enable/disable toggle plumbing
35
+ // #region Client factory inject/apply/dispose entry point
36
+ //
37
+ // DEPENDENCY/NOTE: this is deliberately a SINGLE FILE with no build step.
38
+ // • Everything shares the `factory(require)` closure scope; no cross-file
39
+ // verbundling. Threshold DEFAULT values mirror the HOST constants in
40
+ // src/index.ts (change one, change both — see _MEM_ALERT_DEFAULTS).
41
+ // • `mode=summary` polling is de-duplicated via the Shared trend cache;
42
+ // `mode=full` (sparkline) is fetched independently by the modal.
43
+ //
44
+ // ───────────────────────────────────────────────────────────────────────────
45
+
46
+ window.__ModuleLoader__.load({
47
+ id: 'dsh-flash-mem-mon',
48
+ factory: (require) => {
49
+
50
+ var React = require('react')
51
+ var h = React.createElement
52
+ var useState = React.useState
53
+ var useEffect = React.useEffect
54
+ var useRef = React.useRef
55
+ var Component = React.Component
56
+ var ReactDOMClient = null
57
+ try { ReactDOMClient = require('react-dom/client') } catch (_) {}
58
+
59
+ // ═══════════════════════════════════════════════════════════════════════
60
+ //#region i18n ────────────────────────────────────────────────────────────
61
+
62
+ var zh = {
63
+ alertMemCluster: '性能监控',
64
+ monitorOn: '已开启',
65
+ monitorOff: '已关闭',
66
+ monitorConfig: '配置',
67
+ // Memory thresholds
68
+ memConfigGroupRss: 'RSS 阈值 / 轮询',
69
+ memConfigGroupGc: 'Major GC 阈值',
70
+ memThresholdInfo: 'RSS 信息阈值',
71
+ memThresholdInfoTip: 'RSS(常驻内存集)超过此值时触发信息级告警。信息级仅提示,不影响行为。建议设为正常负载的 1.5–2 倍。',
72
+ memThresholdWarning: 'RSS 警告阈值',
73
+ memThresholdWarningTip: 'RSS 超过此值时触发警告级告警。此时内存压力较大,应关注是否有泄漏。必须大于信息阈值。',
74
+ memThresholdError: 'RSS 错误阈值',
75
+ memThresholdErrorTip: 'RSS 超过此值时触发错误级告警。内存已接近危险区,建议尽快排查。必须大于警告阈值。',
76
+ memThresholdCritical: 'RSS 严重阈值',
77
+ memThresholdCriticalTip: 'RSS 超过此值时触发严重级告警。进程可能即将被 OOM Killer 终止,需立即处理。必须大于错误阈值。',
78
+ memPollBase: '内存轮询基础间隔',
79
+ memPollBaseTip: '堆使用率较低时的采样间隔(天花板)。空闲时以这个频率采样,堆越满采样越密。默认 30 秒,一般不需要调低。',
80
+ memPollMin: '内存轮询最小间隔',
81
+ memPollMinTip: '堆使用率很高时的采样间隔(地板)。无论堆多满,两次采样不会比这更密,避免过度消耗 CPU。默认 2 秒。',
82
+ memGrowthThreshold: 'RSS 增长率阈值',
83
+ memGrowthThresholdTip: 'RSS 线性回归增长率超过此值(且趋势上升)时触发“持续增长”告警。只有一个固定阈值,不区分告警级别。默认 1 MB/min。',
84
+ // GC thresholds
85
+ gcThresholdInfo: 'Major GC 信息阈值',
86
+ gcThresholdInfoTip: '过去 5 分钟内 Major GC(MarkSweep/MarkCompact)频率超过此值时触发信息级告警。1/min 即每分钟一次 Full GC,正常负载下应远低于此。',
87
+ gcThresholdWarning: 'Major GC 警告阈值',
88
+ gcThresholdWarningTip: 'Major GC 频率超过此值时触发警告级告警。GC 已频繁到影响吞吐量,堆分配/回收振荡剧烈。必须大于信息阈值。',
89
+ gcThresholdError: 'Major GC 错误阈值',
90
+ gcThresholdErrorTip: 'Major GC 频率超过此值时触发错误级告警。GC 自身开销占比很高,接近或已进入泄漏状态。必须大于警告阈值。',
91
+ // Alert preview
92
+ alertPreviewTitle: '告警示例',
93
+ alertPreviewToggle: '查看告警消息样例',
94
+ alertPreviewCollapse: '收起',
95
+ alertPreviewCriteriaRss: 'RSS 绝对值',
96
+ alertPreviewCriteriaGrowth: 'RSS 增长率',
97
+ alertPreviewCriteriaGc: 'Major GC 频率',
98
+ alertPreviewTrigger: '触发',
99
+ // Alert title (includes severity level and threshold value)
100
+ alertHostMemInfo: 'RSS 超过信息级阈值 {threshold} MB',
101
+ alertHostMemWarning: 'RSS 超过警告级阈值 {threshold} MB',
102
+ alertHostMemError: 'RSS 超过错误级阈值 {threshold} MB',
103
+ alertHostMemCritical: 'RSS 超过严重级阈值 {threshold} MB',
104
+ alertHostMemMsg: 'RSS {v} MB | 堆 {heap}/{heapTotal} MB ({pct}%) | 外部 {ext} MB | ArrayBuffer {ab} MB | 趋势 {trend} | 增长 +{slope} MB/min{gcInfo}',
105
+ alertHostMemGrowth: 'RSS 持续增长',
106
+ alertHostMemGrowthMsg: '+{r} MB/min | RSS {rss} MB | 堆 {heap}/{heapTotal} MB ({pct}%) | ArrayBuffer {ab} MB | 趋势 {trend}',
107
+ alertHostGcInfo: 'GC 频率较高',
108
+ alertHostGcWarning: 'GC 频率异常',
109
+ alertHostGcError: 'GC 频率极高',
110
+ alertHostGcMsg: 'Major {v} 次/min | 暂停 {pause} ms/min | Minor {minor} 次 | RSS {rss} MB | 堆 {heap}/{heapTotal} MB ({pct}%)',
111
+ // Alert disposal buttons (registered via `actions` — see dsh-flash contract)
112
+ alertDisposeDumpHeap: '立即转储堆快照',
113
+ alertDumping: '正在转储堆快照…',
114
+ alertDumpSaved: '已保存快照 {name}({size}),可在 DevTools → Memory → Load 中载入分析',
115
+ alertDumpFailed: '堆快照失败:{error}',
116
+ alertSevInfo: '信息',
117
+ alertSevWarning: '警告',
118
+ alertSevError: '错误',
119
+ alertSevCritical: '严重',
120
+ // Memory live data
121
+ memLiveData: '当前数据',
122
+ // Host process memory
123
+ memHostSection: 'Host 进程',
124
+ memHostRSS: '常驻内存',
125
+ memHostHeapTotal: '堆总大小',
126
+ memHostHeapUsed: '堆已用',
127
+ memHostExternal: 'C++ 外部',
128
+ memHostArrayBuffers: 'ArrayBuffer',
129
+ memHostHeapRatio: '堆使用率',
130
+ memHostUnavailable: '无法获取 Host 内存数据',
131
+ // GC stats
132
+ memGcMajorPerMin: 'Major GC/min',
133
+ memGcPausePerMin: 'GC 暂停/min',
134
+ memGcMinorCount: 'Minor GC',
135
+ memGcMajorCount: 'Major GC',
136
+ memGcUnavailable: 'GC 监控不可用',
137
+ memGcEventList: 'Full GC 记录',
138
+ memGcEventDuration: '耗时',
139
+ memGcEventTime: '时间',
140
+ memGcEventClick: '点击定位',
141
+ memGcNoEvents: '近期无 Full GC',
142
+ memGcEventRssDrop: '内存释放',
143
+ // Memory trend
144
+ memTrend: '内存趋势',
145
+ memTrendPeak: 'RSS 峰值',
146
+ memTrendDirection: '趋势',
147
+ memTrendUp: '↑ 上升',
148
+ memTrendStable: '→ 稳定',
149
+ memTrendDown: '↓ 下降',
150
+ memTrendSamples: '采样数',
151
+ memTrendSpan: '跨度',
152
+ memTrendHeapRatio: '堆使用率',
153
+ memTrendNoData: '暂无数据',
154
+ memTrendHoverHeap: '堆',
155
+ memTrendHoverRSS: 'RSS',
156
+ memTrendHoverTime: '时间',
157
+ memTrendGrowthRate: 'RSS 增长率',
158
+ memTrendAxisHeap: '堆',
159
+ memTrendAxisTime: '时间',
160
+ memTrendInfo: '趋势信息',
161
+ // Session footprint
162
+ footprintTitle: '会话足迹',
163
+ footprintTotalSessions: '总会话数',
164
+ footprintArchivedSessions: '归档会话数',
165
+ footprintOnDisk: '磁盘占用',
166
+ footprintNoData: '暂无足迹数据',
167
+ }
168
+
169
+ var en = {
170
+ alertMemCluster: 'Performance Monitor',
171
+ monitorOn: 'Enabled',
172
+ monitorOff: 'Disabled',
173
+ monitorConfig: 'Configure',
174
+ // Memory thresholds
175
+ memConfigGroupRss: 'RSS / Polling',
176
+ memConfigGroupGc: 'Major GC',
177
+ memThresholdInfo: 'RSS Info Threshold',
178
+ memThresholdInfoTip: 'Triggers an info-level alert when RSS (Resident Set Size) exceeds this value. Info is informational only. Recommended: 1.5–2× your normal workload RSS.',
179
+ memThresholdWarning: 'RSS Warning Threshold',
180
+ memThresholdWarningTip: 'Triggers a warning alert when RSS exceeds this value. Memory pressure is significant — watch for leaks. Must be greater than the Info threshold.',
181
+ memThresholdError: 'RSS Error Threshold',
182
+ memThresholdErrorTip: 'Triggers an error alert when RSS exceeds this value. Memory is in the danger zone — investigate promptly. Must be greater than the Warning threshold.',
183
+ memThresholdCritical: 'RSS Critical Threshold',
184
+ memThresholdCriticalTip: 'Triggers a critical alert when RSS exceeds this value. The process may be OOM-killed soon — act immediately. Must be greater than the Error threshold.',
185
+ memPollBase: 'Memory Poll Base Interval',
186
+ memPollBaseTip: 'Sampling interval when heap usage is low (the ceiling). At idle, sampling runs at this pace; as heap fills up, sampling gets denser automatically. Default 30s — rarely needs lowering.',
187
+ memPollMin: 'Memory Poll Min Interval',
188
+ memPollMinTip: 'Minimum sampling interval even when heap is full (the floor). Prevents excessive CPU use from too-frequent sampling. Default 2s.',
189
+ memGrowthThreshold: 'RSS Growth Rate Threshold',
190
+ memGrowthThresholdTip: 'Triggers the "steadily growing" alert when the RSS linear-regression slope exceeds this value (and the trend is rising). Single fixed threshold, no severity tiers. Default 1 MB/min.',
191
+ // GC thresholds
192
+ gcThresholdInfo: 'Major GC Info Threshold',
193
+ gcThresholdInfoTip: 'Triggers an info alert when Major GC (MarkSweep/MarkCompact) frequency over the last 5 minutes exceeds this value. 1/min means one Full GC per minute — normal workloads should be well below this.',
194
+ gcThresholdWarning: 'Major GC Warning Threshold',
195
+ gcThresholdWarningTip: 'Triggers a warning when Major GC frequency exceeds this value. GC is frequent enough to hurt throughput — the heap is oscillating heavily. Must be greater than the Info threshold.',
196
+ gcThresholdError: 'Major GC Error Threshold',
197
+ gcThresholdErrorTip: 'Triggers an error when Major GC frequency exceeds this value. GC overhead is dominant — likely a leak. Must be greater than the Warning threshold.',
198
+ // Alert preview
199
+ alertPreviewTitle: 'Alert Preview',
200
+ alertPreviewToggle: 'View alert message samples',
201
+ alertPreviewCollapse: 'Collapse',
202
+ alertPreviewCriteriaRss: 'RSS Absolute',
203
+ alertPreviewCriteriaGrowth: 'RSS Growth',
204
+ alertPreviewCriteriaGc: 'Major GC Freq',
205
+ alertPreviewTrigger: 'trigger',
206
+ // Alert title (includes severity level and threshold value)
207
+ alertHostMemInfo: 'RSS exceeded info threshold {threshold} MB',
208
+ alertHostMemWarning: 'RSS exceeded warning threshold {threshold} MB',
209
+ alertHostMemError: 'RSS exceeded error threshold {threshold} MB',
210
+ alertHostMemCritical: 'RSS exceeded critical threshold {threshold} MB',
211
+ alertHostMemMsg: 'RSS {v} MB | Heap {heap}/{heapTotal} MB ({pct}%) | Ext {ext} MB | ArrayBuffer {ab} MB | Trend {trend} | Growth +{slope} MB/min{gcInfo}',
212
+ alertHostMemGrowth: 'RSS steadily growing',
213
+ alertHostMemGrowthMsg: '+{r} MB/min | RSS {rss} MB | Heap {heap}/{heapTotal} MB ({pct}%) | ArrayBuffer {ab} MB | Trend {trend}',
214
+ alertHostGcInfo: 'GC frequency elevated',
215
+ alertHostGcWarning: 'GC frequency abnormal',
216
+ alertHostGcError: 'GC frequency critical',
217
+ alertHostGcMsg: 'Major {v}/min | Pause {pause} ms/min | Minor {minor} | RSS {rss} MB | Heap {heap}/{heapTotal} MB ({pct}%)',
218
+ // Alert disposal buttons (registered via `actions` — see dsh-flash contract)
219
+ alertDisposeDumpHeap: 'Dump heap now',
220
+ alertDumping: 'Dumping heap snapshot…',
221
+ alertDumpSaved: 'Saved snapshot {name} ({size}). Load it in DevTools → Memory to analyze.',
222
+ alertDumpFailed: 'Heap dump failed: {error}',
223
+ alertSevInfo: 'Info',
224
+ alertSevWarning: 'Warning',
225
+ alertSevError: 'Error',
226
+ alertSevCritical: 'Critical',
227
+ // Memory live data
228
+ memLiveData: 'Live Data',
229
+ // Host process memory
230
+ memHostSection: 'Host Process',
231
+ memHostRSS: 'RSS',
232
+ memHostHeapTotal: 'Heap Total',
233
+ memHostHeapUsed: 'Heap Used',
234
+ memHostExternal: 'C++ External',
235
+ memHostArrayBuffers: 'ArrayBuffer',
236
+ memHostHeapRatio: 'Heap Usage',
237
+ memHostUnavailable: 'Host memory data unavailable',
238
+ // GC stats
239
+ memGcMajorPerMin: 'Major GC/min',
240
+ memGcPausePerMin: 'GC Pause/min',
241
+ memGcMinorCount: 'Minor GC',
242
+ memGcMajorCount: 'Major GC',
243
+ memGcUnavailable: 'GC monitoring unavailable',
244
+ memGcEventList: 'Full GC Events',
245
+ memGcEventDuration: 'Duration',
246
+ memGcEventTime: 'Time',
247
+ memGcEventClick: 'Click to locate',
248
+ memGcNoEvents: 'No Full GC recently',
249
+ memGcEventRssDrop: 'Mem Release',
250
+ // Memory trend
251
+ memTrend: 'Memory Trend',
252
+ memTrendPeak: 'RSS Peak',
253
+ memTrendDirection: 'Trend',
254
+ memTrendUp: '↑ Rising',
255
+ memTrendStable: '→ Stable',
256
+ memTrendDown: '↓ Falling',
257
+ memTrendSamples: 'Samples',
258
+ memTrendSpan: 'Span',
259
+ memTrendHeapRatio: 'Heap Usage',
260
+ memTrendNoData: 'No data yet',
261
+ memTrendHoverHeap: 'Heap',
262
+ memTrendHoverRSS: 'RSS',
263
+ memTrendHoverTime: 'Time',
264
+ memTrendGrowthRate: 'RSS Growth Rate',
265
+ memTrendAxisHeap: 'Heap',
266
+ memTrendAxisTime: 'Time',
267
+ memTrendInfo: 'Trend Info',
268
+ // Session footprint
269
+ footprintTitle: 'Session Footprint',
270
+ footprintTotalSessions: 'Total Sessions',
271
+ footprintArchivedSessions: 'Archived Sessions',
272
+ footprintOnDisk: 'On Disk',
273
+ footprintNoData: 'No footprint data yet',
274
+ }
275
+
276
+ // ── i18n: a proxy over DSH's official locale framework ─────────────────
277
+ // The authority is the client Cordis service `locale`, published by
278
+ // @deepseek-ai/dsh-client-locale. It owns the active-locale preference
279
+ // (Host-backed, browser-derived until the Host answers), the dictionary
280
+ // registry, the `locale/change` event AND `<html lang>` — which is why
281
+ // nothing here touches that attribute any more. The MutationObserver this
282
+ // file used to keep watched an attribute ANOTHER package owns and inferred a
283
+ // preference from it: it could not see a switch that had not reached the DOM
284
+ // yet, and it fired for a value nobody had chosen. Registering our tables
285
+ // with the service and reading through it removes both problems, and makes
286
+ // this plugin follow the global language by contract instead of by accident.
287
+ //
288
+ // The public surface is unchanged — t(key) / L(key) / t.onLocaleChange() —
289
+ // because every call site in this file already reads through it. There is
290
+ // deliberately NO t.setLocale(): this plugin is a pure reader, the language
291
+ // row belongs to dsh-flash, and a second writer would be exactly the drift
292
+ // this block exists to remove.
293
+
294
+ var I18N_NS = 'dsh-flash-mem-mon'
295
+ var LOCALES = { zh: zh, en: en }
296
+ var FALLBACK_LOCALE = 'en'
297
+ var _localeListeners = new Set()
298
+
299
+ /** Fallback active locale — what the official runtime does with no explicit
300
+ * choice: the browser's own language, matched on the PRIMARY subtag. */
301
+ function resolveFallbackLocale() {
302
+ try {
303
+ if (typeof navigator === 'undefined') return FALLBACK_LOCALE
304
+ var tags = [].concat(navigator.languages || [], navigator.language || [])
305
+ for (var i = 0; i < tags.length; i++) {
306
+ var primary = String(tags[i] || '').toLowerCase().split('-')[0]
307
+ if (LOCALES[primary]) return primary
308
+ }
309
+ } catch (_) {}
310
+ return FALLBACK_LOCALE
311
+ }
312
+
313
+ // The active-locale source. `get()` reads the authority; `id` records WHICH
314
+ // authority answered, so a service that arrives LATER (module order can hand
315
+ // us nothing at apply time) can be adopted, and a lost one released.
316
+ var _fallbackLocaleKey = resolveFallbackLocale()
317
+ var _localeSource = { id: 'fallback', get: function () { return _fallbackLocaleKey } }
318
+
319
+ // Single fan-out point for "the active locale actually changed". Drives two
320
+ // kinds of consumer: the imperative surfaces this plugin does NOT render
321
+ // (Quick Control row, alert panel — nudged via refreshLocaleSurfaces, hoisted
322
+ // from the toggle-helpers region), and the React subscribers registered with
323
+ // t.onLocaleChange() (the open config modal). Called from the factory's
324
+ // `locale/change` listener, and from the fallback path in bindLocaleService.
325
+ function notifyLocaleChange() {
326
+ try { refreshLocaleSurfaces() } catch (_) {}
327
+ _localeListeners.forEach(function (fn) { try { fn() } catch (_) {} })
328
+ }
329
+
330
+ /**
331
+ * Point the i18n proxy at DSH's `locale` service: hand the zh/en tables to
332
+ * the official registry under I18N_NS and route every READ through it.
333
+ *
334
+ * Change NOTIFICATION is not wired here. The service's `subscribe()` is the
335
+ * LocaleFace render channel and fires on every revision bump, including
336
+ * dictionary registration — wrong trigger for our imperative nudge. The
337
+ * factory listens to `locale/change` (emitted only on a real switch) and
338
+ * calls notifyLocaleChange(). See the factory's step 0 for why.
339
+ *
340
+ * Two throws in the official API shape this function:
341
+ * - `register(ns, dicts)` REJECTS a locale already registered under the
342
+ * namespace, so any prior claim is released before a new one is made and
343
+ * the claim is never attempted twice for one service;
344
+ * - `setLocale(id)` REJECTS an unregistered id — not our problem here,
345
+ * since this plugin never writes the locale.
346
+ *
347
+ * Idempotent, because apply() calls it from two places: eagerly, for the
348
+ * case where `locale` is already provided, and from `ctx.inject(['locale'])`
349
+ * for the case where it arrives later. Neither is a second registration.
350
+ *
351
+ * @param {any} localeService - the `locale` service, a thunk returning it,
352
+ * or undefined to keep the built-in fallback.
353
+ * @returns {boolean} whether the official service is now in force.
354
+ */
355
+ function bindLocaleService(localeService) {
356
+ var svc = null
357
+ try { svc = typeof localeService === 'function' ? localeService() : localeService } catch (_) { svc = undefined }
358
+ var owns = !!svc && typeof svc.register === 'function' && typeof svc.bind === 'function'
359
+ && typeof svc.getSnapshot === 'function'
360
+ if (!owns) {
361
+ // Nothing to bind. A read that TRANSIENTLY finds no service must not
362
+ // tear down a working registration, so only the fallback is refreshed —
363
+ // which is also the first call, where the browser tag may have settled
364
+ // after this module was evaluated.
365
+ if (_localeSource.id !== 'fallback') return false
366
+ var next = resolveFallbackLocale()
367
+ if (next === _fallbackLocaleKey) return false
368
+ _fallbackLocaleKey = next
369
+ notifyLocaleChange()
370
+ return false
371
+ }
372
+ if (_localeSource.id === 'service' && _localeSource.service === svc) return true
373
+ // A different instance is taking over: give up the old claim on the
374
+ // namespace before making a new one, or the new one would be refused.
375
+ releaseLocaleBinding()
376
+ var disposeRegister = function () {}
377
+ try {
378
+ disposeRegister = svc.register(I18N_NS, { zh: zh, en: en }) || disposeRegister
379
+ } catch (err) {
380
+ console.warn('[dsh-flash-mem-mon] the `locale` service refused our dictionaries — ' +
381
+ 'keeping the built-in zh/en tables for this session:', err)
382
+ return false
383
+ }
384
+ _localeSource = {
385
+ id: 'service',
386
+ service: svc,
387
+ get: function () { try { return svc.getSnapshot().active } catch (_) { return undefined } },
388
+ dispose: function () {
389
+ try { disposeRegister() } catch (_) {}
390
+ },
391
+ }
392
+ // Do NOT re-notify here on purpose. `register()` bumps the snapshot
393
+ // revision so already-rendered LocaleFace outlets pick up our late
394
+ // dictionaries, but a registration is not a language switch — nothing
395
+ // this plugin renders changed language. The refresh nudge is driven only
396
+ // by `locale/change`, wired in the factory.
397
+ return true
398
+ }
399
+
400
+ /** Undo whatever dictionary claim is in force and fall back.
401
+ *
402
+ * One claim, one release, both scoped to the plugin fiber: a remount or a
403
+ * disable→enable cycle that re-registered the namespace each time is
404
+ * exactly what `register`'s duplicate guard punishes. */
405
+ function releaseLocaleBinding() {
406
+ var source = _localeSource
407
+ if (source.id !== 'service') return
408
+ _localeSource = { id: 'fallback', get: function () { return _fallbackLocaleKey } }
409
+ try { source.dispose() } catch (_) {}
410
+ }
411
+
412
+ /** Current locale key ('zh' | 'en'), whatever authority is in force. */
413
+ function currentLocaleKey() {
414
+ var active = _localeSource.get()
415
+ return LOCALES[active] ? active : FALLBACK_LOCALE
416
+ }
417
+
418
+ /** i18n lookup — a read of the official service whenever it is bound. */
419
+ function t(key) {
420
+ if (_localeSource.id === 'service') {
421
+ try {
422
+ var value = _localeSource.service.bind(I18N_NS)(key)
423
+ // The official lookup ends at the key itself; ours ends at the en
424
+ // table, so a zh-only key still reads as English rather than as its
425
+ // own name.
426
+ if (value !== undefined && value !== key) return value
427
+ } catch (_) {}
428
+ }
429
+ var dict = LOCALES[currentLocaleKey()] || LOCALES[FALLBACK_LOCALE]
430
+ return dict[key] || LOCALES[FALLBACK_LOCALE][key] || key
431
+ }
432
+
433
+ /** Get the current locale key ('zh' | 'en'). */
434
+ t.getLocale = currentLocaleKey
435
+
436
+ /** Subscribe to locale changes; returns a disposer. */
437
+ t.onLocaleChange = function (fn) {
438
+ _localeListeners.add(fn)
439
+ return function () { _localeListeners.delete(fn) }
440
+ }
441
+
442
+ /** Functional label helper — returns a function that calls t() so the label
443
+ * updates when the locale service publishes a new active locale. */
444
+ function L(key) {
445
+ return function () { return t(key) }
446
+ }
447
+
448
+ //#endregion ───────────────────────────────────────────────────────────────
449
+
450
+ // ═══════════════════════════════════════════════════════════════════════
451
+ //#region Preference system ──────────────────────────────────────────────
452
+
453
+ // NOTE: these defaults MIRROR the DEFAULT_MEM_*/DEFAULT_GC_* constants in
454
+ // the HOST half (src/index.ts). They are only used as a last-resort fallback
455
+ // when the remote 'dsh-flash-mem-mon' settings namespace is empty/unreadable;
456
+ // at runtime the authoritative values come from that namespace. If you change
457
+ // a threshold default, update src/index.ts AND this table together.
458
+ var _MEM_ALERT_DEFAULTS = {
459
+ memThresholdInfo: 256,
460
+ memThresholdWarning: 512,
461
+ memThresholdError: 1024,
462
+ memThresholdCritical: 1536,
463
+ memPollBase: 60000,
464
+ memPollMin: 2000,
465
+ gcThresholdInfo: 2,
466
+ gcThresholdWarning: 5,
467
+ gcThresholdError: 10,
468
+ memGrowthAlertPerMin: 1,
469
+ }
470
+ var _memPrefs = Object.assign({}, _MEM_ALERT_DEFAULTS)
471
+ var _prefCtx = null
472
+ var MEM_MON_NS = 'dsh-flash-mem-mon'
473
+ var _hostRevision = null
474
+ var _prefWriteTail = Promise.resolve()
475
+ var _pendingRevision = null
476
+
477
+ function _fenceRevision() {
478
+ if (_pendingRevision !== null) return _pendingRevision
479
+ return _hostRevision === null ? undefined : _hostRevision
480
+ }
481
+
482
+ function _remoteSettings(ctx) {
483
+ try {
484
+ if (ctx && ctx.remote && ctx.remote.settings) return ctx.remote.settings
485
+ } catch (e) {
486
+ console.warn('[dsh-flash-mem-mon] ctx.remote.settings threw:', e && e.message)
487
+ }
488
+ try {
489
+ var remote = ctx && ctx.get ? ctx.get('remote') : undefined
490
+ if (remote && remote.settings) return remote.settings
491
+ } catch (e) { console.warn('[dsh-flash-mem-mon] ctx.get("remote") threw:', e && e.message) }
492
+ return undefined
493
+ }
494
+
306
495
  /** Normalize a `describe()` result into a promise. NEVER let this throw.
307
496
  *
308
497
  * Calling `.then` directly on a `describe()` result is not safe: the SAME
@@ -324,1769 +513,2104 @@ window.__ModuleLoader__.load({
324
513
  catch (err) { return Promise.reject(err) }
325
514
  }
326
515
 
327
- var _PREFS_MAX_RETRIES = 8
328
- var _PREFS_RETRY_DELAY_MS = 750
329
-
330
- function loadMemPrefs(ctx, _retryCount) {
331
- if (typeof _retryCount !== 'number') _retryCount = 0
332
- var settings = _remoteSettings(ctx)
333
- if (!settings) return Promise.resolve(false)
334
- if (typeof settings.describe !== 'function') return Promise.resolve(false)
335
- return _describeAsync(settings).then(function (desc) {
336
- if (!desc) return false
337
- var view = desc.value || desc
338
- var list = view && Array.isArray(view.namespaces)
339
- ? view.namespaces
340
- : (Array.isArray(view) ? view : null)
341
- if (!list) return false
342
- var ns = list.find(function (n) {
343
- return (n && (n.ns || n.namespace)) === MEM_MON_NS
344
- })
345
- if (!ns) {
346
- if (_retryCount < _PREFS_MAX_RETRIES) {
347
- return new Promise(function (resolve) {
348
- setTimeout(function () {
349
- resolve(loadMemPrefs(ctx, _retryCount + 1))
350
- }, _PREFS_RETRY_DELAY_MS)
351
- })
352
- }
353
- return false
354
- }
355
- var resolved = ns.value || ns.resolved
356
- if (!resolved) return false
357
- _memPrefs = {
358
- memThresholdInfo: resolved.memThresholdInfo,
359
- memThresholdWarning: resolved.memThresholdWarning,
360
- memThresholdError: resolved.memThresholdError,
361
- memThresholdCritical: resolved.memThresholdCritical,
362
- memPollBase: resolved.memPollBase,
363
- memPollMin: resolved.memPollMin,
364
- gcThresholdInfo: resolved.gcThresholdInfo,
365
- gcThresholdWarning: resolved.gcThresholdWarning,
366
- gcThresholdError: resolved.gcThresholdError,
367
- }
368
- if (typeof ns.revision === 'number') _hostRevision = ns.revision
369
- console.log('[dsh-flash-mem-mon] preferences loaded from ' + MEM_MON_NS + ' namespace')
370
- return true
371
- }).catch(function () {
372
- return false
373
- })
374
- }
375
-
376
- /** Read an alert preference from _memPrefs, falling back to the built-in default. */
377
- function _alertPref(key) {
378
- var v = _memPrefs && _memPrefs[key]
379
- return (typeof v === 'number' && isFinite(v)) ? v : _MEM_ALERT_DEFAULTS[key]
380
- }
381
-
382
- /** Write an alert preference through the preference bridge (host + memory). */
383
- function _writeAlertPref(ctx, key, value, onError) {
384
- savePrefs(ctx, Object.fromEntries([[key, value]]), null, onError)
385
- }
386
-
387
- /** Save preferences: update _memPrefs in memory, then serialize a write to the 'dsh-flash-mem-mon' settings namespace. */
388
- function savePrefs(ctx, patch, localWrites, onError) {
389
- var saved = {}
390
- for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) saved[k] = _memPrefs && _memPrefs[k]
391
- if (_memPrefs) {
392
- for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) _memPrefs[k] = patch[k]
393
- }
394
- try { if (localWrites) localWrites() } catch (_) {}
395
- return _queuePrefWrite(ctx, patch, onError, saved)
396
- }
397
-
398
- function _queuePrefWrite(ctx, patch, onError, saved) {
399
- var task = _prefWriteTail.then(function () {
400
- var settings = _remoteSettings(ctx || _prefCtx)
401
- if (!settings || typeof settings.update !== 'function') {
402
- if (onError) onError(patch, function () {
403
- for (var k in saved) if (Object.prototype.hasOwnProperty.call(saved, k)) _memPrefs[k] = saved[k]
404
- })
405
- return false
406
- }
407
- return settings.update(MEM_MON_NS, patch, _fenceRevision())
408
- .then(function (res) {
409
- try {
410
- if (res && res.ok === false) {
411
- _pendingRevision = null
412
- if (onError) onError(patch, function () {
413
- for (var k in saved) if (Object.prototype.hasOwnProperty.call(saved, k)) _memPrefs[k] = saved[k]
414
- })
415
- try {
416
- _describeAsync(settings).then(function (desc) {
417
- if (desc && desc.ok !== false) {
418
- var view2 = desc.value || desc
419
- var list2 = view2 && Array.isArray(view2.namespaces)
420
- ? view2.namespaces
421
- : (Array.isArray(view2) ? view2 : null)
422
- var ns2 = list2 && list2.find(function (n) {
423
- return (n && (n.ns || n.namespace)) === MEM_MON_NS
424
- })
425
- if (ns2 && typeof ns2.revision === 'number') _hostRevision = ns2.revision
426
- }
427
- }).catch(function () {})
428
- } catch (_) {}
429
- return false
430
- }
431
- var v = res && res.value
432
- if (v && typeof v.revision === 'number') {
433
- _pendingRevision = v.revision
434
- _hostRevision = v.revision
435
- }
436
- } catch (_) {}
437
- return true
438
- }).catch(function (err) {
439
- _pendingRevision = null
440
- if (onError) onError(patch, function () {
441
- for (var k in saved) if (Object.prototype.hasOwnProperty.call(saved, k)) _memPrefs[k] = saved[k]
442
- })
443
- return false
444
- })
445
- })
446
- _prefWriteTail = task.then(function () {}, function () {})
447
- return task
448
- }
449
-
450
- //#endregion ───────────────────────────────────────────────────────────────
451
-
452
- // ═══════════════════════════════════════════════════════════════════════
453
- //#region AlertProvider ──────────────────────────────────────────────────
454
-
455
- function createHostMemoryAlertProvider() {
456
- var timer = null
457
- var callback = null
458
-
459
- function poll() {
460
- if (!callback) return
461
- var alerts = []
462
- fetch('/plugins/dsh-flash-mem-mon/memory-trend?mode=summary')
463
- .then(function (r) { return r.json() })
464
- .then(function (d) {
465
- if (!d || !d.current) {
466
- try { callback(alerts) } catch (_) {}
467
- return
468
- }
469
-
470
- var rssMB = Math.round(d.current.rss / 1048576)
471
- var heapMB = Math.round(d.current.heapUsed / 1048576)
472
- var heapTotalMB = Math.round(d.current.heapTotal / 1048576)
473
- var extMB = Math.round(d.current.external / 1048576)
474
- var abMB = Math.round(d.current.arrayBuffers / 1048576)
475
- var heapPct = d.current.heapTotal > 0 ? Math.round(d.current.heapUsed / d.current.heapTotal * 100) : 0
476
- var trendLabel = d.trend === 'up' ? '↑' : d.trend === 'down' ? '↓' : '→'
477
- var rssSlope = d.rssSlopePerMin || 0
478
- var slopeLabel = rssSlope.toFixed(1)
479
- // GC summary snippet for RSS alerts
480
- var gcInfoSnippet = ''
481
- if (d.gc && d.gc.majorPerMin > 0) {
482
- gcInfoSnippet = ' | GC Major ' + d.gc.majorPerMin.toFixed(1) + '/min 暂停 ' + (d.gc.gcPausePerMin || 0).toFixed(0) + 'ms/min'
483
- }
484
-
485
- // ── Criterion 1: RSS absolute value ────────────────────────
486
- var tInfo = _alertPref('memThresholdInfo')
487
- var tWarn = _alertPref('memThresholdWarning')
488
- var tErr = _alertPref('memThresholdError')
489
- var tCrit = _alertPref('memThresholdCritical')
490
- // Validate: info < warning < error < critical
491
- if (!(tInfo > 0 && tWarn > tInfo && tErr > tWarn && tCrit > tErr)) {
492
- tInfo = 256; tWarn = 512; tErr = 1024; tCrit = 1536
493
- }
494
-
495
- // Shared RSS alert message formatter.
496
- var _rssMsg = function () {
497
- return t('alertHostMemMsg')
498
- .replace('{v}', rssMB)
499
- .replace('{heap}', heapMB)
500
- .replace('{heapTotal}', heapTotalMB)
501
- .replace('{pct}', heapPct)
502
- .replace('{ext}', extMB)
503
- .replace('{ab}', abMB)
504
- .replace('{trend}', trendLabel)
505
- .replace('{slope}', slopeLabel)
506
- .replace('{gcInfo}', gcInfoSnippet)
507
- }
508
-
509
- if (rssMB >= tCrit) {
510
- alerts.push({
511
- id: 'host-mem-critical',
512
- severity: 'critical',
513
- title: function () { return t('alertHostMemCritical').replace('{threshold}', tCrit) },
514
- message: _rssMsg,
515
- icon: '🔴',
516
- timestamp: Date.now(),
517
- dismissible: true,
518
- })
519
- } else if (rssMB >= tErr) {
520
- alerts.push({
521
- id: 'host-mem-error',
522
- severity: 'error',
523
- title: function () { return t('alertHostMemError').replace('{threshold}', tErr) },
524
- message: _rssMsg,
525
- icon: '🟠',
526
- timestamp: Date.now(),
527
- dismissible: true,
528
- })
529
- } else if (rssMB >= tWarn) {
530
- alerts.push({
531
- id: 'host-mem-warning',
532
- severity: 'warning',
533
- title: function () { return t('alertHostMemWarning').replace('{threshold}', tWarn) },
534
- message: _rssMsg,
535
- icon: '🟡',
536
- timestamp: Date.now(),
537
- dismissible: true,
538
- })
539
- } else if (rssMB >= tInfo) {
540
- alerts.push({
541
- id: 'host-mem-info',
542
- severity: 'info',
543
- title: function () { return t('alertHostMemInfo').replace('{threshold}', tInfo) },
544
- message: _rssMsg,
545
- icon: '🔵',
546
- timestamp: Date.now(),
547
- dismissible: true,
548
- })
549
- }
550
-
551
- // ── Criterion 2: RSS growth rate ───────────────────────────
552
- if (rssSlope > 1 && d.trend === 'up') {
553
- var hasRssAlert = alerts.some(function (a) {
554
- return a.id.indexOf('host-mem-') === 0
555
- })
556
- if (!hasRssAlert) {
557
- alerts.push({
558
- id: 'host-mem-growth',
559
- severity: 'info',
560
- title: function () { return t('alertHostMemGrowth') },
561
- message: function () {
562
- return t('alertHostMemGrowthMsg')
563
- .replace('{r}', rssSlope.toFixed(1))
564
- .replace('{rss}', rssMB)
565
- .replace('{heap}', heapMB)
566
- .replace('{heapTotal}', heapTotalMB)
567
- .replace('{pct}', heapPct)
568
- .replace('{ab}', abMB)
569
- .replace('{trend}', trendLabel)
570
- },
571
- icon: '📈',
572
- timestamp: Date.now(),
573
- dismissible: true,
574
- })
575
- }
576
- }
577
-
578
- // ── Criterion 3: Major GC frequency ────────────────────────
579
- if (d.gc && d.gc.majorPerMin > 0) {
580
- var gcInfo = _alertPref('gcThresholdInfo')
581
- var gcWarn = _alertPref('gcThresholdWarning')
582
- var gcErr = _alertPref('gcThresholdError')
583
- if (!(gcInfo > 0 && gcWarn > gcInfo && gcErr > gcWarn)) {
584
- gcInfo = 2; gcWarn = 5; gcErr = 10
585
- }
586
- var mpm = d.gc.majorPerMin
587
- var gcPausePerMin = d.gc.gcPausePerMin || 0
588
- var minorCount = d.gc.minorCount || 0
589
-
590
- // Shared GC alert message formatter.
591
- var _gcMsg = function () {
592
- return t('alertHostGcMsg')
593
- .replace('{v}', mpm.toFixed(1))
594
- .replace('{pause}', gcPausePerMin.toFixed(1))
595
- .replace('{minor}', minorCount)
596
- .replace('{rss}', rssMB)
597
- .replace('{heap}', heapMB)
598
- .replace('{heapTotal}', heapTotalMB)
599
- .replace('{pct}', heapPct)
600
- }
601
-
602
- if (mpm >= gcErr) {
603
- alerts.push({
604
- id: 'host-gc-error',
605
- severity: 'error',
606
- title: function () { return t('alertHostGcError') },
607
- message: _gcMsg,
608
- icon: '🟠',
609
- timestamp: Date.now(),
610
- dismissible: true,
611
- })
612
- } else if (mpm >= gcWarn) {
613
- alerts.push({
614
- id: 'host-gc-warning',
615
- severity: 'warning',
616
- title: function () { return t('alertHostGcWarning') },
617
- message: _gcMsg,
618
- icon: '🟡',
619
- timestamp: Date.now(),
620
- dismissible: true,
621
- })
622
- } else if (mpm >= gcInfo) {
623
- alerts.push({
624
- id: 'host-gc-info',
625
- severity: 'info',
626
- title: function () { return t('alertHostGcInfo') },
627
- message: _gcMsg,
628
- icon: '🔵',
629
- timestamp: Date.now(),
630
- dismissible: true,
631
- })
632
- }
633
- }
634
-
635
- try { callback(alerts) } catch (_) {}
636
- })
637
- .catch(function () {
638
- try { callback([]) } catch (_) {}
639
- })
640
-
641
- // Schedule next poll
642
- var pollBase = _alertPref('memPollBase')
643
- var nextInterval = pollBase
644
- timer = setTimeout(poll, nextInterval)
645
- }
646
-
647
- return {
648
- id: 'dsh-flash-mem-mon:host-memory-alert',
649
- start: function (cb) {
650
- callback = cb
651
- poll()
652
- },
653
- stop: function () {
654
- callback = null
655
- if (timer) { clearTimeout(timer); timer = null }
656
- },
657
- }
658
- }
659
-
660
- //#endregion ───────────────────────────────────────────────────────────────
661
-
662
- // ═══════════════════════════════════════════════════════════════════════
663
- //#region Icon helpers ────────────────────────────────────────────────────
664
-
665
- var _ICON_PATHS = {
666
- bolt: ['M9 2 3.5 9H8l-1 5 5.5-7H8l1-5z'],
667
- signal: ['M3.2 11.6a6.8 6.8 0 0 1 9.6 0M5.6 9.2a4.2 4.2 0 0 1 4.8 0M8 6.8c.6 0 1.2.2 1.6.5M8 13a.9.9 0 1 0 0-1.8.9.9 0 0 0 0 1.8z'],
668
- shield: ['M8 2 3 3.8v3.5c0 3.7 2.2 6.1 5 6.7 2.8-.6 5-3 5-6.7V3.8z', 'M6.2 8.1l1.2 1.2 2.4-2.6'],
669
- }
670
-
671
- function _switchIcon(icon, style) {
672
- var paths = typeof icon === 'string' ? _ICON_PATHS[icon] : null
673
- if (!paths) return h('span', { style: style }, icon)
674
- return h('span', { style: style, 'aria-hidden': 'true' },
675
- h('svg', {
676
- width: 12, height: 12, viewBox: '0 0 16 16',
677
- fill: 'none', xmlns: 'http://www.w3.org/2000/svg',
678
- style: { display: 'block', margin: 'auto', color: 'currentColor' },
679
- }, paths.map(function (d, i) {
680
- return h('path', { key: i, d: d, stroke: 'currentColor', strokeWidth: 1.4,
681
- strokeLinecap: 'round', strokeLinejoin: 'round' })
682
- }))
683
- )
684
- }
685
-
686
- //#endregion ───────────────────────────────────────────────────────────────
687
-
688
- // ═══════════════════════════════════════════════════════════════════════
689
- //#region ErrorBoundary ──────────────────────────────────────────────────
690
-
691
- class PanelErrorBoundary extends Component {
692
- constructor(props) {
693
- super(props)
694
- this.state = { hasError: false, error: null }
695
- }
696
- static getDerivedStateFromError(error) {
697
- return { hasError: true, error: error }
698
- }
699
- componentDidCatch(error, info) {
700
- console.error('[dsh-flash-mem-mon] Panel render error (caught by boundary):', error, info)
701
- }
702
- render() {
703
- if (this.state.hasError) {
704
- return h('div', {
705
- style: {
706
- padding: '12px 16px',
707
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
708
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
709
- fontSize: '12px',
710
- },
711
- },
712
- h('div', { style: { marginBottom: '6px', color: 'var(--dsw-alias-label-warning, #d29922)' } },
713
- '⚠ mem-mon render error'),
714
- h('div', null, String(this.state.error && this.state.error.message || this.state.error || 'Unknown error')),
715
- h('button', {
716
- style: {
717
- marginTop: '8px',
718
- padding: '2px 10px',
719
- fontSize: '11px',
720
- cursor: 'pointer',
721
- borderRadius: '4px',
722
- border: '1px solid var(--dsw-alias-border-l2, #21262d)',
723
- background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.85))',
724
- color: 'var(--dsw-alias-label-primary, #c9d1d9)',
725
- },
726
- onClick: function () { this.setState({ hasError: false, error: null }) }.bind(this),
727
- }, 'Retry'),
728
- )
729
- }
730
- return this.props.children
731
- }
732
- }
733
-
734
- //#endregion ───────────────────────────────────────────────────────────────
735
-
736
- // ═══════════════════════════════════════════════════════════════════════
737
- //#region Styles ─────────────────────────────────────────────────────────
738
-
739
- var R = {
740
- xs: 'var(--dsw-radius-xs, 4px)',
741
- sm: 'var(--dsw-radius-sm, 8px)',
742
- md: 'var(--dsw-radius-md, 12px)',
743
- lg: 'var(--dsw-radius-lg, 16px)',
744
- xl: 'var(--dsw-radius-xl, 20px)',
745
- panel: 'var(--dsw-radius-panel, 28px)',
746
- }
747
- var E = {
748
- prominent: 'var(--dsw-elevation-prominent, 0 0 0 0.5px var(--dsw-alias-border-l4, #0003), 0 3px 8px 0 rgba(0,0,0,.04), 0 0 20px 0 rgba(0,0,0,.05))',
749
- }
750
-
751
- var S = {
752
- // Switch row / slider (used by MonitorConfigModal)
753
- switchRow: {
754
- display: 'flex',
755
- alignItems: 'center',
756
- gap: '8px',
757
- marginBottom: '8px',
758
- },
759
- switchLabel: {
760
- display: 'flex',
761
- alignItems: 'center',
762
- gap: '6px',
763
- minWidth: '120px',
764
- fontSize: '12px',
765
- color: 'var(--dsw-alias-label-primary, #c9d1d9)',
766
- },
767
- switchIcon: {
768
- display: 'inline-flex',
769
- verticalAlign: '-2px',
770
- marginRight: '2px',
771
- },
772
- sliderRow: {
773
- flex: 1,
774
- display: 'flex',
775
- alignItems: 'center',
776
- gap: '8px',
777
- minWidth: 0,
778
- marginBottom: '8px',
779
- },
780
- slider: {
781
- flex: 1,
782
- minWidth: 0,
783
- },
784
- value: {
785
- fontSize: '11px',
786
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
787
- minWidth: '42px',
788
- textAlign: 'right',
789
- },
790
- // Monitor config modal
791
- monitorModalMask: {
792
- position: 'fixed',
793
- inset: 0,
794
- zIndex: 2200,
795
- background: 'var(--dsw-alias-bg-mask-1, #0000003d)',
796
- display: 'flex',
797
- alignItems: 'center',
798
- justifyContent: 'center',
799
- },
800
- monitorModal: {
801
- background: 'var(--dsw-alias-bg-layer-2, #fff)',
802
- border: 0,
803
- borderRadius: R.panel,
804
- width: '90vw',
805
- maxWidth: '680px',
806
- maxHeight: '80vh',
807
- display: 'flex',
808
- flexDirection: 'column',
809
- overflow: 'hidden',
810
- boxShadow: E.prominent,
811
- },
812
- monitorModalHead: {
813
- display: 'flex',
814
- alignItems: 'center',
815
- justifyContent: 'space-between',
816
- padding: '10px 14px',
817
- borderBottom: '0.5px solid var(--dsw-alias-border-l2, #0003)',
818
- },
819
- monitorModalTitle: {
820
- fontWeight: 500,
821
- fontSize: '16px',
822
- lineHeight: '24px',
823
- color: 'var(--dsw-alias-label-primary, #000)',
824
- },
825
- monitorModalClose: {
826
- display: 'inline-flex',
827
- alignItems: 'center',
828
- justifyContent: 'center',
829
- width: '28px',
830
- height: '28px',
831
- border: 0,
832
- borderRadius: R.sm,
833
- background: 'transparent',
834
- color: 'var(--dsw-alias-label-secondary, #61666b)',
835
- cursor: 'pointer',
836
- fontSize: '14px',
837
- padding: 0,
838
- },
839
- monitorModalBody: {
840
- flex: 1,
841
- overflowY: 'auto',
842
- padding: '10px 14px',
843
- scrollbarWidth: 'thin',
844
- scrollbarColor: 'var(--dsw-alias-scrollbar-bg-l1, #ccc) var(--dsw-alias-bg-layer-2, transparent)',
845
- },
846
- monitorModalDivider: {
847
- marginTop: '12px',
848
- paddingTop: '10px',
849
- borderTop: '0.5px solid var(--dsw-alias-border-l2, #0003)',
850
- },
851
- monitorModalSectionTitle: {
852
- display: 'block',
853
- fontWeight: 500,
854
- fontSize: '14px',
855
- lineHeight: '22px',
856
- marginBottom: '8px',
857
- color: 'var(--dsw-alias-label-secondary, #61666b)',
858
- },
859
- monitorFieldsGrid: {
860
- display: 'grid',
861
- gridTemplateColumns: '1fr 1fr',
862
- columnGap: '16px',
863
- rowGap: '12px',
864
- },
865
- monitorConfigTwoCol: {
866
- display: 'grid',
867
- gridTemplateColumns: '1fr 1fr',
868
- columnGap: '16px',
869
- },
870
- monitorConfigGroupTitle: {
871
- display: 'block',
872
- fontWeight: 600,
873
- fontSize: '12px',
874
- marginBottom: '6px',
875
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
876
- },
877
- // Alert preview
878
- alertPreviewToggle: {
879
- background: 'none',
880
- border: 0,
881
- padding: 0,
882
- cursor: 'pointer',
883
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
884
- fontSize: '12px',
885
- },
886
- alertPreviewCard: {
887
- padding: '8px 12px',
888
- background: 'var(--dsw-alias-bg-layer-1, #f5f5f5)',
889
- borderRadius: R.md,
890
- fontSize: '12px',
891
- lineHeight: '1.5',
892
- },
893
- alertPreviewGroup: {
894
- marginBottom: '8px',
895
- },
896
- alertPreviewGroupLabel: {
897
- display: 'block',
898
- fontWeight: 600,
899
- fontSize: '11px',
900
- marginBottom: '4px',
901
- color: 'var(--dsw-alias-label-primary, #c9d1d9)',
902
- },
903
- alertPreviewRow: {
904
- display: 'flex',
905
- alignItems: 'center',
906
- gap: '6px',
907
- marginBottom: '2px',
908
- },
909
- alertPreviewIcon: {
910
- flex: 'none',
911
- fontSize: '11px',
912
- },
913
- alertPreviewMsg: {
914
- flex: 1,
915
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
916
- },
917
- alertPreviewCond: {
918
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
919
- fontSize: '11px',
920
- opacity: 0.6,
921
- },
922
- // Memory live data
923
- memColumnCard: {
924
- padding: '8px 12px',
925
- background: 'var(--dsw-alias-bg-layer-1, #f5f5f5)',
926
- borderRadius: R.md,
927
- },
928
- memSubSectionLabel: {
929
- display: 'block',
930
- fontWeight: 600,
931
- fontSize: '11px',
932
- marginBottom: '6px',
933
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
934
- },
935
- memLiveDataGrid: {
936
- display: 'grid',
937
- gridTemplateColumns: 'auto auto',
938
- columnGap: '10px',
939
- rowGap: '4px',
940
- fontSize: '12px',
941
- },
942
- memLiveDataLabel: {
943
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
944
- },
945
- memLiveDataValue: {
946
- color: 'var(--dsw-alias-label-primary, #c9d1d9)',
947
- fontWeight: 500,
948
- },
949
- memUsageBarTrack: {
950
- height: '4px',
951
- borderRadius: '2px',
952
- background: 'rgba(255,255,255,0.08)',
953
- overflow: 'hidden',
954
- marginTop: '2px',
955
- },
956
- memUsageBarFill: {
957
- height: '100%',
958
- borderRadius: '2px',
959
- transition: 'width 0.3s ease, background 0.3s ease',
960
- },
961
- // Sparkline
962
- sparklineWrap: {
963
- position: 'relative',
964
- height: '80px',
965
- marginTop: '6px',
966
- marginBottom: '2px',
967
- },
968
- sparklineSvg: {
969
- position: 'absolute',
970
- inset: 0,
971
- width: '100%',
972
- height: '100%',
973
- },
974
- sparklineHover: {
975
- position: 'absolute',
976
- top: 0,
977
- bottom: 0,
978
- width: '1px',
979
- background: 'rgba(255,255,255,0.2)',
980
- pointerEvents: 'none',
981
- },
982
- sparklineTooltip: {
983
- position: 'absolute',
984
- top: '-2px',
985
- left: '50%',
986
- transform: 'translateX(-50%)',
987
- fontSize: '10px',
988
- lineHeight: '14px',
989
- padding: '2px 6px',
990
- borderRadius: '4px',
991
- background: 'var(--dsw-alias-bg-layer-1, #f5f5f5)',
992
- border: '0.5px solid var(--dsw-alias-border-l4, #0003)',
993
- color: 'var(--dsw-alias-label-primary, #c9d1d9)',
994
- whiteSpace: 'nowrap',
995
- pointerEvents: 'none',
996
- },
997
- // Trend chart axes (HTML overlays — the SVG uses preserveAspectRatio:none
998
- // so SVG <text> would stretch illegibly).
999
- trendAxisWrap: {
1000
- display: 'flex',
1001
- alignItems: 'stretch',
1002
- gap: '4px',
1003
- },
1004
- trendAxisY: {
1005
- display: 'flex',
1006
- flexDirection: 'column',
1007
- justifyContent: 'space-between',
1008
- width: '34px',
1009
- fontSize: '9px',
1010
- lineHeight: '10px',
1011
- textAlign: 'right',
1012
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
1013
- userSelect: 'none',
1014
- },
1015
- trendChartWrap: {
1016
- display: 'flex',
1017
- flexDirection: 'column',
1018
- flex: '1 1 auto',
1019
- minWidth: '0',
1020
- },
1021
- trendAxisX: {
1022
- display: 'flex',
1023
- justifyContent: 'space-between',
1024
- fontSize: '9px',
1025
- lineHeight: '12px',
1026
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
1027
- marginTop: '2px',
1028
- userSelect: 'none',
1029
- },
1030
- // GC event list
1031
- gcEventListWrap: {
1032
- maxHeight: '120px',
1033
- overflowY: 'auto',
1034
- scrollbarWidth: 'thin',
1035
- },
1036
- gcEventRow: {
1037
- display: 'flex',
1038
- alignItems: 'center',
1039
- gap: '8px',
1040
- padding: '2px 0',
1041
- fontSize: '11px',
1042
- cursor: 'pointer',
1043
- borderRadius: '3px',
1044
- },
1045
- gcEventRowHighlight: {
1046
- background: 'rgba(218, 54, 51, 0.12)',
1047
- },
1048
- gcEventTimeCol: {
1049
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
1050
- minWidth: '56px',
1051
- },
1052
- gcEventDurationCol: {
1053
- color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1054
- minWidth: '100px',
1055
- },
1056
- gcEventKindCol: {
1057
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
1058
- },
1059
- gcEventEmpty: {
1060
- color: 'var(--dsw-alias-label-secondary, #8b949e)',
1061
- fontSize: '11px',
1062
- opacity: 0.5,
1063
- },
1064
- }
1065
-
1066
- //#endregion ───────────────────────────────────────────────────────────────
1067
-
1068
- // ═══════════════════════════════════════════════════════════════════════
1069
- //#region MonitorConfigModal (memory only) ──────────────────────────────
1070
-
1071
- var MONITOR_SLIDER_FIELDS = {
1072
- memory: [
1073
- { key: 'memThresholdInfo', labelKey: 'memThresholdInfo', tooltipKey: 'memThresholdInfoTip', group: 'rss', min: 64, max: 1024, step: 64, format: function (v) { return v + ' MB' } },
1074
- { key: 'memThresholdWarning', labelKey: 'memThresholdWarning', tooltipKey: 'memThresholdWarningTip', group: 'rss', min: 128, max: 1536, step: 64, format: function (v) { return v + ' MB' } },
1075
- { key: 'memThresholdError', labelKey: 'memThresholdError', tooltipKey: 'memThresholdErrorTip', group: 'rss', min: 256, max: 4096, step: 64, format: function (v) { return v + ' MB' } },
1076
- { key: 'memThresholdCritical',labelKey: 'memThresholdCritical',tooltipKey: 'memThresholdCriticalTip',group: 'rss', min: 512, max: 4096, step: 64, format: function (v) { return v + ' MB' } },
1077
- { key: 'memPollBase', labelKey: 'memPollBase', tooltipKey: 'memPollBaseTip', group: 'rss', min: 5000, max: 60000, step: 1000, format: function (v) { return v + 'ms' } },
1078
- { key: 'memPollMin', labelKey: 'memPollMin', tooltipKey: 'memPollMinTip', group: 'rss', min: 1000, max: 10000, step: 500, format: function (v) { return v + 'ms' } },
1079
- { key: 'gcThresholdInfo', labelKey: 'gcThresholdInfo', tooltipKey: 'gcThresholdInfoTip', group: 'gc', min: 1, max: 5, step: 1, format: function (v) { return v + '/min' } },
1080
- { key: 'gcThresholdWarning', labelKey: 'gcThresholdWarning', tooltipKey: 'gcThresholdWarningTip', group: 'gc', min: 2, max: 8, step: 1, format: function (v) { return v + '/min' } },
1081
- { key: 'gcThresholdError', labelKey: 'gcThresholdError', tooltipKey: 'gcThresholdErrorTip', group: 'gc', min: 4, max: 15, step: 1, format: function (v) { return v + '/min' } },
1082
- ],
1083
- }
1084
-
1085
- var MONITOR_TITLE_KEY = {
1086
- memory: 'alertMemCluster',
1087
- }
1088
-
1089
- var _monitorConfigRoot = null
1090
- var _monitorConfigHost = null
1091
-
1092
- function closeMonitorConfig() {
1093
- if (_monitorConfigRoot) { try { _monitorConfigRoot.unmount() } catch (_) {} _monitorConfigRoot = null }
1094
- if (_monitorConfigHost) { try { _monitorConfigHost.remove() } catch (_) {} _monitorConfigHost = null }
1095
- }
1096
-
1097
- function openMonitorConfig(monitor) {
1098
- if (!ReactDOMClient || !ReactDOMClient.createRoot) return
1099
- closeMonitorConfig()
1100
- var host = document.createElement('div')
1101
- host.style.cssText = 'position:fixed;inset:0;z-index:2200;'
1102
- document.body.appendChild(host)
1103
- _monitorConfigHost = host
1104
- try {
1105
- _monitorConfigRoot = ReactDOMClient.createRoot(host)
1106
- _monitorConfigRoot.render(h(MonitorConfigModal, {
1107
- monitor: monitor,
1108
- onClose: closeMonitorConfig,
1109
- }))
1110
- } catch (e) {
1111
- console.error('[dsh-flash-mem-mon] failed to open monitor config:', e)
1112
- closeMonitorConfig()
1113
- }
1114
- }
1115
-
1116
- function MonitorConfigModal(props) {
1117
- var monitor = props.monitor
1118
- var onClose = props.onClose
1119
- var fields = MONITOR_SLIDER_FIELDS[monitor] || []
1120
- var _tick = useState(0)
1121
- var tick = _tick[0], setTick = _tick[1]
1122
-
1123
- // Host process memory — fetched from /health route (cross-plugin, dock-flash).
1124
- var _hostMem = useState(null)
1125
- var hostMem = _hostMem[0], setHostMem = _hostMem[1]
1126
-
1127
- // Memory trend summary — fetched from own /memory-trend route.
1128
- var _trend = useState(null)
1129
- var trendData = _trend[0], setTrendData = _trend[1]
1130
-
1131
- // Memory trend full samples — fetched on demand for sparkline.
1132
- var _trendSamples = useState(null)
1133
- var trendSamples = _trendSamples[0], setTrendSamples = _trendSamples[1]
1134
-
1135
- // GC events from the same full fetch — for sparkline overlay markers.
1136
- var _gcEvents = useState(null)
1137
- var gcEvents = _gcEvents[0], setGcEvents = _gcEvents[1]
1138
-
1139
- // Sparkline hover index (null = not hovering).
1140
- var _hoverIdx = useState(null)
1141
- var hoverIdx = _hoverIdx[0], setHoverIdx = _hoverIdx[1]
1142
-
1143
- // GC event highlight index (null = none selected).
1144
- var _gcHighlightIdx = useState(null)
1145
- var gcHighlightIdx = _gcHighlightIdx[0], setGcHighlightIdx = _gcHighlightIdx[1]
1146
-
1147
- // Close on Escape.
1148
- useEffect(function () {
1149
- function onKey(e) { if (e.key === 'Escape') onClose() }
1150
- document.addEventListener('keydown', onKey)
1151
- return function () { document.removeEventListener('keydown', onKey) }
1152
- }, [onClose])
1153
-
1154
- // Auto-refresh host memory data every 2 seconds while the modal is open.
1155
- useEffect(function () {
1156
- function fetchHost() {
1157
- fetch('/plugins/dock-flash/health').then(function (r) { return r.json() }).then(function (d) {
1158
- if (d && d.memory) setHostMem(d.memory)
1159
- }).catch(function () {})
1160
- }
1161
- fetchHost()
1162
- var hostTimer = setInterval(fetchHost, 2000)
1163
- return function () { clearInterval(hostTimer) }
1164
- }, [])
1165
-
1166
- // Auto-refresh memory trend summary every 5 seconds.
1167
- useEffect(function () {
1168
- function fetchTrend() {
1169
- fetch('/plugins/dsh-flash-mem-mon/memory-trend?mode=summary')
1170
- .then(function (r) { return r.json() })
1171
- .then(function (d) { setTrendData(d) })
1172
- .catch(function () {})
1173
- }
1174
- fetchTrend()
1175
- var trendTimer = setInterval(fetchTrend, 5000)
1176
- return function () { clearInterval(trendTimer) }
1177
- }, [])
1178
-
1179
- // Fetch full samples for sparkline (once on mount + periodic refresh).
1180
- useEffect(function () {
1181
- function fetchSamples() {
1182
- fetch('/plugins/dsh-flash-mem-mon/memory-trend?mode=full')
1183
- .then(function (r) { return r.json() })
1184
- .then(function (d) {
1185
- if (d && d.samples) setTrendSamples(d.samples.slice(-360))
1186
- if (d && d.gcEvents) setGcEvents(d.gcEvents)
1187
- })
1188
- .catch(function () {})
1189
- }
1190
- fetchSamples()
1191
- var sampleTimer = setInterval(fetchSamples, 5000)
1192
- return function () { clearInterval(sampleTimer) }
1193
- }, [])
1194
-
1195
- // Render a single slider field row.
1196
- function renderSliderField(f) {
1197
- var val = Number(_alertPref(f.key)) || 0
1198
- var hintEl = f.hintKey
1199
- ? h('div', { style: { fontSize: '11px', opacity: 0.4, marginTop: '-2px' } }, t(f.hintKey))
1200
- : null
1201
- return h('div', { key: f.key, style: S.switchRow },
1202
- h('span', { style: S.switchLabel },
1203
- h('span', { style: S.switchIcon }, '⚙'),
1204
- h('span', null, t(f.labelKey)),
1205
- hintEl,
1206
- ),
1207
- h('div', { style: S.sliderRow },
1208
- h('input', {
1209
- type: 'range',
1210
- min: f.min, max: f.max, step: f.step,
1211
- value: val,
1212
- style: S.slider,
1213
- onChange: function (e) {
1214
- var next = Number(e.target.value)
1215
- _writeAlertPref(_prefCtx, f.key, next, function (patch, rollback) { rollback() })
1216
- setTick(function (v) { return v + 1 })
1217
- },
1218
- }),
1219
- h('span', { style: S.value }, f.format(val)),
1220
- ),
1221
- )
1222
- }
1223
-
1224
- // For memory config: two-column layout (RSS + polling on the left, GC on the right).
1225
- var fieldEls
1226
- if (fields.some(function (f) { return f.group })) {
1227
- var rssFields = fields.filter(function (f) { return f.group === 'rss' })
1228
- var gcFields = fields.filter(function (f) { return f.group === 'gc' })
1229
- fieldEls = h('div', { style: S.monitorConfigTwoCol },
1230
- h('div', null,
1231
- h('span', { style: S.monitorConfigGroupTitle }, t('memConfigGroupRss')),
1232
- rssFields.map(renderSliderField),
1233
- ),
1234
- h('div', null,
1235
- h('span', { style: S.monitorConfigGroupTitle }, t('memConfigGroupGc')),
1236
- gcFields.map(renderSliderField),
1237
- ),
1238
- )
1239
- } else {
1240
- fieldEls = fields.map(renderSliderField)
1241
- }
1242
-
1243
- // ── Alert preview section ──────────────────────────────────────────
1244
- var _showPreview = useState(false)
1245
- var showPreview = _showPreview[0], setShowPreview = _showPreview[1]
1246
-
1247
- var alertRows = []
1248
- // ── Criterion 1: RSS absolute value ──
1249
- var _tI = _alertPref('memThresholdInfo')
1250
- var _tW = _alertPref('memThresholdWarning')
1251
- var _tE = _alertPref('memThresholdError')
1252
- var _tC = _alertPref('memThresholdCritical')
1253
- if (!(_tI > 0 && _tW > _tI && _tE > _tW && _tC > _tE)) { _tI = 256; _tW = 512; _tE = 1024; _tC = 1536 }
1254
-
1255
- // Helper to fill all RSS alert template placeholders with sample values for preview.
1256
- var _rssPreviewMsg = function (rssVal) {
1257
- return t('alertHostMemMsg')
1258
- .replace('{v}', rssVal + '')
1259
- .replace('{heap}', '…')
1260
- .replace('{heapTotal}', '…')
1261
- .replace('{pct}', '…')
1262
- .replace('{ext}', '…')
1263
- .replace('{ab}', '…')
1264
- .replace('{trend}', '→')
1265
- .replace('{slope}', '0.0')
1266
- .replace('{gcInfo}', '')
1267
- }
1268
-
1269
- alertRows.push(
1270
- h('div', { key: 'rss', style: S.alertPreviewGroup },
1271
- h('span', { style: S.alertPreviewGroupLabel }, t('alertPreviewCriteriaRss')),
1272
- h('div', { style: S.alertPreviewRow },
1273
- h('span', { style: S.alertPreviewIcon }, '🔵'),
1274
- h('span', { style: S.alertPreviewMsg }, t('alertHostMemInfo').replace('{threshold}', _tI)),
1275
- h('span', { style: S.alertPreviewCond }, '≥ ' + _tI + ' MB'),
1276
- ),
1277
- h('div', { style: S.alertPreviewRow },
1278
- h('span', { style: S.alertPreviewIcon }, '🟡'),
1279
- h('span', { style: S.alertPreviewMsg }, t('alertHostMemWarning').replace('{threshold}', _tW)),
1280
- h('span', { style: S.alertPreviewCond }, '≥ ' + _tW + ' MB'),
1281
- ),
1282
- h('div', { style: S.alertPreviewRow },
1283
- h('span', { style: S.alertPreviewIcon }, '🟠'),
1284
- h('span', { style: S.alertPreviewMsg }, t('alertHostMemError').replace('{threshold}', _tE)),
1285
- h('span', { style: S.alertPreviewCond }, '≥ ' + _tE + ' MB'),
1286
- ),
1287
- h('div', { style: S.alertPreviewRow },
1288
- h('span', { style: S.alertPreviewIcon }, '🔴'),
1289
- h('span', { style: S.alertPreviewMsg }, t('alertHostMemCritical').replace('{threshold}', _tC)),
1290
- h('span', { style: S.alertPreviewCond }, '≥ ' + _tC + ' MB'),
1291
- ),
1292
- ),
1293
- )
1294
-
1295
- // ── Criterion 2: RSS growth rate ──
1296
- var _growthPreviewMsg = function () {
1297
- return t('alertHostMemGrowthMsg')
1298
- .replace('{r}', 'N')
1299
- .replace('{rss}', '…')
1300
- .replace('{heap}', '…')
1301
- .replace('{heapTotal}', '…')
1302
- .replace('{pct}', '…')
1303
- .replace('{ab}', '…')
1304
- .replace('{trend}', '↑')
1305
- }
1306
- alertRows.push(
1307
- h('div', { key: 'growth', style: S.alertPreviewGroup },
1308
- h('span', { style: S.alertPreviewGroupLabel }, t('alertPreviewCriteriaGrowth')),
1309
- h('div', { style: S.alertPreviewRow },
1310
- h('span', { style: S.alertPreviewIcon }, '📈'),
1311
- h('span', { style: S.alertPreviewMsg }, t('alertHostMemGrowth') + ': ' + _growthPreviewMsg()),
1312
- h('span', { style: S.alertPreviewCond }, '> 1 MB/min + ↑'),
1313
- ),
1314
- ),
1315
- )
1316
-
1317
- // ── Criterion 3: Major GC frequency ──
1318
- var _gI = _alertPref('gcThresholdInfo')
1319
- var _gW = _alertPref('gcThresholdWarning')
1320
- var _gE = _alertPref('gcThresholdError')
1321
- if (!(_gI > 0 && _gW > _gI && _gE > _gW)) { _gI = 2; _gW = 5; _gE = 10 }
1322
-
1323
- // Helper to fill all GC alert template placeholders with sample values for preview.
1324
- var _gcPreviewMsg = function (gcVal) {
1325
- return t('alertHostGcMsg')
1326
- .replace('{v}', gcVal + '')
1327
- .replace('{pause}', '…')
1328
- .replace('{minor}', '…')
1329
- .replace('{rss}', '…')
1330
- .replace('{heap}', '…')
1331
- .replace('{heapTotal}', '…')
1332
- .replace('{pct}', '…')
1333
- }
1334
-
1335
- alertRows.push(
1336
- h('div', { key: 'gc', style: S.alertPreviewGroup },
1337
- h('span', { style: S.alertPreviewGroupLabel }, t('alertPreviewCriteriaGc')),
1338
- h('div', { style: S.alertPreviewRow },
1339
- h('span', { style: S.alertPreviewIcon }, '🔵'),
1340
- h('span', { style: S.alertPreviewMsg }, t('alertHostGcInfo') + ': ' + _gcPreviewMsg(_gI)),
1341
- h('span', { style: S.alertPreviewCond }, '≥ ' + _gI + '/min'),
1342
- ),
1343
- h('div', { style: S.alertPreviewRow },
1344
- h('span', { style: S.alertPreviewIcon }, '🟡'),
1345
- h('span', { style: S.alertPreviewMsg }, t('alertHostGcWarning') + ': ' + _gcPreviewMsg(_gW)),
1346
- h('span', { style: S.alertPreviewCond }, '≥ ' + _gW + '/min'),
1347
- ),
1348
- h('div', { style: S.alertPreviewRow },
1349
- h('span', { style: S.alertPreviewIcon }, '🟠'),
1350
- h('span', { style: S.alertPreviewMsg }, t('alertHostGcError') + ': ' + _gcPreviewMsg(_gE)),
1351
- h('span', { style: S.alertPreviewCond }, '≥ ' + _gE + '/min'),
1352
- ),
1353
- ),
1354
- )
1355
-
1356
- var alertPreviewEl = h('div', { key: 'alert-preview' },
1357
- h('div', { style: S.monitorModalDivider }),
1358
- h('button', {
1359
- type: 'button',
1360
- style: S.alertPreviewToggle,
1361
- onClick: function () { setShowPreview(function (v) { return !v }) },
1362
- },
1363
- (showPreview ? '▾ ' : '▸ ') + t('alertPreviewToggle'),
1364
- ),
1365
- showPreview ? h('div', { style: Object.assign({}, S.alertPreviewCard, { marginTop: '6px' }) },
1366
- alertRows,
1367
- ) : null,
1368
- )
1369
-
1370
- // ── Live memory data section ──────────────────────────────────────
1371
- void hostMem // force React to re-render when host data arrives
1372
- void trendData // force React to re-render when trend data arrives
1373
- void trendSamples
1374
- void gcEvents
1375
- void gcHighlightIdx
1376
-
1377
- var memLiveDataEl = null
1378
- // Threshold values in MB for RSS bar colour.
1379
- var tInfo = _alertPref('memThresholdInfo')
1380
- var tWarn = _alertPref('memThresholdWarning')
1381
- var tErr = _alertPref('memThresholdError')
1382
- var tCrit = _alertPref('memThresholdCritical')
1383
- if (!(tInfo > 0 && tWarn > tInfo && tErr > tWarn && tCrit > tErr)) { tInfo = 256; tWarn = 512; tErr = 1024; tCrit = 1536 }
1384
- var rssBarBaseline = Math.max(tCrit, 2048)
1385
-
1386
- // ── Host-side Node.js process ──────────────────────────────────
1387
- var hostSectionEl = null
1388
- if (hostMem) {
1389
- var rssMB = (hostMem.rss / 1048576).toFixed(1)
1390
- var rssMBNum = hostMem.rss / 1048576
1391
- var heapTotalMB = (hostMem.heapTotal / 1048576).toFixed(1)
1392
- var heapUsedMB = (hostMem.heapUsed / 1048576).toFixed(1)
1393
- var externalMB = (hostMem.external / 1048576).toFixed(1)
1394
- var arrayBufMB = (hostMem.arrayBuffers / 1048576).toFixed(1)
1395
- var heapPct = hostMem.heapTotal > 0 ? Math.round(hostMem.heapUsed / hostMem.heapTotal * 100) : 0
1396
- var hBarColor = 'var(--dsw-alias-brand-primary, #58a6ff)'
1397
- if (rssMBNum >= tCrit) hBarColor = '#f85149'
1398
- else if (rssMBNum >= tErr) hBarColor = '#f0883e'
1399
- else if (rssMBNum >= tWarn) hBarColor = '#d29922'
1400
- else if (rssMBNum >= tInfo) hBarColor = '#58a6ff'
1401
- var rssBarPct = Math.min(Math.round(rssMBNum / rssBarBaseline * 100), 100)
1402
- hostSectionEl = h('div', { style: S.memColumnCard },
1403
- h('span', { style: S.memSubSectionLabel }, t('memHostSection')),
1404
- h('div', { style: S.monitorConfigTwoCol },
1405
- // ── Left: Process-level memory ──
1406
- h('div', null,
1407
- h('div', { style: S.memLiveDataGrid },
1408
- h('span', { style: S.memLiveDataLabel }, t('memHostRSS')),
1409
- h('span', { style: S.memLiveDataValue }, rssMB + ' MB'),
1410
- h('span', { style: { gridColumn: '1 / -1' } },
1411
- h('div', { style: S.memUsageBarTrack },
1412
- h('div', {
1413
- style: Object.assign({}, S.memUsageBarFill, {
1414
- width: rssBarPct + '%',
1415
- background: hBarColor,
1416
- }),
1417
- })
1418
- )
1419
- ),
1420
- h('span', { style: S.memLiveDataLabel }, t('memHostExternal')),
1421
- h('span', { style: S.memLiveDataValue }, externalMB + ' MB'),
1422
- h('span', { style: S.memLiveDataLabel }, t('memHostArrayBuffers')),
1423
- h('span', { style: S.memLiveDataValue }, arrayBufMB + ' MB'),
1424
- ),
1425
- ),
1426
- // ── Right: Heap memory ──
1427
- h('div', null,
1428
- h('div', { style: S.memLiveDataGrid },
1429
- h('span', { style: S.memLiveDataLabel }, t('memHostHeapUsed')),
1430
- h('span', { style: S.memLiveDataValue }, heapUsedMB + ' MB'),
1431
- h('span', { style: S.memLiveDataLabel }, t('memHostHeapTotal')),
1432
- h('span', { style: S.memLiveDataValue }, heapTotalMB + ' MB'),
1433
- h('span', { style: S.memLiveDataLabel }, t('memHostHeapRatio')),
1434
- h('span', { style: S.memLiveDataValue }, heapPct + '%'),
1435
- h('span', { style: { gridColumn: '1 / -1' } },
1436
- h('div', { style: S.memUsageBarTrack },
1437
- h('div', {
1438
- style: Object.assign({}, S.memUsageBarFill, {
1439
- width: heapPct + '%',
1440
- background: heapPct >= 95 ? '#f85149' : heapPct >= 85 ? '#f0883e' : heapPct >= 70 ? '#d29922' : 'var(--dsw-alias-brand-primary, #58a6ff)',
1441
- }),
1442
- })
1443
- )
1444
- ),
1445
- ),
1446
- ),
1447
- ),
1448
- )
1449
- } else {
1450
- hostSectionEl = h('div', { style: S.memColumnCard },
1451
- h('span', { style: S.memSubSectionLabel }, t('memHostSection')),
1452
- h('span', { style: S.memLiveDataLabel }, t('memHostUnavailable')),
1453
- )
1454
- }
1455
-
1456
- // ── Memory trend section ───────────────────────────────────────
1457
- var trendSectionEl = null
1458
- if (trendData && trendData.current) {
1459
- var cur = trendData.current
1460
-
1461
- // Trend direction
1462
- var dirKey = trendData.trend === 'up' ? 'memTrendUp' : trendData.trend === 'down' ? 'memTrendDown' : 'memTrendStable'
1463
- var dirIcon = trendData.trend === 'up' ? '↗' : trendData.trend === 'down' ? '↘' : '→'
1464
- var dirColor = trendData.trend === 'up' ? '#f85149' : trendData.trend === 'down' ? '#3fb950' : 'var(--dsw-alias-label-secondary, #8b949e)'
1465
-
1466
- // Peak (RSS)
1467
- var peakEl = null
1468
- if (trendData.peak) {
1469
- var peakMB = (trendData.peak.rss / 1048576).toFixed(1)
1470
- peakEl = h('span', { style: S.memLiveDataValue }, peakMB + ' MB')
1471
- } else {
1472
- peakEl = h('span', { style: S.memLiveDataValue }, '—')
1473
- }
1474
-
1475
- // Trend summary stats
1476
- var trendStats = []
1477
- trendStats.push(
1478
- h('span', { style: S.memLiveDataLabel }, t('memTrendDirection')),
1479
- h('span', { style: Object.assign({}, S.memLiveDataValue, { color: dirColor }) }, dirIcon + ' ' + t(dirKey)),
1480
- )
1481
- trendStats.push(
1482
- h('span', { style: S.memLiveDataLabel }, t('memTrendPeak')),
1483
- peakEl,
1484
- )
1485
- if (trendData.sampleCount) {
1486
- trendStats.push(
1487
- h('span', { style: S.memLiveDataLabel }, t('memTrendSamples')),
1488
- h('span', { style: S.memLiveDataValue }, '' + trendData.sampleCount),
1489
- )
1490
- }
1491
- if (trendData.spanMs) {
1492
- var spanMin = Math.round(trendData.spanMs / 60000)
1493
- trendStats.push(
1494
- h('span', { style: S.memLiveDataLabel }, t('memTrendSpan')),
1495
- h('span', { style: S.memLiveDataValue }, spanMin + ' min'),
1496
- )
1497
- }
1498
- if (typeof cur.heapRatio === 'number') {
1499
- trendStats.push(
1500
- h('span', { style: S.memLiveDataLabel }, t('memTrendHeapRatio')),
1501
- h('span', { style: S.memLiveDataValue }, (cur.heapRatio * 100).toFixed(0) + '%'),
1502
- )
1503
- }
1504
- if (trendData.rssSlopePerMin) {
1505
- trendStats.push(
1506
- h('span', { style: S.memLiveDataLabel }, t('memTrendGrowthRate')),
1507
- h('span', { style: S.memLiveDataValue }, (trendData.rssSlopePerMin > 0 ? '+' : '') + trendData.rssSlopePerMin.toFixed(2) + ' MB/min'),
1508
- )
1509
- }
1510
-
1511
- // GC summary
1512
- if (trendData.gc) {
1513
- trendStats.push(
1514
- h('span', { style: S.memLiveDataLabel }, t('memGcMajorPerMin')),
1515
- h('span', { style: S.memLiveDataValue }, trendData.gc.majorPerMin.toFixed(2)),
1516
- )
1517
- trendStats.push(
1518
- h('span', { style: S.memLiveDataLabel }, t('memGcMinorCount')),
1519
- h('span', { style: S.memLiveDataValue }, '' + trendData.gc.minorCount),
1520
- )
1521
- trendStats.push(
1522
- h('span', { style: S.memLiveDataLabel }, t('memGcMajorCount')),
1523
- h('span', { style: S.memLiveDataValue }, '' + trendData.gc.majorCount),
1524
- )
1525
- }
1526
-
1527
- // Sparkline
1528
- var sparklineEl = null
1529
- if (trendSamples && trendSamples.length > 1) {
1530
- var svgW = 280
1531
- var svgH = 80
1532
- var padY = 4
1533
- var rssValues = trendSamples.map(function (s) { return s.rss })
1534
- var maxRss = Math.max.apply(null, rssValues)
1535
- // Shared Y-axis: min fixed at 0, max = RSS peak — both lines on the same absolute scale.
1536
- var yAxisMax = maxRss
1537
- var yAxisRange = yAxisMax || 1
1538
- var xStep = svgW / (trendSamples.length - 1)
1539
-
1540
- // Build polyline points for RSS (0-based scale)
1541
- var points = []
1542
- for (var i = 0; i < trendSamples.length; i++) {
1543
- var x = i * xStep
1544
- var y = svgH - padY - (rssValues[i] / yAxisRange) * (svgH - 2 * padY)
1545
- points.push(x.toFixed(1) + ',' + y.toFixed(1))
1546
- }
1547
-
1548
- // Heap line (same 0-based scale)
1549
- var heapPoints = null
1550
- var heapValues = trendSamples.map(function (s) { return s.heapUsed })
1551
- if (Math.max.apply(null, heapValues) > 0) {
1552
- heapPoints = []
1553
- for (var i = 0; i < trendSamples.length; i++) {
1554
- var x = i * xStep
1555
- var y = svgH - padY - (heapValues[i] / yAxisRange) * (svgH - 2 * padY)
1556
- heapPoints.push(x.toFixed(1) + ',' + y.toFixed(1))
1557
- }
1558
- }
1559
-
1560
- // GC event markers
1561
- // Only Full GC events (non-scavenge, i.e. kind bit 1 unset) are drawn
1562
- // on the trend chart, scoped to the same visible window as
1563
- // `trendSamples` (majors outside [tsFirst, tsLast] are skipped so they
1564
- // don't map off-viewBox), and the x is clamped so an in-window Full GC
1565
- // is always drawn. Each marker's identity is its original index in the
1566
- // `gcEvents` payload so clicking a Full GC row in the list (which sets
1567
- // `gcHighlightIdx` to that same index) can make its marker stand out.
1568
- var gcMarkers = null
1569
- var gcMarkerPositions = [] // [{geIdx, rx, ts}] for click hit-testing in overlay
1570
- if (gcEvents && gcEvents.length > 0) {
1571
- var tsFirst = trendSamples[0].ts
1572
- var tsLast = trendSamples[trendSamples.length - 1].ts
1573
- var tsRange = tsLast - tsFirst || 1
1574
- gcMarkers = gcEvents.map(function (ge, geIdx) {
1575
- if ((ge.kind & 1) !== 0) return null // not a Full GC
1576
- if (ge.ts < tsFirst || ge.ts > tsLast) return null // outside window
1577
- var x = Math.max(0, Math.min(((ge.ts - tsFirst) / tsRange) * svgW, svgW))
1578
- var baseWidth = Math.max(1.5, svgW / (trendSamples.length - 1) / 4)
1579
- // Store position for overlay click hit-testing.
1580
- gcMarkerPositions.push({ geIdx: geIdx, rx: x / svgW, ts: ge.ts })
1581
- // Selected (or none selected) → prominent; otherwise dimmed so the
1582
- // current Full GC visually pops against the rest.
1583
- var isSel = gcHighlightIdx === geIdx
1584
- var width = isSel ? baseWidth * 1.4 : baseWidth * 0.6
1585
- return h('g', { key: 'gc-' + geIdx },
1586
- h('rect', {
1587
- x: x - width / 2, y: 0, width: width, height: svgH,
1588
- fill: isSel ? 'rgba(218, 54, 51, 0.25)' : 'rgba(240, 136, 62, 0.07)',
1589
- }),
1590
- h('line', {
1591
- x1: x, y1: 0, x2: x, y2: svgH,
1592
- stroke: isSel ? '#da3633' : 'rgba(240, 136, 62, 0.30)',
1593
- strokeWidth: isSel ? 1.5 : 1,
1594
- }),
1595
- )
1596
- })
1597
- }
1598
-
1599
- // Hover line + tooltip
1600
- var hoverLineEl = null
1601
- var hoverTooltipEl = null
1602
- if (hoverIdx !== null && hoverIdx >= 0 && hoverIdx < trendSamples.length) {
1603
- var hx = hoverIdx * xStep
1604
- hoverLineEl = h('line', {
1605
- x1: hx, y1: 0, x2: hx, y2: svgH,
1606
- stroke: 'rgba(255,255,255,0.2)', strokeWidth: 1,
1607
- })
1608
- var hSample = trendSamples[hoverIdx]
1609
- var hRssMB = (hSample.rss / 1048576).toFixed(1)
1610
- var hHeapMB = hSample.heapUsed ? (hSample.heapUsed / 1048576).toFixed(1) : null
1611
- var hTime = new Date(hSample.ts)
1612
- var hTimeStr = hTime.getHours().toString().padStart(2, '0') + ':' +
1613
- hTime.getMinutes().toString().padStart(2, '0') + ':' +
1614
- hTime.getSeconds().toString().padStart(2, '0')
1615
- hoverTooltipEl = h('div', {
1616
- style: Object.assign({}, S.sparklineTooltip, {
1617
- left: Math.min(hx, svgW - 80) + 'px',
1618
- }),
1619
- },
1620
- t('memTrendHoverRSS') + ': ' + hRssMB + ' MB',
1621
- hHeapMB ? ' ' + t('memTrendHoverHeap') + ': ' + hHeapMB + ' MB' : '',
1622
- ' ' + t('memTrendHoverTime') + ': ' + hTimeStr,
1623
- )
1624
- }
1625
-
1626
- // ---- Coordinate axes (HTML overlays; the SVG viewBox uses
1627
- // preserveAspectRatio:none so SVG <text> would stretch illegibly) ----
1628
- var _fmtHM = function (ts) {
1629
- var d = new Date(ts)
1630
- return d.getHours().toString().padStart(2, '0') + ':' +
1631
- d.getMinutes().toString().padStart(2, '0')
1632
- }
1633
- var _fmtMB = function (v) { return Math.round(v / 1048576) }
1634
-
1635
- // X axis: first / middle / last sample times (HH:MM).
1636
- var tMid = trendSamples[Math.floor((trendSamples.length - 1) / 2)].ts
1637
- var tLast = trendSamples[trendSamples.length - 1].ts
1638
- var xAxisEl = h('div', { style: S.trendAxisX },
1639
- h('span', null, _fmtHM(trendSamples[0].ts)),
1640
- h('span', null, _fmtHM(tMid)),
1641
- h('span', null, _fmtHM(tLast)),
1642
- )
1643
-
1644
- // Y axis: 0 / mid / max (0-based scale, both lines share it).
1645
- var yAxisEl = h('div', {
1646
- style: Object.assign({}, S.trendAxisY, { height: svgH + 'px' }),
1647
- },
1648
- h('span', null, _fmtMB(yAxisMax) + 'M'),
1649
- h('span', null, _fmtMB(yAxisMax / 2) + 'M'),
1650
- h('span', null, '0'),
1651
- )
1652
-
1653
- sparklineEl = h('div', { style: S.trendAxisWrap },
1654
- yAxisEl,
1655
- h('div', { style: S.trendChartWrap },
1656
- h('div', { style: S.sparklineWrap },
1657
- h('svg', {
1658
- viewBox: '0 0 ' + svgW + ' ' + svgH,
1659
- preserveAspectRatio: 'none',
1660
- style: S.sparklineSvg,
1661
- },
1662
- heapPoints ? h('polyline', {
1663
- points: heapPoints.join(' '),
1664
- fill: 'none',
1665
- stroke: 'rgba(88, 166, 255, 0.3)',
1666
- strokeWidth: 1,
1667
- }) : null,
1668
- h('polyline', {
1669
- points: points.join(' '),
1670
- fill: 'none',
1671
- stroke: '#3fb950',
1672
- strokeWidth: 1.2,
1673
- }),
1674
- gcMarkers,
1675
- hoverLineEl,
1676
- ),
1677
- hoverTooltipEl,
1678
- // Mouse interaction overlay
1679
- h('div', {
1680
- style: { position: 'absolute', inset: 0, cursor: 'crosshair' },
1681
- onMouseMove: function (e) {
1682
- var rect = e.currentTarget.getBoundingClientRect()
1683
- var rx = (e.clientX - rect.left) / rect.width
1684
- var idx = Math.round(rx * (trendSamples.length - 1))
1685
- setHoverIdx(Math.max(0, Math.min(idx, trendSamples.length - 1)))
1686
- // Switch cursor to pointer when hovering near a GC marker.
1687
- var nearGc = false
1688
- var mousePx = e.clientX - rect.left
1689
- for (var gi = 0; gi < gcMarkerPositions.length; gi++) {
1690
- if (Math.abs(gcMarkerPositions[gi].rx * rect.width - mousePx) <= 5) {
1691
- nearGc = true
1692
- break
1693
- }
1694
- }
1695
- e.currentTarget.style.cursor = nearGc ? 'pointer' : 'crosshair'
1696
- },
1697
- onMouseLeave: function (e) {
1698
- setHoverIdx(null)
1699
- e.currentTarget.style.cursor = 'crosshair'
1700
- },
1701
- onClick: function (e) {
1702
- // Check if click is near a Full GC marker.
1703
- // Use pixel distance: only match if within ±5px of a marker.
1704
- var rect = e.currentTarget.getBoundingClientRect()
1705
- var clickPx = e.clientX - rect.left
1706
- var pxPerUnit = rect.width / svgW
1707
- var hitPx = 5
1708
- var hitGc = null
1709
- var hitDist = Infinity
1710
- for (var gi = 0; gi < gcMarkerPositions.length; gi++) {
1711
- var markerPx = gcMarkerPositions[gi].rx * rect.width
1712
- var d = Math.abs(clickPx - markerPx)
1713
- if (d <= hitPx && d < hitDist) {
1714
- hitGc = gcMarkerPositions[gi]
1715
- hitDist = d
1716
- }
1717
- }
1718
- if (hitGc) {
1719
- // Toggle: clicking the same marker again deselects.
1720
- setGcHighlightIdx(gcHighlightIdx === hitGc.geIdx ? null : hitGc.geIdx)
1721
- // Move hover to the nearest sample.
1722
- var bestIdx = 0
1723
- var bestDist2 = Math.abs(trendSamples[0].ts - hitGc.ts)
1724
- for (var si2 = 1; si2 < trendSamples.length; si2++) {
1725
- var dist2 = Math.abs(trendSamples[si2].ts - hitGc.ts)
1726
- if (dist2 < bestDist2) { bestDist2 = dist2; bestIdx = si2 }
1727
- }
1728
- setHoverIdx(bestIdx)
1729
- } else {
1730
- // Clicked empty area → clear GC highlight.
1731
- setGcHighlightIdx(null)
1732
- }
1733
- },
1734
- }),
1735
- ),
1736
- xAxisEl,
1737
- ),
1738
- )
1739
- }
1740
-
1741
- // GC event list
1742
- var gcEventListEl = (function () {
1743
- if (!trendSamples || trendSamples.length === 0) {
1744
- return h('div', null,
1745
- h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
1746
- h('div', { style: S.gcEventEmpty }, t('memGcNoEvents')),
1747
- )
1748
- }
1749
- var _dropThresh = 10 * 1048576 // 10 MB
1750
- var dropEvents = []
1751
- // A) RSS drops from samples
1752
- for (var si = 1; si < trendSamples.length; si++) {
1753
- var rssDrop = trendSamples[si - 1].rss - trendSamples[si].rss
1754
- if (rssDrop >= _dropThresh) {
1755
- dropEvents.push({
1756
- idx: dropEvents.length,
1757
- ts: trendSamples[si].ts,
1758
- rssDropMB: (rssDrop / 1048576).toFixed(1),
1759
- source: 'sample',
1760
- sampleIdx: si,
1761
- })
1762
- }
1763
- }
1764
- // B) gcEvents (kind=2, already filtered server-side)
1765
- if (gcEvents && gcEvents.length > 0) {
1766
- var tsFirst = trendSamples[0].ts
1767
- var tsLast = trendSamples[trendSamples.length - 1].ts
1768
- for (var ei = 0; ei < gcEvents.length; ei++) {
1769
- var ge = gcEvents[ei]
1770
- if (ge.ts < tsFirst || ge.ts > tsLast) continue
1771
- // Try to find the RSS drop around this GC event from samples.
1772
- var rssDropMB = null
1773
- // First: check if it merges with an existing sample-sourced RSS drop event (within 5s).
1774
- var duped = false
1775
- for (var di = 0; di < dropEvents.length; di++) {
1776
- if (dropEvents[di].source === 'sample' && Math.abs(dropEvents[di].ts - ge.ts) < 5000) {
1777
- rssDropMB = dropEvents[di].rssDropMB
1778
- dropEvents[di].source = 'gc'
1779
- dropEvents[di].duration = ge.duration
1780
- dropEvents[di].gcIdx = ei
1781
- duped = true
1782
- break
1783
- }
1784
- }
1785
- // Second: if no existing RSS drop matched, scan nearby samples for a drop.
1786
- if (!duped) {
1787
- for (var si2 = 1; si2 < trendSamples.length; si2++) {
1788
- var timeDiff = Math.abs(trendSamples[si2].ts - ge.ts)
1789
- if (timeDiff > 10000) continue // only look within ±10s
1790
- var nearDrop = trendSamples[si2 - 1].rss - trendSamples[si2].rss
1791
- if (nearDrop > 0) {
1792
- rssDropMB = (nearDrop / 1048576).toFixed(1)
1793
- break
1794
- }
1795
- }
1796
- dropEvents.push({
1797
- idx: dropEvents.length,
1798
- ts: ge.ts,
1799
- rssDropMB: rssDropMB,
1800
- source: 'gc',
1801
- gcIdx: ei,
1802
- duration: ge.duration,
1803
- })
1804
- }
1805
- }
1806
- }
1807
- if (dropEvents.length === 0 && (!trendData.gc || trendData.gc.majorCount === 0)) {
1808
- return h('div', null,
1809
- h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
1810
- h('div', { style: S.gcEventEmpty }, t('memGcNoEvents')),
1811
- )
1812
- }
1813
- var listContent = null
1814
- if (dropEvents.length > 0) {
1815
- listContent = h('div', { style: S.gcEventListWrap },
1816
- dropEvents.map(function (item) {
1817
- var evIdx = item.idx
1818
- var evTs = item.ts
1819
- var evTime = new Date(evTs)
1820
- var evTimeStr = evTime.getHours().toString().padStart(2, '0') + ':' +
1821
- evTime.getMinutes().toString().padStart(2, '0') + ':' +
1822
- evTime.getSeconds().toString().padStart(2, '0')
1823
- var isGcSource = item.source === 'gc'
1824
- // Highlight identity must match the chart marker key, which is
1825
- // the original index in the `gcEvents` payload. GC-sourced rows
1826
- // carry that index in `item.gcIdx`; RSS-drop rows have no chart
1827
- // marker, so fall back to the row index.
1828
- var evKey = isGcSource ? item.gcIdx : evIdx
1829
- var isHighlighted = gcHighlightIdx === evKey
1830
- // Build detail string: show both duration and memory released when available.
1831
- var detailParts = []
1832
- if (isGcSource && item.duration) {
1833
- detailParts.push(item.duration.toFixed(1) + ' ms')
1834
- }
1835
- if (item.rssDropMB && item.rssDropMB !== '—') {
1836
- detailParts.push('-' + item.rssDropMB + ' MB')
1837
- }
1838
- var detailStr = detailParts.length > 0 ? detailParts.join(' | ') : '—'
1839
- var kindStr = isGcSource ? 'Full GC' : t('memGcEventRssDrop')
1840
- return h('div', {
1841
- key: 'gc-ev-' + evIdx,
1842
- ref: isHighlighted ? function (el) {
1843
- if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
1844
- } : undefined,
1845
- style: Object.assign({}, S.gcEventRow, isHighlighted ? S.gcEventRowHighlight : {}),
1846
- onClick: function () {
1847
- if (!trendSamples || trendSamples.length === 0) return
1848
- var bestIdx = 0
1849
- var bestDist = Math.abs(trendSamples[0].ts - evTs)
1850
- for (var si2 = 1; si2 < trendSamples.length; si2++) {
1851
- var dist = Math.abs(trendSamples[si2].ts - evTs)
1852
- if (dist < bestDist) { bestDist = dist; bestIdx = si2 }
1853
- }
1854
- setHoverIdx(bestIdx)
1855
- setGcHighlightIdx(evKey)
1856
- },
1857
- onMouseEnter: function (e) {
1858
- e.currentTarget.style.background = 'rgba(218, 54, 51, 0.18)'
1859
- },
1860
- onMouseLeave: function (e) {
1861
- e.currentTarget.style.background = isHighlighted ? 'rgba(218, 54, 51, 0.12)' : ''
1862
- },
1863
- },
1864
- h('span', { style: S.gcEventTimeCol }, evTimeStr),
1865
- h('span', { style: S.gcEventDurationCol }, detailStr),
1866
- h('span', { style: S.gcEventKindCol }, kindStr),
1867
- )
1868
- })
1869
- )
1870
- } else {
1871
- listContent = h('div', { style: S.gcEventEmpty }, t('memGcNoEvents'))
1872
- }
1873
- return h('div', null,
1874
- h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
1875
- listContent,
1876
- )
1877
- })()
1878
-
1879
- trendSectionEl = h('div', { style: S.memColumnCard },
1880
- h('span', { style: S.memSubSectionLabel }, t('memTrend')),
1881
- // Trend chart on top.
1882
- sparklineEl,
1883
- // Below: two columns — trend info on the left, Full GC list on the right.
1884
- h('div', { style: Object.assign({}, S.monitorConfigTwoCol, { marginTop: '8px' }) },
1885
- h('div', null,
1886
- h('span', { style: S.monitorConfigGroupTitle }, t('memTrendInfo')),
1887
- h('div', { style: S.memLiveDataGrid }, trendStats),
1888
- ),
1889
- gcEventListEl,
1890
- ),
1891
- )
1892
- } else {
1893
- trendSectionEl = h('div', { style: S.memColumnCard },
1894
- h('span', { style: S.memSubSectionLabel }, t('memTrend')),
1895
- h('span', { style: S.memLiveDataLabel }, t('memTrendNoData')),
1896
- )
1897
- }
1898
-
1899
- memLiveDataEl = h('div', { key: 'live-data' },
1900
- h('div', { style: S.monitorModalDivider }),
1901
- h('span', { style: S.monitorModalSectionTitle }, t('memLiveData')),
1902
- hostSectionEl,
1903
- trendSectionEl,
1904
- )
1905
-
1906
- return h('div', {
1907
- style: S.monitorModalMask,
1908
- onMouseDown: function (e) { if (e.target === e.currentTarget) onClose() },
1909
- },
1910
- h('div', { style: S.monitorModal },
1911
- h('div', { style: S.monitorModalHead },
1912
- h('span', { style: S.monitorModalTitle },
1913
- t(MONITOR_TITLE_KEY[monitor] || 'alertMemCluster') + ' — ' + t('monitorConfig')),
1914
- h('button', {
1915
- type: 'button',
1916
- 'data-dock-flash-focus': '',
1917
- style: S.monitorModalClose,
1918
- onClick: onClose,
1919
- }, '✕'),
1920
- ),
1921
- h('div', { style: S.monitorModalBody },
1922
- fieldEls,
1923
- alertPreviewEl,
1924
- memLiveDataEl,
1925
- ),
1926
- ),
1927
- )
1928
- }
1929
-
1930
- //#endregion ───────────────────────────────────────────────────────────────
1931
-
1932
- // ═══════════════════════════════════════════════════════════════════════
1933
- //#region Monitor toggle helpers ──────────────────────────────────────────
1934
-
1935
- /** Check whether dock-flash's system-alerts master toggle is ON. */
1936
- function _getAlertsOn() {
1937
- try { return localStorage.getItem('dock-flash:system-alerts') !== '0' } catch (_) { return true }
1938
- }
1939
-
1940
- var TOGGLE_KEY = 'dsh-flash-mem-mon:monitor-memory'
1941
- var PROVIDER_ID = 'dsh-flash-mem-mon:host-memory-alert'
1942
-
1943
- var _MONITOR_TOGGLES = {
1944
- memory: {
1945
- key: TOGGLE_KEY,
1946
- providers: [PROVIDER_ID],
1947
- },
1948
- }
1949
- var _MONITOR_DEFAULT = { memory: true }
1950
-
1951
- function _getMonitorOn(monitor) {
1952
- var def = _MONITOR_DEFAULT[monitor] !== false
1953
- try { return localStorage.getItem(_MONITOR_TOGGLES[monitor].key) !== '0' } catch (_) { return def }
1954
- }
1955
-
1956
- function _setMonitorOn(monitor, on, alertRegistry) {
1957
- var providers = _MONITOR_TOGGLES[monitor].providers
1958
- for (var i = 0; i < providers.length; i++) {
1959
- try { alertRegistry.setProviderEnabled(providers[i], on) } catch (_) {}
1960
- }
1961
- try { localStorage.setItem(_MONITOR_TOGGLES[monitor].key, on ? '1' : '0') } catch (_) {}
1962
- }
1963
-
1964
- function _monitorSubtitle(m) {
1965
- return function () {
1966
- return _getMonitorOn(m) ? t('monitorOn') : t('monitorOff')
1967
- }
1968
- }
1969
-
1970
- //#endregion ───────────────────────────────────────────────────────────────
1971
-
1972
- // ═══════════════════════════════════════════════════════════════════════
1973
- //#region Client factory ─────────────────────────────────────────────────
1974
-
1975
- var client = {
1976
- inject: ['remote', 'remote.settings'],
1977
- async apply(ctx) {
1978
- // A throw anywhere inside the real apply used to REJECT this async
1979
- // apply — and a failed apply FAILS the plugin's Cordis fiber, so the
1980
- // plugin never activates and its switch silently never appears. That
1981
- // is not hypothetical: a `settings.describe().then(...)` TypeError did
1982
- // exactly this. Registration failures are REPORTED here, never fatal.
1983
- try {
1984
- await client._applyInner(ctx)
1985
- } catch (err) {
1986
- console.error('[dsh-flash-mem-mon] apply failed (non-fatal):', err)
1987
- }
1988
- },
1989
-
1990
- async _applyInner(ctx) {
1991
- _prefCtx = ctx
1992
-
1993
- // 1. Load mem-mon preferences from the 'dsh-flash-mem-mon' settings namespace
1994
- loadMemPrefs(ctx).then(function (ok) {
1995
- // Sync alert providers with the localStorage-backed state.
1996
- var lsOn
1997
- try { lsOn = localStorage.getItem(TOGGLE_KEY) !== '0' } catch (_) { lsOn = true }
1998
- try {
1999
- var reg = ctx.get && ctx.get('quickControl')
2000
- if (reg && typeof reg.notifyChange === 'function') {
2001
- reg.notifyChange(TOGGLE_KEY)
2002
- }
2003
- } catch (_) {}
2004
- var alertReg = ctx.get && ctx.get('dockFlashAlerts')
2005
- if (alertReg) {
2006
- try { alertReg.setProviderEnabled(PROVIDER_ID, lsOn) } catch (_) {}
2007
- }
2008
- })
2009
-
2010
- // 2. Register with dock-flash's quickControl and dockFlashAlerts services.
2011
- var _registered = false
2012
-
2013
- function _registerServices(registry, alertRegistry) {
2014
- if (_registered) return
2015
- if (!registry || !alertRegistry) {
2016
- console.warn('[dsh-flash-mem-mon] _registerServices: missing service — quickControl=' + !!registry + ', alerts=' + !!alertRegistry)
2017
- return
2018
- }
2019
- _registered = true
2020
- console.log('[dsh-flash-mem-mon] registering provider and switch')
2021
-
2022
- // 3. Register memory alert provider
2023
- var memProvider = createHostMemoryAlertProvider()
2024
- alertRegistry.registerProvider(memProvider)
2025
-
2026
- // 4. Register memory monitor toggle switch
2027
- ctx.effect(function () {
2028
- var dispose = registry.registerSwitch({
2029
- id: TOGGLE_KEY,
2030
- label: L('alertMemCluster'),
2031
- icon: 'chip',
2032
- type: 'toggle',
2033
- group: 'system',
2034
- cluster: 'system-alerts',
2035
- order: 58,
2036
- visible: function () {
2037
- return _getAlertsOn()
2038
- },
2039
- config: function () { openMonitorConfig('memory') },
2040
- getValue: function () { return _getMonitorOn('memory') },
2041
- setValue: function (v) {
2042
- _setMonitorOn('memory', v, alertRegistry)
2043
- registry.notifyChange(TOGGLE_KEY)
2044
- try { alertRegistry.notify() } catch (_) {}
2045
- },
2046
- })
2047
- return dispose
2048
- }, 'dsh-flash-mem-mon: monitor-memory switch')
2049
-
2050
- // 5. Apply persisted toggle gates — before the alert registry auto-starts
2051
- Object.keys(_MONITOR_TOGGLES).forEach(function (m) {
2052
- if (!_getMonitorOn(m)) _setMonitorOn(m, false, alertRegistry)
2053
- })
2054
- }
2055
-
2056
- // Passive: listen for dock-flash:ready event
2057
- try {
2058
- ctx.on('dock-flash:ready', function (payload) {
2059
- console.log('[dsh-flash-mem-mon] dock-flash:ready event received — quickControl=' + !!(payload && payload.quickControl) + ', alerts=' + !!(payload && payload.alerts))
2060
- _registerServices(payload.quickControl, payload.alerts)
2061
- })
2062
- } catch (e) {
2063
- console.warn('[dsh-flash-mem-mon] ctx.on("dock-flash:ready") failed:', e)
2064
- }
2065
-
2066
- // Active: check if dock-flash already loaded
2067
- var registry = ctx.get('quickControl')
2068
- var alertRegistry = ctx.get('dockFlashAlerts')
2069
- console.log('[dsh-flash-mem-mon] active check — quickControl=' + !!registry + ', alerts=' + !!alertRegistry)
2070
- if (registry && alertRegistry) _registerServices(registry, alertRegistry)
2071
-
2072
- // Fallback: poll briefly
2073
- if (!_registered) {
2074
- var _retryCount = 0
2075
- function _retryGet() {
2076
- if (_registered) return
2077
- var r = ctx.get('quickControl')
2078
- var a = ctx.get('dockFlashAlerts')
2079
- if (r && a) { _registerServices(r, a); return }
2080
- if (++_retryCount < 15) setTimeout(_retryGet, 200)
2081
- }
2082
- setTimeout(_retryGet, 0)
2083
- }
2084
- },
2085
-
2086
- dispose() {
2087
- closeMonitorConfig()
2088
- },
2089
- }
2090
- return client
2091
- },
2092
- })
516
+ var _PREFS_MAX_RETRIES = 8
517
+ var _PREFS_RETRY_DELAY_MS = 750
518
+
519
+ function loadMemPrefs(ctx, _retryCount) {
520
+ if (typeof _retryCount !== 'number') _retryCount = 0
521
+ var settings = _remoteSettings(ctx)
522
+ if (!settings) return Promise.resolve(false)
523
+ if (typeof settings.describe !== 'function') return Promise.resolve(false)
524
+ return _describeAsync(settings).then(function (desc) {
525
+ if (!desc) return false
526
+ var view = desc.value || desc
527
+ var list = view && Array.isArray(view.namespaces)
528
+ ? view.namespaces
529
+ : (Array.isArray(view) ? view : null)
530
+ if (!list) return false
531
+ var ns = list.find(function (n) {
532
+ return (n && (n.ns || n.namespace)) === MEM_MON_NS
533
+ })
534
+ if (!ns) {
535
+ if (_retryCount < _PREFS_MAX_RETRIES) {
536
+ return new Promise(function (resolve) {
537
+ setTimeout(function () {
538
+ resolve(loadMemPrefs(ctx, _retryCount + 1))
539
+ }, _PREFS_RETRY_DELAY_MS)
540
+ })
541
+ }
542
+ return false
543
+ }
544
+ var resolved = ns.value || ns.resolved
545
+ if (!resolved) return false
546
+ _memPrefs = {
547
+ memThresholdInfo: resolved.memThresholdInfo,
548
+ memThresholdWarning: resolved.memThresholdWarning,
549
+ memThresholdError: resolved.memThresholdError,
550
+ memThresholdCritical: resolved.memThresholdCritical,
551
+ memPollBase: resolved.memPollBase,
552
+ memPollMin: resolved.memPollMin,
553
+ gcThresholdInfo: resolved.gcThresholdInfo,
554
+ gcThresholdWarning: resolved.gcThresholdWarning,
555
+ gcThresholdError: resolved.gcThresholdError,
556
+ memGrowthAlertPerMin: resolved.memGrowthAlertPerMin,
557
+ }
558
+ if (typeof ns.revision === 'number') _hostRevision = ns.revision
559
+ console.log('[dsh-flash-mem-mon] preferences loaded from ' + MEM_MON_NS + ' namespace')
560
+ return true
561
+ }).catch(function () {
562
+ return false
563
+ })
564
+ }
565
+
566
+ /** Read an alert preference from _memPrefs, falling back to the built-in default. */
567
+ function _alertPref(key) {
568
+ var v = _memPrefs && _memPrefs[key]
569
+ return (typeof v === 'number' && isFinite(v)) ? v : _MEM_ALERT_DEFAULTS[key]
570
+ }
571
+
572
+ /** Write an alert preference through the preference bridge (host + memory). */
573
+ function _writeAlertPref(ctx, key, value, onError) {
574
+ savePrefs(ctx, Object.fromEntries([[key, value]]), null, onError)
575
+ }
576
+
577
+ /** Save preferences: update _memPrefs in memory, then serialize a write to the 'dsh-flash-mem-mon' settings namespace. */
578
+ function savePrefs(ctx, patch, localWrites, onError) {
579
+ var saved = {}
580
+ for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) saved[k] = _memPrefs && _memPrefs[k]
581
+ if (_memPrefs) {
582
+ for (var k in patch) if (Object.prototype.hasOwnProperty.call(patch, k)) _memPrefs[k] = patch[k]
583
+ }
584
+ try { if (localWrites) localWrites() } catch (_) {}
585
+ return _queuePrefWrite(ctx, patch, onError, saved)
586
+ }
587
+
588
+ function _queuePrefWrite(ctx, patch, onError, saved) {
589
+ var task = _prefWriteTail.then(function () {
590
+ var settings = _remoteSettings(ctx || _prefCtx)
591
+ if (!settings || typeof settings.update !== 'function') {
592
+ if (onError) onError(patch, function () {
593
+ for (var k in saved) if (Object.prototype.hasOwnProperty.call(saved, k)) _memPrefs[k] = saved[k]
594
+ })
595
+ return false
596
+ }
597
+ return settings.update(MEM_MON_NS, patch, _fenceRevision())
598
+ .then(function (res) {
599
+ try {
600
+ if (res && res.ok === false) {
601
+ _pendingRevision = null
602
+ if (onError) onError(patch, function () {
603
+ for (var k in saved) if (Object.prototype.hasOwnProperty.call(saved, k)) _memPrefs[k] = saved[k]
604
+ })
605
+ try {
606
+ _describeAsync(settings).then(function (desc) {
607
+ if (desc && desc.ok !== false) {
608
+ var view2 = desc.value || desc
609
+ var list2 = view2 && Array.isArray(view2.namespaces)
610
+ ? view2.namespaces
611
+ : (Array.isArray(view2) ? view2 : null)
612
+ var ns2 = list2 && list2.find(function (n) {
613
+ return (n && (n.ns || n.namespace)) === MEM_MON_NS
614
+ })
615
+ if (ns2 && typeof ns2.revision === 'number') _hostRevision = ns2.revision
616
+ }
617
+ }).catch(function () {})
618
+ } catch (_) {}
619
+ return false
620
+ }
621
+ var v = res && res.value
622
+ if (v && typeof v.revision === 'number') {
623
+ _pendingRevision = v.revision
624
+ _hostRevision = v.revision
625
+ }
626
+ } catch (_) {}
627
+ return true
628
+ }).catch(function (err) {
629
+ _pendingRevision = null
630
+ if (onError) onError(patch, function () {
631
+ for (var k in saved) if (Object.prototype.hasOwnProperty.call(saved, k)) _memPrefs[k] = saved[k]
632
+ })
633
+ return false
634
+ })
635
+ })
636
+ _prefWriteTail = task.then(function () {}, function () {})
637
+ return task
638
+ }
639
+
640
+ //#endregion ───────────────────────────────────────────────────────────────
641
+
642
+ // ═══════════════════════════════════════════════════════════════════════
643
+ //#region Shared trend cache ──────────────────────────────────────────────
644
+
645
+ // The alert provider polls `?mode=summary` for alerting. The config modal
646
+ // ALSO needs the same summary for its trend panel. Rather than both polling
647
+ // the same route independently (which doubles the host traffic while the
648
+ // modal is open), the provider writes each successful summary into this
649
+ // module-level cache and notifies subscribers. The modal reads the cache on
650
+ // mount and subscribes for live updates instead of running its own summary
651
+ // poll. `mode=full` (sparkline raw samples + GC events) is heavier and is
652
+ // still fetched by the modal on its own cadence — sharing that would couple
653
+ // the modal's chart to the provider's poll, which is undesirable.
654
+ var _summaryCache = null
655
+ var _summaryVersion = 0
656
+ var _summarySubscribers = []
657
+
658
+ function _setSummaryCache(d) {
659
+ _summaryCache = d
660
+ _summaryVersion++
661
+ var subs = _summarySubscribers.slice()
662
+ _summarySubscribers.length = 0
663
+ for (var i = 0; i < subs.length; i++) {
664
+ try { subs[i](d, _summaryVersion) } catch (_) {}
665
+ }
666
+ }
667
+
668
+ /** Subscribe to the shared summary cache. Immediately invokes `cb` with the
669
+ * current cache (possibly null). Returns an unsubscribe function. */
670
+ function _subscribeSummary(cb) {
671
+ var active = true
672
+ try { cb(_summaryCache, _summaryVersion) } catch (_) {}
673
+ _summarySubscribers.push(function (d, v) { if (active) { try { cb(d, v) } catch (_) {} } })
674
+ return function () { active = false }
675
+ }
676
+
677
+ //#endregion ───────────────────────────────────────────────────────────────
678
+
679
+ // ═══════════════════════════════════════════════════════════════════════
680
+ //#region AlertProvider ──────────────────────────────────────────────────
681
+
682
+ function createHostMemoryAlertProvider() {
683
+ var timer = null
684
+ var callback = null
685
+
686
+ // Hysteresis / de-duplication: remember the last severity emitted for
687
+ // each alert id. A candidate is pushed downstream only when its severity
688
+ // CHANGES (first appearance, escalation, or de-escalation). Repeated
689
+ // polls at the SAME severity are suppressed, so the same problem does not
690
+ // re-fire every poll cycle (which would flood the alert feed).
691
+ // Severity order: info < warning < error < critical.
692
+ var _SEV_RANK = { info: 0, warning: 1, error: 2, critical: 3 }
693
+ var _armed = {}
694
+ var _seenIds = {}
695
+
696
+ /** Push `alert` only if its severity changed since the previous poll. */
697
+ function emitIfChanged(alerts, alert) {
698
+ var id = alert.id
699
+ var rank = _SEV_RANK[alert.severity] || 0
700
+ _seenIds[id] = true
701
+ if (_armed[id] === rank) return // still armed at the same severity → suppress
702
+ _armed[id] = rank
703
+ alerts.push(alert)
704
+ }
705
+
706
+ /** Disarm any alert id that is no longer triggering this poll. */
707
+ function disarmAbsent() {
708
+ for (var id in _armed) {
709
+ if (_armed.hasOwnProperty(id) && !_seenIds[id]) delete _armed[id]
710
+ }
711
+ }
712
+
713
+ /** Called at the end of every poll cycle: disarm ids that stopped
714
+ * triggering. Ids still armed (and suppressed at the same severity) are
715
+ * kept so they continue to be suppressed on the next cycle instead of
716
+ * re-firing as if new. */
717
+ function settleArmed() {
718
+ disarmAbsent()
719
+ }
720
+
721
+ /** Disposal actions attached to every memory/GC alert. dsh-flash renders
722
+ * one button per entry and calls onClick(alert, api). Contract in
723
+ * dsh-flash/lib/client.js `_showAlertDetail` → plugin-registered action
724
+ * buttons. Returning a Promise keeps the button disabled until settled;
725
+ * returning {message} keeps the detail modal open so the saved location
726
+ * stays visible (the user pulls down the .heapsnapshot browser download
727
+ * we trigger programmatically — the status line is plain text, not HTML,
728
+ * so we cannot render a live link into it). */
729
+ function _dumpHeapActions() {
730
+ return [{
731
+ id: 'dump-heap',
732
+ label: function () { return t('alertDisposeDumpHeap') },
733
+ primary: true,
734
+ onClick: function (_alert, api) {
735
+ api.status(t('alertDumping'), 'info')
736
+ return fetch('/plugins/dsh-flash-mem-mon/dump-heap', { method: 'POST' })
737
+ .then(function (r) { return r.json().catch(function () { return null }) })
738
+ .then(function (d) {
739
+ if (!d || !d.ok || !d.name) {
740
+ var err = (d && d.error) || 'unknown'
741
+ api.status(t('alertDumpFailed').replace('{error}', err), 'error')
742
+ return { message: t('alertDumpFailed').replace('{error}', err), level: 'error' }
743
+ }
744
+ // Stream the snapshot down as a browser download (same origin).
745
+ var a = document.createElement('a')
746
+ a.href = '/plugins/dsh-flash-mem-mon/heap-dump'
747
+ a.download = d.name
748
+ document.body.appendChild(a)
749
+ a.click()
750
+ a.remove()
751
+ var size = d.size >= 1048576
752
+ ? (d.size / 1048576).toFixed(1) + ' MB'
753
+ : Math.max(1, Math.round(d.size / 1024)) + ' KB'
754
+ var msg = t('alertDumpSaved').replace('{name}', d.name).replace('{size}', size)
755
+ api.status(msg, 'info')
756
+ return { message: msg, level: 'info' }
757
+ })
758
+ .catch(function (err) {
759
+ var msg = t('alertDumpFailed').replace('{error}', (err && err.message) || 'network')
760
+ api.status(msg, 'error')
761
+ return { message: msg, level: 'error' }
762
+ })
763
+ },
764
+ }]
765
+ }
766
+
767
+ function poll() {
768
+ if (!callback) return
769
+ var alerts = []
770
+ _seenIds = {}
771
+ fetch('/plugins/dsh-flash-mem-mon/memory-trend?mode=summary')
772
+ .then(function (r) { return r.json() })
773
+ .then(function (d) {
774
+ if (!d || !d.current) {
775
+ settleArmed()
776
+ try { callback(alerts) } catch (_) {}
777
+ return
778
+ }
779
+
780
+ var rssMB = Math.round(d.current.rss / 1048576)
781
+ var heapMB = Math.round(d.current.heapUsed / 1048576)
782
+ var heapTotalMB = Math.round(d.current.heapTotal / 1048576)
783
+ var extMB = Math.round(d.current.external / 1048576)
784
+ var abMB = Math.round(d.current.arrayBuffers / 1048576)
785
+ var heapPct = d.current.heapTotal > 0 ? Math.round(d.current.heapUsed / d.current.heapTotal * 100) : 0
786
+ var trendLabel = d.trend === 'up' ? '↑' : d.trend === 'down' ? '↓' : '→'
787
+ var rssSlope = d.rssSlopePerMin || 0
788
+ var slopeLabel = rssSlope.toFixed(1)
789
+ // GC summary snippet for RSS alerts
790
+ var gcInfoSnippet = ''
791
+ if (d.gc && d.gc.majorPerMin > 0) {
792
+ gcInfoSnippet = ' | GC Major ' + d.gc.majorPerMin.toFixed(1) + '/min 暂停 ' + (d.gc.gcPausePerMin || 0).toFixed(0) + 'ms/min'
793
+ }
794
+
795
+ // ── Criterion 1: RSS absolute value ────────────────────────
796
+ var tInfo = _alertPref('memThresholdInfo')
797
+ var tWarn = _alertPref('memThresholdWarning')
798
+ var tErr = _alertPref('memThresholdError')
799
+ var tCrit = _alertPref('memThresholdCritical')
800
+ // Validate: info < warning < error < critical
801
+ if (!(tInfo > 0 && tWarn > tInfo && tErr > tWarn && tCrit > tErr)) {
802
+ tInfo = 256; tWarn = 512; tErr = 1024; tCrit = 1536
803
+ }
804
+
805
+ // Shared RSS alert message formatter.
806
+ var _rssMsg = function () {
807
+ return t('alertHostMemMsg')
808
+ .replace('{v}', rssMB)
809
+ .replace('{heap}', heapMB)
810
+ .replace('{heapTotal}', heapTotalMB)
811
+ .replace('{pct}', heapPct)
812
+ .replace('{ext}', extMB)
813
+ .replace('{ab}', abMB)
814
+ .replace('{trend}', trendLabel)
815
+ .replace('{slope}', slopeLabel)
816
+ .replace('{gcInfo}', gcInfoSnippet)
817
+ }
818
+
819
+ if (rssMB >= tCrit) {
820
+ emitIfChanged(alerts, {
821
+ id: 'host-mem-critical',
822
+ severity: 'critical',
823
+ title: function () { return t('alertHostMemCritical').replace('{threshold}', tCrit) },
824
+ message: _rssMsg,
825
+ icon: '🔴',
826
+ timestamp: Date.now(),
827
+ dismissible: true,
828
+ actions: _dumpHeapActions(),
829
+ })
830
+ } else if (rssMB >= tErr) {
831
+ emitIfChanged(alerts, {
832
+ id: 'host-mem-error',
833
+ severity: 'error',
834
+ title: function () { return t('alertHostMemError').replace('{threshold}', tErr) },
835
+ message: _rssMsg,
836
+ icon: '🟠',
837
+ timestamp: Date.now(),
838
+ dismissible: true,
839
+ actions: _dumpHeapActions(),
840
+ })
841
+ } else if (rssMB >= tWarn) {
842
+ emitIfChanged(alerts, {
843
+ id: 'host-mem-warning',
844
+ severity: 'warning',
845
+ title: function () { return t('alertHostMemWarning').replace('{threshold}', tWarn) },
846
+ message: _rssMsg,
847
+ icon: '🟡',
848
+ timestamp: Date.now(),
849
+ dismissible: true,
850
+ actions: _dumpHeapActions(),
851
+ })
852
+ } else if (rssMB >= tInfo) {
853
+ emitIfChanged(alerts, {
854
+ id: 'host-mem-info',
855
+ severity: 'info',
856
+ title: function () { return t('alertHostMemInfo').replace('{threshold}', tInfo) },
857
+ message: _rssMsg,
858
+ icon: '🔵',
859
+ timestamp: Date.now(),
860
+ dismissible: true,
861
+ actions: _dumpHeapActions(),
862
+ })
863
+ }
864
+
865
+ // ── Criterion 2: RSS growth rate ───────────────────────────
866
+ // Threshold is configurable (memGrowthAlertPerMin); the "trend up"
867
+ // guard is redundant with slope > 0 but kept for clarity.
868
+ var growthThresh = _alertPref('memGrowthAlertPerMin')
869
+ if (growthThresh > 0 && rssSlope > growthThresh && d.trend === 'up') {
870
+ var hasRssAlert = alerts.some(function (a) {
871
+ return a.id.indexOf('host-mem-') === 0
872
+ })
873
+ if (!hasRssAlert) {
874
+ emitIfChanged(alerts, {
875
+ id: 'host-mem-growth',
876
+ severity: 'info',
877
+ title: function () { return t('alertHostMemGrowth') },
878
+ message: function () {
879
+ return t('alertHostMemGrowthMsg')
880
+ .replace('{r}', rssSlope.toFixed(1))
881
+ .replace('{rss}', rssMB)
882
+ .replace('{heap}', heapMB)
883
+ .replace('{heapTotal}', heapTotalMB)
884
+ .replace('{pct}', heapPct)
885
+ .replace('{ab}', abMB)
886
+ .replace('{trend}', trendLabel)
887
+ },
888
+ icon: '📈',
889
+ timestamp: Date.now(),
890
+ dismissible: true,
891
+ actions: _dumpHeapActions(),
892
+ })
893
+ }
894
+ }
895
+
896
+ // ── Criterion 3: Major GC frequency ────────────────────────
897
+ if (d.gc && d.gc.majorPerMin > 0) {
898
+ var gcInfo = _alertPref('gcThresholdInfo')
899
+ var gcWarn = _alertPref('gcThresholdWarning')
900
+ var gcErr = _alertPref('gcThresholdError')
901
+ if (!(gcInfo > 0 && gcWarn > gcInfo && gcErr > gcWarn)) {
902
+ gcInfo = 2; gcWarn = 5; gcErr = 10
903
+ }
904
+ var mpm = d.gc.majorPerMin
905
+ var gcPausePerMin = d.gc.gcPausePerMin || 0
906
+ var minorCount = d.gc.minorCount || 0
907
+
908
+ // Shared GC alert message formatter.
909
+ var _gcMsg = function () {
910
+ return t('alertHostGcMsg')
911
+ .replace('{v}', mpm.toFixed(1))
912
+ .replace('{pause}', gcPausePerMin.toFixed(1))
913
+ .replace('{minor}', minorCount)
914
+ .replace('{rss}', rssMB)
915
+ .replace('{heap}', heapMB)
916
+ .replace('{heapTotal}', heapTotalMB)
917
+ .replace('{pct}', heapPct)
918
+ }
919
+
920
+ if (mpm >= gcErr) {
921
+ emitIfChanged(alerts, {
922
+ id: 'host-gc-error',
923
+ severity: 'error',
924
+ title: function () { return t('alertHostGcError') },
925
+ message: _gcMsg,
926
+ icon: '🟠',
927
+ timestamp: Date.now(),
928
+ dismissible: true,
929
+ actions: _dumpHeapActions(),
930
+ })
931
+ } else if (mpm >= gcWarn) {
932
+ emitIfChanged(alerts, {
933
+ id: 'host-gc-warning',
934
+ severity: 'warning',
935
+ title: function () { return t('alertHostGcWarning') },
936
+ message: _gcMsg,
937
+ icon: '🟡',
938
+ timestamp: Date.now(),
939
+ dismissible: true,
940
+ actions: _dumpHeapActions(),
941
+ })
942
+ } else if (mpm >= gcInfo) {
943
+ emitIfChanged(alerts, {
944
+ id: 'host-gc-info',
945
+ severity: 'info',
946
+ title: function () { return t('alertHostGcInfo') },
947
+ message: _gcMsg,
948
+ icon: '🔵',
949
+ timestamp: Date.now(),
950
+ dismissible: true,
951
+ actions: _dumpHeapActions(),
952
+ })
953
+ }
954
+ }
955
+
956
+ // Publish the summary to the shared cache so the config modal can
957
+ // render its trend panel without issuing its own summary poll
958
+ // (dedup of the /memory-trend?mode=summary round-trip).
959
+ _setSummaryCache(d)
960
+
961
+ try { settleArmed(); callback(alerts) } catch (_) {}
962
+ })
963
+ .catch(function () {
964
+ settleArmed()
965
+ try { callback([]) } catch (_) {}
966
+ })
967
+
968
+ // Schedule next poll
969
+ var pollBase = _alertPref('memPollBase')
970
+ var nextInterval = pollBase
971
+ timer = setTimeout(poll, nextInterval)
972
+ }
973
+
974
+ return {
975
+ id: 'dsh-flash-mem-mon:host-memory-alert',
976
+ start: function (cb) {
977
+ callback = cb
978
+ poll()
979
+ },
980
+ stop: function () {
981
+ callback = null
982
+ if (timer) { clearTimeout(timer); timer = null }
983
+ },
984
+ }
985
+ }
986
+
987
+ //#endregion ───────────────────────────────────────────────────────────────
988
+
989
+ // ═══════════════════════════════════════════════════════════════════════
990
+ //#region Icon helpers ────────────────────────────────────────────────────
991
+
992
+ var _ICON_PATHS = {
993
+ bolt: ['M9 2 3.5 9H8l-1 5 5.5-7H8l1-5z'],
994
+ signal: ['M3.2 11.6a6.8 6.8 0 0 1 9.6 0M5.6 9.2a4.2 4.2 0 0 1 4.8 0M8 6.8c.6 0 1.2.2 1.6.5M8 13a.9.9 0 1 0 0-1.8.9.9 0 0 0 0 1.8z'],
995
+ shield: ['M8 2 3 3.8v3.5c0 3.7 2.2 6.1 5 6.7 2.8-.6 5-3 5-6.7V3.8z', 'M6.2 8.1l1.2 1.2 2.4-2.6'],
996
+ }
997
+
998
+ function _switchIcon(icon, style) {
999
+ var paths = typeof icon === 'string' ? _ICON_PATHS[icon] : null
1000
+ if (!paths) return h('span', { style: style }, icon)
1001
+ return h('span', { style: style, 'aria-hidden': 'true' },
1002
+ h('svg', {
1003
+ width: 12, height: 12, viewBox: '0 0 16 16',
1004
+ fill: 'none', xmlns: 'http://www.w3.org/2000/svg',
1005
+ style: { display: 'block', margin: 'auto', color: 'currentColor' },
1006
+ }, paths.map(function (d, i) {
1007
+ return h('path', { key: i, d: d, stroke: 'currentColor', strokeWidth: 1.4,
1008
+ strokeLinecap: 'round', strokeLinejoin: 'round' })
1009
+ }))
1010
+ )
1011
+ }
1012
+
1013
+ //#endregion ───────────────────────────────────────────────────────────────
1014
+
1015
+ // ═══════════════════════════════════════════════════════════════════════
1016
+ //#region ErrorBoundary ──────────────────────────────────────────────────
1017
+
1018
+ class PanelErrorBoundary extends Component {
1019
+ constructor(props) {
1020
+ super(props)
1021
+ this.state = { hasError: false, error: null }
1022
+ }
1023
+ static getDerivedStateFromError(error) {
1024
+ return { hasError: true, error: error }
1025
+ }
1026
+ componentDidCatch(error, info) {
1027
+ console.error('[dsh-flash-mem-mon] Panel render error (caught by boundary):', error, info)
1028
+ }
1029
+ render() {
1030
+ if (this.state.hasError) {
1031
+ return h('div', {
1032
+ style: {
1033
+ padding: '12px 16px',
1034
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1035
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1036
+ fontSize: '12px',
1037
+ },
1038
+ },
1039
+ h('div', { style: { marginBottom: '6px', color: 'var(--dsw-alias-label-warning, #d29922)' } },
1040
+ '⚠ mem-mon render error'),
1041
+ h('div', null, String(this.state.error && this.state.error.message || this.state.error || 'Unknown error')),
1042
+ h('button', {
1043
+ style: {
1044
+ marginTop: '8px',
1045
+ padding: '2px 10px',
1046
+ fontSize: '11px',
1047
+ cursor: 'pointer',
1048
+ borderRadius: '4px',
1049
+ border: '1px solid var(--dsw-alias-border-l2, #21262d)',
1050
+ background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.85))',
1051
+ color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1052
+ },
1053
+ onClick: function () { this.setState({ hasError: false, error: null }) }.bind(this),
1054
+ }, 'Retry'),
1055
+ )
1056
+ }
1057
+ return this.props.children
1058
+ }
1059
+ }
1060
+
1061
+ //#endregion ───────────────────────────────────────────────────────────────
1062
+
1063
+ // ═══════════════════════════════════════════════════════════════════════
1064
+ //#region Styles ─────────────────────────────────────────────────────────
1065
+
1066
+ var R = {
1067
+ xs: 'var(--dsw-radius-xs, 4px)',
1068
+ sm: 'var(--dsw-radius-sm, 8px)',
1069
+ md: 'var(--dsw-radius-md, 12px)',
1070
+ lg: 'var(--dsw-radius-lg, 16px)',
1071
+ xl: 'var(--dsw-radius-xl, 20px)',
1072
+ panel: 'var(--dsw-radius-panel, 28px)',
1073
+ }
1074
+ var E = {
1075
+ prominent: 'var(--dsw-elevation-prominent, 0 0 0 0.5px var(--dsw-alias-border-l4, #0003), 0 3px 8px 0 rgba(0,0,0,.04), 0 0 20px 0 rgba(0,0,0,.05))',
1076
+ }
1077
+
1078
+ var S = {
1079
+ // Switch row / slider (used by MonitorConfigModal)
1080
+ switchRow: {
1081
+ display: 'flex',
1082
+ alignItems: 'center',
1083
+ gap: '8px',
1084
+ marginBottom: '8px',
1085
+ },
1086
+ switchLabel: {
1087
+ display: 'flex',
1088
+ alignItems: 'center',
1089
+ gap: '6px',
1090
+ minWidth: '120px',
1091
+ fontSize: '12px',
1092
+ color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1093
+ },
1094
+ switchIcon: {
1095
+ display: 'inline-flex',
1096
+ verticalAlign: '-2px',
1097
+ marginRight: '2px',
1098
+ },
1099
+ sliderRow: {
1100
+ flex: 1,
1101
+ display: 'flex',
1102
+ alignItems: 'center',
1103
+ gap: '8px',
1104
+ minWidth: 0,
1105
+ marginBottom: '8px',
1106
+ },
1107
+ slider: {
1108
+ flex: 1,
1109
+ minWidth: 0,
1110
+ },
1111
+ value: {
1112
+ fontSize: '11px',
1113
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1114
+ minWidth: '42px',
1115
+ textAlign: 'right',
1116
+ },
1117
+ // Monitor config modal
1118
+ monitorModalMask: {
1119
+ position: 'fixed',
1120
+ inset: 0,
1121
+ zIndex: 2200,
1122
+ background: 'var(--dsw-alias-bg-mask-1, #0000003d)',
1123
+ display: 'flex',
1124
+ alignItems: 'center',
1125
+ justifyContent: 'center',
1126
+ },
1127
+ monitorModal: {
1128
+ background: 'var(--dsw-alias-bg-layer-2, #fff)',
1129
+ border: 0,
1130
+ borderRadius: R.panel,
1131
+ width: '90vw',
1132
+ maxWidth: '680px',
1133
+ maxHeight: '80vh',
1134
+ display: 'flex',
1135
+ flexDirection: 'column',
1136
+ overflow: 'hidden',
1137
+ boxShadow: E.prominent,
1138
+ },
1139
+ monitorModalHead: {
1140
+ display: 'flex',
1141
+ alignItems: 'center',
1142
+ justifyContent: 'space-between',
1143
+ padding: '10px 14px',
1144
+ borderBottom: '0.5px solid var(--dsw-alias-border-l2, #0003)',
1145
+ },
1146
+ monitorModalTitle: {
1147
+ fontWeight: 500,
1148
+ fontSize: '16px',
1149
+ lineHeight: '24px',
1150
+ color: 'var(--dsw-alias-label-primary, #000)',
1151
+ },
1152
+ monitorModalClose: {
1153
+ display: 'inline-flex',
1154
+ alignItems: 'center',
1155
+ justifyContent: 'center',
1156
+ width: '28px',
1157
+ height: '28px',
1158
+ border: 0,
1159
+ borderRadius: R.sm,
1160
+ background: 'transparent',
1161
+ color: 'var(--dsw-alias-label-secondary, #61666b)',
1162
+ cursor: 'pointer',
1163
+ fontSize: '14px',
1164
+ padding: 0,
1165
+ },
1166
+ monitorModalBody: {
1167
+ flex: 1,
1168
+ overflowY: 'auto',
1169
+ padding: '10px 14px',
1170
+ scrollbarWidth: 'thin',
1171
+ scrollbarColor: 'var(--dsw-alias-scrollbar-bg-l1, #ccc) var(--dsw-alias-bg-layer-2, transparent)',
1172
+ },
1173
+ monitorModalDivider: {
1174
+ marginTop: '12px',
1175
+ paddingTop: '10px',
1176
+ borderTop: '0.5px solid var(--dsw-alias-border-l2, #0003)',
1177
+ },
1178
+ monitorModalSectionTitle: {
1179
+ display: 'block',
1180
+ fontWeight: 500,
1181
+ fontSize: '14px',
1182
+ lineHeight: '22px',
1183
+ marginBottom: '8px',
1184
+ color: 'var(--dsw-alias-label-secondary, #61666b)',
1185
+ },
1186
+ monitorFieldsGrid: {
1187
+ display: 'grid',
1188
+ gridTemplateColumns: '1fr 1fr',
1189
+ columnGap: '16px',
1190
+ rowGap: '12px',
1191
+ },
1192
+ monitorConfigTwoCol: {
1193
+ display: 'grid',
1194
+ gridTemplateColumns: '1fr 1fr',
1195
+ columnGap: '16px',
1196
+ },
1197
+ monitorConfigGroupTitle: {
1198
+ display: 'block',
1199
+ fontWeight: 600,
1200
+ fontSize: '12px',
1201
+ marginBottom: '6px',
1202
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1203
+ },
1204
+ // Alert preview
1205
+ alertPreviewToggle: {
1206
+ background: 'none',
1207
+ border: 0,
1208
+ padding: 0,
1209
+ cursor: 'pointer',
1210
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1211
+ fontSize: '12px',
1212
+ },
1213
+ alertPreviewCard: {
1214
+ padding: '8px 12px',
1215
+ background: 'var(--dsw-alias-bg-layer-1, #f5f5f5)',
1216
+ borderRadius: R.md,
1217
+ fontSize: '12px',
1218
+ lineHeight: '1.5',
1219
+ },
1220
+ alertPreviewGroup: {
1221
+ marginBottom: '8px',
1222
+ },
1223
+ alertPreviewGroupLabel: {
1224
+ display: 'block',
1225
+ fontWeight: 600,
1226
+ fontSize: '11px',
1227
+ marginBottom: '4px',
1228
+ color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1229
+ },
1230
+ alertPreviewRow: {
1231
+ display: 'flex',
1232
+ alignItems: 'center',
1233
+ gap: '6px',
1234
+ marginBottom: '2px',
1235
+ },
1236
+ alertPreviewIcon: {
1237
+ flex: 'none',
1238
+ fontSize: '11px',
1239
+ },
1240
+ alertPreviewMsg: {
1241
+ flex: 1,
1242
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1243
+ },
1244
+ alertPreviewCond: {
1245
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1246
+ fontSize: '11px',
1247
+ opacity: 0.6,
1248
+ },
1249
+ // Memory live data
1250
+ memColumnCard: {
1251
+ padding: '8px 12px',
1252
+ background: 'var(--dsw-alias-bg-layer-1, #f5f5f5)',
1253
+ borderRadius: R.md,
1254
+ },
1255
+ memSubSectionLabel: {
1256
+ display: 'block',
1257
+ fontWeight: 600,
1258
+ fontSize: '11px',
1259
+ marginBottom: '6px',
1260
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1261
+ },
1262
+ memLiveDataGrid: {
1263
+ display: 'grid',
1264
+ gridTemplateColumns: 'auto auto',
1265
+ columnGap: '10px',
1266
+ rowGap: '4px',
1267
+ fontSize: '12px',
1268
+ },
1269
+ memLiveDataLabel: {
1270
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1271
+ },
1272
+ memLiveDataValue: {
1273
+ color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1274
+ fontWeight: 500,
1275
+ },
1276
+ memUsageBarTrack: {
1277
+ height: '4px',
1278
+ borderRadius: '2px',
1279
+ background: 'rgba(255,255,255,0.08)',
1280
+ overflow: 'hidden',
1281
+ marginTop: '2px',
1282
+ },
1283
+ memUsageBarFill: {
1284
+ height: '100%',
1285
+ borderRadius: '2px',
1286
+ transition: 'width 0.3s ease, background 0.3s ease',
1287
+ },
1288
+ // Sparkline
1289
+ sparklineWrap: {
1290
+ position: 'relative',
1291
+ height: '80px',
1292
+ marginTop: '6px',
1293
+ marginBottom: '2px',
1294
+ },
1295
+ sparklineSvg: {
1296
+ position: 'absolute',
1297
+ inset: 0,
1298
+ width: '100%',
1299
+ height: '100%',
1300
+ },
1301
+ sparklineHover: {
1302
+ position: 'absolute',
1303
+ top: 0,
1304
+ bottom: 0,
1305
+ width: '1px',
1306
+ background: 'rgba(255,255,255,0.2)',
1307
+ pointerEvents: 'none',
1308
+ },
1309
+ sparklineTooltip: {
1310
+ position: 'absolute',
1311
+ top: '-2px',
1312
+ left: '50%',
1313
+ transform: 'translateX(-50%)',
1314
+ fontSize: '10px',
1315
+ lineHeight: '14px',
1316
+ padding: '2px 6px',
1317
+ borderRadius: '4px',
1318
+ background: 'var(--dsw-alias-bg-layer-1, #f5f5f5)',
1319
+ border: '0.5px solid var(--dsw-alias-border-l4, #0003)',
1320
+ color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1321
+ whiteSpace: 'nowrap',
1322
+ pointerEvents: 'none',
1323
+ },
1324
+ // Trend chart axes (HTML overlays — the SVG uses preserveAspectRatio:none
1325
+ // so SVG <text> would stretch illegibly).
1326
+ trendAxisWrap: {
1327
+ display: 'flex',
1328
+ alignItems: 'stretch',
1329
+ gap: '4px',
1330
+ },
1331
+ trendAxisY: {
1332
+ display: 'flex',
1333
+ flexDirection: 'column',
1334
+ justifyContent: 'space-between',
1335
+ width: '34px',
1336
+ fontSize: '9px',
1337
+ lineHeight: '10px',
1338
+ textAlign: 'right',
1339
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1340
+ userSelect: 'none',
1341
+ },
1342
+ trendChartWrap: {
1343
+ display: 'flex',
1344
+ flexDirection: 'column',
1345
+ flex: '1 1 auto',
1346
+ minWidth: '0',
1347
+ },
1348
+ trendAxisX: {
1349
+ display: 'flex',
1350
+ justifyContent: 'space-between',
1351
+ fontSize: '9px',
1352
+ lineHeight: '12px',
1353
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1354
+ marginTop: '2px',
1355
+ userSelect: 'none',
1356
+ },
1357
+ // GC event list
1358
+ gcEventListWrap: {
1359
+ maxHeight: '120px',
1360
+ overflowY: 'auto',
1361
+ scrollbarWidth: 'thin',
1362
+ },
1363
+ gcEventRow: {
1364
+ display: 'flex',
1365
+ alignItems: 'center',
1366
+ gap: '8px',
1367
+ padding: '2px 0',
1368
+ fontSize: '11px',
1369
+ cursor: 'pointer',
1370
+ borderRadius: '3px',
1371
+ },
1372
+ gcEventRowHighlight: {
1373
+ background: 'rgba(218, 54, 51, 0.12)',
1374
+ },
1375
+ gcEventTimeCol: {
1376
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1377
+ minWidth: '56px',
1378
+ },
1379
+ gcEventDurationCol: {
1380
+ color: 'var(--dsw-alias-label-primary, #c9d1d9)',
1381
+ minWidth: '100px',
1382
+ },
1383
+ gcEventKindCol: {
1384
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1385
+ },
1386
+ gcEventEmpty: {
1387
+ color: 'var(--dsw-alias-label-secondary, #8b949e)',
1388
+ fontSize: '11px',
1389
+ opacity: 0.5,
1390
+ },
1391
+ }
1392
+
1393
+ //#endregion ───────────────────────────────────────────────────────────────
1394
+
1395
+ // ═══════════════════════════════════════════════════════════════════════
1396
+ //#region MonitorConfigModal (memory only) ──────────────────────────────
1397
+
1398
+ var MONITOR_SLIDER_FIELDS = {
1399
+ memory: [
1400
+ { key: 'memThresholdInfo', labelKey: 'memThresholdInfo', tooltipKey: 'memThresholdInfoTip', group: 'rss', min: 64, max: 1024, step: 64, format: function (v) { return v + ' MB' } },
1401
+ { key: 'memThresholdWarning', labelKey: 'memThresholdWarning', tooltipKey: 'memThresholdWarningTip', group: 'rss', min: 128, max: 1536, step: 64, format: function (v) { return v + ' MB' } },
1402
+ { key: 'memThresholdError', labelKey: 'memThresholdError', tooltipKey: 'memThresholdErrorTip', group: 'rss', min: 256, max: 4096, step: 64, format: function (v) { return v + ' MB' } },
1403
+ { key: 'memThresholdCritical',labelKey: 'memThresholdCritical',tooltipKey: 'memThresholdCriticalTip',group: 'rss', min: 512, max: 4096, step: 64, format: function (v) { return v + ' MB' } },
1404
+ { key: 'memPollBase', labelKey: 'memPollBase', tooltipKey: 'memPollBaseTip', group: 'rss', min: 5000, max: 60000, step: 1000, format: function (v) { return v + 'ms' } },
1405
+ { key: 'memPollMin', labelKey: 'memPollMin', tooltipKey: 'memPollMinTip', group: 'rss', min: 1000, max: 10000, step: 500, format: function (v) { return v + 'ms' } },
1406
+ { key: 'memGrowthAlertPerMin',labelKey: 'memGrowthThreshold', tooltipKey: 'memGrowthThresholdTip', group: 'rss', min: 1, max: 100, step: 1, format: function (v) { return v + ' MB/min' } },
1407
+ { key: 'gcThresholdInfo', labelKey: 'gcThresholdInfo', tooltipKey: 'gcThresholdInfoTip', group: 'gc', min: 1, max: 5, step: 1, format: function (v) { return v + '/min' } },
1408
+ { key: 'gcThresholdWarning', labelKey: 'gcThresholdWarning', tooltipKey: 'gcThresholdWarningTip', group: 'gc', min: 2, max: 8, step: 1, format: function (v) { return v + '/min' } },
1409
+ { key: 'gcThresholdError', labelKey: 'gcThresholdError', tooltipKey: 'gcThresholdErrorTip', group: 'gc', min: 4, max: 15, step: 1, format: function (v) { return v + '/min' } },
1410
+ ],
1411
+ }
1412
+
1413
+ var MONITOR_TITLE_KEY = {
1414
+ memory: 'alertMemCluster',
1415
+ }
1416
+
1417
+ var _monitorConfigRoot = null
1418
+ var _monitorConfigHost = null
1419
+
1420
+ function closeMonitorConfig() {
1421
+ if (_monitorConfigRoot) { try { _monitorConfigRoot.unmount() } catch (_) {} _monitorConfigRoot = null }
1422
+ if (_monitorConfigHost) { try { _monitorConfigHost.remove() } catch (_) {} _monitorConfigHost = null }
1423
+ }
1424
+
1425
+ function openMonitorConfig(monitor) {
1426
+ if (!ReactDOMClient || !ReactDOMClient.createRoot) return
1427
+ closeMonitorConfig()
1428
+ var host = document.createElement('div')
1429
+ host.style.cssText = 'position:fixed;inset:0;z-index:2200;'
1430
+ document.body.appendChild(host)
1431
+ _monitorConfigHost = host
1432
+ try {
1433
+ _monitorConfigRoot = ReactDOMClient.createRoot(host)
1434
+ _monitorConfigRoot.render(h(MonitorConfigModal, {
1435
+ monitor: monitor,
1436
+ onClose: closeMonitorConfig,
1437
+ }))
1438
+ } catch (e) {
1439
+ console.error('[dsh-flash-mem-mon] failed to open monitor config:', e)
1440
+ closeMonitorConfig()
1441
+ }
1442
+ }
1443
+
1444
+ function MonitorConfigModal(props) {
1445
+ var monitor = props.monitor
1446
+ var onClose = props.onClose
1447
+ var fields = MONITOR_SLIDER_FIELDS[monitor] || []
1448
+ var _tick = useState(0)
1449
+ var tick = _tick[0], setTick = _tick[1]
1450
+
1451
+ // Host process memory — fetched from /health route (cross-plugin, dock-flash).
1452
+ var _hostMem = useState(null)
1453
+ var hostMem = _hostMem[0], setHostMem = _hostMem[1]
1454
+
1455
+ // Memory trend summary — fetched from own /memory-trend route.
1456
+ var _trend = useState(null)
1457
+ var trendData = _trend[0], setTrendData = _trend[1]
1458
+
1459
+ // Memory trend full samples — fetched on demand for sparkline.
1460
+ var _trendSamples = useState(null)
1461
+ var trendSamples = _trendSamples[0], setTrendSamples = _trendSamples[1]
1462
+
1463
+ // GC events from the same full fetch — for sparkline overlay markers.
1464
+ var _gcEvents = useState(null)
1465
+ var gcEvents = _gcEvents[0], setGcEvents = _gcEvents[1]
1466
+
1467
+ // Session footprint — total/archived session counts and on-disk usage
1468
+ // from our own /session-footprint route (cached host snapshot).
1469
+ var _footprint = useState(null)
1470
+ var footprint = _footprint[0], setFootprint = _footprint[1]
1471
+
1472
+ // Sparkline hover index (null = not hovering).
1473
+ var _hoverIdx = useState(null)
1474
+ var hoverIdx = _hoverIdx[0], setHoverIdx = _hoverIdx[1]
1475
+
1476
+ // GC event highlight index (null = none selected).
1477
+ var _gcHighlightIdx = useState(null)
1478
+ var gcHighlightIdx = _gcHighlightIdx[0], setGcHighlightIdx = _gcHighlightIdx[1]
1479
+
1480
+ // Close on Escape.
1481
+ useEffect(function () {
1482
+ function onKey(e) { if (e.key === 'Escape') onClose() }
1483
+ document.addEventListener('keydown', onKey)
1484
+ return function () { document.removeEventListener('keydown', onKey) }
1485
+ }, [onClose])
1486
+
1487
+ // Re-render when the global language changes. Every string in this modal is
1488
+ // a `t()` call, so a re-render is the whole fix; without this lever the
1489
+ // open modal would keep its old language until the next slider drag or the
1490
+ // next 2s host-memory poll.
1491
+ useEffect(function () {
1492
+ return t.onLocaleChange(function () { setTick(function (v) { return v + 1 }) })
1493
+ }, [])
1494
+
1495
+ // Auto-refresh host memory data every 2 seconds while the modal is open.
1496
+ useEffect(function () {
1497
+ function fetchHost() {
1498
+ fetch('/plugins/dsh-flash/health').then(function (r) { return r.json() }).then(function (d) {
1499
+ if (d && d.memory) setHostMem(d.memory)
1500
+ }).catch(function () {})
1501
+ }
1502
+ fetchHost()
1503
+ var hostTimer = setInterval(fetchHost, 2000)
1504
+ return function () { clearInterval(hostTimer) }
1505
+ }, [])
1506
+
1507
+ // Trend summary comes from the SHARED cache populated by the alert
1508
+ // provider's poll — subscribing (instead of issuing our own summary poll)
1509
+ // avoids double-fetching /memory-trend?mode=summary while the modal is
1510
+ // open. The provider polls roughly every memPollBase, so live updates
1511
+ // arrive at that cadence.
1512
+ useEffect(function () {
1513
+ var unsub = _subscribeSummary(function (d) {
1514
+ if (d) setTrendData(d)
1515
+ })
1516
+ // If the shared cache is empty (e.g. the alert provider is disabled or
1517
+ // has not polled yet), fetch once so the trend panel still renders.
1518
+ if (!_summaryCache) {
1519
+ fetch('/plugins/dsh-flash-mem-mon/memory-trend?mode=summary')
1520
+ .then(function (r) { return r.json() })
1521
+ .then(function (d) { if (d) _setSummaryCache(d) })
1522
+ .catch(function () {})
1523
+ }
1524
+ return function () { unsub() }
1525
+ }, [])
1526
+
1527
+ // Fetch full samples for sparkline (once on mount + periodic refresh).
1528
+ useEffect(function () {
1529
+ function fetchSamples() {
1530
+ fetch('/plugins/dsh-flash-mem-mon/memory-trend?mode=full')
1531
+ .then(function (r) { return r.json() })
1532
+ .then(function (d) {
1533
+ if (d && d.samples) setTrendSamples(d.samples.slice(-360))
1534
+ if (d && d.gcEvents) setGcEvents(d.gcEvents)
1535
+ })
1536
+ .catch(function () {})
1537
+ }
1538
+ fetchSamples()
1539
+ var sampleTimer = setInterval(fetchSamples, 5000)
1540
+ return function () { clearInterval(sampleTimer) }
1541
+ }, [])
1542
+
1543
+ // Session footprint — the host collector rescans no more often than every
1544
+ // ~5 min and serves a cached snapshot with the routes' last `scannedAt`, so
1545
+ // the panel is fetched only occasionally; a low-frequency fallback keeps
1546
+ // the panel live even if the shared cache never lands here.
1547
+ useEffect(function () {
1548
+ function fetchFootprint() {
1549
+ fetch('/plugins/dsh-flash-mem-mon/session-footprint')
1550
+ .then(function (r) { return r.json() })
1551
+ .then(function (d) { if (d) setFootprint(d) })
1552
+ .catch(function () {})
1553
+ }
1554
+ fetchFootprint()
1555
+ var footprintTimer = setInterval(fetchFootprint, 60000)
1556
+ return function () { clearInterval(footprintTimer) }
1557
+ }, [])
1558
+
1559
+ // Render a single slider field row.
1560
+ function renderSliderField(f) {
1561
+ var val = Number(_alertPref(f.key)) || 0
1562
+ var hintEl = f.hintKey
1563
+ ? h('div', { style: { fontSize: '11px', opacity: 0.4, marginTop: '-2px' } }, t(f.hintKey))
1564
+ : null
1565
+ return h('div', { key: f.key, style: S.switchRow },
1566
+ h('span', { style: S.switchLabel },
1567
+ h('span', { style: S.switchIcon }, '⚙'),
1568
+ h('span', null, t(f.labelKey)),
1569
+ hintEl,
1570
+ ),
1571
+ h('div', { style: S.sliderRow },
1572
+ h('input', {
1573
+ type: 'range',
1574
+ min: f.min, max: f.max, step: f.step,
1575
+ value: val,
1576
+ style: S.slider,
1577
+ onChange: function (e) {
1578
+ var next = Number(e.target.value)
1579
+ _writeAlertPref(_prefCtx, f.key, next, function (patch, rollback) { rollback() })
1580
+ setTick(function (v) { return v + 1 })
1581
+ },
1582
+ }),
1583
+ h('span', { style: S.value }, f.format(val)),
1584
+ ),
1585
+ )
1586
+ }
1587
+
1588
+ // For memory config: two-column layout (RSS + polling on the left, GC on the right).
1589
+ var fieldEls
1590
+ if (fields.some(function (f) { return f.group })) {
1591
+ var rssFields = fields.filter(function (f) { return f.group === 'rss' })
1592
+ var gcFields = fields.filter(function (f) { return f.group === 'gc' })
1593
+ fieldEls = h('div', { style: S.monitorConfigTwoCol },
1594
+ h('div', null,
1595
+ h('span', { style: S.monitorConfigGroupTitle }, t('memConfigGroupRss')),
1596
+ rssFields.map(renderSliderField),
1597
+ ),
1598
+ h('div', null,
1599
+ h('span', { style: S.monitorConfigGroupTitle }, t('memConfigGroupGc')),
1600
+ gcFields.map(renderSliderField),
1601
+ ),
1602
+ )
1603
+ } else {
1604
+ fieldEls = fields.map(renderSliderField)
1605
+ }
1606
+
1607
+ // ── Alert preview section ──────────────────────────────────────────
1608
+ var _showPreview = useState(false)
1609
+ var showPreview = _showPreview[0], setShowPreview = _showPreview[1]
1610
+
1611
+ var alertRows = []
1612
+ // ── Criterion 1: RSS absolute value ──
1613
+ var _tI = _alertPref('memThresholdInfo')
1614
+ var _tW = _alertPref('memThresholdWarning')
1615
+ var _tE = _alertPref('memThresholdError')
1616
+ var _tC = _alertPref('memThresholdCritical')
1617
+ // If user thresholds violate ordering (or are unset), fall back to the
1618
+ // canonical defaults (mirrored from host DEFAULT_MEM_*).
1619
+ if (!(_tI > 0 && _tW > _tI && _tE > _tW && _tC > _tE)) {
1620
+ _tI = _MEM_ALERT_DEFAULTS.memThresholdInfo
1621
+ _tW = _MEM_ALERT_DEFAULTS.memThresholdWarning
1622
+ _tE = _MEM_ALERT_DEFAULTS.memThresholdError
1623
+ _tC = _MEM_ALERT_DEFAULTS.memThresholdCritical
1624
+ }
1625
+
1626
+ // Helper to fill all RSS alert template placeholders with sample values for preview.
1627
+ var _rssPreviewMsg = function (rssVal) {
1628
+ return t('alertHostMemMsg')
1629
+ .replace('{v}', rssVal + '')
1630
+ .replace('{heap}', '…')
1631
+ .replace('{heapTotal}', '…')
1632
+ .replace('{pct}', '…')
1633
+ .replace('{ext}', '…')
1634
+ .replace('{ab}', '…')
1635
+ .replace('{trend}', '→')
1636
+ .replace('{slope}', '0.0')
1637
+ .replace('{gcInfo}', '')
1638
+ }
1639
+
1640
+ alertRows.push(
1641
+ h('div', { key: 'rss', style: S.alertPreviewGroup },
1642
+ h('span', { style: S.alertPreviewGroupLabel }, t('alertPreviewCriteriaRss')),
1643
+ h('div', { style: S.alertPreviewRow },
1644
+ h('span', { style: S.alertPreviewIcon }, '🔵'),
1645
+ h('span', { style: S.alertPreviewMsg }, t('alertHostMemInfo').replace('{threshold}', _tI)),
1646
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _tI + ' MB'),
1647
+ ),
1648
+ h('div', { style: S.alertPreviewRow },
1649
+ h('span', { style: S.alertPreviewIcon }, '🟡'),
1650
+ h('span', { style: S.alertPreviewMsg }, t('alertHostMemWarning').replace('{threshold}', _tW)),
1651
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _tW + ' MB'),
1652
+ ),
1653
+ h('div', { style: S.alertPreviewRow },
1654
+ h('span', { style: S.alertPreviewIcon }, '🟠'),
1655
+ h('span', { style: S.alertPreviewMsg }, t('alertHostMemError').replace('{threshold}', _tE)),
1656
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _tE + ' MB'),
1657
+ ),
1658
+ h('div', { style: S.alertPreviewRow },
1659
+ h('span', { style: S.alertPreviewIcon }, '🔴'),
1660
+ h('span', { style: S.alertPreviewMsg }, t('alertHostMemCritical').replace('{threshold}', _tC)),
1661
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _tC + ' MB'),
1662
+ ),
1663
+ ),
1664
+ )
1665
+
1666
+ // ── Criterion 2: RSS growth rate ──
1667
+ var _growthPreviewMsg = function () {
1668
+ return t('alertHostMemGrowthMsg')
1669
+ .replace('{r}', 'N')
1670
+ .replace('{rss}', '…')
1671
+ .replace('{heap}', '…')
1672
+ .replace('{heapTotal}', '…')
1673
+ .replace('{pct}', '…')
1674
+ .replace('{ab}', '…')
1675
+ .replace('{trend}', '↑')
1676
+ }
1677
+ alertRows.push(
1678
+ h('div', { key: 'growth', style: S.alertPreviewGroup },
1679
+ h('span', { style: S.alertPreviewGroupLabel }, t('alertPreviewCriteriaGrowth')),
1680
+ h('div', { style: S.alertPreviewRow },
1681
+ h('span', { style: S.alertPreviewIcon }, '📈'),
1682
+ h('span', { style: S.alertPreviewMsg }, t('alertHostMemGrowth') + ': ' + _growthPreviewMsg()),
1683
+ h('span', { style: S.alertPreviewCond }, '> 1 MB/min + ↑'),
1684
+ ),
1685
+ ),
1686
+ )
1687
+
1688
+ // ── Criterion 3: Major GC frequency ──
1689
+ var _gI = _alertPref('gcThresholdInfo')
1690
+ var _gW = _alertPref('gcThresholdWarning')
1691
+ var _gE = _alertPref('gcThresholdError')
1692
+ if (!(_gI > 0 && _gW > _gI && _gE > _gW)) { _gI = 2; _gW = 5; _gE = 10 }
1693
+
1694
+ // Helper to fill all GC alert template placeholders with sample values for preview.
1695
+ var _gcPreviewMsg = function (gcVal) {
1696
+ return t('alertHostGcMsg')
1697
+ .replace('{v}', gcVal + '')
1698
+ .replace('{pause}', '…')
1699
+ .replace('{minor}', '…')
1700
+ .replace('{rss}', '…')
1701
+ .replace('{heap}', '…')
1702
+ .replace('{heapTotal}', '…')
1703
+ .replace('{pct}', '…')
1704
+ }
1705
+
1706
+ alertRows.push(
1707
+ h('div', { key: 'gc', style: S.alertPreviewGroup },
1708
+ h('span', { style: S.alertPreviewGroupLabel }, t('alertPreviewCriteriaGc')),
1709
+ h('div', { style: S.alertPreviewRow },
1710
+ h('span', { style: S.alertPreviewIcon }, '🔵'),
1711
+ h('span', { style: S.alertPreviewMsg }, t('alertHostGcInfo') + ': ' + _gcPreviewMsg(_gI)),
1712
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _gI + '/min'),
1713
+ ),
1714
+ h('div', { style: S.alertPreviewRow },
1715
+ h('span', { style: S.alertPreviewIcon }, '🟡'),
1716
+ h('span', { style: S.alertPreviewMsg }, t('alertHostGcWarning') + ': ' + _gcPreviewMsg(_gW)),
1717
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _gW + '/min'),
1718
+ ),
1719
+ h('div', { style: S.alertPreviewRow },
1720
+ h('span', { style: S.alertPreviewIcon }, '🟠'),
1721
+ h('span', { style: S.alertPreviewMsg }, t('alertHostGcError') + ': ' + _gcPreviewMsg(_gE)),
1722
+ h('span', { style: S.alertPreviewCond }, '≥ ' + _gE + '/min'),
1723
+ ),
1724
+ ),
1725
+ )
1726
+
1727
+ var alertPreviewEl = h('div', { key: 'alert-preview' },
1728
+ h('div', { style: S.monitorModalDivider }),
1729
+ h('button', {
1730
+ type: 'button',
1731
+ style: S.alertPreviewToggle,
1732
+ onClick: function () { setShowPreview(function (v) { return !v }) },
1733
+ },
1734
+ (showPreview ? '▾ ' : '▸ ') + t('alertPreviewToggle'),
1735
+ ),
1736
+ showPreview ? h('div', { style: Object.assign({}, S.alertPreviewCard, { marginTop: '6px' }) },
1737
+ alertRows,
1738
+ ) : null,
1739
+ )
1740
+
1741
+ // ── Live memory data section ──────────────────────────────────────
1742
+ void hostMem // force React to re-render when host data arrives
1743
+ void trendData // force React to re-render when trend data arrives
1744
+ void trendSamples
1745
+ void gcEvents
1746
+ void gcHighlightIdx
1747
+
1748
+ var memLiveDataEl = null
1749
+ // Threshold values in MB for RSS bar colour.
1750
+ var tInfo = _alertPref('memThresholdInfo')
1751
+ var tWarn = _alertPref('memThresholdWarning')
1752
+ var tErr = _alertPref('memThresholdError')
1753
+ var tCrit = _alertPref('memThresholdCritical')
1754
+ if (!(tInfo > 0 && tWarn > tInfo && tErr > tWarn && tCrit > tErr)) { tInfo = 256; tWarn = 512; tErr = 1024; tCrit = 1536 }
1755
+ var rssBarBaseline = Math.max(tCrit, 2048)
1756
+
1757
+ // ── Host-side Node.js process ──────────────────────────────────
1758
+ var hostSectionEl = null
1759
+ if (hostMem) {
1760
+ var rssMB = (hostMem.rss / 1048576).toFixed(1)
1761
+ var rssMBNum = hostMem.rss / 1048576
1762
+ var heapTotalMB = (hostMem.heapTotal / 1048576).toFixed(1)
1763
+ var heapUsedMB = (hostMem.heapUsed / 1048576).toFixed(1)
1764
+ var externalMB = (hostMem.external / 1048576).toFixed(1)
1765
+ var arrayBufMB = (hostMem.arrayBuffers / 1048576).toFixed(1)
1766
+ var heapPct = hostMem.heapTotal > 0 ? Math.round(hostMem.heapUsed / hostMem.heapTotal * 100) : 0
1767
+ var hBarColor = 'var(--dsw-alias-brand-primary, #58a6ff)'
1768
+ if (rssMBNum >= tCrit) hBarColor = '#f85149'
1769
+ else if (rssMBNum >= tErr) hBarColor = '#f0883e'
1770
+ else if (rssMBNum >= tWarn) hBarColor = '#d29922'
1771
+ else if (rssMBNum >= tInfo) hBarColor = '#58a6ff'
1772
+ var rssBarPct = Math.min(Math.round(rssMBNum / rssBarBaseline * 100), 100)
1773
+ hostSectionEl = h('div', { style: S.memColumnCard },
1774
+ h('span', { style: S.memSubSectionLabel }, t('memHostSection')),
1775
+ h('div', { style: S.monitorConfigTwoCol },
1776
+ // ── Left: Process-level memory ──
1777
+ h('div', null,
1778
+ h('div', { style: S.memLiveDataGrid },
1779
+ h('span', { style: S.memLiveDataLabel }, t('memHostRSS')),
1780
+ h('span', { style: S.memLiveDataValue }, rssMB + ' MB'),
1781
+ h('span', { style: { gridColumn: '1 / -1' } },
1782
+ h('div', { style: S.memUsageBarTrack },
1783
+ h('div', {
1784
+ style: Object.assign({}, S.memUsageBarFill, {
1785
+ width: rssBarPct + '%',
1786
+ background: hBarColor,
1787
+ }),
1788
+ })
1789
+ )
1790
+ ),
1791
+ h('span', { style: S.memLiveDataLabel }, t('memHostExternal')),
1792
+ h('span', { style: S.memLiveDataValue }, externalMB + ' MB'),
1793
+ h('span', { style: S.memLiveDataLabel }, t('memHostArrayBuffers')),
1794
+ h('span', { style: S.memLiveDataValue }, arrayBufMB + ' MB'),
1795
+ ),
1796
+ ),
1797
+ // ── Right: Heap memory ──
1798
+ h('div', null,
1799
+ h('div', { style: S.memLiveDataGrid },
1800
+ h('span', { style: S.memLiveDataLabel }, t('memHostHeapUsed')),
1801
+ h('span', { style: S.memLiveDataValue }, heapUsedMB + ' MB'),
1802
+ h('span', { style: S.memLiveDataLabel }, t('memHostHeapTotal')),
1803
+ h('span', { style: S.memLiveDataValue }, heapTotalMB + ' MB'),
1804
+ h('span', { style: S.memLiveDataLabel }, t('memHostHeapRatio')),
1805
+ h('span', { style: S.memLiveDataValue }, heapPct + '%'),
1806
+ h('span', { style: { gridColumn: '1 / -1' } },
1807
+ h('div', { style: S.memUsageBarTrack },
1808
+ h('div', {
1809
+ style: Object.assign({}, S.memUsageBarFill, {
1810
+ width: heapPct + '%',
1811
+ background: heapPct >= 95 ? '#f85149' : heapPct >= 85 ? '#f0883e' : heapPct >= 70 ? '#d29922' : 'var(--dsw-alias-brand-primary, #58a6ff)',
1812
+ }),
1813
+ })
1814
+ )
1815
+ ),
1816
+ ),
1817
+ ),
1818
+ ),
1819
+ )
1820
+ } else {
1821
+ hostSectionEl = h('div', { style: S.memColumnCard },
1822
+ h('span', { style: S.memSubSectionLabel }, t('memHostSection')),
1823
+ h('span', { style: S.memLiveDataLabel }, t('memHostUnavailable')),
1824
+ )
1825
+ }
1826
+
1827
+ // ── Memory trend section ───────────────────────────────────────
1828
+ var trendSectionEl = null
1829
+ if (trendData && trendData.current) {
1830
+ var cur = trendData.current
1831
+
1832
+ // Trend direction
1833
+ var dirKey = trendData.trend === 'up' ? 'memTrendUp' : trendData.trend === 'down' ? 'memTrendDown' : 'memTrendStable'
1834
+ var dirIcon = trendData.trend === 'up' ? '↗' : trendData.trend === 'down' ? '↘' : '→'
1835
+ var dirColor = trendData.trend === 'up' ? '#f85149' : trendData.trend === 'down' ? '#3fb950' : 'var(--dsw-alias-label-secondary, #8b949e)'
1836
+
1837
+ // Peak (RSS)
1838
+ var peakEl = null
1839
+ if (trendData.peak) {
1840
+ var peakMB = (trendData.peak.rss / 1048576).toFixed(1)
1841
+ peakEl = h('span', { style: S.memLiveDataValue }, peakMB + ' MB')
1842
+ } else {
1843
+ peakEl = h('span', { style: S.memLiveDataValue }, '—')
1844
+ }
1845
+
1846
+ // Trend summary stats
1847
+ var trendStats = []
1848
+ trendStats.push(
1849
+ h('span', { style: S.memLiveDataLabel }, t('memTrendDirection')),
1850
+ h('span', { style: Object.assign({}, S.memLiveDataValue, { color: dirColor }) }, dirIcon + ' ' + t(dirKey)),
1851
+ )
1852
+ trendStats.push(
1853
+ h('span', { style: S.memLiveDataLabel }, t('memTrendPeak')),
1854
+ peakEl,
1855
+ )
1856
+ if (trendData.sampleCount) {
1857
+ trendStats.push(
1858
+ h('span', { style: S.memLiveDataLabel }, t('memTrendSamples')),
1859
+ h('span', { style: S.memLiveDataValue }, '' + trendData.sampleCount),
1860
+ )
1861
+ }
1862
+ if (trendData.spanMs) {
1863
+ var spanMin = Math.round(trendData.spanMs / 60000)
1864
+ trendStats.push(
1865
+ h('span', { style: S.memLiveDataLabel }, t('memTrendSpan')),
1866
+ h('span', { style: S.memLiveDataValue }, spanMin + ' min'),
1867
+ )
1868
+ }
1869
+ if (typeof cur.heapRatio === 'number') {
1870
+ trendStats.push(
1871
+ h('span', { style: S.memLiveDataLabel }, t('memTrendHeapRatio')),
1872
+ h('span', { style: S.memLiveDataValue }, (cur.heapRatio * 100).toFixed(0) + '%'),
1873
+ )
1874
+ }
1875
+ if (trendData.rssSlopePerMin) {
1876
+ trendStats.push(
1877
+ h('span', { style: S.memLiveDataLabel }, t('memTrendGrowthRate')),
1878
+ h('span', { style: S.memLiveDataValue }, (trendData.rssSlopePerMin > 0 ? '+' : '') + trendData.rssSlopePerMin.toFixed(2) + ' MB/min'),
1879
+ )
1880
+ }
1881
+
1882
+ // GC summary
1883
+ if (trendData.gc) {
1884
+ trendStats.push(
1885
+ h('span', { style: S.memLiveDataLabel }, t('memGcMajorPerMin')),
1886
+ h('span', { style: S.memLiveDataValue }, trendData.gc.majorPerMin.toFixed(2)),
1887
+ )
1888
+ trendStats.push(
1889
+ h('span', { style: S.memLiveDataLabel }, t('memGcMinorCount')),
1890
+ h('span', { style: S.memLiveDataValue }, '' + trendData.gc.minorCount),
1891
+ )
1892
+ trendStats.push(
1893
+ h('span', { style: S.memLiveDataLabel }, t('memGcMajorCount')),
1894
+ h('span', { style: S.memLiveDataValue }, '' + trendData.gc.majorCount),
1895
+ )
1896
+ }
1897
+
1898
+ // Sparkline
1899
+ var sparklineEl = null
1900
+ if (trendSamples && trendSamples.length > 1) {
1901
+ var svgW = 280
1902
+ var svgH = 80
1903
+ var padY = 4
1904
+ var rssValues = trendSamples.map(function (s) { return s.rss })
1905
+ var maxRss = Math.max.apply(null, rssValues)
1906
+ // Shared Y-axis: min fixed at 0, max = RSS peak — both lines on the same absolute scale.
1907
+ var yAxisMax = maxRss
1908
+ var yAxisRange = yAxisMax || 1
1909
+ var xStep = svgW / (trendSamples.length - 1)
1910
+
1911
+ // Build polyline points for RSS (0-based scale)
1912
+ var points = []
1913
+ for (var i = 0; i < trendSamples.length; i++) {
1914
+ var x = i * xStep
1915
+ var y = svgH - padY - (rssValues[i] / yAxisRange) * (svgH - 2 * padY)
1916
+ points.push(x.toFixed(1) + ',' + y.toFixed(1))
1917
+ }
1918
+
1919
+ // Heap line (same 0-based scale)
1920
+ var heapPoints = null
1921
+ var heapValues = trendSamples.map(function (s) { return s.heapUsed })
1922
+ if (Math.max.apply(null, heapValues) > 0) {
1923
+ heapPoints = []
1924
+ for (var i = 0; i < trendSamples.length; i++) {
1925
+ var x = i * xStep
1926
+ var y = svgH - padY - (heapValues[i] / yAxisRange) * (svgH - 2 * padY)
1927
+ heapPoints.push(x.toFixed(1) + ',' + y.toFixed(1))
1928
+ }
1929
+ }
1930
+
1931
+ // GC event markers
1932
+ // Only Full GC events (non-scavenge, i.e. kind bit 1 unset) are drawn
1933
+ // on the trend chart, scoped to the same visible window as
1934
+ // `trendSamples` (majors outside [tsFirst, tsLast] are skipped so they
1935
+ // don't map off-viewBox), and the x is clamped so an in-window Full GC
1936
+ // is always drawn. Each marker's identity is its original index in the
1937
+ // `gcEvents` payload so clicking a Full GC row in the list (which sets
1938
+ // `gcHighlightIdx` to that same index) can make its marker stand out.
1939
+ var gcMarkers = null
1940
+ var gcMarkerPositions = [] // [{geIdx, rx, ts}] for click hit-testing in overlay
1941
+ if (gcEvents && gcEvents.length > 0) {
1942
+ var tsFirst = trendSamples[0].ts
1943
+ var tsLast = trendSamples[trendSamples.length - 1].ts
1944
+ var tsRange = tsLast - tsFirst || 1
1945
+ gcMarkers = gcEvents.map(function (ge, geIdx) {
1946
+ if ((ge.kind & 1) !== 0) return null // not a Full GC
1947
+ if (ge.ts < tsFirst || ge.ts > tsLast) return null // outside window
1948
+ var x = Math.max(0, Math.min(((ge.ts - tsFirst) / tsRange) * svgW, svgW))
1949
+ var baseWidth = Math.max(1.5, svgW / (trendSamples.length - 1) / 4)
1950
+ // Store position for overlay click hit-testing.
1951
+ gcMarkerPositions.push({ geIdx: geIdx, rx: x / svgW, ts: ge.ts })
1952
+ // Selected (or none selected) → prominent; otherwise dimmed so the
1953
+ // current Full GC visually pops against the rest.
1954
+ var isSel = gcHighlightIdx === geIdx
1955
+ var width = isSel ? baseWidth * 1.4 : baseWidth * 0.6
1956
+ return h('g', { key: 'gc-' + geIdx },
1957
+ h('rect', {
1958
+ x: x - width / 2, y: 0, width: width, height: svgH,
1959
+ fill: isSel ? 'rgba(218, 54, 51, 0.25)' : 'rgba(240, 136, 62, 0.07)',
1960
+ }),
1961
+ h('line', {
1962
+ x1: x, y1: 0, x2: x, y2: svgH,
1963
+ stroke: isSel ? '#da3633' : 'rgba(240, 136, 62, 0.30)',
1964
+ strokeWidth: isSel ? 1.5 : 1,
1965
+ }),
1966
+ )
1967
+ })
1968
+ }
1969
+
1970
+ // Hover line + tooltip
1971
+ var hoverLineEl = null
1972
+ var hoverTooltipEl = null
1973
+ if (hoverIdx !== null && hoverIdx >= 0 && hoverIdx < trendSamples.length) {
1974
+ var hx = hoverIdx * xStep
1975
+ hoverLineEl = h('line', {
1976
+ x1: hx, y1: 0, x2: hx, y2: svgH,
1977
+ stroke: 'rgba(255,255,255,0.2)', strokeWidth: 1,
1978
+ })
1979
+ var hSample = trendSamples[hoverIdx]
1980
+ var hRssMB = (hSample.rss / 1048576).toFixed(1)
1981
+ var hHeapMB = hSample.heapUsed ? (hSample.heapUsed / 1048576).toFixed(1) : null
1982
+ var hTime = new Date(hSample.ts)
1983
+ var hTimeStr = hTime.getHours().toString().padStart(2, '0') + ':' +
1984
+ hTime.getMinutes().toString().padStart(2, '0') + ':' +
1985
+ hTime.getSeconds().toString().padStart(2, '0')
1986
+ hoverTooltipEl = h('div', {
1987
+ style: Object.assign({}, S.sparklineTooltip, {
1988
+ left: Math.min(hx, svgW - 80) + 'px',
1989
+ }),
1990
+ },
1991
+ t('memTrendHoverRSS') + ': ' + hRssMB + ' MB',
1992
+ hHeapMB ? ' ' + t('memTrendHoverHeap') + ': ' + hHeapMB + ' MB' : '',
1993
+ ' ' + t('memTrendHoverTime') + ': ' + hTimeStr,
1994
+ )
1995
+ }
1996
+
1997
+ // ---- Coordinate axes (HTML overlays; the SVG viewBox uses
1998
+ // preserveAspectRatio:none so SVG <text> would stretch illegibly) ----
1999
+ var _fmtHM = function (ts) {
2000
+ var d = new Date(ts)
2001
+ return d.getHours().toString().padStart(2, '0') + ':' +
2002
+ d.getMinutes().toString().padStart(2, '0')
2003
+ }
2004
+ var _fmtMB = function (v) { return Math.round(v / 1048576) }
2005
+
2006
+ // X axis: first / middle / last sample times (HH:MM).
2007
+ var tMid = trendSamples[Math.floor((trendSamples.length - 1) / 2)].ts
2008
+ var tLast = trendSamples[trendSamples.length - 1].ts
2009
+ var xAxisEl = h('div', { style: S.trendAxisX },
2010
+ h('span', null, _fmtHM(trendSamples[0].ts)),
2011
+ h('span', null, _fmtHM(tMid)),
2012
+ h('span', null, _fmtHM(tLast)),
2013
+ )
2014
+
2015
+ // Y axis: 0 / mid / max (0-based scale, both lines share it).
2016
+ var yAxisEl = h('div', {
2017
+ style: Object.assign({}, S.trendAxisY, { height: svgH + 'px' }),
2018
+ },
2019
+ h('span', null, _fmtMB(yAxisMax) + 'M'),
2020
+ h('span', null, _fmtMB(yAxisMax / 2) + 'M'),
2021
+ h('span', null, '0'),
2022
+ )
2023
+
2024
+ sparklineEl = h('div', { style: S.trendAxisWrap },
2025
+ yAxisEl,
2026
+ h('div', { style: S.trendChartWrap },
2027
+ h('div', { style: S.sparklineWrap },
2028
+ h('svg', {
2029
+ viewBox: '0 0 ' + svgW + ' ' + svgH,
2030
+ preserveAspectRatio: 'none',
2031
+ style: S.sparklineSvg,
2032
+ },
2033
+ heapPoints ? h('polyline', {
2034
+ points: heapPoints.join(' '),
2035
+ fill: 'none',
2036
+ stroke: 'rgba(88, 166, 255, 0.3)',
2037
+ strokeWidth: 1,
2038
+ }) : null,
2039
+ h('polyline', {
2040
+ points: points.join(' '),
2041
+ fill: 'none',
2042
+ stroke: '#3fb950',
2043
+ strokeWidth: 1.2,
2044
+ }),
2045
+ gcMarkers,
2046
+ hoverLineEl,
2047
+ ),
2048
+ hoverTooltipEl,
2049
+ // Mouse interaction overlay
2050
+ h('div', {
2051
+ style: { position: 'absolute', inset: 0, cursor: 'crosshair' },
2052
+ onMouseMove: function (e) {
2053
+ var rect = e.currentTarget.getBoundingClientRect()
2054
+ var rx = (e.clientX - rect.left) / rect.width
2055
+ var idx = Math.round(rx * (trendSamples.length - 1))
2056
+ setHoverIdx(Math.max(0, Math.min(idx, trendSamples.length - 1)))
2057
+ // Switch cursor to pointer when hovering near a GC marker.
2058
+ var nearGc = false
2059
+ var mousePx = e.clientX - rect.left
2060
+ for (var gi = 0; gi < gcMarkerPositions.length; gi++) {
2061
+ if (Math.abs(gcMarkerPositions[gi].rx * rect.width - mousePx) <= 5) {
2062
+ nearGc = true
2063
+ break
2064
+ }
2065
+ }
2066
+ e.currentTarget.style.cursor = nearGc ? 'pointer' : 'crosshair'
2067
+ },
2068
+ onMouseLeave: function (e) {
2069
+ setHoverIdx(null)
2070
+ e.currentTarget.style.cursor = 'crosshair'
2071
+ },
2072
+ onClick: function (e) {
2073
+ // Check if click is near a Full GC marker.
2074
+ // Use pixel distance: only match if within ±5px of a marker.
2075
+ var rect = e.currentTarget.getBoundingClientRect()
2076
+ var clickPx = e.clientX - rect.left
2077
+ var pxPerUnit = rect.width / svgW
2078
+ var hitPx = 5
2079
+ var hitGc = null
2080
+ var hitDist = Infinity
2081
+ for (var gi = 0; gi < gcMarkerPositions.length; gi++) {
2082
+ var markerPx = gcMarkerPositions[gi].rx * rect.width
2083
+ var d = Math.abs(clickPx - markerPx)
2084
+ if (d <= hitPx && d < hitDist) {
2085
+ hitGc = gcMarkerPositions[gi]
2086
+ hitDist = d
2087
+ }
2088
+ }
2089
+ if (hitGc) {
2090
+ // Toggle: clicking the same marker again deselects.
2091
+ setGcHighlightIdx(gcHighlightIdx === hitGc.geIdx ? null : hitGc.geIdx)
2092
+ // Move hover to the nearest sample.
2093
+ var bestIdx = 0
2094
+ var bestDist2 = Math.abs(trendSamples[0].ts - hitGc.ts)
2095
+ for (var si2 = 1; si2 < trendSamples.length; si2++) {
2096
+ var dist2 = Math.abs(trendSamples[si2].ts - hitGc.ts)
2097
+ if (dist2 < bestDist2) { bestDist2 = dist2; bestIdx = si2 }
2098
+ }
2099
+ setHoverIdx(bestIdx)
2100
+ } else {
2101
+ // Clicked empty area → clear GC highlight.
2102
+ setGcHighlightIdx(null)
2103
+ }
2104
+ },
2105
+ }),
2106
+ ),
2107
+ xAxisEl,
2108
+ ),
2109
+ )
2110
+ }
2111
+
2112
+ // GC event list
2113
+ var gcEventListEl = (function () {
2114
+ if (!trendSamples || trendSamples.length === 0) {
2115
+ return h('div', null,
2116
+ h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
2117
+ h('div', { style: S.gcEventEmpty }, t('memGcNoEvents')),
2118
+ )
2119
+ }
2120
+ var _dropThresh = 10 * 1048576 // 10 MB
2121
+ var dropEvents = []
2122
+ // A) RSS drops from samples
2123
+ for (var si = 1; si < trendSamples.length; si++) {
2124
+ var rssDrop = trendSamples[si - 1].rss - trendSamples[si].rss
2125
+ if (rssDrop >= _dropThresh) {
2126
+ dropEvents.push({
2127
+ idx: dropEvents.length,
2128
+ ts: trendSamples[si].ts,
2129
+ rssDropMB: (rssDrop / 1048576).toFixed(1),
2130
+ source: 'sample',
2131
+ sampleIdx: si,
2132
+ })
2133
+ }
2134
+ }
2135
+ // B) gcEvents (kind=2, already filtered server-side)
2136
+ if (gcEvents && gcEvents.length > 0) {
2137
+ var tsFirst = trendSamples[0].ts
2138
+ var tsLast = trendSamples[trendSamples.length - 1].ts
2139
+ // Track which sample RSS-drop segments have already been attributed to a
2140
+ // Full GC event. Without this, two Full GCs that fire around the same
2141
+ // memory release can both grab the same -XX MB drop and double count it.
2142
+ var sampleDropClaimed = {}
2143
+ for (var ei = 0; ei < gcEvents.length; ei++) {
2144
+ var ge = gcEvents[ei]
2145
+ if (ge.ts < tsFirst || ge.ts > tsLast) continue
2146
+ // Try to find the RSS drop around this GC event from samples.
2147
+ var rssDropMB = null
2148
+ var merged = false
2149
+ // First: check if it merges with an existing sample-sourced RSS drop event (within 5s).
2150
+ for (var di = 0; di < dropEvents.length; di++) {
2151
+ var dropItem = dropEvents[di]
2152
+ if (dropItem.source === 'sample' && Math.abs(dropItem.ts - ge.ts) < 5000) {
2153
+ if (!sampleDropClaimed[dropItem.sampleIdx]) {
2154
+ sampleDropClaimed[dropItem.sampleIdx] = true
2155
+ rssDropMB = dropItem.rssDropMB
2156
+ dropItem.source = 'gc'
2157
+ dropItem.duration = ge.duration
2158
+ dropItem.gcIdx = ei
2159
+ merged = true
2160
+ }
2161
+ // If this drop was already claimed by an earlier GC, don't
2162
+ // attribute it again — fall through and add this GC as its own
2163
+ // row without reusing the same memory-release number.
2164
+ break
2165
+ }
2166
+ }
2167
+ // Second: if no existing RSS drop merged, scan nearby samples for a
2168
+ // drop, skipping segments already claimed by an earlier event.
2169
+ if (!merged) {
2170
+ for (var si2 = 1; si2 < trendSamples.length; si2++) {
2171
+ var timeDiff = Math.abs(trendSamples[si2].ts - ge.ts)
2172
+ if (timeDiff > 10000) continue // only look within ±10s
2173
+ if (sampleDropClaimed[si2]) continue // drop already attributed elsewhere
2174
+ var nearDrop = trendSamples[si2 - 1].rss - trendSamples[si2].rss
2175
+ if (nearDrop > 0) {
2176
+ sampleDropClaimed[si2] = true
2177
+ rssDropMB = (nearDrop / 1048576).toFixed(1)
2178
+ break
2179
+ }
2180
+ }
2181
+ dropEvents.push({
2182
+ idx: dropEvents.length,
2183
+ ts: ge.ts,
2184
+ rssDropMB: rssDropMB,
2185
+ source: 'gc',
2186
+ gcIdx: ei,
2187
+ duration: ge.duration,
2188
+ })
2189
+ }
2190
+ }
2191
+ }
2192
+ // Merge both sources back into strict chronological order. A Full GC event
2193
+ // found outside any sample's RSS-drop window is appended after the sample
2194
+ // events, which may leave the combined list out of order.
2195
+ dropEvents.sort(function (a, b) { return a.ts - b.ts })
2196
+ if (dropEvents.length === 0 && (!trendData.gc || trendData.gc.majorCount === 0)) {
2197
+ return h('div', null,
2198
+ h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
2199
+ h('div', { style: S.gcEventEmpty }, t('memGcNoEvents')),
2200
+ )
2201
+ }
2202
+ var listContent = null
2203
+ if (dropEvents.length > 0) {
2204
+ listContent = h('div', { style: S.gcEventListWrap },
2205
+ dropEvents.map(function (item) {
2206
+ var evIdx = item.idx
2207
+ var evTs = item.ts
2208
+ var evTime = new Date(evTs)
2209
+ var evTimeStr = evTime.getHours().toString().padStart(2, '0') + ':' +
2210
+ evTime.getMinutes().toString().padStart(2, '0') + ':' +
2211
+ evTime.getSeconds().toString().padStart(2, '0')
2212
+ var isGcSource = item.source === 'gc'
2213
+ // Highlight identity must match the chart marker key, which is
2214
+ // the original index in the `gcEvents` payload. GC-sourced rows
2215
+ // carry that index in `item.gcIdx`; RSS-drop rows have no chart
2216
+ // marker, so fall back to the row index.
2217
+ var evKey = isGcSource ? item.gcIdx : evIdx
2218
+ var isHighlighted = gcHighlightIdx === evKey
2219
+ // Build detail string: show both duration and memory released when available.
2220
+ var detailParts = []
2221
+ if (isGcSource && item.duration) {
2222
+ detailParts.push(item.duration.toFixed(1) + ' ms')
2223
+ }
2224
+ if (item.rssDropMB && item.rssDropMB !== '—') {
2225
+ detailParts.push('-' + item.rssDropMB + ' MB')
2226
+ }
2227
+ var detailStr = detailParts.length > 0 ? detailParts.join(' | ') : '—'
2228
+ var kindStr = isGcSource ? 'Full GC' : t('memGcEventRssDrop')
2229
+ return h('div', {
2230
+ key: 'gc-ev-' + evIdx,
2231
+ ref: isHighlighted ? function (el) {
2232
+ if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
2233
+ } : undefined,
2234
+ style: Object.assign({}, S.gcEventRow, isHighlighted ? S.gcEventRowHighlight : {}),
2235
+ onClick: function () {
2236
+ if (!trendSamples || trendSamples.length === 0) return
2237
+ var bestIdx = 0
2238
+ var bestDist = Math.abs(trendSamples[0].ts - evTs)
2239
+ for (var si2 = 1; si2 < trendSamples.length; si2++) {
2240
+ var dist = Math.abs(trendSamples[si2].ts - evTs)
2241
+ if (dist < bestDist) { bestDist = dist; bestIdx = si2 }
2242
+ }
2243
+ setHoverIdx(bestIdx)
2244
+ setGcHighlightIdx(evKey)
2245
+ },
2246
+ onMouseEnter: function (e) {
2247
+ e.currentTarget.style.background = 'rgba(218, 54, 51, 0.18)'
2248
+ },
2249
+ onMouseLeave: function (e) {
2250
+ e.currentTarget.style.background = isHighlighted ? 'rgba(218, 54, 51, 0.12)' : ''
2251
+ },
2252
+ },
2253
+ h('span', { style: S.gcEventTimeCol }, evTimeStr),
2254
+ h('span', { style: S.gcEventDurationCol }, detailStr),
2255
+ h('span', { style: S.gcEventKindCol }, kindStr),
2256
+ )
2257
+ })
2258
+ )
2259
+ } else {
2260
+ listContent = h('div', { style: S.gcEventEmpty }, t('memGcNoEvents'))
2261
+ }
2262
+ return h('div', null,
2263
+ h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
2264
+ listContent,
2265
+ )
2266
+ })()
2267
+
2268
+ trendSectionEl = h('div', { style: S.memColumnCard },
2269
+ h('span', { style: S.memSubSectionLabel }, t('memTrend')),
2270
+ // Trend chart on top.
2271
+ sparklineEl,
2272
+ // Below: two columns — trend info on the left, Full GC list on the right.
2273
+ h('div', { style: Object.assign({}, S.monitorConfigTwoCol, { marginTop: '8px' }) },
2274
+ h('div', null,
2275
+ h('span', { style: S.monitorConfigGroupTitle }, t('memTrendInfo')),
2276
+ h('div', { style: S.memLiveDataGrid }, trendStats),
2277
+ ),
2278
+ gcEventListEl,
2279
+ ),
2280
+ )
2281
+ } else {
2282
+ trendSectionEl = h('div', { style: S.memColumnCard },
2283
+ h('span', { style: S.memSubSectionLabel }, t('memTrend')),
2284
+ h('span', { style: S.memLiveDataLabel }, t('memTrendNoData')),
2285
+ )
2286
+ }
2287
+
2288
+ var footprintSectionEl = (function () {
2289
+ function formatBytesSync(n) {
2290
+ if (!isFinite(n) || n < 0) return '—'
2291
+ if (n < 1024) return n + ' B'
2292
+ var units = ['KB', 'MB', 'GB', 'TB', 'PB']
2293
+ var v = n
2294
+ var u = -1
2295
+ do { v /= 1024; u++ } while (v >= 1024 && u < units.length - 1)
2296
+ return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[u]
2297
+ }
2298
+ var fp = footprint
2299
+ var totalBytes = 0
2300
+ var totalSessions = 0
2301
+ var totalArchived = 0
2302
+ var archivedKnown = false
2303
+ if (fp && Array.isArray(fp.workspaces)) {
2304
+ fp.workspaces.forEach(function (w) {
2305
+ totalSessions += Number(w.totalSessions) || 0
2306
+ totalArchived += Number(w.archivedSessions) || 0
2307
+ totalBytes += Number(w.bytes) || 0
2308
+ if (typeof w.archivedSessions === 'number') archivedKnown = true
2309
+ })
2310
+ }
2311
+ if (!fp) {
2312
+ return h('div', { style: Object.assign({}, S.memColumnCard, { marginTop: '8px' }) },
2313
+ h('span', { style: S.memSubSectionLabel }, t('footprintTitle')),
2314
+ h('span', { style: S.memLiveDataLabel }, t('footprintNoData')),
2315
+ )
2316
+ }
2317
+ return h('div', { style: Object.assign({}, S.memColumnCard, { marginTop: '8px' }) },
2318
+ h('span', { style: S.memSubSectionLabel }, t('footprintTitle')),
2319
+ h('div', { style: S.memLiveDataGrid },
2320
+ h('span', { style: S.memLiveDataLabel }, t('footprintTotalSessions')),
2321
+ h('span', { style: S.memLiveDataValue }, String(totalSessions)),
2322
+ h('span', { style: S.memLiveDataLabel }, t('footprintArchivedSessions')),
2323
+ h('span', { style: S.memLiveDataValue },
2324
+ archivedKnown
2325
+ ? String(totalArchived)
2326
+ : '—',
2327
+ ),
2328
+ h('span', { style: S.memLiveDataLabel }, t('footprintOnDisk')),
2329
+ h('span', { style: S.memLiveDataValue }, formatBytesSync(totalBytes)),
2330
+ ),
2331
+ )
2332
+ })()
2333
+
2334
+ memLiveDataEl = h('div', { key: 'live-data' },
2335
+ h('div', { style: S.monitorModalDivider }),
2336
+ h('span', { style: S.monitorModalSectionTitle }, t('memLiveData')),
2337
+ hostSectionEl,
2338
+ trendSectionEl,
2339
+ footprintSectionEl,
2340
+ )
2341
+
2342
+ return h('div', {
2343
+ style: S.monitorModalMask,
2344
+ onMouseDown: function (e) { if (e.target === e.currentTarget) onClose() },
2345
+ },
2346
+ h('div', { style: S.monitorModal },
2347
+ h('div', { style: S.monitorModalHead },
2348
+ h('span', { style: S.monitorModalTitle },
2349
+ t(MONITOR_TITLE_KEY[monitor] || 'alertMemCluster') + ' — ' + t('monitorConfig')),
2350
+ h('button', {
2351
+ type: 'button',
2352
+ 'data-dock-flash-focus': '',
2353
+ style: S.monitorModalClose,
2354
+ onClick: onClose,
2355
+ }, '✕'),
2356
+ ),
2357
+ h('div', { style: S.monitorModalBody },
2358
+ fieldEls,
2359
+ alertPreviewEl,
2360
+ memLiveDataEl,
2361
+ ),
2362
+ ),
2363
+ )
2364
+ }
2365
+
2366
+ //#endregion ───────────────────────────────────────────────────────────────
2367
+
2368
+ // ═══════════════════════════════════════════════════════════════════════
2369
+ //#region Monitor toggle helpers ──────────────────────────────────────────
2370
+
2371
+ /** Check whether dock-flash's system-alerts master toggle is ON. */
2372
+ function _getAlertsOn() {
2373
+ try { return localStorage.getItem('dock-flash:system-alerts') !== '0' } catch (_) { return true }
2374
+ }
2375
+
2376
+ var TOGGLE_KEY = 'dsh-flash-mem-mon:monitor-memory'
2377
+ var PROVIDER_ID = 'dsh-flash-mem-mon:host-memory-alert'
2378
+
2379
+ // The two registries this plugin registers INTO, kept so an imperative
2380
+ // surface can be nudged after the fact. Only the locale listener uses them.
2381
+ var _qcRegistry = null
2382
+ var _alertRegistryRef = null
2383
+
2384
+ /**
2385
+ * Re-render the surfaces this plugin draws IMPERATIVELY after the locale
2386
+ * changed.
2387
+ *
2388
+ * `t()` is a live read, so anything rendered inside a React render is
2389
+ * already correct on the next paint — but the Quick Control row is drawn by
2390
+ * dsh-flash's registry, which re-reads `label` / `subtitle` only when it is
2391
+ * told to. `notifyChange(id)` is that nudge, and `alerts.notify()` lets the
2392
+ * alert panel re-read the `title` / `message` FUNCTIONS this plugin emits
2393
+ * (they are functions precisely so they re-read the dictionary). Without
2394
+ * this, a language switch would sit half-applied — modal and rows in the
2395
+ * old language until the next poll — which is exactly the "language is
2396
+ * sticky per component" bug the official framework removes.
2397
+ */
2398
+ function refreshLocaleSurfaces() {
2399
+ if (_qcRegistry && typeof _qcRegistry.notifyChange === 'function') {
2400
+ try { _qcRegistry.notifyChange(TOGGLE_KEY) } catch (_) {}
2401
+ }
2402
+ if (_alertRegistryRef && typeof _alertRegistryRef.notify === 'function') {
2403
+ try { _alertRegistryRef.notify() } catch (_) {}
2404
+ }
2405
+ // The config modal re-renders itself — it subscribes to t.onLocaleChange()
2406
+ // where it is mounted, because that is where its re-render lever lives.
2407
+ }
2408
+
2409
+ var _MONITOR_TOGGLES = {
2410
+ memory: {
2411
+ key: TOGGLE_KEY,
2412
+ providers: [PROVIDER_ID],
2413
+ },
2414
+ }
2415
+ var _MONITOR_DEFAULT = { memory: true }
2416
+
2417
+ function _getMonitorOn(monitor) {
2418
+ var def = _MONITOR_DEFAULT[monitor] !== false
2419
+ try { return localStorage.getItem(_MONITOR_TOGGLES[monitor].key) !== '0' } catch (_) { return def }
2420
+ }
2421
+
2422
+ function _setMonitorOn(monitor, on, alertRegistry) {
2423
+ var providers = _MONITOR_TOGGLES[monitor].providers
2424
+ for (var i = 0; i < providers.length; i++) {
2425
+ try { alertRegistry.setProviderEnabled(providers[i], on) } catch (_) {}
2426
+ }
2427
+ try { localStorage.setItem(_MONITOR_TOGGLES[monitor].key, on ? '1' : '0') } catch (_) {}
2428
+ }
2429
+
2430
+ function _monitorSubtitle(m) {
2431
+ return function () {
2432
+ return _getMonitorOn(m) ? t('monitorOn') : t('monitorOff')
2433
+ }
2434
+ }
2435
+
2436
+ //#endregion ───────────────────────────────────────────────────────────────
2437
+
2438
+ // ═══════════════════════════════════════════════════════════════════════
2439
+ //#region Client factory ─────────────────────────────────────────────────
2440
+
2441
+ var client = {
2442
+ inject: ['remote', 'remote.settings'],
2443
+ async apply(ctx) {
2444
+ // A throw anywhere inside the real apply used to REJECT this async
2445
+ // apply — and a failed apply FAILS the plugin's Cordis fiber, so the
2446
+ // plugin never activates and its switch silently never appears. That
2447
+ // is not hypothetical: a `settings.describe().then(...)` TypeError did
2448
+ // exactly this. Registration failures are REPORTED here, never fatal.
2449
+ try {
2450
+ await client._applyInner(ctx)
2451
+ } catch (err) {
2452
+ console.error('[dsh-flash-mem-mon] apply failed (non-fatal):', err)
2453
+ }
2454
+ },
2455
+
2456
+ async _applyInner(ctx) {
2457
+ _prefCtx = ctx
2458
+
2459
+ // 0. i18n: hand our dictionaries to DSH's official locale framework.
2460
+ // The `locale` service (@deepseek-ai/dsh-client-locale) is the single
2461
+ // authority for the active UI language. Resolved lazily — it may not
2462
+ // be provided yet at apply() time — and adopted for good by the
2463
+ // inject watcher below. The claim lives on THIS fiber so a dispose or
2464
+ // a disable→enable cycle gives the namespace back instead of leaving
2465
+ // a stale registration that `register` would then refuse.
2466
+ try {
2467
+ ctx.effect(function () {
2468
+ var svc
2469
+ try { svc = ctx.get('locale') } catch (_) { svc = undefined }
2470
+ bindLocaleService(svc)
2471
+ return releaseLocaleBinding
2472
+ }, 'dsh-flash-mem-mon: i18n locale dictionaries')
2473
+ } catch (e) {
2474
+ console.warn('[dsh-flash-mem-mon] could not claim the locale namespace; i18n stays on the browser-language fallback:', e)
2475
+ }
2476
+
2477
+ // …and adopt it if it turns up LATER. `dsh.client.inject` orders the
2478
+ // MODULES, not the service registrations, so a provided-late locale
2479
+ // plugin is a real case. Without this watcher the plugin would sit on
2480
+ // the browser-language fallback for the whole session and follow the
2481
+ // global language only by accident. A fiber waiting for a service
2482
+ // that never arrives is harmless — the fallback already renders every
2483
+ // string — and bindLocaleService is idempotent, so repeating the
2484
+ // eager claim above costs nothing.
2485
+ try {
2486
+ ctx.inject(['locale'], function (scope) {
2487
+ bindLocaleService(scope && scope.locale)
2488
+ })
2489
+ } catch (e) {
2490
+ console.warn('[dsh-flash-mem-mon] ctx.inject(["locale"]) failed; i18n stays on the browser-language fallback:', e)
2491
+ }
2492
+
2493
+ // A language switch is only half done by `t()` becoming a live read.
2494
+ // The Quick Control row and the alert panel are drawn by dsh-flash, so
2495
+ // they need the imperative nudge below.
2496
+ //
2497
+ // The trigger is the `locale/change` EVENT, not `subscribe()`. The
2498
+ // official runtime publishes on two channels and they are not the
2499
+ // same: `subscribe()` (LocaleFace) fires on EVERY revision bump —
2500
+ // including a dictionary registration, which happens once per plugin
2501
+ // at boot — while `locale/change` is emitted only when the ACTIVE
2502
+ // locale actually switches (client-locale's `publish(active, changed)`
2503
+ // passes `false` for registrations precisely so "registration-heavy
2504
+ // boot cannot storm event listeners"). Nudging the panel on every
2505
+ // registration would re-render it under a language that has not
2506
+ // changed. One listener, fiber-scoped, so a disable→enable cycle
2507
+ // cannot stack a second one on the module-level listener set.
2508
+ try {
2509
+ ctx.on('locale/change', notifyLocaleChange)
2510
+ } catch (e) {
2511
+ console.warn('[dsh-flash-mem-mon] could not listen for locale/change; i18n will not refresh the imperative surfaces:', e)
2512
+ }
2513
+
2514
+ // 1. Load mem-mon preferences from the 'dsh-flash-mem-mon' settings namespace
2515
+ loadMemPrefs(ctx).then(function (ok) {
2516
+ // Sync alert providers with the localStorage-backed state.
2517
+ var lsOn
2518
+ try { lsOn = localStorage.getItem(TOGGLE_KEY) !== '0' } catch (_) { lsOn = true }
2519
+ try {
2520
+ var reg = ctx.get && ctx.get('quickControl')
2521
+ if (reg && typeof reg.notifyChange === 'function') {
2522
+ reg.notifyChange(TOGGLE_KEY)
2523
+ }
2524
+ } catch (_) {}
2525
+ var alertReg = ctx.get && ctx.get('dockFlashAlerts')
2526
+ if (alertReg) {
2527
+ try { alertReg.setProviderEnabled(PROVIDER_ID, lsOn) } catch (_) {}
2528
+ }
2529
+ })
2530
+
2531
+ // 2. Register with dock-flash's quickControl and dockFlashAlerts services.
2532
+ var _registered = false
2533
+
2534
+ function _registerServices(registry, alertRegistry) {
2535
+ if (_registered) return
2536
+ if (!registry || !alertRegistry) {
2537
+ console.warn('[dsh-flash-mem-mon] _registerServices: missing service — quickControl=' + !!registry + ', alerts=' + !!alertRegistry)
2538
+ return
2539
+ }
2540
+ _registered = true
2541
+ _qcRegistry = registry
2542
+ _alertRegistryRef = alertRegistry
2543
+ console.log('[dsh-flash-mem-mon] registering provider and switch')
2544
+
2545
+ // 3. Register memory alert provider
2546
+ var memProvider = createHostMemoryAlertProvider()
2547
+ alertRegistry.registerProvider(memProvider)
2548
+
2549
+ // 4. Register memory monitor toggle switch
2550
+ ctx.effect(function () {
2551
+ var dispose = registry.registerSwitch({
2552
+ id: TOGGLE_KEY,
2553
+ label: L('alertMemCluster'),
2554
+ icon: 'chip',
2555
+ type: 'toggle',
2556
+ group: 'system',
2557
+ cluster: 'system-alerts',
2558
+ order: 58,
2559
+ visible: function () {
2560
+ return _getAlertsOn()
2561
+ },
2562
+ config: function () { openMonitorConfig('memory') },
2563
+ getValue: function () { return _getMonitorOn('memory') },
2564
+ setValue: function (v) {
2565
+ _setMonitorOn('memory', v, alertRegistry)
2566
+ registry.notifyChange(TOGGLE_KEY)
2567
+ try { alertRegistry.notify() } catch (_) {}
2568
+ },
2569
+ })
2570
+ return dispose
2571
+ }, 'dsh-flash-mem-mon: monitor-memory switch')
2572
+
2573
+ // 5. Apply persisted toggle gates — before the alert registry auto-starts
2574
+ Object.keys(_MONITOR_TOGGLES).forEach(function (m) {
2575
+ if (!_getMonitorOn(m)) _setMonitorOn(m, false, alertRegistry)
2576
+ })
2577
+ }
2578
+
2579
+ // Passive: listen for dock-flash:ready event
2580
+ try {
2581
+ ctx.on('dock-flash:ready', function (payload) {
2582
+ console.log('[dsh-flash-mem-mon] dock-flash:ready event received — quickControl=' + !!(payload && payload.quickControl) + ', alerts=' + !!(payload && payload.alerts))
2583
+ _registerServices(payload.quickControl, payload.alerts)
2584
+ })
2585
+ } catch (e) {
2586
+ console.warn('[dsh-flash-mem-mon] ctx.on("dock-flash:ready") failed:', e)
2587
+ }
2588
+
2589
+ // Active: check if dock-flash already loaded
2590
+ var registry = ctx.get('quickControl')
2591
+ var alertRegistry = ctx.get('dockFlashAlerts')
2592
+ console.log('[dsh-flash-mem-mon] active check — quickControl=' + !!registry + ', alerts=' + !!alertRegistry)
2593
+ if (registry && alertRegistry) _registerServices(registry, alertRegistry)
2594
+
2595
+ // Fallback: poll briefly
2596
+ if (!_registered) {
2597
+ var _retryCount = 0
2598
+ function _retryGet() {
2599
+ if (_registered) return
2600
+ var r = ctx.get('quickControl')
2601
+ var a = ctx.get('dockFlashAlerts')
2602
+ if (r && a) { _registerServices(r, a); return }
2603
+ if (++_retryCount < 15) setTimeout(_retryGet, 200)
2604
+ }
2605
+ setTimeout(_retryGet, 0)
2606
+ }
2607
+ },
2608
+
2609
+ dispose() {
2610
+ closeMonitorConfig()
2611
+ },
2612
+ }
2613
+ return client
2614
+ //#endregion ───────────────────────────────────────────────────────────────
2615
+ },
2616
+ })