Porovnat revize

...

6 Commity

Autor SHA1 Zpráva Datum
tree a46f39184c fix 2023-07-12 17:24:28 +00:00
tree fad6c18de7 0.8.0-alpha 2023-07-12 17:23:11 +00:00
tree 89e1fd7e0d frontend updates 2023-07-12 17:22:49 +00:00
tree ba593bd9f7 firehose updates 2023-07-12 16:45:45 +00:00
tree a8c48dca4c indexer rework & use redis 2023-07-12 16:44:25 +00:00
tree 24f6e61dd6 bgs tickers 2023-07-12 16:42:35 +00:00
26 změnil soubory, kde provedl 878 přidání a 97 odebrání

Zobrazit soubor

@ -12,4 +12,12 @@ INFLUXDB_TOKEN=XXXX
INFLUXDB_ORG=XXXX
INFLUXDB_BUCKET=XXXX
TYPESENSE_HOST=http://localhost:8108
TYPESENSE_API_KEY=XXXXX
TYPESENSE_API_KEY=XXXXX
ATSCAN_TICK_SANDBOX_HANDLE=tick-sandbox.atscan.net
ATSCAN_TICK_SANDBOX_PASSWORD=XXXXXX
ATSCAN_TICK_BLUESKY_HANDLE=tick.bsky.social
ATSCAN_TICK_BLUESKY_PASSWORD=XXXXXX
ATSCAN_TICK_BLUESKY_PDS=bsky.social
ATSCAN_TICK_BLUESKY_REPO=did:plc:kwmcvt4maab47n7dgvepg4tr
ATSACN_TICK_BLUESKY_POST=3k2djecjpk22c
ATSCAN_TICK_BLUESKY_INTERVAL=5000

Zobrazit soubor

@ -12,7 +12,7 @@ plc-crawl:
deno run --unstable --allow-net --allow-read --allow-env --allow-sys ./backend/plc-crawler.js
index:
deno run --unstable --allow-net --allow-read --allow-env --allow-sys ./backend/indexer.js
deno run --unstable --allow-net --allow-read --allow-env --allow-sys --allow-ffi ./backend/indexer.js
index-daemon:
deno run --unstable --allow-net --allow-read --allow-env --allow-sys ./backend/indexer.js daemon
@ -29,6 +29,9 @@ repo-worker:
firehose:
deno run --unstable --allow-net --allow-read --allow-env --allow-sys --allow-ffi ./backend/firehose.js
tick:
deno run --unstable --allow-net --allow-read --allow-env ./backend/tick.js
fe-rebuild:
cd frontend && npm run build
rm -rf frontend/prod-build

Zobrazit soubor

