commit ee2249c5f8591b5b7ad00ccebae237060a236163 Author: Hexadual Date: Thu Apr 30 01:21:22 2026 -0500 Initial commit — GCP Dot self-hosted clone Server (FastAPI + SQLite) runs Stouffer Z network variance analysis. Egg container uses os.urandom for hardware-entropy 200-bit trials. Gitea Actions workflow auto-builds and publishes both Docker images. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9258b3d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto +Dockerfile text eol=lf +*.sh text eol=lf +*.py text eol=lf diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..6ce0d2c --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -0,0 +1,81 @@ +name: Build & publish Docker images + +on: + push: + branches: [main] + release: + types: [published] + +env: + REGISTRY: git.hexadual.io + +jobs: + server: + name: Server image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Gitea container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/rocobo/gcp-dot-server + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=sha,prefix=sha-,format=short + + - name: Build and push server image + uses: docker/build-push-action@v5 + with: + context: ./server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + egg: + name: Egg image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Gitea container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/rocobo/gcp-dot-egg + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=sha,prefix=sha-,format=short + + - name: Build and push egg image + uses: docker/build-push-action@v5 + with: + context: ./egg + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4408ec7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +.env +*.db +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..a42a0a8 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Global Consciousness Project — Self-Hosted + +A live network-variance tracker inspired by the [Global Consciousness Project](https://global-mind.org). +Distributed "egg" containers contribute random numbers; the server runs statistical analysis and displays a colored dot. + +## Architecture + +``` +Egg containers (anyone can run) + │ POST /api/data (one 200-bit trial per second) + ▼ + Server (FastAPI + SQLite) + │ runs Stouffer Z network variance analysis every 60 s + ▼ + Website → animated dot + history chart + embeddable iframe +``` + +## Quick start — run the server + +```bash +git clone +cd gcp +docker compose up -d +``` + +The site is now at **http://localhost:8000**. + +To expose it publicly, put it behind a reverse proxy (nginx, Caddy) or deploy to any VPS / cloud service. + +## Contribute an egg + +Anyone can run an egg — one Docker command: + +```bash +docker run -d --restart unless-stopped \ + -e SERVER_URL=https://your-domain.com \ + -v gcp_egg_data:/data \ + git.hexadual.io/rocobo/gcp-dot-egg:latest +``` + +The egg: +- Reads 200-bit trials from the OS hardware-entropy pool (`os.urandom`) once per second +- Sends the trial count (0–200) to the server +- Persists its ID across restarts via `/data/egg_id` + +## Images (built automatically by Gitea Actions) + +Every push to `main` builds and publishes both images to the Gitea container registry: + +| Image | Pull command | +|-------|-------------| +| Server | `docker pull git.hexadual.io/rocobo/gcp-dot-server:latest` | +| Egg | `docker pull git.hexadual.io/rocobo/gcp-dot-egg:latest` | + +## How the analysis works + +Every 60 seconds the server analyses the past hour of data: + +1. **Normalise** each trial to a Z-score: `z = (trial − 100) / √50` + (expected mean = 100, variance = 50 for Binomial(200, 0.5)) + +2. **Stouffer Z** per second across all active eggs: `S_t = Σzᵢₜ / √N` + +3. **Network variance**: `V = Σ S_t²` — follows χ²(T) under H₀ + +4. **Index** = lower-tail CDF × 100: + - **High index (> 95)** → small variance → eggs are more coherent than chance → **blue** + - **Low index (< 5)** → large variance → eggs are noisier than chance → **red** + - **Middle (40–90)** → normal random behavior → **green** + +### Color table + +| Color | Index | Meaning | +|--------|-----------|---------| +| Blue | > 95% | Significantly small variance — deep coherence | +| Cyan | 90–95% | Small variance — probable coherence | +| Green | 40–90% | Normal random behavior | +| Yellow | 10–40% | Slightly elevated variance | +| Orange | 5–10% | Strongly elevated variance | +| Red | < 5% | Significantly large variance | + +## Embed the dot + +```html + +``` + +## API + +| Endpoint | Description | +|----------|-------------| +| `POST /api/data` | Submit a trial `{egg_id, timestamp, trial}` | +| `GET /api/status` | Latest analysis result | +| `GET /api/history?limit=60` | Last N analysis records | +| `GET /api/eggs` | Eggs active in the last 2 minutes | + +## Environment variables + +### Server +| Variable | Default | Description | +|----------|---------|-------------| +| `DB_PATH` | `/data/gcp.db` | SQLite database path | + +### Egg +| Variable | Default | Description | +|----------|---------|-------------| +| `SERVER_URL` | `http://localhost:8000` | Server to send data to | +| `EGG_ID` | auto-generated | Override the egg's identifier | diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..27123e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + server: + build: ./server + ports: + - "8000:8000" + volumes: + - gcp_data:/data + restart: unless-stopped + environment: + - DB_PATH=/data/gcp.db + +volumes: + gcp_data: diff --git a/egg/Dockerfile b/egg/Dockerfile new file mode 100644 index 0000000..3e23f47 --- /dev/null +++ b/egg/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY egg.py . + +VOLUME ["/data"] + +CMD ["python", "egg.py"] diff --git a/egg/egg.py b/egg/egg.py new file mode 100644 index 0000000..b09a05b --- /dev/null +++ b/egg/egg.py @@ -0,0 +1,71 @@ +import os +import time +import hashlib +import platform + +import requests + +ID_FILE = "/data/egg_id" + + +def generate_trial(): + """Sum of 200 random bits drawn from the OS CSPRNG (hardware entropy pool).""" + raw = os.urandom(25) # 25 bytes = 200 bits + return sum(bin(b).count("1") for b in raw) + + +def load_or_create_id(): + try: + with open(ID_FILE) as f: + egg_id = f.read().strip() + if egg_id: + return egg_id + except OSError: + pass + egg_id = hashlib.sha256(os.urandom(32)).hexdigest()[:16] + try: + os.makedirs("/data", exist_ok=True) + with open(ID_FILE, "w") as f: + f.write(egg_id) + except OSError: + pass + return egg_id + + +def main(): + server_url = os.environ.get("SERVER_URL", "http://localhost:8000").rstrip("/") + egg_id = os.environ.get("EGG_ID") or load_or_create_id() + + print(f"GCP Egg id={egg_id} server={server_url} platform={platform.system()}") + + session = requests.Session() + errors = 0 + + while True: + loop_start = time.time() + timestamp = int(loop_start) + trial = generate_trial() + + try: + resp = session.post( + f"{server_url}/api/data", + json={"egg_id": egg_id, "timestamp": timestamp, "trial": trial}, + timeout=5, + ) + if resp.status_code == 200: + errors = 0 + else: + errors += 1 + if errors % 10 == 1: + print(f"Server returned {resp.status_code} (error #{errors})") + except Exception as exc: + errors += 1 + if errors % 10 == 1: + print(f"Send error (#{errors}): {exc}") + + elapsed = time.time() - loop_start + time.sleep(max(0.0, 1.0 - elapsed)) + + +if __name__ == "__main__": + main() diff --git a/egg/requirements.txt b/egg/requirements.txt new file mode 100644 index 0000000..2c24336 --- /dev/null +++ b/egg/requirements.txt @@ -0,0 +1 @@ +requests==2.31.0 diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..7e81452 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . +COPY static/ ./static/ + +VOLUME ["/data"] + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..582cadb --- /dev/null +++ b/server/main.py @@ -0,0 +1,231 @@ +""" +GCP Server — collects random trials from distributed eggs, runs Stouffer Z +network variance analysis every 60 seconds, and serves the dot website. +""" + +import os +import sqlite3 +import threading +import time +from collections import defaultdict +from contextlib import contextmanager + +import numpy as np +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field +from scipy import stats + +DB_PATH = os.environ.get("DB_PATH", "/data/gcp.db") +ANALYSIS_INTERVAL = 60 # seconds between analysis runs +DATA_OFFSET = 60 # analyse data at least 60 s old (propagation buffer) +WINDOW_SECONDS = 3600 # 1-hour analysis window (matches GCP primary window) +KEEP_SECONDS = 48 * 3600 # purge raw trials older than 48 h +MIN_EGGS = 2 # need at least 2 eggs to compute network variance +MIN_SAMPLES = 10 # need at least 10 time-steps in window + + +# --------------------------------------------------------------------------- +# Database helpers +# --------------------------------------------------------------------------- + +def init_db(): + with db() as con: + con.executescript(""" + CREATE TABLE IF NOT EXISTS trials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + egg_id TEXT NOT NULL, + timestamp INTEGER NOT NULL, + trial INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_trials_ts ON trials(timestamp); + + CREATE TABLE IF NOT EXISTS analysis ( + timestamp INTEGER PRIMARY KEY, + index_value REAL NOT NULL, + n_eggs INTEGER NOT NULL, + n_samples INTEGER NOT NULL, + window_seconds INTEGER NOT NULL + ); + """) + + +@contextmanager +def db(): + con = sqlite3.connect(DB_PATH) + con.row_factory = sqlite3.Row + try: + yield con + con.commit() + finally: + con.close() + + +# --------------------------------------------------------------------------- +# Statistical analysis +# --------------------------------------------------------------------------- + +def compute_index(rows): + """ + Compute the GCP network variance index from raw trial rows. + + Each egg produces one 200-bit trial per second: trial = count of 1-bits. + Under H0: trial ~ Binomial(200, 0.5), mean=100, var=50. + + Steps: + 1. Normalise each trial to a Z-score. + 2. For each second, combine eggs via Stouffer Z. + 3. Sum squared Stouffer Zs → network variance statistic ~ χ²(T). + 4. Index = lower-tail CDF × 100. + High index (>95) = small variance = coherence = blue. + Low index (<5) = large variance = noise spike = red. + """ + # Build {timestamp: {egg_id: trial}} dict + by_ts = defaultdict(dict) + for row in rows: + by_ts[row["timestamp"]][row["egg_id"]] = row["trial"] + + stouffer_sq = [] + for ts in sorted(by_ts): + egg_vals = list(by_ts[ts].values()) + if len(egg_vals) < MIN_EGGS: + continue + z_scores = [(v - 100) / np.sqrt(50) for v in egg_vals] + n = len(z_scores) + stouffer = sum(z_scores) / np.sqrt(n) + stouffer_sq.append(stouffer ** 2) + + if len(stouffer_sq) < MIN_SAMPLES: + return None, 0, 0 + + T = len(stouffer_sq) + net_var = float(np.sum(stouffer_sq)) + index_value = float(stats.chi2.cdf(net_var, df=T)) * 100.0 + + n_eggs = len({row["egg_id"] for row in rows}) + return index_value, n_eggs, T + + +def analysis_loop(): + """Background thread: run analysis every ANALYSIS_INTERVAL seconds.""" + # Wait a bit on startup so the DB is definitely initialised. + time.sleep(10) + + while True: + try: + now = int(time.time()) + end = now - DATA_OFFSET + start = end - WINDOW_SECONDS + + with db() as con: + rows = con.execute( + "SELECT egg_id, timestamp, trial FROM trials " + "WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp", + (start, end), + ).fetchall() + + index_value, n_eggs, n_samples = compute_index(rows) + + if index_value is not None: + with db() as con: + con.execute( + "INSERT OR REPLACE INTO analysis " + "(timestamp, index_value, n_eggs, n_samples, window_seconds) " + "VALUES (?, ?, ?, ?, ?)", + (now, index_value, n_eggs, n_samples, WINDOW_SECONDS), + ) + print( + f"[analysis] index={index_value:.1f}% eggs={n_eggs} samples={n_samples}" + ) + + # Purge old raw data + with db() as con: + con.execute( + "DELETE FROM trials WHERE timestamp < ?", (now - KEEP_SECONDS,) + ) + + except Exception as exc: + print(f"[analysis] error: {exc}") + + time.sleep(ANALYSIS_INTERVAL) + + +# --------------------------------------------------------------------------- +# FastAPI app +# --------------------------------------------------------------------------- + +app = FastAPI(title="Global Consciousness Project") + + +class TrialData(BaseModel): + egg_id: str = Field(..., min_length=1, max_length=64) + timestamp: int + trial: int = Field(..., ge=0, le=200) + + +@app.on_event("startup") +def startup(): + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + init_db() + threading.Thread(target=analysis_loop, daemon=True, name="analysis").start() + + +@app.post("/api/data") +def receive_trial(data: TrialData): + now = int(time.time()) + # Reject data more than 5 minutes out of sync + if abs(data.timestamp - now) > 300: + raise HTTPException(status_code=422, detail="timestamp out of range") + with db() as con: + con.execute( + "INSERT INTO trials (egg_id, timestamp, trial) VALUES (?, ?, ?)", + (data.egg_id, data.timestamp, data.trial), + ) + return {"status": "ok"} + + +@app.get("/api/status") +def get_status(): + with db() as con: + row = con.execute( + "SELECT * FROM analysis ORDER BY timestamp DESC LIMIT 1" + ).fetchone() + if row: + return { + "index": round(row["index_value"], 2), + "n_eggs": row["n_eggs"], + "n_samples": row["n_samples"], + "timestamp": row["timestamp"], + } + return {"index": 50.0, "n_eggs": 0, "n_samples": 0, "timestamp": 0} + + +@app.get("/api/history") +def get_history(limit: int = 60): + with db() as con: + rows = con.execute( + "SELECT timestamp, index_value, n_eggs FROM analysis " + "ORDER BY timestamp DESC LIMIT ?", + (min(limit, 1440),), + ).fetchall() + return [ + {"timestamp": r["timestamp"], "index": round(r["index_value"], 2), "n_eggs": r["n_eggs"]} + for r in rows + ] + + +@app.get("/api/eggs") +def get_active_eggs(): + """Return eggs that have submitted data in the last 2 minutes.""" + cutoff = int(time.time()) - 120 + with db() as con: + rows = con.execute( + "SELECT DISTINCT egg_id, MAX(timestamp) as last_seen FROM trials " + "WHERE timestamp > ? GROUP BY egg_id ORDER BY last_seen DESC", + (cutoff,), + ).fetchall() + return [{"egg_id": r["egg_id"], "last_seen": r["last_seen"]} for r in rows] + + +# Serve static files last so API routes take priority +app.mount("/", StaticFiles(directory="/app/static", html=True), name="static") diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..6793416 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.110.0 +uvicorn[standard]==0.27.1 +numpy==1.26.4 +scipy==1.12.0 +pydantic==2.6.3 diff --git a/server/static/gcp.html b/server/static/gcp.html new file mode 100644 index 0000000..3aa8140 --- /dev/null +++ b/server/static/gcp.html @@ -0,0 +1,51 @@ + + + + + + + +
+ + + diff --git a/server/static/index.html b/server/static/index.html new file mode 100644 index 0000000..4f7e367 --- /dev/null +++ b/server/static/index.html @@ -0,0 +1,434 @@ + + + + + + Global Consciousness Project — Live Dot + + + +

Global Consciousness Project

+

Real-time network variance

+ +
+
+
+ +

Connecting…

+ +
+
+ + Index +
+
+ + Active Eggs +
+
+ + Updated +
+
+ + + + +
+

What the color means

+
+
+ Deeply coherent — significantly small network variance + > 95% +
+
+
+ Small variance — probably coherence + 90 – 95% +
+
+
+ Normal — expected random behavior + 40 – 90% +
+
+
+ Slightly elevated variance — probably chance + 10 – 40% +
+
+
+ Strongly elevated variance — possible shared focus + 5 – 10% +
+
+
+ Significantly large variance — broadly shared emotion + < 5% +
+
+ + +
+

Embed on your site

+
+

Click to copy · The dot updates every 60 seconds

+
+ +
+

Methodology based on the Global Consciousness Project by Roger Nelson.

+

Contribute an egg: docker run -e SERVER_URL= yourname/gcp-egg

+
+ +

+
Copied!
+ + + +