diff --git a/README.md b/README.md index 02e634a..f94b96d 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,22 @@ Add this to your `services.yaml`: To get the icon to work, you have to add the icon to `/app/public/icons`. See detailed [instructions](https://gethomepage.dev/configs/services/#icons). + +### API Documentation + +For developers who are not satisfied in just looking a number on the screen but are craving to add new features, a comprehensive development API documentation is available in [`devdocs/API.md`](devdocs/API.md). + +**Available Endpoints:** +- `GET /api/stats` - Current node statistics +- `POST /api/chat` - Send P2P chat messages +- `GET /api/github/latest-release` - Latest release information +- `GET /events` - Server-Sent Events stream for real-time updates +- `GET /js/lists.js` - Dynamic adjectives/nouns for screenname generation +- `GET /js/screenname.js` - Screenname generation logic +- `GET /` - Main HTML page + +The documentation includes security best practices, rate limiting guidelines, modular route structure, and examples for creating new endpoints. +
diff --git a/devdocs/API.md b/devdocs/API.md new file mode 100644 index 0000000..78e91a4 --- /dev/null +++ b/devdocs/API.md @@ -0,0 +1,209 @@ +# API Documentation + +HTTP endpoints for Hypermind integration and development. + +## Endpoints + +You can test all the available endpoints running: `nude test-api.js`. + +
+GET /api/stats + +Returns node statistics and swarm information. + +```json +{ + "count": 42, + "totalUnique": 1337, + "direct": 8, + "id": "abc123...", + "screenname": "BraveElephant", + "diagnostics": {...}, + "chatEnabled": true, + "mapEnabled": true, + "peers": [...] +} +``` + +
+ +
+POST /api/chat + +Send P2P chat message. Rate limited: 5 messages per 5 seconds. + +```json +{ + "content": "Hello, world!", + "scope": "GLOBAL", + "target": null +} +``` + +
+ +
+GET /api/github/latest-release + +Latest GitHub release information. + +```json +{ + "tag_name": "v1.2.3", + "html_url": "https://github.com/lklynet/hypermind/releases/tag/v1.2.3", + "published_at": "2026-01-08T12:00:00Z", + "body": "Release notes..." +} +``` + +
+ +
+GET /events + +Server-Sent Events stream for real-time updates. Returns same data as `/api/stats`. + +
+ +
+GET /js/lists.js + +Dynamic JavaScript file containing adjectives and nouns for screenname generation. + +
+ +
+GET /js/screenname.js + +Dynamic JavaScript file with screenname generation logic for browser. + +
+ +
+GET / + +Main HTML page with server-side template rendering. + +
+ +## Development + +### Route Structure + +Routes are modular in `src/web/routes/`: + +``` +src/web/routes/ +├── your-new-route.js +``` + +### Creating Routes + +
+1. Create module + +`src/web/routes/your-new-route.js`: + +```javascript +const setupYourNewRouteRoutes = (router, dependencies) => { + const { identity, peerManager } = dependencies; + + router.get("/api/your-new-route", (req, res) => { + res.json({ success: true }); + }); +}; + +module.exports = { setupYourNewRouteRoutes }; +``` + +
+ +
+2. Register route + +In `src/web/routes.js`: + +```javascript +const { setupYourNewRouteRoutes } = require("./routes/your-new-route"); + +const yourNewRouteDeps = { identity, peerManager }; +setupYourNewRouteRoutes(app, yourNewRouteDeps); +``` + +
+ +
+3. Add constants + +In `src/config/constants.js`: + +```javascript +const YOUR_CONFIG = { + enabled: process.env.ENABLE_YOUR_FEATURE === "true", + rateLimit: parseInt(process.env.YOUR_RATE_LIMIT) || 1000, +}; + +module.exports = { YOUR_CONFIG }; +``` + +
+ +### Security + +
+Rate limiting + +```javascript +let requestHistory = []; + +router.get("/api/your-new-route", (req, res) => { + const now = Date.now(); + requestHistory = requestHistory.filter((time) => now - time < 10000); + + if (requestHistory.length >= 5) { + return res.status(429).json({ error: "Rate limit exceeded" }); + } + + requestHistory.push(now); +}); +``` + +
+ +
+Input validation + +```javascript +router.post("/api/your-new-route", (req, res) => { + const { data } = req.body; + + if (!data || typeof data !== "string" || data.length > 1000) { + return res.status(400).json({ error: "Invalid data" }); + } +}); +``` + +
+ +
+Error handling + +```javascript +router.get("/api/your-new-route", async (req, res) => { + try { + const result = await someOperation(); + res.json({ success: true, data: result }); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ error: "Operation failed" }); + } +}); +``` + +
+ +### Testing + +```bash +curl http://localhost:3000/api/your-new-route +``` diff --git a/devdocs/CHAT-COMMANDS.md b/devdocs/CHAT-COMMANDS.md new file mode 100644 index 0000000..2878585 --- /dev/null +++ b/devdocs/CHAT-COMMANDS.md @@ -0,0 +1,25 @@ +# Chat Commands + +Chat commands are modular and located in `public/js/chat-commands/`. + +## Adding a Command + +1. Create `chat-commands/mycommand.js`: +```javascript +export const myCommand = { + description: "What it does", + execute: () => { + const output = document.getElementById("terminal-output"); + // implementation + }, +}; +``` + +2. Import and register in `commands.js`: +```javascript +import { myCommand } from "./chat-commands/mycommand.js"; + +const actions = { + // ... existing commands + "/mycommand": myCommand, +}; \ No newline at end of file diff --git a/public/app.js b/public/app.js index 813b989..5397171 100644 --- a/public/app.js +++ b/public/app.js @@ -479,7 +479,7 @@ const appendMessage = (msg) => { senderName = msg.sender.slice(-8); } - // Update name map + // Update name map (before changing senderName to "You") nameToId.set(senderName, msg.sender); if (msg.sender === myId) senderName = "You"; @@ -504,7 +504,7 @@ const appendMessage = (msg) => { const contentSpan = document.createElement("span"); contentSpan.className = "msg-content"; - // Use formatMessage for rich text rendering + const rawContent = ` > ${msg.content}`; if (window.ChatCommands) { contentSpan.innerHTML = window.ChatCommands.formatMessage(rawContent); @@ -536,13 +536,24 @@ terminalInput.addEventListener("keypress", async (e) => { if (window.ChatCommands) { const result = window.ChatCommands.processInput(content); if (result.type === "action") { - window.ChatCommands.actions[result.command].execute(); + const action = window.ChatCommands.actions[result.command]; + if (action && typeof action.execute === "function") { + try { + action.execute(); + } catch (err) { + console.error("Command execution error:", err); + systemStatusBar.innerText = `[SYSTEM] Command failed: ${result.command}`; + } + } else { + systemStatusBar.innerText = `[SYSTEM] Unknown command: ${result.command}`; + } return; } else if (result.type === "text") { content = result.content; } } + let scope = "GLOBAL"; let target = null; @@ -607,10 +618,11 @@ terminalInput.addEventListener("keypress", async (e) => { target = nameToId.get(potentialName); content = msg; scope = "GLOBAL"; + } else if (!msg) { + systemStatusBar.innerText = `[SYSTEM] Usage: /${potentialName} `; + return; } else { - // If it looks like a command but we don't recognize the user or command - // Prevent sending it as raw text to chat - systemStatusBar.innerText = `[SYSTEM] Unknown command or user: ${potentialName}`; + systemStatusBar.innerText = `[SYSTEM] Unknown user: ${potentialName}`; return; } } @@ -818,4 +830,6 @@ function cycleTheme() { } } +window.cycleTheme = cycleTheme; + document.getElementById("theme-switcher").addEventListener("click", cycleTheme); diff --git a/public/index.html b/public/index.html index 869913b..1bc8b04 100644 --- a/public/index.html +++ b/public/index.html @@ -1,81 +1,76 @@ - - Hypermind - - - - - - - - - - - - - -
-
- {{COUNT}} -
-
Active Nodes
- -
- ID: {{ID}}
- Direct Connections: {{DIRECT}}
- Total Unique: {{TOTAL_UNIQUE}}
- diagnostics - - | - map -
-
- + + Hypermind + + + + + + + + + + + + -