zrcadlo https://github.com/atscan/atscan
0.7.0
This commit is contained in:
rodič
6a6d5fb807
revize
8693a52a71
3
Makefile
3
Makefile
|
@ -26,6 +26,9 @@ repo-crawler:
|
|||
repo-worker:
|
||||
deno run --unstable --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-sys ./backend/repo-worker.js
|
||||
|
||||
firehose:
|
||||
deno run --unstable --allow-net --allow-read --allow-env --allow-sys --allow-ffi ./backend/firehose.js
|
||||
|
||||
fe-rebuild:
|
||||
cd frontend && npm run build && pm2 restart atscan-fe
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ function prepareObject(type, item) {
|
|||
case "pds":
|
||||
item.host = item.url.replace(/^https?:\/\//, "");
|
||||
item.fed = findPDSFed(item);
|
||||
item.err = Boolean(item.inspect?.current?.err);
|
||||
item.status = !item.inspect
|
||||
? "unknown"
|
||||
: (item.inspect?.current.err ? "offline" : "online");
|
||||
|
|
|
@ -1,15 +1,41 @@
|
|||
import {
|
||||
ComAtprotoSyncSubscribeRepos,
|
||||
subscribeRepos,
|
||||
SubscribeReposMessage,
|
||||
//SubscribeReposMessage,
|
||||
} from "npm:atproto-firehose";
|
||||
|
||||
import * as atprotoApi from "npm:@atproto/api";
|
||||
const { AppBskyActorProfile } = atprotoApi.default;
|
||||
|
||||
const client = subscribeRepos(`wss://bsky.social`, { decodeRepoOps: true });
|
||||
client.on("message", (m) => {
|
||||
//console.log(m)
|
||||
if (ComAtprotoSyncSubscribeRepos.isHandle(m)) {
|
||||
console.log("handle", m);
|
||||
}
|
||||
if (ComAtprotoSyncSubscribeRepos.isCommit(m)) {
|
||||
//console.log(m)
|
||||
m.ops.forEach((op) => {
|
||||
//console.log(op.payload)
|
||||
console.log(op.payload?.$type);
|
||||
if (!op.payload) {
|
||||
console.log(m);
|
||||
}
|
||||
if (op.payload?.$type === "app.bsky.actor.profile") {
|
||||
if (AppBskyActorProfile.isRecord(op.payload)) {
|
||||
console.log(`Profile updated: ${m.repo}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (ComAtprotoSyncSubscribeRepos.isHandle(m)) {
|
||||
console.log("handle update", m);
|
||||
}
|
||||
if (ComAtprotoSyncSubscribeRepos.isMigrate(m)) {
|
||||
console.log("migrate update", m);
|
||||
}
|
||||
if (ComAtprotoSyncSubscribeRepos.isTombstone(m)) {
|
||||
console.log("tombstone update", m);
|
||||
}
|
||||
if (ComAtprotoSyncSubscribeRepos.isInfo(m)) {
|
||||
console.log("info update", m);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -56,14 +56,22 @@ async function crawlUrl(ats, url, host = "local") {
|
|||
return { err: "unknown host" };
|
||||
}
|
||||
const codec = ats.JSONCodec;
|
||||
const resp = await ats.nats.request(
|
||||
`ats-nodes.${host}.http`,
|
||||
codec.encode({ url }),
|
||||
{
|
||||
timeout: 60000,
|
||||
},
|
||||
);
|
||||
const { err, data, ms } = codec.decode(resp.data);
|
||||
let err, data, ms;
|
||||
try {
|
||||
const resp = await ats.nats.request(
|
||||
`ats-nodes.${host}.http`,
|
||||
codec.encode({ url }),
|
||||
{
|
||||
timeout: 60000,
|
||||
},
|
||||
);
|
||||
const decoded = codec.decode(resp.data);
|
||||
err = decoded.err;
|
||||
data = decoded.data;
|
||||
ms = decoded.ms;
|
||||
} catch (e) {
|
||||
err = e.message;
|
||||
}
|
||||
return { err, data, ms };
|
||||
}
|
||||
|
||||
|
@ -149,11 +157,11 @@ async function crawl(ats) {
|
|||
"ats.service.pds.update",
|
||||
ats.JSONCodec.encode({ url: i.url }),
|
||||
);
|
||||
console.log(
|
||||
/*console.log(
|
||||
`[${chost}] -> ${i.url} ${ms ? "[" + ms + "ms]" : ""} ${
|
||||
err ? "error = " + err : ""
|
||||
}`,
|
||||
);
|
||||
);*/
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
{
|
||||
"name": "typesense",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "typesense",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"typesense": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
|
||||
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
|
||||
"integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typesense": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typesense/-/typesense-1.5.4.tgz",
|
||||
"integrity": "sha512-51/lyTWbn4NmH4oe3dXv4xSi9eqZwOB7/hXnzyGl84pgFn2MdeeMKb4Lr7z8fUNFmS0TNl7rujaIHRmNR1f2ZA==",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"loglevel": "^1.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "^7.17.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "typesense",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "typesense-init.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"typesense": "^1.5.4"
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
const Typesense = require("typesense");
|
||||
|
||||
let client = new Typesense.Client({
|
||||
"nodes": [{
|
||||
"host": "localhost", // For Typesense Cloud use xxx.a1.typesense.net
|
||||
"port": "8108", // For Typesense Cloud use 443
|
||||
"protocol": "http", // For Typesense Cloud use https
|
||||
}],
|
||||
"apiKey": "Kaey9ahMo7xoob1haivaithe2Aighoo3azohl2Joo5Aemoh4aishoogugh3Oowim",
|
||||
"connectionTimeoutSeconds": 2,
|
||||
});
|
||||
|
||||
/*const schema = {
|
||||
name: 'dids',
|
||||
fields: [
|
||||
{ name: 'did', type: 'string' },
|
||||
{ name: 'handles', type: 'string[]' },
|
||||
]
|
||||
}
|
||||
|
||||
client.collections().create(schema)
|
||||
.then(function (data) {
|
||||
console.log(data)
|
||||
})*/
|
|
@ -1 +0,0 @@
|
|||
../.env
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "atscan-fe",
|
||||
"version": "0.6.2-alpha",
|
||||
"version": "0.7.0-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "atscan-fe",
|
||||
"version": "0.6.2-alpha",
|
||||
"version": "0.7.0-alpha",
|
||||
"dependencies": {
|
||||
"i18next": "^23.2.6",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -36,6 +36,7 @@
|
|||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-echarts": "^0.0.5",
|
||||
"svelte-local-storage-store": "^0.5.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.0"
|
||||
}
|
||||
|
@ -3124,6 +3125,18 @@
|
|||
"svelte": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-local-storage-store": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.5.0.tgz",
|
||||
"integrity": "sha512-SEDrpapeia6fUqta+r1NvSLlJYPkZ4pBcl15EYIOSPNzy6vhpoXu8cnzUDmZxsWl7fZGAHxrVH9UyZCbyO4W+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.48.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "atscan-fe",
|
||||
"version": "0.6.2-alpha",
|
||||
"version": "0.7.0-alpha",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
@ -33,6 +33,7 @@
|
|||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-echarts": "^0.0.5",
|
||||
"svelte-local-storage-store": "^0.5.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.0"
|
||||
},
|
||||
|
|
|
@ -13,6 +13,13 @@
|
|||
<link href="/font-awesome/css/fontawesome.min.css" rel="stylesheet" />
|
||||
<link href="/font-awesome/css/brands.min.css" rel="stylesheet" />
|
||||
<link href="/font-awesome/css/solid.min.css" rel="stylesheet" />
|
||||
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.2/echarts.min.js"
|
||||
integrity="sha512-VdqgeoWrVJcsDXFlQEKqE5MyhaIgB9yXUVaiUa8DR2J4Lr1uWcFm+ZH/YnzV5WqgKf4GPyHQ64vVLgzqGIchyw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" data-theme="skeleton">
|
||||
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
//import { fetch } from 'svelte/fetch';
|
||||
import { config } from '$lib/config';
|
||||
|
||||
export async function request(fetch, path, ...args) {
|
||||
console.log(path);
|
||||
const res = await fetch(config.api + path, ...args);
|
||||
if (!res || res.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
import { format } from 'echarts';
|
||||
export let sourceData;
|
||||
export let data;
|
||||
export let sorting = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -83,19 +84,27 @@
|
|||
val = `/${row.did}`;
|
||||
}
|
||||
if (key === 'commits') {
|
||||
val = row.repo?.commits ? formatNumber(row.repo.commits) : '-';
|
||||
val = row.repo?.commits
|
||||
? formatNumber(row.repo.commits)
|
||||
: '<i class="fa-solid fa-clock opacity-20" alt="Not yet indexed" title="Not yet indexed"></i>';
|
||||
}
|
||||
if (key === 'size') {
|
||||
val =
|
||||
'<div class="text-lg">' +
|
||||
(row.repo?.size
|
||||
? filesize(row.repo.size) +
|
||||
'</div><div>' +
|
||||
formatNumber(
|
||||
Object.keys(row.repo.collections).reduce((t, c) => (t += row.repo.collections[c]), 0)
|
||||
) +
|
||||
' items</div>'
|
||||
: '-');
|
||||
val = row.repo?.size
|
||||
? '<div class="text-lg">' +
|
||||
filesize(row.repo.size) +
|
||||
'</div><div>' +
|
||||
formatNumber(
|
||||
Object.keys(row.repo.collections).reduce((t, c) => (t += row.repo.collections[c]), 0)
|
||||
) +
|
||||
' items</div>'
|
||||
: '<i class="fa-solid fa-clock opacity-20" alt="Not yet indexed" title="Not yet indexed"></i>';
|
||||
}
|
||||
if (key === 'lastSnapshot') {
|
||||
val = `<span class="text-sm">${
|
||||
row.repo?.time
|
||||
? dateDistance(row.repo.time) + ' ago'
|
||||
: '<i class="fa-solid fa-clock opacity-20" alt="Not yet indexed" title="Not yet indexed"></i>'
|
||||
}</span>`;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
@ -107,6 +116,7 @@
|
|||
['PDS (PLC)', 'pds'],
|
||||
['Repo size', 'size'],
|
||||
['Commits', 'commits'],
|
||||
//['Last snapshot', 'lastSnapshot'],
|
||||
['Updated', 'lastMod']
|
||||
],
|
||||
body: customTableMapper(
|
||||
|
@ -114,12 +124,13 @@
|
|||
['img', 'did', 'srcHost', 'size', 'commits', 'lastMod'],
|
||||
tableMap
|
||||
),
|
||||
meta: customTableMapper(sourceData || [], ['did_raw', 'url'], tableMap)
|
||||
meta: customTableMapper(sourceData || [], ['did_raw', 'url', '_isChange'], tableMap)
|
||||
};
|
||||
</script>
|
||||
|
||||
<Table
|
||||
source={tableSimple}
|
||||
{sorting}
|
||||
currentSort={data.sort}
|
||||
defaultSort=""
|
||||
on:headSelected={(e) => onHeadSelected(e)}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
} from '$lib/utils.js';
|
||||
export let sourceData;
|
||||
export let data;
|
||||
export let sorting = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -19,6 +20,10 @@
|
|||
dispatch('headSelected', event.detail);
|
||||
}
|
||||
|
||||
function onFavoriteClick(event) {
|
||||
dispatch('favoriteClick', event.detail);
|
||||
}
|
||||
|
||||
function tableMap({ val, key, row }) {
|
||||
if (key === 'plcs' && val) {
|
||||
val = val
|
||||
|
@ -72,7 +77,9 @@
|
|||
val = `<a href="/dids?q=pds:${row.host}" class="anchor">${formatNumber(val)}</a>`;
|
||||
}
|
||||
if (key === 'size') {
|
||||
val = row.size ? filesize(row.size) : '-';
|
||||
val = row.size
|
||||
? `<span alt="${row.size}" title="${row.size}">${filesize(row.size)}</span>`
|
||||
: '-';
|
||||
}
|
||||
if (key === 'lastOnline' && row.inspect) {
|
||||
val = `<span class="text-xs">${
|
||||
|
@ -107,7 +114,7 @@
|
|||
['Size', 'size'],
|
||||
['Location', 'country'],
|
||||
['PLCs (User Domains)', 'plcs'],
|
||||
['Resp. time', 'responseTime'],
|
||||
['Latency', 'responseTime'],
|
||||
['Last Online', 'lastOnline']
|
||||
],
|
||||
body: customTableMapper(
|
||||
|
@ -125,13 +132,16 @@
|
|||
],
|
||||
tableMap
|
||||
),
|
||||
meta: customTableMapper(sourceData, ['host_raw', 'url'], tableMap)
|
||||
meta: customTableMapper(sourceData, ['host_raw', 'url', '_isChange', '_isFavorite'], tableMap)
|
||||
};
|
||||
</script>
|
||||
|
||||
<Table
|
||||
source={tableSimple}
|
||||
{sorting}
|
||||
currentSort={data.sort}
|
||||
defaultSort=""
|
||||
favoriteColumn={2}
|
||||
on:favoriteClick={(e) => onFavoriteClick(e)}
|
||||
on:headSelected={(e) => onHeadSelected(e)}
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { tableA11y } from '@skeletonlabs/skeleton';
|
||||
import { goto } from '$app/navigation';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -13,7 +14,9 @@
|
|||
export let source;
|
||||
|
||||
export let currentSort = null;
|
||||
export let defaultSort;
|
||||
export let defaultSort = null;
|
||||
export let favoriteColumn = null;
|
||||
export let sorting = false;
|
||||
|
||||
if (!currentSort) {
|
||||
currentSort = defaultSort;
|
||||
|
@ -67,6 +70,12 @@
|
|||
dispatch('headSelected', meta);
|
||||
}
|
||||
|
||||
function onFavoriteClick(event, id) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dispatch('favoriteClick', id);
|
||||
}
|
||||
|
||||
// Row Keydown Handler
|
||||
function onRowKeydown(event, rowIndex) {
|
||||
if (['Enter', 'Space'].includes(event.code)) onRowClick(event, rowIndex);
|
||||
|
@ -91,10 +100,10 @@
|
|||
<thead class="table-head {regionHead}">
|
||||
<tr>
|
||||
{#each source.head as heading}
|
||||
<th class="{regionHeadCell} cursor-pointer hover:underline text-sm"
|
||||
<th class="{regionHeadCell} {sorting ? 'cursor-pointer hover:underline' : ''} text-sm"
|
||||
on:click={(e) => { onHeaderClick(e, Array.isArray(heading) ? heading[1] : heading) }}>
|
||||
{@html Array.isArray(heading) ? heading[0] : heading}
|
||||
{#if Array.isArray(heading) && (currentSort?.replace(/^!/, '') === heading[1] || '!'+currentSort === heading[1])}
|
||||
{#if sorting && Array.isArray(heading) && (currentSort?.replace(/^!/, '') === heading[1] || '!'+currentSort === heading[1])}
|
||||
{#if currentSort?.startsWith('!')}
|
||||
↑
|
||||
{:else}
|
||||
|
@ -111,6 +120,7 @@
|
|||
<!-- Row -->
|
||||
<!-- prettier-ignore -->
|
||||
<tr
|
||||
id={source.meta[rowIndex][0]}
|
||||
on:click={(e) => { onRowClick(e, rowIndex); }}
|
||||
on:keydown={(e) => { onRowKeydown(e, rowIndex); }}
|
||||
aria-rowindex={rowIndex + 1}
|
||||
|
@ -119,12 +129,15 @@
|
|||
<!-- Cell -->
|
||||
<!-- prettier-ignore -->
|
||||
<td
|
||||
class="{regionCell}"
|
||||
class="{source.meta[rowIndex][2] === 'update' ? 'bg-yellow-500/50' : (source.meta[rowIndex][2] === 'create' ? 'bg-green-500/50' : '')} transition-all duration-[2500ms] ease-out {regionCell}"
|
||||
role="gridcell"
|
||||
aria-colindex={cellIndex + 1}
|
||||
tabindex={cellIndex === 0 ? 0 : -1}
|
||||
>
|
||||
{@html cell ? cell : '-'}
|
||||
{#if favoriteColumn !== null && favoriteColumn === cellIndex}
|
||||
<i class="favorite fa-regular fa-star ml-1 {source.meta[rowIndex][3] !== undefined && source.meta[rowIndex][3] ? 'inline-block active text-yellow-500 opacity-100 hover:text-red-500' : 'opacity-50 hidden hover:text-green-500'} hover:opacity-100" on:click={(ev) => onFavoriteClick(ev, source.meta[rowIndex][0])}></i>
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
|
@ -142,3 +155,12 @@
|
|||
{/if}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table-body tr td .favorite {
|
||||
display: none;
|
||||
}
|
||||
.table-body tr:hover td .favorite {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export const config = {
|
||||
name: 'ATScan',
|
||||
domain: 'atscan.net',
|
||||
api: 'https://api.atscan.net',
|
||||
web: 'https://atscan.net',
|
||||
git: 'https://github.com/atscan/atscan',
|
||||
status: 'https://status.gwei.cz/status/atscan',
|
||||
ecosystem: 'https://mirror.ecosystem.atscan.net/index.json'
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import { config } from '$lib/config';
|
||||
|
||||
export let ecosystem = null;
|
||||
|
||||
export async function loadEcosystem() {
|
||||
if (ecosystem) {
|
||||
return ecosystem;
|
||||
}
|
||||
const ecosystemRes = await fetch(config.ecosystem);
|
||||
ecosystem = ecosystemRes ? await ecosystemRes.json() : null;
|
||||
return ecosystem;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { connect as NATSConnect, StringCodec, JSONCodec } from 'nats.ws';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let connected = writable(null);
|
||||
export let nats = null;
|
||||
export let codec = JSONCodec();
|
||||
|
||||
export async function connect() {
|
||||
nats = await NATSConnect({ servers: 'wss://nats.gwei.cz' });
|
||||
await connected.set(true);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { persisted } from 'svelte-local-storage-store';
|
||||
|
||||
export const preferences = persisted('preferences', {
|
||||
favoritePDS: []
|
||||
});
|
|
@ -1,22 +1,11 @@
|
|||
import pkg from '../../package.json';
|
||||
import { config } from '$lib/config';
|
||||
import { loadEcosystem } from '$lib/ecosystem';
|
||||
|
||||
export async function load({ fetch }) {
|
||||
const config = {
|
||||
name: 'ATScan',
|
||||
domain: 'atscan.net',
|
||||
api: 'https://api.atscan.net',
|
||||
web: 'https://atscan.net',
|
||||
git: 'https://github.com/atscan/atscan',
|
||||
status: 'https://status.gwei.cz/status/atscan',
|
||||
ecosystem: 'https://mirror.ecosystem.atscan.net/index.json'
|
||||
};
|
||||
|
||||
const ecosystemRes = await fetch(config.ecosystem);
|
||||
const ecosystem = ecosystemRes ? await ecosystemRes.json() : null;
|
||||
|
||||
return {
|
||||
config,
|
||||
ecosystem,
|
||||
ecosystem: await loadEcosystem(),
|
||||
pkg,
|
||||
basicSpacing: 'p-4 md:p-4 space-y-6 md:space-y-6'
|
||||
};
|
||||
|
|
|
@ -14,30 +14,24 @@
|
|||
import { storeHighlightJs } from '@skeletonlabs/skeleton';
|
||||
import { Drawer, drawerStore } from '@skeletonlabs/skeleton';
|
||||
import { onMount } from 'svelte';
|
||||
import { connect, StringCodec, JSONCodec } from 'nats.ws';
|
||||
import { i18n } from '$lib/i18n.js';
|
||||
import { nats, connect, connected } from '$lib/sockets.js';
|
||||
import { navigating } from '$app/stores';
|
||||
import { ProgressBar } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let data;
|
||||
|
||||
storeHighlightJs.set(hljs);
|
||||
|
||||
onMount(() => {
|
||||
connect();
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
//console.log('scrolltop');
|
||||
//window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
const nc = await connect({ servers: 'wss://nats.gwei.cz' });
|
||||
const codec = JSONCodec();
|
||||
const sub = nc.subscribe('greet.sue');
|
||||
(async () => {
|
||||
for await (const m of sub) {
|
||||
console.log(`[${sub.getProcessed()}]: ${JSON.stringify(codec.decode(m.data))}`);
|
||||
}
|
||||
console.log('subscription closed');
|
||||
})();
|
||||
});
|
||||
|
||||
function drawerOpen() {
|
||||
drawerStore.open({});
|
||||
}
|
||||
|
@ -103,8 +97,13 @@
|
|||
<!-- App Shell -->
|
||||
<AppShell>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="h-1.5 bg-surface-100-800-token">
|
||||
{#if $navigating}
|
||||
<div class="w-full"><ProgressBar meter="bg-primary-500" track="bg-surface-100-800-token" /></div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- App Bar -->
|
||||
<AppBar>
|
||||
<AppBar padding="px-4 pb-4 pt-2.5">
|
||||
<svelte:fragment slot="lead">
|
||||
<div class="flex items-center">
|
||||
<button class="lg:hidden btn btn-sm" on:click={drawerOpen}>
|
||||
|
@ -176,6 +175,11 @@
|
|||
Twitter
|
||||
</a>
|
||||
-->
|
||||
{#if $connected}
|
||||
<div class="text-xs pr-4">
|
||||
<i class="fa-solid fa-circle text-green-500 text-xs mr-1 animate-pulse" /> Connected
|
||||
</div>
|
||||
{/if}
|
||||
<LightSwitch />
|
||||
<div class="relative hidden lg:block">
|
||||
<a
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { request } from '$lib/api';
|
||||
|
||||
export async function load({ params, fetch, parent }) {
|
||||
const { config } = await parent();
|
||||
const did = `did:${params.id}`;
|
||||
const itemRes = await fetch(`${config.api}/${did}`);
|
||||
if (!itemRes) {
|
||||
const item = await request(fetch, `/${did}`);
|
||||
if (!item) {
|
||||
throw error(404, { message: 'Not found' });
|
||||
}
|
||||
|
||||
const item = await itemRes.json();
|
||||
const pdsHost = item.pds[0]?.replace(/^https?:\/\//, '');
|
||||
const pdsRes = pdsHost ? await fetch(`${config.api}/pds/${pdsHost}`) : null;
|
||||
return {
|
||||
|
|
|
@ -59,6 +59,11 @@
|
|||
let current;
|
||||
let currentError;
|
||||
|
||||
$: records =
|
||||
item.repo && item.repo.collections
|
||||
? Object.keys(item.repo?.collections).reduce((t, c) => (t += item.repo.collections[c]), 0)
|
||||
: null;
|
||||
|
||||
onMount(async () => {
|
||||
if (item.repo) {
|
||||
try {
|
||||
|
@ -102,7 +107,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="h2">Revisions <span class="font-normal text-2xl">({sourceData.length})</span></h2>
|
||||
<h2 class="h2">History <span class="font-normal text-2xl">({sourceData.length})</span></h2>
|
||||
<Table source={historyTable} />
|
||||
|
||||
{#if data.pds}
|
||||
|
@ -139,26 +144,20 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th class="text-right">Records</th>
|
||||
<td
|
||||
>{formatNumber(
|
||||
Object.keys(item.repo?.collections).reduce(
|
||||
(t, c) => (t += item.repo.collections[c]),
|
||||
0
|
||||
)
|
||||
)} items</td
|
||||
>
|
||||
<td>{formatNumber(records)} items</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-right">Collections</th>
|
||||
<td
|
||||
>{#if item.repo?.collections.length > 0}{Object.keys(item.repo?.collections)
|
||||
>{#if item.repo?.collections && records > 0}
|
||||
{Object.keys(item.repo?.collections)
|
||||
.map((c) => `${formatNumber(item.repo.collections[c])} ${c}`)
|
||||
.join(', ')}{:else}<i>No items</i>{/if}</td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-right">Indexed</th>
|
||||
<td>{dateDistance(item.repo?.time)} ago</td>
|
||||
<td>{dateDistance(item.repo?.time)} ago ({item.repo?.time})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-right">Up to date?</th>
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import DIDTable from '$lib/components/DIDTable.svelte';
|
||||
import BasicPage from '$lib/components/BasicPage.svelte';
|
||||
import { nats, connected, codec } from '$lib/sockets.js';
|
||||
|
||||
export let data;
|
||||
const search = writable(data.q?.trim() || '');
|
||||
$: sourceData = data.did;
|
||||
$: totalCount = data.totalCount;
|
||||
let onlySandbox = data.onlySandbox || null;
|
||||
let sort = data.sort || null;
|
||||
|
||||
|
@ -20,13 +22,69 @@
|
|||
}
|
||||
|
||||
let periodicUpdate = null;
|
||||
const subscriptions = [];
|
||||
|
||||
function subscribeDIDs() {
|
||||
const sub = nats.subscribe('ats.api.did.*');
|
||||
subscriptions.push(sub);
|
||||
(async () => {
|
||||
for await (const m of sub) {
|
||||
//console.log(`[${sub.getProcessed()}x]: ${JSON.stringify(m.data)}`);
|
||||
switch (m.subject) {
|
||||
case 'ats.api.did.update':
|
||||
case 'ats.api.did.create':
|
||||
const did = codec.decode(m.data);
|
||||
const type = m.subject.replace(/^ats.api.did\./, '');
|
||||
did._isChange = type;
|
||||
|
||||
if (m.subject === 'ats.api.did.create' && onlySandbox && did.fed === 'sandbox') {
|
||||
totalCount += 1;
|
||||
$: sourceData = [did, ...sourceData].slice(0, 100);
|
||||
} else if (m.subject === 'ats.api.did.create' && !data.q && !sort && !onlySandbox) {
|
||||
totalCount += 1;
|
||||
$: sourceData = [did, ...sourceData].slice(0, 100);
|
||||
} else {
|
||||
const exist = sourceData.find((i) => i.did === did.did);
|
||||
if (exist) {
|
||||
const index = sourceData.indexOf(exist);
|
||||
$: sourceData = [
|
||||
...sourceData.slice(0, index),
|
||||
did,
|
||||
...sourceData.slice(index + 1)
|
||||
];
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
$: sourceData = sourceData.map((sd) => {
|
||||
if (sd.did === did.did) {
|
||||
did._isChange = false;
|
||||
}
|
||||
return sd;
|
||||
});
|
||||
}, 2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('subscription closed');
|
||||
})();
|
||||
}
|
||||
|
||||
connected.subscribe((val) => {
|
||||
if (val === true) {
|
||||
subscribeDIDs();
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
periodicUpdate = setInterval(() => {
|
||||
/*periodicUpdate = setInterval(() => {
|
||||
invalidate((url) => url.pathname === '/dids');
|
||||
}, 60 * 1000);
|
||||
}, 60 * 1000);*/
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(periodicUpdate);
|
||||
for (const sub of subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
function gotoNewTableState() {
|
||||
|
@ -136,11 +194,11 @@
|
|||
<div class="text-xl">
|
||||
{#if $search && $search?.trim() !== ''}
|
||||
Search for <code class="code text-2xl variant-tertiary">{$search.trim()}</code>
|
||||
{#if onlySandbox}(only sandbox){/if} ({formatNumber(data.totalCount)}):
|
||||
{#if onlySandbox}(only sandbox){/if} ({formatNumber(totalCount)}):
|
||||
{:else}
|
||||
All DIDs {#if onlySandbox} on sandbox{/if} ({formatNumber(data.totalCount)}):
|
||||
All DIDs {#if onlySandbox} on sandbox{/if} ({formatNumber(totalCount)}):
|
||||
{/if}
|
||||
</div>
|
||||
<DIDTable {sourceData} {data} on:headSelected={(e) => onHeadSelected(e)} />
|
||||
<DIDTable {sourceData} {data} sorting="true" on:headSelected={(e) => onHeadSelected(e)} />
|
||||
{/if}
|
||||
</BasicPage>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
export async function load({ fetch, parent }) {
|
||||
const { config } = await parent();
|
||||
import { request } from '$lib/api';
|
||||
|
||||
const plcRes = await fetch(`${config.api}/plc`);
|
||||
export async function load({ fetch }) {
|
||||
return {
|
||||
plc: await plcRes.json()
|
||||
plc: request(fetch, '/plc')
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import * as _ from 'lodash';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
async function loadPDS(config) {
|
||||
const res = await fetch(`${config.api}/pds`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ fetch, parent, url }) {
|
||||
const { config } = await parent();
|
||||
const res = await fetch(`${config.api}/pds`);
|
||||
const arr = (await res.json()).map((i) => {
|
||||
i.err = Boolean(i.inspect?.current?.err);
|
||||
return i;
|
||||
});
|
||||
//const pds = _.orderBy(arr, ['env', 'err', 'didsCount'], ['asc', 'asc', 'desc']);
|
||||
|
||||
let q = url.searchParams.get('q');
|
||||
let sort = url.searchParams.get('sort');
|
||||
|
||||
const pds = loadPDS(config);
|
||||
|
||||
return {
|
||||
pds: arr,
|
||||
pds: browser ? pds : await pds,
|
||||
sort,
|
||||
q
|
||||
};
|
||||
|
|
|
@ -7,13 +7,22 @@
|
|||
import PDSTable from '$lib/components/PDSTable.svelte';
|
||||
import BasicPage from '$lib/components/BasicPage.svelte';
|
||||
import { orderBy } from 'lodash';
|
||||
import { compute_slots } from 'svelte/internal';
|
||||
import { nats, connected, codec } from '$lib/sockets.js';
|
||||
import { preferences } from '$lib/stores.js';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const subscriptions = [];
|
||||
|
||||
const search = writable($page.url.searchParams.get('q') || '');
|
||||
let sort = data.sort || null;
|
||||
|
||||
let baseData = data.pds;
|
||||
let sortedData = sortData(baseData);
|
||||
let sourceData = filterSourceData(sortedData);
|
||||
let favoritesData = baseData.filter((i) => $preferences.favoritePDS.includes(i.host)) || [];
|
||||
|
||||
function gotoNewTableState() {
|
||||
let args = [];
|
||||
if ($search !== '') {
|
||||
|
@ -32,6 +41,7 @@
|
|||
|
||||
search.subscribe((val) => {
|
||||
gotoNewTableState();
|
||||
updateData();
|
||||
return val;
|
||||
});
|
||||
|
||||
|
@ -39,9 +49,22 @@
|
|||
return goto(`/pds/${i.detail[0]}`);
|
||||
}
|
||||
|
||||
$: sourceData = (function filterSourceData(bd) {
|
||||
function updateData(base = null) {
|
||||
if (!base) {
|
||||
base = baseData;
|
||||
}
|
||||
base = base.map((i) => {
|
||||
i._isFavorite = $preferences.favoritePDS.includes(i.host);
|
||||
return i;
|
||||
});
|
||||
sortedData = sortData(base);
|
||||
sourceData = filterSourceData(sortedData);
|
||||
favoritesData = baseData.filter((i) => $preferences.favoritePDS.includes(i.host)) || [];
|
||||
}
|
||||
|
||||
function filterSourceData(bd) {
|
||||
const tokens = $search.split(' ');
|
||||
let base = JSON.parse(JSON.stringify(bd)); //.filter(d => d.inspect?.lastOnline)
|
||||
let base = bd; //.filter(d => d.inspect?.lastOnline)
|
||||
base = base.map((i) => {
|
||||
i.country = i.ip?.country;
|
||||
//i.ms = !i.inspect?.current.err ? i.inspect?.current?.ms : null;
|
||||
|
@ -70,6 +93,12 @@
|
|||
if (txt) {
|
||||
base = base.filter((i) => i.url.match(new RegExp(txt, 'i')));
|
||||
}
|
||||
|
||||
return base;
|
||||
//return sortData(base);
|
||||
}
|
||||
|
||||
function sortData(base) {
|
||||
if (sort) {
|
||||
let sortReal = sort;
|
||||
let sortDirection = 1;
|
||||
|
@ -81,9 +110,8 @@
|
|||
} else {
|
||||
base = orderBy(base, ['env', 'err', 'didsCount'], ['asc', 'asc', 'desc']);
|
||||
}
|
||||
|
||||
return base;
|
||||
})(data.pds);
|
||||
}
|
||||
|
||||
function formSubmit() {
|
||||
const url = '?q=' + $search;
|
||||
|
@ -94,10 +122,76 @@
|
|||
function onHeadSelected(e) {
|
||||
sort = sort === e.detail ? (sort.startsWith('!') ? '' : '!') + e.detail : e.detail;
|
||||
gotoNewTableState();
|
||||
updateData();
|
||||
}
|
||||
|
||||
function onFavoriteClick(e) {
|
||||
let favs = $preferences.favoritePDS;
|
||||
if (favs.includes(e.detail)) {
|
||||
favs.splice(favs.indexOf(e.detail), 1);
|
||||
} else {
|
||||
favs.push(e.detail);
|
||||
}
|
||||
preferences.set(Object.assign($preferences, { favoritePDS: favs }));
|
||||
updateData();
|
||||
}
|
||||
|
||||
function subscribePDS() {
|
||||
const sub = nats.subscribe('ats.api.pds.*');
|
||||
subscriptions.push(sub);
|
||||
(async () => {
|
||||
for await (const m of sub) {
|
||||
//console.log(`[${sub.getProcessed()}x]: ${JSON.stringify(m.data)}`);
|
||||
switch (m.subject) {
|
||||
case 'ats.api.pds.update':
|
||||
const pds = codec.decode(m.data);
|
||||
pds._isChange = 'update';
|
||||
const exist = baseData.find((i) => i.url === pds.url);
|
||||
if (exist) {
|
||||
const index = baseData.indexOf(exist);
|
||||
baseData[index] = pds;
|
||||
updateData(baseData);
|
||||
|
||||
//console.log(pds._isChange);
|
||||
setTimeout(() => {
|
||||
updateData(
|
||||
baseData.map((sd) => {
|
||||
if (sd.url === pds.url) {
|
||||
pds._isChange = false;
|
||||
}
|
||||
return sd;
|
||||
})
|
||||
);
|
||||
}, 250);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('subscription closed');
|
||||
})();
|
||||
}
|
||||
|
||||
connected.subscribe((val) => {
|
||||
if (val === true) {
|
||||
subscribePDS();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
for (const sub of subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<BasicPage {data} title="PDS Instances">
|
||||
{#if $preferences.favoritePDS.length > 0}
|
||||
<h2 class="h2">Your favourites</h2>
|
||||
<PDSTable sourceData={favoritesData} {data} on:favoriteClick={(e) => onFavoriteClick(e)} />
|
||||
|
||||
<h2 class="h2">All instances</h2>
|
||||
{/if}
|
||||
|
||||
<form on:submit|preventDefault={formSubmit} class="flex gap-4">
|
||||
<input
|
||||
class="input"
|
||||
|
@ -108,7 +202,6 @@
|
|||
/>
|
||||
<!--button type="submit" class="btn variant-filled">Search</button-->
|
||||
</form>
|
||||
|
||||
<div class="text-xl">
|
||||
{#if $search && $search?.trim() !== ''}
|
||||
Search for <code class="code text-2xl variant-tertiary">{$search.trim()}</code>
|
||||
|
@ -117,5 +210,11 @@
|
|||
All PDS Instances ({formatNumber(sourceData.length)}):
|
||||
{/if}
|
||||
</div>
|
||||
<PDSTable {sourceData} {data} on:headSelected={(e) => onHeadSelected(e)} />
|
||||
<PDSTable
|
||||
{sourceData}
|
||||
{data}
|
||||
sorting="true"
|
||||
on:headSelected={(e) => onHeadSelected(e)}
|
||||
on:favoriteClick={(e) => onFavoriteClick(e)}
|
||||
/>
|
||||
</BasicPage>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
export async function load({ params, fetch, parent }) {
|
||||
const { config } = await parent();
|
||||
import { request } from '$lib/api';
|
||||
|
||||
const itemResp = await fetch(`${config.api}/pds/${params.host}`);
|
||||
const didsResp = await fetch(`${config.api}/dids?q=pds:${params.host}&limit=10`, {
|
||||
headers: { 'x-ats-wrapped': 'true' }
|
||||
});
|
||||
export async function load({ params, fetch }) {
|
||||
return {
|
||||
item: await itemResp.json(),
|
||||
dids: await didsResp.json()
|
||||
item: request(fetch, `/pds/${params.host}`),
|
||||
dids: request(fetch, `/dids?q=pds:${params.host}&limit=10`, {
|
||||
headers: { 'x-ats-wrapped': 'true' }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,6 +17,12 @@ module.exports = {
|
|||
args: "daemon",
|
||||
interpreter: "deno",
|
||||
interpreterArgs: "run --unstable --allow-net --allow-read --allow-env --allow-sys",
|
||||
}, {
|
||||
name: "atscan-firehose",
|
||||
script: "./backend/firehose.js",
|
||||
args: "daemon",
|
||||
interpreter: "deno",
|
||||
interpreterArgs: "run --unstable --allow-net --allow-read --allow-env --allow-sys --allow-ffi",
|
||||
}, {
|
||||
name: "atscan-fe-dev",
|
||||
interpreter: "mullvad-exclude",
|
||||
|
|
Načítá se…
Odkázat v novém úkolu