clavitor/clavitor.com/templates/hosted.tmpl

291 lines
14 KiB
Cheetah

{{define "hosted"}}
<!-- Hero -->
<div class="hero container">
<p class="label accent mb-4"><span class="vaultname">clavitor</span> hosted</p>
<h1>Zero cache. Every request hits the vault.</h1>
<p class="lead">Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run {{len .Pops}} regions across every continent. Your data lives where you choose. <s>$20</s> $12/yr.</p>
</div>
<!-- Map -->
<div class="container">
<div class="map-wrap">
<svg id="worldmap" viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
<image href="/worldmap.svg" x="0" y="0" width="1000" height="460"/>
<text x="500" y="440" font-family="Figtree,sans-serif" font-size="18" font-weight="700" fill="#0A0A0A" text-anchor="middle" opacity="0.35" letter-spacing="0.3em">CLAVITOR GLOBAL PRESENCE</text>
</svg>
</div>
<div class="mt-12"></div>
<div id="dc-grid" class="mb-8">
<!-- Self-hosted -->
<div class="dc-card red" data-lon="-999">
<div class="dc-icon">🖥️</div>
<div class="dc-name">Self-hosted</div>
<div class="dc-sub">Your machine. Your rules.</div>
<div class="dc-status"><span class="dc-dot"></span>Free forever</div>
<a href="/install" class="btn btn-red btn-block">Download now &rarr;</a>
</div>
<!-- Zürich HQ -->
<div class="dc-card gold" data-lon="8.5">
<div class="dc-icon">🇨🇭</div>
<div class="dc-name">Zürich, Switzerland</div>
<div class="dc-sub">Capital of Privacy</div>
<div class="dc-status"><span class="dc-dot"></span>Headquarters</div>
<a href="/signup?region=eu-central-2" class="btn btn-gold btn-block">Buy now &rarr;</a>
</div>
<!-- Closest POP — populated by JS -->
<div id="closest-pop" class="dc-card" data-lon="999">
<div class="dc-icon">📍</div>
<div id="closest-name" class="dc-name">Nearest region</div>
<div id="closest-sub" class="dc-sub">Locating you…</div>
<div class="dc-status"><span class="dc-dot"></span>Closest to you</div>
<a id="closest-buy" href="/signup" class="btn btn-accent btn-block">Buy now &rarr;</a>
</div>
</div>
</div>
<hr class="divider">
<!-- Why Zürich -->
<div class="section container">
<p class="label gold mb-3">Why Zürich</p>
<h2 class="mb-4">Identity Encryption: jurisdiction irrelevant.<br>Credential Encryption: it isn't.</h2>
<p class="lead mb-8">Identity fields are protected by math — where the server sits doesn't matter. But Credential fields live on a server in a jurisdiction. A US server is subject to the CLOUD Act. Zürich, Switzerland is subject to Swiss law — which does not cooperate with foreign government data requests. No backdoors. Both layers protected.</p>
<div class="grid-3">
<div class="card">
<p class="label mb-2">Self-hosted · US</p>
<p>Your server, your rules — until a court says otherwise. CLOUD Act applies to US persons regardless of encryption.</p>
</div>
<div class="card">
<p class="label mb-2">Self-hosted · anywhere</p>
<p>Full control. Your infrastructure, your jurisdiction. The right choice if you know what you're doing.</p>
</div>
<div class="card gold">
<p class="label gold mb-2">Hosted · Zürich, Switzerland</p>
<p>Swiss law. Swiss courts. Capital of Privacy. No CLOUD Act. No backdoors. We handle the infrastructure — you get the protection.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- What hosted adds -->
<div class="section container">
<p class="label accent mb-3">What hosted adds</p>
<h2 class="mb-8">Everything in self-hosted, plus</h2>
<div class="grid-3">
<div class="card alt">
<h3 class="mb-2">Managed infrastructure</h3>
<p>We run it, monitor it, and keep it up. You just use it.</p>
</div>
<div class="card alt">
<h3 class="mb-2">Daily encrypted backups</h3>
<p>Automatic daily backups. Encrypted at rest. Restorable on request.</p>
</div>
<div class="card alt">
<h3 class="mb-2">22 regions</h3>
<p>Pick your region at signup. Your data stays there. Every continent covered.</p>
</div>
<div class="card alt">
<h3 class="mb-2">Automatic updates</h3>
<p>Security patches and new features deployed automatically. No downtime.</p>
</div>
<div class="card alt">
<h3 class="mb-2">TLS included</h3>
<p>HTTPS out of the box. No Caddy, no certbot, no renewal headaches.</p>
</div>
<div class="card alt">
<h3 class="mb-2">Email support</h3>
<p>Real human support. Not a chatbot. Not a forum post into the void.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- CTA -->
<div class="section container">
<h2 class="mb-4">Ready?</h2>
<p class="lead mb-6"><s>$20</s> $12/yr. 7-day money-back. Every feature included.</p>
<div class="btn-row">
<a href="/signup" class="btn btn-primary">Get started</a>
<a href="/pricing" class="btn btn-ghost">Compare plans &rarr;</a>
</div>
</div>
{{end}}
{{define "hosted-script"}}
<script>
(function() {
const W = 1000, H = 460;
const ns = 'http://www.w3.org/2000/svg';
function project(lon, lat) {
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
const miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
const maxMiller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*80*Math.PI/180));
const x = (lon + 180) / 360 * W;
const y = H/2 - (miller / (2*maxMiller)) * H;
return [Math.round(x*10)/10, Math.round(y*10)/10];
}
function addPopDot(svg, pop, delay) {
const [x, y] = project(pop.lon, pop.lat);
const isHQ = pop.city === 'Zürich';
const isLive = pop.status === 'live';
const dotColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
const textColor = isHQ ? '#0A0A0A' : isLive ? '#B91C1C' : '#777777';
const pulseColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
const dotSize = isHQ ? 11 : 9;
const pulseR = isHQ ? 5 : 4;
const pulseMax = isHQ ? 18 : 13;
const tooltip = isLive ? pop.city + ' · Live' : pop.city + ' · Planned · Q2 2026';
// Pulse ring 1
const r1 = document.createElementNS(ns, 'circle');
r1.setAttribute('cx', x); r1.setAttribute('cy', y);
r1.setAttribute('r', pulseR); r1.setAttribute('fill', 'none');
r1.setAttribute('stroke', pulseColor); r1.setAttribute('stroke-width', isHQ ? '2' : '1.5');
const a1 = document.createElementNS(ns, 'animate');
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
a1.setAttribute('dur', '2.4s'); a1.setAttribute('begin', delay+'s'); a1.setAttribute('repeatCount', 'indefinite');
const a2 = document.createElementNS(ns, 'animate');
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.6;0;0.6');
a2.setAttribute('dur', '2.4s'); a2.setAttribute('begin', delay+'s'); a2.setAttribute('repeatCount', 'indefinite');
r1.appendChild(a1); r1.appendChild(a2);
// Pulse ring 2
const r2 = document.createElementNS(ns, 'circle');
r2.setAttribute('cx', x); r2.setAttribute('cy', y);
r2.setAttribute('r', pulseR); r2.setAttribute('fill', 'none');
r2.setAttribute('stroke', pulseColor); r2.setAttribute('stroke-width', isHQ ? '1.5' : '1');
const a3 = document.createElementNS(ns, 'animate');
a3.setAttribute('attributeName', 'r'); a3.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
a3.setAttribute('dur', '2.4s'); a3.setAttribute('begin', (delay+0.8)+'s'); a3.setAttribute('repeatCount', 'indefinite');
const a4 = document.createElementNS(ns, 'animate');
a4.setAttribute('attributeName', 'stroke-opacity'); a4.setAttribute('values', '0.4;0;0.4');
a4.setAttribute('dur', '2.4s'); a4.setAttribute('begin', (delay+0.8)+'s'); a4.setAttribute('repeatCount', 'indefinite');
r2.appendChild(a3); r2.appendChild(a4);
// Square dot
const half = dotSize / 2;
const dot = document.createElementNS(ns, 'rect');
dot.setAttribute('x', x - half); dot.setAttribute('y', y - half);
dot.setAttribute('width', dotSize); dot.setAttribute('height', dotSize);
dot.setAttribute('fill', dotColor); dot.setAttribute('stroke', '#F5F5F5'); dot.setAttribute('stroke-width', '1.5');
const title = document.createElementNS(ns, 'title');
title.textContent = tooltip;
dot.appendChild(title);
// Label
const label = document.createElementNS(ns, 'text');
label.setAttribute('x', x); label.setAttribute('y', y - half - 4);
label.setAttribute('font-family', 'Inter,sans-serif');
label.setAttribute('font-size', '8.5');
label.setAttribute('fill', textColor);
label.setAttribute('text-anchor', 'middle');
label.setAttribute('opacity', '0.85');
label.textContent = pop.city;
const labelTitle = document.createElementNS(ns, 'title');
labelTitle.textContent = tooltip;
label.appendChild(labelTitle);
svg.appendChild(r1);
svg.appendChild(r2);
svg.appendChild(dot);
svg.appendChild(label);
}
function addVisitorDot(lat, lon, city) {
const svg = document.getElementById('worldmap');
if (!svg) return;
const [x, y] = project(lon, lat);
const ring = document.createElementNS(ns, 'circle');
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', '#0A0A0A'); ring.setAttribute('stroke-width', '1.5');
const a1 = document.createElementNS(ns, 'animate');
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
const a2 = document.createElementNS(ns, 'animate');
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.8;0;0.8');
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
ring.appendChild(a1); ring.appendChild(a2);
const dot = document.createElementNS(ns, 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#0A0A0A');
dot.setAttribute('stroke', '#ffffff'); dot.setAttribute('stroke-width', '1.5');
const label = document.createElementNS(ns, 'text');
label.setAttribute('x', x); label.setAttribute('y', y + 15);
label.setAttribute('font-family', 'Inter,sans-serif');
label.setAttribute('font-size', '10');
label.setAttribute('fill', '#0A0A0A');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('font-weight', '500');
label.textContent = city || 'You';
svg.appendChild(ring);
svg.appendChild(dot);
svg.appendChild(label);
}
const POPS = [{{range .Pops}}
{city:"{{.City}}", region:"{{.RegionName}}", lat:{{.Lat}}, lon:{{.Lon}}, status:"{{.Status}}", provider:"{{.Provider}}"},{{end}}
];
// Render all POP dots from DB data
const svg = document.getElementById('worldmap');
if (svg) {
POPS.forEach((pop, i) => addPopDot(svg, pop, (i * 0.08).toFixed(2)));
}
function findClosestPop(lat, lon) {
return POPS.reduce((best, p) => {
const d = (lat-p.lat)**2 + (lon-p.lon)**2;
const bd = (lat-best.lat)**2 + (lon-best.lon)**2;
return d < bd ? p : best;
});
}
function handleGeoData(d) {
if (!d.latitude || !d.longitude) return;
addVisitorDot(d.latitude, d.longitude, d.city || 'You');
const closest = findClosestPop(d.latitude, d.longitude);
const nameEl = document.getElementById('closest-name');
const subEl = document.getElementById('closest-sub');
const buyEl = document.getElementById('closest-buy');
if (nameEl) nameEl.textContent = closest.city;
if (subEl) subEl.textContent = d.city ? `~${d.city}` : 'Your region';
if (buyEl) buyEl.href = `/signup?region=${closest.region}`;
}
// Ask browser geolocation first (accurate, triggers permission prompt)
// Fall back to server-side IP lookup if denied or unavailable
function tryIPGeo() {
fetch('/geo')
.then(r => r.json())
.then(d => { if (d.latitude) handleGeoData(d); })
.catch(() => {});
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
pos => {
handleGeoData({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
city: '', region: '', country_name: '', country_code: ''
});
},
() => tryIPGeo() // denied — fall back to IP
);
} else {
tryIPGeo();
}
})();
</script>
{{end}}