loopsy 1.0.10 → 1.0.11
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/dist/cli/index.js +1 -1
- package/dist/dashboard/public/style.css +3 -0
- package/dist/dashboard/public/views/peers.js +75 -2
- package/dist/dashboard/routes/peers-all.d.ts.map +1 -1
- package/dist/dashboard/routes/peers-all.js +55 -0
- package/dist/dashboard/routes/peers-all.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -90,6 +90,6 @@ yargs(hideBin(process.argv))
|
|
|
90
90
|
.command('doctor', 'Run health checks on your Loopsy installation', {}, doctorCommand)
|
|
91
91
|
.demandCommand(1, 'You need at least one command')
|
|
92
92
|
.help()
|
|
93
|
-
.version('1.0.
|
|
93
|
+
.version('1.0.11')
|
|
94
94
|
.parse();
|
|
95
95
|
//# sourceMappingURL=index.js.map
|
|
@@ -581,7 +581,10 @@ select option { background: var(--bg-base); color: var(--text-primary); }
|
|
|
581
581
|
transition: border-color 0.15s;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
.peer-card { cursor: pointer; }
|
|
584
585
|
.peer-card:hover { border-color: var(--border-bright); }
|
|
586
|
+
.peer-card.peer-selected { border-color: var(--red-dim); background: rgba(255,51,102,0.05); }
|
|
587
|
+
.peer-check { accent-color: var(--red); cursor: pointer; width: 14px; height: 14px; }
|
|
585
588
|
|
|
586
589
|
/* ═══ Message Rows ═══ */
|
|
587
590
|
.msg-row {
|
|
@@ -11,6 +11,8 @@ function platformSvg(platform) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
let refreshTimer = null;
|
|
14
|
+
let allPeers = []; // cached peer list for selection
|
|
15
|
+
let selectedPeers = new Set(); // nodeIds of selected peers
|
|
14
16
|
|
|
15
17
|
function mount(container) {
|
|
16
18
|
container.innerHTML = `
|
|
@@ -20,7 +22,10 @@ function mount(container) {
|
|
|
20
22
|
<div class="form-group">
|
|
21
23
|
<select class="input" id="peers-session"></select>
|
|
22
24
|
</div>
|
|
23
|
-
<
|
|
25
|
+
<div class="flex gap-sm items-center">
|
|
26
|
+
<button class="btn btn-primary btn-sm" id="btn-peers-refresh">Refresh</button>
|
|
27
|
+
<button class="btn btn-danger btn-sm" id="btn-delete-peers" style="display:none">Delete Selected</button>
|
|
28
|
+
</div>
|
|
24
29
|
</div>
|
|
25
30
|
|
|
26
31
|
<div class="peer-grid" id="peer-grid"></div>
|
|
@@ -39,12 +44,15 @@ function mount(container) {
|
|
|
39
44
|
document.getElementById('btn-peers-refresh').addEventListener('click', loadPeers);
|
|
40
45
|
document.getElementById('btn-add-peer').addEventListener('click', addPeer);
|
|
41
46
|
document.getElementById('peers-session').addEventListener('change', loadPeers);
|
|
47
|
+
document.getElementById('btn-delete-peers').addEventListener('click', deleteSelectedPeers);
|
|
42
48
|
|
|
43
49
|
refreshTimer = setInterval(loadPeers, 15000);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
function unmount() {
|
|
47
53
|
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
54
|
+
selectedPeers.clear();
|
|
55
|
+
allPeers = [];
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
async function loadSessions() {
|
|
@@ -76,19 +84,30 @@ async function loadPeers() {
|
|
|
76
84
|
peers = data.peers || [];
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
allPeers = peers;
|
|
88
|
+
|
|
89
|
+
// Clean up selected peers that no longer exist
|
|
90
|
+
const currentNodeIds = new Set(peers.map(p => p.nodeId));
|
|
91
|
+
for (const id of selectedPeers) {
|
|
92
|
+
if (!currentNodeIds.has(id)) selectedPeers.delete(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
if (peers.length === 0) {
|
|
80
96
|
grid.innerHTML = '<div class="empty">No peers discovered</div>';
|
|
97
|
+
updateDeleteButton();
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
grid.innerHTML = peers.map(p => {
|
|
85
102
|
const dotClass = p.status === 'online' ? 'online' : p.status === 'offline' ? 'offline' : 'unknown';
|
|
86
103
|
const platformIcon = platformSvg(p.platform);
|
|
104
|
+
const isSelected = selectedPeers.has(p.nodeId);
|
|
87
105
|
|
|
88
106
|
return `
|
|
89
|
-
<div class="peer-card">
|
|
107
|
+
<div class="peer-card ${isSelected ? 'peer-selected' : ''}" data-node-id="${escapeHtml(p.nodeId)}" onclick="window.__togglePeer('${escapeHtml(p.nodeId)}')">
|
|
90
108
|
<div class="flex items-center justify-between mb-1">
|
|
91
109
|
<div class="flex items-center gap-sm">
|
|
110
|
+
<input type="checkbox" class="peer-check" ${isSelected ? 'checked' : ''} onclick="event.stopPropagation(); window.__togglePeer('${escapeHtml(p.nodeId)}')">
|
|
92
111
|
<span class="status-dot ${dotClass}"></span>
|
|
93
112
|
<span class="font-mono text-sm" style="font-weight:600">${escapeHtml(p.hostname)}</span>
|
|
94
113
|
</div>
|
|
@@ -104,11 +123,65 @@ async function loadPeers() {
|
|
|
104
123
|
</div>
|
|
105
124
|
`;
|
|
106
125
|
}).join('');
|
|
126
|
+
|
|
127
|
+
updateDeleteButton();
|
|
107
128
|
} catch (err) {
|
|
108
129
|
grid.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`;
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
|
|
133
|
+
function updateDeleteButton() {
|
|
134
|
+
const btn = document.getElementById('btn-delete-peers');
|
|
135
|
+
if (!btn) return;
|
|
136
|
+
if (selectedPeers.size > 0) {
|
|
137
|
+
btn.style.display = '';
|
|
138
|
+
btn.textContent = `Delete Selected (${selectedPeers.size})`;
|
|
139
|
+
} else {
|
|
140
|
+
btn.style.display = 'none';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
window.__togglePeer = (nodeId) => {
|
|
145
|
+
if (selectedPeers.has(nodeId)) {
|
|
146
|
+
selectedPeers.delete(nodeId);
|
|
147
|
+
} else {
|
|
148
|
+
selectedPeers.add(nodeId);
|
|
149
|
+
}
|
|
150
|
+
// Update UI without full reload
|
|
151
|
+
const card = document.querySelector(`[data-node-id="${nodeId}"]`);
|
|
152
|
+
if (card) {
|
|
153
|
+
card.classList.toggle('peer-selected', selectedPeers.has(nodeId));
|
|
154
|
+
const check = card.querySelector('.peer-check');
|
|
155
|
+
if (check) check.checked = selectedPeers.has(nodeId);
|
|
156
|
+
}
|
|
157
|
+
updateDeleteButton();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
async function deleteSelectedPeers() {
|
|
161
|
+
if (selectedPeers.size === 0) return;
|
|
162
|
+
if (!confirm(`Delete ${selectedPeers.size} peer(s) from all sessions?`)) return;
|
|
163
|
+
|
|
164
|
+
const btn = document.getElementById('btn-delete-peers');
|
|
165
|
+
if (btn) btn.disabled = true;
|
|
166
|
+
|
|
167
|
+
const peersToDelete = allPeers
|
|
168
|
+
.filter(p => selectedPeers.has(p.nodeId))
|
|
169
|
+
.map(p => ({ nodeId: p.nodeId, address: p.address, port: p.port, _seenBySessions: p._seenBySessions }));
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await dashboardApi('/peers/delete', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify({ peers: peersToDelete }),
|
|
175
|
+
});
|
|
176
|
+
selectedPeers.clear();
|
|
177
|
+
await loadPeers();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
alert('Failed: ' + err.message);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (btn) btn.disabled = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
112
185
|
async function addPeer() {
|
|
113
186
|
const portVal = document.getElementById('peers-session').value;
|
|
114
187
|
if (portVal === 'all') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peers-all.d.ts","sourceRoot":"","sources":["../../src/routes/peers-all.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"peers-all.d.ts","sourceRoot":"","sources":["../../src/routes/peers-all.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAgB/C,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QA2D/G"}
|
|
@@ -1,8 +1,63 @@
|
|
|
1
1
|
import { fetchAndDeduplicatePeers } from './peer-utils.js';
|
|
2
|
+
import { listSessions } from '../session-manager.js';
|
|
3
|
+
function resolveApiKey(address, localApiKey, allowedKeys) {
|
|
4
|
+
if (address === '127.0.0.1' || address === 'localhost')
|
|
5
|
+
return localApiKey;
|
|
6
|
+
for (const key of Object.values(allowedKeys)) {
|
|
7
|
+
if (key !== localApiKey)
|
|
8
|
+
return key;
|
|
9
|
+
}
|
|
10
|
+
return localApiKey;
|
|
11
|
+
}
|
|
2
12
|
export function registerPeersAllRoute(app, apiKey, allowedKeys) {
|
|
3
13
|
app.get('/dashboard/api/peers/all', async () => {
|
|
4
14
|
const peers = await fetchAndDeduplicatePeers(apiKey, allowedKeys);
|
|
5
15
|
return { peers, timestamp: Date.now() };
|
|
6
16
|
});
|
|
17
|
+
// Delete peers by nodeId across all local sessions and optionally remote machines
|
|
18
|
+
app.post('/dashboard/api/peers/delete', async (request) => {
|
|
19
|
+
const { peers: peersToDelete } = request.body;
|
|
20
|
+
if (!peersToDelete?.length)
|
|
21
|
+
return { deleted: 0 };
|
|
22
|
+
const { main, sessions } = await listSessions();
|
|
23
|
+
const localPorts = [];
|
|
24
|
+
if (main && main.status === 'running')
|
|
25
|
+
localPorts.push(main.port);
|
|
26
|
+
for (const s of sessions.filter((s) => s.status === 'running')) {
|
|
27
|
+
localPorts.push(s.port);
|
|
28
|
+
}
|
|
29
|
+
let deleted = 0;
|
|
30
|
+
for (const peer of peersToDelete) {
|
|
31
|
+
// Delete from all local sessions
|
|
32
|
+
for (const port of localPorts) {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/v1/peers/${encodeURIComponent(peer.nodeId)}`, {
|
|
35
|
+
method: 'DELETE',
|
|
36
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
37
|
+
signal: AbortSignal.timeout(3000),
|
|
38
|
+
});
|
|
39
|
+
if (res.ok)
|
|
40
|
+
deleted++;
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
}
|
|
44
|
+
// If this peer is on a remote machine, also tell that machine to remove it
|
|
45
|
+
const isRemote = peer.address !== '127.0.0.1' && peer.address !== 'localhost';
|
|
46
|
+
if (isRemote && allowedKeys) {
|
|
47
|
+
const remoteKey = resolveApiKey(peer.address, apiKey, allowedKeys);
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`http://${peer.address}:${peer.port}/api/v1/peers/${encodeURIComponent(peer.nodeId)}`, {
|
|
50
|
+
method: 'DELETE',
|
|
51
|
+
headers: { Authorization: `Bearer ${remoteKey}` },
|
|
52
|
+
signal: AbortSignal.timeout(3000),
|
|
53
|
+
});
|
|
54
|
+
if (res.ok)
|
|
55
|
+
deleted++;
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { deleted, requested: peersToDelete.length };
|
|
61
|
+
});
|
|
7
62
|
}
|
|
8
63
|
//# sourceMappingURL=peers-all.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peers-all.js","sourceRoot":"","sources":["../../src/routes/peers-all.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"peers-all.js","sourceRoot":"","sources":["../../src/routes/peers-all.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED