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

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

+
+
+ + +
+ + +
+
+
View
@@ -92,22 +119,18 @@

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

-
-
- -
+
-
-
+
+
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 {