Full VPS
+
+ ampgateway.site is the target for a full public example with
+ clearnet, Tor, I2P, and Gemini routes managed from one deploy plan.
+
diff --git a/README.md b/README.md index 4bb8eb2..bccf8a3 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ API remains internal even when the public site is available on Tor or I2P. - [Interactive applications](docs/interactive-apps.md) - [Wownero integration](docs/wownero-integration.md) - [Autodocs and drift gates](docs/autodocs.md) +- [AMPGateway visual site source](sites/ampgateway-online/index.html) +- [AMPGateway visual site config](examples/ampgateway-online.gateway.toml) ## Checks diff --git a/examples/ampgateway-online.gateway.toml b/examples/ampgateway-online.gateway.toml new file mode 100644 index 0000000..cac04c9 --- /dev/null +++ b/examples/ampgateway-online.gateway.toml @@ -0,0 +1,76 @@ +[gateway] +state_dir = "./.ampg/ampgateway-online/state" +cache_dir = "./.ampg/ampgateway-online/cache" +run_dir = "./.ampg/ampgateway-online/run" +user = "ampg" + +[profiles.visual-vps] +description = "Full VPS deployment for the AMPGateway visual docs site." +protocols = ["clearnet", "tor", "i2p", "gemini"] +platform = "linux-systemd" +write_artifacts = true + +[profiles.visual-lan] +description = "Clearnet-only LAN deployment for the AMPGateway visual docs site." +protocols = ["clearnet"] +platform = "linux-user" + +[[site]] +id = "ampgateway-online" +domain = "ampgateway.online" + +[site.source] +kind = "static-html" +path = "../sites/ampgateway-online" +canonical_url = "https://ampgateway.online" + +[site.outputs] +root = "../dist/ampgateway-online" +plan_root = "../dist/ampg-plan" + +[site.interactions] +default_tier = "static" +deny_routes = ["/admin/*", "/internal/*", "/webhooks/*"] + +[site.protocols.clearnet] +enabled = true +renderer = "clearnet" +daemon = "nginx" +daemon_policy = "external" + +[site.protocols.tor] +enabled = true +renderer = "privacy-html" +daemon = "tor" +daemon_policy = "auto" +local_port = 18180 +hidden_service_dir = "/var/lib/ampg/tor/ampgateway-online" +onion_location = "auto" +max_asset_bytes = 1048576 +script_policy = "strip" + +[site.protocols.i2p] +enabled = true +renderer = "privacy-html" +daemon = "i2pd" +daemon_policy = "auto" +local_port = 18181 +keys_file = "ampgateway-online-web.dat" +tunnel_name = "ampgateway-online-web" +max_asset_bytes = 1048576 +script_policy = "strip" + +[site.protocols.gemini] +enabled = true +renderer = "gemtext" +daemon = "agate" +daemon_policy = "auto" +port = 1965 +max_asset_bytes = 1048576 + +[site.protocols.reticulum] +enabled = false +renderer = "micron" +daemon = "rnsd" +daemon_policy = "auto" +aspect = "web" diff --git a/sites/ampgateway-online/app.js b/sites/ampgateway-online/app.js new file mode 100644 index 0000000..23ecc06 --- /dev/null +++ b/sites/ampgateway-online/app.js @@ -0,0 +1,248 @@ +(function () { + const canvas = document.getElementById("topologyCanvas"); + const caption = document.getElementById("topologyCaption"); + const buttons = Array.from(document.querySelectorAll("[data-topology]")); + const signalWord = document.getElementById("signalWord"); + + if (!canvas || !caption || buttons.length === 0) { + return; + } + + const ctx = canvas.getContext("2d"); + if (!ctx) { + return; + } + + const palette = { + text: "#f6f1e4", + muted: "rgba(246, 241, 228, 0.52)", + line: "rgba(246, 241, 228, 0.18)", + teal: "#39d4c5", + green: "#8fe388", + amber: "#f0b84a", + coral: "#f06f5f" + }; + + const topologies = { + vps: { + caption: "A VPS can publish clearnet, Tor, I2P, and Gemini outputs from one AMPG plan.", + 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.68, 0.72, "green"] + ], + links: [ + ["source", "ampg"], + ["ampg", "vps"], + ["vps", "web"], + ["vps", "tor"], + ["vps", "i2p"], + ["vps", "gemini"] + ] + }, + router: { + caption: "Behind a router, clearnet may need DNS and port choices; Tor and I2P can avoid inbound port forwarding.", + 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.82, 0.66, "amber"] + ], + links: [ + ["visitor", "dns"], + ["dns", "router"], + ["router", "host"], + ["host", "ampg"], + ["ampg", "tor"], + ["ampg", "i2p"], + ["visitor", "tor"], + ["visitor", "i2p"] + ] + }, + phone: { + caption: "A spare Android phone can run Termux packages, AMPG state, and user-space transport services.", + 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"] + ], + links: [ + ["phone", "pkg"], + ["phone", "ampg"], + ["phone", "state"], + ["pkg", "services"], + ["ampg", "services"], + ["state", "services"], + ["services", "routes"] + ] + }, + browser: { + caption: "Fixture manifests let AMPB or another checker verify every route through the expected 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"] + ], + links: [ + ["site", "ampg"], + ["ampg", "manifest"], + ["manifest", "ampb"], + ["ampb", "transport"], + ["transport", "ampb"] + ] + } + }; + + const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"]; + let wordIndex = 0; + let active = "vps"; + let width = 0; + let height = 0; + let start = performance.now(); + + function resize() { + const rect = canvas.getBoundingClientRect(); + const ratio = Math.min(window.devicePixelRatio || 1, 2); + width = Math.max(1, rect.width); + height = Math.max(1, rect.height); + canvas.width = Math.floor(width * ratio); + canvas.height = Math.floor(height * ratio); + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + } + + function nodeMap(topology) { + return new Map(topology.nodes.map((node) => [node[0], node])); + } + + function draw(now) { + const topology = topologies[active]; + const nodes = nodeMap(topology); + const tick = (now - start) / 1000; + ctx.clearRect(0, 0, width, height); + drawField(tick); + topology.links.forEach((link, index) => drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index)); + topology.nodes.forEach((node, index) => drawNode(node, tick, index)); + requestAnimationFrame(draw); + } + + function drawField(tick) { + ctx.save(); + ctx.globalAlpha = 0.38; + for (let i = 0; i < 36; i += 1) { + const x = ((i * 137 + tick * 16) % (width + 120)) - 60; + const y = ((i * 71 + tick * 9) % (height + 120)) - 60; + ctx.fillStyle = i % 3 === 0 ? palette.teal : i % 3 === 1 ? palette.amber : palette.green; + ctx.beginPath(); + ctx.arc(x, y, i % 2 === 0 ? 1.6 : 1, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } + + function drawLink(from, to, tick, index) { + if (!from || !to) return; + const ax = from[2] * width; + const ay = from[3] * height; + const bx = to[2] * width; + const by = to[3] * height; + ctx.save(); + ctx.strokeStyle = palette.line; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(ax, ay); + 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; + ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber; + ctx.beginPath(); + ctx.arc(px, py, 3.5, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + + function drawNode(node, tick, index) { + const [, label, nx, ny, colorKey] = node; + const x = nx * width; + const y = ny * height; + const pulse = Math.sin(tick * 2.2 + index) * 2; + const color = palette[colorKey] || palette.teal; + ctx.save(); + ctx.shadowColor = color; + ctx.shadowBlur = 20; + ctx.fillStyle = "rgba(16, 16, 15, 0.86)"; + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.roundRect(x - 58, y - 23, 116, 46, 8); + ctx.fill(); + ctx.stroke(); + ctx.shadowBlur = 0; + ctx.fillStyle = palette.text; + ctx.font = "700 13px system-ui, sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(label, x, y + pulse * 0.25); + ctx.restore(); + } + + function setTopology(name) { + if (!topologies[name]) return; + active = name; + caption.textContent = topologies[name].caption; + buttons.forEach((button) => { + const selected = button.dataset.topology === name; + button.classList.toggle("active", selected); + button.setAttribute("aria-pressed", selected ? "true" : "false"); + }); + } + + buttons.forEach((button) => { + button.addEventListener("click", () => setTopology(button.dataset.topology)); + }); + + if (signalWord) { + window.setInterval(() => { + wordIndex = (wordIndex + 1) % words.length; + signalWord.textContent = words[wordIndex]; + signalWord.classList.remove("swap"); + void signalWord.offsetWidth; + signalWord.classList.add("swap"); + }, 1800); + } + + if (!CanvasRenderingContext2D.prototype.roundRect) { + CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { + this.beginPath(); + this.moveTo(x + r, y); + this.lineTo(x + w - r, y); + this.quadraticCurveTo(x + w, y, x + w, y + r); + this.lineTo(x + w, y + h - r); + this.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + this.lineTo(x + r, y + h); + this.quadraticCurveTo(x, y + h, x, y + h - r); + this.lineTo(x, y + r); + this.quadraticCurveTo(x, y, x + r, y); + this.closePath(); + }; + } + + window.addEventListener("resize", resize); + resize(); + setTopology(active); + requestAnimationFrame(draw); +})(); diff --git a/sites/ampgateway-online/index.html b/sites/ampgateway-online/index.html new file mode 100644 index 0000000..ee0c47f --- /dev/null +++ b/sites/ampgateway-online/index.html @@ -0,0 +1,177 @@ + + +
+ + +Adaptive Multi-Protocol Gateway
++ One site source, many transport outputs, one staged deploy spine from package + install to published health checks. +
++ publish over + clearnet + without losing the map. +
+ ++ A VPS can publish clearnet, Tor, I2P, and Gemini outputs from one AMPG plan. +
+Network topologies
+
+ ampgateway.site is the target for a full public example with
+ clearnet, Tor, I2P, and Gemini routes managed from one deploy plan.
+
+ ampgateway.website is the target for a home or lab deployment
+ that shows Dynamic DNS, router constraints, and transports that avoid
+ inbound ports.
+
+ Android and Termux can run user-space daemons for small static sites, + demos, and transport experiments on hardware that would otherwise sit idle. +
++ AMPG can deploy only Tor, only I2P, only Gemini, or a selected subset when + the operator wants a narrow surface. +
+Example domains
+Visual hub
++ Public explanation site with interactive topology views, deployment flow, + and links into the clean docs. +
+ ampgateway.online +VPS example
++ Planned all-transport reference deployment on a normal server, useful for + showing the complete package, state, supervisor, start, address, and health path. +
+ ampgateway.site +Behind-router example
++ Planned home-network reference deployment for Dynamic DNS, port-forwarding + choices, and transports that do not need clearnet inbound reachability. +
+ ampgateway.website +Deploy spine
+Generate clearnet, privacy HTML, Gemtext, and other selected outputs.
Install selected daemon packages with allowlisted platform commands.
Copy approved generated config into AMPG-owned state.
Install AMPG-named service files after config exists.
Start AMPG-owned services with structured platform commands.
Capture daemon-written onion, I2P, or other public identities.
Verify published fixture URLs through the selected transport profiles.
Public docs
++ The source for this page lives in the AMPG repository and can be rendered by + AMPG like any other static site. The Markdown docs stay concise; this site + turns the same concepts into topology-first views. +
+