zrcadlo https://github.com/atscan/atscan
Porovnat revize
6 Commity
36292e9eda
...
a46f39184c
Autor | SHA1 | Datum |
---|---|---|
tree | a46f39184c | |
tree | fad6c18de7 | |
tree | 89e1fd7e0d | |
tree | ba593bd9f7 | |
tree | a8c48dca4c | |
tree | 24f6e61dd6 |
10
.env.example
10
.env.example
|
@ -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
|
5
Makefile
5
Makefile
|
@ -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
|
||||
|
|
143
backend/api.js
143
backend/api.js
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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}`);
|
|
@ -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";
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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();
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "atscan-fe",
|
||||
"version": "0.7.4-alpha",
|
||||
"version": "0.8.0-alpha",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -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]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { request } from '$lib/api';
|
||||
|
||||
export async function load({ fetch }) {
|
||||
return {
|
||||
bgs: request(fetch, '/bgs')
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
import { request } from '$lib/api';
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
return {
|
||||
item: request(fetch, `/bgs/${params.host}`)
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -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) || []
|
||||
};
|
||||
})
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { request } from '$lib/api';
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
return {
|
||||
item: request(fetch, `/plc/${params.host}`)
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
import { request } from '$lib/api';
|
||||
|
||||
export async function load({ fetch }) {
|
||||
return {
|
||||
plcs: request(fetch, '/plcs')
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -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",
|
||||
|
|
Načítá se…
Odkázat v novém úkolu