From 4289feb1e54ff88f4d1e8267b9f9e22d10537404 Mon Sep 17 00:00:00 2001
From: jwinterm
Date: Tue, 7 Jul 2026 14:32:58 -0400
Subject: [PATCH] Refine AMPGateway topology theater
---
sites/ampgateway-online/app.js | 414 +++++++++++++++++++----------
sites/ampgateway-online/index.html | 104 +++++---
sites/ampgateway-online/styles.css | 115 +++++---
3 files changed, 420 insertions(+), 213 deletions(-)
diff --git a/sites/ampgateway-online/app.js b/sites/ampgateway-online/app.js
index b981783..9fdbcfe 100644
--- a/sites/ampgateway-online/app.js
+++ b/sites/ampgateway-online/app.js
@@ -13,186 +13,264 @@
const topologies = {
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: [
- ["source", "source site", 0.16, 0.42, "amber"],
- ["ampg", "AMPG", 0.34, 0.42, "teal"],
- ["vps", "VPS", 0.52, 0.42, "green"],
- ["web", "clearnet", 0.72, 0.22, "teal"],
- ["tor", "Tor", 0.78, 0.39, "coral"],
- ["i2p", "I2P", 0.76, 0.56, "amber"],
- ["gemini", "Gemini", 0.66, 0.72, "green"],
- ["reticulum", "Reticulum", 0.84, 0.72, "coral"]
+ ["source", "source\nsite", 0.11, 0.48, "amber"],
+ ["build", "AMPG\nbuild", 0.27, 0.48, "teal"],
+ ["outputs", "rendered\noutputs", 0.42, 0.3, "green"],
+ ["state", "deploy\nstate", 0.42, 0.62, "amber"],
+ ["vps", "VPS\nhost", 0.56, 0.46, "green"],
+ ["web", "HTTPS\nnginx", 0.73, 0.23, "teal"],
+ ["tor", "Tor\nonion", 0.76, 0.42, "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: [
- ["source", "ampg"],
- ["ampg", "vps"],
- ["vps", "web"],
+ ["source", "build"],
+ ["build", "outputs", -0.08],
+ ["build", "state", 0.08],
+ ["outputs", "vps"],
+ ["state", "vps"],
+ ["vps", "web", -0.08],
["vps", "tor"],
- ["vps", "i2p"],
- ["vps", "gemini"],
- ["vps", "reticulum"]
+ ["vps", "i2p", 0.04],
+ ["vps", "gemini", 0.1],
+ ["vps", "reticulum", 0.12]
]
},
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: [
- ["visitor", "visitor", 0.13, 0.33, "green"],
- ["dns", "DNS/DDNS", 0.3, 0.24, "amber"],
- ["router", "router", 0.45, 0.38, "coral"],
- ["host", "home host", 0.61, 0.38, "teal"],
- ["ampg", "AMPG", 0.76, 0.38, "teal"],
- ["tor", "Tor", 0.64, 0.66, "coral"],
- ["i2p", "I2P", 0.78, 0.66, "amber"],
- ["reticulum", "Reticulum", 0.9, 0.54, "green"]
+ ["visitor", "visitor", 0.1, 0.42, "green"],
+ ["dns", "DNS /\nDDNS", 0.25, 0.24, "amber"],
+ ["router", "router\nNAT", 0.4, 0.42, "coral"],
+ ["host", "home\nhost", 0.55, 0.42, "teal"],
+ ["ampg", "AMPG", 0.7, 0.42, "teal"],
+ ["clearnet", "port\nforward", 0.55, 0.22, "green"],
+ ["onion", "Tor\nonion", 0.68, 0.69, "coral"],
+ ["i2p", "I2P\nserver\ntunnel", 0.84, 0.61, "amber"],
+ ["rns", "RNS\npeer", 0.88, 0.3, "green"]
],
links: [
["visitor", "dns"],
["dns", "router"],
["router", "host"],
["host", "ampg"],
- ["ampg", "tor"],
- ["ampg", "i2p"],
- ["ampg", "reticulum"],
- ["visitor", "tor"],
- ["visitor", "i2p"],
- ["visitor", "reticulum"]
+ ["ampg", "clearnet", -0.04],
+ ["clearnet", "router"],
+ ["ampg", "onion", 0.05],
+ ["ampg", "i2p", 0.04],
+ ["ampg", "rns", -0.05],
+ ["visitor", "onion", 0.16],
+ ["visitor", "i2p", 0.12],
+ ["visitor", "rns", -0.14]
]
},
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: [
- ["phone", "Android", 0.24, 0.45, "green"],
- ["pkg", "pkg", 0.43, 0.26, "amber"],
- ["ampg", "AMPG", 0.45, 0.45, "teal"],
- ["state", "state", 0.43, 0.64, "coral"],
- ["services", "services", 0.64, 0.45, "green"],
- ["routes", "routes", 0.82, 0.45, "teal"]
+ ["phone", "Android\nphone", 0.14, 0.46, "green"],
+ ["termux", "Termux\npackages", 0.31, 0.28, "amber"],
+ ["state", "AMPG\nstate", 0.31, 0.64, "coral"],
+ ["ampg", "AMPG\nbuild", 0.48, 0.46, "teal"],
+ ["daemons", "user-space\ndaemons", 0.65, 0.46, "green"],
+ ["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: [
- ["phone", "pkg"],
- ["phone", "ampg"],
+ ["phone", "termux", -0.05],
["phone", "state"],
- ["pkg", "services"],
- ["ampg", "services"],
- ["state", "services"],
- ["services", "routes"]
+ ["phone", "power", 0.08],
+ ["termux", "ampg"],
+ ["state", "ampg"],
+ ["ampg", "daemons"],
+ ["daemons", "network", -0.05],
+ ["daemons", "routes", 0.05],
+ ["power", "routes", -0.1]
]
},
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: [
- ["site", "site", 0.17, 0.5, "amber"],
- ["ampg", "AMPG", 0.33, 0.5, "teal"],
- ["manifest", "manifest", 0.5, 0.5, "green"],
- ["ampb", "AMPB", 0.66, 0.5, "coral"],
- ["transport", "transport", 0.82, 0.5, "teal"]
+ ["manifest", "fixture\nmanifest", 0.12, 0.43, "amber"],
+ ["ampb", "AMPB", 0.29, 0.43, "teal"],
+ ["policy", "route\npolicy", 0.45, 0.25, "green"],
+ ["adapters", "transport\nadapters", 0.5, 0.53, "coral"],
+ ["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: [
- ["site", "ampg"],
- ["ampg", "manifest"],
["manifest", "ampb"],
- ["ampb", "transport"],
- ["transport", "ampb"]
+ ["ampb", "policy", -0.08],
+ ["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 = {
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: [
- ["browser", "Visitor\nbrowser", 0.12, 0.44, "green"],
- ["dns", "DNS", 0.31, 0.32, "amber"],
- ["ingress", "HTTPS\ningress", 0.5, 0.44, "teal"],
- ["server", "web\nserver", 0.69, 0.32, "coral"],
- ["output", "AMPG\nclearnet", 0.88, 0.44, "teal"]
+ ["client", "visitor\nbrowser", 0.11, 0.48, "green"],
+ ["resolver", "recursive\nDNS", 0.29, 0.25, "amber"],
+ ["auth", "authoritative\nDNS", 0.5, 0.18, "amber"],
+ ["ip", "public\naddress", 0.49, 0.42, "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: [
- ["browser", "dns"],
- ["dns", "ingress"],
- ["ingress", "server"],
- ["server", "output"]
+ ["client", "resolver", -0.1],
+ ["resolver", "auth", -0.06],
+ ["auth", "ip"],
+ ["resolver", "client", 0.1],
+ ["client", "tls", 0.08],
+ ["tls", "ingress"],
+ ["ingress", "proxy"],
+ ["proxy", "output", 0.05],
+ ["health", "ingress"],
+ ["health", "output", 0.05]
]
},
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: [
- ["browser", "Tor\nBrowser", 0.12, 0.44, "green"],
- ["descriptor", "onion\nservice\ndescriptor", 0.31, 0.32, "amber"],
- ["circuits", "Tor\ncircuits", 0.5, 0.44, "coral"],
- ["service", "onion\nservice", 0.69, 0.32, "teal"],
- ["output", "AMPG\nprivacy\nHTML", 0.88, 0.44, "teal"]
+ ["browser", "Tor\nBrowser", 0.09, 0.5, "green"],
+ ["hsdir", "HSDir\nrelays", 0.28, 0.2, "amber"],
+ ["descriptor", "onion\nservice\ndescriptor", 0.48, 0.2, "amber"],
+ ["guard", "client\nguard", 0.28, 0.48, "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: [
- ["browser", "descriptor"],
- ["descriptor", "circuits"],
- ["circuits", "service"],
+ ["browser", "hsdir", -0.1],
+ ["hsdir", "descriptor"],
+ ["descriptor", "intro"],
+ ["browser", "guard"],
+ ["guard", "middle", 0.05],
+ ["middle", "rendezvous", 0.05],
+ ["intro", "service", 0.08],
+ ["service", "rendezvous", -0.1],
+ ["service", "local"],
["service", "output"]
]
},
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: [
- ["browser", "I2P\nbrowser", 0.12, 0.44, "green"],
- ["b32", "b32\ndestination", 0.31, 0.32, "amber"],
- ["leaseset", "LeaseSet\nlookup", 0.5, 0.44, "coral"],
- ["tunnel", "I2P\nserver\ntunnel", 0.69, 0.32, "teal"],
- ["output", "AMPG\nprivacy\nHTML", 0.88, 0.44, "teal"]
+ ["browser", "I2P\nbrowser", 0.09, 0.52, "green"],
+ ["address", "b32 /\naddressbook", 0.25, 0.26, "amber"],
+ ["floodfill", "floodfill\nnetDb", 0.46, 0.22, "amber"],
+ ["leaseset", "LeaseSet", 0.64, 0.24, "green"],
+ ["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: [
- ["browser", "b32"],
- ["b32", "leaseset"],
- ["leaseset", "tunnel"],
- ["tunnel", "output"]
+ ["browser", "address", -0.08],
+ ["address", "floodfill"],
+ ["floodfill", "leaseset"],
+ ["leaseset", "server", -0.06],
+ ["browser", "outbound", 0.05],
+ ["outbound", "routers", 0.04],
+ ["routers", "inbound", 0.04],
+ ["inbound", "server"],
+ ["server", "output", 0.05]
]
},
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: [
- ["client", "Gemini\nclient", 0.12, 0.44, "green"],
- ["address", "capsule\naddress", 0.31, 0.32, "amber"],
- ["tls", "TLS\nrequest", 0.5, 0.44, "teal"],
- ["daemon", "Gemini\ndaemon", 0.69, 0.32, "coral"],
- ["output", "AMPG\nGemtext", 0.88, 0.44, "teal"]
+ ["client", "Gemini\nclient", 0.12, 0.5, "green"],
+ ["name", "capsule\nname", 0.28, 0.24, "amber"],
+ ["cert", "server\ncertificate", 0.48, 0.2, "green"],
+ ["tcp", "TCP\n1965", 0.42, 0.5, "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: [
- ["client", "address"],
- ["address", "tls"],
- ["tls", "daemon"],
- ["daemon", "output"]
+ ["client", "name", -0.07],
+ ["name", "tcp"],
+ ["client", "tcp"],
+ ["cert", "tcp"],
+ ["tcp", "daemon"],
+ ["daemon", "gemtext", -0.08],
+ ["daemon", "output"],
+ ["output", "links", 0.07],
+ ["links", "client", 0.16]
]
},
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: [
- ["client", "RNS\nclient", 0.12, 0.44, "green"],
- ["path", "path\ndiscovery", 0.31, 0.32, "amber"],
- ["mesh", "Reticulum\nmesh", 0.5, 0.44, "coral"],
- ["node", "NomadNet\nnode", 0.69, 0.32, "teal"],
- ["output", "AMPG\nMicron", 0.88, 0.44, "teal"]
+ ["client", "RNS\nclient", 0.1, 0.5, "green"],
+ ["cache", "path\ncache", 0.28, 0.24, "amber"],
+ ["request", "path\nrequest", 0.29, 0.61, "teal"],
+ ["mesh", "Reticulum\nmesh\npeers", 0.49, 0.44, "coral"],
+ ["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: [
- ["client", "path"],
- ["path", "mesh"],
- ["mesh", "node"],
- ["node", "output"]
+ ["client", "cache", -0.08],
+ ["client", "request", 0.08],
+ ["request", "mesh"],
+ ["cache", "mesh"],
+ ["mesh", "transport", -0.06],
+ ["mesh", "identity"],
+ ["transport", "identity", 0.06],
+ ["identity", "nomad"],
+ ["nomad", "output"],
+ ["output", "client", 0.16]
]
},
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: [
- ["browser", "browser\nor gateway", 0.12, 0.44, "green"],
- ["name", "CID\nor IPNS", 0.31, 0.32, "amber"],
- ["routing", "DHT\nproviders", 0.5, 0.44, "coral"],
- ["peers", "IPFS\npeers", 0.69, 0.32, "teal"],
- ["output", "AMPG\nstatic\ntree", 0.88, 0.44, "teal"]
+ ["browser", "browser\nor gateway", 0.1, 0.5, "green"],
+ ["name", "CID /\nIPNS", 0.28, 0.24, "amber"],
+ ["dnslink", "DNSLink", 0.27, 0.65, "amber"],
+ ["dht", "DHT\nprovider\nrecords", 0.49, 0.44, "coral"],
+ ["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: [
- ["browser", "name"],
- ["name", "routing"],
+ ["browser", "name", -0.08],
+ ["browser", "dnslink", 0.08],
+ ["name", "dht"],
+ ["dnslink", "dht"],
+ ["dht", "routing", -0.05],
+ ["dht", "bitswap", 0.05],
["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) {
const graphSet = options.graphSet || topologies;
const canvas = document.getElementById(options.canvasId);
- const caption = document.getElementById(options.captionId);
- const buttons = Array.from(document.querySelectorAll(options.buttonSelector));
- if (!canvas || !caption || buttons.length === 0) return;
+ const caption = options.captionId ? document.getElementById(options.captionId) : null;
+ const buttons = options.buttonSelector ? Array.from(document.querySelectorAll(options.buttonSelector)) : [];
+ if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
@@ -249,7 +327,7 @@
ctx.clearRect(0, 0, width, height);
drawField(tick);
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));
requestAnimationFrame(draw);
@@ -270,7 +348,7 @@
ctx.restore();
}
- function drawLink(from, to, tick, index) {
+ function drawLink(from, to, tick, index, curveOffset) {
if (!from || !to) return;
const fromPosition = nodePosition(from);
const toPosition = nodePosition(to);
@@ -278,17 +356,29 @@
const ay = fromPosition.y;
const bx = toPosition.x;
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.strokeStyle = palette.line;
ctx.lineWidth = options.lineWidth || 1;
ctx.beginPath();
ctx.moveTo(ax, ay);
- ctx.lineTo(bx, by);
+ if (curve) {
+ ctx.quadraticCurveTo(cx, cy, bx, by);
+ } else {
+ ctx.lineTo(bx, by);
+ }
ctx.stroke();
const progress = (tick * 0.22 + index * 0.13) % 1;
- const px = ax + (bx - ax) * progress;
- const py = ay + (by - ay) * progress;
+ const inverse = 1 - 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.beginPath();
ctx.arc(px, py, options.packetRadius || 3.5, 0, Math.PI * 2);
@@ -330,8 +420,9 @@
}
function responsiveNodeWidth(baseWidth) {
- if (width < 720) return Math.max(86, Math.min(baseWidth, width * 0.24));
- if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.17));
+ if (width < 520) return Math.max(104, Math.min(baseWidth, width * 0.38));
+ 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;
}
@@ -341,21 +432,21 @@
}
const topology = graphSet[active];
const index = topology.nodes.findIndex((candidate) => candidate[0] === node[0]);
- const wrapped = [
- [0.18, 0.28],
- [0.5, 0.28],
- [0.82, 0.28],
- [0.34, 0.55],
- [0.66, 0.55],
- [0.5, 0.76]
- ][index] || [node[2], node[3]];
- return { x: wrapped[0] * width, y: wrapped[1] * height };
+ const columns = width < 520 ? 2 : 3;
+ const rows = Math.max(1, Math.ceil(topology.nodes.length / columns));
+ const col = index % columns;
+ const row = Math.floor(index / columns);
+ const x = (col + 0.5) / columns;
+ const y = rows === 1 ? 0.34 : 0.15 + (row / (rows - 1)) * 0.48;
+ return { x: x * width, y: y * height };
}
function setTopology(name) {
if (!graphSet[name]) return;
active = name;
- caption.textContent = graphSet[name].caption;
+ if (caption) {
+ caption.textContent = graphSet[name].caption;
+ }
buttons.forEach((button) => {
const selected = button.dataset[options.dataKey] === name;
button.classList.toggle("active", selected);
@@ -367,18 +458,66 @@
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);
resize();
setTopology(active);
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({
canvasId: "topologyCanvas",
- captionId: "topologyCaption",
- buttonSelector: "[data-topology]",
- dataKey: "topology",
- initial: "vps"
+ initial: "vps",
+ autoCycleMs: 4600
});
createTopologyMap({
@@ -395,7 +534,8 @@
nodeHeight: 54,
fontSize: 15,
fontWeight: 800,
- shadowBlur: 26
+ shadowBlur: 26,
+ wrapNodes: true
});
createTopologyMap({
@@ -418,6 +558,8 @@
wrapNodes: true
});
+ createMapModeTabs();
+
if (signalWord) {
const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
let wordIndex = 0;
diff --git a/sites/ampgateway-online/index.html b/sites/ampgateway-online/index.html
index 2853a8a..b9c836c 100644
--- a/sites/ampgateway-online/index.html
+++ b/sites/ampgateway-online/index.html
@@ -49,18 +49,6 @@
See example domains
-
-
Topology
-
- VPS
- Router
- Phone
- Browser
-
-
- A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
-
-
@@ -72,18 +60,57 @@
clear nodes, and transport-specific paths.
-
-
-
-
-
View
-
Deployment shape
-
-
VPS, router, phone, and browser validation views show where AMPG sits in the stack.
+
+
+
+
View
+
Deployment shape
-
-
-
+
+ VPS, router, phone, and browser validation views show where AMPG sits in the stack.
+
+
+
+
+
+
+
+ Deployment shape
+
+
+ Protocol flow
+
+
+
+
View
VPS
@@ -92,22 +119,18 @@
Browser
- 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.
-
-
-
-
-
-
Transport
-
Protocol flow
-
-
Each transport gets its own animated path instead of a static comparison table.
-
-
-
-
+
Transport
Clearnet
@@ -118,12 +141,13 @@
IPFS
- 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.
-
-
+
+
diff --git a/sites/ampgateway-online/styles.css b/sites/ampgateway-online/styles.css
index f642940..f56708e 100644
--- a/sites/ampgateway-online/styles.css
+++ b/sites/ampgateway-online/styles.css
@@ -114,13 +114,27 @@ a {
#topologyCanvas,
#cleanTopologyCanvas,
-#protocolTopologyCanvas {
+#protocolTopologyCanvas,
+.map-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
+#topologyCanvas {
+ opacity: 0.64;
+}
+
+.map-canvas {
+ opacity: 0;
+ transition: opacity 260ms ease;
+}
+
+.map-canvas.active {
+ opacity: 1;
+}
+
.hero::after {
content: "";
position: absolute;
@@ -134,7 +148,7 @@ a {
z-index: 1;
max-width: var(--max);
margin: 0 auto;
- padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 160px;
+ padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 130px;
}
.eyebrow,
@@ -241,19 +255,6 @@ h3 {
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 {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -290,6 +291,7 @@ h3 {
width: min(var(--max), calc(100% - 36px));
margin: 0 auto;
padding: clamp(64px, 10vw, 112px) 0;
+ scroll-margin-top: 82px;
}
.split {
@@ -333,12 +335,7 @@ h3 {
}
.live-map-section {
- width: min(1440px, calc(100% - 36px));
-}
-
-.map-stack {
- display: grid;
- gap: clamp(28px, 5vw, 58px);
+ width: min(1520px, calc(100% - 36px));
}
.map-stage {
@@ -366,7 +363,7 @@ h3 {
.map-theater {
position: relative;
- min-height: clamp(540px, 72vw, 820px);
+ min-height: clamp(640px, 74vw, 880px);
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
@@ -383,16 +380,47 @@ h3 {
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 {
position: absolute;
left: 18px;
right: 18px;
bottom: 18px;
z-index: 2;
- display: grid;
- grid-template-columns: 84px minmax(0, 520px) minmax(260px, 1fr);
- gap: 12px;
- align-items: center;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
@@ -400,8 +428,19 @@ h3 {
backdrop-filter: blur(18px);
}
-.map-theater-controls .mode-label,
-.map-theater-controls .topology-caption {
+.map-control-panel {
+ 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;
}
@@ -517,7 +556,7 @@ h3 {
}
.hero {
- min-height: 860px;
+ min-height: 780px;
}
.hero::after {
@@ -525,7 +564,7 @@ h3 {
}
.hero-inner {
- padding-bottom: 280px;
+ padding-bottom: 150px;
}
.split,
@@ -539,12 +578,12 @@ h3 {
grid-template-columns: 1fr;
}
- .map-theater-controls {
+ .map-control-panel {
grid-template-columns: 1fr;
}
.map-theater {
- min-height: 680px;
+ min-height: 740px;
}
.flow-list {
@@ -572,17 +611,19 @@ h3 {
justify-content: center;
}
- .topology-panel {
- bottom: 18px;
- }
-
.segmented,
.flow-list {
grid-template-columns: 1fr;
}
.map-theater {
- min-height: 760px;
+ min-height: 820px;
+ }
+
+ .map-tabs {
+ left: 12px;
+ right: 12px;
+ grid-template-columns: 1fr;
}
.map-theater-controls {