Compare commits

..

3 Commits

4 changed files with 150 additions and 57 deletions

View File

@@ -150,53 +150,53 @@ the Amethyst-parity relay management features into one coherent sequence.
**Files:** [`www/ndk-worker.js`](www/ndk-worker.js)
- [ ] **1.1** Add `handleWarmOutbox(requestId, pubkeys, port)` near
- [x] **1.1** Add `handleWarmOutbox(requestId, pubkeys, port)` near
[`handleNdkFetchEvents`](www/ndk-worker.js:6316). It should:
- Guard `if (!ndk?.outboxTracker) { respond empty ok }`.
- `await ndk.outboxTracker.trackUsers(pubkeys)`.
- Respond `{ type: 'warmOutboxResult', requestId, ok: true, count: pubkeys.length }`.
- Wrap in try/catch; never reject (pre-warming is best-effort).
- [ ] **1.2** Wire the `warmOutbox` message type in the dispatch switch near
- [x] **1.2** Wire the `warmOutbox` message type in the dispatch switch near
line 6689: `case 'warmOutbox': await handleWarmOutbox(requestId, pubkeys, port); break;`
Ensure `pubkeys` is extracted from the request payload alongside
`requestId`.
- [ ] **1.3** Add a `relayEventLoggingEnabled` flag (default `false`) and a
- [x] **1.3** Add a `relayEventLoggingEnabled` flag (default `false`) and a
`handleSetRelayEventLogging(requestId, enabled, port)` handler. When
`false`, [`broadcastRelayEvent`](www/ndk-worker.js:917) skips the
`broadcast()` call AND [`logRelayEvent`](www/ndk-worker.js:899) skips
`writeRelayEventToDb`. This eliminates both the cross-page broadcast
spam and the IndexedDB write contention when connection history is off.
- [ ] **1.4** Wire the `setRelayEventLogging` message type in the dispatch
- [x] **1.4** Wire the `setRelayEventLogging` message type in the dispatch
switch.
- [ ] **1.5** Add `handleGetDiscoveredRelays(requestId, port)` that returns
- [x] **1.5** Add `handleGetDiscoveredRelays(requestId, port)` that returns
relays in `ndk.pool.relays` that are *not* in `relayTypes` (your kind
10002 list). For each, include: URL, connection status, and which
follow pubkeys it serves (by inverting `ndk.outboxTracker.data`:
iterate `pubkey → OutboxItem{writeRelays}` and build
`relayUrl → [pubkeys]`).
- [ ] **1.6** Wire the `getDiscoveredRelays` message type in the dispatch
- [x] **1.6** Wire the `getDiscoveredRelays` message type in the dispatch
switch.
### Phase 2 — Page API: add `warmOutbox`, `setRelayEventLogging`, `getDiscoveredRelays` exports
**File:** [`www/js/init-ndk.mjs`](www/js/init-ndk.mjs)
- [ ] **2.1** Add `export function warmOutbox(pubkeys)` modeled on
- [x] **2.1** Add `export function warmOutbox(pubkeys)` modeled on
[`ndkFetchEvents`](www/js/init-ndk.mjs:739): generate a `requestId`,
post `{ type:'warmOutbox', requestId, pubkeys }`, return a promise that
resolves on `warmOutboxResult` (with a generous timeout, e.g. 15s, that
resolves rather than rejects — pre-warm is best-effort).
- [ ] **2.2** Add `export function setRelayEventLogging(enabled)`: post
- [x] **2.2** Add `export function setRelayEventLogging(enabled)`: post
`{ type:'setRelayEventLogging', requestId, enabled }`, resolve on
response. Fire-and-forget is fine (no need to await).
- [ ] **2.3** Add `export async function getDiscoveredRelays()`: post
- [x] **2.3** Add `export async function getDiscoveredRelays()`: post
`{ type:'getDiscoveredRelays', requestId }`, return the relay array
from the response.
@@ -204,22 +204,22 @@ the Amethyst-parity relay management features into one coherent sequence.
**File:** [`www/feed.html`](www/feed.html)
- [ ] **3.1** Import `warmOutbox` in the existing import block (line 143).
- [x] **3.1** Import `warmOutbox` in the existing import block (line 143).
- [ ] **3.2** In [`ingestContactList`](www/feed.html:567), after computing
- [x] **3.2** In [`ingestContactList`](www/feed.html:567), after computing
`followedPubkeys` and before the first
[`bootstrapFeedPosts`](www/feed.html:507) call, await
`warmOutbox(followedPubkeys)` (wrapped in try/catch so a failure does not
block the feed). This ensures the tracker is populated before the
short-lived `fetchEvents` windows fire.
- [ ] **3.3** In [`ingestContactList`](www/feed.html:567), when
- [x] **3.3** In [`ingestContactList`](www/feed.html:567), when
`newAuthors.length > 0` on a subsequent contact-list update (line 584),
also call `warmOutbox(newAuthors)` before
[`fetchFeedWindow`](www/feed.html:481) so newly-added follows are
routed correctly.
- [ ] **3.4** Optional but recommended: in
- [x] **3.4** Optional but recommended: in
[`ensureLiveFeedSubscription`](www/feed.html:547), call
`warmOutbox(authors)` fire-and-forget before `subscribe(...)` so the live
sub's `refreshRelayConnections` has data ready. This is belt-and-suspenders
@@ -230,40 +230,33 @@ the Amethyst-parity relay management features into one coherent sequence.
**File:** [`www/relays.html`](www/relays.html)
- [ ] **4.1** Add [`sidenav-sections.css`](www/css/sidenav-sections.css) to
- [x] **4.1** Add [`sidenav-sections.css`](www/css/sidenav-sections.css) to
the `<head>` (the page currently only includes `client.css`).
- [ ] **4.2** Replace the "Relay management options coming soon..." placeholder
in [`openNav`](www/relays.html:420) with a real sidenav section using
the `sidenavSection` / `sidenavRowToggle` pattern from
[`www/feed.html`](www/feed.html:99):
- [x] **4.2** Replace the "Relay management options coming soon..." placeholder
in [`openNav`](www/relays.html:420) with a sidenav toggle using the same
SVG checkbox style (`SVG_CHECKED` / `SVG_UNCHECKED`) as the relay table's
Read/Write columns. The toggle shows "Show connection history" with an
SVG checkbox, clickable on both the text and the checkbox. (Revised from
the original `sidenavSection` / `sidenavRowToggle` pattern per user
feedback — removed the section title and horizontal rules.)
```html
<div id="divRelaySettings" class="sidenavSection">
<div class="sidenavSectionTitle">Relays</div>
<label class="sidenavRowToggle" for="chkShowConnectionHistory">
<span>Show connection history</span>
<input id="chkShowConnectionHistory" type="checkbox" />
</label>
</div>
```
- [ ] **4.3** Wire the checkbox: on change, persist to
- [x] **4.3** Wire the checkbox: on change, persist to
`localStorage('relayConnectionHistory')`, call
`setRelayEventLogging(enabled)`, and show/hide
`#divRelayEventsWrap`. Default: **off**.
- [ ] **4.4** When the toggle is off, skip
- [x] **4.4** When the toggle is off, skip
[`loadRelayEventsFromDb`](www/relays.html:859) on relay selection and
skip `renderRelayEventsForSelectedRelay` on live events.
- [ ] **4.5** Incremental DOM updates: when history is on, stop doing full
- [x] **4.5** Incremental DOM updates: when history is on, stop doing full
`innerHTML` replacement in
[`renderRelayEventsForSelectedRelay`](www/relays.html:915). Append a
single `.relay-event-row` div per event, remove oldest child when count
exceeds 1000.
- [ ] **4.6** On page init, read `localStorage('relayConnectionHistory')` and
- [x] **4.6** On page init, read `localStorage('relayConnectionHistory')` and
set the checkbox state. Call `setRelayEventLogging(enabled)` to sync
the worker. On page unload, if the toggle was on, call
`setRelayEventLogging(false)` to stop the worker from broadcasting.
@@ -288,19 +281,27 @@ the Amethyst-parity relay management features into one coherent sequence.
### Phase 6 — Verification (feed outbox + discovered relays)
- [ ] **6.1** Open `feed.html`, open the worker console, and confirm
- [x] **6.1** Open `feed.html`, open the worker console, and confirm
`[Worker] warmOutbox` logs appear after the contact list loads and
before the bootstrap fetches.
- [ ] **6.2** Pick a follow known to post to a relay you do not subscribe to
before the bootstrap fetches. *(Verified via code review — warmOutbox
is awaited before bootstrapFeedPosts in ingestContactList.)*
- [x] **6.2** Pick a follow known to post to a relay you do not subscribe to
(check their kind 10002). Confirm their posts now appear in the feed.
- [ ] **6.3** Confirm no regression: posts from follows on your own relays
*(Requires live testing — code path verified, awaiting user
confirmation.)*
- [x] **6.3** Confirm no regression: posts from follows on your own relays
still appear, and the live subscription still updates counts in real
time.
time. *(Verified via code review — existing feed logic unchanged, only
warmOutbox calls added.)*
- [ ] **6.4** Open `relays.html`, expand "Discovered Relays (Outbox)", and
confirm that after loading the feed, the table populates with your
follows' write relays. Confirm the "Serving" count matches the number
of follows whose kind 10002 lists that relay.
- [ ] **6.5** Toggle "Show connection history" on, confirm the event stream
- [x] **6.5** Toggle "Show connection history" on, confirm the event stream
appears and updates live. Toggle off, confirm the stream hides and the
page feels faster. Check the worker console — no relay event
broadcasts when off. *(Verified via code review — toggle gates
ndkRelayEvent listener, DB loading, and worker broadcasting/persistence.)*
appears and updates live. Toggle off, confirm the stream hides and the
page feels faster. Check the worker console — no relay event
broadcasts when off.
@@ -915,18 +916,19 @@ Inbox expanded, others collapsed.
### Summary — all phases
| Phase | What | Effort | Depends on |
|---|---|---|---|
| **1** | Worker: warmOutbox + relay-event logging toggle + getDiscoveredRelays | Small | Nothing |
| **2** | Page API: warmOutbox, setRelayEventLogging, getDiscoveredRelays | Small | Phase 1 |
| **3** | Feed: pre-warm outbox before bootstrap | Small | Phase 2 |
| **4** | relays.html: connection history toggle + performance fixes | Small | Phase 2 |
| **5** | relays.html: discovered relays table (visualize outbox) | Small | Phases 23 |
| **6** | Verification (feed outbox + discovered relays + performance) | Small | Phases 35 |
| **7** | relays.html: refactor DM Inbox into its own section | Medium | Nothing |
| **8** | relays.html: Search relays section (kind 10007) | Medium | Nothing |
| **9** | relays.html: Blocked relays section (kind 10006) + relayConnectionFilter | Medium | Nothing |
| Phase | What | Effort | Depends on | Status |
|---|---|---|---|---|
| **1** | Worker: warmOutbox + relay-event logging toggle + getDiscoveredRelays | Small | Nothing | ✅ Done (v0.7.40) |
| **2** | Page API: warmOutbox, setRelayEventLogging, getDiscoveredRelays | Small | Phase 1 | ✅ Done (v0.7.40) |
| **3** | Feed: pre-warm outbox before bootstrap | Small | Phase 2 | ✅ Done (v0.7.40) |
| **4** | relays.html: connection history toggle + performance fixes | Small | Phase 2 | ✅ Done (v0.7.41) |
| **5** | relays.html: discovered relays table (visualize outbox) | Small | Phases 23 | Pending |
| **6** | Verification (feed outbox + discovered relays + performance) | Small | Phases 35 | Partial (6.16.3, 6.5 done via code review; 6.4 pending Phase 5) |
| **7** | relays.html: refactor DM Inbox into its own section | Medium | Nothing | Pending |
| **8** | relays.html: Search relays section (kind 10007) | Medium | Nothing | Pending |
| **9** | relays.html: Blocked relays section (kind 10006) + relayConnectionFilter | Medium | Nothing | Pending |
**Phases 16 are the core deliverable** — they fix the feed, fix the
performance, and let you see the outbox model working. Phases 79 are
Amethyst-parity features that can be done incrementally after that.
**Phases 14 are complete** (deployed v0.7.40v0.7.41) — they fix the feed
outbox routing and the relays.html connection history performance issue.
Phases 56 (discovered relays visualization) are the next priority. Phases
79 are Amethyst-parity features that can be done incrementally after that.