@ -23,7 +23,7 @@ if (Number(ats.env.PORT) === 6677) {
if (!item) {
continue;
}
Object.assign(item, prepareObject("did", item));
Object.assign(item, await prepareObject("did", item));
item.revs = [item.revs[item.revs.length - 1]];
ats.nats.publish(
`ats.api.did.${
@ -45,7 +45,7 @@ if (Number(ats.env.PORT) === 6677) {
if (!item) {
continue;
}
Object.assign(item, prepareObject("pds", item));
Object.assign(item, await prepareObject("pds", item));
ats.nats.publish(
`ats.api.pds.${sub === "ats.service.pds.create" ? "create" : "update"}`,
codec.encode(item),
@ -112,7 +112,7 @@ function getPDSStatus(item) {
return offlineNum > 0 ? "degraded" : "online";
}
function prepareObject(type, item) {
async function prepareObject(type, item) {
switch (type) {
case "pds":
item.host = item.url.replace(/^https?:\/\//, "");
@ -141,6 +141,21 @@ function prepareObject(type, item) {
item.srcHost = item.src.replace(/^https?:\/\//, "");
item.fed = findDIDFed(item);
break;
case "bgs":
item.host = item.url.replace(/^https?:\/\//, "");
item.status = await ats.redis.hGetAll(`ats:bgs:${item.host}`);
break;
case "plc":
item.host = item.url.replace(/^https?:\/\//, "");
item.didsCount = await ats.db.did.countDocuments({ src: item.url });
item.pdsCount = await ats.db.pds.countDocuments({
plcs: { $in: [item.url] },
});
item.lastUpdate =
(await ats.db.meta.findOne({ key: `lastUpdate:${item.url}` })).value;
break;
}
return item;
}
@ -160,7 +175,7 @@ router
console.error("PDS without url? ", item);
continue;
}
Object.assign(item, prepareObject("pds", item));
Object.assign(item, await prepareObject("pds", item));
//item.didsCount = await ats.db.did.countDocuments({ 'pds': { $in: [ item.url ] }})
out.push(item);
}
@ -173,7 +188,7 @@ router
if (!item) {
return ctx.response.code = 404;
}
Object.assign(item, prepareObject("pds", item));
Object.assign(item, await prepareObject("pds", item));
ctx.response.body = item;
perf(ctx);
@ -309,7 +324,7 @@ router
const did
of (await ats.db.did.find(query).sort(sort).limit(limit).toArray())
) {
Object.assign(did, prepareObject("did", did));
Object.assign(did, await prepareObject("did", did));
out.push(did);
}
@ -330,6 +345,98 @@ router
: out;
perf(ctx);
})
.get("/bgs", async (ctx) => {
const items = await Promise.all(
ats.ecosystem.data["bgs-instances"].map(async (item) => {
return Object.assign(item, await prepareObject("bgs", item));
}),
);
ctx.response.body = items;
perf(ctx);
})
.get("/bgs/:host", async (ctx) => {
const item = ats.ecosystem.data["bgs-instances"].find((f) =>
f.url === "https://" + ctx.params.host
);
if (!item) {
return ctx.response.code = 404;
}
Object.assign(item, await prepareObject("bgs", item));
ctx.response.body = item;
perf(ctx);
})
.get("/bgs/:host/ops", async (ctx) => {
const item = ats.ecosystem.data["bgs-instances"].find((f) =>
f.url === "https://" + ctx.params.host
);
if (!item) {
return ctx.response.code = 404;
}
const host = item.url.replace(/^https?:\/\//, "");
const allowedRanges = ["1h", "24h", "7d", "30d"];
const rangesAggregation = ["15s", "5m", "30m", "2h"];
const userRange = ctx.request.url.searchParams.get("range");
const range = userRange && allowedRanges.includes(userRange)
? userRange
: "24h";
const aggregation = rangesAggregation[allowedRanges.indexOf(range)];
let query = `
from(bucket: "ats-nodes")
|> range(start: -${range})
|> filter(fn: (r) => r["_measurement"] == "firehose_event")
|> filter(fn: (r) => r["server"] == "snowden")
|> filter(fn: (r) => r["bgs"] == "${host}")
|> filter(fn: (r) => r["_field"] == "value")
|> derivative(unit: 1s, nonNegative: true)
|> aggregateWindow(every: ${aggregation}, fn: mean, createEmpty: true)`;
if (!ctx.request.url.searchParams.get("complex")) {
query += `
|> group(columns: ["_time"], mode:"by")
|> sum()
|> group()`;
}
const data = await ats.influxQuery.collectRows(query);
ctx.response.body = { range, aggregation, data };
perf(ctx);
})
.get("/bgs/:host/postLatency", async (ctx) => {
const item = ats.ecosystem.data["bgs-instances"].find((f) =>
f.url === "https://" + ctx.params.host
);
if (!item) {
return ctx.response.code = 404;
}
const host = item.url.replace(/^https?:\/\//, "");
const allowedRanges = ["1h", "24h", "7d", "30d"];
const rangesAggregation = ["15s", "5m", "30m", "2h"];
const userRange = ctx.request.url.searchParams.get("range");
const range = userRange && allowedRanges.includes(userRange)
? userRange
: "24h";
const aggregation = rangesAggregation[allowedRanges.indexOf(range)];
let query = `
from(bucket: "ats-nodes")
|> range(start: -${range})
|> filter(fn: (r) => r["_measurement"] == "post_latency")
|> filter(fn: (r) => r["server"] == "snowden")
|> filter(fn: (r) => r["bgs"] == "${host}")
|> filter(fn: (r) => r["_field"] == "value")
|> aggregateWindow(every: ${aggregation}, fn: mean, createEmpty: true)`;
if (!ctx.request.url.searchParams.get("complex")) {
query += `
|> group(columns: ["_time"], mode:"by")
|> sum()
|> group()`;
}
const data = await ats.influxQuery.collectRows(query);
ctx.response.body = { range, aggregation, data };
perf(ctx);
})
.get("/feds", (ctx) => {
ctx.response.body = ats.ecosystem.data.federations;
perf(ctx);
@ -356,21 +463,25 @@ router
ctx.response.body = item;
perf(ctx);
})
.get("/plc", async (ctx) => {
.get("/plcs", async (ctx) => {
const out = [];
for (const plc of ats.ecosystem.data["plc-directories"]) {
plc.host = plc.url.replace(/^https?:\/\//, "");
plc.didsCount = await ats.db.did.countDocuments({ src: plc.url });
plc.pdsCount = await ats.db.pds.countDocuments({
plcs: { $in: [plc.url] },
});
plc.lastUpdate =
(await ats.db.meta.findOne({ key: `lastUpdate:${plc.url}` })).value;
out.push(plc);
out.push(Object.assign(plc, await prepareObject("plc", plc)));
}
ctx.response.body = out;
perf(ctx);
})
.get("/plc/:host", async (ctx) => {
const item = ats.ecosystem.data['plc-directories'].find((f) =>
f.url === `https://${ctx.params.host}`
);
if (!item) {
return ctx.response.code = 404;
}
Object.assign(item, await prepareObject("plc", item));
ctx.response.body = item;
perf(ctx);
})
.get("/_metrics", async (ctx) => {
const metrics = {};
@ -449,7 +560,7 @@ router
if (!item) {
return ctx.status = 404;
}
Object.assign(item, prepareObject("did", item));
Object.assign(item, await prepareObject("did", item));
ctx.response.body = item;
perf(ctx);
});

Zobrazit soubor

