This commit is contained in:
tree 2023-06-26 20:38:43 +00:00
revize ebcad77150
317 změnil soubory, kde provedl 4637 přidání a 0 odebrání

21
LICENSE Normal file
Zobrazit soubor

@ -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.

16
Makefile Normal file
Zobrazit soubor

@ -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

9
README.md Normal file
Zobrazit soubor

@ -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

1
backend/.env.example Normal file
Zobrazit soubor

@ -0,0 +1 @@
IPINFO_TOKEN=XXXXX

1
backend/.gitignore vendorováno Normal file
Zobrazit soubor

@ -0,0 +1 @@
.env

79
backend/api.js Normal file
Zobrazit soubor

@ -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}`);

99
backend/lib/atscan.js Normal file
Zobrazit soubor

@ -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;
}
}

114
backend/pds-crawler.js Normal file
Zobrazit soubor

@ -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);
}

38
backend/plc-crawler.js Normal file
Zobrazit soubor

@ -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);
}

39
ecosystem.config.js Normal file
Zobrazit soubor

@ -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,
}],
};

13
frontend/.eslintignore Normal file
Zobrazit soubor

@ -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

14
frontend/.eslintrc.cjs Normal file
Zobrazit soubor

@ -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,
},
};

10
frontend/.gitignore vendorováno Normal file
Zobrazit soubor

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

2
frontend/.npmrc Normal file
Zobrazit soubor

@ -0,0 +1,2 @@
engine-strict=true
resolution-mode=highest

13
frontend/.prettierignore Normal file
Zobrazit soubor

@ -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

9
frontend/.prettierrc Normal file
Zobrazit soubor

@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

106
frontend/.vscode/settings.json vendorováno Normal file
Zobrazit soubor

@ -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"
]
}

41
frontend/README.md Normal file
Zobrazit soubor

@ -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.

3353
frontend/package-lock.json vygenerováno Normal file

Rozdílový obsah nebyl zobrazen, protože je příliš veliký Načíst rozdílové porovnání

37
frontend/package.json Normal file
Zobrazit soubor

@ -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"
}
}

Zobrazit soubor

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

9
frontend/src/app.d.ts vendorováno Normal file
Zobrazit soubor

@ -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 {}
}

12
frontend/src/app.html Normal file
Zobrazit soubor

@ -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>

5
frontend/src/app.postcss Normal file
Zobrazit soubor

@ -0,0 +1,5 @@
/*place global styles here */
html,
body {
@apply h-full overflow-hidden;
}

Zobrazit soubor

@ -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>&rsaquo;</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>

11
frontend/src/lib/utils.js Normal file
Zobrazit soubor

@ -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))
}

Zobrazit soubor

@ -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>

Zobrazit soubor

@ -0,0 +1,6 @@
import { redirect } from '@sveltejs/kit';
export function load() {
// ...
throw redirect(302, '/did');
}

Zobrazit soubor

@ -0,0 +1,6 @@
<div class="w-full h-full bg-ats-bsky"></div>
<script>
/** dark:bg-ats-bsky dark:bg-ats-sbox */
</script>

Zobrazit soubor

@ -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()
};
}

Zobrazit soubor

@ -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>

Zobrazit soubor

@ -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
}
}

Zobrazit soubor

@ -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>

Zobrazit soubor

@ -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()
};
}

Zobrazit soubor

@ -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>

Zobrazit soubor

@ -0,0 +1,7 @@
export async function load({ params }) {
const res = await fetch(`https://api.atscan.net/pds/${params.host}`);
return {
item: res.json()
}
}

Zobrazit soubor

@ -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>

Zobrazit soubor

@ -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 };
}

Zobrazit soubor

@ -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>

binární
frontend/static/cc/_abkhazia.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 442 B

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 445 B

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 493 B

binární
frontend/static/cc/_commonwealth.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 587 B

binární
frontend/static/cc/_england.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 327 B

binární
frontend/static/cc/_gosquared.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 444 B

binární
frontend/static/cc/_kosovo.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 560 B

binární
frontend/static/cc/_mars.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 417 B

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 355 B

binární
frontend/static/cc/_nato.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 353 B

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 407 B

binární
frontend/static/cc/_olympics.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 469 B

binární
frontend/static/cc/_red-cross.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 315 B

binární
frontend/static/cc/_scotland.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 472 B

binární
frontend/static/cc/_somaliland.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 441 B

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 287 B

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 507 B

binární
frontend/static/cc/_unknown.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 393 B

binární
frontend/static/cc/_wales.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 577 B

binární
frontend/static/cc/ad.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 528 B

binární
frontend/static/cc/ae.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 386 B

binární
frontend/static/cc/af.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 528 B

binární
frontend/static/cc/ag.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 410 B

binární
frontend/static/cc/ai.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 502 B

binární
frontend/static/cc/al.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 467 B

binární
frontend/static/cc/am.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 293 B

binární
frontend/static/cc/an.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 361 B

binární
frontend/static/cc/ao.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 416 B

binární
frontend/static/cc/aq.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 516 B

binární
frontend/static/cc/ar.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 389 B

binární
frontend/static/cc/as.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 560 B

binární
frontend/static/cc/at.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 295 B

binární
frontend/static/cc/au.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 442 B

binární
frontend/static/cc/aw.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 368 B

binární
frontend/static/cc/ax.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 362 B

binární
frontend/static/cc/az.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 437 B

binární
frontend/static/cc/ba.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 520 B

binární
frontend/static/cc/bb.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 379 B

binární
frontend/static/cc/bd.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 420 B

binární
frontend/static/cc/be.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 413 B

binární
frontend/static/cc/bf.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 343 B

binární
frontend/static/cc/bg.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 287 B

binární
frontend/static/cc/bh.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 449 B

binární
frontend/static/cc/bi.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 552 B

binární
frontend/static/cc/bj.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 387 B

binární
frontend/static/cc/bl.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 578 B

binární
frontend/static/cc/bm.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 491 B

binární
frontend/static/cc/bn.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 581 B

binární
frontend/static/cc/bo.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 418 B

binární
frontend/static/cc/br.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 529 B

binární
frontend/static/cc/bs.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 435 B

binární
frontend/static/cc/bt.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 581 B

binární
frontend/static/cc/bv.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 512 B

binární
frontend/static/cc/bw.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 292 B

binární
frontend/static/cc/by.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 436 B

binární
frontend/static/cc/bz.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 473 B

binární
frontend/static/cc/ca.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 389 B

binární
frontend/static/cc/cc.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 462 B

binární
frontend/static/cc/cd.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 573 B

binární
frontend/static/cc/cf.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 358 B

binární
frontend/static/cc/cg.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 471 B

Některé soubory nejsou zobrazny, neboť je v této revizi změněno mnoho souborů Zobrazit více