Refine AMPGateway topology theater

This commit is contained in:
jwinterm
2026-07-07 14:32:58 -04:00
parent cfffb4c37c
commit 4289feb1e5
3 changed files with 420 additions and 213 deletions
+278 -136
View File
@@ -13,186 +13,264 @@
const topologies = { const topologies = {
vps: { vps: {
caption: "A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.", caption: "A VPS shape starts with one source tree, renders transport outputs, then hands them to service-specific daemons on the host.",
nodes: [ nodes: [
["source", "source site", 0.16, 0.42, "amber"], ["source", "source\nsite", 0.11, 0.48, "amber"],
["ampg", "AMPG", 0.34, 0.42, "teal"], ["build", "AMPG\nbuild", 0.27, 0.48, "teal"],
["vps", "VPS", 0.52, 0.42, "green"], ["outputs", "rendered\noutputs", 0.42, 0.3, "green"],
["web", "clearnet", 0.72, 0.22, "teal"], ["state", "deploy\nstate", 0.42, 0.62, "amber"],
["tor", "Tor", 0.78, 0.39, "coral"], ["vps", "VPS\nhost", 0.56, 0.46, "green"],
["i2p", "I2P", 0.76, 0.56, "amber"], ["web", "HTTPS\nnginx", 0.73, 0.23, "teal"],
["gemini", "Gemini", 0.66, 0.72, "green"], ["tor", "Tor\nonion", 0.76, 0.42, "coral"],
["reticulum", "Reticulum", 0.84, 0.72, "coral"] ["i2p", "I2P\ntunnel", 0.73, 0.61, "amber"],
["gemini", "Gemini\ncapsule", 0.6, 0.78, "green"],
["reticulum", "RNS\nnode", 0.88, 0.73, "coral"]
], ],
links: [ links: [
["source", "ampg"], ["source", "build"],
["ampg", "vps"], ["build", "outputs", -0.08],
["vps", "web"], ["build", "state", 0.08],
["outputs", "vps"],
["state", "vps"],
["vps", "web", -0.08],
["vps", "tor"], ["vps", "tor"],
["vps", "i2p"], ["vps", "i2p", 0.04],
["vps", "gemini"], ["vps", "gemini", 0.1],
["vps", "reticulum"] ["vps", "reticulum", 0.12]
] ]
}, },
router: { router: {
caption: "Behind a router, clearnet may need DNS and port choices; Tor, I2P, and Reticulum can avoid depending on inbound web ports.", caption: "Behind a router, clearnet depends on DNS and a chosen inbound path; onion, I2P, and Reticulum routes can publish without exposing normal web ports.",
nodes: [ nodes: [
["visitor", "visitor", 0.13, 0.33, "green"], ["visitor", "visitor", 0.1, 0.42, "green"],
["dns", "DNS/DDNS", 0.3, 0.24, "amber"], ["dns", "DNS /\nDDNS", 0.25, 0.24, "amber"],
["router", "router", 0.45, 0.38, "coral"], ["router", "router\nNAT", 0.4, 0.42, "coral"],
["host", "home host", 0.61, 0.38, "teal"], ["host", "home\nhost", 0.55, 0.42, "teal"],
["ampg", "AMPG", 0.76, 0.38, "teal"], ["ampg", "AMPG", 0.7, 0.42, "teal"],
["tor", "Tor", 0.64, 0.66, "coral"], ["clearnet", "port\nforward", 0.55, 0.22, "green"],
["i2p", "I2P", 0.78, 0.66, "amber"], ["onion", "Tor\nonion", 0.68, 0.69, "coral"],
["reticulum", "Reticulum", 0.9, 0.54, "green"] ["i2p", "I2P\nserver\ntunnel", 0.84, 0.61, "amber"],
["rns", "RNS\npeer", 0.88, 0.3, "green"]
], ],
links: [ links: [
["visitor", "dns"], ["visitor", "dns"],
["dns", "router"], ["dns", "router"],
["router", "host"], ["router", "host"],
["host", "ampg"], ["host", "ampg"],
["ampg", "tor"], ["ampg", "clearnet", -0.04],
["ampg", "i2p"], ["clearnet", "router"],
["ampg", "reticulum"], ["ampg", "onion", 0.05],
["visitor", "tor"], ["ampg", "i2p", 0.04],
["visitor", "i2p"], ["ampg", "rns", -0.05],
["visitor", "reticulum"] ["visitor", "onion", 0.16],
["visitor", "i2p", 0.12],
["visitor", "rns", -0.14]
] ]
}, },
phone: { phone: {
caption: "A spare Android phone can run Termux packages, AMPG state, and user-space transport services.", caption: "A spare Android phone can run Termux packages, local AMPG state, and user-space daemons for small always-on sites.",
nodes: [ nodes: [
["phone", "Android", 0.24, 0.45, "green"], ["phone", "Android\nphone", 0.14, 0.46, "green"],
["pkg", "pkg", 0.43, 0.26, "amber"], ["termux", "Termux\npackages", 0.31, 0.28, "amber"],
["ampg", "AMPG", 0.45, 0.45, "teal"], ["state", "AMPG\nstate", 0.31, 0.64, "coral"],
["state", "state", 0.43, 0.64, "coral"], ["ampg", "AMPG\nbuild", 0.48, 0.46, "teal"],
["services", "services", 0.64, 0.45, "green"], ["daemons", "user-space\ndaemons", 0.65, 0.46, "green"],
["routes", "routes", 0.82, 0.45, "teal"] ["network", "Wi-Fi /\ncell", 0.79, 0.28, "amber"],
["routes", "published\nroutes", 0.86, 0.58, "teal"],
["power", "power /\nstorage", 0.5, 0.78, "coral"]
], ],
links: [ links: [
["phone", "pkg"], ["phone", "termux", -0.05],
["phone", "ampg"],
["phone", "state"], ["phone", "state"],
["pkg", "services"], ["phone", "power", 0.08],
["ampg", "services"], ["termux", "ampg"],
["state", "services"], ["state", "ampg"],
["services", "routes"] ["ampg", "daemons"],
["daemons", "network", -0.05],
["daemons", "routes", 0.05],
["power", "routes", -0.1]
] ]
}, },
browser: { browser: {
caption: "Fixture manifests let AMPB or another checker verify every route through the expected transport profile.", caption: "Browser validation consumes AMPG fixture manifests, chooses an adapter, then verifies each route through the intended transport profile.",
nodes: [ nodes: [
["site", "site", 0.17, 0.5, "amber"], ["manifest", "fixture\nmanifest", 0.12, 0.43, "amber"],
["ampg", "AMPG", 0.33, 0.5, "teal"], ["ampb", "AMPB", 0.29, 0.43, "teal"],
["manifest", "manifest", 0.5, 0.5, "green"], ["policy", "route\npolicy", 0.45, 0.25, "green"],
["ampb", "AMPB", 0.66, 0.5, "coral"], ["adapters", "transport\nadapters", 0.5, 0.53, "coral"],
["transport", "transport", 0.82, 0.5, "teal"] ["existing", "existing\ndaemon", 0.67, 0.32, "green"],
["managed", "managed\ndaemon", 0.67, 0.64, "amber"],
["context", "browser\ncontext", 0.82, 0.47, "teal"],
["result", "health\nresult", 0.93, 0.47, "green"]
], ],
links: [ links: [
["site", "ampg"],
["ampg", "manifest"],
["manifest", "ampb"], ["manifest", "ampb"],
["ampb", "transport"], ["ampb", "policy", -0.08],
["transport", "ampb"] ["ampb", "adapters", 0.06],
["policy", "adapters"],
["adapters", "existing", -0.08],
["adapters", "managed", 0.08],
["existing", "context"],
["managed", "context"],
["context", "result"],
["result", "ampb", 0.16]
] ]
} }
}; };
const protocolTopologies = { const protocolTopologies = {
clearnet: { clearnet: {
caption: "Clearnet requests resolve DNS, reach HTTPS ingress, and read AMPG clearnet output.", caption: "Clearnet splits into a DNS discovery path and an HTTPS request path before the web server reads AMPG output.",
nodes: [ nodes: [
["browser", "Visitor\nbrowser", 0.12, 0.44, "green"], ["client", "visitor\nbrowser", 0.11, 0.48, "green"],
["dns", "DNS", 0.31, 0.32, "amber"], ["resolver", "recursive\nDNS", 0.29, 0.25, "amber"],
["ingress", "HTTPS\ningress", 0.5, 0.44, "teal"], ["auth", "authoritative\nDNS", 0.5, 0.18, "amber"],
["server", "web\nserver", 0.69, 0.32, "coral"], ["ip", "public\naddress", 0.49, 0.42, "teal"],
["output", "AMPG\nclearnet", 0.88, 0.44, "teal"] ["tls", "TLS\nhandshake", 0.34, 0.66, "green"],
["ingress", "HTTPS\ningress", 0.57, 0.62, "teal"],
["proxy", "nginx /\nproxy", 0.72, 0.42, "coral"],
["output", "AMPG\nclearnet\nfiles", 0.86, 0.56, "teal"],
["health", "public\nhealth\ncheck", 0.82, 0.23, "green"]
], ],
links: [ links: [
["browser", "dns"], ["client", "resolver", -0.1],
["dns", "ingress"], ["resolver", "auth", -0.06],
["ingress", "server"], ["auth", "ip"],
["server", "output"] ["resolver", "client", 0.1],
["client", "tls", 0.08],
["tls", "ingress"],
["ingress", "proxy"],
["proxy", "output", 0.05],
["health", "ingress"],
["health", "output", 0.05]
] ]
}, },
tor: { tor: {
caption: "Tor clients use onion-service descriptors and circuits before the local hidden service reaches AMPG privacy HTML.", caption: "Tor onion traffic builds client and service circuits through relays; the rendezvous point joins them without revealing the origin.",
nodes: [ nodes: [
["browser", "Tor\nBrowser", 0.12, 0.44, "green"], ["browser", "Tor\nBrowser", 0.09, 0.5, "green"],
["descriptor", "onion\nservice\ndescriptor", 0.31, 0.32, "amber"], ["hsdir", "HSDir\nrelays", 0.28, 0.2, "amber"],
["circuits", "Tor\ncircuits", 0.5, 0.44, "coral"], ["descriptor", "onion\nservice\ndescriptor", 0.48, 0.2, "amber"],
["service", "onion\nservice", 0.69, 0.32, "teal"], ["guard", "client\nguard", 0.28, 0.48, "teal"],
["output", "AMPG\nprivacy\nHTML", 0.88, 0.44, "teal"] ["middle", "middle\nrelay", 0.46, 0.6, "coral"],
["rendezvous", "rendezvous\nrelay", 0.62, 0.46, "teal"],
["intro", "intro\npoints", 0.68, 0.24, "green"],
["service", "onion\nservice", 0.78, 0.52, "coral"],
["local", "local\nHTTP", 0.88, 0.36, "teal"],
["output", "AMPG\nprivacy\nHTML", 0.88, 0.69, "teal"]
], ],
links: [ links: [
["browser", "descriptor"], ["browser", "hsdir", -0.1],
["descriptor", "circuits"], ["hsdir", "descriptor"],
["circuits", "service"], ["descriptor", "intro"],
["browser", "guard"],
["guard", "middle", 0.05],
["middle", "rendezvous", 0.05],
["intro", "service", 0.08],
["service", "rendezvous", -0.1],
["service", "local"],
["service", "output"] ["service", "output"]
] ]
}, },
i2p: { i2p: {
caption: "I2P clients resolve a b32 destination, fetch routing data, and traverse tunnels to the server tunnel.", caption: "I2P uses netDb LeaseSets and one-way tunnels; client outbound tunnels meet server inbound tunnels inside the I2P network.",
nodes: [ nodes: [
["browser", "I2P\nbrowser", 0.12, 0.44, "green"], ["browser", "I2P\nbrowser", 0.09, 0.52, "green"],
["b32", "b32\ndestination", 0.31, 0.32, "amber"], ["address", "b32 /\naddressbook", 0.25, 0.26, "amber"],
["leaseset", "LeaseSet\nlookup", 0.5, 0.44, "coral"], ["floodfill", "floodfill\nnetDb", 0.46, 0.22, "amber"],
["tunnel", "I2P\nserver\ntunnel", 0.69, 0.32, "teal"], ["leaseset", "LeaseSet", 0.64, 0.24, "green"],
["output", "AMPG\nprivacy\nHTML", 0.88, 0.44, "teal"] ["outbound", "client\noutbound\ntunnel", 0.3, 0.6, "teal"],
["routers", "I2P\ntransit\nrouters", 0.5, 0.66, "coral"],
["inbound", "server\ninbound\ntunnel", 0.7, 0.6, "teal"],
["server", "i2pd\nserver\ntunnel", 0.84, 0.42, "coral"],
["output", "AMPG\nprivacy\nHTML", 0.9, 0.67, "teal"]
], ],
links: [ links: [
["browser", "b32"], ["browser", "address", -0.08],
["b32", "leaseset"], ["address", "floodfill"],
["leaseset", "tunnel"], ["floodfill", "leaseset"],
["tunnel", "output"] ["leaseset", "server", -0.06],
["browser", "outbound", 0.05],
["outbound", "routers", 0.04],
["routers", "inbound", 0.04],
["inbound", "server"],
["server", "output", 0.05]
] ]
}, },
gemini: { gemini: {
caption: "Gemini clients open a TLS capsule request and receive AMPG-rendered Gemtext.", caption: "Gemini is intentionally simple: resolve the capsule, open TLS on 1965, and serve text-first AMPG Gemtext.",
nodes: [ nodes: [
["client", "Gemini\nclient", 0.12, 0.44, "green"], ["client", "Gemini\nclient", 0.12, 0.5, "green"],
["address", "capsule\naddress", 0.31, 0.32, "amber"], ["name", "capsule\nname", 0.28, 0.24, "amber"],
["tls", "TLS\nrequest", 0.5, 0.44, "teal"], ["cert", "server\ncertificate", 0.48, 0.2, "green"],
["daemon", "Gemini\ndaemon", 0.69, 0.32, "coral"], ["tcp", "TCP\n1965", 0.42, 0.5, "teal"],
["output", "AMPG\nGemtext", 0.88, 0.44, "teal"] ["daemon", "Gemini\ndaemon", 0.62, 0.5, "coral"],
["gemtext", "Gemtext\ntree", 0.78, 0.3, "teal"],
["output", "AMPG\nGemtext", 0.86, 0.58, "green"],
["links", "capsule\nlinks", 0.62, 0.73, "amber"]
], ],
links: [ links: [
["client", "address"], ["client", "name", -0.07],
["address", "tls"], ["name", "tcp"],
["tls", "daemon"], ["client", "tcp"],
["daemon", "output"] ["cert", "tcp"],
["tcp", "daemon"],
["daemon", "gemtext", -0.08],
["daemon", "output"],
["output", "links", 0.07],
["links", "client", 0.16]
] ]
}, },
reticulum: { reticulum: {
caption: "Reticulum clients route to a destination hash; NomadNet-style nodes can serve compact AMPG Micron output.", caption: "Reticulum routes to destination identities across known paths and peers; it is transport-flexible, not automatically anonymous.",
nodes: [ nodes: [
["client", "RNS\nclient", 0.12, 0.44, "green"], ["client", "RNS\nclient", 0.1, 0.5, "green"],
["path", "path\ndiscovery", 0.31, 0.32, "amber"], ["cache", "path\ncache", 0.28, 0.24, "amber"],
["mesh", "Reticulum\nmesh", 0.5, 0.44, "coral"], ["request", "path\nrequest", 0.29, 0.61, "teal"],
["node", "NomadNet\nnode", 0.69, 0.32, "teal"], ["mesh", "Reticulum\nmesh\npeers", 0.49, 0.44, "coral"],
["output", "AMPG\nMicron", 0.88, 0.44, "teal"] ["transport", "transport\nnode", 0.62, 0.2, "green"],
["identity", "destination\nidentity", 0.72, 0.44, "amber"],
["nomad", "NomadNet\nnode", 0.84, 0.33, "teal"],
["output", "AMPG\nMicron", 0.88, 0.62, "green"]
], ],
links: [ links: [
["client", "path"], ["client", "cache", -0.08],
["path", "mesh"], ["client", "request", 0.08],
["mesh", "node"], ["request", "mesh"],
["node", "output"] ["cache", "mesh"],
["mesh", "transport", -0.06],
["mesh", "identity"],
["transport", "identity", 0.06],
["identity", "nomad"],
["nomad", "output"],
["output", "client", 0.16]
] ]
}, },
ipfs: { ipfs: {
caption: "IPFS-style publishing addresses content by CID or IPNS and distributes AMPG static output through peers or gateways.", caption: "IPFS-style publishing addresses AMPG output by content hash, then discovers providers and fetches blocks from peers or gateways.",
nodes: [ nodes: [
["browser", "browser\nor gateway", 0.12, 0.44, "green"], ["browser", "browser\nor gateway", 0.1, 0.5, "green"],
["name", "CID\nor IPNS", 0.31, 0.32, "amber"], ["name", "CID /\nIPNS", 0.28, 0.24, "amber"],
["routing", "DHT\nproviders", 0.5, 0.44, "coral"], ["dnslink", "DNSLink", 0.27, 0.65, "amber"],
["peers", "IPFS\npeers", 0.69, 0.32, "teal"], ["dht", "DHT\nprovider\nrecords", 0.49, 0.44, "coral"],
["output", "AMPG\nstatic\ntree", 0.88, 0.44, "teal"] ["routing", "peer\nrouting", 0.63, 0.22, "green"],
["bitswap", "Bitswap /\nGraphsync", 0.66, 0.58, "teal"],
["peers", "IPFS\npeer\ncluster", 0.8, 0.4, "coral"],
["blocks", "content\nblocks", 0.89, 0.23, "teal"],
["output", "AMPG\nstatic\ntree", 0.9, 0.65, "green"]
], ],
links: [ links: [
["browser", "name"], ["browser", "name", -0.08],
["name", "routing"], ["browser", "dnslink", 0.08],
["name", "dht"],
["dnslink", "dht"],
["dht", "routing", -0.05],
["dht", "bitswap", 0.05],
["routing", "peers"], ["routing", "peers"],
["peers", "output"] ["bitswap", "peers"],
["peers", "blocks", -0.05],
["peers", "output", 0.05],
["output", "browser", 0.16]
] ]
} }
}; };
@@ -216,9 +294,9 @@
function createTopologyMap(options) { function createTopologyMap(options) {
const graphSet = options.graphSet || topologies; const graphSet = options.graphSet || topologies;
const canvas = document.getElementById(options.canvasId); const canvas = document.getElementById(options.canvasId);
const caption = document.getElementById(options.captionId); const caption = options.captionId ? document.getElementById(options.captionId) : null;
const buttons = Array.from(document.querySelectorAll(options.buttonSelector)); const buttons = options.buttonSelector ? Array.from(document.querySelectorAll(options.buttonSelector)) : [];
if (!canvas || !caption || buttons.length === 0) return; if (!canvas) return;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
@@ -249,7 +327,7 @@
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
drawField(tick); drawField(tick);
topology.links.forEach((link, index) => { topology.links.forEach((link, index) => {
drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index); drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index, link[2] || 0);
}); });
topology.nodes.forEach((node, index) => drawNode(node, tick, index)); topology.nodes.forEach((node, index) => drawNode(node, tick, index));
requestAnimationFrame(draw); requestAnimationFrame(draw);
@@ -270,7 +348,7 @@
ctx.restore(); ctx.restore();
} }
function drawLink(from, to, tick, index) { function drawLink(from, to, tick, index, curveOffset) {
if (!from || !to) return; if (!from || !to) return;
const fromPosition = nodePosition(from); const fromPosition = nodePosition(from);
const toPosition = nodePosition(to); const toPosition = nodePosition(to);
@@ -278,17 +356,29 @@
const ay = fromPosition.y; const ay = fromPosition.y;
const bx = toPosition.x; const bx = toPosition.x;
const by = toPosition.y; const by = toPosition.y;
const curve = Number(curveOffset) || 0;
const cx = (ax + bx) / 2 - (by - ay) * curve;
const cy = (ay + by) / 2 + (bx - ax) * curve;
ctx.save(); ctx.save();
ctx.strokeStyle = palette.line; ctx.strokeStyle = palette.line;
ctx.lineWidth = options.lineWidth || 1; ctx.lineWidth = options.lineWidth || 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(ax, ay); ctx.moveTo(ax, ay);
ctx.lineTo(bx, by); if (curve) {
ctx.quadraticCurveTo(cx, cy, bx, by);
} else {
ctx.lineTo(bx, by);
}
ctx.stroke(); ctx.stroke();
const progress = (tick * 0.22 + index * 0.13) % 1; const progress = (tick * 0.22 + index * 0.13) % 1;
const px = ax + (bx - ax) * progress; const inverse = 1 - progress;
const py = ay + (by - ay) * progress; const px = curve
? inverse * inverse * ax + 2 * inverse * progress * cx + progress * progress * bx
: ax + (bx - ax) * progress;
const py = curve
? inverse * inverse * ay + 2 * inverse * progress * cy + progress * progress * by
: ay + (by - ay) * progress;
ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber; ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber;
ctx.beginPath(); ctx.beginPath();
ctx.arc(px, py, options.packetRadius || 3.5, 0, Math.PI * 2); ctx.arc(px, py, options.packetRadius || 3.5, 0, Math.PI * 2);
@@ -330,8 +420,9 @@
} }
function responsiveNodeWidth(baseWidth) { function responsiveNodeWidth(baseWidth) {
if (width < 720) return Math.max(86, Math.min(baseWidth, width * 0.24)); if (width < 520) return Math.max(104, Math.min(baseWidth, width * 0.38));
if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.17)); if (width < 720) return Math.max(104, Math.min(baseWidth, width * 0.28));
if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.18));
return baseWidth; return baseWidth;
} }
@@ -341,21 +432,21 @@
} }
const topology = graphSet[active]; const topology = graphSet[active];
const index = topology.nodes.findIndex((candidate) => candidate[0] === node[0]); const index = topology.nodes.findIndex((candidate) => candidate[0] === node[0]);
const wrapped = [ const columns = width < 520 ? 2 : 3;
[0.18, 0.28], const rows = Math.max(1, Math.ceil(topology.nodes.length / columns));
[0.5, 0.28], const col = index % columns;
[0.82, 0.28], const row = Math.floor(index / columns);
[0.34, 0.55], const x = (col + 0.5) / columns;
[0.66, 0.55], const y = rows === 1 ? 0.34 : 0.15 + (row / (rows - 1)) * 0.48;
[0.5, 0.76] return { x: x * width, y: y * height };
][index] || [node[2], node[3]];
return { x: wrapped[0] * width, y: wrapped[1] * height };
} }
function setTopology(name) { function setTopology(name) {
if (!graphSet[name]) return; if (!graphSet[name]) return;
active = name; active = name;
caption.textContent = graphSet[name].caption; if (caption) {
caption.textContent = graphSet[name].caption;
}
buttons.forEach((button) => { buttons.forEach((button) => {
const selected = button.dataset[options.dataKey] === name; const selected = button.dataset[options.dataKey] === name;
button.classList.toggle("active", selected); button.classList.toggle("active", selected);
@@ -367,18 +458,66 @@
button.addEventListener("click", () => setTopology(button.dataset[options.dataKey])); button.addEventListener("click", () => setTopology(button.dataset[options.dataKey]));
}); });
if (options.autoCycleMs) {
const names = Object.keys(graphSet);
window.setInterval(() => {
const currentIndex = names.indexOf(active);
setTopology(names[(currentIndex + 1) % names.length]);
}, options.autoCycleMs);
}
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
resize(); resize();
setTopology(active); setTopology(active);
requestAnimationFrame(draw); requestAnimationFrame(draw);
} }
function createMapModeTabs() {
const tabs = Array.from(document.querySelectorAll("[data-map-mode]"));
const title = document.getElementById("map-stage-title");
const eyebrow = document.getElementById("map-stage-eyebrow");
const description = document.getElementById("map-stage-description");
if (tabs.length === 0) return;
function setMode(mode) {
tabs.forEach((tab) => {
const selected = tab.dataset.mapMode === mode;
tab.classList.toggle("active", selected);
tab.setAttribute("aria-selected", selected ? "true" : "false");
if (selected) {
title.textContent = tab.dataset.mapTitle;
eyebrow.textContent = tab.dataset.mapEyebrow;
description.textContent = tab.dataset.mapDescription;
}
});
document.querySelectorAll(".map-canvas").forEach((canvas) => {
canvas.classList.toggle(
"active",
(mode === "deployment" && canvas.id === "cleanTopologyCanvas") ||
(mode === "protocol" && canvas.id === "protocolTopologyCanvas")
);
});
document.querySelectorAll("[data-map-controls]").forEach((panel) => {
const selected = panel.dataset.mapControls === mode;
panel.hidden = !selected;
panel.classList.toggle("active", selected);
});
window.dispatchEvent(new Event("resize"));
}
tabs.forEach((tab) => {
tab.addEventListener("click", () => setMode(tab.dataset.mapMode));
});
setMode("deployment");
}
createTopologyMap({ createTopologyMap({
canvasId: "topologyCanvas", canvasId: "topologyCanvas",
captionId: "topologyCaption", initial: "vps",
buttonSelector: "[data-topology]", autoCycleMs: 4600
dataKey: "topology",
initial: "vps"
}); });
createTopologyMap({ createTopologyMap({
@@ -395,7 +534,8 @@
nodeHeight: 54, nodeHeight: 54,
fontSize: 15, fontSize: 15,
fontWeight: 800, fontWeight: 800,
shadowBlur: 26 shadowBlur: 26,
wrapNodes: true
}); });
createTopologyMap({ createTopologyMap({
@@ -418,6 +558,8 @@
wrapNodes: true wrapNodes: true
}); });
createMapModeTabs();
if (signalWord) { if (signalWord) {
const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"]; const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
let wordIndex = 0; let wordIndex = 0;
+64 -40
View File
@@ -49,18 +49,6 @@
<a class="button secondary" href="#domains">See example domains</a> <a class="button secondary" href="#domains">See example domains</a>
</div> </div>
</div> </div>
<div class="topology-panel" aria-label="Topology modes">
<div class="mode-label">Topology</div>
<div class="segmented" role="group" aria-label="Topology selection">
<button class="mode-button active" type="button" data-topology="vps">VPS</button>
<button class="mode-button" type="button" data-topology="router">Router</button>
<button class="mode-button" type="button" data-topology="phone">Phone</button>
<button class="mode-button" type="button" data-topology="browser">Browser</button>
</div>
<p id="topologyCaption" class="topology-caption" aria-live="polite">
A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
</p>
</div>
</section> </section>
<section id="live-map" class="section live-map-section" aria-labelledby="live-map-title"> <section id="live-map" class="section live-map-section" aria-labelledby="live-map-title">
@@ -72,18 +60,57 @@
clear nodes, and transport-specific paths. clear nodes, and transport-specific paths.
</p> </p>
</div> </div>
<div class="map-stack"> <article class="map-stage" aria-labelledby="map-stage-title">
<article class="map-stage" aria-labelledby="deployment-map-title"> <div class="map-stage-heading">
<div class="map-stage-heading"> <div>
<div> <p id="map-stage-eyebrow" class="eyebrow">View</p>
<p class="eyebrow">View</p> <h3 id="map-stage-title">Deployment shape</h3>
<h3 id="deployment-map-title">Deployment shape</h3>
</div>
<p>VPS, router, phone, and browser validation views show where AMPG sits in the stack.</p>
</div> </div>
<div class="map-theater" aria-label="Interactive deployment topology map"> <p id="map-stage-description">
<canvas id="cleanTopologyCanvas" aria-hidden="true"></canvas> VPS, router, phone, and browser validation views show where AMPG sits in the stack.
<div class="map-theater-controls"> </p>
</div>
<div class="map-theater" aria-label="Interactive topology and protocol map">
<canvas id="cleanTopologyCanvas" class="map-canvas active" aria-hidden="true"></canvas>
<canvas id="protocolTopologyCanvas" class="map-canvas" aria-hidden="true"></canvas>
<div class="map-tabs" role="tablist" aria-label="Map mode">
<button
id="deployment-map-tab"
class="map-tab active"
type="button"
role="tab"
aria-selected="true"
aria-controls="deployment-map-controls"
data-map-mode="deployment"
data-map-title="Deployment shape"
data-map-eyebrow="View"
data-map-description="VPS, router, phone, and browser validation views show where AMPG sits in the stack."
>
Deployment shape
</button>
<button
id="protocol-map-tab"
class="map-tab"
type="button"
role="tab"
aria-selected="false"
aria-controls="protocol-map-controls"
data-map-mode="protocol"
data-map-title="Protocol flow"
data-map-eyebrow="Transport"
data-map-description="Transport maps show the real network shape: discovery, routing, service endpoints, and AMPG output."
>
Protocol flow
</button>
</div>
<div class="map-theater-controls">
<div
id="deployment-map-controls"
class="map-control-panel active"
role="tabpanel"
aria-labelledby="deployment-map-tab"
data-map-controls="deployment"
>
<div class="mode-label">View</div> <div class="mode-label">View</div>
<div class="segmented" role="group" aria-label="Deployment topology selection"> <div class="segmented" role="group" aria-label="Deployment topology selection">
<button class="mode-button active" type="button" data-clean-topology="vps">VPS</button> <button class="mode-button active" type="button" data-clean-topology="vps">VPS</button>
@@ -92,22 +119,18 @@
<button class="mode-button" type="button" data-clean-topology="browser">Browser</button> <button class="mode-button" type="button" data-clean-topology="browser">Browser</button>
</div> </div>
<p id="cleanTopologyCaption" class="topology-caption" aria-live="polite"> <p id="cleanTopologyCaption" class="topology-caption" aria-live="polite">
A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan. A VPS shape starts with one source tree, renders transport outputs, then hands them
to service-specific daemons on the host.
</p> </p>
</div> </div>
</div> <div
</article> id="protocol-map-controls"
<article class="map-stage" aria-labelledby="protocol-map-title"> class="map-control-panel"
<div class="map-stage-heading"> role="tabpanel"
<div> aria-labelledby="protocol-map-tab"
<p class="eyebrow">Transport</p> data-map-controls="protocol"
<h3 id="protocol-map-title">Protocol flow</h3> hidden
</div> >
<p>Each transport gets its own animated path instead of a static comparison table.</p>
</div>
<div class="map-theater" aria-label="Interactive protocol flow map">
<canvas id="protocolTopologyCanvas" aria-hidden="true"></canvas>
<div class="map-theater-controls protocol-controls">
<div class="mode-label">Transport</div> <div class="mode-label">Transport</div>
<div class="segmented protocol-segmented" role="group" aria-label="Protocol flow selection"> <div class="segmented protocol-segmented" role="group" aria-label="Protocol flow selection">
<button class="mode-button active" type="button" data-protocol-topology="clearnet">Clearnet</button> <button class="mode-button active" type="button" data-protocol-topology="clearnet">Clearnet</button>
@@ -118,12 +141,13 @@
<button class="mode-button" type="button" data-protocol-topology="ipfs">IPFS</button> <button class="mode-button" type="button" data-protocol-topology="ipfs">IPFS</button>
</div> </div>
<p id="protocolTopologyCaption" class="topology-caption" aria-live="polite"> <p id="protocolTopologyCaption" class="topology-caption" aria-live="polite">
Clearnet requests resolve DNS, reach HTTPS ingress, and read AMPG clearnet output. Clearnet splits into a DNS discovery path and an HTTPS request path before the web
server reads AMPG output.
</p> </p>
</div> </div>
</div> </div>
</article> </div>
</div> </article>
</section> </section>
<section id="topologies" class="section split" aria-labelledby="topologies-title"> <section id="topologies" class="section split" aria-labelledby="topologies-title">
+78 -37
View File
@@ -114,13 +114,27 @@ a {
#topologyCanvas, #topologyCanvas,
#cleanTopologyCanvas, #cleanTopologyCanvas,
#protocolTopologyCanvas { #protocolTopologyCanvas,
.map-canvas {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#topologyCanvas {
opacity: 0.64;
}
.map-canvas {
opacity: 0;
transition: opacity 260ms ease;
}
.map-canvas.active {
opacity: 1;
}
.hero::after { .hero::after {
content: ""; content: "";
position: absolute; position: absolute;
@@ -134,7 +148,7 @@ a {
z-index: 1; z-index: 1;
max-width: var(--max); max-width: var(--max);
margin: 0 auto; margin: 0 auto;
padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 160px; padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 130px;
} }
.eyebrow, .eyebrow,
@@ -241,19 +255,6 @@ h3 {
background: rgba(246, 241, 228, 0.06); background: rgba(246, 241, 228, 0.06);
} }
.topology-panel {
position: absolute;
right: clamp(18px, 4vw, 48px);
bottom: 30px;
z-index: 2;
width: min(440px, calc(100% - 36px));
padding: 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(27, 28, 26, 0.82);
backdrop-filter: blur(18px);
}
.segmented { .segmented {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -290,6 +291,7 @@ h3 {
width: min(var(--max), calc(100% - 36px)); width: min(var(--max), calc(100% - 36px));
margin: 0 auto; margin: 0 auto;
padding: clamp(64px, 10vw, 112px) 0; padding: clamp(64px, 10vw, 112px) 0;
scroll-margin-top: 82px;
} }
.split { .split {
@@ -333,12 +335,7 @@ h3 {
} }
.live-map-section { .live-map-section {
width: min(1440px, calc(100% - 36px)); width: min(1520px, calc(100% - 36px));
}
.map-stack {
display: grid;
gap: clamp(28px, 5vw, 58px);
} }
.map-stage { .map-stage {
@@ -366,7 +363,7 @@ h3 {
.map-theater { .map-theater {
position: relative; position: relative;
min-height: clamp(540px, 72vw, 820px); min-height: clamp(640px, 74vw, 880px);
overflow: hidden; overflow: hidden;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
@@ -383,16 +380,47 @@ h3 {
radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%); radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%);
} }
.map-tabs {
position: absolute;
top: 18px;
left: 18px;
z-index: 2;
display: inline-grid;
grid-template-columns: repeat(2, minmax(160px, 1fr));
gap: 8px;
padding: 8px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(16, 16, 15, 0.72);
backdrop-filter: blur(18px);
}
.map-tab {
min-height: 42px;
padding: 8px 12px;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
color: var(--muted);
font: inherit;
font-size: 0.92rem;
font-weight: 900;
cursor: pointer;
}
.map-tab.active,
.map-tab:focus-visible {
border-color: var(--teal);
background: rgba(57, 212, 197, 0.12);
color: var(--text);
}
.map-theater-controls { .map-theater-controls {
position: absolute; position: absolute;
left: 18px; left: 18px;
right: 18px; right: 18px;
bottom: 18px; bottom: 18px;
z-index: 2; z-index: 2;
display: grid;
grid-template-columns: 84px minmax(0, 520px) minmax(260px, 1fr);
gap: 12px;
align-items: center;
padding: 14px; padding: 14px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
@@ -400,8 +428,19 @@ h3 {
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
} }
.map-theater-controls .mode-label, .map-control-panel {
.map-theater-controls .topology-caption { display: grid;
grid-template-columns: 84px minmax(0, 650px) minmax(280px, 1fr);
gap: 12px;
align-items: center;
}
.map-control-panel[hidden] {
display: none;
}
.map-control-panel .mode-label,
.map-control-panel .topology-caption {
margin: 0; margin: 0;
} }
@@ -517,7 +556,7 @@ h3 {
} }
.hero { .hero {
min-height: 860px; min-height: 780px;
} }
.hero::after { .hero::after {
@@ -525,7 +564,7 @@ h3 {
} }
.hero-inner { .hero-inner {
padding-bottom: 280px; padding-bottom: 150px;
} }
.split, .split,
@@ -539,12 +578,12 @@ h3 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.map-theater-controls { .map-control-panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.map-theater { .map-theater {
min-height: 680px; min-height: 740px;
} }
.flow-list { .flow-list {
@@ -572,17 +611,19 @@ h3 {
justify-content: center; justify-content: center;
} }
.topology-panel {
bottom: 18px;
}
.segmented, .segmented,
.flow-list { .flow-list {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.map-theater { .map-theater {
min-height: 760px; min-height: 820px;
}
.map-tabs {
left: 12px;
right: 12px;
grid-template-columns: 1fr;
} }
.map-theater-controls { .map-theater-controls {