new design
Todas las comprobaciones han sido exitosas
continuous-integration/drone/push Build is passing

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-11-27 03:01:40 +01:00
padre 07f9496ea6
commit eb5d218de9
Se han modificado 12 ficheros con 1076 adiciones y 614 borrados

Ver fichero

@@ -1,6 +1,5 @@
module.exports = (app, client) => {
const constant = require('../constant'),
zlib = require('zlib'),
asyncHandler = require('express-async-handler'),
{ param, validationResult } = require('express-validator')
/**
@@ -513,33 +512,4 @@ module.exports = (app, client) => {
res.status(404).end()
}
}))
/**
* @swagger
* /api/download_index:
* get:
* summary: Retrieve all content of ElasticSearch index.
* description: Retrieve all content of ElasticSearch index.
* responses:
* 200:
* description: A file compressed with gzip.
* content:
* application/gzip:
*/
app.get('/api/download_index', asyncHandler(async (req, res) => {
try {
res.setHeader('Content-Type', 'application/gzip')
res.setHeader('Content-disposition', 'attachment; filename=fediblock-index.json.gz')
const result = await client.search({
index: constant.index,
size: 9999,
query: {
match_all: {}
}
}, { asStream: true, meta: false })
result.pipe(zlib.createGzip()).pipe(res)
} catch (e) {
console.error(e)
res.status(404).end()
}
}))
}

Ver fichero

@@ -6,6 +6,7 @@
"cra-template": "1.2.0",
"dayjs": "^1.11.13",
"html2canvas": "^1.4.1",
"react-icons": "^5.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-scripts": "5.0.1",

Ver fichero