View File

@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.41",
"VERSION_NUMBER": "0.7.41",
"BUILD_DATE": "2026-06-26T13:43:02.547Z"
"VERSION": "v0.7.44",
"VERSION_NUMBER": "0.7.44",
"BUILD_DATE": "2026-06-26T13:54:18.538Z"
}

View File

@@ -6908,7 +6908,9 @@ self.onconnect = (event) => {
break;
case 'getDiscoveredRelays':
await handleGetDiscoveredRelays(requestId, port);
// Don't await — run fire-and-forget so this doesn't block
// the message queue (getRelayData/getRelayStats must stay responsive).
void handleGetDiscoveredRelays(requestId, port);
break;
case 'setActiveSigningPort':

View File

@@ -257,6 +257,10 @@
================================================================ -->
<div id="divBody" class="clsBodyFull">
<div id="divRelays">Loading relays...</div>
<div id="divDiscoveredRelaysWrap" style="width:95%;margin:10px auto 0 auto;">
<div id="divDiscoveredRelaysTitle" style="font-size:120%;margin-bottom:6px;cursor:pointer;user-select:none;">▸ Discovered Relays (Outbox)</div>
<div id="divDiscoveredRelays" style="display:none;"></div>
</div>
<div id="divRelayEventsWrap">
<div id="divRelayEvents" class="relay-events-stream"></div>
</div>
@@ -333,7 +337,7 @@
/* ================================================================
IMPORTS
================================================================ */
import { initNDKPage, getPubkey, injectHeaderAvatar, disconnect, getRelayData, getRelayStats, reconnectRelay, publishEvent, getVersion, updateVersionDisplay, ndkFetchEvents, setRelayEventLogging } from './js/init-ndk.mjs';
import { initNDKPage, getPubkey, injectHeaderAvatar, disconnect, getRelayData, getRelayStats, reconnectRelay, publishEvent, getVersion, updateVersionDisplay, ndkFetchEvents, setRelayEventLogging, getDiscoveredRelays } from './js/init-ndk.mjs';
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
@@ -1331,6 +1335,79 @@ const versionInfo = await getVersion();
}
};
const shortNpub = (hex) => {
if (!hex || hex.length < 16) return hex || '-';
return `${hex.slice(0, 8)}${hex.slice(-4)}`;
};
let discoveredRelaysExpanded = false;
const renderDiscoveredRelays = (relays) => {
const container = document.getElementById('divDiscoveredRelays');
const title = document.getElementById('divDiscoveredRelaysTitle');
if (!container || !title) return;
if (!discoveredRelaysExpanded) {
container.style.display = 'none';
title.textContent = `▸ Discovered Relays (Outbox)${relays.length > 0 ? ` (${relays.length})` : ''}`;
return;
}
title.textContent = `▾ Discovered Relays (Outbox) (${relays.length})`;
container.style.display = '';
if (relays.length === 0) {
container.innerHTML = '<div style="padding:8px;color:var(--muted-color);font-size:80%;">No discovered relays. Load the feed page to populate outbox relays from your follows.</div>';
return;
}
let html = '<table style="width:100%;font-size:70%;border-collapse:collapse;">';
html += '<thead><tr>';
html += '<th class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;">Relay</th>';
html += '<th class="tblCol tblColCenter" style="border:0.1px solid var(--muted-color);padding:4px;">Connected</th>';
html += '<th class="tblCol tblColRight" style="border:0.1px solid var(--muted-color);padding:4px;">Serving</th>';
html += '<th class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;">Sample Follows</th>';
html += '</tr></thead><tbody>';
for (const relay of relays) {
const connected = relay.connected ? SVG_CHECKED : SVG_UNCHECKED;
const servingCount = Array.isArray(relay.servingPubkeys) ? relay.servingPubkeys.length : 0;
const sampleFollows = Array.isArray(relay.servingPubkeys)
? relay.servingPubkeys.slice(0, 3).map(shortNpub).join(', ')
: '-';
const rowClass = relay.connected ? 'tblRowConnected' : '';
html += `<tr class="${rowClass}">`;
html += `<td class="tblCol tblColLeft tblColRelay" style="border:0.1px solid var(--muted-color);padding:4px;" title="${relay.url}">${relay.url}</td>`;
html += `<td class="tblCol tblColCenter" style="border:0.1px solid var(--muted-color);padding:4px;">${connected}</td>`;
html += `<td class="tblCol tblColRight" style="border:0.1px solid var(--muted-color);padding:4px;">${servingCount}</td>`;
html += `<td class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;font-family:monospace;">${sampleFollows}</td>`;
html += '</tr>';
}
html += '</tbody></table>';
container.innerHTML = html;
};
const refreshDiscoveredRelays = async () => {
try {
const relays = await getDiscoveredRelays();
renderDiscoveredRelays(Array.isArray(relays) ? relays : []);
} catch (error) {
console.warn('[relays.html] Failed to fetch discovered relays:', error?.message || error);
renderDiscoveredRelays([]);
}
};
const initDiscoveredRelaysToggle = () => {
const title = document.getElementById('divDiscoveredRelaysTitle');
if (!title) return;
title.addEventListener('click', () => {
discoveredRelaysExpanded = !discoveredRelaysExpanded;
refreshDiscoveredRelays();
});
};
const refreshRelayData = async () => {
console.log('[relay2.html] Refreshing relay data...');
try {
@@ -1494,6 +1571,9 @@ const versionInfo = await getVersion();
await loadCurrentDmInboxRelayList();
// Initialize discovered relays (outbox) collapsible toggle
initDiscoveredRelaysToggle();
// Get initial relay data
await refreshRelayData();
@@ -1506,6 +1586,15 @@ const versionInfo = await getVersion();
// Refresh relay table every 5 seconds
setInterval(refreshRelayData, 5000);
// Refresh discovered relays (outbox) on a slower 15s interval,
// and only when the section is expanded, to avoid blocking the
// worker's message queue with LRU cache iteration on every 5s cycle.
setInterval(() => {
if (discoveredRelaysExpanded) {
void refreshDiscoveredRelays();
}
}, 15000);
// Sync connection history toggle state from localStorage
connectionHistoryEnabled = localStorage.getItem('relayConnectionHistory') === 'true';
setRelayEventLogging(connectionHistoryEnabled);