Skip to content

Git-Commit Weather Station: Node.js + RedisTimeSeries + IPFS + WebRTC (Docker)

Objective: Convert Git activity into live "weather" metrics, store them as proper time series, snapshot to IPFS for immutable history, and stream live to browsers over WebRTC. Because code commits are atmospheric phenomena.

We will convert Git activity into live "weather" metrics, store them as proper time series, snapshot to IPFS for immutable history, and stream live to browsers over WebRTC.

0) Prerequisites (Read Once, Live by Them)

The Five Commandments

  1. Embrace the atmospheric metaphor
  2. Commits are pressure systems
  3. Merges are weather fronts
  4. Rebases are storms
  5. Code velocity is wind speed

  6. Understand the stack

  7. Node.js for orchestration
  8. RedisTimeSeries for metrics storage
  9. IPFS for immutable snapshots
  10. WebRTC for live streaming

  11. Master Docker orchestration

  12. Multi-service composition
  13. Volume mounting
  14. Network configuration
  15. Health monitoring

  16. Visualize the chaos

  17. Canvas particle systems
  18. Real-time data binding
  19. WebRTC data channels
  20. Fallback mechanisms

  21. Scale the weather

  22. Multiple repositories
  23. Historical analysis
  24. Prometheus integration
  25. Production hardening

Why These Principles: Git weather monitoring requires understanding time series storage, content addressing, real-time communication, and creative visualization. Understanding these patterns prevents monitoring chaos and enables innovative observability.

1) Stack Architecture

System Flow Diagram

flowchart LR
    G[Git Repo] -->|git log| N[Node.js Fastify]
    N -->|TS.ADD| RT[(RedisTimeSeries)]
    N -->|pin snapshots| IPFS[(IPFS Node)]
    N <-->|WebRTC Signaling| B[Browser UI]
    N -->|DataChannel| B

    subgraph "Data Sources"
        H[Commit Velocity]
        I[Merge Fronts]
        J[Rebase Storms]
    end

    subgraph "Storage Layer"
        RT
        IPFS
    end

    subgraph "Visualization"
        B
        K[Particle System]
        L[Weather Dashboard]
    end

    H --> N
    I --> N
    J --> N
    N --> K
    N --> L

Why Stack Architecture Matters: Understanding data flow enables efficient system design and troubleshooting. Understanding these patterns prevents architectural chaos and enables reliable weather monitoring.

2) Docker Compose Setup

Complete Docker Compose Configuration

# docker-compose.yml
version: "3.9"

services:
  # Redis with TimeSeries module
  redis:
    image: redis/redis-stack:7.4.0-v0
    ports:
      - "6379:6379"
      - "8001:8001"  # Redis Insight
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  # IPFS node for content addressing
  ipfs:
    image: ipfs/kubo:latest
    ports:
      - "4001:4001"   # Swarm
      - "5001:5001"   # API
      - "8080:8080"   # Gateway
    volumes:
      - ipfs-data:/data/ipfs
    environment:
      - IPFS_PROFILE=server
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5001/api/v0/version"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Node.js weather station
  weather-station:
    build: ./weather-station
    ports:
      - "3000:3000"
    environment:
      - REDIS_URL=redis://redis:6379
      - IPFS_API=http://ipfs:5001/api/v0
      - GIT_REPO_PATH=/work/repo
    volumes:
      - ../repo-under-test:/work/repo:ro  # Mount your Git repo
      - ./weather-station:/app
    depends_on:
      redis:
        condition: service_healthy
      ipfs:
        condition: service_healthy
    restart: unless-stopped

volumes:
  redis-data:
  ipfs-data:

Why Docker Compose Matters: Orchestrated services enable complete weather monitoring stack deployment. Understanding these patterns prevents service chaos and enables reliable weather station operation.

3) Node.js Weather Station

Package Configuration

{
  "name": "git-weather-station",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "fastify": "^4.27.2",
    "ioredis": "^5.4.1",
    "undici": "^6.19.8",
    "uuid": "^9.0.1",
    "ws": "^8.16.0"
  }
}

Why Package Configuration Matters: Proper dependency management prevents version conflicts and enables reproducible builds. Understanding these patterns prevents deployment chaos and enables reliable weather station operation.

Weather Station Server

// server.js
import Fastify from "fastify";
import { spawn } from "node:child_process";
import Redis from "ioredis";
import { fetch } from "undici";
import { v4 as uuidv4 } from "uuid";
import WebSocket from "ws";

const app = Fastify({ logger: true });
const redis = new Redis(process.env.REDIS_URL);

// WebRTC signaling endpoints
const peers = new Map(); // id -> { offer/answer, iceCandidates, send(json) }

