@nordbyte/nordrelay-auto-updater 0.1.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.
@@ -0,0 +1,446 @@
1
+ import { attr, badge, escapeHtml, formatAge, formatDuration, metric, statusForCount, table, truncate } from "./format.js";
2
+
3
+ const HISTORY_RANGE = "24h";
4
+
5
+ export function renderDashboardPanel(input = {}, context = {}, settings = {}) {
6
+ const aggregate = input.aggregate && typeof input.aggregate === "object" ? input.aggregate : {};
7
+ const results = Array.isArray(aggregate.results) ? aggregate.results : [];
8
+ const nodes = normalizeNodes(results, context);
9
+ const selectedNodeKey = selectedNode(nodes, input.selectedNodeKey);
10
+ const selectedTab = normalizeTab(input.selectedTab);
11
+ const selected = nodes.find((item) => item.key === selectedNodeKey) || nodes[0];
12
+ return `<div class="stack" data-auto-updater-panel data-selected-node-key="${attr(selectedNodeKey)}" data-selected-tab="${attr(selectedTab)}">
13
+ <div class="section-header">
14
+ <div>
15
+ <h1>Auto Updater <small>- ${escapeHtml(nodes.length)} node${nodes.length === 1 ? "" : "s"}</small></h1>
16
+ <small>OS package-manager and global npm version checks with explicit update actions.</small>
17
+ </div>
18
+ <div class="row">
19
+ <button type="button" class="secondary mini-button" data-refresh-updates>Refresh</button>
20
+ </div>
21
+ </div>
22
+ <div class="panel compact-panel">
23
+ <div class="row">
24
+ <label class="mini-control"><span>Filter packages</span><input type="search" data-package-filter placeholder="package, manager, status"></label>
25
+ <label class="checkbox"><input type="checkbox" data-outdated-only> Outdated only</label>
26
+ </div>
27
+ </div>
28
+ ${renderComparison(nodes, selectedNodeKey)}
29
+ ${selected ? renderNodeDetail(selected, selectedTab) : '<div class="empty-state">No update data available.</div>'}
30
+ </div>`;
31
+ }
32
+
33
+ function normalizeNodes(results, context) {
34
+ const fallback = [{ node: context.runtime || { name: "Local node", platform: "local" }, ok: false, error: "No aggregate data available." }];
35
+ return (results.length ? results : fallback).map((item, index) => {
36
+ const output = item.result?.output && typeof item.result.output === "object" ? item.result.output : {};
37
+ const panelData = output.panelData && typeof output.panelData === "object"
38
+ ? output.panelData
39
+ : output.snapshot
40
+ ? { snapshot: output.snapshot }
41
+ : null;
42
+ const snapshot = panelData?.snapshot || null;
43
+ const node = item.node || snapshot?.node || {};
44
+ const key = String(node.id || node.name || `node-${index + 1}`);
45
+ return {
46
+ key,
47
+ ok: item.ok !== false && Boolean(snapshot),
48
+ error: item.error || item.result?.stderr || "",
49
+ node: {
50
+ id: key,
51
+ name: String(node.name || node.id || `Node ${index + 1}`),
52
+ platform: String(node.platform || ""),
53
+ },
54
+ snapshot,
55
+ history: panelData?.history || [],
56
+ automation: panelData?.automation || { rules: [] },
57
+ storage: panelData?.storage || null,
58
+ settings: panelData?.settings || {},
59
+ };
60
+ }).sort((left, right) => updateRisk(right) - updateRisk(left) || left.node.name.localeCompare(right.node.name));
61
+ }
62
+
63
+ function selectedNode(nodes, requested) {
64
+ const text = String(requested || "");
65
+ return nodes.some((item) => item.key === text) ? text : nodes[0]?.key || "";
66
+ }
67
+
68
+ function normalizeTab(value) {
69
+ const tab = String(value || "").toLowerCase();
70
+ return ["overview", "os", "npm", "history", "diagnostics"].includes(tab) ? tab : "overview";
71
+ }
72
+
73
+ function renderComparison(nodes, selectedNodeKey) {
74
+ const rows = nodes.map((item) => {
75
+ const snapshot = item.snapshot;
76
+ const selected = item.key === selectedNodeKey ? " selected" : "";
77
+ if (!item.ok || !snapshot) {
78
+ return `<tr class="${selected}" data-auto-updater-node-row data-node-key="${attr(item.key)}"><td><button type="button" class="monitor-node-select" data-auto-updater-node="${attr(item.key)}">${escapeHtml(item.node.name)}</button></td><td colspan="6">${badge("unavailable", "failed")} ${escapeHtml(item.error || "Plugin not installed or no data available.")}</td></tr>`;
79
+ }
80
+ return `<tr class="${selected}" data-auto-updater-node-row data-node-key="${attr(item.key)}">
81
+ <td><button type="button" class="monitor-node-select" data-auto-updater-node="${attr(item.key)}">${escapeHtml(item.node.name)}</button></td>
82
+ <td>${escapeHtml(item.node.platform || "-")}</td>
83
+ <td>${badge(snapshot.os.updateCount, statusForCount(snapshot.os.updateCount))}</td>
84
+ <td>${badge(snapshot.npm.outdatedCount, statusForCount(snapshot.npm.outdatedCount))}</td>
85
+ <td>${escapeHtml(snapshot.npm.packageCount)}</td>
86
+ <td title="${attr(snapshot.checkedAt)}">${escapeHtml(formatAge(snapshot.checkedAt))}</td>
87
+ <td>${snapshot.errors.length ? badge(`${snapshot.errors.length} warnings`, "warning") : badge("ok", "enabled")}</td>
88
+ </tr>`;
89
+ });
90
+ return `<section class="panel monitor-comparison-panel auto-updater-comparison-panel">
91
+ <div class="section-header"><h2>Node comparison</h2><small>Nodes without this plugin enabled are skipped or shown unavailable.</small></div>
92
+ ${table(["Node", "Platform", "OS updates", "npm outdated", "npm packages", "Checked", "Status"], rows)}
93
+ </section>`;
94
+ }
95
+
96
+ function renderNodeDetail(item, selectedTab) {
97
+ const tabButtons = [
98
+ ["overview", "Overview"],
99
+ ["os", "OS Updates"],
100
+ ["npm", "npm Packages"],
101
+ ["history", "History"],
102
+ ["diagnostics", "Diagnostics"],
103
+ ].map(([id, label]) => `<button type="button" class="${id === selectedTab ? "active" : ""}" data-auto-updater-tab="${attr(id)}">${escapeHtml(label)}</button>`).join("");
104
+ return `<section class="panel monitor-node-panel auto-updater-node-panel">
105
+ <div class="section-header"><h2>${escapeHtml(item.node.name)} <small>${escapeHtml(item.node.platform || "")}</small></h2></div>
106
+ <div class="section-tabs" role="tablist">${tabButtons}</div>
107
+ <div class="callout" data-action-result hidden></div>
108
+ <div class="stack plugin-tab-content">
109
+ ${selectedTab === "overview" ? renderOverview(item) : ""}
110
+ ${selectedTab === "os" ? renderOsUpdates(item) : ""}
111
+ ${selectedTab === "npm" ? renderNpmPackages(item) : ""}
112
+ ${selectedTab === "history" ? renderHistory(item) : ""}
113
+ ${selectedTab === "diagnostics" ? renderDiagnostics(item) : ""}
114
+ </div>
115
+ </section>`;
116
+ }
117
+
118
+ function renderOverview(item) {
119
+ const snapshot = item.snapshot;
120
+ if (!snapshot) return `<div class="empty-state">${escapeHtml(item.error || "No snapshot available.")}</div>`;
121
+ const managers = [...new Set((snapshot.os.updates || []).map((update) => update.manager).filter(Boolean))].join(", ") || "none";
122
+ return `<div class="metrics-grid">
123
+ ${metric("OS updates", snapshot.os.updateCount, managers, statusForCount(snapshot.os.updateCount))}
124
+ ${metric("npm outdated", snapshot.npm.outdatedCount, `${snapshot.npm.packageCount} packages`, statusForCount(snapshot.npm.outdatedCount))}
125
+ ${metric("Last checked", formatAge(snapshot.checkedAt), snapshot.checkedAt)}
126
+ ${metric("Duration", formatDuration(snapshot.durationMs), `${snapshot.errors.length} warning(s)`, snapshot.errors.length ? "warn" : "ok")}
127
+ </div>
128
+ ${snapshot.errors.length ? `<div class="callout warn">${escapeHtml(snapshot.errors.map((error) => `${error.area || ""} ${error.manager || ""}: ${error.error || ""}`).join(" | "))}</div>` : ""}`;
129
+ }
130
+
131
+ function renderOsUpdates(item) {
132
+ const updates = item.snapshot?.os?.updates || [];
133
+ const rows = updates.map((update) => `<tr data-package-row data-package-text="${attr([update.manager, update.name, update.current, update.latest, update.source].join(" ").toLowerCase())}" data-outdated="true">
134
+ <td><input type="checkbox" data-os-update data-manager="${attr(update.manager || "")}" data-package="${attr(update.name || "")}" aria-label="Select ${attr(update.name || "OS update")}"></td>
135
+ <td>${escapeHtml(update.manager || "-")}</td>
136
+ <td><span class="truncate-cell" title="${attr(update.name)}">${escapeHtml(truncate(update.name, 80))}</span></td>
137
+ <td>${escapeHtml(update.current || "-")}</td>
138
+ <td>${escapeHtml(update.latest || "-")}</td>
139
+ <td><span class="truncate-cell" title="${attr(update.source)}">${escapeHtml(truncate(update.source, 120))}</span></td>
140
+ </tr>`);
141
+ return `<div class="row table-toolbar">
142
+ <button type="button" class="secondary mini-button" data-select-all-os>Select all</button>
143
+ <button type="button" class="secondary mini-button" data-update-os-selected>Update selected</button>
144
+ <button type="button" class="secondary mini-button" data-update-os-all>Update all</button>
145
+ </div>${table(["", "Manager", "Package", "Current", "Latest", "Source"], rows, "No OS updates found.")}`;
146
+ }
147
+
148
+ function renderNpmPackages(item) {
149
+ const packages = item.snapshot?.npm?.packages || [];
150
+ const rules = npmRuleMap(item);
151
+ const rows = packages.map((pkg) => {
152
+ const rule = rules.get(String(pkg.name || "")) || {};
153
+ return `<tr data-package-row data-package-text="${attr([pkg.name, pkg.current, pkg.wanted, pkg.latest, pkg.status, pkg.location].join(" ").toLowerCase())}" data-outdated="${pkg.status === "outdated" ? "true" : "false"}">
154
+ <td><input type="checkbox" data-npm-package data-package="${attr(pkg.name || "")}" data-outdated="${pkg.status === "outdated" ? "true" : "false"}" aria-label="Select ${attr(pkg.name || "npm package")}"></td>
155
+ <td><span class="truncate-cell" title="${attr(pkg.name)}">${escapeHtml(truncate(pkg.name, 90))}</span></td>
156
+ <td>${escapeHtml(pkg.current || "-")}</td>
157
+ <td>${escapeHtml(pkg.wanted || "-")}</td>
158
+ <td>${escapeHtml(pkg.latest || "-")}</td>
159
+ <td>${npmStatusBadge(pkg.status)}</td>
160
+ <td><span class="truncate-cell" title="${attr(pkg.location)}">${escapeHtml(truncate(pkg.location, 120))}</span></td>
161
+ <td><div class="data-table-actions"><button type="button" class="secondary mini-button" data-open-npm-auto="${attr(pkg.name || "")}" data-auto-enabled="${rule.enabled ? "true" : "false"}" data-auto-interval-value="${attr(Number(rule.intervalValue) || 24)}" data-auto-interval-unit="${attr(String(rule.intervalUnit || "hours"))}" data-auto-install-latest="${rule.installLatest ? "true" : "false"}" data-auto-last-status="${attr(autoRuleStatus(rule))}" data-auto-last-message="${attr(rule.lastMessage || "")}">Auto</button><button type="button" class="secondary mini-button" data-update-npm-package="${attr(pkg.name || "")}">Update</button><button type="button" class="danger mini-button" data-uninstall-npm="${attr(pkg.name || "")}">Delete</button></div></td>
162
+ </tr>`;
163
+ });
164
+ return `<div class="row table-toolbar">
165
+ <button type="button" class="secondary mini-button" data-select-all-npm>Select all</button>
166
+ <button type="button" class="secondary mini-button" data-update-npm-selected>Update selected</button>
167
+ <button type="button" class="secondary mini-button" data-update-npm-all>Update all outdated</button>
168
+ </div>${table(["", "Package", "Installed", "Wanted", "Latest", "Status", "Location", "Actions"], rows, "No global npm packages found.")}${renderNpmAutoModal()}`;
169
+ }
170
+
171
+ function npmStatusBadge(status) {
172
+ const text = String(status || "unknown");
173
+ if (text === "outdated") return badge("outdated", "failed");
174
+ if (text === "latest") return badge("latest", "enabled");
175
+ return badge(text, "disabled");
176
+ }
177
+
178
+ function npmRuleMap(item) {
179
+ const map = new Map();
180
+ for (const rule of item.automation?.rules || []) {
181
+ map.set(String(rule.name || ""), rule);
182
+ }
183
+ return map;
184
+ }
185
+
186
+ function autoRuleStatus(rule = {}) {
187
+ return rule.lastStatus ? `${rule.lastStatus}${rule.lastCheckedAt ? ` · ${formatAge(rule.lastCheckedAt)}` : ""}` : "not configured";
188
+ }
189
+
190
+ function renderNpmAutoModal() {
191
+ return `<div class="auto-updater-modal" data-npm-auto-modal hidden>
192
+ <div class="auto-updater-modal-backdrop" data-close-npm-auto></div>
193
+ <div class="panel auto-updater-modal-panel" role="dialog" aria-modal="true" aria-labelledby="autoUpdaterNpmAutoTitle">
194
+ <div class="section-header">
195
+ <div>
196
+ <h3 id="autoUpdaterNpmAutoTitle">Auto install</h3>
197
+ <small data-npm-auto-modal-package></small>
198
+ </div>
199
+ <button type="button" class="secondary mini-button" data-close-npm-auto>Close</button>
200
+ </div>
201
+ <div class="stack auto-updater-modal-body">
202
+ <label class="checkbox"><input type="checkbox" data-npm-auto-modal-enabled> <span>Enable automatic global install checks for this package</span></label>
203
+ <label class="mini-control"><span>Check interval</span><span class="npm-auto-interval"><input type="number" min="1" max="365" value="24" data-npm-auto-modal-interval aria-label="Auto install interval"><select data-npm-auto-modal-unit aria-label="Auto install interval unit"><option value="hours">hours</option><option value="days">days</option></select></span></label>
204
+ <label class="checkbox"><input type="checkbox" data-npm-auto-modal-latest> <span>Install even when already latest</span></label>
205
+ <div class="callout" data-npm-auto-modal-status></div>
206
+ <div class="row">
207
+ <button type="button" data-save-npm-auto-modal>Save</button>
208
+ <button type="button" class="secondary" data-close-npm-auto>Cancel</button>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>`;
213
+ }
214
+
215
+ function renderHistory(item) {
216
+ const rows = (item.history || []).map((row) => `<tr>
217
+ <td title="${attr(row.checkedAt)}">${escapeHtml(formatAge(row.checkedAt))}</td>
218
+ <td>${escapeHtml(row.osUpdateCount)}</td>
219
+ <td>${escapeHtml(row.npmOutdatedCount)}</td>
220
+ <td>${escapeHtml(row.npmPackageCount)}</td>
221
+ <td>${escapeHtml(formatDuration(row.durationMs))}</td>
222
+ <td>${row.errors?.length ? badge(row.errors.length, "warning") : badge("ok", "enabled")}</td>
223
+ </tr>`);
224
+ return `<div class="section-header auto-updater-history-header"><h3>History <small>${escapeHtml(HISTORY_RANGE)}</small></h3></div>${table(["Checked", "OS updates", "npm outdated", "npm packages", "Duration", "Status"], rows, "No history yet.")}`;
225
+ }
226
+
227
+ function renderDiagnostics(item) {
228
+ const snapshot = item.snapshot;
229
+ const rows = [
230
+ ["Storage snapshots", item.storage?.snapshots ?? "-"],
231
+ ["Storage integrity", item.storage?.integrity ?? "-"],
232
+ ["Storage size", item.storage?.sizeBytes ?? "-"],
233
+ ["OS supported", snapshot?.os?.supported ? "yes" : "no"],
234
+ ["npm supported", snapshot?.npm?.supported ? "yes" : "no"],
235
+ ["Errors", snapshot?.errors?.length ?? 0],
236
+ ].map(([key, value]) => `<tr><td>${escapeHtml(key)}</td><td>${escapeHtml(value)}</td></tr>`);
237
+ return table(["Metric", "Value"], rows);
238
+ }
239
+
240
+ function updateRisk(item) {
241
+ const snapshot = item.snapshot;
242
+ if (!snapshot) return -1;
243
+ return Number(snapshot.os?.updateCount || 0) + Number(snapshot.npm?.outdatedCount || 0);
244
+ }
245
+
246
+ export function dashboardPanelScript() {
247
+ return `
248
+ const root = api.root.querySelector('[data-auto-updater-panel]') || api.root;
249
+ function state(extra = {}) {
250
+ return {
251
+ selectedNodeKey: root.dataset.selectedNodeKey || '',
252
+ selectedTab: root.dataset.selectedTab || 'overview',
253
+ ...extra,
254
+ };
255
+ }
256
+ function reload(extra = {}) {
257
+ api.reload(state(extra));
258
+ }
259
+ function actionTargetPeerId() {
260
+ return root.dataset.selectedNodeKey || 'local';
261
+ }
262
+ function selectedOsUpdates(all = false) {
263
+ return Array.from(root.querySelectorAll('[data-os-update]'))
264
+ .filter((input) => all || input.checked)
265
+ .map((input) => ({ manager: input.dataset.manager || '', name: input.dataset.package || '' }))
266
+ .filter((item) => item.manager && item.name);
267
+ }
268
+ function selectedNpmPackages(all = false, outdatedOnly = false) {
269
+ return Array.from(root.querySelectorAll('[data-npm-package]'))
270
+ .filter((input) => (all || input.checked) && (!outdatedOnly || input.dataset.outdated === 'true'))
271
+ .map((input) => input.dataset.package || '')
272
+ .filter(Boolean);
273
+ }
274
+ function setActionBusy(busy) {
275
+ root.querySelectorAll('[data-update-os-selected],[data-update-os-all],[data-update-npm-selected],[data-update-npm-all],[data-update-npm-package],[data-uninstall-npm],[data-save-npm-auto-modal]').forEach((button) => {
276
+ button.disabled = busy;
277
+ });
278
+ }
279
+ function showActionResult(result, label) {
280
+ const target = root.querySelector('[data-action-result]');
281
+ if (!target) return;
282
+ const action = result && result.output && result.output.action ? result.output.action : result && result.action ? result.action : null;
283
+ const ok = action ? action.ok !== false : result && result.ok !== false;
284
+ const failed = action && Array.isArray(action.results) ? action.results.filter((item) => item && item.ok === false).length : 0;
285
+ const completedAt = action && action.completedAt ? action.completedAt : new Date().toISOString();
286
+ target.hidden = false;
287
+ target.classList.toggle('warn', !ok);
288
+ target.textContent = label + (ok ? ' completed' : ' failed') + (failed ? ' (' + failed + ' failed command' + (failed === 1 ? '' : 's') + ')' : '') + ' at ' + completedAt;
289
+ }
290
+ async function runPackageAction(command, input, label) {
291
+ if (!api.invokeCommand) {
292
+ api.toast('This NordRelay build cannot run plugin commands from panels.', { duration: 6000 });
293
+ return;
294
+ }
295
+ setActionBusy(true);
296
+ try {
297
+ const result = await api.invokeCommand(command, input, { peerId: actionTargetPeerId(), timeoutMs: 600000 });
298
+ showActionResult(result, label);
299
+ api.toast(label + ' started/completed', { duration: 5000 });
300
+ reload({ force: true });
301
+ } catch (error) {
302
+ const message = error && error.message ? error.message : String(error || 'Action failed');
303
+ const target = root.querySelector('[data-action-result]');
304
+ if (target) {
305
+ target.hidden = false;
306
+ target.classList.add('warn');
307
+ target.textContent = label + ' failed: ' + message;
308
+ }
309
+ api.toast(message, { duration: 8000 });
310
+ } finally {
311
+ setActionBusy(false);
312
+ }
313
+ }
314
+ function applyPackageFilter() {
315
+ const query = String(root.querySelector('[data-package-filter]')?.value || '').trim().toLowerCase();
316
+ const outdatedOnly = Boolean(root.querySelector('[data-outdated-only]')?.checked);
317
+ root.querySelectorAll('[data-package-row]').forEach((row) => {
318
+ const matchesQuery = !query || String(row.dataset.packageText || '').includes(query);
319
+ const matchesStatus = !outdatedOnly || row.dataset.outdated === 'true';
320
+ row.hidden = !(matchesQuery && matchesStatus);
321
+ });
322
+ }
323
+ root.querySelectorAll('[data-auto-updater-node]').forEach((button) => button.addEventListener('click', () => reload({ selectedNodeKey: button.dataset.autoUpdaterNode || '' })));
324
+ root.querySelectorAll('[data-auto-updater-tab]').forEach((button) => button.addEventListener('click', () => reload({ selectedTab: button.dataset.autoUpdaterTab || 'overview' })));
325
+ root.querySelector('[data-refresh-updates]')?.addEventListener('click', () => reload({ force: true }));
326
+ root.querySelector('[data-package-filter]')?.addEventListener('input', applyPackageFilter);
327
+ root.querySelector('[data-outdated-only]')?.addEventListener('change', applyPackageFilter);
328
+ root.querySelector('[data-select-all-os]')?.addEventListener('click', () => {
329
+ const choices = Array.from(root.querySelectorAll('[data-os-update]'));
330
+ const checked = choices.some((input) => !input.checked);
331
+ choices.forEach((input) => { input.checked = checked; });
332
+ });
333
+ root.querySelector('[data-select-all-npm]')?.addEventListener('click', () => {
334
+ const choices = Array.from(root.querySelectorAll('[data-npm-package]'));
335
+ const checked = choices.some((input) => !input.checked);
336
+ choices.forEach((input) => { input.checked = checked; });
337
+ });
338
+ root.querySelector('[data-update-os-selected]')?.addEventListener('click', () => {
339
+ const packages = selectedOsUpdates(false);
340
+ if (!packages.length) {
341
+ api.toast('Select at least one OS update.', { duration: 5000 });
342
+ return;
343
+ }
344
+ if (confirm('Update ' + packages.length + ' selected OS package' + (packages.length === 1 ? '' : 's') + '?')) {
345
+ runPackageAction('update-os', { packages }, 'OS update');
346
+ }
347
+ });
348
+ root.querySelector('[data-update-os-all]')?.addEventListener('click', () => {
349
+ const packages = selectedOsUpdates(true);
350
+ if (!packages.length) {
351
+ api.toast('No OS updates are available.', { duration: 5000 });
352
+ return;
353
+ }
354
+ if (confirm('Update all ' + packages.length + ' listed OS package' + (packages.length === 1 ? '' : 's') + '?')) {
355
+ runPackageAction('update-os', { packages }, 'OS update');
356
+ }
357
+ });
358
+ root.querySelector('[data-update-npm-selected]')?.addEventListener('click', () => {
359
+ const packages = selectedNpmPackages(false);
360
+ if (!packages.length) {
361
+ api.toast('Select at least one npm package.', { duration: 5000 });
362
+ return;
363
+ }
364
+ if (confirm('Update ' + packages.length + ' selected npm package' + (packages.length === 1 ? '' : 's') + '?')) {
365
+ runPackageAction('update-npm', { packages }, 'npm update');
366
+ }
367
+ });
368
+ root.querySelector('[data-update-npm-all]')?.addEventListener('click', () => {
369
+ const packages = selectedNpmPackages(true, true);
370
+ if (!packages.length) {
371
+ api.toast('No outdated npm packages are available.', { duration: 5000 });
372
+ return;
373
+ }
374
+ if (confirm('Update all ' + packages.length + ' outdated npm package' + (packages.length === 1 ? '' : 's') + '?')) {
375
+ runPackageAction('update-npm', { packages }, 'npm update');
376
+ }
377
+ });
378
+ root.querySelectorAll('[data-update-npm-package]').forEach((button) => button.addEventListener('click', () => {
379
+ const name = button.dataset.updateNpmPackage || '';
380
+ if (!name) return;
381
+ if (confirm('Update global npm package "' + name + '"?')) {
382
+ runPackageAction('update-npm', { packages: [name] }, 'npm update');
383
+ }
384
+ }));
385
+ function closeNpmAutoModal() {
386
+ const modal = root.querySelector('[data-npm-auto-modal]');
387
+ if (modal) modal.hidden = true;
388
+ }
389
+ root.querySelectorAll('[data-close-npm-auto]').forEach((button) => button.addEventListener('click', closeNpmAutoModal));
390
+ root.querySelectorAll('[data-open-npm-auto]').forEach((button) => button.addEventListener('click', () => {
391
+ const modal = root.querySelector('[data-npm-auto-modal]');
392
+ if (!modal) return;
393
+ const name = button.dataset.openNpmAuto || '';
394
+ modal.dataset.package = name;
395
+ modal.querySelector('[data-npm-auto-modal-package]').textContent = name;
396
+ modal.querySelector('[data-npm-auto-modal-enabled]').checked = button.dataset.autoEnabled === 'true';
397
+ modal.querySelector('[data-npm-auto-modal-interval]').value = button.dataset.autoIntervalValue || '24';
398
+ modal.querySelector('[data-npm-auto-modal-unit]').value = button.dataset.autoIntervalUnit || 'hours';
399
+ modal.querySelector('[data-npm-auto-modal-latest]').checked = button.dataset.autoInstallLatest === 'true';
400
+ const status = button.dataset.autoLastStatus || 'not configured';
401
+ const message = button.dataset.autoLastMessage || status;
402
+ const statusEl = modal.querySelector('[data-npm-auto-modal-status]');
403
+ statusEl.textContent = 'Status: ' + status;
404
+ statusEl.title = message;
405
+ modal.hidden = false;
406
+ }));
407
+ root.querySelector('[data-save-npm-auto-modal]')?.addEventListener('click', () => {
408
+ const modal = root.querySelector('[data-npm-auto-modal]');
409
+ const name = modal?.dataset.package || '';
410
+ if (!name) return;
411
+ const enabled = Boolean(modal.querySelector('[data-npm-auto-modal-enabled]')?.checked);
412
+ const intervalValue = Math.max(1, Number(modal.querySelector('[data-npm-auto-modal-interval]')?.value) || 24);
413
+ const intervalUnit = modal.querySelector('[data-npm-auto-modal-unit]')?.value || 'hours';
414
+ const installLatest = Boolean(modal.querySelector('[data-npm-auto-modal-latest]')?.checked);
415
+ runPackageAction('configure-npm-auto', { package: name, enabled, intervalValue, intervalUnit, installLatest }, 'npm auto install settings');
416
+ });
417
+ root.querySelectorAll('[data-uninstall-npm]').forEach((button) => button.addEventListener('click', () => {
418
+ const name = button.dataset.uninstallNpm || '';
419
+ if (!name) return;
420
+ if (confirm('Uninstall global npm package "' + name + '"?')) {
421
+ runPackageAction('uninstall-npm', { packages: [name] }, 'npm uninstall');
422
+ }
423
+ }));
424
+ `;
425
+ }
426
+
427
+ export function dashboardPanelStyles() {
428
+ return `
429
+ [data-auto-updater-panel] .section-tabs{margin-bottom:10px}
430
+ [data-auto-updater-panel] .plugin-tab-content{gap:10px}
431
+ [data-auto-updater-panel] .table-toolbar{margin-bottom:0}
432
+ [data-auto-updater-panel] .table-toolbar+.data-table-wrap{margin-top:2px}
433
+ [data-auto-updater-panel] .auto-updater-history-header{margin-bottom:0}
434
+ [data-auto-updater-panel] .auto-updater-history-header+.data-table-wrap{margin-top:2px}
435
+ [data-auto-updater-panel] .auto-updater-history-header h3{margin-bottom:0}
436
+ [data-auto-updater-panel] .data-table-actions{display:flex;align-items:center;gap:6px;flex-wrap:nowrap}
437
+ [data-auto-updater-panel] .auto-updater-modal{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;padding:24px}
438
+ [data-auto-updater-panel] .auto-updater-modal[hidden]{display:none}
439
+ [data-auto-updater-panel] .auto-updater-modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.42)}
440
+ [data-auto-updater-panel] .auto-updater-modal-panel{position:relative;z-index:1;width:min(560px,calc(100vw - 32px));max-height:calc(100vh - 48px);overflow:auto}
441
+ [data-auto-updater-panel] .auto-updater-modal-body{gap:12px}
442
+ [data-auto-updater-panel] .npm-auto-interval{display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
443
+ [data-auto-updater-panel] .npm-auto-interval input{width:58px}
444
+ [data-auto-updater-panel] .npm-auto-interval select{width:auto;min-width:84px}
445
+ `;
446
+ }