From 153db905859b34a1ff40b27b5508cccd9551b473 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 5 Jan 2026 02:18:30 +0300 Subject: [PATCH] docs: add human-readable documentation - Update README with chat, map features and documentation links - Add docs/ARCHITECTURE.md explaining system internals - Add docs/API.md with endpoint reference and integration examples - Add docs/TROUBLESHOOTING.md for common issues --- README.md | 55 +++++++++- docs/API.md | 207 ++++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 227 ++++++++++++++++++++++++++++++++++++++++ docs/TROUBLESHOOTING.md | 202 +++++++++++++++++++++++++++++++++++ 4 files changed, 689 insertions(+), 2 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/TROUBLESHOOTING.md diff --git a/README.md b/README.md index fe6d741..afeb480 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,21 @@ You need a service that: There is no central server. There is no database. There is only **The Swarm**. +## Β» Features + +- **πŸ”’ Live Counter**: See how many nodes are running worldwide +- **πŸ—ΊοΈ Peer Map**: Visualize node locations on an interactive world map +- **πŸ’¬ Ephemeral Chat**: Send fleeting messages that vanish when you restart +- **πŸ“Š Diagnostics**: Watch message flows, bandwidth, and peer health +- **✨ Particle Visualization**: Because numbers alone are boring + ## Β» How it works 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. +3. **Consensus:** Each node maintains a list of peers seen in the last 15 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). @@ -67,6 +75,7 @@ services: restart: unless-stopped environment: - PORT=3000 + - ENABLE_CHAT=true # Optional: enable ephemeral chat ``` @@ -126,6 +135,7 @@ See detailed [instructions](https://gethomepage.dev/configs/services/#icons). | --- | --- | --- | | `PORT` | `3000` | The port the web dashboard listens on. Since `--network host` is used, this port opens directly on the host. | | `MAX_PEERS` | `1000000` | Maximum number of peers to track in the swarm. Unless you're expecting the entire internet to join, the default is probably fine. | +| `ENABLE_CHAT` | `false` | Enable ephemeral P2P chat. Messages exist only in memory and vanish when nodes restart. | ## Β» Usage @@ -137,6 +147,18 @@ The dashboard updates in **Realtime** via Server-Sent Events. * **Active Nodes:** The total number of people currently running this joke. * **Direct Connections:** The number of peers your node is actually holding hands with. +* **Peer Map:** Click "map" to see where your fellow nodes are located (approximately). +* **Diagnostics:** Click "diagnostics" for the nerdy details. + +### Chat (Optional) + +When `ENABLE_CHAT=true`, a terminal-style chat panel appears. You can: + +- Set a nickname (stored locally, max 16 chars) +- Send short messages (max 140 chars, because constraints breed creativity) +- Watch messages appear from across the swarm + +Messages are rate-limited (2s cooldown, 5 per 30s) and disappear when you restart. It's like Snapchat, but for infrastructure nerds. ## Β» Local Development @@ -149,6 +171,9 @@ npm install # Run the beast npm start +# With chat enabled +ENABLE_CHAT=true npm start + ``` ### Simulating Friends (Local Testing) @@ -166,6 +191,29 @@ PORT=3001 npm start They should discover each other, and the number will become `2`. Dopamine achieved. +### Testing in WSL2 / Docker Desktop + +DHT discovery sometimes fails in virtualized environments. Use the TCP relay: + +```bash +# Terminal 1 - Start relay server +node relay-server.js + +# Terminal 2 - Node 1 +RELAY_PORT=4000 ENABLE_CHAT=true PORT=3000 node server.js + +# Terminal 3 - Node 2 +RELAY_PORT=4000 ENABLE_CHAT=true PORT=3001 node server.js +``` + +--- + +## Β» Documentation + +- [Architecture Guide](docs/ARCHITECTURE.md) β€” How it works under the hood +- [API Reference](docs/API.md) β€” Endpoints for integration +- [Troubleshooting](docs/TROUBLESHOOTING.md) β€” When things go wrong + --- ### FAQ @@ -174,11 +222,14 @@ They should discover each other, and the number will become `2`. Dopamine achiev A: No. We respect your GPU too much. **Q: Does this store data?** -A: No. It has the short-term working memory of a honeybee (approx. 2.5 seconds). Which is biologically accurate and thematically consistent. +A: No. It has the short-term working memory of a honeybee (approx. 15 seconds). Which is biologically accurate and thematically consistent. **Q: Why did you make this?** A: The homelab must grow. Β―\\_(ツ)_/Β― +**Q: Is the chat secure?** +A: Messages are signed but not encrypted. Don't share secrets. It's gossip, literally. + ## Β» Star History!! diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..3db904c --- /dev/null +++ b/docs/API.md @@ -0,0 +1,207 @@ +# API Reference + +Hypermind exposes a simple HTTP API for monitoring and integration. + +## Endpoints + +### GET `/` + +Returns the HTML dashboard. + +### GET `/api/stats` + +Returns current node statistics as JSON. + +**Response:** + +```json +{ + "count": 42, + "totalUnique": 150, + "direct": 5, + "id": "302a300506032b6570032100...", + "diagnostics": { + "heartbeatsReceived": 100, + "heartbeatsRelayed": 80, + "invalidPoW": 0, + "duplicateSeq": 5, + "invalidSig": 0, + "newPeersAdded": 3, + "bytesReceived": 15000, + "bytesRelayed": 12000, + "leaveMessages": 1 + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `count` | number | Current active peer count | +| `totalUnique` | number | Estimated total unique peers ever seen | +| `direct` | number | Number of direct connections | +| `id` | string | This node's identity (hex-encoded public key) | +| `diagnostics` | object | Metrics from the last 10-second interval | + +**Diagnostics Fields:** + +| Field | Description | +|-------|-------------| +| `heartbeatsReceived` | HEARTBEAT messages received | +| `heartbeatsRelayed` | Messages forwarded to other peers | +| `invalidPoW` | Messages rejected for bad Proof-of-Work | +| `duplicateSeq` | Messages rejected for stale sequence number | +| `invalidSig` | Messages rejected for bad signature | +| `newPeersAdded` | New unique peers discovered | +| `bytesReceived` | Total bytes received | +| `bytesRelayed` | Total bytes forwarded | +| `leaveMessages` | LEAVE messages processed | + +### GET `/events` + +Server-Sent Events stream for real-time updates. + +**Event Format:** + +``` +data: {"count":42,"totalUnique":150,"direct":5,...} + +data: {"type":"CHAT","sender":"302a...","nick":"alice","content":"hello","timestamp":1704326400000} + +data: {"type":"SYSTEM","content":"Connection established with Node ...abc12345","timestamp":1704326400000} +``` + +**Event Types:** + +1. **Stats Update**: Regular payload with `count`, `direct`, etc. +2. **Chat Message**: Has `type: "CHAT"` with message content +3. **System Message**: Has `type: "SYSTEM"` for connect/disconnect events + +**Usage Example (JavaScript):** + +```javascript +const events = new EventSource('/events'); + +events.onmessage = (event) => { + const data = JSON.parse(event.data); + + if (data.type === 'CHAT') { + console.log(`${data.nick || data.sender}: ${data.content}`); + } else if (data.type === 'SYSTEM') { + console.log(`[SYSTEM] ${data.content}`); + } else { + console.log(`Peers: ${data.count}, Direct: ${data.direct}`); + } +}; +``` + +### POST `/api/chat` + +Send a chat message (requires `ENABLE_CHAT=true`). + +**Request:** + +```json +{ + "msg": "Hello, swarm!", + "nick": "alice" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `msg` | string | Yes | Message content (max 140 chars) | +| `nick` | string | No | Nickname (max 16 chars, alphanumeric) | + +**Response (Success):** + +```json +{ + "success": true +} +``` + +**Response (Rate Limited):** + +```json +{ + "error": "Rate limited. Please wait." +} +``` + +**Response (Chat Disabled):** + +```json +{ + "error": "Chat is disabled" +} +``` + +**Rate Limits:** +- Minimum 2 seconds between messages +- Maximum 5 messages per 30-second window + +## Integration Examples + +### Homepage Dashboard Widget + +Add to your `services.yaml`: + +```yaml +- Hypermind: + icon: /icons/hypermind2.png + href: http://YOUR_IP:3000 + widget: + type: customapi + url: http://YOUR_IP:3000/api/stats + method: GET + mappings: + - field: count + label: Swarm Size + - field: direct + label: Direct Peers +``` + +### Home Assistant Sensor + +```yaml +sensor: + - platform: rest + name: Hypermind Swarm Size + resource: http://YOUR_IP:3000/api/stats + value_template: "{{ value_json.count }}" + json_attributes: + - direct + - totalUnique + scan_interval: 30 +``` + +### Shell Script Monitoring + +```bash +#!/bin/bash +while true; do + stats=$(curl -s http://localhost:3000/api/stats) + count=$(echo $stats | jq .count) + direct=$(echo $stats | jq .direct) + echo "$(date): Peers=$count Direct=$direct" + sleep 10 +done +``` + +### Python Integration + +```python +import requests +import sseclient + +# One-time stats fetch +stats = requests.get('http://localhost:3000/api/stats').json() +print(f"Current peers: {stats['count']}") + +# Real-time monitoring +response = requests.get('http://localhost:3000/events', stream=True) +client = sseclient.SSEClient(response) +for event in client.events(): + data = json.loads(event.data) + print(f"Peers: {data.get('count', 'N/A')}") +``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..9d8d7c1 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,227 @@ +# Hypermind Architecture + +This document explains how Hypermind works under the hood, for developers who want to contribute or understand the system. + +## Overview + +Hypermind is a decentralized peer counter built on [Hyperswarm](https://github.com/holepunchto/hyperswarm), a DHT-based peer discovery network. Nodes discover each other, exchange heartbeats, and maintain a local count of active peers. + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Your Node β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Identity β”‚ P2P Layer β”‚ State β”‚ Web Layer β”‚ +β”‚ (Ed25519) β”‚ (Swarm) β”‚ (Peers) β”‚ (Express + SSE) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Hyperswarm DHT β”‚ + β”‚ (The Internet) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Directory Structure + +``` +hypermind/ +β”œβ”€β”€ server.js # Entry point - wires everything together +β”œβ”€β”€ relay-server.js # TCP relay for local testing +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ └── constants.js # All configuration values +β”‚ β”œβ”€β”€ core/ +β”‚ β”‚ β”œβ”€β”€ identity.js # Ed25519 keypair + PoW mining +β”‚ β”‚ └── security.js # Signature verification +β”‚ β”œβ”€β”€ p2p/ +β”‚ β”‚ β”œβ”€β”€ swarm.js # Hyperswarm connection management +β”‚ β”‚ β”œβ”€β”€ messaging.js # Message validation & handlers +β”‚ β”‚ └── relay.js # Gossip message forwarding +β”‚ β”œβ”€β”€ state/ +β”‚ β”‚ β”œβ”€β”€ peers.js # Peer tracking (LRU + HyperLogLog) +β”‚ β”‚ β”œβ”€β”€ bloom.js # Deduplication filter +β”‚ β”‚ β”œβ”€β”€ diagnostics.js # Metrics collection +β”‚ β”‚ β”œβ”€β”€ ratelimit.js # Chat rate limiting +β”‚ β”‚ β”œβ”€β”€ lru.js # LRU cache implementation +β”‚ β”‚ └── hyperloglog.js # Cardinality estimator +β”‚ └── web/ +β”‚ β”œβ”€β”€ server.js # Express app factory +β”‚ β”œβ”€β”€ routes.js # HTTP endpoints +β”‚ └── sse.js # Server-Sent Events manager +└── public/ + β”œβ”€β”€ index.html # Dashboard template + β”œβ”€β”€ app.js # Frontend JavaScript + └── style.css # Styling +``` + +## Startup Flow + +When you run `npm start`: + +1. **Generate Identity**: Create Ed25519 keypair, mine a Proof-of-Work nonce +2. **Initialize Managers**: PeerManager, DiagnosticsManager, SSEManager +3. **Add Self**: Register this node in the peer list +4. **Start Swarm**: Join the Hyperswarm DHT topic +5. **Begin Heartbeat Loop**: Announce presence every 5 seconds +6. **Start Web Server**: Listen for HTTP connections + +## Core Components + +### Identity (`src/core/identity.js`) + +Each node generates a unique Ed25519 keypair at startup. The public key (in DER format, hex-encoded) becomes the node's identity. + +A Proof-of-Work nonce is mined by finding a number that, when combined with the identity and hashed with SHA-256, produces a hash starting with `0000`. This makes creating fake identities computationally expensive. + +### Swarm Manager (`src/p2p/swarm.js`) + +Manages connections to other nodes: + +- **Discovery**: Joins the DHT topic `sha256("hypermind-lklynet-v1")` +- **Heartbeat**: Sends HEARTBEAT messages every 5 seconds +- **Connection Rotation**: Prevents hoarding connections (max 32, rotates oldest) +- **Graceful Shutdown**: Sends LEAVE message to all peers before exit + +### Peer Manager (`src/state/peers.js`) + +Tracks known peers: + +- **LRU Cache**: Stores up to 1 million peers, evicts least-recently-seen +- **Stale Cleanup**: Removes peers not seen in 15 seconds +- **HyperLogLog**: Estimates total unique peers ever seen (~3% error) + +### Message Handler (`src/p2p/messaging.js`) + +Validates and processes incoming messages: + +1. Parse JSON +2. Validate message structure +3. Verify Proof-of-Work +4. Check sequence number (prevent replays) +5. Verify Ed25519 signature +6. Update peer state +7. Relay to other peers (if not already seen) + +### Bloom Filter (`src/state/bloom.js`) + +Prevents relaying the same message multiple times: + +- Two rotating filters (current + previous) +- Rotates every 30 seconds +- Messages are only relayed if not in either filter + +## Message Protocol + +All messages are newline-delimited JSON sent over TCP connections. + +### HEARTBEAT + +Sent every 5 seconds to announce "I'm alive": + +```json +{ + "type": "HEARTBEAT", + "id": "302a300506032b6570...", + "seq": 42, + "hops": 0, + "nonce": 12345, + "sig": "a1b2c3..." +} +``` + +- `seq`: Incrementing counter (for replay protection) +- `hops`: How many relays this message has passed through +- `sig`: Signature of `seq:42` + +### LEAVE + +Sent on graceful shutdown: + +```json +{ + "type": "LEAVE", + "id": "302a300506032b6570...", + "hops": 0, + "sig": "a1b2c3..." +} +``` + +- `sig`: Signature of `type:LEAVE:${id}` + +### CHAT + +Ephemeral chat message (when enabled): + +```json +{ + "type": "CHAT", + "id": "302a300506032b6570...", + "nick": "alice", + "msg": "hello world", + "ts": 1704326400000, + "hops": 0, + "nonce": 12345, + "sig": "a1b2c3..." +} +``` + +- `nick`: Optional nickname (max 16 chars) +- `msg`: Message content (max 140 chars) +- `ts`: Timestamp for deduplication +- `sig`: Signature of `chat:${msg}:${ts}` + +## Gossip Relay + +Messages propagate through the network via gossip: + +1. Node A sends HEARTBEAT to Node B +2. Node B validates, updates state +3. Node B increments `hops` and forwards to Nodes C, D, E... +4. Each node only relays if `hops < 2` and not in bloom filter + +This ensures messages reach the entire network without flooding. + +## Web Dashboard + +The dashboard (`public/index.html`) shows: + +- **Active Nodes**: Current peer count +- **Direct Connections**: Peers you're directly connected to +- **Particle Visualization**: Animated particles representing peers +- **Diagnostics Modal**: Detailed metrics +- **Peer Map**: Geographic visualization (when enabled) +- **Chat Terminal**: Ephemeral P2P chat (when enabled) + +Updates arrive via Server-Sent Events (SSE) at `/events`. + +## Configuration + +All tunable values are in `src/config/constants.js`: + +| Constant | Default | Description | +|----------|---------|-------------| +| `HEARTBEAT_INTERVAL` | 5000ms | How often to announce | +| `PEER_TIMEOUT` | 15000ms | When to consider a peer stale | +| `MAX_CONNECTIONS` | 32 | Maximum direct connections | +| `MAX_PEERS` | 1000000 | Maximum peers to track | +| `MAX_RELAY_HOPS` | 2 | Maximum gossip relay depth | + +## Security + +- **Identity**: Ed25519 keypairs (cryptographically secure) +- **Proof-of-Work**: Prevents Sybil attacks +- **Signatures**: All messages are signed +- **Replay Protection**: Sequence numbers prevent replay attacks +- **No Persistence**: Nothing is stored; restart = fresh start + +## Contributing + +When modifying the codebase: + +1. **Don't change the TOPIC hash** - This would isolate your node +2. **Don't change the POW_PREFIX** - Would cause network split +3. **Don't change signature formats** - Would break message validation +4. **Test with 2+ nodes locally** before submitting changes + +See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..7665607 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,202 @@ +# Troubleshooting Guide + +Common issues and how to fix them. + +## Node Discovery Issues + +### Count stuck at 1 + +Your node isn't finding others on the DHT. + +**Causes:** +- Running in Docker without `--network host` +- Firewall blocking UDP/TCP +- NAT not supporting hole-punching +- Corporate network restrictions + +**Solutions:** + +1. **Docker users**: Always use `--network host`: + ```bash + docker run --network host -e PORT=3000 ghcr.io/lklynet/hypermind:latest + ``` + +2. **Check firewall**: Hyperswarm needs UDP + TCP access to random high ports + +3. **Use TCP relay for local testing**: + ```bash + # Terminal 1 + node relay-server.js + + # Terminal 2 + RELAY_PORT=4000 PORT=3000 node server.js + + # Terminal 3 + RELAY_PORT=4000 PORT=3001 node server.js + ``` + +### No direct connections (direct: 0) + +You're seeing peers but not directly connected to any. + +**Cause:** All peers are discovered through gossip relay, not DHT. + +**Solution:** This is normal if you have a strict NAT. As long as `count` is > 1, the network is working. + +## Chat Issues + +### Chat not visible + +**Cause:** Chat is disabled by default. + +**Solution:** Enable it with the environment variable: +```bash +ENABLE_CHAT=true PORT=3000 npm start +``` + +### Chat messages not sending + +**Causes:** +- Rate limited (2s cooldown, max 5 messages per 30s) +- Not connected to any peers +- Message too long (max 140 chars) + +**Solutions:** +1. Wait for the cooldown timer in the UI +2. Check that `direct` > 0 in stats +3. Keep messages under 140 characters + +### Chat messages not received + +**Cause:** Nodes aren't connected to each other. + +**Solution:** Use TCP relay for reliable local testing: +```bash +RELAY_PORT=4000 ENABLE_CHAT=true PORT=3000 node server.js +``` + +## Dashboard Issues + +### Dashboard won't load + +**Causes:** +- Wrong port +- Node crashed +- Port already in use + +**Solutions:** +1. Check the console output for the actual port +2. Check for errors in terminal +3. Try a different port: `PORT=3001 npm start` + +### Count not updating + +**Cause:** SSE connection dropped. + +**Solution:** Refresh the page. SSE auto-reconnects, but sometimes a refresh helps. + +### Diagnostics modal empty + +**Cause:** Metrics haven't been collected yet. + +**Solution:** Wait 10 seconds for the first diagnostics interval. + +## Performance Issues + +### High memory usage + +**Cause:** Too many peers tracked. + +**Solution:** Lower the max peers limit: +```bash +MAX_PEERS=10000 npm start +``` + +### High CPU usage + +**Causes:** +- Proof-of-Work mining at startup (normal, lasts ~1 second) +- Processing many messages + +**Solution:** If CPU stays high after startup, check for message floods in diagnostics. + +## Docker Issues + +### Container exits immediately + +**Causes:** +- Missing `--network host` +- Port conflict + +**Solutions:** +```bash +# Correct way +docker run --network host -e PORT=3000 ghcr.io/lklynet/hypermind:latest + +# Check logs +docker logs hypermind +``` + +### Can't access dashboard from other machines + +**Cause:** Firewall on host machine. + +**Solution:** Open the port (e.g., 3000) in your host firewall. + +## Development Issues + +### Environment variables not working with npm + +**Cause:** Some shells don't pass env vars through npm. + +**Solution:** Run node directly: +```bash +# Instead of +PORT=3000 npm start + +# Use +PORT=3000 node server.js +``` + +### Tests failing + +**Cause:** Node version mismatch. + +**Solution:** Use Node.js 18 or later: +```bash +node --version # Should be v18+ +``` + +## Diagnostic Commands + +Check if your node is healthy: + +```bash +# Get stats +curl http://localhost:3000/api/stats | jq + +# Expected output: +{ + "count": 42, + "totalUnique": 150, + "direct": 5, + "id": "302a...", + "diagnostics": { ... } +} +``` + +Key metrics to check: +- `count`: Should be > 1 if network is healthy +- `direct`: Should be > 0 for best performance +- `diagnostics.invalidPoW`: Should be low (< 10% of heartbeats) +- `diagnostics.invalidSig`: Should be 0 + +## Getting Help + +If none of the above helps: + +1. Check the [GitHub Issues](https://github.com/lklynet/hypermind/issues) +2. Open a new issue with: + - Your environment (OS, Node version, Docker version) + - Output of `curl http://localhost:3000/api/stats` + - Any error messages from the console