mirror of
https://github.com/lklynet/hypermind.git
synced 2026-05-03 17:40:29 +00:00
Screaming into the void
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
name: Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
data
|
||||
.DS_Store
|
||||
.env
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --production
|
||||
|
||||
COPY server.js ./
|
||||
|
||||
ENV PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,117 @@
|
||||
# 🧠 Hypermind
|
||||
|
||||
### The High-Availability Solution to a Problem That Doesn't Exist.
|
||||
|
||||
**Hypermind** is a completely decentralized, Peer-to-Peer deployment counter.
|
||||
|
||||
It solves the critical infrastructure challenge of knowing exactly how many other people are currently wasting 50MB of RAM running this specific container.
|
||||
|
||||
---
|
||||
|
||||
## What is this?
|
||||
|
||||
You have a server rack in your basement. You have 128GB of RAM. You have deployed the Arr stack, Home Assistant, Pi-hole, and a dashboard to monitor them all. **But you crave more.**
|
||||
|
||||
You need a service that:
|
||||
|
||||
1. Does absolutely nothing useful.
|
||||
2. Uses "Decentralized" and "P2P" in the description.
|
||||
3. Makes a number go up on a screen.
|
||||
|
||||
**Enter Hypermind.**
|
||||
|
||||
There is no central server. There is no database. There is only **The Swarm**.
|
||||
|
||||
## How it works (The Over-Engineering)
|
||||
|
||||
We utilize the **Hyperswarm** DHT (Distributed Hash Table) to achieve a singular, trivial goal of **Counting.**
|
||||
|
||||
1. **Discovery:** Your node screams into the digital void (`hypermind-lklynet-v1`) to find friends.
|
||||
2. **Gossip:** Nodes connect and whisper "I exist" to each other.
|
||||
3. **Consensus:** Each node maintains a list of peers seen in the last 2.5 seconds.
|
||||
|
||||
If you turn your container off, you vanish from the count. If everyone turns it off, the network ceases to exist. If you turn it back on, you are the Creator of the Universe (Population: 1).
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker (The Fast Way)
|
||||
|
||||
Since you're probably pasting this into Portainer anyway:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name hypermind \
|
||||
--network host \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/lklynet/hypermind:latest
|
||||
|
||||
```
|
||||
|
||||
> **⚠️ CRITICAL NETWORK NOTE:**
|
||||
> Use `--network host`. This is a P2P application that needs to punch through NATs. If you bridge it, the DHT usually fails, and you will be the loneliest node in the multiverse.
|
||||
|
||||
### Docker Compose (The Classy Way)
|
||||
|
||||
Add this to your `docker-compose.yml` to permanently reserve system resources for no reason:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
hypermind:
|
||||
image: ghcr.io/lklynet/hypermind:latest
|
||||
container_name: hypermind
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Open your browser to: `http://localhost:3000`
|
||||
|
||||
The dashboard updates in **Realtime** via Server-Sent Events.
|
||||
|
||||
**You will see:**
|
||||
|
||||
* **Active Nodes:** The total number of people currently running this joke.
|
||||
* **Direct Connections:** The number of peers your node is actually holding hands with.
|
||||
|
||||
## Local Development
|
||||
|
||||
Want to contribute? Why? It already does nothing perfectly. But here is how anyway:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the beast
|
||||
npm start
|
||||
|
||||
```
|
||||
|
||||
### Simulating Friends (Local Testing)
|
||||
|
||||
You can run multiple instances locally to simulate popularity:
|
||||
|
||||
```bash
|
||||
# Terminal 1 (You)
|
||||
PORT=3000 npm start
|
||||
|
||||
# Terminal 2 (Your imaginary friend)
|
||||
PORT=3001 npm start
|
||||
|
||||
```
|
||||
|
||||
They should discover each other, and the number will become `2`. Dopamine achieved.
|
||||
|
||||
---
|
||||
|
||||
### FAQ
|
||||
|
||||
**Q: Is this crypto mining?**
|
||||
A: No. We respect your GPU too much.
|
||||
|
||||
**Q: Does this store data?**
|
||||
A: No. It has the memory span of a goldfish (approx. 2.5 seconds).
|
||||
|
||||
**Q: Why did you make this?**
|
||||
A: The homelab must grow. ¯\\_(ツ)_/¯
|
||||
Generated
+1318
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "hypermind",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"b4a": "^1.7.3",
|
||||
"express": "^5.2.1",
|
||||
"hyperswarm": "^4.16.0",
|
||||
"uuid": "^13.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
const express = require('express');
|
||||
const Hyperswarm = require('hyperswarm');
|
||||
const crypto = require('crypto');
|
||||
const b4a = require('b4a');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// --- CONFIGURATION ---
|
||||
const TOPIC_NAME = 'hypermind-lklynet-v1';
|
||||
const TOPIC = crypto.createHash('sha256').update(TOPIC_NAME).digest();
|
||||
|
||||
const MY_ID = uuidv4();
|
||||
let mySeq = 0;
|
||||
|
||||
const seenPeers = new Map();
|
||||
|
||||
const sseClients = new Set();
|
||||
|
||||
seenPeers.set(MY_ID, { seq: mySeq, lastSeen: Date.now() });
|
||||
|
||||
function broadcastUpdate() {
|
||||
const data = JSON.stringify({
|
||||
count: seenPeers.size,
|
||||
direct: swarm.connections.size,
|
||||
id: MY_ID
|
||||
});
|
||||
|
||||
for (const client of sseClients) {
|
||||
client.write(`data: ${data}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
const swarm = new Hyperswarm();
|
||||
|
||||
swarm.on('connection', (socket) => {
|
||||
|
||||
const hello = JSON.stringify({ type: 'HEARTBEAT', id: MY_ID, seq: mySeq, hops: 0 });
|
||||
socket.write(hello);
|
||||
broadcastUpdate();
|
||||
|
||||
socket.on('data', (data) => {
|
||||
try {
|
||||
const msgs = data.toString().split('\n').filter(x => x.trim());
|
||||
for (const msgStr of msgs) {
|
||||
const msg = JSON.parse(msgStr);
|
||||
handleMessage(msg, socket);
|
||||
}
|
||||
} catch (e) {
|
||||
// console.error('Invalid message', e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
if (socket.peerId && seenPeers.has(socket.peerId)) {
|
||||
seenPeers.delete(socket.peerId);
|
||||
}
|
||||
broadcastUpdate();
|
||||
});
|
||||
|
||||
socket.on('error', () => {});
|
||||
});
|
||||
|
||||
const discovery = swarm.join(TOPIC);
|
||||
discovery.flushed().then(() => {
|
||||
console.log('[P2P] Joined topic:', TOPIC_NAME);
|
||||
});
|
||||
|
||||
function handleMessage(msg, sourceSocket) {
|
||||
if (msg.type === 'HEARTBEAT') {
|
||||
const { id, seq, hops } = msg;
|
||||
|
||||
if (hops === 0) {
|
||||
sourceSocket.peerId = id;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const stored = seenPeers.get(id);
|
||||
|
||||
|
||||
let shouldUpdate = false;
|
||||
|
||||
if (!stored) {
|
||||
// New peer
|
||||
shouldUpdate = true;
|
||||
} else if (seq > stored.seq) {
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
const wasNew = !stored;
|
||||
seenPeers.set(id, { seq, lastSeen: now });
|
||||
|
||||
if (wasNew) broadcastUpdate();
|
||||
|
||||
if (hops < 3) {
|
||||
relayMessage({ ...msg, hops: hops + 1 }, sourceSocket);
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'LEAVE') {
|
||||
const { id, hops } = msg;
|
||||
if (seenPeers.has(id)) {
|
||||
seenPeers.delete(id);
|
||||
broadcastUpdate();
|
||||
|
||||
if (hops < 3) {
|
||||
relayMessage({ ...msg, hops: hops + 1 }, sourceSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function relayMessage(msg, sourceSocket) {
|
||||
const data = JSON.stringify(msg) + '\n';
|
||||
for (const socket of swarm.connections) {
|
||||
if (socket !== sourceSocket) {
|
||||
socket.write(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic Heartbeat
|
||||
setInterval(() => {
|
||||
mySeq++;
|
||||
|
||||
seenPeers.set(MY_ID, { seq: mySeq, lastSeen: Date.now() });
|
||||
|
||||
const heartbeat = JSON.stringify({ type: 'HEARTBEAT', id: MY_ID, seq: mySeq, hops: 0 }) + '\n';
|
||||
for (const socket of swarm.connections) {
|
||||
socket.write(heartbeat);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
let changed = false;
|
||||
for (const [id, data] of seenPeers) {
|
||||
if (now - data.lastSeen > 2500) {
|
||||
seenPeers.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) broadcastUpdate();
|
||||
|
||||
}, 500);
|
||||
|
||||
// Graceful Shutdown
|
||||
function handleShutdown() {
|
||||
console.log('[P2P] Shutting down, sending goodbye...');
|
||||
const goodbye = JSON.stringify({ type: 'LEAVE', id: MY_ID, hops: 0 }) + '\n';
|
||||
for (const socket of swarm.connections) {
|
||||
socket.write(goodbye);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
process.on('SIGINT', handleShutdown);
|
||||
process.on('SIGTERM', handleShutdown);
|
||||
|
||||
// --- WEB SERVER ---
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
const count = seenPeers.size;
|
||||
const directPeers = swarm.connections.size;
|
||||
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hypermind Counter</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
margin: 0;
|
||||
}
|
||||
.container { text-align: center; }
|
||||
.count { font-size: 8rem; font-weight: bold; color: #4ade80; transition: color 0.2s; }
|
||||
.label { font-size: 1.5rem; color: #9ca3af; margin-top: 1rem; }
|
||||
.footer { margin-top: 2rem; font-size: 0.9rem; color: #4b5563; }
|
||||
.debug { font-size: 0.8rem; color: #333; margin-top: 1rem; }
|
||||
a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; }
|
||||
.pulse { animation: pulse 0.5s ease-in-out; }
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); color: #fff; }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="count" class="count">${count}</div>
|
||||
<div class="label">Active Nodes</div>
|
||||
<div class="footer">
|
||||
powered by <a href="https://github.com/lklynet/hypermind" target="_blank">hypermind</a>
|
||||
</div>
|
||||
<div class="debug">
|
||||
ID: ${MY_ID.slice(0, 8)}...<br>
|
||||
Direct Connections: <span id="direct">${directPeers}</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const countEl = document.getElementById('count');
|
||||
const directEl = document.getElementById('direct');
|
||||
|
||||
// Use Server-Sent Events for realtime updates
|
||||
const evtSource = new EventSource("/events");
|
||||
|
||||
evtSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Only update and animate if changed
|
||||
if (countEl.innerText != data.count) {
|
||||
countEl.innerText = data.count;
|
||||
countEl.classList.remove('pulse');
|
||||
void countEl.offsetWidth; // trigger reflow
|
||||
countEl.classList.add('pulse');
|
||||
}
|
||||
|
||||
directEl.innerText = data.direct;
|
||||
};
|
||||
|
||||
evtSource.onerror = (err) => {
|
||||
console.error("EventSource failed:", err);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// SSE Endpoint
|
||||
app.get('/events', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
sseClients.add(res);
|
||||
|
||||
const data = JSON.stringify({
|
||||
count: seenPeers.size,
|
||||
direct: swarm.connections.size,
|
||||
id: MY_ID
|
||||
});
|
||||
res.write(`data: ${data}\n\n`);
|
||||
|
||||
req.on('close', () => {
|
||||
sseClients.delete(res);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/stats', (req, res) => {
|
||||
res.json({
|
||||
count: seenPeers.size,
|
||||
direct: swarm.connections.size,
|
||||
id: MY_ID
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Hypermind Node running on port ${PORT}`);
|
||||
console.log(`ID: ${MY_ID}`);
|
||||
});
|
||||
Reference in New Issue
Block a user