@ -0,0 +1,90 @@
import {
ComAtprotoSyncSubscribeRepos,
subscribeRepos,
} from "npm:atproto-firehose";
import * as atprotoApi from "npm:@atproto/api";
const HTTP_PORT = Deno.env.get("ATSCAN_FIREHOSE_PORT") || "6990";
const SERVER = Deno.env.get("ATSCAN_FIREHOSE_SERVER") || "hex";
const BGS_HOSTNAME = Deno.env.get("ATSCAN_FIREHOSE_BGS_HOSTNAME") ||
"bsky.social";
const TICK_REPO = Deno.env.get("ATSCAN_FIREHOSE_TICK_REPO") ||
"did:plc:pzovq4a22hpji6pfzofgk7gc";
const TICK_ARRAY_SIZE = parseInt(
Deno.env.get("ATSCAN_FIREHOSE_TICK_ARRAY_SIZE") || 5,
);
//const TICK_POST_PATH = Deno.env.get('ATSCAN_FIREHOSE_TICK_POST') || 'app.bsky.feed.post/3k2bwjgozws2q';
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
const router = new Router();
let totalCommits = 0;
const counters = {};
const postLatency = [];
const client = subscribeRepos(`wss://${BGS_HOSTNAME}`, { decodeRepoOps: true });
client.on("message", (m) => {
const receivedTime = new Date();
if (ComAtprotoSyncSubscribeRepos.isHandle(m)) {
console.log("handle", m);
}
if (ComAtprotoSyncSubscribeRepos.isCommit(m)) {
m.ops.forEach(async (op) => {
if (!counters[op.action]) {
counters[op.action] = {};
}
let type = op.payload?.$type;
if (op.action === "delete") {
const delMatch = op.path.match(/^([^\/]+)\//);
if (delMatch) {
type = delMatch[1];
}
}
if (
m.repo === TICK_REPO && op.path.startsWith("app.bsky.feed.post/") &&
op.action === "create"
) {
const postDate = new Date(op.payload.createdAt);
const latency = receivedTime - postDate;
if (postLatency.length >= TICK_ARRAY_SIZE) {
postLatency.shift();
}
postLatency.push(latency);
console.log(JSON.stringify(postLatency));
}
if (type) {
if (!counters[op.action][type]) {
counters[op.action][type] = 0;
}
counters[op.action][type]++;
}
});
totalCommits++;
}
});
router
.get("/counters", (ctx) => {
ctx.response.body = counters;
})
.get("/_metrics", (ctx) => {
const avgLatency = postLatency.reduce((ps, a) => ps + a, 0) /
postLatency.length;
ctx.response.body = Object.keys(counters).map((mod) => {
return Object.keys(counters[mod]).map((type) => {
const val = counters[mod][type];
return `firehose_event{server="${SERVER}",bgs="${BGS_HOSTNAME}",mod="${mod}",type="${type}"} ${val}`;
}).filter((v) => v.trim()).join("\n");
}).join("\n") + "\n" +
`post_latency{server="${SERVER}",bgs="${BGS_HOSTNAME}"} ${avgLatency}` +
"\n";
});
app.use(router.routes());
app.listen({ port: HTTP_PORT });
console.log(`ATScan Firehose metrics API started at port ${HTTP_PORT}`);

Zobrazit soubor

@ -9,6 +9,8 @@ import * as atprotoApi from "npm:@atproto/api";
const { AppBskyActorProfile } = atprotoApi.default;
const HTTP_PORT = "6990";
const SERVER = "hex";
const BGS_HOSTNAME = "bsky.social";
const ats = new ATScan({ enableQueues: true });
ats.debug = true;
@ -19,9 +21,10 @@ import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
const router = new Router();
let totalCommits = 0;
const counters = {};
const client = subscribeRepos(`wss://bsky.social`, { decodeRepoOps: true });
const client = subscribeRepos(`wss://${BGS_HOSTNAME}`, { decodeRepoOps: true });
client.on("message", (m) => {
if (ComAtprotoSyncSubscribeRepos.isHandle(m)) {
console.log("handle", m);
@ -31,11 +34,18 @@ client.on("message", (m) => {
if (!counters[op.action]) {
counters[op.action] = {};
}
if (op.payload?.$type) {
if (!counters[op.action][op.payload.$type]) {
counters[op.action][op.payload.$type] = 0;
let type = op.payload?.$type;
if (op.action === "delete") {
const delMatch = op.path.match(/^([^\/]+)\//);
if (delMatch) {
type = delMatch[1];
}
counters[op.action][op.payload.$type]++;
}
if (type) {
if (!counters[op.action][type]) {
counters[op.action][type] = 0;
}
counters[op.action][type]++;
}
if (op.payload?.$type === "app.bsky.actor.profile") {
if (AppBskyActorProfile.isRecord(op.payload)) {
@ -55,6 +65,7 @@ client.on("message", (m) => {
}
}
});
totalCommits++;
}
});
@ -66,7 +77,7 @@ router
ctx.response.body = Object.keys(counters).map((mod) => {
return Object.keys(counters[mod]).map((type) => {
const val = counters[mod][type];
return `firehose_event{mod="${mod}",type="${type}"} ${val}`;
return `firehose_event{server="${SERVER}",bgs="${BGS_HOSTNAME}",mod="${mod}",type="${type}"} ${val}`;
}).filter((v) => v.trim()).join("\n");
}).join("\n") + "\n";
});

Zobrazit soubor

@ -1,61 +1,92 @@
import { ATScan } from "./lib/atscan.js";
import whoiser from "npm:whoiser";
const wait = 60 * 1;
const indexers = {
pdsIndex: {
interval: 60 * 1000, // 1 min
handler: async (ats) => {
for (const pds of await ats.db.pds.find().toArray()) {
const didsCount = await ats.db.did.countDocuments({
"pds": { $in: [pds.url] },
});
const host = pds.url.replace(/^https?:\/\//, "");
async function index(ats) {
for (const pds of await ats.db.pds.find().toArray()) {
const didsCount = await ats.db.did.countDocuments({
"pds": { $in: [pds.url] },
});
const host = pds.url.replace(/^https?:\/\//, "");
const stages = [
{ $match: { pds: { $in: [pds.url] } } },
{
$group: {
_id: "$groupField",
sum: {
$sum: "$repo.size",
const stages = [
{ $match: { pds: { $in: [pds.url] } } },
{
$group: {
_id: "$groupField",
sum: {
$sum: "$repo.size",
},
},
},
},
},
];
const sizeRes = await ats.db.did.aggregate(stages).toArray();
const size = sizeRes[0].sum;
];
const sizeRes = await ats.db.did.aggregate(stages).toArray();
const size = sizeRes[0].sum;
await ats.db.pds.updateOne({ url: pds.url }, { $set: { didsCount, size } });
await ats.writeInflux("pds_dids_count", "intField", didsCount, [[
"pds",
host,
]]);
await ats.writeInflux("pds_size", "intField", size, [["pds", host]]);
ats.nats.publish(
"ats.service.pds.update",
ats.JSONCodec.encode({ url: pds.url }),
);
}
console.log("indexer round finished");
//console.log(await whoiser("dev.otaso-sky.blue"));
}
if (Deno.args[0] === "daemon") {
console.log("Initializing ATScan ..");
const ats = new ATScan({ enableNats: true });
ats.debug = true;
await ats.init();
console.log("indexer daemon started");
console.log("Performing initial index round ..");
// initial crawl
await index(ats);
console.log(`Initial index round done`);
ats.debug = false;
console.log(`Processing [wait=${wait}s] ..`);
setInterval(() => index(ats), wait * 1000);
} else {
const ats = new ATScan({ enableNats: true, debug: true });
await ats.init();
await index(ats);
Deno.exit(0);
await ats.db.pds.updateOne({ url: pds.url }, {
$set: { didsCount, size },
});
await ats.writeInflux("pds_dids_count", "intField", didsCount, [[
"pds",
host,
]]);
await ats.writeInflux("pds_size", "intField", size, [["pds", host]]);
ats.nats.publish(
"ats.service.pds.update",
ats.JSONCodec.encode({ url: pds.url }),
);
}
//console.log("indexer round finished");
},
},
bgsIndex: {
interval: 10 * 1000,
handler: async (ats) => {
await Promise.all(ats.ecosystem.data["bgs-instances"].map(async (bgs) => {
const host = bgs.url.replace(/^https?:\/\//, "");
const query = `
from(bucket: "ats-nodes")
|> range(start: -1m)
|> filter(fn: (r) => r["_measurement"] == "firehose_event")
|> filter(fn: (r) => r["server"] == "snowden")
|> filter(fn: (r) => r["bgs"] == "${host}")
|> filter(fn: (r) => r["_field"] == "value")
|> group(columns: ["_time"], mode:"by")
|> sum()
|> group()
|> derivative(unit: 1s, nonNegative: true)
|> mean()`;
const data = await ats.influxQuery.collectRows(query);
const value = data[0]?._value;
if (value) {
await ats.redis.HSET(`ats:bgs:${host}`, "ops", value); // 'OK'
}
}));
},
},
};
// start
console.log("Initializing ATScan ..");
const ats = new ATScan({ enableNats: true });
ats.debug = true;
await ats.init();
console.log("indexer daemon started");
// initial crawl
console.log("Performing initial index round ..");
await Promise.all(Object.keys(indexers).map((k) => indexers[k].handler(ats)));
console.log(`Initial index round done`);
// intervals
const intervals = [];
for (const key of Object.keys(indexers)) {
const indexer = indexers[key];
const interval = indexer.interval || 60 * 1000;
console.log(`Setting up indexer: ${key} interval=${interval / 1000}s`);
intervals.push(setInterval(() => indexer.handler(ats), interval));
}

Zobrazit soubor

@ -3,6 +3,7 @@ import { load as envLoad } from "https://deno.land/std@0.192.0/dotenv/mod.ts";
import { parse, stringify } from "https://deno.land/std@0.192.0/yaml/mod.ts";
import { MongoClient } from "npm:mongodb";
import { InfluxDB } from "npm:@influxdata/influxdb-client";
import { createClient as redisCreateClient } from "npm:redis@^4.6";
import { makeQueues } from "./queues.js";
import {
connect as NATSConnect,
@ -16,18 +17,28 @@ export class ATScan {
this.debug = opts.debug;
this.enableQueues = opts.enableQueues || false;
this.enableNats = opts.enableNats || false;
console.log(this.enableQueues);
//console.log(this.enableQueues);
}
async init() {
this.env = Object.assign(Deno.env.toObject(), await envLoad());
await this.ecosystemLoad();
// redis
const redisUrl = "redis://localhost:6379";
this.redis = redisCreateClient({
url: redisUrl,
pingInterval: 1000,
});
await this.redis.connect();
console.log(`Connected to Redis: ${redisUrl}`);
// influxdb
const influxConfig = {
url: this.env.INFLUXDB_HOST,
token: this.env.INFLUXDB_TOKEN,
};
this.influx = new InfluxDB(influxConfig);
this.influxQuery = this.influx.getQueryApi(this.env.INFLUXDB_ORG);
// monbodb
this.client = new MongoClient(this.env.MONGODB_URL);
await this.client.connect();
this.dbRaw = this.client.db("test");
@ -37,6 +48,7 @@ export class ATScan {
meta: this.dbRaw.collection("meta"),
};
console.log(`Connected to MongoDB: ${this.env.MONGODB_URL}`);
// nats - optional
if (this.enableNats) {
await (async () => {
this.nats = await NATSConnect({

29
backend/lib/interval.js Normal file
Zobrazit soubor

@ -0,0 +1,29 @@
export function Interval(fn, duration, ...args) {
const _this = this;
this.baseline = undefined;
this.run = function (flag) {
if (_this.baseline === undefined) {
_this.baseline = performance.now() - duration;
}
if (flag) {
fn(...args);
}
const end = performance.now();
_this.baseline += duration;
let nextTick = duration - (end - _this.baseline);
if (nextTick < 0) {
nextTick = 0;
}
//console.log(nextTick);
_this.timer = setTimeout(function () {
_this.run(true);
}, nextTick);
};
this.stop = function () {
clearTimeout(_this.timer);
};
}

52
backend/tick.js Normal file
Zobrazit soubor

@ -0,0 +1,52 @@
import * as atprotoApi from "npm:@atproto/api";
import "https://deno.land/std@0.193.0/dotenv/load.ts";
import { Interval } from "./lib/interval.js";
const ENV = Deno.env.get("ATSCAN_TICK_ENV") || "SANDBOX";
const options = {
identifier: Deno.env.get(`ATSCAN_TICK_${ENV}_HANDLE`),
password: Deno.env.get(`ATSCAN_TICK_${ENV}_PASSWORD`),
};
const TICK_INTERVAL = parseInt(
Deno.env.get(`ATSCAN_TICK_${ENV}_INTERVAL`) || 1000,
);
const TICK_PDS = Deno.env.get(`ATSCAN_TICK_${ENV}_PDS`) || "test-pds.gwei.cz";
const TICK_REPO = Deno.env.get(`ATSCAN_TICK_${ENV}_REPO`) ||
"did:plc:pzovq4a22hpji6pfzofgk7gc";
//const TICK_POST = Deno.env.get(`ATSCAN_TICK_${ENV}_POST`) || '3k2bwjgozws2q';
const agent = new atprotoApi.default.BskyAgent({
service: `https://${TICK_PDS}`,
});
await agent.login(options);
console.log(`Logged in as "${options.identifier}" [pds=${TICK_PDS}] ...`);
console.log(`Ticking in interval: ${TICK_INTERVAL}ms ..`);
const repo = TICK_REPO;
//const rkey = TICK_POST;
/*const resp = await agent.getTimeline({ limit: 100 });
for (const { post } of resp.data.feed) {
if (post.author.did === repo) {
await agent.deletePost(post.uri)
}
}*/
const interval = new Interval(async () => {
const time = (new Date()).toISOString();
await agent.com.atproto.repo.createRecord({
repo,
collection: "app.bsky.feed.post",
//rkey,
record: {
$type: "app.bsky.feed.post",
text: time,
createdAt: time,
},
});
/*await agent.app.bsky.feed.post.delete({
repo,
rkey,
})*/
}, TICK_INTERVAL);
interval.run();

Zobrazit soubor

@ -1,6 +1,6 @@
{
"name": "atscan-fe",
"version": "0.7.4-alpha",
"version": "0.8.0-alpha",
"private": true,
"scripts": {
"dev": "vite dev",

Zobrazit soubor

@ -0,0 +1,47 @@
<script>
import { onMount, beforeUpdate, afterUpdate } from 'svelte';
export let src;
let loaded = false;
let failed = false;
let loading = false;
let last = src;
let img;
beforeUpdate(() => {
if (src !== last) {
img.src = src;
last = src;
loading = true;
loaded = false;
console.log('loading again');
}
});
function processImage() {
img = new Image();
img.src = src;
loading = true;
img.onload = () => {
loading = false;
console.log('loaded');
loaded = true;
};
img.onerror = () => {
loading = false;
failed = true;
};
}
onMount(() => {
processImage();
});
</script>
{#if loaded}
<img {src} class="ratio-square w-full h-full rounded-full object-contains" />
{:else if loading}
<div class="bg-white/20 w-full h-full rounded-full" />
{/if}

Zobrazit soubor

@ -0,0 +1,157 @@
<script>
import {
TabGroup,
Tab,
TabAnchor,
ProgressRadial,
SlideToggle,
dataTableHandler
} from '@skeletonlabs/skeleton';
import Chart from '$lib/components/Chart.svelte';
import { formatNumber } from '$lib/utils';
import { writable } from 'svelte/store';
import { request } from '$lib/api';
import { onDestroy } from 'svelte';
export let title;
export let endpoint;
export let height = 'h-[40vh]';
export let processSeries = (data) =>
data ? [{ name: title, type: 'line', data: data.map((r) => r._value) }] : [];
export let unit = 'op/s';
export let valueFormatter = (val) => `${val !== undefined ? formatNumber(val) : '-'} ${unit}`;
export let axisDataProcess = (data) =>
data
.filter((r) => r.table === 0)
.map((r) => r._time)
.slice(1, -1) || [];
export let ranges = ['1h', '24h', '7d', '30d'];
export let chartType = null;
export let tabState = writable(0);
const chartActivityTab = tabState;
let chartActivity = null;
function renderActivityChart(chartData) {
let types = [];
let typesCount = {};
if (chartType === 'complex') {
for (const ci of chartData) {
const key = `${ci.mod}:${ci.type}`;
if (!types.includes(key)) {
types.push(key);
}
if (!typesCount[key]) {
typesCount[key] = 0;
}
typesCount[key] += ci._value;
}
}
types = types.sort((x, y) => (typesCount[x] < typesCount[y] ? 1 : -1));
return {
animationDuration: 250,
tooltip: {
trigger: 'axis',
valueFormatter
/*position: function (point, params, dom, rect, size) {
// fixed at top
return [point[0] + 50, point[1] + 50];
},*/
},
/*legend: {
data: types.map((c) => {
let [ mod, type ] = c.split(':')
return `${type.replace(/^app.bsky./, '')} (${mod})`
})
},*/
grid: {
left: '2%',
right: '2%',
bottom: '3%',
top: '10%',
containLabel: true
},
toolbox: {
feature: {
magicType: { show: true, type: ['stack', 'tiled'] },
saveAsImage: {}
}
},
xAxis: {
type: 'category',
//boundaryGap: false,
data: axisDataProcess(chartData)
},
yAxis: {
type: 'value',
axisLabel: {
formatter: `{value} ${unit}`
}
},
series: processSeries(chartData, types)
/*||chartType === 'complex' ?
types.map()
: [{
name: item.host,
type: 'line',
data: chartData.map((r) => r._value) || []
}]*/
};
}
let chartInterval = null;
chartActivityTab.subscribe(async (num) => {
if (!endpoint) {
return null;
}
chartActivity = null;
if (chartInterval) {
clearInterval(chartInterval);
}
async function render() {
const data = await request(
fetch,
`${endpoint}?range=${ranges[num]}&complex=${chartType === 'complex' ? 1 : ''}`
);
if (data) {
chartActivity = renderActivityChart(data.data);
}
}
render();
chartInterval = setInterval(() => {
render();
}, 15 * 1000);
});
onDestroy(() => {
if (chartInterval) {
clearInterval(chartInterval);
}
});
</script>
<h2 class="h2">{title}</h2>
<TabGroup>
<Tab bind:group={$chartActivityTab} name="tab1" value={0}>Last 1h</Tab>
<Tab bind:group={$chartActivityTab} name="tab2" value={1}>Last 24h</Tab>
<Tab bind:group={$chartActivityTab} name="tab3" value={2}>Last 7d</Tab>
<Tab bind:group={$chartActivityTab} name="tab4" value={3}>Last 30d</Tab>
<!-- Tab Panels --->
<svelte:fragment slot="panel">
<div class="w-full {height}">
{#if chartActivity}
<Chart options={chartActivity} />
{:else}
<div class="flex items-center justify-center w-full h-full">
<div>
<ProgressRadial />
</div>
</div>
{/if}
</div>
</svelte:fragment>
</TabGroup>

Zobrazit soubor

@ -9,7 +9,9 @@
pds: { url: `${data.config.api}/pds/%`, key: 'host' },
did: { url: `${data.config.api}/%`, key: 'did' },
client: { url: `${data.config.api}/client/%`, key: 'id' },
fed: { url: `${data.config.api}/fed/%`, key: 'id' }
fed: { url: `${data.config.api}/fed/%`, key: 'id' },
bgs: { url: `${data.config.api}/bgs/%`, key: 'host' },
plc: { url: `${data.config.api}/plc/%`, key: 'host' }
};
const config = models[model];
const url = config.url.replace('%', data.item[config.key]);

Zobrazit soubor

@ -1,6 +1,6 @@
<script>
import { blobUrl } from '$lib/utils';
import { is_empty } from 'svelte/internal';
import Avatar from '$lib/components/Avatar.svelte';
export let items;
export let data;
@ -26,12 +26,11 @@
<div class="flex gap-4 bg-surface-500/10 p-4" id={item.did}>
<div class="w-20 h-20 shrink-0">
<a href="/{item.did}" class="w-full h-full">
<img
<Avatar
id="image-{item.did}"
src={item.repo?.profile?.avatar?.ref?.$link
? blobUrl(item.did, item.repo?.profile?.avatar?.ref?.$link)
: '/avatar.svg'}
class="aspect-square object-cover rounded-full w-full h-full"
/>
</a>
</div>

Zobrazit soubor

@ -14,11 +14,12 @@ export function identicon(...args) {
}
numbro.setDefaults({
thousandSeparated: true
//mantissa: 2
thousandSeparated: true,
mantissa: 2,
trimMantissa: true
});
export function formatNumber(number) {
return numbro(number).format();
export function formatNumber(number, ...args) {
return numbro(number).format(...args);
}
export function customTableMapper(source, keys, process) {

Zobrazit soubor

@ -40,7 +40,9 @@
[
{ title: 'DIDs', url: '/dids' },
{ title: 'PDS Instances', url: '/pds' },
{ title: 'Federations', url: '/feds' }
{ title: 'BGS Instances', url: '/bgs' },
{ title: 'PLC Directories', url: '/plcs' }
//{ title: 'Federations', url: '/feds' }
],
[
{ title: 'API', url: '/api' },
@ -145,17 +147,33 @@
href="/pds"
class="btn hover:variant-soft-primary"
class:bg-primary-active-token={$page.url.pathname.startsWith('/pds')}
><span>{$i18n.t('PDS Instances')}</span></a
><span>{$i18n.t('PDS')}</span></a
>
</div>
<div class="relative hidden lg:block">
<a
href="/bgs"
class="btn hover:variant-soft-primary"
class:bg-primary-active-token={$page.url.pathname === '/bgs' ||
$page.url.pathname.startsWith('/bgs/')}><span>BGS</span></a
>
</div>
<div class="relative hidden lg:block">
<a
href="/plcs"
class="btn hover:variant-soft-primary"
class:bg-primary-active-token={$page.url.pathname === '/plcs' ||
$page.url.pathname.startsWith('/plcs/')}><span>PLC</span></a
>
</div>
<!--div class="relative hidden lg:block">
<a
href="/feds"
class="btn hover:variant-soft-primary"
class:bg-primary-active-token={$page.url.pathname === '/feds' ||
$page.url.pathname.startsWith('/fed/')}><span>Federations</span></a
>
</div>
</div-->
<!--div class="relative hidden lg:block">
<a
href="/clients"

Zobrazit soubor

@ -0,0 +1,7 @@
import { request } from '$lib/api';
export async function load({ fetch }) {
return {
bgs: request(fetch, '/bgs')
};
}

Zobrazit soubor

@ -0,0 +1,38 @@
<script>
import Table from '$lib/components/Table.svelte';
import { dataTableHandler, tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
import { dateDistance, formatNumber, customTableMapper } from '$lib/utils.js';
import BasicPage from '$lib/components/BasicPage.svelte';
export let data;
function tableMap({ val, key, row }) {
if (key === 'world') {
val = `<span class="badge variant-filled bg-ats-fed-${row.id} dark:bg-ats-fed-${row.id} opacity-70 text-white dark:text-black ucfirst">${row.id}</span>`;
}
if (key === 'host') {
val = `<span class="text-xl font-semibold">${row.url.replace(/^https?:\/\//, '')}</span>`;
}
if (key === 'url_raw') {
val = `/bgs/${row.host}`;
}
if (key === 'ops') {
val = row.status?.ops
? formatNumber(row.status?.ops, { trimMantissa: true, mantissa: 2 }) + ' op/s'
: '-';
}
return val;
}
const sourceData = data.bgs;
const tableSimple = {
// A list of heading labels.
head: ['Federation', 'Host', 'Activity'],
body: customTableMapper(sourceData, ['world', 'host', 'ops'], tableMap),
meta: customTableMapper(sourceData, ['id', 'url_raw'], tableMap)
};
</script>
<BasicPage {data} title="BGS Instances">
<Table source={tableSimple} />
</BasicPage>

Zobrazit soubor

@ -0,0 +1,7 @@
import { request } from '$lib/api';
export async function load({ params, fetch }) {
return {
item: request(fetch, `/bgs/${params.host}`)
};
}

Zobrazit soubor

@ -0,0 +1,58 @@
<script>
import Breadcrumb from '$lib/components/Breadcrumb.svelte';
import SourceSection from '$lib/components/SourceSection.svelte';
import BasicPage from '$lib/components/BasicPage.svelte';
import BasicChart from '$lib/components/BasicChart.svelte';
import { writable } from 'svelte/store';
export let data;
const tabState = writable(0);
function activityMapSeries(data, types) {
return types.map((key) => {
let [mod, type] = key.split(':');
return {
name: `${type.replace(/^app.bsky./, '')} (${mod})`,
type: 'line',
data:
data
.filter((i) => i.mod === mod && i.type === type)
.map((r) => r._value)
.slice(1, -1) || [],
lineStyle: {
normal: {
width: 1
}
},
stack: 'x',
areaStyle: {
opacity: 0.5
},
smooth: false
};
});
}
</script>
<BasicPage {data} title={data.item.host} breadcrumb={[{ label: 'BGS Instances', link: '/bgs' }]}>
<BasicChart
title="Activity"
endpoint="/bgs/{data.item.host}/ops"
chartType="complex"
processSeries={activityMapSeries}
{tabState}
/>
<BasicChart
title="Post latency"
endpoint="/bgs/{data.item.host}/postLatency"
unit="ms"
height="h-[20vh]"
{tabState}
/>
<SourceSection {data} model="bgs" hide="true" />
<div class="min-h-screen" />
</BasicPage>

Zobrazit soubor

@ -125,8 +125,8 @@
return {
animationDuration: 250,
tooltip: {
trigger: 'axis'
//formatter: '{b}: {c} ms'
trigger: 'axis',
valueFormatter: (val) => `${val !== undefined ? formatNumber(val) : '-'} ms`
},
legend: {
data: Object.keys(crawlers).map((c) => `${crawlers[c].region} (${crawlers[c].location})`)
@ -145,7 +145,11 @@
xAxis: {
type: 'category',
boundaryGap: false,
data: chartData.filter((r) => r.table === 0).map((r) => r._time) || []
data:
chartData
.filter((r) => r.table === 0)
.map((r) => r._time)
.slice(0, -1) || []
},
yAxis: {
type: 'value',
@ -159,7 +163,11 @@
name: `${crawlerOptions.region} (${crawlerOptions.location})`,
type: 'line',
//stack: 'ms',
data: chartData.filter((r) => r.crawler === crawler).map((r) => r._value) || []
data:
chartData
.filter((r) => r.crawler === crawler)
.map((r) => r._value)
.slice(0, -1) || []
};
})
};

Zobrazit soubor

@ -0,0 +1,7 @@
import { request } from '$lib/api';
export async function load({ params, fetch }) {
return {
item: request(fetch, `/plc/${params.host}`)
};
}

Zobrazit soubor

@ -0,0 +1,13 @@
<script>
import Breadcrumb from '$lib/components/Breadcrumb.svelte';
import SourceSection from '$lib/components/SourceSection.svelte';
import BasicPage from '$lib/components/BasicPage.svelte';
export let data;
const item = data.item;
</script>
<BasicPage {data} title={item.host} breadcrumb={[{ label: 'PLC Directories', link: '/plcs' }]}>
<SourceSection {data} model="plc" />
</BasicPage>

Zobrazit soubor

@ -0,0 +1,7 @@
import { request } from '$lib/api';
export async function load({ fetch }) {
return {
plcs: request(fetch, '/plcs')
};
}

Zobrazit soubor

@ -0,0 +1,52 @@
<script>
import Table from '$lib/components/Table.svelte';
import { dataTableHandler, tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
import { dateDistance, formatNumber, customTableMapper } from '$lib/utils.js';
import BasicPage from '$lib/components/BasicPage.svelte';
export let data;
function tableMap({ val, key, row }) {
console.log({ key, val });
if (key === 'world') {
val = `<span class="badge variant-filled bg-ats-fed-${row.federation} dark:bg-ats-fed-${row.federation} opacity-70 text-white dark:text-black ucfirst">${row.federation}</span>`;
}
if (key === 'host') {
val = `<span class="text-xl font-semibold">${val}</span>`;
}
if (key === 'didsCount') {
val = `<a href="/dids?q=fed:${row.federation}" class="anchor">${formatNumber(val)}</a>`;
}
if (key === 'pdsCount') {
val = `<a href="/pds?q=fed:${row.federation}" class="anchor">${
row.host === 'plc.directory' ? 1 : formatNumber(val)
}</a>`;
if (row.host === 'plc.directory') {
val += ` <div class="text-xs inline-block ml-2">(+${formatNumber(row.pdsCount)})</div>`;
}
}
if (key === 'time') {
val = row.lastUpdate ? `<span class="text-xs">${dateDistance(row.lastUpdate)} ago</a>` : '-';
}
if (key === 'url_raw') {
val = `/plc/${row.host}`;
}
return val;
}
const sourceData = data.plcs.sort((x, y) => (x.didsCount > y.didsCount ? -1 : 1));
const tableSimple = {
// A list of heading labels.
head: ['Federation', 'Host', 'DIDs', 'PDS', 'Last mod'],
body: customTableMapper(
sourceData,
['world', 'host', 'didsCount', 'pdsCount', 'time'],
tableMap
),
meta: customTableMapper(sourceData, ['id', 'url_raw'], tableMap)
};
</script>
<BasicPage {data} title="PLC Directories">
<Table source={tableSimple} />
</BasicPage>

Zobrazit soubor

@ -68,7 +68,20 @@ module.exports = {
script: "./backend/repo-worker.js",
interpreter: "mullvad-exclude",
interpreterArgs: "deno run --unstable --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-sys ./backend/repo-worker.js",
instances: 4,
instances: 6,
}, {
name: "atscan-tick-sandbox",
script: "./backend/tick.js",
interpreter: "deno",
interpreterArgs: "run --unstable --allow-net --allow-read --allow-env"
}, {
name: "atscan-tick-bluesky",
script: "./backend/tick.js",
interpreter: "deno",
interpreterArgs: "run --unstable --allow-net --allow-read --allow-env",
env: {
ATSCAN_TICK_ENV: 'BLUESKY'
}
}, {
name: "bull-ui",
script: "index.js",