dskvr
2 years ago
24 changed files with 5113 additions and 204 deletions
@ -0,0 +1,29 @@ |
|||
<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> |
|||
</script> |
@ -0,0 +1,109 @@ |
|||
<template> |
|||
|
|||
<l-map |
|||
ref="map" |
|||
v-model:zoom="zoom" |
|||
:center="[47.41322, -1.219482]" |
|||
:minZoom="zoom" |
|||
:maxZoom="zoom" |
|||
:zoomControl="false" |
|||
> |
|||
|
|||
<l-tile-layer |
|||
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" |
|||
layer-type="base" |
|||
name="OpenStreetMap" |
|||
/> |
|||
|
|||
<!-- <l-marker v-for="([relay, result]) in Object.entries(geo)" :lat-lng="getLatLng(result)" :key="relay"> |
|||
<l-popup> |
|||
{{ relay }} |
|||
</l-popup> |
|||
</l-marker> --> |
|||
|
|||
<l-circle-marker |
|||
:lat-lng="getLatLng()" |
|||
:radius="3" |
|||
:weight="6" |
|||
:color="getCircleColor(relay)" |
|||
:fillOpacity="1" |
|||
:class="relay" |
|||
> |
|||
<l-popup> |
|||
{{ relay }} |
|||
</l-popup> |
|||
</l-circle-marker> |
|||
</l-map> |
|||
|
|||
</template> |
|||
<script> |
|||
import "leaflet/dist/leaflet.css" |
|||
import { LMap, LTileLayer, LCircleMarker, LPopup } from "@vue-leaflet/vue-leaflet"; |
|||
export default { |
|||
components: { |
|||
LMap, |
|||
LTileLayer, |
|||
LCircleMarker, |
|||
LPopup |
|||
}, |
|||
methods: { |
|||
getLatLng(){ |
|||
console.log("geo", this.relay, this.geo[this.relay].lat, this.geo[this.relay].lon) |
|||
// if (!geo[this.relay]) console.log("no geo?", geo, this.relay, geo[this.relay]) |
|||
return [this.geo[this.relay].lat, this.geo[this.relay].lon] |
|||
}, |
|||
getCircleColor(relay){ |
|||
|
|||
if(this.result[relay]?.aggregate == 'public') { |
|||
return '#00AA00' |
|||
} |
|||
else if(this.result[relay]?.aggregate == 'restricted') { |
|||
return '#FFA500' |
|||
} |
|||
else if(this.result[relay]?.aggregate == 'offline') { |
|||
return '#FF0000' |
|||
} |
|||
return 'black' |
|||
} |
|||
}, |
|||
async mounted() { |
|||
console.log('GEO', this.geo[this.relay]) |
|||
}, |
|||
props: { |
|||
geo: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
result: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
relay: { |
|||
type: String, |
|||
default(){ |
|||
return "" |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
zoom: 2 |
|||
}; |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
.leaflet-container { |
|||
margin:0; |
|||
height:250px !important; |
|||
width:100%; |
|||
} |
|||
.leaflet-control-zoom { |
|||
display: none !important; |
|||
} |
|||
</style> |
@ -1,8 +1,53 @@ |
|||
<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">Add Relay</a> |
|||
<span> |
|||
<PreferencesComponent /> |
|||
</span> |
|||
</ul> |
|||
</nav> |
|||
</template> |
|||
<style scoped> |
|||
nav.menu { |
|||
position:relative; |
|||
z-index:10; |
|||
} |
|||
nav span, |
|||
nav.menu a { |
|||
display: inline-block; |
|||
} |
|||
|
|||
nav.menu a { |
|||
text-decoration: none; |
|||
|
|||
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; |
|||
} |
|||
|
|||
</style> |
|||
<script> |
|||
import { defineComponent } from 'vue' |
|||
import PreferencesComponent from '../components/PreferencesComponent.vue' |
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'NavComponent', |
|||
components: { |
|||
PreferencesComponent, |
|||
} |
|||
}); |
|||
</script> |
|||
|
@ -0,0 +1,114 @@ |
|||
<template> |
|||
<section id="preferences-toggle"> |
|||
<button @click="toggle">⚙️</button> |
|||
<section id="preferences" :class="{active:isActive}"> |
|||
<span><input type="checkbox" id="checkbox" v-model="preferences.refresh" /><label for="">Refresh Automatically</label></span> |
|||
<span v-if="preferences.refresh"> |
|||
Refresh Every |
|||
<ul> |
|||
<li> |
|||
<input type="radio" id="1w" :value="1000*60*60*24*7" v-model="preferences.cacheExpiration" /> |
|||
<label for="1w">1 Week</label> |
|||
</li> |
|||
<li> |
|||
<input type="radio" id="1d" :value="1000*60*60*24" v-model="preferences.cacheExpiration" /> |
|||
<label for="1d">1 day</label> |
|||
</li> |
|||
<li> |
|||
<input type="radio" id="30m" :value="1000*60*30" v-model="preferences.cacheExpiration" /> |
|||
<label for="30m">30 minutes</label> |
|||
</li> |
|||
<li> |
|||
<input type="radio" id="10m" :value="1000*60*10" v-model="preferences.cacheExpiration" /> |
|||
<label for="10m">10 minutes</label> |
|||
</li> |
|||
<li> |
|||
<input type="radio" id="1m" :value="1000*60" v-model="preferences.cacheExpiration" /> |
|||
<label for="1m">1 Minute</label> |
|||
</li> |
|||
</ul> |
|||
</span> |
|||
</section> |
|||
</section> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
section#preferences { |
|||
display:none; |
|||
} |
|||
section#preferences.active { |
|||
display:block; |
|||
} |
|||
|
|||
section#preferences-toggle { |
|||
position:relative; |
|||
text-align:left; |
|||
} |
|||
|
|||
section#preferences { |
|||
position:absolute; |
|||
top:40px; |
|||
left:0px; |
|||
padding: 5px 10px; |
|||
background:#f5f5f5; |
|||
width:200px; |
|||
|
|||
} |
|||
section#preferences > span { |
|||
display:block; |
|||
margin: 5px 0 0; |
|||
} |
|||
|
|||
ul, li { |
|||
list-style:none; |
|||
padding:0; |
|||
margin:0; |
|||
} |
|||
|
|||
button { |
|||
cursor: pointer; |
|||
padding:0; |
|||
margin:0; |
|||
background: transparent; |
|||
border: none; |
|||
} |
|||
</style> |
|||
|
|||
<script> |
|||
import { defineComponent } from 'vue' |
|||
import sharedMethods from '../shared' |
|||
import { useStorage } from "vue3-storage"; |
|||
|
|||
const localMethods = { |
|||
toggle() { |
|||
this.isActive = !this.isActive; |
|||
}, |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'PreferencesComponent', |
|||
components: {}, |
|||
mounted(){ |
|||
this.storage = useStorage() |
|||
this.preferences = this.getState('preferences') || this.preferences |
|||
}, |
|||
updated(){ |
|||
this.saveState('preferences') |
|||
}, |
|||
computed: {}, |
|||
methods: Object.assign(localMethods, sharedMethods), |
|||
props: {}, |
|||
data() { |
|||
return { |
|||
storage: null, |
|||
refresh: true, |
|||
preferences: { |
|||
refresh: true, |
|||
cacheExpiration: 30*60*1000 |
|||
}, |
|||
isActive: false, |
|||
} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
@ -0,0 +1,105 @@ |
|||
<template> |
|||
<section id="refresh"> |
|||
<span>Updated {{ refreshData?.sinceLast }} ago <button @click="invalidate(true)">Update Now</button></span> |
|||
<span v-if="preferences.refresh"> Next refresh in: {{ refreshData?.untilNext }}</span> |
|||
</section> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
|
|||
</style> |
|||
|
|||
<script> |
|||
import { defineComponent, reactive } from 'vue' |
|||
import sharedMethods from '../shared' |
|||
import { useStorage } from "vue3-storage"; |
|||
|
|||
const localMethods = { |
|||
timeUntilRefresh(){ |
|||
return this.timeSince(Date.now()-(this.lastUpdate+this.preferences.cacheExpiration-Date.now())) |
|||
}, |
|||
timeSinceRefresh(){ |
|||
return this.timeSince(this.lastUpdate) |
|||
}, |
|||
|
|||
nextRefresh: function(){ |
|||
return this.timeSince(Date.now()-(this.lastUpdate+this.preferences.cacheExpiration-Date.now())) |
|||
}, |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'RefreshComponent', |
|||
components: {}, |
|||
mounted(){ |
|||
this.storage = useStorage() |
|||
this.lastUpdate = this.getState('lastUpdate')|| this.lastUpdate |
|||
this.preferences = this.getState('preferences') || this.preferences |
|||
|
|||
this.refreshData = reactive({ |
|||
untilNext: this.timeUntilRefresh(), |
|||
sinceLast: this.timeSinceRefresh() |
|||
}) |
|||
|
|||
setInterval(() => { |
|||
this.preferences = this.getState('preferences') || this.preferences |
|||
|
|||
this.refreshData.untilNext = this.timeUntilRefresh() |
|||
this.refreshData.sinceLast = this.timeSinceRefresh() |
|||
|
|||
if(this.isExpired()) |
|||
this.invalidate() |
|||
|
|||
}, 1000) |
|||
}, |
|||
updated(){ |
|||
this.saveState('preferences') |
|||
|
|||
if(this.isDone()) { |
|||
this.saveState('lastUpdate') |
|||
console.log('isDone()', this.getState('lastUpdate') ) |
|||
} |
|||
|
|||
this.refreshData.untilNext = this.timeUntilRefresh() |
|||
this.refreshData.sinceLast = this.timeSinceRefresh() |
|||
}, |
|||
computed: {}, |
|||
methods: Object.assign(localMethods, sharedMethods), |
|||
props: { |
|||
relaysProp:{ |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
messagesProp:{ |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
resultProp: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
relays: this.relaysProp, |
|||
result: this.resultProp, |
|||
messages: this.messagesProp, |
|||
storage: null, |
|||
lastUpdate: null, |
|||
refresh: true, |
|||
refreshData: this.refreshDataProp, |
|||
interval: null, |
|||
preferences: { |
|||
refresh: true, |
|||
cacheExpiration: 30*60*1000 |
|||
} |
|||
} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
@ -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> |
@ -1,14 +1,20 @@ |
|||
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.mixin(titleMixin) |
|||
|
|||
.use(router) |
|||
.use(Vue3Storage, { namespace: "nostrwatch_" }) |
|||
.component('tabs', Tabs) |
|||
.component('tab', Tab) |
|||
.mixin(titleMixin) |
|||
|
|||
directives(app); |
|||
|
|||
|
|||
app.mount('#app') |
@ -0,0 +1,324 @@ |
|||
<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> |
|||
|
|||
|
|||
<section id="footer"> |
|||
<RefreshComponent |
|||
:relaysProp="relays" |
|||
v-bind:resultProp="result" |
|||
:messagesProp="messages" |
|||
/> |
|||
|
|||
<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> |
|||
</section> |
|||
|
|||
</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 RefreshComponent from '../components/RefreshComponent.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, |
|||
RefreshComponent |
|||
}, |
|||
|
|||
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> |
@ -0,0 +1,139 @@ |
|||
<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> |
|||
|
|||
<row container :gutter="12"> |
|||
<column :xs="12" :md="12" :lg="12" class="list"> |
|||
<table> |
|||
<RelayListComponent |
|||
:relays="relays" |
|||
:result="result" |
|||
:geo="geo" |
|||
:messages="messages" |
|||
:alerts="alerts" |
|||
:connections="connections" |
|||
/> |
|||
</table> |
|||
</column> |
|||
</row> |
|||
|
|||
<row container :gutter="12" v-if="(relaysTotal()-relaysConnected()>0)"> |
|||
<column :xs="12" :md="12" :lg="12" class="processing-card loading"> |
|||
<span>Processing {{ relaysConnected() }}/{{ relaysTotal() }}</span> |
|||
</column> |
|||
</row> |
|||
|
|||
|
|||
<section id="footer"> |
|||
<RefreshComponent |
|||
:relaysProp="relays" |
|||
v-bind:resultProp="result" |
|||
:messagesProp="messages" |
|||
/> |
|||
|
|||
<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> |
|||
</section> |
|||
</div> |
|||
</template> |
|||
<script> |
|||
|
|||
|
|||
import { defineComponent } from 'vue' |
|||
import { useStorage } from "vue3-storage"; |
|||
// import { CallbackResult } from "vue3-storage/dist/lib/types"; |
|||
|
|||
import { Row, Column } from 'vue-grid-responsive'; |
|||
// import { Inspector, InspectorObservation } from '../../lib/nostr-relay-inspector' |
|||
|
|||
import sharedMethods from '../shared' |
|||
|
|||
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 RefreshComponent from '../components/RefreshComponent.vue' |
|||
|
|||
import { version } from '../../package.json' |
|||
import { relays } from '../../relays.yaml' |
|||
import { geo } from '../../geo.yaml' |
|||
|
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'RelayTableComponent', |
|||
components: { |
|||
Row, |
|||
Column, |
|||
RelayListComponent, |
|||
// RelayGroupedListComponent, |
|||
LeafletComponent, |
|||
NavComponent, |
|||
RefreshComponent, |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
relays: relays, |
|||
result: {}, |
|||
messages: {}, |
|||
connections: {}, |
|||
alerts: {}, |
|||
timeouts: {}, |
|||
intervals: {}, |
|||
count: 0, |
|||
storage: null, |
|||
geo, |
|||
version: version, |
|||
hasStorage: false, |
|||
// cacheExpiration: 10*60*1000, //10 minutes |
|||
} |
|||
}, |
|||
|
|||
updated(){}, |
|||
|
|||
async mounted() { |
|||
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.preferences.cacheExpiration))) |
|||
}, |
|||
|
|||
computed: { |
|||
|
|||
}, |
|||
|
|||
methods: sharedMethods |
|||
|
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.list { |
|||
position:relative; |
|||
z-index:1; |
|||
} |
|||
</style> |
@ -0,0 +1,429 @@ |
|||
<template> |
|||
<LeafletSingleComponent |
|||
:geo="geo" |
|||
:relay="relay" |
|||
:result="result" |
|||
/> |
|||
<!-- <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> <!-- ? --> |
|||
<br > |
|||
|
|||
<span class="badges"> |
|||
<span><img :src="badgeCheck('connect')" /></span> |
|||
<span><img :src="badgeCheck('read')" /></span> |
|||
<span><img :src="badgeCheck('write')" /></span> |
|||
</span> |
|||
|
|||
<br /> |
|||
|
|||
<span v-if="result.info?.supported_nips" class="badges"> |
|||
<span v-for="(nip) in result.info.supported_nips" :key="`${relay}_${nip}`"> |
|||
<a :href="nipLink(nip)" target="_blank"><img :src="badgeLink(nip)" /></a> |
|||
</span> |
|||
</span> |
|||
|
|||
<!--table> |
|||
<tr> |
|||
<th colspan="3"><h4>Status</h4></th> |
|||
</tr> |
|||
<tr v-if="result.checkClass"> |
|||
<td :class="result.checkClass.connect" class="connect indicator">Connected</td> |
|||
<td :class="result.checkClass.read" class="read indicator">Read</td> |
|||
<td :class="result.checkClass.write" class="write indicator">Write</td> |
|||
</tr> |
|||
</table--> |
|||
|
|||
|
|||
<table v-if="result.info"> |
|||
<tr> |
|||
<th colspan="2"><h4>Info</h4></th> |
|||
</tr> |
|||
<tr v-for="(value, key) in Object.entries(result.info).filter(value => value[0] != 'id' && value[0] != 'supported_nips')" :key="`${value}_${key}`"> |
|||
<td>{{ value[0] }}</td> |
|||
<td v-if="value[0]!='contact' && value[0]!='pubkey' && value[0]!='software' && value[0]!='version'">{{ value[1] }} </td> |
|||
<td v-if="value[0]=='contact'"><SafeMail :email="value[1]" /></td> |
|||
<td v-if="value[0]=='pubkey' || value[0]=='version'"><code>{{ value[1] }}</code></td> |
|||
<td v-if="value[0]=='software'"><a href="{{ value[1] }}">{{ value[1] }}</a></td> |
|||
</tr> |
|||
</table> |
|||
|
|||
<h4>Identities</h4> |
|||
<table v-if="result.identities"> |
|||
|
|||
<tr v-for="(value, key) in Object.entries(result?.identities)" :key="`${value}_${key}`"> |
|||
|
|||
<td>{{ value[0] }}</td> |
|||
<td><code>{{ value[1] }}</code></td> |
|||
</tr> |
|||
</table> |
|||
|
|||
<div style="display: none">{{result}}</div> <!-- ? --> |
|||
</column> |
|||
</row> |
|||
|
|||
<row container :gutter="12"> |
|||
<column :xs="12" :md="6" :lg="6" class="title-card"> |
|||
|
|||
<h4>GEO {{geo?.countryCode ? getFlag() : ''}}</h4> |
|||
<table v-if="geo[relay]"> |
|||
<tr v-for="(value, key) in Object.entries(geo[relay])" :key="`${value}_${key}`"> |
|||
<td>{{ value[0] }}</td> |
|||
<td>{{ value[1] }} </td> |
|||
</tr> |
|||
</table> |
|||
|
|||
</column> |
|||
<column :xs="12" :md="6" :lg="6" class="title-card"> |
|||
<h4>DNS</h4> |
|||
<table v-if="geo[relay]"> |
|||
<tr v-for="(value, key) in Object.entries(geo[relay].dns)" :key="`${value}_${key}`"> |
|||
<td>{{ value[0] }}</td> |
|||
<td>{{ value[1] }} </td> |
|||
</tr> |
|||
</table> |
|||
|
|||
<div style="display: none">{{result}}</div> <!-- ? --> |
|||
|
|||
</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 LeafletSingleComponent from '../components/LeafletSingleComponent.vue' |
|||
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 crypto from "crypto" |
|||
|
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'SingleRelay', |
|||
components: { |
|||
Row, |
|||
Column, |
|||
LeafletSingleComponent, |
|||
NavComponent, |
|||
SafeMail, |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
relays, |
|||
result: {}, |
|||
messages: {}, |
|||
nips: {}, |
|||
alerts: {}, |
|||
timeouts: {}, |
|||
intervals: {}, |
|||
lastPing: Date.now(), |
|||
nextPing: Date.now() + (60*1000), |
|||
count: 0, |
|||
geo, |
|||
relay: "", |
|||
version: version, |
|||
storage: null, |
|||
lastUpdate: null, |
|||
cacheExpiration: 10*60*1000 //10 minutes |
|||
} |
|||
}, |
|||
|
|||
async mounted() { |
|||
this.relay = this.relayUrl() |
|||
|
|||
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('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.preferences.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}` |
|||
}, |
|||
async check(relay){ |
|||
//const self = this |
|||
/* return new Promise(function(resolve, reject) { */ |
|||
/* let nip = new Array(99).fill(false); |
|||
nip[5] = true |
|||
nip[11] = true */ |
|||
|
|||
const opts = { |
|||
checkLatency: true, |
|||
checkNips: true, |
|||
/* checkNip: nip, */ |
|||
/* debug: true */ |
|||
} |
|||
|
|||
let inspect = new Inspector(relay, opts) |
|||
.on('run', (result) => { |
|||
result.aggregate = 'processing' |
|||
}) |
|||
.on('open', (e, result) => { |
|||
this.result = result |
|||
this.result.checkClass = {read: null, write: null, connect: null} |
|||
this.setResultClass('connect') |
|||
/* console.log('result on open', this.result) */ |
|||
}) |
|||
.on('complete', (instance) => { |
|||
/* console.log('on_complete', instance.result.aggregate) */ |
|||
this.result = instance.result |
|||
this.messages[this.relay] = instance.inbox |
|||
|
|||
/* this.setFlag(relay) */ |
|||
this.setAggregateResult() |
|||
/* 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) */ |
|||
}) |
|||
.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.observations.push( new InspectorObservation('notice', response_obj.code, response_obj.description, response_obj.relates_to) ) |
|||
|
|||
/* console.log(this.result.observations) */ |
|||
}) |
|||
.on('close', (msg) => { |
|||
console.warn("CAUTION", msg) |
|||
/* console.log('supported_nips', inspect.result.info) */ |
|||
}) |
|||
.on('error', (err) => { |
|||
console.error("ERROR", err) |
|||
/* reject(err) */ |
|||
}) |
|||
.run() |
|||
|
|||
return inspect; |
|||
/* }) */ |
|||
|
|||
}, |
|||
setResultClass (key) { |
|||
let result = this.result?.check?.[key] === true |
|||
? 'success' |
|||
: this.result?.check?.[key] === false |
|||
? 'failure' |
|||
: 'pending' |
|||
|
|||
/* console.log('result class', result) */ |
|||
this.result.checkClass[key] = result |
|||
}, |
|||
getLoadingClass () { |
|||
return this.result?.state == 'complete' ? "relay loaded" : "relay" |
|||
}, |
|||
generateKey (url, key) { |
|||
return `${url}_${key}` |
|||
}, |
|||
|
|||
getFlag () { |
|||
return this.geo?.countryCode ? countryCodeEmoji(this.geo.countryCode) : emoji.get('shrug'); |
|||
}, |
|||
|
|||
setCheck (bool) { |
|||
return bool ? '✅ ' : '' |
|||
}, |
|||
|
|||
badgeLink(nip){ |
|||
return `https://img.shields.io/static/v1?style=for-the-badge&label=NIP&message=${this.nipSignature(nip)}&color=black` |
|||
}, |
|||
|
|||
badgeCheck(which){ |
|||
return `https://img.shields.io/static/v1?style=for-the-badge&label=&message=${which}&color=${this.result?.check?.[which] ? 'green' : 'red'}` |
|||
}, |
|||
|
|||
setCross (bool) { |
|||
return !bool ? '❌' : '' |
|||
}, |
|||
setCaution (bool) { |
|||
return !bool ? '⚠️' : '' |
|||
}, |
|||
identityList () { |
|||
let string = '', |
|||
extraString = '', |
|||
users = Object.entries(this.result.identities), |
|||
count = 0 |
|||
|
|||
// console.log(this.result.uri, 'admin', this.result.identities.serverAdmin) |
|||
|
|||
if(this.result.identities) { |
|||
if(this.result.identities.serverAdmin) { |
|||
string = `Relay has registered an administrator pubkey: ${this.result.identities.serverAdmin}. ` |
|||
extraString = "Additionally, " |
|||
} |
|||
|
|||
const total = users.filter(([key]) => key!='serverAdmin').length, |
|||
isOne = total==1 |
|||
|
|||
if(total) { |
|||
string = `${string}${extraString}Relay domain contains NIP-05 verification data for:` |
|||
users.forEach( ([key]) => { |
|||
if(key == "serverAdmin") return |
|||
count++ |
|||
string = `${string} ${(count==total && !isOne) ? 'and' : ''} @${key}${(count!=total && !isOne) ? ', ' : ''}` |
|||
}) |
|||
} |
|||
} |
|||
return string |
|||
}, |
|||
nipSignature(key){ |
|||
return key.toString().length == 1 ? `0${key}` : key |
|||
}, |
|||
nipFormatted(key){ |
|||
return `NIP-${this.nipSignature(key)}` |
|||
}, |
|||
nipLink(key){ |
|||
return `https://github.com/nostr-protocol/nips/blob/master/${this.nipSignature(key)}.md` |
|||
}, |
|||
async copy(text) { |
|||
// console.log('copy', text) |
|||
try { |
|||
await navigator.clipboard.writeText(text); |
|||
} catch($e) { |
|||
//console.log('Cannot copy'); |
|||
} |
|||
}, |
|||
|
|||
// adjustResult (relay) { |
|||
// this.result.observations.forEach( observation => { |
|||
// if (observation.code == "BLOCKS_WRITE_STATUS_CHECK") { |
|||
// this.result.check.write = false |
|||
// this.result.aggregate = 'public' |
|||
// } |
|||
// }) |
|||
// }, |
|||
|
|||
setAggregateResult () { |
|||
if(!this.result) return |
|||
|
|||
let aggregateTally = 0 |
|||
aggregateTally += this.result?.check.connect ? 1 : 0 |
|||
aggregateTally += this.result?.check.read ? 1 : 0 |
|||
aggregateTally += this.result?.check.write ? 1 : 0 |
|||
if (aggregateTally == 3) { |
|||
this.result.aggregate = 'public' |
|||
} |
|||
else if (aggregateTally == 0) { |
|||
this.result.aggregate = 'offline' |
|||
} |
|||
else { |
|||
this.result.aggregate = 'restricted' |
|||
} |
|||
}, |
|||
|
|||
|
|||
|
|||
sha1 (message) { |
|||
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex') |
|||
// //console.log(message, ':', hash) |
|||
return hash |
|||
}, |
|||
|
|||
}, |
|||
|
|||
}) |
|||
</script> |
|||
<style scoped> |
|||
ul, ul li { padding:0; margin:0; list-style:none; } |
|||
td { padding:5px 10px; } |
|||
th h4 { text-align:center; padding:5px 10px; margin:0 0 6px; background:#f0f0f0; } |
|||
table {width:90%; max-width:90%; margin:0 auto 20px ; border: 2px solid #f5f5f5; padding:20px} |
|||
tr td:first-child { text-align:right } |
|||
tr td:last-child { text-align:left } |
|||
.indicator { display: table-cell; width:33% ; font-weight:bold; text-align: center !important; color: white; text-transform: uppercase; font-size:0.8em} |
|||
body, .grid-column { padding:0; margin:0; } |
|||
.badges { display:block; margin: 10px 0 0} |
|||
.badges > span {margin-right:5px} |
|||
#wrapper {max-width:800px} |
|||
h1 {margin: 25px 0 15px; padding:0 0 10px; border-bottom:3px solid #e9e9e9} |
|||
</style> |
@ -0,0 +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: '/relay/:relayUrl(.*)', |
|||
// name: 'nostr.watch - :relayUrl',
|
|||
component: SingleRelay |
|||
}, |
|||
{ |
|||
path: '/status', |
|||
// name: 'nostr.watch',
|
|||
component: ByStatus |
|||
}, |
|||
// Added our new route file named profile.vue
|
|||
{ |
|||
path: '/', |
|||
// name: 'nostr.watch',
|
|||
component: HomePage |
|||
}, |
|||
|
|||
] |
|||
|
|||
// Create Vue Router Object
|
|||
const router = createRouter({ |
|||
history: createWebHistory(), |
|||
// base: process.env.BASE_URL,
|
|||
routes: routes |
|||
}) |
|||
|
|||
export default router |
@ -0,0 +1,235 @@ |
|||
import { Inspector, InspectorObservation } from 'nostr-relay-inspector' |
|||
import { messages as RELAY_MESSAGES, codes as RELAY_CODES } from '../codes.yaml' |
|||
|
|||
import crypto from "crypto" |
|||
|
|||
export default { |
|||
invalidate: function(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)
|
|||
|
|||
}, |
|||
|
|||
isExpired: function(){ |
|||
return typeof this.lastUpdate === 'undefined' || Date.now() - this.lastUpdate > this.preferences.cacheExpiration |
|||
}, |
|||
|
|||
getState: function(key){ |
|||
return this.storage.getStorageSync(key) |
|||
}, |
|||
|
|||
check: async function(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('complete', (instance) => { |
|||
// console.log('getinfo()', instance.result.info)
|
|||
|
|||
this.result[relay] = instance.result |
|||
|
|||
// this.setFlag(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) |
|||
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() |
|||
inspect |
|||
}) |
|||
}, |
|||
|
|||
saveState: function(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: function(){ |
|||
this.relays.forEach(relay=>{ |
|||
this.storage.removeStorage(relay) |
|||
}) |
|||
}, |
|||
recheck: function(relay){ |
|||
const inspect = this.connections[relay] |
|||
inspect.checkLatency() |
|||
|
|||
}, |
|||
|
|||
adjustResult: function(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' |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
getAggregate: function(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) { |
|||
return 'public' |
|||
} |
|||
else if (aggregateTally == 0) { |
|||
return 'offline' |
|||
} |
|||
else { |
|||
return 'restricted' |
|||
} |
|||
}, |
|||
|
|||
relaysTotal: function() { |
|||
return this.relays.length //TODO: Figure out WHY?
|
|||
}, |
|||
|
|||
relaysConnected: function() { |
|||
return Object.entries(this.result).length |
|||
}, |
|||
|
|||
relaysComplete: function() { |
|||
return this.relays.filter(relay => this.results?.[relay]?.state == 'complete').length |
|||
}, |
|||
|
|||
sha1: function(message) { |
|||
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex') |
|||
return hash |
|||
}, |
|||
|
|||
isDone: function(){ |
|||
console.log('is done', this.relaysTotal(), '-', this.relaysConnected(), '<=', 0, this.relaysTotal()-this.relaysConnected() <= 0) |
|||
return this.relaysTotal()-this.relaysComplete() <= 0 |
|||
}, |
|||
|
|||
loadingComplete: function(){ |
|||
return this.isDone() ? 'loaded' : '' |
|||
}, |
|||
|
|||
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"; |
|||
}, |
|||
} |
@ -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"; |
|||
} |
File diff suppressed because it is too large
Loading…
Reference in new issue