Skip to content

Commit 4442db8

Browse files
committed
fix(admin): address review feedback
Broadcast newly persisted NIP-66 events, prefer relay_url in bootstrap relay list, clamp frequency tags to the worker minimum, add bootstrap TTL, and complete Network Health dashboard coverage for issue #698.
1 parent 84df14c commit 4442db8

12 files changed

Lines changed: 281 additions & 38 deletions

File tree

‎.changeset/admin-network-health-panel.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
feat(admin): add Network Health panel to observability dashboard
66

7-
Adds GET `/admin/network-health` and a dashboard section that displays the latest NIP-66 external probe snapshot alongside live KPI metrics.
7+
Adds a dashboard section that renders the latest NIP-66 probe snapshot with per-target DNS, TLS, WebSocket RTT, and NIP-11 status.

‎resources/admin/assets/dashboard.css‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,14 +765,35 @@
765765
padding: 0.85rem 1rem;
766766
}
767767

768+
.network-health-target-header {
769+
align-items: flex-start;
770+
display: flex;
771+
gap: 0.65rem;
772+
justify-content: space-between;
773+
margin-bottom: 0.65rem;
774+
}
775+
768776
.network-health-target-url {
769777
color: var(--text);
778+
flex: 1;
770779
font-size: 0.85rem;
771780
font-weight: 600;
772-
margin-bottom: 0.65rem;
781+
margin-bottom: 0;
773782
word-break: break-all;
774783
}
775784

785+
.network-health-network-type {
786+
background: var(--panel-border);
787+
border: 1px solid var(--panel-border);
788+
color: var(--label);
789+
font-size: 0.62rem;
790+
font-weight: 700;
791+
letter-spacing: 0.06em;
792+
padding: 0.15rem 0.4rem;
793+
text-transform: uppercase;
794+
white-space: nowrap;
795+
}
796+
776797
.network-health-checks {
777798
display: grid;
778799
gap: 0.45rem;

‎resources/admin/assets/dashboard.js‎

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,12 @@
385385
networkHealthSync.innerHTML = `<span class="prompt-char">&gt;</span> probes: ${message}`
386386
}
387387

