tie it all together, needs cleanup

This commit is contained in:
dskvr
2022-12-17 17:36:59 +01:00
parent 6824147db0
commit dda81e466f
18 changed files with 1255 additions and 255 deletions

View File

@@ -4,5 +4,10 @@ server {
location / {
index index.html;
# try_files $uri $uri/ /index.html;
}
location ~ /?(.*)$ {
index index.html
}
}

View File

@@ -30,7 +30,7 @@
"node-emoji": "1.11.0",
"node-polyfill-webpack-plugin": "2.0.1",
"nostr": "0.2.5",
"nostr-relay-inspector": "0.0.7",
"nostr-relay-inspector": "0.0.9",
"nostr-tools": "0.24.1",
"onion-regex": "2.0.8",
"requests": "0.3.0",
@@ -45,6 +45,8 @@
"vue-router": "4.1.6",
"vue-simple-maps": "1.1.3",
"vue3-popper": "1.5.0",
"vue3-storage": "0.1.11",
"vue3-tabs-component": "1.1.2",
"yaml-loader": "^0.6.0",
"yaml2json": "1.0.2"
},

View File

@@ -45,7 +45,7 @@ const query = async function(){
// console.log(dns, ip)
geo = await getGeo(ip)
console.log(geo, ip, dns)
// console.log(geo, ip, dns)
if(dns)
geo.dns = dns[dns.length-1]

View File

@@ -0,0 +1,42 @@
<template>
<section id="footer">
<span>Updated {{ refreshData.sinceLast }} ago</span>
<span><button @click="invalidate(true)">Update Now</button></span>
<span><input type="checkbox" id="checkbox" v-model="preferences.refresh" /><label for="">Refresh Automatically</label></span>
<span v-if="preferences.refresh"> Next refresh in: {{ refreshData.untilNext }}</span>
<span v-if="preferences.refresh">
Refresh Every
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="cacheExpiration" />
<label for="1w">1 Week</label>
<input type="radio" id="1d" :value="1000*60*60*24" v-model="cacheExpiration" />
<label for="1d">1 day</label>
<input type="radio" id="30m" :value="1000*60*30" v-model="cacheExpiration" />
<label for="30m">30 minutes</label>
<input type="radio" id="10m" :value="1000*60*10" v-model="cacheExpiration" />
<label for="10m">10 minutes</label>
<input type="radio" id="1m" :value="1000*60" v-model="cacheExpiration" />
<label for="1m">1 Minute</label>
</span>
</section>
</template>
<script>
export default defineComponent({
name: 'FooterComponent',
components: {},
props : {
},
updated : {
},
data() {
return {
preferences,
refreshData
}
}
</script>

View File

@@ -92,8 +92,10 @@ export default {
<style>
.leaflet-container {
margin-top:37px;
margin:0;
padding:0;
height:250px !important;
width:100%;
}
.leaflet-control-zoom {
display: none !important;

View File

@@ -7,7 +7,6 @@
:minZoom="zoom"
:maxZoom="zoom"
:zoomControl="false"
style="height:50vh"
>
<l-tile-layer
@@ -102,8 +101,7 @@ export default {
.leaflet-container {
margin:0;
height:250px !important;
width:1000%;
width:100%;
}
.leaflet-control-zoom {
display: none !important;

View File

@@ -1,8 +1,9 @@
<template>
<nav class="menu">
<ul>
<li><a href="#">add relay</a></li>
<li><a href="#">github</a></li>
<router-link :to="`/`" active-class="active">Home</router-link>
<router-link :to="`/status`" active-class="active">Grouped</router-link>
<a href="https://github.com/dskvr/nostr-watch/edit/main/relays.yaml" target="_blank">Submit</a>
</ul>
</nav>
</template>

View File

@@ -0,0 +1,274 @@
<template>
<tr :class="getHeadingClass()">
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
<div class="modal__content">
<span>
{{ queryJson(section) }}
</span>
</div>
</vue-final-modal>
<td colspan="11">
<h2><span class="indicator badge">{{ query(section).length }}</span>{{ section }} <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
</td>
</tr>
<tr :class="getHeadingClass()" v-if="query(section).length > 0">
<th class="table-column status-indicator"></th>
<th class="table-column relay"></th>
<th class="table-column verified">
<span class="verified-shape-wrapper">
<span class="shape verified"></span>
</span>
</th>
<th class="table-column location" v-tooltip:top.tooltip="Ping">
🌎
</th>
<th class="table-column latency" v-tooltip:top.tooltip="'Relay Latency on Read'">
</th>
<th class="table-column connect" v-tooltip:top.tooltip="'Relay connection status'">
🔌
</th>
<th class="table-column read" v-tooltip:top.tooltip="'Relay read status'">
👁🗨
</th>
<th class="table-column write" v-tooltip:top.tooltip="'Relay write status'">
</th>
<th class="table-column info" v-tooltip:top.tooltip="'Additional information detected regarding the relay during processing'">
</th>
<!-- <th class="table-column nip nip-20" v-tooltip:top.tooltip="'Does the relay support NIP-20'">
<span>NIP-11</span>
</th> -->
</tr>
<tr v-for="relay in query(section)" :key="{relay}" :class="getResultClass(relay)" class="relay">
<RelaySingleComponent
:relay="relay"
:result="result[relay]"
:geo="geo[relay]"
:showColumns="showColumns"
:connection="connections[relay]"
/>
</tr>
</template>
<script>
import { defineComponent} from 'vue'
import RelaySingleComponent from './RelaySingleComponent.vue'
import { VueFinalModal } from 'vue-final-modal'
export default defineComponent({
name: 'RelayListComponent',
components: {
RelaySingleComponent,
VueFinalModal
},
props: {
showJson: {
type: Boolean,
default(){
return true
}
},
section: {
type: String,
required: true,
default: ""
},
relays:{
type: Object,
default(){
return {}
}
},
result: {
type: Object,
default(){
return {}
}
},
geo: {
type: Object,
default(){
return {}
}
},
messages: {
type: Object,
default(){
return {}
}
},
alerts: {
type: Object,
default(){
return {}
}
},
connections: {
type: Object,
default(){
return {}
}
},
showColumns: {
type: Object,
default() {
return {
connectionStatuses: false,
nips: false,
geo: false,
additionalInfo: false
}
}
},
grouping: {
type: Boolean,
default(){
return true
}
}
},
data() {
return {
showModal: false
}
},
mounted(){
console.log('')
},
computed: {},
methods: {
getHeadingClass(){
return {
online: this.section != "offline",
public: this.section == "public",
offline: this.section == "offline",
restricted: this.section == "restricted"
}
},
getResultClass (relay) {
return {
loaded: this.result?.[relay]?.state == 'complete',
online: this.section != "offline",
offline: this.section == "offline",
public: this.section == "public"
}
},
query (aggregate) {
let unsorted,
sorted,
filterFn
filterFn = (relay) => this.grouping ? this.result?.[relay]?.aggregate == aggregate : true
unsorted = this.relays.filter(filterFn);
console.log('unsorted', unsorted)
console.log('isDone', this.isDone())
if(!this.isDone()) {
return unsorted
}
if (unsorted.length) {
sorted = unsorted.sort((relay1, relay2) => {
return this.result?.[relay1]?.latency.final - this.result?.[relay2]?.latency.final
})
console.log('sorted', sorted)
return sorted
}
return []
},
queryJson(aggregate){
const relays = this.query(aggregate)
const result = {}
result.relays = relays.map( relay => relay )
return JSON.stringify(result,null,'\t')
},
relaysTotal () {
return this.relays.length
},
relaysConnected () {
return Object.keys(this.result).length
},
relaysCompleted () {
let value = Object.entries(this.result).map((value) => { return value.state == 'complete' }).length
console.log('relaysCompleted', value)
return value
},
isDone(){
console.log('isDone()', this.relaysTotal(), '-', this.relaysCompleted(), '=', this.relaysTotal()-this.relaysCompleted() )
return this.relaysTotal()-this.relaysCompleted() == 0
},
}
})
</script>
<style lang='css' scoped>
.nip span {
text-transform: uppercase;
letter-spacing:-1px;
font-size:12px;
}
.section-json {
font-size:13px;
color: #555;
cursor:pointer;
}
::v-deep(.modal-container) {
display: flex;
justify-content: center;
align-items: center;
}
::v-deep(.modal-content) {
position: relative;
display: flex;
flex-direction: column;
max-height: 90%;
max-width:400px;
margin: 0 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.25rem;
background: #fff;
}
.modal__title {
margin: 0 2rem 0 0;
font-size: 1.5rem;
font-weight: 700;
}
.modal__content {
flex-grow: 1;
overflow-y: auto;
}
.modal__action {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
padding: 1rem 0 0;
}
.modal__close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.nip-11 a { cursor: pointer }
</style>
<style scoped>
.dark-mode div ::v-deep(.modal-content) {
border-color: #2d3748;
background-color: #1a202c;
}
</style>

View File

@@ -1,17 +1,17 @@
<template>
<tr :class="getHeadingClass()">
<tr>
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
<div class="modal__content">
<span>
{{ queryJson(section) }}
{{ queryJson() }}
</span>
</div>
</vue-final-modal>
<td colspan="11">
<h2><span class="indicator badge">{{ query(section).length }}</span>{{ section }} <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
<h2><span class="indicator badge">{{ this.relays.length }}</span>Relays <a @click="showModal=true" class="section-json" v-if="showJson">{...}</a></h2>
</td>
</tr>
<tr :class="getHeadingClass()" v-if="query(section).length > 0">
<tr v-if="this.relays.length > 0">
<th class="table-column status-indicator"></th>
<th class="table-column relay"></th>
@@ -43,12 +43,11 @@
<span>NIP-11</span>
</th> -->
</tr>
<tr v-for="relay in query(section)" :key="{relay}" :class="getResultClass(relay)" class="relay">
<tr v-for="relay in sortByLatency()" :key="{relay}" class="relay" :class="getResultClass(relay)">
<RelaySingleComponent
:relay="relay"
:result="result[relay]"
:geo="geo[relay]"
:showColumns="showColumns"
:connection="connections[relay]"
/>
</tr>
@@ -74,11 +73,6 @@ export default defineComponent({
return true
}
},
section: {
type: String,
required: true,
default: "public"
},
relays:{
type: Object,
default(){
@@ -115,17 +109,6 @@ export default defineComponent({
return {}
}
},
showColumns: {
type: Object,
default() {
return {
connectionStatuses: false,
nips: false,
geo: false,
additionalInfo: false
}
}
}
},
data() {
return {
@@ -137,61 +120,91 @@ export default defineComponent({
},
computed: {},
methods: {
getHeadingClass(){
return {
online: this.section != "offline",
public: this.section == "public",
offline: this.section == "offline",
restricted: this.section == "restricted"
}
},
// getHeadingClass(){
// return {
// online: this.section != "offline",
// public: this.section == "public",
// offline: this.section == "offline",
// restricted: this.section == "restricted"
// }
// },
getResultClass (relay) {
return {
loaded: this.result?.[relay]?.state == 'complete',
online: this.section != "offline",
offline: this.section == "offline",
public: this.section == "public"
loaded: this.result?.[relay]?.state == 'complete'
}
},
query (aggregate) {
let unordered,
filterFn
filterFn = (relay) => this.result?.[relay]?.aggregate == aggregate
unordered = this.relays.filter(filterFn);
if(!this.isDone()) {
return unordered
sort_by_latency(ascending) {
const self = this
return function (a, b) {
// equal items sort equally
if (self.result?.[a]?.latency.final === self.result?.[b]?.latency.final) {
return 0;
}
if (unordered.length) {
return unordered.sort((relay1, relay2) => {
return this.result?.[relay1]?.latency.final - this.result?.[relay2]?.latency.final
})
// nulls sort after anything else
if (self.result?.[a]?.latency.final === null) {
return 1;
}
if (self.result?.[b]?.latency.final === null) {
return -1;
}
// otherwise, if we're ascending, lowest sorts first
if (ascending) {
return self.result?.[a]?.latency.final - self.result?.[b]?.latency.final;
}
// if descending, highest sorts first
return self.result?.[b]?.latency.final-self.result?.[a]?.latency.final;
};
},
sortByLatency () {
let unsorted
unsorted = this.relays;
console.log('unsorted', unsorted)
// console.log('isDone', this.isDone())
// if(!this.isDone())
// return unsorted
if (unsorted.length)
return unsorted.sort(this.sort_by_latency(true))
return []
},
queryJson(aggregate){
const relays = this.query(aggregate)
const result = {}
result.relays = relays.map( relay => relay )
queryJson(){
const result = { relays: this.relays }
return JSON.stringify(result,null,'\t')
},
relaysTotal () {
return this.relays.length
return this.relays.length //TODO: Figure out WHY?
},
relaysConnected () {
return Object.keys(this.result).length
return Object.entries(this.result).length
},
relaysCompleted () {
let value = Object.entries(this.result).length
return value
relaysComplete () {
if(!Object.keys(this.results).length) return 0
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
return hash
},
isDone(){
return this.relaysTotal()-this.relaysCompleted() == 0
console.log('is done', this.relaysTotal(), '-', this.relaysComplete(), '<=', 0, this.relaysTotal()-this.relaysComplete() <= 0)
return this.relaysTotal()-this.relaysComplete() <= 0
},
loadingComplete(){
return this.isDone() ? 'loaded' : ''
}
}
})
</script>

View File

@@ -1,24 +1,23 @@
<template>
<td class="status-indicator" :key="generateKey(relay, 'aggregate')">
<span :class="result.aggregate" class="aggregate indicator">
<span :class="result?.aggregate" class="aggregate indicator">
<span></span>
<span></span>
</span>
</td>
<td class="relay left-align relay-url">
<router-link :to="`/${relayClean(relay)}`" active-class="active">{{ relay }}</router-link>
<!-- <span @click="copy(relay)" v-tooltip:top.tooltip="'Click to copy'">{{ relay }}</span> -->
<router-link :to="`/relay/${relayClean(relay)}`" active-class="active">{{ relay }}</router-link>
</td>
<td class="verified">
<span v-tooltip:top.tooltip="identityList()"> <span class="verified-shape-wrapper" v-if="Object.entries(result.identities).length"><span class="shape verified"></span></span></span>
<td class="verified" v-if="result?.identities">
<span v-tooltip:top.tooltip="identityList()"> <span class="verified-shape-wrapper" v-if="Object.entries(result?.identities).length"><span class="shape verified"></span></span></span>
</td>
<td class="location">{{ getFlag() }}</td>
<td class="latency">
<span>{{ result.latency.final }}<span v-if="result.check.latency">ms</span></span>
<span>{{ result?.latency.final }}<span v-if="result?.check.latency">ms</span></span>
</td>
<td class="connect" :key="generateKey(relay, 'check.connect')">
@@ -34,8 +33,8 @@
</td>
<td class="info">
<ul v-if="result.observations && result.observations.length">
<li class="observation" v-for="(alert) in result.observations" :key="generateKey(relay, alert.description)">
<ul v-if="result?.observations && result?.observations.length">
<li class="observation" v-for="(alert) in result?.observations" :key="generateKey(relay, alert.description)">
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'notice'"></span>
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'caution'"></span>
</li>
@@ -43,18 +42,18 @@
</td>
<!-- <td class="nip nip-11">
<a v-if="result.info" @click="showModal=true"> </a>
<a v-if="result?.info" @click="showModal=true"> </a>
</td> -->
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
<div class="modal__title">
<span>{{ result.info?.name }}</span>
<span>{{ result?.info?.name }}</span>
</div>
<div class="modal__content">
<div v-if="result.info?.description">
{{ result.info?.description }} <br/>
<strong v-if="result.info?.pubkey">Public Key:</strong> {{ result.info?.pubkey }} <br/>
<strong v-if="result.info?.contact">Contact:</strong> <SafeMail :email="result.info?.contact" v-if="result.info?.contact" />
<div v-if="result?.info?.description">
{{ result?.info?.description }} <br/>
<strong v-if="result?.info?.pubkey">Public Key:</strong> {{ result?.info?.pubkey }} <br/>
<strong v-if="result?.info?.contact">Contact:</strong> <SafeMail :email="result?.info?.contact" v-if="result?.info?.contact" />
</div>
<div>
<h4>Status</h4>
@@ -66,12 +65,12 @@
</div>
<h4>Relay Info</h4>
<ul>
<li><strong>Software:</strong> {{ result.info?.software }} </li>
<li><strong>Version</strong>: {{ result.info?.version }} </li>
<li><strong>Software:</strong> {{ result?.info?.software }} </li>
<li><strong>Version</strong>: {{ result?.info?.version }} </li>
</ul>
<h4>NIP Support</h4>
<ul>
<li v-for="(nip) in result.info?.supported_nips" :key="`${relay}_${nip}`">
<li v-for="(nip) in result?.info?.supported_nips" :key="`${relay}_${nip}`">
<a :href="nipLink(nip)" target="_blank">{{ nipFormatted(nip) }}</a>
</li>
</ul>
@@ -168,14 +167,16 @@ export default defineComponent({
identityList () {
let string = '',
extraString = '',
users = Object.entries(this.result.identities),
users = Object.entries(this.result?.identities),
count = 0
console.log(this.result.uri, 'admin', this.result.identities.serverAdmin)
// if(!this.result?.identities) return
if(this.result.identities) {
if(this.result.identities.serverAdmin) {
string = `Relay has registered an administrator pubkey: ${this.result.identities.serverAdmin}. `
console.log(this.result?.uri, 'admin', this.result?.identities.serverAdmin, this.result.info)
if(this.result?.identities) {
if(this.result?.identities.serverAdmin) {
string = `Relay has registered an administrator pubkey: ${this.result?.identities.serverAdmin}. `
extraString = "Additionally, "
}
@@ -277,4 +278,9 @@ td.verified span {
border-color: #2d3748;
background-color: #1a202c;
}
.restricted.aggregate.indicator {
position:relative;
left:-7px;
}
</style>

View File

@@ -213,6 +213,11 @@ export default defineComponent({
return Object.entries(this.result).length
},
relaysComplete () {
if(!Object.keys(this.results).length) return 0
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
// //console.log(message, ':', hash)
@@ -220,7 +225,7 @@ export default defineComponent({
},
isDone(){
return this.relaysTotal()-this.relaysConnected() == 0
return this.relaysTotal()-this.relaysComplete() == 0
},
loadingComplete(){

View File

@@ -1,13 +1,18 @@
import { createApp } from 'vue'
import App from './App.vue'
import Vue3Storage from "vue3-storage";
import router from './router'
import "./styles/main.scss"
import directives from "./directives/"
import titleMixin from './mixins/titleMixin'
import {Tabs, Tab} from 'vue3-tabs-component';
const app = createApp(App)
app
.use(router)
.use(Vue3Storage, { namespace: "nostrwatch_" })
.component('tabs', Tabs)
.component('tab', Tab)
.mixin(titleMixin)
directives(app);

343
src/pages/ByStatus.vue Normal file
View File

@@ -0,0 +1,343 @@
<template>
<!-- <NavComponent /> -->
<LeafletComponent
:geo="geo"
:result="result"
/>
<div id="wrapper" :class="loadingComplete()">
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<h1>nostr.watch<sup>{{version}}</sup></h1>
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</column>
</row>
<div>
<table>
<RelayGroupedListComponent
section="public"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayGroupedListComponent
section="restricted"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayGroupedListComponent
section="offline"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayGroupedListComponent
section="processing"
:relays="relays"
:result="result"
:messages="messages"
:alerts="alerts"
:connections="connections"
:showJson="false"
/>
</table>
</div>
<!-- <row container :gutter="12">
<column :xs="12" :md="12" :lg="12">
</column>
</row> -->
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12">
<div class="block">
<table>
</table>
</div>
</column>
</row>
<!-- <row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="processing-card loading">
<span v-if="(relaysTotal()-relaysConnected()>0)">Processing {{ relaysConnected() }}/{{ relaysTotal() }}</span>
</column>
</row> -->
<span class="credit"><a href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>, built with <a href="https://github.com/jb55/nostr-js">nostr-js</a> and <a href="https://github.com/dskvr/nostr-relay-inspector">nostr-relay-inspector</a>, inspired by <a href="https://github.com/fiatjaf/nostr-relay-registry">nostr-relay-registry</a></span>
</div>
</template>
<script>
import { defineComponent} from 'vue'
import { useStorage } from "vue3-storage";
// import { CallbackResult } from "vue3-storage/dist/lib/types";
import crypto from "crypto"
import { Row, Column } from 'vue-grid-responsive';
import { Inspector, InspectorObservation, InspectorResult } from 'nostr-relay-inspector'
// import RelayListComponent from '../components/RelayListComponent.vue'
import RelayGroupedListComponent from '../components/RelayGroupedListComponent.vue'
import LeafletComponent from '../components/LeafletComponent.vue'
import NavComponent from '../components/NavComponent.vue'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../geo.yaml'
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../../codes.yaml'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'RelayTableComponent',
components: {
Row,
Column,
// RelayListComponent,
RelayGroupedListComponent,
LeafletComponent,
NavComponent
},
data() {
return {
relays,
result: {},
messages: {},
connections: {},
nips: {},
alerts: {},
timeouts: {},
intervals: {},
lastPing: Date.now(),
nextPing: Date.now() + (30*60*1000),
count: 0,
storage: null,
geo,
version: version,
hasStorage: false,
lastUpdate: null,
cacheExpiration: (30*60*1000),
}
},
updated() {
Object.keys(this.timeouts).forEach(timeout => clearTimeout(this.timeouts[timeout]))
Object.keys(this.intervals).forEach(interval => clearInterval(this.intervals[interval]))
},
async mounted() {
this.relays.forEach(relay => {
this.result[relay] = structuredClone(InspectorResult)
})
this.storage = useStorage()
this.storage.setStorageSync('relays', relays)
this.lastUpdate = this.storage.getStorageSync('lastUpdate')
this.relays.forEach(async relay => {
this.result[relay] = this.storage.getStorageSync(relay)
})
console.log('meow', this.result)
if(Object.keys(this.result).length)
this.hasStorage = true
if(this.isExpired())
this.relays.forEach(async relay => await this.check(relay) )
console.log('zzz', this.result)
// this.relays.forEach( relay => {
// // this.result[relay].state = 'complete'
// // this.setAggregateResult(relay)
// // this.adjustResult(relay)
// console.log('boom', relay, this.result[relay])
// })
// }
// console.log(`zing ${Date.now()} - ${this.lastUpdate} = ${Date.now()-this.lastUpdate} > ${60*1000}`)
return true
},
// head: {
// // creates a title tag in header.
// base () {
// return {
// href: "/"
// }
// }
// },
computed: {},
methods: {
isExpired(){
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.cacheExpiration
},
saveState(relay){
this.storage
.setStorage({
key: relay,
data: this.result[relay]
})
.then(successCallback => {
console.log(successCallback.errMsg);
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
this.storage
.setStorage({
key: "lastUpdate",
data: Date.now()
})
.then(successCallback => {
console.log(successCallback.errMsg);
this.lastUpdate = Date.now()
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
},
resetState(){
this.relays.forEach(relay=>{
this.storage.removeStorage(relay)
})
},
async check(relay){
return new Promise( (resolve, reject) => {
const opts = {
checkLatency: true,
setIP: false,
setGeo: false,
debug: true,
}
let inspect = new Inspector(relay, opts)
.on('run', (result) => {
result
// result.aggregate = 'processing'
})
.on('open', (e, result) => {
this.result[relay] = result
})
.on('complete', (instance) => {
this.result[relay] = Object.assign(this.result[relay], instance.result)
this.messages[relay] = instance.inbox
// this.setFlag(relay)
this.setAggregateResult(relay)
// this.adjustResult(relay)
this.saveState(relay)
resolve(this.result[relay])
})
.on('notice', (notice) => {
const hash = this.sha1(notice)
let message_obj = RELAY_MESSAGES[hash]
let code_obj = RELAY_CODES[message_obj.code]
let response_obj = {...message_obj, ...code_obj}
this.result[relay].observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) )
})
.on('close', () => {})
.on('error', () => {
reject(this.result[relay])
})
.run()
this.connections[relay] = inspect
})
},
recheck(relay){
const inspect = this.connections[relay]
inspect.checkLatency()
},
// adjustResult (relay) {
// this.result[relay].observations.forEach( observation => {
// if (observation.code == "BLOCKS_WRITE_STATUS_CHECK") {
// this.result[relay].check.write = false
// this.result[relay].aggregate = 'public'
// }
// })
// },
setAggregateResult (relay) {
let aggregateTally = 0
aggregateTally += this.result?.[relay]?.check.connect ? 1 : 0
aggregateTally += this.result?.[relay]?.check.read ? 1 : 0
aggregateTally += this.result?.[relay]?.check.write ? 1 : 0
if (aggregateTally == 3) {
this.result[relay].aggregate = 'public'
}
else if (aggregateTally == 0) {
this.result[relay].aggregate = 'offline'
}
else {
this.result[relay].aggregate = 'restricted'
}
},
relaysTotal () {
return this.relays.length //TODO: Figure out WHY?
},
relaysConnected () {
return Object.entries(this.result).filter(result => result.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
return hash
},
isDone(){
return this.relaysTotal()-this.relaysConnected() <= 0
},
loadingComplete(){
return this.isDone() ? 'loaded' : ''
},
},
})
</script>

View File

@@ -1,5 +1,10 @@
<template>
<!-- <NavComponent /> -->
<LeafletComponent
:geo="geo"
:result="result"
/>
<div id="wrapper" :class="loadingComplete()">
<row container :gutter="12">
@@ -9,21 +14,15 @@
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12">
<LeafletComponent
:geo="geo"
:result="result"
/>
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12">
<div class="block">
<column :xs="12" :md="12" :lg="12" class="title-card">
<table>
<RelayListComponent
section="public"
:relays="relays"
:result="result"
:geo="geo"
@@ -31,71 +30,75 @@
:alerts="alerts"
:connections="connections"
/>
<RelayListComponent
section="restricted"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayListComponent
section="offline"
:relays="relays"
:result="result"
:geo="geo"
:messages="messages"
:alerts="alerts"
:connections="connections"
/>
<RelayListComponent
section="processing"
:relays="relays"
:result="result"
:messages="messages"
:alerts="alerts"
:connections="connections"
:showJson="false"
/>
</table>
</div>
</column>
</row>
<row container :gutter="12">
<row container :gutter="12" v-if="(relaysTotal()-relaysConnected()>0)">
<column :xs="12" :md="12" :lg="12" class="processing-card loading">
<span v-if="(relaysTotal()-relaysConnected()>0)">Processing {{ relaysConnected() }}/{{ relaysTotal() }}</span>
<span>Processing {{ relaysConnected() }}/{{ relaysTotal() }}</span>
</column>
</row>
<section id="footer">
<span>Updated {{ refreshData.sinceLast }} ago</span>
<span><button @click="invalidate(true)">Update Now</button></span>
<span><input type="checkbox" id="checkbox" v-model="preferences.refresh" /><label for="">Refresh Automatically</label></span>
<span v-if="preferences.refresh"> Next refresh in: {{ refreshData.untilNext }}</span>
<span v-if="preferences.refresh">
Refresh Every
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="cacheExpiration" />
<label for="1w">1 Week</label>
<input type="radio" id="1d" :value="1000*60*60*24" v-model="cacheExpiration" />
<label for="1d">1 day</label>
<input type="radio" id="30m" :value="1000*60*30" v-model="cacheExpiration" />
<label for="30m">30 minutes</label>
<input type="radio" id="10m" :value="1000*60*10" v-model="cacheExpiration" />
<label for="10m">10 minutes</label>
<input type="radio" id="1m" :value="1000*60" v-model="cacheExpiration" />
<label for="1m">1 Minute</label>
</span>
</section>
<span class="credit"><a href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>, built with <a href="https://github.com/jb55/nostr-js">nostr-js</a> and <a href="https://github.com/dskvr/nostr-relay-inspector">nostr-relay-inspector</a>, inspired by <a href="https://github.com/fiatjaf/nostr-relay-registry">nostr-relay-registry</a></span>
</div>
</template>
<script>
import { defineComponent} from 'vue'
import RelayListComponent from '../components/RelayListComponent.vue'
import LeafletComponent from '../components/LeafletComponent.vue'
// import NavComponent from './NavComponent.vue'
import { defineComponent, reactive} from 'vue'
import { useStorage } from "vue3-storage";
// import { CallbackResult } from "vue3-storage/dist/lib/types";
import crypto from "crypto"
import { Row, Column } from 'vue-grid-responsive';
import { Inspector, InspectorObservation } from 'nostr-relay-inspector'
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
import RelayListComponent from '../components/RelayListComponent.vue'
// import RelayGroupedListComponent from '../components/RelayGroupedListComponent.vue'
import LeafletComponent from '../components/LeafletComponent.vue'
import NavComponent from '../components/NavComponent.vue'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../geo.yaml'
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../../codes.yaml'
import { Inspector, InspectorObservation } from 'nostr-relay-inspector'
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
import { timeSince } from '../utils'
import crypto from "crypto"
reactive
export default defineComponent({
title: "nostr.watch registry & network status",
@@ -104,59 +107,241 @@ export default defineComponent({
Row,
Column,
RelayListComponent,
LeafletComponent
// NavComponent
// RelayGroupedListComponent,
LeafletComponent,
NavComponent
},
data() {
return {
relays,
relays: relays,
result: {},
messages: {},
connections: {},
nips: {},
alerts: {},
timeouts: {},
intervals: {},
lastPing: Date.now(),
nextPing: Date.now() + (60*1000),
count: 0,
storage: null,
geo,
version: version
version: version,
hasStorage: false,
lastUpdate: null,
refresh: true,
preferences: {
refresh: true
},
refreshData: {},
// cacheExpiration: 10*60*1000, //10 minutes
cacheExpiration: 30*60*1000, //30 minutes
}
},
updated(){
console.log(`zing - refresh? ${this.preferences.refresh} ${Date.now()} - ${this.lastUpdate} = ${Date.now()-this.lastUpdate} > ${this.cacheExpiration}`)
this.saveState('preferences')
Object.keys(this.timeouts).forEach(timeout => clearTimeout(this.timeouts[timeout]))
Object.keys(this.intervals).forEach(interval => clearInterval(this.intervals[interval]))
if(this.isDone()) {
this.saveState('lastUpdate')
console.log('isDone()', this.getState('lastUpdate') )
}
this.refreshData.untilNext = this.timeUntilRefresh()
this.refreshData.sinceLast = this.timeSinceRefresh()
// if(this.preferences.refresh)
// this.timeouts.invalidate = setTimeout(()=> this.invalidate(), 1000)
},
async mounted() {
this.relays.forEach(relay => {
this.check(relay)
this.storage = useStorage()
this.lastUpdate = this.getState('lastUpdate')|| this.lastUpdate
this.preferences = this.getState('preferences') || this.preferences
this.relays.forEach(async relay => {
this.result[relay] = this.getState(relay)
this.messages[relay] = this.getState(`${relay}_inbox`)
})
this.invalidate()
console.log('last update',-1*(Date.now()-(this.lastUpdate+this.cacheExpiration)))
this.refreshData = reactive({
untilNext: this.timeUntilRefresh(),
sinceLast: this.timeSinceRefresh()
})
setInterval(() => {
this.refreshData.untilNext = this.timeUntilRefresh()
this.refreshData.sinceLast = this.timeSinceRefresh()
console.log('timesince22222', this.refreshData.untilNext, this.refreshData.sinceLast)
if(this.isExpired())
this.invalidate()
}, 1000)
},
computed: {
},
computed: {},
methods: {
check(relay){
timeUntilRefresh(){
return timeSince(Date.now()-(this.lastUpdate+this.cacheExpiration-Date.now()))
},
timeSinceRefresh(){
return timeSince(this.lastUpdate)
},
invalidate(force){
if(!this.isExpired() && !force)
return
this.relays.forEach(async relay => {
await this.check(relay)
this.relays[relay] = this.getState(relay)
this.messages[relay] = this.getState(`${relay}_inbox`)
})
// if(this.preferences.refresh)
// this.timeouts.invalidate = setTimeout(()=> this.invalidate(), 1000)
},
nextRefresh(){
return timeSince(Date.now()-(this.lastUpdate+this.cacheExpiration-Date.now()))
},
isExpired(){
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.cacheExpiration
},
getState(key){
return this.storage.getStorageSync(key)
},
saveState(type, key, data){
const now = Date.now()
let store, success, error, instance
switch(type){
case 'relay':
console.log('savestate', 'relay', data || this.result[data])
if(data)
data.aggregate = this.getAggregate(key)
store = {
key: key,
data: data || this.result[key],
// expire: Date.now()+1000*60*60*24*180,
}
success = () => {
if(data)
this.result[key] = data
}
break;
case 'messages':
console.log('savestate', 'messages', this.messages[data])
store = {
key: `${key}_inbox`,
data: data || this.messages[key],
// expire: Date.now()+1000*60*60*24*180,
}
success = () => {
if(data)
this.messages[key] = data
}
break;
case 'lastUpdate':
console.log('savestate', 'lastUpdate', now)
store = {
key: "lastUpdate",
data: now
}
success = () => {
// console.log('lastupdate success', successCallback.msg)
this.lastUpdate = now
}
break;
case 'preferences':
console.log('savestate', 'preferences', this.preferences)
store = {
key: "preferences",
data: this.preferences
}
break;
}
if(store)
instance = this.storage.setStorage(store)
if(success && store)
instance.then(success)
if(error && store)
instance.catch(error)
},
resetState(){
this.relays.forEach(relay=>{
this.storage.removeStorage(relay)
})
},
async check(relay){
return new Promise( (resolve, reject) => {
// if(!this.isExpired())
// return reject(relay)
const opts = {
checkLatency: true,
setIP: false,
setGeo: false,
getInfo: true,
debug: true,
// data: { result: this.result[relay] }
}
let inspect = new Inspector(relay, opts)
.on('run', (result) => {
result.aggregate = 'processing'
})
.on('open', (e, result) => {
this.result[relay] = result
})
// .on('run', (result) => {
// result.aggregate = 'processing'
// })
// .on('open', (e, result) => {
// this.result[relay] = result
// })
.on('complete', (instance) => {
// console.log('getinfo()', instance.result.info)
this.result[relay] = instance.result
this.messages[relay] = instance.inbox
// this.setFlag(relay)
this.setAggregateResult(relay)
this.adjustResult(relay)
// this.adjustResult(relay)
this.result[relay].aggregate = this.getAggregate(relay)
this.saveState('relay', relay)
this.saveState('messages', relay, instance.inbox)
this.saveState('lastUpdate')
resolve(this.result[relay])
})
.on('notice', (notice) => {
const hash = this.sha1(notice)
@@ -166,15 +351,21 @@ export default defineComponent({
let response_obj = {...message_obj, ...code_obj}
this.result[relay].observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) )
})
.on('close', () => {})
.on('error', () => {
reject(this.result[relay])
})
.run()
this.connections[relay] = inspect
})
},
recheck(relay){
const inspect = this.connections[relay]
inspect.checkLatency()
},
adjustResult (relay) {
@@ -186,19 +377,20 @@ export default defineComponent({
})
},
setAggregateResult (relay) {
getAggregate (relay) {
console.log('getAggregate()', this.result?.[relay]?.check.connect, this.result?.[relay]?.check.read, this.result?.[relay]?.check.write)
let aggregateTally = 0
aggregateTally += this.result?.[relay]?.check.connect ? 1 : 0
aggregateTally += this.result?.[relay]?.check.read ? 1 : 0
aggregateTally += this.result?.[relay]?.check.write ? 1 : 0
if (aggregateTally == 3) {
this.result[relay].aggregate = 'public'
return 'public'
}
else if (aggregateTally == 0) {
this.result[relay].aggregate = 'offline'
return 'offline'
}
else {
this.result[relay].aggregate = 'restricted'
return 'restricted'
}
},
@@ -210,18 +402,48 @@ export default defineComponent({
return Object.entries(this.result).length
},
relaysComplete () {
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
return hash
},
isDone(){
return this.relaysTotal()-this.relaysConnected() == 0
console.log('is done', this.relaysTotal(), '-', this.relaysConnected(), '<=', 0, this.relaysTotal()-this.relaysConnected() <= 0)
return this.relaysTotal()-this.relaysComplete() <= 0
},
loadingComplete(){
return this.isDone() ? 'loaded' : ''
},
timeSince(date) {
var seconds = Math.floor((new Date() - date) / 1000);
var interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
},
},
})

View File

@@ -7,13 +7,22 @@
<!-- <NavComponent /> -->
<div id="wrapper">
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<h1>{{ relayUrl() }}</h1>
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<NavComponent />
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="12" :lg="12" class="title-card">
<div style="display: none">{{result}}</div> <!-- ? -->
<h1>{{ relayUrl() }}</h1>
<!-- <h2>nostr.watch<sup>{{version}}</sup></h2> -->
<br >
<span class="badges">
@@ -92,32 +101,6 @@
<div style="display: none">{{result}}</div> <!-- ? -->
</column>
</row>
<row container :gutter="12">
<column :xs="12" :md="6" :lg="6">
<div style="display: none">{{result}}</div> <!-- ? -->
</column>
<column :xs="12" :md="6" :lg="6">
<!-- <h4 v-if="result.info?.supported_nips">NIP Support</h4> -->
</column>
</row>
@@ -127,26 +110,29 @@
</template>
<script>
import { defineComponent} from 'vue'
import { useStorage } from "vue3-storage";
import LeafletSingleComponent from '../components/LeafletSingleComponent.vue'
// import NavComponent from './NavComponent.vue'\
import SafeMail from "@2alheure/vue-safe-mail";
import { countryCodeEmoji } from 'country-code-emoji';
import emoji from 'node-emoji';
import NavComponent from '../components/NavComponent.vue'
import { Row, Column } from 'vue-grid-responsive';
import SafeMail from "@2alheure/vue-safe-mail";
import emoji from 'node-emoji';
import { countryCodeEmoji } from 'country-code-emoji';
// import { Inspector, InspectorObservation } from 'nostr-relay-inspector'
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector'
import { version } from '../../package.json'
import { relays } from '../../relays.yaml'
import { geo } from '../../geo.yaml'
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../../codes.yaml'
import { Inspector, InspectorObservation } from 'nostr-relay-inspector'
/* import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector' */
import crypto from "crypto"
@@ -157,8 +143,8 @@ export default defineComponent({
Row,
Column,
LeafletSingleComponent,
// NavComponent
SafeMail
NavComponent,
SafeMail,
},
data() {
@@ -169,28 +155,72 @@ export default defineComponent({
nips: {},
alerts: {},
timeouts: {},
intervals: {},
lastPing: Date.now(),
nextPing: Date.now() + (60*1000),
count: 0,
geo,
relay: "",
version: version
version: version,
storage: null,
lastUpdate: null,
cacheExpiration: 10*60*1000 //10 minutes
}
},
async mounted() {
console.log('mounted')
this.relay = this.relayUrl()
console.log('relay', this.relay)
this.storage = useStorage()
this.lastUpdate = this.storage.getStorageSync('lastUpdate')
this.result = this.storage.getStorageSync(this.relay)
if(this.isExpired())
this.check(this.relay)
console.log('relay is compete', this.relay, this.result.check)
// console.log('zing ', (Date.now() - this.lastUpdate) /1000)
},
computed: {
},
updated() {
Object.keys(this.timeouts).forEach(timeout => clearTimeout(this.timeouts[timeout]))
Object.keys(this.intervals).forEach(interval => clearInterval(this.intervals[interval]))
},
methods: {
isExpired(){
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.cacheExpiration
},
saveState(relay){
this.storage
.setStorage({
key: relay,
data: this.result
})
.then(successCallback => {
console.log(successCallback.errMsg);
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
this.storage
.setStorage({
key: "lastUpdate",
data: Date.now()
})
.then(successCallback => {
console.log(successCallback.errMsg);
this.lastUpdate = Date.now()
})
.catch(failCallback => {
console.log(failCallback.errMsg);
})
},
relayUrl() {
// We will see what `params` is shortly
return `wss://${this.$route.params.relayUrl}`
@@ -229,6 +259,7 @@ export default defineComponent({
/* this.adjustResult(relay) */
this.setResultClass('read')
this.setResultClass('write')
this.saveState(relay)
/* console.log(this.result)
console.log(this.result.info.supported_nips) */
/* resolve(this.result) */

View File

@@ -1,27 +1,34 @@
// /router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '../pages/HomePage.vue'
import ByStatus from '../pages/ByStatus.vue'
import SingleRelay from '../pages/SingleRelay.vue'
const routes = [
{
path: '/',
name: 'nostr.watch',
component: HomePage
path: '/relay/:relayUrl(.*)',
// name: 'nostr.watch - :relayUrl',
component: SingleRelay
},
{
path: '/status',
// name: 'nostr.watch',
component: ByStatus
},
// Added our new route file named profile.vue
{
path: '/:relayUrl',
name: 'nostr.watch - :relayUrl',
component: SingleRelay
}
path: '/',
// name: 'nostr.watch',
component: HomePage
},
]
// Create Vue Router Object
const router = createRouter({
history: createWebHistory(),
base: process.env.BASE_URL,
routes
// base: process.env.BASE_URL,
routes: routes
})
export default router

View File

@@ -328,3 +328,23 @@ tr.offline .location {
}
}
nav.menu a {
text-decoration: none;
display: inline-block;
margin: 0 22px 0 0;
padding:5px 10px;
color:#000;
border-bottom: 1px dotted #999;
}
nav.menu a.active {
background:#000;
color: #fff;
border: none;
}
nav.menu a:hover {
background: #f0f0f0;
}

24
src/utils/index.js Normal file
View File

@@ -0,0 +1,24 @@
export const timeSince = function(date) {
var seconds = Math.floor((new Date() - date) / 1000);
var interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}