@bitpub/cli 2.1.2 → 2.1.4
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/package.json +1 -1
- package/src/commands/browser.js +310 -0
- package/src/commands/group.js +40 -3
- package/src/commands/sync.js +18 -13
- package/static/console.html +340 -5
- package/static/welcome.html +705 -25
package/static/console.html
CHANGED
|
@@ -217,9 +217,27 @@ svg { flex-shrink: 0; }
|
|
|
217
217
|
#main:hover::-webkit-scrollbar-thumb { background: var(--border); background-clip: content-box; }
|
|
218
218
|
|
|
219
219
|
/* ── Sidebar / Tree ─────────────────────────────────── */
|
|
220
|
-
.sidebar-header { padding: 12px 14px 8px; font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-subtle); display: flex; align-items: center; justify-content: space-between; }
|
|
220
|
+
.sidebar-header { padding: 12px 14px 8px; font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-subtle); display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
221
221
|
.sidebar-header .clear-btn { font-size: 10.5px; color: var(--text-subtle); cursor: pointer; font-weight: 500; letter-spacing: 0; text-transform: none; }
|
|
222
222
|
.sidebar-header .clear-btn:hover { color: var(--accent); }
|
|
223
|
+
|
|
224
|
+
/* Sync button — icon-only, ghost style, lives in the sidebar header.
|
|
225
|
+
Spins while a /bridge/sync request is in flight; pulses green for a
|
|
226
|
+
moment after a successful sync so the user gets feedback even if no
|
|
227
|
+
new slices landed. */
|
|
228
|
+
.sync-btn { display: inline-flex; align-items: center; gap: 4px; padding: 3px 6px; border: 0; background: transparent; color: var(--text-subtle); cursor: pointer; border-radius: var(--radius-sm); font: inherit; font-size: 10.5px; font-weight: 500; text-transform: none; letter-spacing: 0; }
|
|
229
|
+
.sync-btn:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); }
|
|
230
|
+
.sync-btn:disabled { opacity: .6; cursor: default; }
|
|
231
|
+
.sync-btn svg { width: 12px; height: 12px; transition: transform .2s ease-out; }
|
|
232
|
+
.sync-btn.is-syncing svg { animation: bp-spin 1s linear infinite; }
|
|
233
|
+
.sync-btn.is-done { color: #10b981; }
|
|
234
|
+
@keyframes bp-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
235
|
+
.sync-status { padding: 4px 14px 8px; font-size: 11px; color: var(--text-subtle); line-height: 1.4; max-height: 120px; overflow-y: auto; }
|
|
236
|
+
.sync-status.hidden { display: none; }
|
|
237
|
+
.sync-status .row { display: flex; justify-content: space-between; gap: 8px; padding: 2px 0; }
|
|
238
|
+
.sync-status .row .label { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
239
|
+
.sync-status .row .count { color: var(--text-subtle); flex-shrink: 0; font-variant-numeric: tabular-nums; }
|
|
240
|
+
.sync-status .row.error .count { color: var(--scope-group); }
|
|
223
241
|
.tree-wrap { flex: 1; overflow-y: auto; padding: 0 6px 12px; }
|
|
224
242
|
.tree-wrap::-webkit-scrollbar { width: 8px; }
|
|
225
243
|
.tree-wrap::-webkit-scrollbar-thumb { background: transparent; border-radius: 4px; }
|
|
@@ -972,8 +990,15 @@ svg { flex-shrink: 0; }
|
|
|
972
990
|
<aside id="sidebar">
|
|
973
991
|
<div class="sidebar-header">
|
|
974
992
|
<span>Namespaces</span>
|
|
975
|
-
<span
|
|
993
|
+
<span style="display:inline-flex; align-items:center; gap:6px;">
|
|
994
|
+
<span id="clear-filter-btn" class="clear-btn hidden" onclick="clearFilter()">Clear</span>
|
|
995
|
+
<button id="sync-btn" class="sync-btn" type="button" onclick="runFullSync()" title="Sync all namespaces from the cloud into your local cache">
|
|
996
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 3a5 5 0 0 1 4.546 2.914.5.5 0 0 0 .908-.418A6 6 0 1 0 2 8a.5.5 0 1 0 1 0 5 5 0 0 1 5-5Z"/><path d="M8 13a5 5 0 0 1-4.546-2.914.5.5 0 0 0-.908.418A6 6 0 1 0 14 8a.5.5 0 1 0-1 0 5 5 0 0 1-5 5Z"/><path d="M11.5 1.5a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1 0-1H11V2a.5.5 0 0 1 .5-.5ZM4.5 10.5a.5.5 0 0 1 .5.5v2.5h2.5a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 .5-.5Z"/></svg>
|
|
997
|
+
<span class="sync-btn-label">Sync</span>
|
|
998
|
+
</button>
|
|
999
|
+
</span>
|
|
976
1000
|
</div>
|
|
1001
|
+
<div id="sync-status" class="sync-status hidden"></div>
|
|
977
1002
|
<div id="tree" class="tree-wrap"></div>
|
|
978
1003
|
</aside>
|
|
979
1004
|
<main id="main"></main>
|
|
@@ -992,6 +1017,7 @@ const S = {
|
|
|
992
1017
|
stats: {},
|
|
993
1018
|
mode: 'remote',
|
|
994
1019
|
domain: '',
|
|
1020
|
+
knownGroups: [],
|
|
995
1021
|
apiKey: sessionStorage.getItem('bitpub_key'),
|
|
996
1022
|
raw: false,
|
|
997
1023
|
searchQuery: '',
|
|
@@ -1180,7 +1206,8 @@ function ingestData(data) {
|
|
|
1180
1206
|
S.slices = data.slices.map(parseSlice);
|
|
1181
1207
|
S.mode = data.mode || 'remote';
|
|
1182
1208
|
S.domain = data.domain || '';
|
|
1183
|
-
S.
|
|
1209
|
+
S.knownGroups = Array.isArray(data.known_groups) ? data.known_groups : [];
|
|
1210
|
+
S.tree = buildTree(S.slices, S.knownGroups);
|
|
1184
1211
|
S.stats = computeStats(S.slices);
|
|
1185
1212
|
S.lastUpdated = maxTs(S.slices);
|
|
1186
1213
|
}
|
|
@@ -1229,7 +1256,8 @@ async function pollData() {
|
|
|
1229
1256
|
|
|
1230
1257
|
S.slices = nextSlices;
|
|
1231
1258
|
S.mode = data.mode || S.mode;
|
|
1232
|
-
S.
|
|
1259
|
+
S.knownGroups = Array.isArray(data.known_groups) ? data.known_groups : S.knownGroups;
|
|
1260
|
+
S.tree = buildTree(S.slices, S.knownGroups);
|
|
1233
1261
|
S.stats = computeStats(S.slices);
|
|
1234
1262
|
S.lastUpdated = maxTs(S.slices);
|
|
1235
1263
|
S.newArrivals = arrivals;
|
|
@@ -1320,7 +1348,7 @@ function pathOf(hcu) {
|
|
|
1320
1348
|
return (m && m[1]) ? m[1].replace(/^\//, '').split('/') : [];
|
|
1321
1349
|
}
|
|
1322
1350
|
|
|
1323
|
-
function buildTree(slices) {
|
|
1351
|
+
function buildTree(slices, knownGroups) {
|
|
1324
1352
|
const tree = {};
|
|
1325
1353
|
for (const s of slices) {
|
|
1326
1354
|
const { scope, type } = scopeOf(s.hcu);
|
|
@@ -1333,6 +1361,17 @@ function buildTree(slices) {
|
|
|
1333
1361
|
if (i === segs.length - 1) node.slice = s;
|
|
1334
1362
|
}
|
|
1335
1363
|
}
|
|
1364
|
+
// Seed empty top-level nodes for groups the user belongs to but
|
|
1365
|
+
// hasn't put any slices in yet. The tree is otherwise slice-driven,
|
|
1366
|
+
// so brand-new groups would be invisible in the sidebar until the
|
|
1367
|
+
// first save — confusing right after `bitpub group create`.
|
|
1368
|
+
if (Array.isArray(knownGroups)) {
|
|
1369
|
+
for (const g of knownGroups) {
|
|
1370
|
+
if (!g || !g.slug) continue;
|
|
1371
|
+
const scope = `group:${g.slug}`;
|
|
1372
|
+
if (!tree[scope]) tree[scope] = { type: 'group', children: {}, count: 0 };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1336
1375
|
return tree;
|
|
1337
1376
|
}
|
|
1338
1377
|
|
|
@@ -1456,6 +1495,136 @@ function clearFilter() {
|
|
|
1456
1495
|
renderAll();
|
|
1457
1496
|
}
|
|
1458
1497
|
|
|
1498
|
+
/* ── Sidebar Sync button ────────────────────────────────
|
|
1499
|
+
*
|
|
1500
|
+
* Streams /bridge/sync (SSE) and renders per-scope progress under the
|
|
1501
|
+
* header. On `done`, force a fresh /api/data poll so the tree picks up
|
|
1502
|
+
* any new arrivals immediately (without waiting for the 30s tick).
|
|
1503
|
+
*
|
|
1504
|
+
* The button is browser chrome — not an app function — so it talks
|
|
1505
|
+
* directly to /bridge/sync rather than going through window.bitpub.run
|
|
1506
|
+
* and the manifest dispatcher.
|
|
1507
|
+
*
|
|
1508
|
+
* Reentrancy: a second click while a sync is in flight is a no-op
|
|
1509
|
+
* (we disable the button), so we don't need to track an EventSource
|
|
1510
|
+
* handle for cancellation.
|
|
1511
|
+
*/
|
|
1512
|
+
async function runFullSync() {
|
|
1513
|
+
const btn = $('sync-btn');
|
|
1514
|
+
const status = $('sync-status');
|
|
1515
|
+
if (!btn || btn.disabled) return;
|
|
1516
|
+
|
|
1517
|
+
btn.disabled = true;
|
|
1518
|
+
btn.classList.add('is-syncing');
|
|
1519
|
+
btn.classList.remove('is-done');
|
|
1520
|
+
status.classList.remove('hidden');
|
|
1521
|
+
status.innerHTML = '';
|
|
1522
|
+
|
|
1523
|
+
// Track rows so 'syncing' → 'done' overwrites in place rather than
|
|
1524
|
+
// appending a second line per scope.
|
|
1525
|
+
const rows = new Map();
|
|
1526
|
+
|
|
1527
|
+
function setRow(label, pattern, statusText, klass) {
|
|
1528
|
+
let row = rows.get(label);
|
|
1529
|
+
if (!row) {
|
|
1530
|
+
row = document.createElement('div');
|
|
1531
|
+
row.className = 'row';
|
|
1532
|
+
const a = document.createElement('span'); a.className = 'label';
|
|
1533
|
+
const b = document.createElement('span'); b.className = 'count';
|
|
1534
|
+
row.append(a, b);
|
|
1535
|
+
status.appendChild(row);
|
|
1536
|
+
rows.set(label, row);
|
|
1537
|
+
}
|
|
1538
|
+
row.className = 'row' + (klass ? ' ' + klass : '');
|
|
1539
|
+
row.title = pattern;
|
|
1540
|
+
row.children[0].textContent = label;
|
|
1541
|
+
row.children[1].textContent = statusText;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const base = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/';
|
|
1545
|
+
const es = new EventSource(base + 'bridge/sync');
|
|
1546
|
+
|
|
1547
|
+
function cleanup() {
|
|
1548
|
+
es.close();
|
|
1549
|
+
btn.disabled = false;
|
|
1550
|
+
btn.classList.remove('is-syncing');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
es.addEventListener('started', (e) => {
|
|
1554
|
+
let payload = {};
|
|
1555
|
+
try { payload = JSON.parse(e.data); } catch {}
|
|
1556
|
+
for (const t of (payload.targets || [])) setRow(t.label, t.pattern, 'queued', '');
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
es.addEventListener('scope', (e) => {
|
|
1560
|
+
let payload = {};
|
|
1561
|
+
try { payload = JSON.parse(e.data); } catch {}
|
|
1562
|
+
if (payload.status === 'syncing') {
|
|
1563
|
+
setRow(payload.label, payload.pattern, 'syncing…', '');
|
|
1564
|
+
} else if (payload.status === 'done') {
|
|
1565
|
+
const cnt = (payload.count ?? 0) + ' slices';
|
|
1566
|
+
const suffix = payload.hitLimit ? ' (capped)' : '';
|
|
1567
|
+
setRow(payload.label, payload.pattern, cnt + suffix, '');
|
|
1568
|
+
} else if (payload.status === 'error') {
|
|
1569
|
+
setRow(payload.label, payload.pattern, 'error', 'error');
|
|
1570
|
+
console.error('[sync]', payload.label, payload.message);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
es.addEventListener('error', (e) => {
|
|
1575
|
+
// Server-sent `event: error` (CLI-reported); EventSource also fires
|
|
1576
|
+
// 'error' on connection drops with no payload. Treat both the same.
|
|
1577
|
+
let payload = {};
|
|
1578
|
+
if (e && e.data) { try { payload = JSON.parse(e.data); } catch {} }
|
|
1579
|
+
if (payload.message) {
|
|
1580
|
+
const row = document.createElement('div');
|
|
1581
|
+
row.className = 'row error';
|
|
1582
|
+
row.innerHTML = '<span class="label">sync failed</span><span class="count"></span>';
|
|
1583
|
+
row.children[1].textContent = payload.message.slice(0, 60);
|
|
1584
|
+
status.appendChild(row);
|
|
1585
|
+
}
|
|
1586
|
+
// Don't cleanup here — wait for `done`, or for the connection to
|
|
1587
|
+
// close on its own. Real errors will be followed by `done`.
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
es.addEventListener('done', async (e) => {
|
|
1591
|
+
let payload = {};
|
|
1592
|
+
try { payload = JSON.parse(e.data); } catch {}
|
|
1593
|
+
cleanup();
|
|
1594
|
+
btn.classList.add('is-done');
|
|
1595
|
+
setTimeout(() => btn.classList.remove('is-done'), 2000);
|
|
1596
|
+
|
|
1597
|
+
// Force-refresh the data so any newly-synced slices show up in
|
|
1598
|
+
// the tree immediately instead of on the next 30s poll tick.
|
|
1599
|
+
const data = await fetchData({ silent: true });
|
|
1600
|
+
if (data) {
|
|
1601
|
+
const arrivals = new Set();
|
|
1602
|
+
const oldKey = new Set(S.slices.map(s => s.hcu + '|' + (s.last_synced || '')));
|
|
1603
|
+
const nextSlices = data.slices.map(parseSlice);
|
|
1604
|
+
for (const s of nextSlices) {
|
|
1605
|
+
const k = s.hcu + '|' + (s.last_synced || '');
|
|
1606
|
+
if (!oldKey.has(k)) arrivals.add(s.hcu);
|
|
1607
|
+
}
|
|
1608
|
+
S.slices = nextSlices;
|
|
1609
|
+
S.knownGroups = Array.isArray(data.known_groups) ? data.known_groups : S.knownGroups;
|
|
1610
|
+
S.tree = buildTree(S.slices, S.knownGroups);
|
|
1611
|
+
S.stats = computeStats(S.slices);
|
|
1612
|
+
S.lastUpdated = maxTs(S.slices);
|
|
1613
|
+
S.newArrivals = arrivals;
|
|
1614
|
+
renderAll();
|
|
1615
|
+
setTimeout(() => { S.newArrivals = new Set(); }, 2500);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Auto-hide the status panel after a beat if everything succeeded.
|
|
1619
|
+
const anyErrors = Array.from(rows.values()).some(r => r.classList.contains('error'));
|
|
1620
|
+
if (!anyErrors) {
|
|
1621
|
+
setTimeout(() => {
|
|
1622
|
+
if (!btn.classList.contains('is-syncing')) status.classList.add('hidden');
|
|
1623
|
+
}, 4000);
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1459
1628
|
/* ══════════════════════════════════════════════════════
|
|
1460
1629
|
Top-level render
|
|
1461
1630
|
══════════════════════════════════════════════════════ */
|
|
@@ -2644,6 +2813,23 @@ const BRIDGE_SHIM_SOURCE = String.raw`
|
|
|
2644
2813
|
private_prefix: d.private_prefix,
|
|
2645
2814
|
});
|
|
2646
2815
|
else ih.reject(new Error(d.error || 'identity lookup failed'));
|
|
2816
|
+
} else if (d.type === 'bitpub.groups.result') {
|
|
2817
|
+
var gh = readHandlers.get(d.requestId);
|
|
2818
|
+
if (!gh) return;
|
|
2819
|
+
readHandlers.delete(d.requestId);
|
|
2820
|
+
if (d.ok) {
|
|
2821
|
+
// Three response shapes share one event for symmetry with how
|
|
2822
|
+
// the parent router replies. Callers just see their op's keys.
|
|
2823
|
+
var payload = {};
|
|
2824
|
+
if (typeof d.group !== 'undefined') payload.group = d.group;
|
|
2825
|
+
if (typeof d.invite_url!== 'undefined') payload.invite_url = d.invite_url;
|
|
2826
|
+
if (typeof d.groups !== 'undefined') payload.groups = d.groups;
|
|
2827
|
+
if (typeof d.rotated !== 'undefined') payload.rotated = d.rotated;
|
|
2828
|
+
if (typeof d.needs_rotate !== 'undefined') payload.needs_rotate = d.needs_rotate;
|
|
2829
|
+
gh.resolve(payload);
|
|
2830
|
+
} else {
|
|
2831
|
+
gh.reject(new Error(d.error || 'groups op failed'));
|
|
2832
|
+
}
|
|
2647
2833
|
}
|
|
2648
2834
|
});
|
|
2649
2835
|
window.bitpub = {
|
|
@@ -2725,6 +2911,69 @@ const BRIDGE_SHIM_SOURCE = String.raw`
|
|
|
2725
2911
|
}, 5000);
|
|
2726
2912
|
});
|
|
2727
2913
|
}
|
|
2914
|
+
},
|
|
2915
|
+
groups: {
|
|
2916
|
+
// Group management (browser chrome — backed by the local CLI's
|
|
2917
|
+
// bridge endpoints, not the manifest/apps system). Available to
|
|
2918
|
+
// any slice the parent has agreed to render; the user already
|
|
2919
|
+
// trusts these endpoints by running 'bitpub browser'.
|
|
2920
|
+
//
|
|
2921
|
+
// create(display) -> Promise of { group, invite_url }
|
|
2922
|
+
// list() -> Promise of { groups }
|
|
2923
|
+
// invite(slug, opts) -> Promise of { invite_url, rotated, needs_rotate }
|
|
2924
|
+
// opts.rotate=true rotates server-side
|
|
2925
|
+
// (invalidates any link teammates hold).
|
|
2926
|
+
create: function (display) {
|
|
2927
|
+
return new Promise(function (resolve, reject) {
|
|
2928
|
+
var requestId = ++nextId;
|
|
2929
|
+
readHandlers.set(requestId, { resolve: resolve, reject: reject });
|
|
2930
|
+
window.parent.postMessage({
|
|
2931
|
+
__bitpub: true,
|
|
2932
|
+
type: 'bitpub.groups.create',
|
|
2933
|
+
requestId: requestId,
|
|
2934
|
+
display: String(display || ''),
|
|
2935
|
+
}, '*');
|
|
2936
|
+
setTimeout(function () {
|
|
2937
|
+
if (!readHandlers.has(requestId)) return;
|
|
2938
|
+
readHandlers.delete(requestId);
|
|
2939
|
+
reject(new Error('groups.create timed out'));
|
|
2940
|
+
}, 30000);
|
|
2941
|
+
});
|
|
2942
|
+
},
|
|
2943
|
+
list: function () {
|
|
2944
|
+
return new Promise(function (resolve, reject) {
|
|
2945
|
+
var requestId = ++nextId;
|
|
2946
|
+
readHandlers.set(requestId, { resolve: resolve, reject: reject });
|
|
2947
|
+
window.parent.postMessage({
|
|
2948
|
+
__bitpub: true,
|
|
2949
|
+
type: 'bitpub.groups.list',
|
|
2950
|
+
requestId: requestId,
|
|
2951
|
+
}, '*');
|
|
2952
|
+
setTimeout(function () {
|
|
2953
|
+
if (!readHandlers.has(requestId)) return;
|
|
2954
|
+
readHandlers.delete(requestId);
|
|
2955
|
+
reject(new Error('groups.list timed out'));
|
|
2956
|
+
}, 5000);
|
|
2957
|
+
});
|
|
2958
|
+
},
|
|
2959
|
+
invite: function (slug, opts) {
|
|
2960
|
+
return new Promise(function (resolve, reject) {
|
|
2961
|
+
var requestId = ++nextId;
|
|
2962
|
+
readHandlers.set(requestId, { resolve: resolve, reject: reject });
|
|
2963
|
+
window.parent.postMessage({
|
|
2964
|
+
__bitpub: true,
|
|
2965
|
+
type: 'bitpub.groups.invite',
|
|
2966
|
+
requestId: requestId,
|
|
2967
|
+
slug: String(slug || ''),
|
|
2968
|
+
rotate: !!(opts && opts.rotate),
|
|
2969
|
+
}, '*');
|
|
2970
|
+
setTimeout(function () {
|
|
2971
|
+
if (!readHandlers.has(requestId)) return;
|
|
2972
|
+
readHandlers.delete(requestId);
|
|
2973
|
+
reject(new Error('groups.invite timed out'));
|
|
2974
|
+
}, 30000);
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2728
2977
|
}
|
|
2729
2978
|
};
|
|
2730
2979
|
})();
|
|
@@ -3019,6 +3268,89 @@ async function handleBridgeNamespaceListMessage(source, msg) {
|
|
|
3019
3268
|
}
|
|
3020
3269
|
}
|
|
3021
3270
|
|
|
3271
|
+
async function handleBridgeGroupsMessage(source, msg) {
|
|
3272
|
+
// Three ops share one reply event for symmetry with the iframe-side
|
|
3273
|
+
// dispatcher. Each op posts whatever subset of keys the caller asked
|
|
3274
|
+
// for; the shim unpacks them.
|
|
3275
|
+
const { requestId, type } = msg;
|
|
3276
|
+
if (typeof requestId !== 'number') return;
|
|
3277
|
+
const reply = (extra) => {
|
|
3278
|
+
try {
|
|
3279
|
+
source.postMessage({
|
|
3280
|
+
__bitpub: true,
|
|
3281
|
+
type: 'bitpub.groups.result',
|
|
3282
|
+
requestId,
|
|
3283
|
+
...extra,
|
|
3284
|
+
}, '*');
|
|
3285
|
+
} catch (_) { /* iframe gone */ }
|
|
3286
|
+
};
|
|
3287
|
+
|
|
3288
|
+
try {
|
|
3289
|
+
if (type === 'bitpub.groups.create') {
|
|
3290
|
+
const r = await fetch('/bridge/group/create', {
|
|
3291
|
+
method: 'POST',
|
|
3292
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3293
|
+
body: JSON.stringify({ display: msg.display || '' }),
|
|
3294
|
+
});
|
|
3295
|
+
const body = await r.json();
|
|
3296
|
+
if (!r.ok || !body.ok) {
|
|
3297
|
+
reply({ ok: false, error: (body && body.error) || `http ${r.status}` });
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
// The new group will appear in the local config; refresh the
|
|
3301
|
+
// sidebar tree so it shows up alongside any existing namespaces.
|
|
3302
|
+
// We refetch /api/data immediately (rather than waiting for the
|
|
3303
|
+
// 30s poll) and re-run buildTree so the empty group node lands
|
|
3304
|
+
// in the sidebar.
|
|
3305
|
+
(async () => {
|
|
3306
|
+
try {
|
|
3307
|
+
const fresh = await fetchData({ silent: true });
|
|
3308
|
+
if (!fresh) return;
|
|
3309
|
+
S.slices = fresh.slices.map(parseSlice);
|
|
3310
|
+
S.knownGroups = Array.isArray(fresh.known_groups) ? fresh.known_groups : S.knownGroups;
|
|
3311
|
+
S.tree = buildTree(S.slices, S.knownGroups);
|
|
3312
|
+
S.stats = computeStats(S.slices);
|
|
3313
|
+
renderAll();
|
|
3314
|
+
} catch (_) { /* poll will catch up */ }
|
|
3315
|
+
})();
|
|
3316
|
+
reply({ ok: true, group: body.group, invite_url: body.invite_url });
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
if (type === 'bitpub.groups.list') {
|
|
3320
|
+
const r = await fetch('/bridge/group/list');
|
|
3321
|
+
const body = await r.json();
|
|
3322
|
+
if (!r.ok || !body.ok) {
|
|
3323
|
+
reply({ ok: false, error: (body && body.error) || `http ${r.status}` });
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
reply({ ok: true, groups: body.groups || [] });
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
if (type === 'bitpub.groups.invite') {
|
|
3330
|
+
const r = await fetch('/bridge/group/invite', {
|
|
3331
|
+
method: 'POST',
|
|
3332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3333
|
+
body: JSON.stringify({ slug: msg.slug || '', rotate: !!msg.rotate }),
|
|
3334
|
+
});
|
|
3335
|
+
const body = await r.json();
|
|
3336
|
+
if (!r.ok || !body.ok) {
|
|
3337
|
+
reply({ ok: false, error: (body && body.error) || `http ${r.status}` });
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
reply({
|
|
3341
|
+
ok: true,
|
|
3342
|
+
invite_url: body.invite_url || null,
|
|
3343
|
+
rotated: !!body.rotated,
|
|
3344
|
+
needs_rotate: !!body.needs_rotate,
|
|
3345
|
+
});
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
reply({ ok: false, error: `unknown groups op: ${type}` });
|
|
3349
|
+
} catch (err) {
|
|
3350
|
+
reply({ ok: false, error: (err && err.message) || 'fetch failed' });
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3022
3354
|
async function handleBridgeIdentityMessage(source, msg) {
|
|
3023
3355
|
const { requestId } = msg;
|
|
3024
3356
|
if (typeof requestId !== 'number') return;
|
|
@@ -3058,6 +3390,9 @@ window.addEventListener('message', (e) => {
|
|
|
3058
3390
|
if (d.type === 'bitpub.run') handleBridgeRunMessage(e.source, d);
|
|
3059
3391
|
else if (d.type === 'bitpub.namespace.list') handleBridgeNamespaceListMessage(e.source, d);
|
|
3060
3392
|
else if (d.type === 'bitpub.identity.me') handleBridgeIdentityMessage(e.source, d);
|
|
3393
|
+
else if (d.type === 'bitpub.groups.create' ||
|
|
3394
|
+
d.type === 'bitpub.groups.list' ||
|
|
3395
|
+
d.type === 'bitpub.groups.invite') handleBridgeGroupsMessage(e.source, d);
|
|
3061
3396
|
else if (d.type === 'bitpub.navigate' || d.type === 'navigate') handleBridgeNavigateMessage(e.source, d);
|
|
3062
3397
|
});
|
|
3063
3398
|
|