first commit
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 ATScan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,16 @@
|
|||
.PHONY: all
|
||||
|
||||
all: plc-crawl
|
||||
|
||||
plc-daemon:
|
||||
deno run --unstable --allow-net ./backend/plc-crawler.js daemon
|
||||
|
||||
plc-init:
|
||||
deno run --unstable --allow-net ./backend/plc-crawler.js init
|
||||
|
||||
plc-crawl:
|
||||
deno run --unstable --allow-net ./backend/plc-crawler.js
|
||||
|
||||
test:
|
||||
deno test --unstable --allow-read ./backend/test.js
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# ATScan
|
||||
|
||||
This is a monorepo containing the backend and frontend of ATScan. The current version is hosted on [atscan.net](https://atscan.net).
|
||||
|
||||
## Authors
|
||||
- Tree ([burningtree](https://github.com/burningtree))
|
||||
|
||||
## License
|
||||
MIT
|
|
@ -0,0 +1 @@
|
|||
IPINFO_TOKEN=XXXXX
|
|
@ -0,0 +1 @@
|
|||
.env
|
|
@ -0,0 +1,79 @@
|
|||
import { ATScan } from "./lib/atscan.js";
|
||||
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
|
||||
import { oakCors } from "https://deno.land/x/cors/mod.ts";
|
||||
|
||||
const ats = new ATScan();
|
||||
await ats.init();
|
||||
|
||||
const HTTP_PORT = 6677;
|
||||
const app = new Application();
|
||||
|
||||
const router = new Router();
|
||||
router
|
||||
.get("/pds", async (ctx) => {
|
||||
const out = []
|
||||
for (const item of (await ats.db.pds.find({}).toArray())) {
|
||||
item.host = item.url.replace(/^https?:\/\//, "");
|
||||
item.env = (ats.BSKY_OFFICIAL_PDS.includes(item.url) &&
|
||||
item.plcs.includes("https://plc.directory"))
|
||||
? "bsky"
|
||||
: (item.plcs.includes("https://plc.bsky-sandbox.dev")
|
||||
? "sandbox"
|
||||
: null);
|
||||
//item.didsCount = await ats.db.did.countDocuments({ 'pds': { $in: [ item.url ] }})
|
||||
out.push(item)
|
||||
}
|
||||
ctx.response.body = out.filter((i) => i.env)
|
||||
})
|
||||
.get("/plc", async (ctx) => {
|
||||
const out = []
|
||||
for (const plc of ats.defaultPLC) {
|
||||
plc.host = plc.url.replace(/^https?:\/\//, "");
|
||||
plc.didsCount = await ats.db.did.countDocuments({ src: plc.url })
|
||||
plc.lastUpdate = (await ats.db.meta.findOne({ key: `lastUpdate:${plc.url}` })).value
|
||||
out.push(plc)
|
||||
}
|
||||
ctx.response.body = out;
|
||||
})
|
||||
.get("/did", async (ctx) => {
|
||||
const out = []
|
||||
for (const did of (await ats.db.did.find({}).sort({ time: -1 }).limit(100).toArray())) {
|
||||
did.srcHost = did.src.replace(/^https?:\/\//, "");
|
||||
out.push(did)
|
||||
}
|
||||
ctx.response.body = out;
|
||||
})
|
||||
.get("/:id", async (ctx) => {
|
||||
if (!ctx.params.id.match(/^did\:/)) {
|
||||
return ctx.status = 404;
|
||||
}
|
||||
const did = ctx.params.id
|
||||
const item = await ats.db.did.findOne({ did })
|
||||
item.env = ((item.src === "https://plc.directory")
|
||||
? "bsky"
|
||||
: (item.src === "https://plc.bsky-sandbox.dev")
|
||||
? "sbox"
|
||||
: null);
|
||||
ctx.response.body = item
|
||||
})
|
||||
.get("/pds/:host", async (ctx) => {
|
||||
const host = ctx.params.host
|
||||
const item = await ats.db.pds.findOne({ url: `https://${host}` })
|
||||
item.host = item.url.replace(/^https?:\/\//, "");
|
||||
item.env = (ats.BSKY_OFFICIAL_PDS.includes(item.url) &&
|
||||
item.plcs.includes("https://plc.directory"))
|
||||
? "bsky"
|
||||
: (item.plcs.includes("https://plc.bsky-sandbox.dev")
|
||||
? "sandbox"
|
||||
: null);
|
||||
ctx.response.body = item
|
||||
})
|
||||
.get("/", ctx => {
|
||||
ctx.response.body = "ATScan API"
|
||||
})
|
||||
|
||||
app.use(oakCors()); // Enable CORS for All Routes
|
||||
app.use(router.routes());
|
||||
app.listen({ port: HTTP_PORT });
|
||||
|
||||
console.log(`ATScan API started at port ${HTTP_PORT}`);
|
|
@ -0,0 +1,99 @@
|
|||
import { Bson, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import { parse, stringify } from "https://deno.land/std@0.184.0/yaml/mod.ts";
|
||||
|
||||
const BSKY_OFFICIAL_PDS = [
|
||||
"https://bsky.social",
|
||||
];
|
||||
|
||||
export class ATScan {
|
||||
constructor(opts = {}) {
|
||||
this.verbose = opts.verbose;
|
||||
this.debug = opts.debug;
|
||||
this.defaultPLC = parse(Deno.readTextFileSync('./spec/plc.yaml'))
|
||||
this.BSKY_OFFICIAL_PDS = BSKY_OFFICIAL_PDS;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.client = new MongoClient();
|
||||
await this.client.connect("mongodb://127.0.0.1:27017");
|
||||
this.dbRaw = this.client.database("test");
|
||||
this.db = {
|
||||
did: this.dbRaw.collection("did"),
|
||||
pds: this.dbRaw.collection("pds"),
|
||||
meta: this.dbRaw.collection("meta"),
|
||||
};
|
||||
}
|
||||
|
||||
async processPlcExport(plc, after = null) {
|
||||
const url = plc.url + "/export?after=" + (after || "");
|
||||
if (this.debug) {
|
||||
console.log(`ProcessPlcExport: ${url}`);
|
||||
}
|
||||
const req = await fetch(url);
|
||||
const lines = await req.text();
|
||||
const arr = lines.split("\n").map((l) => JSON.parse(l));
|
||||
|
||||
for (const data of arr) {
|
||||
const pdsUrl = data.operation.services?.atproto_pds?.endpoint;
|
||||
const matcher = { did: data.did, src: plc.url };
|
||||
const obj = {
|
||||
did: data.did,
|
||||
src: plc.url,
|
||||
revs: [data],
|
||||
time: new Date().toISOString(),
|
||||
pds: pdsUrl ? [pdsUrl] : [],
|
||||
};
|
||||
let didRev = 0;
|
||||
const found = await this.db.did.findOne(matcher);
|
||||
if (found) {
|
||||
const revFound = found.revs.find((r) => r.cid === data.cid);
|
||||
let updated = false;
|
||||
if (!revFound) {
|
||||
updated = true;
|
||||
didRev = found.revs.length;
|
||||
found.revs.push(data);
|
||||
//found.time = new Date().toISOString()
|
||||
console.log(`DID: Adding new DID revision: ${data.did}@${didRev}`);
|
||||
}
|
||||
if (pdsUrl && !found.pds.includes(pdsUrl)) {
|
||||
updated = true;
|
||||
found.pds.push(pdsUrl);
|
||||
}
|
||||
if (updated) {
|
||||
await this.db.did.updateOne(matcher, {
|
||||
$set: {
|
||||
time: new Date().toISOString(),
|
||||
revs: found.revs,
|
||||
pds: found.pds,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(`DID: Adding new DID revision: ${data.did}@0 (init)`);
|
||||
await this.db.did.insertOne(obj);
|
||||
}
|
||||
const pdsFound = await this.db.pds.findOne({ url: pdsUrl });
|
||||
const didId = [data.did, didRev].join("@");
|
||||
if (pdsFound) {
|
||||
if (!pdsFound.plcs.includes(plc.url)) {
|
||||
pdsFound.plcs.push(plcUrl);
|
||||
console.log(`PDS [${pdsUrl}]: Adding new PLC: ${plc.url}`);
|
||||
await this.db.pds.updateOne({ url: pdsUrl }, {
|
||||
$set: { plcs: pdsFound.plcs },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.db.pds.insertOne({
|
||||
url: pdsUrl,
|
||||
plcs: [plc.url],
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
const key = `lastUpdate:${plc.url}`;
|
||||
await this.db.meta.updateOne({ key }, {
|
||||
$set: { key, value: arr[arr.length - 1].createdAt },
|
||||
}, { upsert: true });
|
||||
return arr.length !== 1 ? arr[arr.length - 1].createdAt : false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import { ATScan } from "./lib/atscan.js";
|
||||
import { pooledMap } from "https://deno.land/std/async/mod.ts";
|
||||
import "https://deno.land/std@0.192.0/dotenv/load.ts";
|
||||
|
||||
function timeout(ms, promise) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const start = performance.now();
|
||||
setTimeout(function () {
|
||||
reject(new Error("timeout"));
|
||||
}, ms);
|
||||
promise.then((v) => {
|
||||
const end = performance.now();
|
||||
return resolve([v, end - start]);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function crawl(ats) {
|
||||
const arr = await ats.db.pds.find().toArray();
|
||||
const results = pooledMap(25, arr.slice(0, 1000), async (i) => {
|
||||
let err = null;
|
||||
let res, data, ms;
|
||||
|
||||
const host = i.url.replace(/^https?:\/\//, "");
|
||||
if (!i.dns) {
|
||||
console.log('sending dns request: ', i.url)
|
||||
let dns =
|
||||
await (await fetch(`https://dns.google/resolve?name=${host}&type=A`))
|
||||
.json();
|
||||
i.dns = dns;
|
||||
ats.db.pds.updateOne({ url: i.url }, { $set: { dns } });
|
||||
}
|
||||
if (!i.ip && (i.dns.Answer && i.dns.Answer.filter(a => a.type === 1).length > 0)) {
|
||||
const ipAddr = i.dns.Answer.filter(a => a.type === 1)[0].data
|
||||
let ip;
|
||||
try {
|
||||
ip =
|
||||
await (await fetch(`http://ipinfo.io/${ipAddr}?token=${Deno.env.get('IPINFO_TOKEN')}`))
|
||||
.json();
|
||||
} catch (e) {}
|
||||
if (ip || (i.ip && i.ip.Question && i.ip && i.ip.Question[0].name !== host+'.') || !i.ip) {
|
||||
i.ip = ip;
|
||||
ats.db.pds.updateOne({ url: i.url }, { $set: { ip } });
|
||||
}
|
||||
}
|
||||
|
||||
if (i.url.match(/^https?:\/\/(localhost|example.com)/)) {
|
||||
err = "not allowed domain";
|
||||
}
|
||||
if (!i.dns.Answer) {
|
||||
err = "not existing domain";
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
const url = `${i.url}/xrpc/com.atproto.server.describeServer`;
|
||||
try {
|
||||
[res, ms] = await timeout(
|
||||
5000,
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"ATScan Crawler",
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (res) {
|
||||
data = await res.json();
|
||||
}
|
||||
} catch (e) {
|
||||
err = e.message;
|
||||
}
|
||||
}
|
||||
const inspect = {
|
||||
err,
|
||||
data,
|
||||
ms,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
const dbSet = { "inspect.current": inspect };
|
||||
if (!err && data) {
|
||||
dbSet["inspect.lastOnline"] = (new Date()).toISOString();
|
||||
}
|
||||
await ats.db.pds.updateOne({ url: i.url }, {
|
||||
$set: dbSet,
|
||||
});
|
||||
console.log(
|
||||
`-> ${i.url} ${ms ? "[" + ms + "ms]" : ""} ${
|
||||
err ? "error = " + err : ""
|
||||
}`,
|
||||
);
|
||||
});
|
||||
for await (const value of results) {}
|
||||
}
|
||||
|
||||
if (Deno.args[0] === "daemon") {
|
||||
const wait = 60 * 10;
|
||||
|
||||
console.log("Initializing ATScan ..");
|
||||
const ats = new ATScan();
|
||||
ats.debug = true;
|
||||
await ats.init();
|
||||
console.log("pds-crawl daemon started");
|
||||
console.log("Performing initial crawl ..");
|
||||
// initial crawl
|
||||
await crawl(ats);
|
||||
console.log(`Initial crawl done`);
|
||||
ats.debug = false;
|
||||
console.log(`Processing events [wait=${wait}s] ..`);
|
||||
setInterval(() => crawl(ats), wait * 1000);
|
||||
} else {
|
||||
const ats = new ATScan({ debug: true });
|
||||
await ats.init();
|
||||
await crawl(ats);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { ATScan } from "./lib/atscan.js";
|
||||
|
||||
async function crawl(ats) {
|
||||
for (const plc of ats.defaultPLC) {
|
||||
let start = 0;
|
||||
if (Deno.args[0] !== "init") {
|
||||
const item = await ats.db.meta.findOne({ key: `lastUpdate:${plc.url}` });
|
||||
if (item) {
|
||||
start = item.value;
|
||||
}
|
||||
}
|
||||
let after = await ats.processPlcExport(plc, start);
|
||||
while (after) {
|
||||
after = await ats.processPlcExport(plc, after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Deno.args[0] === "daemon") {
|
||||
const wait = 60;
|
||||
|
||||
console.log("Initializing ATScan ..");
|
||||
const ats = new ATScan();
|
||||
ats.debug = true;
|
||||
await ats.init();
|
||||
console.log("plc-crawl daemon started");
|
||||
console.log("Performing initial crawl ..");
|
||||
// initial crawl
|
||||
await crawl(ats);
|
||||
console.log(`Initial crawl done`);
|
||||
ats.debug = false;
|
||||
console.log(`Processing events [wait=${wait}s] ..`);
|
||||
setInterval(() => crawl(ats), wait * 1000);
|
||||
} else {
|
||||
const ats = new ATScan({ debug: true });
|
||||
await ats.init();
|
||||
await crawl(ats);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
module.exports = {
|
||||
apps: [{
|
||||
name: "atscan-plc-crawler",
|
||||
script: "./backend/plc-crawler.js",
|
||||
args: "daemon",
|
||||
interpreter: "deno",
|
||||
interpreterArgs: "run --unstable --allow-net --allow-read",
|
||||
}, {
|
||||
name: "atscan-pds-crawler",
|
||||
script: "./backend/pds-crawler.js",
|
||||
args: "daemon",
|
||||
interpreter: "mullvad-exclude",
|
||||
interpreterArgs: "deno run --unstable --allow-net --allow-read",
|
||||
}, {
|
||||
name: "atscan-fe-dev",
|
||||
interpreter: "mullvad-exclude",
|
||||
interpreterArgs: "npm run dev",
|
||||
env: {
|
||||
HOST: "127.0.0.1",
|
||||
PORT: 4010,
|
||||
}
|
||||
}, {
|
||||
name: "atscan-fe",
|
||||
script: "./frontend/build/index.js",
|
||||
interpreter: "mullvad-exclude",
|
||||
interpreterArgs: "node",
|
||||
env: {
|
||||
HOST: "127.0.0.1",
|
||||
PORT: 4000,
|
||||
}
|
||||
}, {
|
||||
name: "atscan-api",
|
||||
script: "./backend/api.js",
|
||||
//args : "daemon",
|
||||
interpreter: "deno",
|
||||
interpreterArgs: "run --unstable -A",
|
||||
watch: true,
|
||||
}],
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ["eslint:recommended", "plugin:svelte/recommended", "prettier"],
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: [".svelte"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
|
@ -0,0 +1,2 @@
|
|||
engine-strict=true
|
||||
resolution-mode=highest
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"prettier.documentSelectors": [
|
||||
"**/*.svelte"
|
||||
],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"background",
|
||||
"badge",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonAction",
|
||||
"buttonBack",
|
||||
"buttonClasses",
|
||||
"buttonComplete",
|
||||
"buttonDismiss",
|
||||
"buttonNeutral",
|
||||
"buttonNext",
|
||||
"buttonPositive",
|
||||
"buttonTextCancel",
|
||||
"buttonTextConfirm",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"buttonTextSubmit",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"chips",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"position",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionFooter",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by
|
||||
[`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or
|
||||
`pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an
|
||||
> [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "atscan-fe",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/dom": "^1.4.2",
|
||||
"@skeletonlabs/skeleton": "^1.8.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.2.4",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"date-fns": "^2.30.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.26.0",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.8.0",
|
||||
"minidenticons": "^4.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" data-theme="skeleton">
|
||||
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,5 @@
|
|||
/*place global styles here */
|
||||
html,
|
||||
body {
|
||||
@apply h-full overflow-hidden;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<ol class="breadcrumb">
|
||||
{#each data as crumb, i}
|
||||
<!-- If crumb index is less than the breadcrumb length minus 1 -->
|
||||
{#if i < data.length - 1}
|
||||
<li class="crumb"><a class="btn variant-soft-secondary" href={crumb.link}>{crumb.label}</a></li>
|
||||
<li class="crumb-separator" aria-hidden>›</li>
|
||||
{:else}
|
||||
<li class="crumb">
|
||||
{#if crumb.link}
|
||||
<a class="btn variant-soft-secondary" href={crumb.link}>{@html crumb.label}</a>
|
||||
{:else}
|
||||
{@html crumb.label}
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ol>
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
|
||||
export function dateDistance (date) {
|
||||
return formatDistanceToNow(new Date(date))
|
||||
}
|
||||
|
||||
export function identicon (...args) {
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(...args))
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
// The ordering of these imports is critical to your app working properly
|
||||
import '@skeletonlabs/skeleton/themes/theme-skeleton.css';
|
||||
// If you have source.organizeImports set to true in VSCode, then it will auto change this ordering
|
||||
import '@skeletonlabs/skeleton/styles/skeleton.css';
|
||||
// Most of your app wide CSS should be put in this file
|
||||
import '../app.postcss';
|
||||
import { AppShell, AppBar, LightSwitch } from '@skeletonlabs/skeleton';
|
||||
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { storeHighlightJs } from '@skeletonlabs/skeleton';
|
||||
|
||||
storeHighlightJs.set(hljs);
|
||||
|
||||
</script>
|
||||
|
||||
<!-- App Shell -->
|
||||
<AppShell>
|
||||
<svelte:fragment slot="header">
|
||||
<!-- App Bar -->
|
||||
<AppBar>
|
||||
<svelte:fragment slot="lead">
|
||||
<a href="/"><strong class="text-xl uppercase">ATScan</strong></a>
|
||||
<div class="lg:ml-8 flex">
|
||||
<div class="relative hidden lg:block">
|
||||
<a href="/did" class="btn hover:variant-soft-primary"><span>DIDs</span></a>
|
||||
</div>
|
||||
<div class="relative hidden lg:block">
|
||||
<a href="/pds" class="btn hover:variant-soft-primary"><span>PDS Instances</span></a>
|
||||
</div>
|
||||
<div class="relative hidden lg:block">
|
||||
<a href="/plc" class="btn hover:variant-soft-primary"><span>PLC Directories</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trail">
|
||||
<!--
|
||||
<a
|
||||
class="btn btn-sm variant-ghost-surface"
|
||||
href="https://discord.gg/EXqV7W8MtY"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-sm variant-ghost-surface"
|
||||
href="https://twitter.com/SkeletonUI"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Twitter
|
||||
</a>
|
||||
-->
|
||||
<a
|
||||
class="btn btn-sm variant-ghost-surface hover:variant-soft-primary"
|
||||
href="https://github.com/burningtree/atscan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<LightSwitch />
|
||||
</svelte:fragment>
|
||||
</AppBar>
|
||||
</svelte:fragment>
|
||||
<!-- Page Route Content -->
|
||||
<slot />
|
||||
</AppShell>
|
|
@ -0,0 +1,6 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
// ...
|
||||
throw redirect(302, '/did');
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
<div class="w-full h-full bg-ats-bsky"></div>
|
||||
|
||||
<script>
|
||||
/** dark:bg-ats-bsky dark:bg-ats-sbox */
|
||||
</script>
|
|
@ -0,0 +1,13 @@
|
|||
import * as _ from "lodash";
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ params }) {
|
||||
const res = await fetch("https://api.atscan.net/did");
|
||||
const did = _.orderBy(await res.json(), ["time"], [
|
||||
"desc",
|
||||
]);
|
||||
return {
|
||||
did,
|
||||
plc: await (await fetch("https://api.atscan.net/plc")).json()
|
||||
};
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
<script>
|
||||
import { Table } from '@skeletonlabs/skeleton';
|
||||
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||
import { dateDistance, identicon } from '$lib/utils.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import { writable } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
|
||||
export let data;
|
||||
let search = ""
|
||||
/*search.subscribe(val => {
|
||||
console.log(`/did?q=${val}`)
|
||||
setTimeout(() => {
|
||||
throw redirect(307, `/did?q=${''}`);
|
||||
}, 500)
|
||||
})
|
||||
//*/
|
||||
|
||||
function formSubmit () {
|
||||
console.log(search)
|
||||
}
|
||||
|
||||
function selectionHandler (i) {
|
||||
return goto(`/${i.detail[0]}`)
|
||||
}
|
||||
|
||||
function tableMapperValuesLocal (source, keys) {
|
||||
return tableSourceValues(source.map((row) => {
|
||||
const mappedRow = {};
|
||||
keys.forEach((key) => {
|
||||
let val = row[key]
|
||||
if (key === 'world') {
|
||||
}
|
||||
if (key === 'srcHost') {
|
||||
val = `<a href="/plc/${val}" class="hover:underline">${val}</a>`
|
||||
}
|
||||
if (key === 'pds') {
|
||||
const host =
|
||||
val = val.map(i => {
|
||||
const host = i.replace(/^https?:\/\//, '')
|
||||
return `<a href="/pds/${host}" class='hover:underline'>${host}</a>`
|
||||
}).join(', ')
|
||||
}
|
||||
if (key === 'did') {
|
||||
const did = val
|
||||
const plc = data.plc.find(i => i.url === row.src)
|
||||
val = `<div class="flex gap-6">`
|
||||
val += ` <div>`
|
||||
val += ` <div class="text-lg inline-block"><a href="/${did}" class="hover:underline"><span class="opacity-50">did:plc:</span><span class="font-semibold opacity-100">${did.replace(/^did:plc:/, '')}</span></a></div>`
|
||||
const handles = row.revs[row.revs.length-1].operation.alsoKnownAs.map(h => h.replace(/^at:\/\//, ''))
|
||||
val += ` <div class="mt-1.5">`
|
||||
val += ` <span class="mr-2 badge text-xs variant-filled ${plc.color} dark:${plc.color} opacity-70 text-white dark:text-black">${plc.name}</span>`
|
||||
val += ` <span>${handles.map(h => `<a href="https://bsky.app/profile/${h}" target="_blank" class="hover:underline">@${h}</a>`).join(', ')}</span>`
|
||||
val += ` </div>`
|
||||
val += ` </div>`
|
||||
val += "</div>"
|
||||
}
|
||||
if (key === 'time') {
|
||||
val = dateDistance(val)
|
||||
}
|
||||
if (key === 'deep') {
|
||||
val = row.revs.length
|
||||
}
|
||||
if (key === 'img') {
|
||||
val = `<div class="text-right w-full"><div class="inline-block"><a href="/${row.did}"><img src="${identicon(row.did)}" class="w-16 h-16 rounded-lg bg-gray-200 dark:bg-gray-800 float-left" /></a></div></div>`
|
||||
}
|
||||
|
||||
return mappedRow[key] = val
|
||||
})
|
||||
return mappedRow;
|
||||
}))
|
||||
}
|
||||
|
||||
const sourceData = data.did;
|
||||
const tableSimple = {
|
||||
// A list of heading labels.
|
||||
head: ['', 'DID', '#', 'PLC', 'PDS', 'Last mod'],
|
||||
body: tableMapperValuesLocal(sourceData, ['img', 'did', 'deep', 'srcHost', 'pds', 'time']),
|
||||
meta: tableMapperValues(sourceData, ['did']),
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-8 space-y-8">
|
||||
<h1 class="h1">DIDs</h1>
|
||||
|
||||
<form on:submit={formSubmit}>
|
||||
<input class="input" title="Input (text)" type="text" placeholder="Search for DID .." bind:value={search} />
|
||||
</form>
|
||||
|
||||
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||
|
||||
<!--h2 class="h2">Active DIDs ({sourceData.length})</h2-->
|
||||
<Table source={tableSimple} interactive={true} on:selected={selectionHandler} />
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
export async function load({ params }) {
|
||||
const did = `did:${params.id}`
|
||||
const res = await fetch(`https://api.atscan.net/${did}`);
|
||||
return {
|
||||
item: res.json(),
|
||||
did
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<script>
|
||||
import { CodeBlock } from '@skeletonlabs/skeleton';
|
||||
import Breadcrumb from '$lib/components/Breadcrumb.svelte';
|
||||
import { dateDistance, identicon } from '$lib/utils.js';
|
||||
import { Table } from '@skeletonlabs/skeleton';
|
||||
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let data;
|
||||
|
||||
const item = data.item;
|
||||
const handles = item.revs[item.revs.length-1].operation.alsoKnownAs.map(h => h.replace(/^at:\/\//, ''))
|
||||
|
||||
function tableMapperValuesLocal (source, keys) {
|
||||
let i = 0
|
||||
return tableSourceValues(source.map((row) => {
|
||||
const mappedRow = {};
|
||||
keys.forEach((key) => {
|
||||
let val = row[key]
|
||||
if (key === 'num') {
|
||||
val = String('#'+i)
|
||||
}
|
||||
if (key === 'handle') {
|
||||
val = row.operation.alsoKnownAs.map(a => a.replace(/^at:\/\//,'@')).join(', ')
|
||||
}
|
||||
if (key === 'createdAt') {
|
||||
val = `<span title="${val}" alt="${val}">${dateDistance(val)}</span>`
|
||||
}
|
||||
return mappedRow[key] = val
|
||||
})
|
||||
i++
|
||||
return mappedRow;
|
||||
}).reverse())
|
||||
}
|
||||
|
||||
const sourceData = item.revs
|
||||
const historyTable = {
|
||||
head: [ '#', 'Handle', 'CID', 'Age' ],
|
||||
body: tableMapperValuesLocal(sourceData, ['num', 'handle', 'cid', 'createdAt']),
|
||||
meta: tableMapperValues(sourceData, ['cid']),
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-8 space-y-8">
|
||||
<Breadcrumb data={[
|
||||
{ label: 'DIDs', link: '/did' },
|
||||
{ label: `<span class="mr-2 badge ${item.env ? 'bg-ats-'+item.env : 'bg-gray-500'} text-white dark:text-black">${item.env}</span> federation`, link: `/did?federation=${item.env}` }
|
||||
]} />
|
||||
<div class="flex gap-8">
|
||||
<div>
|
||||
<img src="{identicon(item.did)}" class="w-40 h-40 rounded-2xl bg-gray-200 dark:bg-gray-800 float-left" />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<h1 class="h1">
|
||||
<span class="opacity-50 font-normal">did:plc:</span><span class="font-semibold opacity-100">{item.did.replace(/^did:plc:/, '')}</span>
|
||||
</h1>
|
||||
<div class="text-2xl mt-4">
|
||||
{@html handles.map(h => `<a href="https://bsky.app/profile/${h}" target="_blank" class="hover:underline">@${h}</a>`).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="h2">Revisions <span class="font-normal text-2xl">({sourceData.length})</span></h2>
|
||||
<Table source={historyTable} />
|
||||
|
||||
<h2 class="h2">Source</h2>
|
||||
<CodeBlock code={JSON.stringify(item, null, 2)} language="json" />
|
||||
|
||||
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
import * as _ from "lodash";
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ params }) {
|
||||
const res = await fetch("https://api.atscan.net/pds");
|
||||
const pds = _.orderBy(await res.json(), ["env", "inspect.current.ms"], [
|
||||
"asc",
|
||||
"asc",
|
||||
]);
|
||||
return {
|
||||
pds,
|
||||
plc: await (await fetch("https://api.atscan.net/plc")).json()
|
||||
};
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<script>
|
||||
import { Table } from '@skeletonlabs/skeleton';
|
||||
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||
import { dateDistance } from '$lib/utils.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import { writable } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export let data;
|
||||
const search = writable($page.url.searchParams.get('q'))
|
||||
|
||||
function selectionHandler (i) {
|
||||
return goto(`/pds/${i.detail[0]}`)
|
||||
}
|
||||
|
||||
function tableMapperValuesLocal (source, keys) {
|
||||
return tableSourceValues(source.map((row) => {
|
||||
const mappedRow = {};
|
||||
keys.forEach((key) => {
|
||||
let val = row[key]
|
||||
if (key === 'plcs' && val) {
|
||||
val = val.map(i => i.replace(/^https?:\/\//, '')).join(', ')
|
||||
}
|
||||
if (key === 'env') {
|
||||
let arr = []
|
||||
const plc = (val === "sandbox") ? data.plc.find(i => i.code === 'sbox') : ((val === "bsky") ? data.plc.find(i => i.code === 'bsky') : null)
|
||||
if (plc) {
|
||||
arr.push(`<span class="badge variant-filled ${plc.color} dark:${plc.color} opacity-70 text-white dark:text-black">${plc.name}</span>`)
|
||||
}
|
||||
val = arr.reverse().join(' ')
|
||||
}
|
||||
if (key === 'host') {
|
||||
val = `<a href="/pds/${val}" class="hover:underline"><span class="font-semibold text-lg">${val}</span></a>`
|
||||
}
|
||||
if (key === 'ms') {
|
||||
val = row.inspect?.current.err
|
||||
? `<a href="${row.url}/xrpc/com.atproto.server.describeServer" target="_blank" title="${row.inspect.current.err}" class="hover:underline">error</a>`
|
||||
: row.inspect?.current.ms ? `<a href="${row.url}/xrpc/com.atproto.server.describeServer" target="_blank" class="hover:underline">${row.inspect.current.ms + 'ms'}</a>` : '-'
|
||||
}
|
||||
if (key === 'userDomains') {
|
||||
val = row.inspect?.current.data?.availableUserDomains.join(', ')
|
||||
}
|
||||
if (key === 'location') {
|
||||
val = row.ip && row.ip.country
|
||||
? `<img src="/cc/${row.ip.country.toLowerCase()}.png" alt="${row.ip.country}" title="${row.ip.country}" class="inline-block mr-2" />`
|
||||
: ''
|
||||
if (row.ip && row.ip.city) {
|
||||
val += `${row.ip.city} - `
|
||||
}
|
||||
const dnsIp = row.dns ? row.dns.Answer?.filter(a => a.type === 1)[0].data : null
|
||||
val += `<a href="http://ipinfo.io/${dnsIp}" target="_blank" class="hover:underline">${dnsIp}</a>` || '-'
|
||||
if (row.ip && row.ip.regionName) {
|
||||
val += ' ('+row.ip.regionName+')'
|
||||
}
|
||||
val += `<br /><span class="text-xs">${row.ip?.org || 'n/a'}</span>`
|
||||
}
|
||||
if (key === 'didsCount') {
|
||||
val = `<a href="/did?pds=${row.host}" class="hover:underline">${val}</a>`
|
||||
}
|
||||
if (key === 'lastOnline' && row.inspect) {
|
||||
val = row.inspect?.lastOnline ? dateDistance(row.inspect?.lastOnline) : '-'
|
||||
}
|
||||
|
||||
return mappedRow[key] = val
|
||||
});
|
||||
return mappedRow;
|
||||
}))
|
||||
}
|
||||
|
||||
const filtered = data.pds.filter(d => d.inspect?.lastOnline)
|
||||
let sourceData = JSON.parse(JSON.stringify(filtered))
|
||||
let sourceDataOffline = data.pds.filter(d => !d.inspect?.lastOnline)
|
||||
|
||||
|
||||
function formSubmit() {
|
||||
const url = '?q='+$search
|
||||
//goto(url)
|
||||
// refilter
|
||||
sourceData = data.pds.filter(d => d.inspect?.lastOnline).filter(i => i.url.match(new RegExp($search, 'i')))
|
||||
genTableSimple()
|
||||
return false
|
||||
}
|
||||
|
||||
let tableSimple;
|
||||
function genTableSimple () {
|
||||
tableSimple = {
|
||||
// A list of heading labels.
|
||||
head: ['Federation', 'Host', 'Delay', 'Location', 'DIDs', 'PLCs', 'User Domains', 'Last mod'],
|
||||
// The data visibly shown in your table body UI.
|
||||
//body: mapper(sourceData, ['host', 'type', 'plc']),
|
||||
body: tableMapperValuesLocal(sourceData, ['env', 'host', 'ms', 'location', 'didsCount', 'plcs', 'userDomains', 'lastOnline' ]),
|
||||
// Optional: The data returned when interactive is enabled and a row is clicked.
|
||||
meta: tableMapperValues(sourceData, ['host']),
|
||||
// Optional: A list of footer labels.
|
||||
//foot: ['Total', '', '<code class="code">5</code>']
|
||||
};
|
||||
}
|
||||
genTableSimple()
|
||||
|
||||
const tableSimpleOffline = {
|
||||
// A list of heading labels.
|
||||
head: ['Federation', 'Host', 'Delay', 'Location', 'DIDs', 'PLCs',],
|
||||
body: tableMapperValuesLocal(sourceDataOffline, ['env', 'host', 'ms', 'location', 'didsCount', 'plcs']),
|
||||
meta: tableMapperValues(sourceDataOffline, ['host']),
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-8 space-y-8">
|
||||
<h1 class="h1">PDS Instances ({sourceData.length})</h1>
|
||||
|
||||
<form on:submit={formSubmit} class="flex gap-4">
|
||||
<input class="input" title="Input (text)" type="text" placeholder="Search for PDS .." bind:value={$search} />
|
||||
<button type="submit" class="btn variant-filled">Search</button>
|
||||
</form>
|
||||
|
||||
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||
|
||||
<!--h2 class="h2">Active instances ({sourceData.length})</h2-->
|
||||
<Table source={tableSimple} interactive={true} on:selected={selectionHandler} />
|
||||
|
||||
<!--<h2 class="h2">Inactive instances ({sourceDataOffline.length})</h2>
|
||||
<Table source={tableSimpleOffline} />-->
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
export async function load({ params }) {
|
||||
const res = await fetch(`https://api.atscan.net/pds/${params.host}`);
|
||||
return {
|
||||
item: res.json()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { CodeBlock } from '@skeletonlabs/skeleton';
|
||||
import Breadcrumb from '$lib/components/Breadcrumb.svelte';
|
||||
export let data;
|
||||
|
||||
const item = data.item;
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-8 space-y-8">
|
||||
<Breadcrumb data={[
|
||||
{ label: 'PDS Instances', link: '/pds' },
|
||||
{ label: `<span class="mr-2 badge ${item.env === 'bsky' ? 'bg-ats-bsky' : (item.env === 'sandbox' ? 'bg-ats-sbox' : 'bg-gray-500')} text-white dark:text-black">${item.env}</span> federation`, link: `/pds?federation=${item.env}` }
|
||||
]} />
|
||||
<h1 class="h1">
|
||||
{item.host}
|
||||
</h1>
|
||||
|
||||
<h2 class="h2">Source</h2>
|
||||
<CodeBlock code={JSON.stringify(item, null, 2)} language="json" />
|
||||
|
||||
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
import * as _ from "lodash";
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ params }) {
|
||||
const res = await fetch("https://api.atscan.net/plc");
|
||||
const plc = _.orderBy(await res.json(), ["code"], [
|
||||
"asc",
|
||||
]);
|
||||
return { plc };
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { Table } from '@skeletonlabs/skeleton';
|
||||
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||
import { dateDistance } from '$lib/utils.js';
|
||||
|
||||
export let data;
|
||||
|
||||
function tableMapperValuesLocal (source, keys) {
|
||||
return tableSourceValues(source.map((row) => {
|
||||
const mappedRow = {};
|
||||
keys.forEach((key) => {
|
||||
let val = row[key]
|
||||
if (key === 'world') {
|
||||
val = `<span class="badge variant-filled ${row.color} dark:${row.color} opacity-70 text-white dark:text-black">${row.name}</span>`
|
||||
}
|
||||
if (key === 'host') {
|
||||
val = `<span class="text-xl font-semibold">${val}</span>`
|
||||
}
|
||||
if (key === 'code') {
|
||||
val = `<div class="inline-block font-mono">${val}</div>`
|
||||
}
|
||||
if (key === 'lastUpdate') {
|
||||
val = dateDistance(val)
|
||||
}
|
||||
if (key === 'didsCount') {
|
||||
val = `<a href="/did?plc=${row.host}" class="hover:underline">${val}</a>`
|
||||
}
|
||||
return mappedRow[key] = val
|
||||
})
|
||||
return mappedRow;
|
||||
}))
|
||||
}
|
||||
|
||||
const sourceData = data.plc;
|
||||
const tableSimple = {
|
||||
// A list of heading labels.
|
||||
head: ['Federation', 'Host', 'DIDs', 'Last mod'],
|
||||
body: tableMapperValuesLocal(sourceData, ['world', 'host', 'didsCount', 'lastUpdate']),
|
||||
meta: tableMapperValues(sourceData, ['position', 'name', 'symbol', 'weight']),
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="container mx-auto p-8 space-y-8">
|
||||
<h1 class="h1">PLC Directories ({sourceData.length})</h1>
|
||||
|
||||
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||
|
||||
<!--<h2 class="h2">Active directories </h2>-->
|
||||
<Table source={tableSimple} interactive={true} />
|
||||
</div>
|
Za Šířka: | Výška: | Velikost: 442 B |
Za Šířka: | Výška: | Velikost: 445 B |
Za Šířka: | Výška: | Velikost: 493 B |
Za Šířka: | Výška: | Velikost: 587 B |
Za Šířka: | Výška: | Velikost: 327 B |
Za Šířka: | Výška: | Velikost: 444 B |
Za Šířka: | Výška: | Velikost: 560 B |
Za Šířka: | Výška: | Velikost: 417 B |
Za Šířka: | Výška: | Velikost: 355 B |
Za Šířka: | Výška: | Velikost: 353 B |
Za Šířka: | Výška: | Velikost: 407 B |
Za Šířka: | Výška: | Velikost: 469 B |
Za Šířka: | Výška: | Velikost: 315 B |
Za Šířka: | Výška: | Velikost: 472 B |
Za Šířka: | Výška: | Velikost: 441 B |
Za Šířka: | Výška: | Velikost: 287 B |
Za Šířka: | Výška: | Velikost: 507 B |
Za Šířka: | Výška: | Velikost: 393 B |
Za Šířka: | Výška: | Velikost: 577 B |
Za Šířka: | Výška: | Velikost: 528 B |
Za Šířka: | Výška: | Velikost: 386 B |
Za Šířka: | Výška: | Velikost: 528 B |
Za Šířka: | Výška: | Velikost: 410 B |
Za Šířka: | Výška: | Velikost: 502 B |
Za Šířka: | Výška: | Velikost: 467 B |
Za Šířka: | Výška: | Velikost: 293 B |
Za Šířka: | Výška: | Velikost: 361 B |
Za Šířka: | Výška: | Velikost: 416 B |
Za Šířka: | Výška: | Velikost: 516 B |
Za Šířka: | Výška: | Velikost: 389 B |
Za Šířka: | Výška: | Velikost: 560 B |
Za Šířka: | Výška: | Velikost: 295 B |
Za Šířka: | Výška: | Velikost: 442 B |
Za Šířka: | Výška: | Velikost: 368 B |
Za Šířka: | Výška: | Velikost: 362 B |
Za Šířka: | Výška: | Velikost: 437 B |
Za Šířka: | Výška: | Velikost: 520 B |
Za Šířka: | Výška: | Velikost: 379 B |
Za Šířka: | Výška: | Velikost: 420 B |
Za Šířka: | Výška: | Velikost: 413 B |
Za Šířka: | Výška: | Velikost: 343 B |
Za Šířka: | Výška: | Velikost: 287 B |
Za Šířka: | Výška: | Velikost: 449 B |
Za Šířka: | Výška: | Velikost: 552 B |
Za Šířka: | Výška: | Velikost: 387 B |
Za Šířka: | Výška: | Velikost: 578 B |
Za Šířka: | Výška: | Velikost: 491 B |
Za Šířka: | Výška: | Velikost: 581 B |
Za Šířka: | Výška: | Velikost: 418 B |
Za Šířka: | Výška: | Velikost: 529 B |
Za Šířka: | Výška: | Velikost: 435 B |
Za Šířka: | Výška: | Velikost: 581 B |
Za Šířka: | Výška: | Velikost: 512 B |
Za Šířka: | Výška: | Velikost: 292 B |
Za Šířka: | Výška: | Velikost: 436 B |
Za Šířka: | Výška: | Velikost: 473 B |
Za Šířka: | Výška: | Velikost: 389 B |
Za Šířka: | Výška: | Velikost: 462 B |
Za Šířka: | Výška: | Velikost: 573 B |
Za Šířka: | Výška: | Velikost: 358 B |
Za Šířka: | Výška: | Velikost: 471 B |