// --- 1) Git -> "Weather" metrics (commit velocity, merge rate, rebase storms)
function gitStats() {
  return new Promise((resolve, reject) => {
    const since = "--since=5 minutes ago";
    const log = spawn("git", ["-C", "/work/repo", "log", since, "--pretty=%H %P %s"]);
    let out = "";
    let err = "";

    log.stdout.on("data", d => out += d.toString());
    log.stderr.on("data", d => err += d.toString());

    log.on("close", (code) => {
      if (code !== 0) {
        console.warn("Git command failed:", err);
        resolve({ commits: 0, merges: 0, rebases: 0, ts: Date.now() });
        return;
      }

      const lines = out.trim().split("\n").filter(Boolean);
      const commits = lines.length;
      const merges = lines.filter(l => l.split(" ").slice(1, -1).length >= 2).length;
      const rebases = lines.filter(l => /rebase/i.test(l)).length;

      resolve({ commits, merges, rebases, ts: Date.now() });
    });
  });
}

// --- 2) Ingest to RedisTimeSeries
async function recordStats(s) {
  try {
    // Create time series if they don't exist
    await redis.call("TS.CREATE", "git:commits", "RETENTION", "86400000", "LABELS", "metric", "commits").catch(() => {});
    await redis.call("TS.CREATE", "git:merges", "RETENTION", "86400000", "LABELS", "metric", "merges").catch(() => {});
    await redis.call("TS.CREATE", "git:rebases", "RETENTION", "86400000", "LABELS", "metric", "rebases").catch(() => {});

    // Add data points
    await redis.call("TS.ADD", "git:commits", s.ts, s.commits);
    await redis.call("TS.ADD", "git:merges", s.ts, s.merges);
    await redis.call("TS.ADD", "git:rebases", s.ts, s.rebases);

    app.log.info({ commits: s.commits, merges: s.merges, rebases: s.rebases }, "Recorded weather stats");
  } catch (error) {
    app.log.error({ error }, "Failed to record stats");
  }
}