388-
const probeCheckStatusClass = (status) => {
388+
const probeCheckStatusClass = (status, options = {}) => {
389389
if (status === 'ok') {
390+
if (typeof options.tlsDaysUntilExpiry === 'number' && options.tlsDaysUntilExpiry < 14) {
391+
return 'status-degraded'
392+
}
393+
390394
return 'status-ok'
391395
}
392396
if (status === 'error') {
@@ -396,13 +400,14 @@
396400
return 'status-no-data'
397401
}
398402

399-
const formatProbeCheckDetail = (check, formatter) => {
403+
const formatProbeCheckDetail = (check, formatter, options = {}) => {
400404
const label = statusLabels.probeCheck[check?.status] ?? statusLabels.probeCheck.skipped
401405
const detail = typeof formatter === 'function' && check?.status === 'ok' ? formatter(check.data) : check?.error
406+
const className = probeCheckStatusClass(check?.status, options)
402407

403408
return {
404409
label,
405-
className: probeCheckStatusClass(check?.status),
410+
className,
406411
detail: detail ? String(detail) : '',
407412
}
408413
}
@@ -458,31 +463,78 @@
458463
const card = document.createElement('article')
459464
card.className = 'network-health-target'
460465

466+
const header = document.createElement('div')
467+
header.className = 'network-health-target-header'
468+
461469
const title = document.createElement('p')
462470
title.className = 'network-health-target-url mb-0'
463471
title.textContent = result?.target?.relayUrl ?? result?.target?.wsUrl ?? 'Unknown target'
464-
card.appendChild(title)
472+
header.appendChild(title)
473+
474+
const networkType = result?.target?.networkType
475+
if (networkType) {
476+
const badge = document.createElement('span')
477+
badge.className = 'network-health-network-type'
478+
badge.textContent = networkType
479+
header.appendChild(badge)
480+
}
481+
482+
card.appendChild(header)
465483

466484
const checks = document.createElement('div')
467485
checks.className = 'network-health-checks'
468486

469487
const dns = formatProbeCheckDetail(result.dns, (data) => {
470-
const count = Array.isArray(data?.records) ? data.records.length : 0
471-
return `${count} record${count === 1 ? '' : 's'}`
472-
})
473-
const tls = formatProbeCheckDetail(result.tls, (data) => {
474-
if (typeof data?.daysUntilExpiry === 'number') {
475-
return `${data.daysUntilExpiry}d remaining`
488+
const records = Array.isArray(data?.records) ? data.records : []
489+
490+
if (records.length === 0) {
491+
return 'no records'
476492
}
477493

478-
return data?.issuer ?? 'valid'
494+
const preview = records.slice(0, 3).map((record) => {
495+
const ttl = typeof record?.ttl === 'number' ? ` TTL ${record.ttl}` : ''
496+
return `${record.type} ${record.value}${ttl}`
497+
})
498+
499+
if (records.length > 3) {
500+
preview.push(`+${records.length - 3} more`)
501+
}
502+
503+
return preview.join('; ')
479504
})
505+
const tlsDaysUntilExpiry =
506+
result.tls?.status === 'ok' && typeof result.tls?.data?.daysUntilExpiry === 'number'
507+
? result.tls.data.daysUntilExpiry
508+
: undefined
509+
const tls = formatProbeCheckDetail(
510+
result.tls,
511+
(data) => {
512+
if (typeof data?.daysUntilExpiry === 'number') {
513+
return `${data.daysUntilExpiry}d remaining`
514+
}
515+
516+
return data?.issuer ?? 'valid'
517+
},
518+
{ tlsDaysUntilExpiry },
519+
)
480520
const wsRtt = formatProbeCheckDetail(result.wsRtt, (data) => `${data.rttOpenMs} ms`)
481521
const nip11 = formatProbeCheckDetail(result.nip11, (data) => {
482522
const name = data?.name ? ` ${data.name}` : ''
483-
return `HTTP ${data.statusCode}${name}`
523+
const supportedNips = Array.isArray(data?.supportedNips) ? data.supportedNips : null
524+
const nip66Warning =
525+
supportedNips && !supportedNips.includes(66) ? ' · NIP-66 not in supported_nips' : ''
526+
527+
return `HTTP ${data.statusCode}${name}${nip66Warning}`
484528
})
485529

530+
if (
531+
result.nip11?.status === 'ok' &&
532+
Array.isArray(result.nip11?.data?.supportedNips) &&
533+
!result.nip11.data.supportedNips.includes(66)
534+
) {
535+
nip11.className = 'status-degraded'
536+
}
537+
486538
;[
487539
['DNS', dns],
488540
['TLS', tls],

‎src/app/relay-monitor-worker.ts‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import { INip66EventPublisher } from '../services/nip66-event-publisher'
66
import { shutdownMetricsTelemetry } from '../telemetry/metrics'
77
import { filterValidProbeTargets, resolveProbeTargets } from '../utils/relay-probe-targets'
88
import { deriveRelayProbeRunStatus, serializeProbeResults } from '../utils/relay-probe-snapshot'
9+
import { getEffectiveProbeIntervalSeconds, getProbeIntervalMs } from '../utils/nip66-schedule'
910
import { runProbe } from '../utils/relay-probe'
1011
import { ProbeOptions, ProbeResult } from '../utils/relay-probe/types'
1112

1213
const logger = createLogger('relay-monitor-worker')
1314

14-
const DEFAULT_PROBE_INTERVAL_SECONDS = 3600
15-
const MIN_PROBE_INTERVAL_SECONDS = 60
16-
1715
export type RunProbeFn = (relayUrl: string, options?: ProbeOptions) => Promise<ProbeResult>
1816

1917
export const buildProbeOptions = (settings: Settings): ProbeOptions => {
@@ -25,12 +23,7 @@ export const buildProbeOptions = (settings: Settings): ProbeOptions => {
2523
}
2624
}
2725

28-
export const getProbeIntervalMs = (settings: Settings): number => {
29-
const configured = settings.nip66?.probeIntervalSeconds ?? DEFAULT_PROBE_INTERVAL_SECONDS
30-
const intervalSeconds = Math.max(configured, MIN_PROBE_INTERVAL_SECONDS)
31-
32-
return intervalSeconds * 1000
33-
}
26+
export { getProbeIntervalMs } from '../utils/nip66-schedule'
3427

3528
export class RelayMonitorWorker implements IRunnable {
3629
private interval: NodeJS.Timeout | undefined
@@ -130,10 +123,7 @@ export class RelayMonitorWorker implements IRunnable {
130123
status: deriveRelayProbeRunStatus(results),
131124
}
132125

133-
const expirySeconds = Math.max(
134-
(currentSettings.nip66?.probeIntervalSeconds ?? DEFAULT_PROBE_INTERVAL_SECONDS) * 2,
135-
MIN_PROBE_INTERVAL_SECONDS * 2,
136-
)
126+
const expirySeconds = getEffectiveProbeIntervalSeconds(currentSettings) * 2
137127

138128
await this.snapshotStore.saveLatest(snapshot, expirySeconds)
139129
logger('saved probe snapshot for %d target(s) with status %s', valid.length, snapshot.status)

‎src/services/nip66-event-publisher.ts‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { ICacheAdapter } from '../@types/adapters'
2-
import { ParameterizedReplaceableEvent, UnidentifiedEvent } from '../@types/event'
2+
import { Event, ParameterizedReplaceableEvent, UnidentifiedEvent } from '../@types/event'
33
import { RelayProbeRunSnapshot } from '../@types/relay-probe-snapshot'
44
import { IEventRepository } from '../@types/repositories'
55
import { Settings } from '../@types/settings'
66
import { EventDeduplicationMetadataKey, EventTags } from '../constants/base'
77
import { createLogger } from '../factories/logger-factory'
8-
import { getPublicKey, identifyEvent, isParameterizedReplaceableEvent, signEvent } from '../utils/event'
8+
import { broadcastEvent, getPublicKey, identifyEvent, isParameterizedReplaceableEvent, signEvent } from '../utils/event'
99
import { getMonitorPrivateKey } from '../utils/monitor-identity'
1010
import {
1111
buildMonitorAnnouncementEvent,
@@ -18,6 +18,7 @@ import { resolveProbeTargets } from '../utils/relay-probe-targets'
1818
const logger = createLogger('nip66-event-publisher')
1919

2020
export const NIP66_MONITOR_BOOTSTRAPPED_KEY = 'nip66:monitor:bootstrapped'
21+
export const NIP66_MONITOR_BOOTSTRAP_TTL_SECONDS = 30 * 24 * 60 * 60
2122

2223
export interface INip66EventPublisher {
2324
publishAfterProbe(snapshot: RelayProbeRunSnapshot, settings: Settings): Promise<void>
@@ -63,32 +64,40 @@ export class Nip66EventPublisher implements INip66EventPublisher {
6364
return
6465
}
6566

66-
const relayUrl = resolveProbeTargets(settings)[0] ?? settings.info.relay_url
67+
const relayUrl = settings.info?.relay_url?.trim() || resolveProbeTargets(settings)[0]
68+
69+
if (!relayUrl) {
70+
logger.warn('no relay URL available for NIP-66 bootstrap relay list; skipping kind 10002 publish')
71+
return
72+
}
6773

6874
await this.persistSignedEvent(buildMonitorProfileEvent(monitorPubkey, createdAt), privkey)
6975
await this.persistSignedEvent(buildMonitorRelayListEvent(relayUrl, monitorPubkey, createdAt), privkey)
7076

71-
await this.cache.setKey(NIP66_MONITOR_BOOTSTRAPPED_KEY, monitorPubkey)
77+
await this.cache.setKey(NIP66_MONITOR_BOOTSTRAPPED_KEY, monitorPubkey, NIP66_MONITOR_BOOTSTRAP_TTL_SECONDS)
7278
logger('bootstrapped NIP-66 monitor identity for pubkey %s', monitorPubkey)
7379
}
7480

7581
private async persistSignedEvent(unsigned: UnidentifiedEvent, privkey: string): Promise<void> {
7682
const signed = await signEvent(privkey)(await identifyEvent(unsigned))
83+
let count: number
7784

7885
if (isParameterizedReplaceableEvent(signed)) {
7986
const [, deduplication] = signed.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [
8087
null,
8188
'',
8289
]
8390

84-
await this.eventRepository.upsert({
91+
count = await this.eventRepository.upsert({
8592
...signed,
8693
[EventDeduplicationMetadataKey]: deduplication ? [deduplication] : [''],
8794
} as ParameterizedReplaceableEvent)
88-
89-
return
95+
} else {
96+
count = await this.eventRepository.upsert(signed as Event)
9097
}
9198

92-
await this.eventRepository.upsert(signed)
99+
if (count) {
100+
await broadcastEvent(signed)
101+
}
93102
}
94103
}

‎src/utils/nip66-events.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Tag } from '../@types/base'
33
import { StoredProbeResult } from '../@types/relay-probe-snapshot'
44
import { Settings } from '../@types/settings'
55
import { EventKinds, EventTags } from '../constants/base'
6+
import { getEffectiveProbeIntervalSeconds } from './nip66-schedule'
67

78
export const normalizeRelayUrlForDTag = (relayUrl: string): string => {
89
const parsed = new URL(relayUrl)
@@ -57,7 +58,7 @@ export const buildMonitorAnnouncementEvent = (
5758
const timeouts = nip66?.timeouts
5859

5960
const tags: Tag[] = [
60-
['frequency', String(nip66?.probeIntervalSeconds ?? 3600)],
61+
['frequency', String(getEffectiveProbeIntervalSeconds(settings))],
6162
['c', 'ws'],
6263
['c', 'nip11'],
6364
['c', 'ssl'],

‎src/utils/nip66-schedule.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Settings } from '../@types/settings'
2+
3+
export const DEFAULT_PROBE_INTERVAL_SECONDS = 3600
4+
export const MIN_PROBE_INTERVAL_SECONDS = 60
5+
6+
export const getEffectiveProbeIntervalSeconds = (settings: Settings): number => {
7+
const configured = settings.nip66?.probeIntervalSeconds ?? DEFAULT_PROBE_INTERVAL_SECONDS
8+
9+
return Math.max(configured, MIN_PROBE_INTERVAL_SECONDS)
10+
}
11+
12+
export const getProbeIntervalMs = (settings: Settings): number => {
13+
return getEffectiveProbeIntervalSeconds(settings) * 1000
14+
}

‎src/utils/relay-probe/nip11-probe.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const nip11DocumentSchema = z
1111
.object({
1212
name: z.string().optional(),
1313
pubkey: pubkeySchema.optional(),
14+
supported_nips: z.array(z.number().int().positive()).optional(),
1415
})
1516
.passthrough()
1617

@@ -96,6 +97,7 @@ export const createNodeNip11Fetcher = (): Nip11Fetcher => ({
9697
statusCode: response.status,
9798
name: parsed.data.name,
9899
pubkey: parsed.data.pubkey,
100+
supportedNips: parsed.data.supported_nips,
99101
}
100102
} catch (error: unknown) {
101103
const axiosError = error as AxiosError

‎src/utils/relay-probe/types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface Nip11Result {
5151
statusCode: number
5252
name?: string
5353
pubkey?: string
54+
supportedNips?: number[]
5455
}
5556

5657
export interface ProbeResult {

0 commit comments

Comments
 (0)