rsrc/extra/etc/net.html
2026-10-20 10:13:53 +00:00

813 lines
22 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>network tools</title>
<style>
:root{
--bg:#000;
--panel:#000;
--fg:#f5f5f5;
--muted:#8a8a8a;
--grid:#1a1a1a;
--green:#7CFFB2;
--red:#FF7A7A;
--amber:#FFD166;
--cyan:#8BE9FD;
--shadow:none;
}
*{box-sizing:border-box}
html{
background:var(--bg);
-webkit-text-size-adjust:100%;
text-size-adjust:100%;
}
body{
margin:0;
background:var(--bg);
color:var(--fg);
font:14px/1.5 "SF Mono","Cascadia Code","Roboto Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
}
.mono,.num,.pill,.bar,.controls input,.controls select,.controls button,.weight{
font-family:"SF Mono","Cascadia Code","Roboto Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
.a2{border:0 solid transparent!important;border:0}
.wrap{max-width:2500px;margin:auto;padding:14px}
.head{
position:sticky;
top:0;
z-index:20;
padding:10px 0;
background:rgba(0,0,0,.98);
backdrop-filter:blur(10px);
border-bottom:1px solid var(--grid);
}
.controls{
display:flex;
gap:8px;
align-items:center;
flex-wrap:wrap;
}
.controls input,
.controls select,
.controls button{
border:1px solid var(--grid);
background:#000;
color:var(--fg);
border-radius:0;
padding:8px 10px;
font-size:12px;
outline:none;
}
.controls input{width:min(345px,100%)}
.controls select{width:185px}
.controls button{
width:auto;
cursor:pointer;
transition:.15s;
}
.controls button:hover,
.controls button.active{
background:#03110a;
border-color:var(--green);
color:var(--green);
}
.controls button.reset{color:var(--amber)}
.controls button.weights-btn{color:var(--cyan)}
.legend{
display:flex;
gap:8px;
flex-wrap:wrap;
margin-top:8px;
color:var(--muted);
font-size:11px;
}
.pill{
display:inline-block;
border:1px solid var(--grid);
border-radius:0;
padding:1px 6px;
color:var(--fg);
font-size:10px;
white-space:nowrap;
background:#000;
}
.weights-drawer{
max-height:0;
overflow:hidden;
opacity:0;
transition:max-height .28s ease,opacity .2s ease,margin-top .2s ease;
margin-top:0;
}
.weights-drawer.open{
max-height:170px;
opacity:1;
margin-top:9px;
}
.weights-inner{
display:flex;
gap:7px;
align-items:center;
flex-wrap:wrap;
padding:8px;
background:#000;
border:1px solid var(--grid);
border-radius:0;
}
.weight-title{
color:var(--cyan);
font:11px "SF Mono","Cascadia Code",ui-monospace,monospace;
}
.weight{
display:grid;
grid-template-columns:auto 90px 34px;
align-items:center;
gap:6px;
font-size:11px;
color:var(--muted);
}
.weight input{
width:90px;
accent-color:var(--green);
}
.weight output{
color:var(--green);
text-align:right;
}
.meta{
display:flex;
gap:16px;
flex-wrap:wrap;
margin:12px 0;
color:var(--muted);
font-size:12px;
}
.meta b{color:var(--fg)}
.section-title{
margin:16px 0 7px;
font-size:12px;
letter-spacing:.055em;
text-transform:uppercase;
color:var(--green);
}
.table-box{
overflow:auto;
border-radius:0;
background:var(--panel);
box-shadow:none;
border:1px solid var(--grid);
}
table{
width:100%;
border-collapse:collapse;
table-layout:fixed;
min-width:1980px;
}
.table-box thead th{
position:sticky;
top:0;
z-index:5;
background:#050505;
color:#e8e8e8;
font-size:11px;
letter-spacing:.035em;
text-transform:uppercase;
}
th,td{
padding:9px 10px;
text-align:left;
vertical-align:top;
word-break:break-word;
border-bottom:1px solid #0f0f0f;
}
tbody tr:nth-child(even){background:rgba(255,255,255,.01)}
tbody tr:hover{background:rgba(255,255,255,.03)}
th.sortable{cursor:pointer;user-select:none}
th.sortable:hover{color:var(--green)}
.sort-mark{
font-size:9px;
color:var(--green);
margin-left:3px;
}
.tool{
font-weight:550;
color:var(--fg);
letter-spacing:.005em;
}
.stack{color:var(--cyan)}
.star{
color:var(--amber);
font-size:15px;
margin-right:4px;
text-shadow:0 0 6px rgba(255,209,102,.18);
}
.tool-link{
color:inherit;
text-decoration:none;
}
.tool-link:hover{
color:var(--cyan);
text-decoration:underline;
}
.num{
font-size:11px;
white-space:nowrap;
}
.bar{
height:8px;
min-width:84px;
margin-top:4px;
overflow:hidden;
border:1px solid #262626;
border-radius:999px;
background:#0a0a0a;
}
.fill{display:block;height:100%;background:#666}
.fill.red{background:var(--red)}
.fill.amber{background:var(--amber)}
.fill.cyan{background:var(--cyan)}
.top-row{}
.low-row{background:rgba(255,122,255,.055)!important}
.rank{
font-weight:500;
color:var(--amber);
}
.footer{
margin:12px 0 3px;
color:var(--muted);
font-size:11px;
}
@media(max-width:850px){
.head{position:relative}
.controls input,.controls select{width:100%}
.weights-inner{display:grid;grid-template-columns:1fr}
.weight{grid-template-columns:auto 1fr 34px}
.weight input{width:100%}
.wrap{padding:9px}
}
</style>
</head>
<body>
<main class="wrap">
<header class="head">
<div class="controls">
<input id="search" type="search" placeholder="search tools, category, features...">
<select id="category"><option value="">all groups</option></select>
<button id="toolsOnly">only tools</button>
<button data-sort="overall" class="active">overall</button>
<button data-sort="functionality">functionality</button>
<button data-sort="difficulty">setup difficulty</button>
<button data-sort="privacy">privacy</button>
<button data-sort="stealth">stealth</button>
<button data-sort="security">security</button>
<button data-sort="maturity">maturity</button>
<button id="weightsToggle" class="weights-btn">weights</button>
<button id="resetFilters" class="reset">reset</button>
</div>
<div id="weightsDrawer" class="weights-drawer">
<div class="weights-inner">
<span class="weight-title">score weights</span>
<label class="weight">functionality <input id="wf" type="range" min="0" max="100" value="25"><output id="of">25%</output></label>
<label class="weight">privacy <input id="wp" type="range" min="0" max="100" value="20"><output id="op">20%</output></label>
<label class="weight">security <input id="ws" type="range" min="0" max="100" value="20"><output id="os">20%</output></label>
<label class="weight">stealth <input id="wt" type="range" min="0" max="100" value="20"><output id="ot">20%</output></label>
<label class="weight">maturity <input id="wm" type="range" min="0" max="100" value="15"><output id="om">15%</output></label>
<button id="resetWeights" class="reset">reset weights</button>
</div>
</div>
<div class="legend">
<span class="pill a2">★ high overall score</span>
<span class="pill a2">green = functionality / top score</span>
<span class="pill a2">red = security / lowest score</span>
<span class="pill a2">cyan = privacy</span>
<span class="pill a2">amber = stealth</span>
</div>
</header>
<div class="meta">
<span>shown: <b id="shown">0</b></span>
<span>total: <b id="total">0</b></span>
<span>sort: <b id="sortLabel">overall descending</b></span>
<span>weights: <b id="weightLabel">f25 p20 s20 t20 m15</b></span>
</div>
<h2 class="section-title"></h2>
<div class="table-box">
<table>
<thead>
<tr>
<th style="width:4%">rank</th>
<th style="width:18%">tool / stack</th>
<th style="width:13%">group</th>
<th style="width:29%">usage / notes</th>
<th style="width:11%">overall</th>
<th style="width:8%">privacy</th>
<th style="width:8%">stealth</th>
<th style="width:9%">security</th>
</tr>
</thead>
<tbody id="topRows"></tbody>
</table>
</div>
<h2 class="section-title"></h2>
<div class="table-box">
<table>
<thead>
<tr>
<th class="sortable" data-sort="name" style="width:12%">tool / stack <span class="sort-mark"></span></th>
<th class="sortable" data-sort="category" style="width:12%">group <span class="sort-mark"></span></th>
<th style="width:21%">usage / notes</th>
<th class="sortable" data-sort="overall" style="width:11%">overall <span class="sort-mark"></span></th>
<th class="sortable" data-sort="functionality" style="width:11%">functionality <span class="sort-mark"></span></th>
<th class="sortable" data-sort="difficulty" style="width:11%">setup difficulty <span class="sort-mark"></span></th>
<th class="sortable" data-sort="privacy" style="width:11%">privacy <span class="sort-mark"></span></th>
<th class="sortable" data-sort="stealth" style="width:11%">stealth <span class="sort-mark"></span></th>
<th class="sortable" data-sort="security" style="width:11%">security <span class="sort-mark"></span></th>
<th class="sortable" data-sort="maturity" style="width:11%">maturity <span class="sort-mark"></span></th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
</div>
<p class="footer">
</p>
</main>
<script>
const defaults = {
functionality: 25,
privacy: 20,
security: 20,
stealth: 20,
maturity: 15
};
const weights = { ...defaults };
const state = {
toolsOnly: false,
sort: "overall",
direction: "desc"
};
let data = [];
let timer = null;
const rows = document.getElementById("rows");
const topRows = document.getElementById("topRows");
const search = document.getElementById("search");
const category = document.getElementById("category");
const shown = document.getElementById("shown");
const total = document.getElementById("total");
const sortLabel = document.getElementById("sortLabel");
const weightLabel = document.getElementById("weightLabel");
const drawer = document.getElementById("weightsDrawer");
const weightsToggle = document.getElementById("weightsToggle");
const toolsOnly = document.getElementById("toolsOnly");
const resetWeightsBtn = document.getElementById("resetWeights");
const resetFiltersBtn = document.getElementById("resetFilters");
function normalize(raw) {
const mapped = raw.map(item => {
if (Array.isArray(item)) {
const [
name,
category,
usage,
official,
repo,
functionality,
difficulty,
privacy,
stealth,
security,
maturity
] = item;
return {
name,
category,
usage,
official,
repo,
functionality: Number(functionality) || 0,
difficulty: Number(difficulty) || 0,
privacy: Number(privacy) || 0,
stealth: Number(stealth) || 0,
security: Number(security) || 0,
maturity: Number(maturity) || 0,
overall: 0,
stack: category === "composed stacks"
};
}
return {
name: item.name || "",
category: item.category || "",
usage: item.usage || "",
official: item.official || "",
repo: item.repo || "",
functionality: Number(item.functionality) || 0,
difficulty: Number(item.difficulty) || 0,
privacy: Number(item.privacy) || 0,
stealth: Number(item.stealth) || 0,
security: Number(item.security) || 0,
maturity: Number(item.maturity) || 0,
overall: 0,
stack: item.category === "composed stacks"
};
});
const seen = new Set();
return mapped.filter(tool => {
const key = [
tool.name.trim().toLowerCase(),
tool.category.trim().toLowerCase(),
(tool.repo || tool.official || "").trim().toLowerCase()
].join("__");
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function calc(tool) {
let sum =
weights.functionality +
weights.privacy +
weights.security +
weights.stealth +
weights.maturity;
if (!sum) {
Object.assign(weights, defaults);
sum = 100;
}
return +(
(
tool.functionality * weights.functionality +
tool.privacy * weights.privacy +
tool.security * weights.security +
tool.stealth * weights.stealth +
tool.maturity * weights.maturity
) / sum
).toFixed(1);
}
function bar(value, type) {
const colorClass =
type === "difficulty" || type === "security"
? "red"
: type === "privacy"
? "cyan"
: type === "stealth"
? "amber"
: "";
const safe = Math.max(0, Math.min(100, Number(value) || 0));
return `
<div class="num">${safe}/100</div>
<div class="bar" title="${safe}/100">
<span class="fill ${colorClass}" style="width:${safe}%"></span>
</div>
`;
}
function getFiltered() {
const q = search.value.trim().toLowerCase();
const selectedCategory = category.value;
return data.filter(tool => {
const haystack = `${tool.name} ${tool.category} ${tool.usage}`.toLowerCase();
return (
(!q || haystack.includes(q)) &&
(!selectedCategory || tool.category === selectedCategory) &&
(!state.toolsOnly || !tool.stack)
);
});
}
function compare(a, b, key = state.sort, dir = state.direction) {
let x = a[key];
let y = b[key];
if (typeof x === "string") {
x = x.toLowerCase();
y = y.toLowerCase();
}
if (x < y) return dir === "asc" ? -1 : 1;
if (x > y) return dir === "asc" ? 1 : -1;
return b.overall - a.overall || a.name.localeCompare(b.name);
}
function nameCell(tool) {
const star = tool.overall >= 88
? `<span class="star" title="high composite score">★</span>`
: "";
const href = tool.repo || tool.official || "#";
const cls = tool.stack ? "tool stack" : "tool";
const title = `open ${tool.repo ? "source repository" : "official site"}`;
return `
<a class="tool-link ${cls}" href="${href}" target="_blank" rel="noopener noreferrer" title="${title}">
${star}${tool.name} ↗
</a>
`;
}
function render() {
data.forEach(tool => {
tool.overall = calc(tool);
});
const filtered = getFiltered().sort(compare);
const ranked = [...filtered].sort((a, b) =>
b.overall - a.overall ||
b.security - a.security ||
a.name.localeCompare(b.name)
);
const hi = ranked.length ? ranked[0].overall : null;
const lo = ranked.length ? ranked[ranked.length - 1].overall : null;
topRows.innerHTML = ranked.length
? ranked.slice(0, 10).map((tool, i) => `
<tr class="top-row">
<td class="rank">${i + 1}</td>
<td>${nameCell(tool)}</td>
<td><span class="pill">${tool.category}</span></td>
<td>${tool.usage}</td>
<td>${bar(tool.overall, "overall")}</td>
<td>${bar(tool.privacy, "privacy")}</td>
<td>${bar(tool.stealth, "stealth")}</td>
<td>${bar(tool.security, "security")}</td>
</tr>
`).join("")
: `<tr><td colspan="8">no matching entries</td></tr>`;
rows.innerHTML = filtered.length
? filtered.map(tool => {
const cls =
tool.overall === hi && hi !== lo ? "top-row" :
tool.overall === lo && hi !== lo ? "low-row" :
"";
return `
<tr class="${cls}">
<td>${nameCell(tool)}</td>
<td><span class="pill">${tool.category}</span></td>
<td>${tool.usage}</td>
<td>${bar(tool.overall, "overall")}</td>
<td>${bar(tool.functionality, "functionality")}</td>
<td>${bar(tool.difficulty, "difficulty")}</td>
<td>${bar(tool.privacy, "privacy")}</td>
<td>${bar(tool.stealth, "stealth")}</td>
<td>${bar(tool.security, "security")}</td>
<td>${bar(tool.maturity, "maturity")}</td>
</tr>
`;
}).join("")
: `<tr><td colspan="10">no matching entries</td></tr>`;
shown.textContent = filtered.length;
total.textContent = data.length;
const labels = {
overall: "overall",
functionality: "functionality",
difficulty: "setup difficulty",
privacy: "privacy",
stealth: "stealth",
security: "security",
maturity: "maturity",
name: "name",
category: "group"
};
sortLabel.textContent = `${labels[state.sort]} ${state.direction === "asc" ? "ascending" : "descending"}`;
weightLabel.textContent = `f${weights.functionality} p${weights.privacy} s${weights.security} t${weights.stealth} m${weights.maturity}`;
document.querySelectorAll(".controls [data-sort]").forEach(btn => {
btn.classList.toggle("active", btn.dataset.sort === state.sort);
});
document.querySelectorAll("th[data-sort] .sort-mark").forEach(mark => {
const col = mark.parentElement.dataset.sort;
mark.textContent = col === state.sort
? (state.direction === "asc" ? "↑" : "↓")
: "↕";
});
toolsOnly.classList.toggle("active", state.toolsOnly);
}
function setSort(col) {
if (state.sort === col) {
state.direction = state.direction === "asc" ? "desc" : "asc";
} else {
state.sort = col;
state.direction = ["difficulty", "name", "category"].includes(col) ? "asc" : "desc";
}
render();
}
function syncWeights() {
const map = {
functionality: "f",
privacy: "p",
security: "s",
stealth: "t",
maturity: "m"
};
Object.entries(map).forEach(([key, id]) => {
weights[key] = Number(document.getElementById("w" + id).value);
document.getElementById("o" + id).value = `${weights[key]}%`;
});
clearTimeout(timer);
timer = setTimeout(render, 120);
}
function resetWeights() {
Object.assign(weights, defaults);
const map = {
functionality: "f",
privacy: "p",
security: "s",
stealth: "t",
maturity: "m"
};
Object.entries(map).forEach(([key, id]) => {
document.getElementById("w" + id).value = weights[key];
document.getElementById("o" + id).value = `${weights[key]}%`;
});
clearTimeout(timer);
render();
}
function populateCategories() {
category.innerHTML = `<option value="">all groups</option>`;
[...new Set(data.map(tool => tool.category))]
.sort()
.forEach(group => {
const option = document.createElement("option");
option.value = group;
option.textContent = group;
category.appendChild(option);
});
}
async function loadData() {
const candidateInputs = [
"/tools.json",
"./tools.json",
"/static/tools.json"
];
const uniqueCandidates = [];
const seenUrls = new Set();
for (const input of candidateInputs) {
const absolute = new URL(input, window.location.href).href;
if (!seenUrls.has(absolute)) {
seenUrls.add(absolute);
uniqueCandidates.push({ input, absolute });
}
}
topRows.innerHTML = `<tr><td colspan="8">loading…</td></tr>`;
rows.innerHTML = `<tr><td colspan="10">loading…</td></tr>`;
let lastError = null;
for (const candidate of uniqueCandidates) {
try {
console.log("Trying:", candidate.absolute);
const response = await fetch(candidate.absolute, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${response.url}`);
}
const raw = await response.json();
data = normalize(raw);
populateCategories();
render();
console.log("Loaded tools.json from:", response.url);
return;
} catch (error) {
console.warn("Failed:", candidate.absolute, error);
lastError = error;
}
}
console.error("All JSON load attempts failed.", lastError);
topRows.innerHTML = `<tr><td colspan="8">failed to load tools.json</td></tr>`;
rows.innerHTML = `<tr><td colspan="10">failed to load tools.json</td></tr>`;
}
search.addEventListener("input", render);
category.addEventListener("change", render);
document.querySelectorAll("[data-sort]").forEach(el => {
el.addEventListener("click", () => setSort(el.dataset.sort));
});
toolsOnly.addEventListener("click", () => {
state.toolsOnly = !state.toolsOnly;
render();
});
weightsToggle.addEventListener("click", () => {
drawer.classList.toggle("open");
weightsToggle.classList.toggle("active", drawer.classList.contains("open"));
});
["f", "p", "s", "t", "m"].forEach(id => {
document.getElementById("w" + id).addEventListener("input", syncWeights);
});
resetWeightsBtn.addEventListener("click", resetWeights);
resetFiltersBtn.addEventListener("click", () => {
search.value = "";
category.value = "";
state.toolsOnly = false;
state.sort = "overall";
state.direction = "desc";
render();
});
loadData();
</script>
</body>
</html>