// --- 3) Snapshot to IPFS (periodic content-addressed history)
async function snapshotToIPFS(seriesJson) {
  try {
    const boundary = uuidv4();
    const body = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="snapshot.json"\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(seriesJson)}\r\n--${boundary}--\r\n`;

    const response = await fetch(`${process.env.IPFS_API}/add`, {
      method: "POST",
      headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
      body
    });

    const text = await response.text();
    const cid = text.match(/"Hash":"([^"]+)"/)?.[1] || "unknown";
    return cid;
  } catch (error) {
    app.log.error({ error }, "Failed to snapshot to IPFS");
    return null;
  }
}

// --- 4) WebRTC signaling endpoints
app.post("/signal/offer", async (req, reply) => {
  const { id, sdp } = req.body;
  peers.set(id, { sdpOffer: sdp, queue: [] });
  app.log.info({ id }, "Received WebRTC offer");
  return { ok: true };
});

app.post("/signal/answer", async (req, reply) => {
  const { id, sdp } = req.body;
  const p = peers.get(id);
  if (!p) return reply.code(404).send({ error: "Peer not found" });

  p.sdpAnswer = sdp;
  app.log.info({ id }, "Received WebRTC answer");
  return { ok: true };
});

app.get("/signal/status/:id", async (req, reply) => {
  const p = peers.get(req.params.id);
  return p ? { haveAnswer: !!p.sdpAnswer } : reply.code(404).send({ error: "Peer not found" });
});

// --- 5) REST API for recent data (fallback)
app.get("/api/recent", async (req, reply) => {
  try {
    const now = Date.now();
    const from = now - 5 * 60 * 1000; // Last 5 minutes

    const [commits, merges, rebases] = await Promise.all([
      redis.call("TS.RANGE", "git:commits", from, now),
      redis.call("TS.RANGE", "git:merges", from, now),
      redis.call("TS.RANGE", "git:rebases", from, now),
    ]);

    return { commits, merges, rebases, timestamp: now };
  } catch (error) {
    app.log.error({ error }, "Failed to get recent data");
    return reply.code(500).send({ error: "Failed to get recent data" });
  }
});

// --- 6) WebSocket for real-time updates
const wss = new WebSocket.Server({ port: 3001 });
const clients = new Set();

wss.on("connection", (ws) => {
  clients.add(ws);
  app.log.info("WebSocket client connected");

  ws.on("close", () => {
    clients.delete(ws);
    app.log.info("WebSocket client disconnected");
  });
});

// Broadcast function
function broadcast(data) {
  const message = JSON.stringify(data);
  clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// Start server
app.listen({ port: 3000, host: "0.0.0.0" }, (err, address) => {
  if (err) throw err;
  app.log.info(`Weather station listening at ${address}`);
});

// --- 7) Main loop: poll git, write RedisTS, periodic IPFS snapshot, broadcast
setInterval(async () => {
  try {
    const stats = await gitStats();
    await recordStats(stats);

    // Periodic IPFS snapshot (20% chance every 5 minutes)
    if (Math.random() < 0.2) {
      const now = Date.now();
      const from = now - 15 * 60 * 1000; // 15 minutes window

      const window = {
        commits: await redis.call("TS.RANGE", "git:commits", from, now),
        merges: await redis.call("TS.RANGE", "git:merges", from, now),
        rebases: await redis.call("TS.RANGE", "git:rebases", from, now),
        timestamp: now
      };

      const cid = await snapshotToIPFS(window);
      if (cid) {
        app.log.info({ cid }, "Pinned snapshot to IPFS");
      }
    }

    // Broadcast live update
    broadcast({ type: "tick", payload: stats });

  } catch (error) {
    app.log.error({ error }, "Error in main loop");
  }
}, 5000); // Poll every 5 seconds

Why Weather Station Server Matters: The Node.js server orchestrates the entire weather monitoring system. Understanding these patterns prevents system chaos and enables reliable weather station operation.

4) Browser UI: WebRTC + Particle Visualization

HTML Interface

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Git Weather Station</title>
    <style>
        html, body, canvas {
            margin: 0;
            height: 100%;
            width: 100%;
            display: block;
            background: #0a0a13;
            overflow: hidden;
        }

        #status {
            position: absolute;
            top: 10px;
            left: 10px;
            color: #7fffd4;
            font-family: monospace;
            font-size: 12px;
            z-index: 100;
        }

        #metrics {
            position: absolute;
            top: 10px;
            right: 10px;
            color: #7fffd4;
            font-family: monospace;
            font-size: 12px;
            z-index: 100;
            text-align: right;
        }
    </style>
</head>
<body>
    <div id="status">Connecting...</div>
    <div id="metrics">
        <div>Commits: <span id="commits">0</span></div>
        <div>Merges: <span id="merges">0</span></div>
        <div>Rebases: <span id="rebases">0</span></div>
    </div>
    <canvas id="view"></canvas>

    <script type="module">
        // Weather station connection
        class WeatherStation {
            constructor() {
                this.ws = null;
                this.connected = false;
                this.connect();
            }

            connect() {
                try {
                    this.ws = new WebSocket('ws://localhost:3001');

                    this.ws.onopen = () => {
                        this.connected = true;
                        document.getElementById('status').textContent = 'Connected';
                        console.log('Connected to weather station');
                    };

                    this.ws.onmessage = (event) => {
                        const data = JSON.parse(event.data);
                        if (data.type === 'tick') {
                            this.updateWeather(data.payload);
                        }
                    };

                    this.ws.onclose = () => {
                        this.connected = false;
                        document.getElementById('status').textContent = 'Disconnected';
                        console.log('Disconnected from weather station');
                        // Reconnect after 5 seconds
                        setTimeout(() => this.connect(), 5000);
                    };

                    this.ws.onerror = (error) => {
                        console.error('WebSocket error:', error);
                        document.getElementById('status').textContent = 'Error';
                    };
                } catch (error) {
                    console.error('Failed to connect:', error);
                    document.getElementById('status').textContent = 'Failed';
                }
            }

            updateWeather(stats) {
                // Update metrics display
                document.getElementById('commits').textContent = stats.commits;
                document.getElementById('merges').textContent = stats.merges;
                document.getElementById('rebases').textContent = stats.rebases;

                // Trigger particle storm
                particleSystem.storm(stats);
            }
        }

        // Particle system for weather visualization
        class ParticleSystem {
            constructor() {
                this.canvas = document.getElementById('view');
                this.ctx = this.canvas.getContext('2d');
                this.particles = [];
                this.stormIntensity = 0;

                this.resize();
                window.addEventListener('resize', () => this.resize());

                // Create particles
                for (let i = 0; i < 300; i++) {
                    this.particles.push({
                        x: Math.random() * this.canvas.width,
                        y: Math.random() * this.canvas.height,
                        vx: 0,
                        vy: 0,
                        life: 1.0
                    });
                }

                this.animate();
            }

            resize() {
                this.canvas.width = window.innerWidth;
                this.canvas.height = window.innerHeight;
            }

            storm(stats) {
                // Calculate storm intensity from Git stats
                const intensity = stats.commits + stats.merges * 2 + stats.rebases * 3;
                this.stormIntensity = Math.min(intensity * 0.1, 5.0);
            }

            animate() {
                // Clear canvas with fade effect
                this.ctx.fillStyle = 'rgba(10, 10, 19, 0.1)';
                this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

                // Update and draw particles
                this.ctx.fillStyle = '#7fffd4';
                this.particles.forEach(particle => {
                    // Apply storm forces
                    particle.vx += (Math.random() - 0.5) * this.stormIntensity;
                    particle.vy += (Math.random() - 0.5) * this.stormIntensity;

                    // Apply damping
                    particle.vx *= 0.95;
                    particle.vy *= 0.95;

                    // Update position
                    particle.x += particle.vx;
                    particle.y += particle.vy;

                    // Wrap around screen
                    if (particle.x < 0) particle.x = this.canvas.width;
                    if (particle.x > this.canvas.width) particle.x = 0;
                    if (particle.y < 0) particle.y = this.canvas.height;
                    if (particle.y > this.canvas.height) particle.y = 0;

                    // Draw particle
                    this.ctx.globalAlpha = particle.life;
                    this.ctx.fillRect(particle.x, particle.y, 2, 2);
                });

                this.ctx.globalAlpha = 1.0;
                requestAnimationFrame(() => this.animate());
            }
        }

        // Initialize weather station and particle system
        const weatherStation = new WeatherStation();
        const particleSystem = new ParticleSystem();
    </script>
</body>
</html>

Why Browser UI Matters: Visual representation of weather data enables understanding of Git activity patterns. Understanding these patterns prevents monitoring confusion and enables effective weather visualization.

5) RedisTimeSeries Queries

Essential RedisTS Commands

# Check if RedisTimeSeries module is loaded
docker exec -it git-weather-station-redis-1 redis-cli MODULE LIST

# Get latest values
docker exec -it git-weather-station-redis-1 redis-cli TS.GET git:commits
docker exec -it git-weather-station-redis-1 redis-cli TS.GET git:merges
docker exec -it git-weather-station-redis-1 redis-cli TS.GET git:rebases

# Get time range with aggregation
docker exec -it git-weather-station-redis-1 redis-cli TS.RANGE git:commits - + AGGREGATION avg 60000
docker exec -it git-weather-station-redis-1 redis-cli TS.RANGE git:merges - + AGGREGATION sum 300000

# Get all time series
docker exec -it git-weather-station-redis-1 redis-cli TS.QUERYINDEX metric=commits
docker exec -it git-weather-station-redis-1 redis-cli TS.QUERYINDEX metric=merges
docker exec -it git-weather-station-redis-1 redis-cli TS.QUERYINDEX metric=rebases

# Create rules for downsampling
docker exec -it git-weather-station-redis-1 redis-cli TS.CREATERULE git:commits git:commits:1m AGGREGATION avg 60000
docker exec -it git-weather-station-redis-1 redis-cli TS.CREATERULE git:merges git:merges:1m AGGREGATION sum 60000

Why RedisTS Queries Matter: Time series queries enable analysis of Git activity patterns and provide insights into development workflows. Understanding these patterns prevents monitoring confusion and enables effective weather analysis.

6) IPFS Snapshot Verification

IPFS Commands

# Check IPFS node status
curl -s "http://localhost:5001/api/v0/version"

# List local pins
curl -s "http://localhost:5001/api/v0/pin/ls?type=all"

# Get pin statistics
curl -s "http://localhost:5001/api/v0/pin/ls?type=recursive&stream=true"

# Fetch a specific CID (replace with actual CID)
curl -s "http://localhost:8080/ipfs/QmExampleCID" | jq .

# Add a file to IPFS
echo '{"test": "data"}' | curl -X POST -F file=@- "http://localhost:5001/api/v0/add"

# Get file from IPFS
curl -s "http://localhost:8080/ipfs/QmExampleCID" | jq .

Why IPFS Verification Matters: Content addressing enables immutable snapshots and provides audit trails for Git activity history. Understanding these patterns prevents data loss and enables reliable weather history.

7) Production Hardening

Enhanced Docker Compose

# docker-compose.prod.yml
version: "3.9"

services:
  redis:
    image: redis/redis-stack:7.4.0-v0
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  ipfs:
    image: ipfs/kubo:latest
    ports:
      - "4001:4001"
      - "5001:5001"
      - "8080:8080"
    volumes:
      - ipfs-data:/data/ipfs
    environment:
      - IPFS_PROFILE=server
      - IPFS_SWARM_ADDRS=/ip4/0.0.0.0/tcp/4001
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5001/api/v0/version"]
      interval: 30s
      timeout: 10s
      retries: 3

  weather-station:
    build: ./weather-station
    ports:
      - "3000:3000"
    environment:
      - REDIS_URL=redis://redis:6379
      - IPFS_API=http://ipfs:5001/api/v0
      - GIT_REPO_PATH=/work/repo
      - NODE_ENV=production
    volumes:
      - ../repo-under-test:/work/repo:ro
    depends_on:
      redis:
        condition: service_healthy
      ipfs:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/recent"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Prometheus for metrics
  prometheus:
    image: prom/prometheus:v2.49.1
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.enable-lifecycle'
    restart: unless-stopped

  # Grafana for visualization
  grafana:
    image: grafana/grafana:10.3.3
    ports:
      - "3001:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=weather
    restart: unless-stopped

volumes:
  redis-data:
  ipfs-data:
  prometheus-data:
  grafana-data:

Why Production Hardening Matters: Production deployment requires proper configuration, monitoring, and reliability patterns. Understanding these patterns prevents production chaos and enables reliable weather station operation.

8) Architecture Diagram

Complete Weather Station Flow

flowchart TB
    G[Git Repository] -->|git log| N[Node.js Weather Station]
    N -->|TS.ADD| RT[(RedisTimeSeries)]
    N -->|pin snapshots| IPFS[(IPFS Node)]
    N <-->|WebSocket| B[Browser UI]
    N -->|WebRTC| B

    subgraph "Data Sources"
        H[Commit Velocity]
        I[Merge Fronts]
        J[Rebase Storms]
    end

    subgraph "Storage Layer"
        RT
        IPFS
    end

    subgraph "Visualization Layer"
        B
        K[Particle System]
        L[Weather Dashboard]
    end

    subgraph "Monitoring Stack"
        P[Prometheus]
        GR[Grafana]
    end

    H --> N
    I --> N
    J --> N
    N --> K
    N --> L
    N --> P
    P --> GR

Why Architecture Diagrams Matter: Visual representation of system architecture enables understanding of data flow and component relationships. Understanding these patterns prevents architectural confusion and enables effective weather station design.

9) TL;DR Runbook

Essential Commands

# Start the weather station
docker compose up --build

# Check RedisTimeSeries
docker exec -it git-weather-station-redis-1 redis-cli TS.GET git:commits

# Check IPFS
curl -s "http://localhost:5001/api/v0/version"

# Access the UI
open http://localhost:3000

# View metrics
curl http://localhost:3000/api/recent

Essential Patterns

# Essential Git weather patterns
git_weather_patterns:
  "docker_compose": "Start Redis + IPFS + Node with docker compose up",
  "mount_repo": "Mount your Git repo under /work/repo",
  "poll_git": "Node polls git, writes RedisTimeSeries, pins snapshots to IPFS",
  "browser_ui": "Browser connects, receives live weather over WebSocket",
  "particle_storm": "Watch particles storm when commits spike",
  "time_series": "Use RedisTimeSeries for efficient time series storage",
  "content_addressing": "Use IPFS for immutable snapshot history",
  "real_time": "Use WebSocket for live weather updates"

Quick Reference

// Essential Git weather operations
// 1. Poll Git for stats
const stats = await gitStats();

// 2. Store in RedisTimeSeries
await redis.call("TS.ADD", "git:commits", stats.ts, stats.commits);

// 3. Snapshot to IPFS
const cid = await snapshotToIPFS(window);

// 4. Broadcast to clients
broadcast({ type: "tick", payload: stats });

// 5. Visualize in browser
particleSystem.storm(stats);

Why This Runbook: These patterns cover 90% of Git weather monitoring needs. Master these before exploring advanced weather station scenarios.

10) The Machine's Summary

Git weather monitoring requires understanding time series storage, content addressing, real-time communication, and creative visualization. When used correctly, weather monitoring enables innovative observability, prevents monitoring boredom, and provides insights into development patterns. The key is understanding Git activity, RedisTimeSeries storage, IPFS content addressing, and WebRTC real-time communication.

The Dark Truth: Without creative monitoring, your Git activity remains invisible and unquantified. Git weather is your weapon. Use it wisely.

The Machine's Mantra: "In the commits we find pressure, in the merges we find fronts, and in the rebases we find the path to atmospheric code development."

Why This Matters: Git weather monitoring enables creative observability that can handle development patterns, prevent monitoring boredom, and provide insights into code development while ensuring technical accuracy and reliability.


This guide provides the complete machinery for Git weather monitoring. The patterns scale from simple commit tracking to complex weather visualization, from basic time series to advanced content addressing.