Files
GCP-Dot/server/static/index.html
Hexadual 62b9995cd9
Some checks failed
Build & publish Docker images / Server image (push) Failing after 15s
Build & publish Docker images / Egg image (push) Failing after 17s
Add anonymised world map of active eggs
Server geolocates each egg by IP on first contact (ip-api.com), snaps
coordinates to the nearest 1-degree grid (~111 km) and adds a small
deterministic per-egg jitter so eggs in the same cell don't overlap.
Only country + rounded lat/lon are stored; exact IPs are never saved.

Front-end uses Leaflet with CartoDB dark tiles. Markers update on every
poll cycle and are coloured to match the current coherence index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 01:32:19 -05:00

524 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Global Consciousness Project — Live Dot</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
:root {
--dot-color: #00cc44;
--glow: rgba(0,204,68,.35);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #080812;
color: #b0b0c8;
font-family: 'Segoe UI', system-ui, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2.5rem 1rem 4rem;
gap: 0;
}
/* ── header ── */
h1 {
font-size: 1rem;
font-weight: 300;
letter-spacing: .25em;
text-transform: uppercase;
color: #505070;
margin-bottom: .4rem;
}
.subtitle {
font-size: .75rem;
letter-spacing: .12em;
color: #303050;
text-transform: uppercase;
}
/* ── dot ── */
.dot-wrap {
position: relative;
width: 220px;
height: 220px;
margin: 2.5rem 0 1.5rem;
flex-shrink: 0;
}
.dot {
width: 160px;
height: 160px;
border-radius: 50%;
background: var(--dot-color);
box-shadow:
0 0 50px 18px var(--glow),
0 0 110px 40px var(--glow);
position: absolute;
inset: 0;
margin: auto;
transition: background 2.5s ease, box-shadow 2.5s ease;
animation: breathe 4s ease-in-out infinite;
}
@keyframes breathe {
0%,100% { transform: scale(1); }
50% { transform: scale(1.04); }
}
/* ── status label ── */
.status-label {
font-size: 1rem;
color: #d0d0e8;
text-align: center;
min-height: 1.6em;
transition: color 2s ease;
}
.status-label strong { color: #fff; }
/* ── stats row ── */
.stats {
display: flex;
gap: 3rem;
margin: 2rem 0 1rem;
}
.stat { text-align: center; }
.stat .val {
display: block;
font-size: 1.8rem;
font-weight: 200;
color: #9090b8;
line-height: 1;
}
.stat .lbl {
display: block;
font-size: .65rem;
letter-spacing: .12em;
text-transform: uppercase;
color: #383858;
margin-top: .3rem;
}
/* ── history chart ── */
#chart {
display: block;
width: min(480px, 96vw);
height: 72px;
margin: .5rem 0 2rem;
}
/* ── map ── */
#map-wrap {
width: min(480px, 96vw);
background: #0e0e1c;
border: 1px solid #18182a;
border-radius: 12px;
overflow: hidden;
margin-bottom: 1rem;
}
#map-title {
font-size: .65rem;
letter-spacing: .14em;
text-transform: uppercase;
color: #303050;
padding: 1rem 1.6rem .6rem;
}
#map {
height: 240px;
width: 100%;
background: #080812;
}
/* Override Leaflet chrome for dark theme */
.leaflet-container { background: #080812; }
.leaflet-control-attribution { display: none; }
.leaflet-tooltip {
background: #10101e;
border: 1px solid #202038;
color: #9090b8;
font-size: .75rem;
padding: .3rem .6rem;
border-radius: 6px;
box-shadow: none;
}
.leaflet-tooltip-top::before { border-top-color: #202038; }
/* ── legend ── */
.card {
width: min(480px, 96vw);
background: #0e0e1c;
border: 1px solid #18182a;
border-radius: 12px;
padding: 1.4rem 1.6rem;
margin-bottom: 1rem;
}
.card-title {
font-size: .65rem;
letter-spacing: .14em;
text-transform: uppercase;
color: #303050;
margin-bottom: 1rem;
}
.legend-row {
display: flex;
align-items: center;
gap: .75rem;
padding: .35rem 0;
font-size: .82rem;
color: #7070a0;
border-bottom: 1px solid #131325;
}
.legend-row:last-child { border-bottom: none; }
.ldot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-range {
margin-left: auto;
font-size: .7rem;
color: #383858;
white-space: nowrap;
}
/* ── embed box ── */
.embed-code {
background: #080812;
border: 1px solid #1a1a2e;
border-radius: 6px;
padding: .7rem 1rem;
font-family: monospace;
font-size: .75rem;
color: #6060a0;
word-break: break-all;
cursor: pointer;
transition: border-color .2s;
}
.embed-code:hover { border-color: #404068; }
.copy-hint {
font-size: .65rem;
color: #282840;
margin-top: .4rem;
}
/* ── footer ── */
footer {
margin-top: 2rem;
font-size: .75rem;
color: #282840;
text-align: center;
line-height: 1.8;
}
footer a { color: #383858; }
/* ── tick ── */
#tick {
font-size: .65rem;
color: #282840;
margin-top: 1.5rem;
letter-spacing: .05em;
}
/* ── copied toast ── */
#toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(4rem);
background: #1c1c30;
color: #8080c0;
padding: .5rem 1.2rem;
border-radius: 99px;
font-size: .8rem;
transition: transform .3s ease;
pointer-events: none;
}
#toast.show { transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<h1>Global Consciousness Project</h1>
<p class="subtitle">Real-time network variance</p>
<div class="dot-wrap">
<div class="dot" id="dot"></div>
</div>
<p class="status-label" id="status-label">Connecting…</p>
<div class="stats">
<div class="stat">
<span class="val" id="s-index"></span>
<span class="lbl">Index</span>
</div>
<div class="stat">
<span class="val" id="s-eggs"></span>
<span class="lbl">Active Eggs</span>
</div>
<div class="stat">
<span class="val" id="s-age"></span>
<span class="lbl">Updated</span>
</div>
</div>
<canvas id="chart"></canvas>
<!-- map -->
<div id="map-wrap">
<p id="map-title">Active eggs — anonymised to ~111 km</p>
<div id="map"></div>
</div>
<!-- legend -->
<div class="card">
<p class="card-title">What the color means</p>
<div class="legend-row">
<div class="ldot" style="background:#0055ff;box-shadow:0 0 6px #0055ff"></div>
Deeply coherent — significantly small network variance
<span class="legend-range">&gt; 95%</span>
</div>
<div class="legend-row">
<div class="ldot" style="background:#00ccff;box-shadow:0 0 6px #00ccff"></div>
Small variance — probably coherence
<span class="legend-range">90 95%</span>
</div>
<div class="legend-row">
<div class="ldot" style="background:#00cc44;box-shadow:0 0 6px #00cc44"></div>
Normal — expected random behavior
<span class="legend-range">40 90%</span>
</div>
<div class="legend-row">
<div class="ldot" style="background:#ffdd00;box-shadow:0 0 6px #ffdd00"></div>
Slightly elevated variance — probably chance
<span class="legend-range">10 40%</span>
</div>
<div class="legend-row">
<div class="ldot" style="background:#ff8800;box-shadow:0 0 6px #ff8800"></div>
Strongly elevated variance — possible shared focus
<span class="legend-range">5 10%</span>
</div>
<div class="legend-row">
<div class="ldot" style="background:#ff2200;box-shadow:0 0 6px #ff2200"></div>
Significantly large variance — broadly shared emotion
<span class="legend-range">&lt; 5%</span>
</div>
</div>
<!-- embed -->
<div class="card">
<p class="card-title">Embed on your site</p>
<div class="embed-code" id="embed-code" onclick="copyEmbed()" title="Click to copy"></div>
<p class="copy-hint">Click to copy · The dot updates every 60 seconds</p>
</div>
<footer>
<p>Methodology based on the <a href="https://global-mind.org" target="_blank" rel="noopener">Global Consciousness Project</a> by Roger Nelson.</p>
<p>Contribute an egg: <code>docker run -e SERVER_URL=<span class="origin-span"></span> yourname/gcp-egg</code></p>
</footer>
<p id="tick"></p>
<div id="toast">Copied!</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ── color mapping ──────────────────────────────────────────────────────
function palette(index) {
if (index > 95) return ['#0055ff', 'rgba(0,85,255,.35)'];
if (index > 90) return ['#00ccff', 'rgba(0,204,255,.35)'];
if (index > 40) return ['#00cc44', 'rgba(0,204,68,.35)'];
if (index > 10) return ['#ffdd00', 'rgba(255,221,0,.35)'];
if (index > 5) return ['#ff8800', 'rgba(255,136,0,.35)'];
return ['#ff2200', 'rgba(255,34,0,.35)'];
}
function label(index, n_eggs) {
if (!n_eggs) return 'No eggs connected — waiting for data';
if (index > 95) return '<strong>Deeply coherent</strong> — significantly small network variance';
if (index > 90) return '<strong>Slightly coherent</strong> — probably chance fluctuation';
if (index > 40) return '<strong>Normal</strong> — expected random behavior';
if (index > 10) return '<strong>Slightly elevated</strong> variance — probably chance';
if (index > 5) return '<strong>Strongly elevated</strong> variance — possible shared focus';
return '<strong>Significantly large</strong> variance — broadly shared emotion';
}
// ── dot ───────────────────────────────────────────────────────────────
const dotEl = document.getElementById('dot');
function applyColor(index) {
const [color, glow] = palette(index);
dotEl.style.background = color;
dotEl.style.boxShadow = `0 0 50px 18px ${glow}, 0 0 110px 40px ${glow}`;
}
// ── chart ─────────────────────────────────────────────────────────────
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
function drawChart(history) {
const W = canvas.offsetWidth || 480;
const H = 72;
canvas.width = W * devicePixelRatio;
canvas.height = H * devicePixelRatio;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.scale(devicePixelRatio, devicePixelRatio);
if (!history.length) return;
const n = history.length;
const step = W / Math.max(n - 1, 1);
const pad = 4;
ctx.clearRect(0, 0, W, H);
// Draw subtle grid lines at 5%, 40%, 90%, 95%
[5, 40, 90, 95].forEach(pct => {
const y = H - pad - ((pct / 100) * (H - 2 * pad));
ctx.beginPath();
ctx.strokeStyle = '#14142a';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
ctx.setLineDash([]);
});
// Draw polyline
history.forEach((pt, i) => {
const x = i * step;
const y = H - pad - (pt.index / 100) * (H - 2 * pad);
const [color] = palette(pt.index);
if (i === 0) {
ctx.beginPath();
ctx.moveTo(x, y);
} else {
const prev = history[i - 1];
const px = (i - 1) * step;
const py = H - pad - (prev.index / 100) * (H - 2 * pad);
ctx.lineTo(x, y);
ctx.strokeStyle = color + 'aa';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y);
}
// dot
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
});
}
// ── fetch & render ─────────────────────────────────────────────────────
async function fetchStatus() {
const r = await fetch('/api/status');
return r.json();
}
async function fetchHistory() {
const r = await fetch('/api/history?limit=60');
return r.json();
}
function ageStr(ts) {
if (!ts) return '—';
const d = Math.floor(Date.now() / 1000) - ts;
if (d < 60) return d + 's';
if (d < 3600) return Math.floor(d / 60) + 'm';
return Math.floor(d / 3600) + 'h';
}
async function update() {
try {
const [status, history] = await Promise.all([fetchStatus(), fetchHistory()]);
currentIndex = status.index;
applyColor(status.index);
document.getElementById('status-label').innerHTML = label(status.index, status.n_eggs);
document.getElementById('s-index').textContent = Math.round(status.index) + '%';
document.getElementById('s-eggs').textContent = status.n_eggs;
document.getElementById('s-age').textContent = ageStr(status.timestamp);
drawChart(history.slice().reverse());
updateMap();
} catch (e) {
document.getElementById('status-label').textContent = 'Connection error — retrying…';
}
}
// ── map ───────────────────────────────────────────────────────────────
const leafletMap = L.map('map', {
center: [20, 10],
zoom: 1,
zoomControl: false,
scrollWheelZoom: false,
attributionControl: false,
});
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', {
subdomains: 'abcd',
maxZoom: 6,
}).addTo(leafletMap);
let eggMarkers = [];
let currentIndex = 50;
async function updateMap() {
try {
const r = await fetch('/api/map');
const eggs = await r.json();
eggMarkers.forEach(m => m.remove());
eggMarkers = [];
const [color] = palette(currentIndex);
eggs.forEach(egg => {
const m = L.circleMarker([egg.lat, egg.lon], {
radius: 7,
fillColor: color,
color: color,
fillOpacity: 0.75,
weight: 0,
}).addTo(leafletMap);
m.bindTooltip(egg.country || 'Unknown', { direction: 'top', offset: [0, -4] });
eggMarkers.push(m);
});
// Update egg count in map title
document.getElementById('map-title').textContent =
`Active eggs (${eggs.length}) — anonymised to ~111 km`;
} catch (_) {}
}
// ── embed code ────────────────────────────────────────────────────────
const origin = window.location.origin;
document.querySelectorAll('.origin-span').forEach(el => el.textContent = origin);
document.getElementById('embed-code').textContent =
`<iframe src="${origin}/gcp.html" height="48" width="48" scrolling="no" frameborder="0"></iframe>`;
// ── copy toast ────────────────────────────────────────────────────────
function copyEmbed() {
const text = document.getElementById('embed-code').textContent;
navigator.clipboard?.writeText(text).catch(() => {});
const toast = document.getElementById('toast');
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1800);
}
// ── countdown ─────────────────────────────────────────────────────────
let countdown = 0;
setInterval(() => {
if (countdown <= 0) {
update();
countdown = 60;
}
document.getElementById('tick').textContent = `Next update in ${countdown}s`;
countdown--;
}, 1000);
</script>
</body>
</html>