@@ -1,405 +1,611 @@
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
:root[data-theme="dark"] {
--background-color: #212529;
--color: #fefefe;
--instance-list: #333333;
--footer: #cccccc;
--bg: #05060a;
--bg-soft: #0f1118;
--panel: rgba(255, 255, 255, 0.04);
--panel-strong: rgba(255, 255, 255, 0.08);
--text: #f7f7fb;
--muted: #a7adc7;
--accent: #7ef3e4;
--accent-strong: #5ad1ff;
--border: rgba(255, 255, 255, 0.1);
}
:root[data-theme="light"] {
--background-color: #f5f5f5;
--color: #333333;
--instance-list: #f0f0f0;
--footer: #666666;
--bg: #f6f7fb;
--bg-soft: #ffffff;
--panel: rgba(15, 17, 24, 0.04);
--panel-strong: rgba(15, 17, 24, 0.08);
--text: #0f1118;
--muted: #5a6078;
--accent: #0066ff;
--accent-strong: #111dff;
--border: rgba(15, 17, 24, 0.1);
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
background-color: var(--background-color);
text-align: center;
font-family: Verdana, Geneva, Tahoma, sans-serif;
margin: 0;
font-family: 'Space Grotesk', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
scrollbar-width: thin;
}
h1,
h3,
h4,
.scan {
margin: 0 auto;
color: var(--color);
text-align: center;
}
h3 {
padding: 10px 0;
border-bottom: 1px solid var(--footer);
margin-bottom: 1rem;
}
a,
a:hover {
color: var(--color);
a {
color: inherit;
text-decoration: none;
}
input[type="text"] {
padding: 10px;
.muted {
color: var(--muted);
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.75rem;
color: var(--accent);
}
.subtitle {
margin: 0.25rem 0 0;
color: var(--muted);
}
.app-shell {
padding: 2rem clamp(1rem, 4vw, 4rem) 3rem;
background: radial-gradient(circle at top, rgba(126, 243, 228, 0.15), transparent 45%), var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 2rem;
}
.hero-grid {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.content-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
gap: 2rem;
align-items: start;
}
@media (max-width: 900px) {
.content-layout {
grid-template-columns: 1fr;
}
}
.title-card {
display: flex;
justify-content: space-between;
gap: 1.5rem;
padding: 1.75rem;
border-radius: 1.5rem;
background: linear-gradient(135deg, rgba(126, 243, 228, 0.15), rgba(90, 209, 255, 0.05));
border: 1px solid var(--border);
flex-wrap: wrap;
}
.title-stack {
flex: 1;
}
.title-card h1 {
margin: 0.2rem 0 0.35rem;
font-size: clamp(2rem, 5vw, 3.4rem);
}
.ghost-button,
.secondary-button,
.footer-actions button,
.footer-actions a,
.ticker-chip,
.suggestions button,
.instance-card__header button,
.instance-card__import button,
.modal-actions button,
.search-panel__header .ghost-button {
border: 1px solid transparent;
background: transparent;
color: var(--text);
padding: 0.65rem 1rem;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, border 0.2s ease, transform 0.2s ease;
}
.ghost-button {
border-color: var(--border);
background: var(--panel);
}
.ghost-button:hover,
.secondary-button:hover,
.footer-actions button:hover,
.footer-actions a:hover,
.ticker-chip:hover,
.suggestions button:hover,
.instance-card__import button:hover,
.modal-actions button:hover {
border-color: var(--accent);
background: var(--panel-strong);
transform: translateY(-1px);
}
.ticker-card,
.stats-card,
.search-panel,
.scan-card,
.instance-card,
.instance-card__import {
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 1.25rem;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
}
.ticker-card__header {
display: flex;
gap: 0.75rem;
align-items: center;
margin-bottom: 1rem;
font-weight: 600;
}
.ticker-card__title small {
display: block;
font-size: 0.8rem;
color: var(--muted);
}
.ticker-card__body {
overflow: hidden;
}
.ticker-marquee {
display: inline-flex;
gap: 0.75rem;
animation: ticker 45s linear infinite;
min-width: 100%;
}
.ticker-card:hover .ticker-marquee {
animation-play-state: paused;
}
@keyframes ticker {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.ticker-chip {
font-size: 0.9rem;
border-color: var(--border);
background: var(--panel);
white-space: nowrap;
}
.ticker-chip__time {
font-size: 0.75rem;
color: var(--muted);
}
.stats-card {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
align-items: start;
}
.stats-card__main {
display: flex;
gap: 1rem;
align-items: center;
}
.stats-card__icon {
width: 3.5rem;
height: 3.5rem;
border-radius: 1rem;
display: grid;
place-items: center;
background: var(--panel);
color: var(--accent);
}
.stats-card__main h2 {
margin: 0.2rem 0;
font-size: 2.2rem;
}
.stats-card__links {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stats-card__links a {
color: var(--accent);
font-weight: 600;
display: inline-flex;
gap: 0.4rem;
align-items: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin: 0;
}
.stats-grid div {
padding: 0.75rem 1rem;
background: var(--panel);
border-radius: 0.85rem;
}
.stats-grid dt {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin: 0 0 0.25rem;
}
.stats-grid dd {
margin: 0;
font-weight: 600;
font-size: 1.1rem;
}
.search-panel {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.search-panel__header {
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.input-stack {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.input-field {
flex: 1;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.65rem 1rem;
border-radius: 999px;
background: var(--panel);
border: 1px solid var(--border);
}
.input-field input {
flex: 1;
border: none;
border-bottom: 1px solid var(--footer);
background-color: var(--instance-list);
color: var(--color);
background: transparent;
color: var(--text);
font-size: 1rem;
outline: none;
}
.placeholder {
font-size: small;
margin: 0 auto;
color: var(--color);
.secondary-button {
border-color: var(--accent);
color: var(--accent);
}
.title, .placeholder a, footer a, .download-csv {
cursor: pointer;
.suggestions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
hr {
margin: 1rem auto;
border-bottom: 1px solid var(--footer);
width: 50%;
.suggestions button {
border-color: var(--border);
background: var(--panel);
}
.tooltip {
visibility: hidden;
font-size: small;
text-align: left;
width: 2.2in;
background-color: var(--background-color);
color: var(--color);
border-radius: 5px;
padding: 1rem 1rem 0 0;
position: absolute;
z-index: 1;
opacity: 0;
transition: 0.5s;
border: 1px solid var(--footer);
}
.count:hover .tooltip {
visibility: visible;
opacity: 1;
}
.reverse {
margin: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--footer);
background-color: var(--color);
color: var(--background-color);
}
.reverse:hover {
color: var(--color);
background-color: var(--background-color);
cursor: pointer;
.list-shell {
max-height: 65vh;
overflow: hidden;
border-radius: 1rem;
border: 1px solid var(--border);
}
.instancelist {
list-style-type: none;
margin: 20px 0;
list-style: none;
margin: 0;
padding: 0;
color: var(--color);
max-height: 58vh;
overflow: auto;
max-height: inherit;
overflow-y: auto;
scrollbar-width: thin;
}
@media (min-width: 992px) {
.instancelist li {
width: 30%;
}
.instancelist li:hover {
width: 35%;
}
.instance,
.placeholder,
h4 {
width: 30%;
}
.modal-content {
width: 28%;
}
.instance-card,
.instance-card__import {
border-radius: 0;
border: none;
box-shadow: none;
border-bottom: 1px solid var(--border);
}
@media (min-width: 600px) and (max-width: 992px) {
.instancelist li {
width: 60%;
}
.instancelist li:hover {
width: 65%;
}
.instance,
.placeholder,
h4 {
width: 60%;
}
.modal-content {
width: 58%;
}
.instance-card:last-child {
border-bottom: none;
}
@media (max-width: 600px) {
.instancelist li {
width: 90%;
}
.instance-card__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.instancelist li:hover {
width: 95%;
}
.instance-card__header button {
padding: 0;
border: none;
background: none;
font-size: 1.05rem;
}
.instance,
.placeholder,
h4 {
width: 90%;
}
.instance-card__badge {
font-size: 0.8rem;
color: var(--accent);
}
.api {
.instance-card__body {
font-size: 0.85rem;
color: var(--muted);
display: grid;
gap: 0.25rem;
}
.instance-card__body img {
width: 100%;
border-radius: 0.75rem;
}
.instance-card__import {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.instance-card__order {
font-weight: 700;
margin-right: 1rem;
color: var(--accent);
}
.scan-card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.scan-card header {
display: flex;
gap: 1rem;
align-items: center;
}
.scan-card p {
margin: 0;
font-size: 1rem;
}
.app-footer {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
border-radius: 1.25rem;
border: 1px solid var(--border);
background: var(--bg-soft);
}
.footer-metrics {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.95rem;
}
.footer-metrics svg {
color: var(--accent);
margin-right: 0.5rem;
}
.footer-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.footer-actions a {
text-decoration: none;
}
.footer-credit {
margin-left: auto;
font-size: 0.85rem;
color: var(--muted);
}
.footer-actions a.footer-credit {
border: none;
padding: 0;
background: transparent;
color: var(--muted);
}
.footer-actions a.footer-credit:hover {
color: var(--text);
border: none;
background: transparent;
transform: none;
}
.loader-content {
z-index: 100;
background: rgba(5, 6, 10, 0.75);
width: 100%;
height: 100%;
display: none;
}
.modal-content {
width: 88%;
}
position: fixed;
inset: 0;
backdrop-filter: blur(6px);
}
.instancelist li {
color: var(--color);
padding: 10px;
border-bottom: 1px solid var(--footer);
margin: 0 auto;
max-height: 1.5em;
overflow: hidden;
min-height: 1.5em;
line-height: 1.5;
}
.instancelist li:hover {
background-color: var(--instance-list);
cursor: pointer;
max-height: fit-content;
}
.instancelist li img {
width: 70%;
margin: 0 auto;
}
@keyframes opacity {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
footer {
.loader {
background: var(--bg-soft);
padding: 2rem;
border-radius: 1.5rem;
border: 1px solid var(--border);
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-size: 12px;
padding: 10px;
color: var(--footer);
}
.count {
text-decoration: underline dotted var(--color);
cursor: help;
position: relative;
display: inline-block;
text-underline-offset: 2px;
}
.blocklist {
width: 90%;
text-align: left;
list-style: none;
padding: 1rem;
}
.blocklist li {
color: var(--color);
white-space: nowrap;
word-wrap: break-word;
overflow: hidden;
}
.blocklist li:hover {
white-space: inherit;
cursor: pointer;
color: var(--color);
}
.blocklist li a,
.blocklist li a:hover,
.blocklist li a:visited {
color: var(--color);
text-decoration: underline;
}
.blockinstance a,
.blockinstance a:hover {
color: var(--color);
text-decoration: underline;
cursor: help;
}
.blockcount {
font-weight: bolder;
.loader p {
margin-top: 1rem;
color: var(--muted);
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
scrollbar-width: thin;
background-color: rgba(0, 0, 0, 0.4);
inset: 0;
background: rgba(5, 6, 10, 0.8);
backdrop-filter: blur(8px);
z-index: 200;
padding: 1rem;
}
.modal-content {
position: relative;
background-color: var(--background-color);
margin: 10px auto;
padding: 0;
border: 1px solid var(--footer);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
max-width: 640px;
margin: auto;
background: var(--bg-soft);
border-radius: 1.5rem;
border: 1px solid var(--border);
overflow: hidden;
animation-duration: 0.4s;
animation-name: animatetop;
}
.modal-header {
padding: 6px;
background-color: var(--color);
color: var(--background-color);
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.modal-actions {
display: flex;
gap: 0.5rem;
}
.modal-body {
color: var(--color);
background-color: var(--background-color);
padding: 6px;
padding: 1.5rem;
}
.blocklist {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
max-height: 60vh;
overflow-y: auto;
scrollbar-width: thin;
}
.blocklist li {
line-height: 1.4;
}
.blocklist a {
color: var(--accent);
cursor: pointer;
}
.link-button {
border: none;
background: transparent;
color: var(--accent);
text-decoration: underline;
cursor: pointer;
padding: 0;
font: inherit;
}
.link-button:hover {
color: var(--accent-strong);
}
.blockcount {
font-size: 1.4rem;
color: var(--accent);
}
.modal-footer {
padding: 6px;
background-color: var(--color);
color: var(--background-color);
}
.closemodal,
.download,
.capture {
color: var(--footer);
float: right;
font-size: 3rem;
font-weight: bolder;
margin: 0px 15px;
}
.closemodal:hover,
.closemodal:focus,
.download:hover,
.download:focus,
.capture:hover,
.capture:focus {
color: var(--color);
text-decoration: none;
cursor: pointer;
border-top: 1px solid var(--border);
padding: 1rem 1.5rem;
}
@keyframes animatetop {
from {
top: -300px;
opacity: 0;
transform: translateY(60px);
}
to {
top: 0;
opacity: 1;
transform: translateY(0);
}
}
@keyframes animatebottom {
from {
top: 0;
opacity: 1;
transform: translateY(0);
}
to {
top: -300px;
opacity: 0
opacity: 0;
transform: translateY(60px);
}
}
h4 {
white-space: nowrap;
overflow: hidden;
position: relative;
border-left: 1px solid var(--background-color);
border-right: 1px solid var(--background-color);
transition: 0.2s;
}
h4 p {
margin: 0.3rem 0;
}
h4:hover {
width: 90%;
}
.bounce {
display: inline-block;
animation: marquee 90s linear infinite;
}
.bounce:hover {
animation-play-state: paused;
}
@keyframes marquee {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-100%);
}
}
.loader-content {
z-index: 0;
background-color: rgba(0, 0, 0, 0.4);
width: 100%;
height: 100%;
display: none;
position: fixed;
top: 0;
left: 0;
overflow: auto;
scrollbar-width: thin;
}
.loader {
background-color: var(--color);
width: fit-content;
margin: 50vh auto;
z-index: 1;
position: relative;
color: var(--background-color);
}

Ver fichero

@@ -12,16 +12,19 @@ function App() {
const [searchTerm, setSearchTerm] = useState(''),
[matrix, setMatrix] = useState('off')
return (
<>
<div className="app-shell">
<header className="hero-grid">
<Title />
<Bar setSearch={d => setSearchTerm(d)} />
<Count />
</header>
<main className="content-layout">
<Form searchTerm={searchTerm} matrix={matrix} />
<Scan />
<hr />
</main>
<Footer setCurrentMatrix={m => setMatrix(m)} />
<Loader />
</>
</div>
);
}

Ver fichero

@@ -1,9 +1,11 @@
import { useEffect, useState, useCallback } from 'react';
import dayjs from '../../node_modules/dayjs/';
import relativeTime from '../../node_modules/dayjs/plugin/relativeTime';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { FiActivity } from 'react-icons/fi';
dayjs.extend(relativeTime)
const Bar = (prop) => {
const Bar = ({ setSearch }) => {
const [bounce, setBounce] = useState([]),
fillBounce = useCallback(async () => {
try {
@@ -15,14 +17,47 @@ const Bar = (prop) => {
} catch (e) {
console.error(e)
}
})
}, [])
useEffect(() => {
fillBounce()
}, [])
}, [fillBounce])
return (
<h4>
<p className="bounce">{bounce && bounce.length > 0 ? bounce.map(b => <a onClick={() => prop.setSearch(b.content.replace(/.*#/g, '').replace(/\<\/a\>\<\/p\>/, ''))}>{b.content.replace(/<[^>]*>/g, '').replace(new RegExp(new URL(window.location.href).host), '')} - {dayjs().to(b.published)}</a>).reduce((prev, curr) => [prev, ' | ', curr]) : []}</p>
</h4>
<section className="ticker-card" aria-live="polite">
<header className="ticker-card__header">
<FiActivity aria-hidden="true" />
<div className="ticker-card__title">
<span>Recent activity</span>
<small>Select any entry to reuse as a search</small>
</div>
</header>
<div className="ticker-card__body">
{bounce?.length ? (
<div className="ticker-marquee">
{bounce.map((b, index) => {
const sanitized = b.content.replace(/.*#/g, '').replace(/<\/a><\/p>/, ''),
plainText = b.content
.replace(/<[^>]*>/g, '')
.replace(new RegExp(new URL(window.location.href).host), '')
return (
<button
type="button"
className="ticker-chip"
key={`${b.id ?? index}-${b.published}`}
onClick={() => setSearch(sanitized)}
>
<span className="ticker-chip__content">{plainText}</span>
<span className="ticker-chip__time">{dayjs().to(b.published)}</span>
</button>
)
})}
</div>
) : (
<span className="muted">No recent activity</span>
)}
</div>
</section>
)
}

Ver fichero

@@ -1,8 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { FiTrendingUp, FiExternalLink } from 'react-icons/fi';
const Count = () => {
const [count, setCount] = useState('0'),
[statsres, setStatsres] = useState(''),
const [count, setCount] = useState(0),
[statsres, setStatsres] = useState(null),
fillStats = useCallback(async () => {
try {
const [res, stats] = await Promise.all([(await fetch('/api/count')).json(), (await fetch('/api/stats')).json()])
@@ -11,30 +12,82 @@ const Count = () => {
} catch (e) {
console.error(e)
}
})
useEffect(() => {
(async () => {
await fillStats()
})()
}, [])
useEffect(() => {
fillStats()
}, [fillStats])
const statLines = useMemo(() => {
if (!statsres) {
return []
}
const safeDiv = (numerator, denominator) => {
if (!denominator) return 0
return numerator / denominator
}
return [
{ label: 'Statuses AVG', value: Math.round(statsres.status_avg ?? 0) },
{ label: 'Statuses MAX', value: statsres.status_max },
{ label: 'Domain AVG', value: Math.round(statsres.domain_avg ?? 0) },
{ label: 'Domain MAX', value: statsres.domain_max },
{ label: 'Users AVG', value: Math.round(statsres.user_avg ?? 0) },
{ label: 'Users MAX', value: statsres.user_max },
{ label: 'Stats Instances', value: statsres.stats_filtered },
{ label: 'Total Instances', value: statsres.instance_count },
{
label: 'Users per Instance',
value: safeDiv(Math.round(statsres.user_avg ?? 0), statsres.instance_count ?? 1).toFixed(2)
},
{
label: 'Statuses per Domain',
value: safeDiv(Math.round(statsres.status_avg ?? 0), Math.round(statsres.domain_avg ?? 1)).toFixed(2)
},
{
label: 'Statuses per User',
value: safeDiv(Math.round(statsres.status_avg ?? 0), Math.round(statsres.user_avg ?? 1)).toFixed(2)
}
]
}, [statsres])
const formatNumber = value => {
if (Number.isNaN(Number(value))) {
return value ?? '--'
}
return new Intl.NumberFormat().format(value ?? 0)
}
return (
<h3>Search in <a href="/api/stats" target="_blank">
<span className="count">{count}<div className="tooltip">
<u><strong><center>STATS</center></strong></u>
<ul><li>Statuses AVG: {Math.round(statsres.status_avg)}</li>
<li>Statuses MAX: {statsres.status_max}</li>
<li>Domain AVG: {Math.round(statsres.domain_avg)}</li>
<li>Domain MAX: {statsres.domain_max}</li>
<li>Users AVG: {Math.round(statsres.user_avg)}</li>
<li>Users MAX: {statsres.user_max}</li>
<li>Stats Instances: {statsres.stats_filtered}</li>
<li>Total Instances: {statsres.instance_count}</li>
<li>Users by Instance: {(Math.round(statsres.user_avg) / statsres.instance_count).toFixed(2)}</li>
<li>Statuses by Domain: {(Math.round(statsres.status_avg) / Math.round(statsres.domain_avg)).toFixed(2)}</li>
<li>Statuses by User: {(Math.round(statsres.status_avg) / Math.round(statsres.user_avg)).toFixed(2)}</li>
</ul></div></span>
</a> public instances<span className="api"> - <a href="/api-docs" target="_blank">API docs</a></span>
</h3>
<section className="stats-card">
<div className="stats-card__main">
<div className="stats-card__icon">
<FiTrendingUp aria-hidden="true" />
</div>
<div>
<p className="muted">Searching across</p>
<h2>{formatNumber(count)}</h2>
<p className="muted">public instances</p>
</div>
</div>
<div className="stats-card__links">
<a href="/api/stats" target="_blank" rel="noreferrer">
View stats
<FiExternalLink aria-hidden="true" />
</a>
<a href="/api-docs" target="_blank" rel="noreferrer">
API docs
<FiExternalLink aria-hidden="true" />
</a>
</div>
<dl className="stats-grid">
{statLines.map(item => (
<div key={item.label}>
<dt>{item.label}</dt>
<dd>{formatNumber(item.value)}</dd>
</div>
))}
</dl>
</section>
)
}

Ver fichero

@@ -1,24 +1,23 @@
import { useEffect, useState, useCallback } from 'react';
import { FiSun, FiMoon, FiDownload, FiGrid, FiZap } from 'react-icons/fi';
import Messenger from '../random-text';
const Footer = (prop) => {
const Footer = ({ setCurrentMatrix }) => {
const [theme, setTheme] = useState('dark'),
[matrix, setMatrix] = useState('off'),
[served, setServed] = useState({ served: 0, lastscan: 0, server: 0, instances: 0, peers: 0, created: 0, updated: 0 }),
loading = () => {
document.querySelector('.loader-content').style.display = 'initial'
toggleLoader = useCallback(() => {
const loader = document.querySelector('.loader-content')
if (loader) {
loader.style.display = 'initial'
setTimeout(() => {
document.querySelector('.loader-content').style.display = 'none'
loader.style.display = 'none'
}, 60 * 1000)
},
toggleTheme = useCallback(() => {
let tm = document.documentElement.dataset.theme
if (!theme || tm === 'light') {
setTheme('dark')
} else {
setTheme('light')
}
}),
}, []),
toggleTheme = useCallback(() => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'))
}, []),
refreshServed = useCallback(async () => {
try {
const response = await fetch('/api/served')
@@ -28,38 +27,67 @@ const Footer = (prop) => {
} catch (e) {
console.error(e)
}
}),
}, []),
toggleMatrix = useCallback(() => {
if (matrix === 'off') {
setMatrix('on')
prop.setCurrentMatrix('on')
} else {
setMatrix('off')
prop.setCurrentMatrix('off')
}
setMatrix(prev => {
const next = prev === 'off' ? 'on' : 'off'
setCurrentMatrix(next)
return next
})
}, [setCurrentMatrix])
useEffect(() => {
setTheme(theme)
document.documentElement.dataset.theme = theme
}, [theme])
useEffect(() => {
refreshServed()
}, [refreshServed])
useEffect(() => {
if (matrix === 'on') {
var walker = document.createTreeWalker(document.getElementById('root'), NodeFilter.SHOW_TEXT)
const root = document.getElementById('root')
if (root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
}
}
}
}, [theme])
}
}, [matrix])
return (
<footer>Served <span className="served">{served.served}</span> times - Last scan <span className="lastscan">{served.lastscan}</span> peers of <span
className="server">{served.server}</span><br />
Total scanned <span id="instances">{served.instances}</span> instances with <span className="peers">{served.peers}</span> peers - <span
className="created">{served.created}</span> created - <span className="updated">{served.updated}</span> updated<br />
matrix <a className="matrix" onClick={() => toggleMatrix()}>{matrix}</a> - download json <a className="download_index" href="/api/download_index"
onClick={() => loading()} download="fediblock-index.json.gz" target="_blank">index</a> -
by <a href="https://about.manalejandro.com" target="_blank">ale</a> <s>&copy;</s>2025&nbsp;
<a className="darklight" onClick={() => toggleTheme()}>{!theme || theme === 'dark' ? '☼' : '☽'}</a>
<footer className="app-footer">
<div className="footer-metrics">
<div>
<FiGrid aria-hidden="true" />
<p>
<strong>{served.served}</strong> requests served - Last scan {served.lastscan}
</p>
</div>
<p>
Total {served.instances} instances - {served.peers} peers - {served.created} created - {served.updated} updated - server {served.server}
</p>
</div>
<div className="footer-actions">
<button type="button" onClick={toggleMatrix}>
<FiZap aria-hidden="true" />
Matrix {matrix === 'on' ? 'ON' : 'OFF'}
</button>
<a className="download_index" href="/api/download_index" onClick={toggleLoader} download="fediblock-index.json.gz" target="_blank" rel="noreferrer">
<FiDownload aria-hidden="true" />
Download index
</a>
<button type="button" onClick={toggleTheme}>
{theme === 'dark' ? <FiSun aria-hidden="true" /> : <FiMoon aria-hidden="true" />}
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
</button>
<a href="https://about.manalejandro.com" target="_blank" rel="noreferrer" className="footer-credit">
by ale <s>&copy;</s>2025
</a>
</div>
</footer>
)
}

Ver fichero

@@ -1,167 +1,249 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { FiSearch, FiRotateCcw, FiRefreshCcw, FiChevronRight } from 'react-icons/fi';
import Modal from './Modal';
import Messenger from '../random-text';
const Form = (prop) => {
let csv
const sanitizeInstanceInput = value => {
if (!value) return ''
let sanitized = value.replace(/[^a-z0-9.\-*:]/gi, '')
if (sanitized.startsWith('.')) {
sanitized = sanitized.substring(1)
}
return sanitized
}
const Form = ({ searchTerm: externalSearch, matrix }) => {
const [searchTerm, setSearchTerm] = useState(''),
[list, setList] = useState([]),
[typeList, setTypeList] = useState('ranking'),
[domain, setDomain] = useState({ domain: '' }),
[reverse, setReverse] = useState(false),
refList = useRef(null),
refDownload = useRef(null),
loading = () => {
document.querySelector('.loader-content').style.display = 'initial'
},
download = () => {
if (csv.split('\n').length > 2) {
refDownload.current.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
refDownload.current.download = 'fediblock-top100.csv'
csvRef = useRef(''),
toggleLoader = useCallback((show = true) => {
const loader = document.querySelector('.loader-content')
if (loader) {
loader.style.display = show ? 'initial' : 'none'
}
},
}, []),
download = useCallback(() => {
if (csvRef.current.split('\n').length > 2) {
const blob = new Blob([csvRef.current], { type: 'text/csv' }),
url = window.URL.createObjectURL(blob),
anchor = document.createElement('a')
anchor.href = url
anchor.download = 'fediblock-top100.csv'
anchor.click()
window.URL.revokeObjectURL(url)
}
}, []),
listinstance = useCallback(async content => {
loading()
if (content && content.length > 0) {
if (!content || !content.length) {
toggleLoader(false)
return
}
toggleLoader(true)
try {
const result = await fetch('/api/list/' + content),
res = await result.json()
if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) {
setTypeList('list')
setList(res)
document.querySelector('.loader-content').style.display = 'none'
} else {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: No response')
}
} catch (e) {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: ' + e.message)
} finally {
toggleLoader(false)
}
} else {
document.querySelector('.loader-content').style.display = 'none'
}
}),
}, [toggleLoader]),
ranking = useCallback(async () => {
loading()
toggleLoader(true)
try {
const result = await fetch('/api/ranking'),
res = await result.json()
if (Array.isArray(res) && res.length > 0) {
setTypeList('ranking')
setList(res)
document.querySelector('.loader-content').style.display = 'none'
} else {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: No response')
}
} catch (e) {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: ' + e.message)
} finally {
toggleLoader(false)
}
}),
filterKeys = useCallback(event => {
if (event.key && !event.ctrlKey && !event.altKey) {
if ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && event.target.value !== '')) {
if (event.key === '.' && event.target.value === '') {
return
} else if (event.key === 'Backspace' && event.target.value !== '') {
setSearchTerm(searchTerm.substring(0, searchTerm.length - 1))
} else {
setSearchTerm(searchTerm + event.key)
}, [toggleLoader]),
handleReverseSearch = useCallback(() => {
if (searchTerm && searchTerm.length > 0) {
setReverse(true)
setDomain({ domain: searchTerm })
toggleLoader(true)
}
}
}
})
}, [searchTerm, toggleLoader])
const handleInputChange = useCallback(event => {
setSearchTerm(sanitizeInstanceInput(event.target.value))
}, [])
useEffect(() => {
if (searchTerm && searchTerm.length > 0) {
listinstance(searchTerm)
window.location.hash = searchTerm
} else {
(async () => {
await ranking()
})()
ranking()
}
if (prop.matrix === 'on') {
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
}, [searchTerm, listinstance, ranking])
useEffect(() => {
if (matrix === 'on' && refList.current) {
const walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
}
}
}
}, [searchTerm, prop.matrix])
}, [list, matrix])
useEffect(() => {
if (prop.searchTerm && prop.searchTerm.length > 0) {
setSearchTerm(prop.searchTerm)
if (externalSearch && externalSearch.length > 0) {
setSearchTerm(sanitizeInstanceInput(externalSearch))
}
}, [prop.searchTerm])
}, [externalSearch])
useEffect(() => {
if (window.location.hash && window.location.hash !== '#') {
setSearchTerm(window.location.hash.substring(1))
setSearchTerm(sanitizeInstanceInput(window.location.hash.substring(1)))
}
}, [])
const suggestions = typeList === 'list' && list.suggests && list.suggests.length > 0 ? list.suggests : []
const host = typeof window !== 'undefined' ? new URL(window.location.href).host : 'fediblock'
if (typeList === 'ranking') {
csvRef.current = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
}
return (
<>
<section>
<section className="search-panel">
<header className="search-panel__header">
<div>
<span className="eyebrow">Explore instances</span>
<h2>Review blocklists in seconds</h2>
<p className="muted">Search a specific domain or browse the most reported top 100.</p>
</div>
<button type="button" className="ghost-button" onClick={ranking}>
<FiRefreshCcw aria-hidden="true" />
Refresh ranking
</button>
</header>
<div className="input-stack">
<label className="input-field">
<FiSearch aria-hidden="true" />
<input
autoFocus
type="text"
autoComplete="off"
className="instance"
placeholder="Type the name of the instance"
onKeyUp={(e) => filterKeys(e)}
value={searchTerm} />
<button className="reverse" title="Reverse search..." onClick={() => {
if (searchTerm && searchTerm.length > 0) {
setReverse(true)
setDomain({ domain: searchTerm })
loading()
}
}}>Reverse</button>
<div className="placeholder">{typeList === 'list' ? list.suggests && list.suggests.length > 0 ? list.suggests.map(suggest => (<a onClick={() => setSearchTerm(suggest)}>{suggest}</a>)).reduce((prev, curr) => [prev, ', ', curr]) : [] : []}</div>
<ul className="instancelist" ref={refList}>{typeList === 'list' ? list.instances.map(r =>
(<li><a onClick={() => {
placeholder="Type an instance domain"
value={searchTerm}
onChange={handleInputChange}
/>
</label>
<button className="secondary-button" title="Reverse lookup" type="button" onClick={handleReverseSearch}>
<FiRotateCcw aria-hidden="true" />
Reverse lookup
</button>
</div>
{suggestions.length > 0 && (
<div className="suggestions">
{suggestions.map(suggest => (
<button type="button" key={suggest} onClick={() => setSearchTerm(sanitizeInstanceInput(suggest))}>
{suggest}
<FiChevronRight aria-hidden="true" />
</button>
))}
</div>
)}
<div className="list-shell" ref={refList}>
<ul className="instancelist">
{typeList === 'list' && Array.isArray(list.instances)
? list.instances.map(r => (
<li className="instance-card" key={r.domain}>
<div className="instance-card__header">
<button
type="button"
onClick={() => {
if (r.blocks) {
setReverse(false)
setDomain({ domain: r.domain })
loading()
toggleLoader(true)
} else {
var a = document.createElement('a')
a.href = '/api/detail_api/' + r.domain
a.title = 'API info for ' + r.domain
a.target = '_blank'
a.dispatchEvent(new MouseEvent('click'))
window.open('/api/detail_api/' + r.domain, '_blank', 'noopener,noreferrer')
}
}}>{r.domain}</a>
<span dangerouslySetInnerHTML={{ __html: r.blocks ? ` - ` + r.blocks + ` blocks&nbsp;` : `&nbsp;` }}></span>
}}
>
{r.domain}
</button>
<span
className="instance-card__badge"
dangerouslySetInnerHTML={{ __html: r.blocks ? `${r.blocks} blocks` : '&nbsp;' }}
></span>
</div>
<div className="instance-card__body">
<span dangerouslySetInnerHTML={{
__html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">&#9432;</a>` + `<br />` : `<br />`
__html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">&#9432;</a>` : ''
}}></span>
<span dangerouslySetInnerHTML={{
__html: r.api?.title ? `<br /><br />` + r.api.title + ` - ` + r.api.uri + `<br />` : `<br /><br />`
__html: r.api?.title ? `${r.api.title} - ${r.api.uri}<br />` : ''
}}></span>
<span dangerouslySetInnerHTML={{ __html: r.last ? `Last update: ` + (new Date(r.last)).toLocaleString() + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.email ? `Email: ` + r.api.email + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: `Registration: ` + (r.api?.registrations ? `open` : `closed`) + ` - Version: ` + r.api?.version + `<br />` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.stats ? `Users: ` + r.api.stats.user_count + ` - Statuses: ` + r.api.stats.status_count + ` - Domains: ` + r.api.stats.domain_count + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.description ? `Description: ` + r.api.description + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.thumbnail ? `<img domain="${r.domain}" loading="lazy" src="${r.api.thumbnail}" />` + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.last ? `Last updated: ${(new Date(r.last)).toLocaleString()}<br />` : '' }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.email ? `Email: ${r.api.email}<br />` : '' }}></span>
<span dangerouslySetInnerHTML={{ __html: `Registration: ${(r.api?.registrations ? 'open' : 'closed')} - Version: ${r.api?.version ?? 'N/A'}<br />` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.stats ? `Users: ${r.api.stats.user_count} - Statuses: ${r.api.stats.status_count} - Domains: ${r.api.stats.domain_count}<br />` : '' }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.description ? `Description: ${r.api.description}<br />` : '' }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.thumbnail ? `<img domain="${r.domain}" loading="lazy" src="${r.api.thumbnail}" alt="${r.domain} thumbnail" />` : '' }}></span>
</div>
</li>
)) : typeList === 'ranking' ? list.map((r, i) => {
if (i === 0) {
csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
return (<><li><strong>Top 100</strong><br /><small className="download-top"><a ref={refDownload} onClick={() => download()}>(Import CSV)</a></small></li><li>{i + 1} - {r.domain} - {r.count} blocks</li></>)
} else {
csv += !r.domain.match(/\*/) ? r.domain + ',suspend,False,False,"suspended by top 100 of ' + new URL(window.location.href).host + '",False\n' : ''
return (<li>{i + 1} - {r.domain} - {r.count} blocks</li>)
))
: null}
{typeList === 'ranking' && Array.isArray(list)
? (
<>
<li className="instance-card instance-card__import">
<div>
<strong>Top 100</strong>
<p className="muted">Ready-to-import CSV for Mastodon.</p>
</div>
<button type="button" className="ghost-button" onClick={download}>
Download CSV
</button>
</li>
{list.map((r, i) => {
if (!r) return null
if (!r.domain.match(/\*/) && i > 0) {
csvRef.current += `${r.domain},suspend,False,False,"suspended by top 100 of ${host}",False\n`
}
}) : ''}
</ul>
</section >
<Modal domain={domain} reverse={reverse} setSearch={d => setSearchTerm(d)} matrix={prop.matrix} />
return (
<li className="instance-card" key={`${r.domain}-${i}`}>
<span className="instance-card__order">{i + 1}</span>
<div>
<strong>{r.domain}</strong>
<p className="muted">{r.count} reported blocks</p>
</div>
</li>
)
})}
</>
)
: null}
</ul>
</div>
<Modal domain={domain} reverse={reverse} setSearch={d => setSearchTerm(sanitizeInstanceInput(d))} matrix={matrix} />
</section>
)
}
export default Form;

Ver fichero

@@ -1,16 +1,18 @@
import { useEffect, useState } from 'react';
import '../loaders.css';
const loaderClasses = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario']
const Loader = () => {
const loaders = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario'],
[load, setLoad] = useState('load ' + loaders[Math.floor(Math.random() * loaders.length)])
const [load, setLoad] = useState(() => 'load ' + loaderClasses[Math.floor(Math.random() * loaderClasses.length)])
useEffect(() => {
setLoad('load ' + loaders[Math.floor(Math.random() * loaders.length)])
setLoad('load ' + loaderClasses[Math.floor(Math.random() * loaderClasses.length)])
}, [])
return (
<div className="loader-content">
<div className="loader-content" role="alert" aria-live="assertive">
<div className="loader">
<div className={load}></div>
<p>Preparing live data...</p>
</div>
</div>
)

Ver fichero

@@ -1,9 +1,9 @@
import { useEffect, useCallback, useState, useRef } from 'react';
import html2canvas from '../../node_modules/html2canvas/dist/html2canvas.js';
import html2canvas from 'html2canvas';
import { FiDownload, FiCamera, FiX } from 'react-icons/fi';
import Messenger from '../random-text.js';
const Modal = (prop) => {
let csv
const [blocktitle, setBlocktitle] = useState(''),
[blockcount, setBlockcount] = useState(''),
[blockinstance, setBlockinstance] = useState(''),
@@ -11,33 +11,54 @@ const Modal = (prop) => {
[blocklist, setBlocklist] = useState([]),
hrefCanvas = useRef(null),
refList = useRef(null),
csvRef = useRef(''),
currentDomain = prop.domain?.domain ?? 'instance',
loading = () => {
document.querySelector('.loader-content').style.display = 'initial'
const loader = document.querySelector('.loader-content')
if (loader) {
loader.style.display = 'initial'
}
},
stopLoading = () => {
const loader = document.querySelector('.loader-content')
if (loader) {
loader.style.display = 'none'
}
},
closeModal = event => {
if (event.target.classList.contains('modal') || event.target.classList.contains('closemodal')) {
document.querySelector('.modal-content').style.animationName = 'animatebottom'
const modalContent = document.querySelector('.modal-content')
if (modalContent) {
modalContent.style.animationName = 'animatebottom'
}
}
},
animationEnd = event => {
if (event.animationName === 'animatebottom') {
document.querySelector('.modal').style.display = 'none'
const modal = document.querySelector('.modal')
if (modal) {
modal.style.display = 'none'
}
}
},
capture = useCallback(async () => {
const canvas = await html2canvas(hrefCanvas.current, { useCORS: true }),
capture = document.querySelector('.capture')
capture.download = 'fediblock-' + Date.now() + '.png'
capture.href = canvas.toDataURL('image/png', 1.0)
capture.dispatchEvent(new MouseEvent('click'))
}),
download = () => {
const download = document.querySelector('.download')
if (csv.split('\n').length > 2) {
download.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
download.download = 'fediblock-' + prop.domain.domain + '.csv'
captureLink = document.createElement('a')
captureLink.download = 'fediblock-' + Date.now() + '.png'
captureLink.href = canvas.toDataURL('image/png', 1.0)
captureLink.click()
}, []),
download = useCallback(() => {
if (csvRef.current.split('\n').length > 2) {
const blob = new Blob([csvRef.current], { type: 'text/csv' }),
url = window.URL.createObjectURL(blob),
anchor = document.createElement('a')
anchor.href = url
anchor.download = 'fediblock-' + currentDomain + '.csv'
anchor.click()
window.URL.revokeObjectURL(url)
}
},
}, [currentDomain]),
reverse = useCallback(async content => {
if (content && content.length > 0) {
loading()
@@ -49,11 +70,16 @@ const Modal = (prop) => {
setBlockinstance('ing ' + content)
setBlocktook('took ' + res.took + 'ms')
setBlocklist(res.instances)
document.querySelector('.loader-content').style.display = 'none'
document.querySelector('.modal-content').style.animationName = 'animatetop'
document.querySelector('.modal').style.display = 'block'
const modalContent = document.querySelector('.modal-content')
const modal = document.querySelector('.modal')
if (modalContent) {
modalContent.style.animationName = 'animatetop'
}
if (modal) {
modal.style.display = 'block'
}
if (prop.matrix === 'on') {
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
const walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
@@ -61,13 +87,14 @@ const Modal = (prop) => {
}
}
}
stopLoading()
}
}),
}, [prop.matrix]),
listblock = useCallback(async (domain) => {
const result = await fetch('/api/detail/' + domain),
res = await result.json()
if (res.blocks && Array.isArray(res.blocks)) {
csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
csvRef.current = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
setBlocktitle('Blocked List')
setBlockcount(res.blocks.length)
setBlocktook('took ' + res.took + 'ms')
@@ -76,7 +103,7 @@ const Modal = (prop) => {
+ `Last update: ` + (new Date(res.last)).toLocaleString())
setBlocklist(res.blocks)
if (prop.matrix === 'on') {
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
const walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
@@ -84,16 +111,23 @@ const Modal = (prop) => {
}
}
} else {
var a = document.createElement('a')
a.href = '/api/detail_api/' + domain
a.title = 'API info for ' + domain
a.target = '_blank'
a.dispatchEvent(new MouseEvent('click'))
const link = document.createElement('a')
link.href = '/api/detail_api/' + domain
link.title = 'API info for ' + domain
link.target = '_blank'
link.dispatchEvent(new MouseEvent('click'))
}
document.querySelector('.loader-content').style.display = 'none'
document.querySelector('.modal-content').style.animationName = 'animatetop'
document.querySelector('.modal').style.display = 'block'
})
stopLoading()
const modalContent = document.querySelector('.modal-content')
const modal = document.querySelector('.modal')
if (modalContent) {
modalContent.style.animationName = 'animatetop'
}
if (modal) {
modal.style.display = 'block'
}
}, [prop.matrix])
useEffect(() => {
if (prop.domain && prop.domain.domain.length > 0) {
if (prop.reverse) {
@@ -102,38 +136,63 @@ const Modal = (prop) => {
listblock(prop.domain.domain)
}
}
}, [prop.domain, prop.reverse])
}, [prop.domain, prop.reverse, listblock, reverse])
return (
<div className="modal" onClick={(e) => closeModal(e)}>
<div className="modal-content" onAnimationEnd={(e) => animationEnd(e)} ref={refList}>
<div className="modal" onClick={closeModal}>
<div className="modal-content" onAnimationEnd={animationEnd} ref={refList}>
<div className="modal-header">
<span className="closemodal" title="Close" onClick={(e) => closeModal(e)}>&times;</span>
<a className="capture" title="Take snapshot" onClick={capture} type="image/png" target="_blank">&#128247;</a>
{!prop.reverse ? <a className="download" title="Download fediblock CSV mastodon file" onClick={download} type="text/csv" target="_blank">&#8681;</a> : ''}
<h2>{blocktitle}</h2>
<div>
<p className="eyebrow">{blocktitle}</p>
<h2>
<span className="blockcount">{blockcount}</span> public instances block<span className="blockinstance" dangerouslySetInnerHTML={{ __html: blockinstance }}></span>
</h2>
<small className="blocktook">{blocktook}</small>
</div>
<div className="modal-actions">
<button className="capture" title="Capture snapshot" onClick={capture} type="button">
<FiCamera aria-hidden="true" />
</button>
{!prop.reverse && (
<button className="download" title="Download CSV" onClick={download} type="button">
<FiDownload aria-hidden="true" />
</button>
)}
<button className="closemodal" title="Close" onClick={closeModal} type="button">
<FiX aria-hidden="true" />
</button>
</div>
</div>
<div className="modal-body" ref={hrefCanvas}>
<p>
<span className="blockcount">{blockcount}</span> public instances are block<span className="blockinstance" dangerouslySetInnerHTML={{ __html: blockinstance }}></span>
<br /><small className="blocktook">{blocktook}</small>
</p>
<ul className="blocklist">{prop.reverse ? blocklist.map((instance, index) => {
return (<li>{index + 1}. <a onClick={() => {
<ul className="blocklist">{prop.reverse ? blocklist.map((instance, index) => (
<li key={`reverse-${instance.instance}-${index}`}>
{index + 1}. <button type="button" className="link-button" onClick={() => {
prop.setSearch(instance.instance)
document.querySelector('.modal-content').style.animationName = 'animatebottom'
}}>{instance.instance}</a>{instance.comment ? ' - ' + instance.comment : ''}</li>)
}) : blocklist.map((r, i) => {
if (r?.domain) {
csv += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : ''
return (<li>{i + 1}. <a onClick={() => {
prop.setSearch(r.domain)
document.querySelector('.modal-content').style.animationName = 'animatebottom'
}}>{r.domain}</a>{r.severity ? ' - ' + r.severity : ''}{r.comment ? ' - ' + r.comment : ''}</li>)
const modalContent = document.querySelector('.modal-content')
if (modalContent) {
modalContent.style.animationName = 'animatebottom'
}
}}>{instance.instance}</button>{instance.comment ? ' - ' + instance.comment : ''}
</li>
)) : blocklist.map((r, i) => {
if (r?.domain) {
csvRef.current += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : ''
return (
<li key={`block-${r.domain}-${i}`}>
{i + 1}. <button type="button" className="link-button" onClick={() => {
prop.setSearch(r.domain)
const modalContent = document.querySelector('.modal-content')
if (modalContent) {
modalContent.style.animationName = 'animatebottom'
}
}}>{r.domain}</button>{r.severity ? ' - ' + r.severity : ''}{r.comment ? ' - ' + r.comment : ''}
</li>
)
}
return null
})}</ul>
</div>
<div className="modal-footer">
</div>
<div className="modal-footer"></div>
</div>
</div>
)

Ver fichero

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { FiCpu } from 'react-icons/fi';
const Scan = () => {
const [scan, setScan] = useState('Scanning...'),
@@ -9,7 +10,7 @@ const Scan = () => {
source.onmessage = event => {
if (event.data) {
if (event.data.length > 0) {
setScan('Async Scanning' + event.data)
setScan('Async Scanning ' + event.data)
} else {
setScan('Async Scanning...')
}
@@ -19,12 +20,22 @@ const Scan = () => {
console.error(error)
}
load.current = true
return () => {
source.close()
}
})
}
}, [])
return (
<section className="scan-card" aria-live="polite">
<header>
<FiCpu aria-hidden="true" />
<div>
<span className="scan">{scan}</span>
<p className="eyebrow">Live scan</p>
<strong>Federated monitor</strong>
</div>
</header>
<p>{scan}</p>
</section>
)
}

Ver fichero

@@ -1,12 +1,24 @@
import { useCallback } from 'react';
import { FiRefreshCcw } from 'react-icons/fi';
const Title = () => {
const click = useCallback(() => {
window.location.hash = ''
window.location.reload(false)
})
}, [])
return (
<h1><a onClick={() => click()} className="title">Fediblock Instance &#934;</a></h1>
<div className="title-card">
<div className="title-stack">
<span className="eyebrow">Fediverse safety toolkit</span>
<h1>Fediblock Instance &#934;</h1>
<p className="subtitle">Monitor instance health, blocks, and metrics in a clean, real-time dashboard.</p>
</div>
<button className="ghost-button" type="button" onClick={click}>
<FiRefreshCcw aria-hidden="true" />
Reset view
</button>
</div>
)
}