feat: docker and nomad config

This commit is contained in:
2026-04-03 14:46:27 +02:00
parent 576af715bd
commit f082a30b7f
7 changed files with 271 additions and 38 deletions

View File

@@ -4,7 +4,7 @@ from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from pages import router as pages_router
from pages import router as pages_router, ensure_default_pages
from graph import router as graph_router
from docker_utils import router as docker_router
from converter import router as converter_router
@@ -17,6 +17,11 @@ app.include_router(graph_router, prefix="/api")
app.include_router(docker_router, prefix="/api")
@app.on_event("startup")
async def startup():
ensure_default_pages()
@app.get("/api/health")
async def health():
return {"status": "ok"}

View File

@@ -12,6 +12,48 @@ router = APIRouter()
PAGES_DIR = Path(os.environ.get("PAGES_DIR", "/data/pages"))
SOURCES_DIR = Path(os.environ.get("SOURCES_DIR", "/data/sources"))
DEFAULT_INDEX_SOURCE = '''\
page "Welcome" 60
bigtitle "uFrame" thin
box rounded "Micronomicon"
align center
text "Decentralized Page Server"
text "Powered by @bold{Reticulum} and @bold{NomadNet}"
spacer
heading 2 "Pages"
text "This node is serving pages built with the uFrame DSL."
text "Use the web IDE to create and publish new pages."
spacer
divider light
label "Node" "Micronomicon"
label "Engine" "uFrame v1"
status "Node" online
'''
def ensure_default_pages():
"""Create a default index page if none exists."""
PAGES_DIR.mkdir(parents=True, exist_ok=True)
SOURCES_DIR.mkdir(parents=True, exist_ok=True)
index_mu = PAGES_DIR / "index.mu"
index_src = SOURCES_DIR / "index.uf"
if not index_mu.is_file():
# Compile and publish the default index
result = uframe.compile(DEFAULT_INDEX_SOURCE)
index_mu.write_text(result.micron, encoding="utf-8")
index_mu.chmod(0o644)
if not index_src.is_file():
index_src.write_text(DEFAULT_INDEX_SOURCE, encoding="utf-8")
class PageMeta(BaseModel):
name: str

View File

@@ -1,14 +1,34 @@
services:
micron-editor:
build: .
micronomicon:
build:
context: .
dockerfile: Dockerfile
ports:
- "127.0.0.1:8080:8080"
volumes:
- ~/.nomadnetwork/storage/pages:/data/pages
- ~/.micron-editor/sources:/data/sources
- /var/run/docker.sock:/var/run/docker.sock
environment:
- PAGES_DIR=/data/pages
- SOURCES_DIR=/data/sources
- NOMADNET_CONTAINER=nomadnet
restart: always
volumes:
- pages:/data/pages
- sources:/data/sources
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
depends_on:
- nomadnet
nomadnet:
image: ghcr.io/markqvist/nomadnet:latest
ports:
- "0.0.0.0:4242:4242"
volumes:
- pages:/root/.nomadnetwork/storage/pages
- nomadnet-config:/root/.nomadnetwork
- ./nomadnet.conf:/root/.nomadnetwork/config:ro
- ./reticulum.conf:/root/.reticulum/config:ro
restart: unless-stopped
volumes:
pages:
sources:
nomadnet-config:

View File

@@ -1,30 +0,0 @@
services:
micronomicon:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- PAGES_DIR=/data/pages
- SOURCES_DIR=/data/sources
- NOMADNET_CONTAINER=nomadnet
volumes:
- pages:/data/pages
- sources:/data/sources
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
depends_on:
- nomadnet
nomadnet:
image: ghcr.io/markqvist/nomadnet:latest
volumes:
- pages:/root/.nomadnetwork/storage/pages
- nomadnet-config:/root/.nomadnetwork
restart: unless-stopped
volumes:
pages:
sources:
nomadnet-config:

145
docs/multi-node-plan.md Normal file
View File

@@ -0,0 +1,145 @@
# Multi-Node Support for Micronomicon
## Context
Currently Micronomicon runs a single NomadNet node — one identity, one set of pages, one `index.mu`. The user wants to host multiple independent "sites" on the Reticulum network, each with its own node identity, name, and pages. This requires changes across Docker infrastructure, backend API, and frontend UI.
## Approach: Dynamic Node Containers via Docker API
The FastAPI backend already has Docker socket access. Instead of statically defining NomadNet services in `compose.yml`, the backend will create/manage NomadNet containers dynamically — one per node. Each node gets its own directory with pages, sources, config, and identity.
## Storage Layout
```
/data/nodes/
default/
node.json # {"id", "display_name", "port", "created_at"}
nomadnet.conf # generated — unique node_name
pages/ # compiled .mu files (mounted into container)
sources/ # .uf source files
my-relay/
node.json
nomadnet.conf
pages/
sources/
```
## Files to Create
### `backend/nodes.py` — Node lifecycle management
- `NodeConfig` pydantic model: `id, display_name, port, created_at`
- `NODES_DIR = Path(os.environ.get("NODES_DIR", "/data/nodes"))`
- CRUD: `list_nodes()`, `get_node(id)`, `create_node(id, display_name)`, `delete_node(id)`
- `_generate_nomadnet_conf(display_name)` — template from existing `nomadnet.conf` with node-specific `node_name`
- `_next_port()` — allocate TCP ports starting from 4242
- Docker container management via Docker SDK (already a dependency):
- `start_node_container(node)` — create container from `ghcr.io/markqvist/nomadnet:latest`, mount node's `pages/` dir, per-node identity volume, shared `reticulum.conf`, generated `nomadnet.conf`
- `stop_node_container(id)`, `restart_node_container(id)`, `get_node_status(id)`
- `ensure_default_node()` — migration: if `NODES_DIR/default` doesn't exist, create it and move contents from old `PAGES_DIR`/`SOURCES_DIR`
- Startup reconciliation: check existing containers, start any that should be running
- API router:
- `GET /api/nodes` — list all nodes with status
- `POST /api/nodes` — create `{id, display_name}`
- `GET /api/nodes/{node_id}` — node details
- `DELETE /api/nodes/{node_id}` — stop container, remove dir
- `POST /api/nodes/{node_id}/restart`
### `frontend/src/stores/nodesStore.ts` — Node state
- `nodes: NodeMeta[]`, `activeNodeId: string` (persisted to localStorage, defaults to `"default"`)
- `fetchNodes()`, `setActiveNode(id)`, `createNode(id, displayName)`, `deleteNode(id)`, `restartNode(id)`
### `frontend/src/components/shared/NodeSelector.tsx` — Header dropdown
- Dropdown listing nodes with status indicator (green/red dot)
- Switching active node refetches pages
- "Manage Nodes" link to `/nodes`
### `frontend/src/routes/NodesView.tsx` — Node management page
- Table: Name, Status, Port, Created, Actions (restart/delete)
- "Create Node" button with dialog (ID + display name)
## Files to Modify
### `backend/pages.py`
- Replace global `PAGES_DIR`/`SOURCES_DIR` with helpers: `_node_pages_dir(node_id)` -> `NODES_DIR/node_id/pages`, `_node_sources_dir(node_id)` -> `NODES_DIR/node_id/sources`
- Refactor all functions to accept `node_id` parameter
- Add node-scoped endpoints: `GET/POST/DELETE /api/nodes/{node_id}/pages/{name}`
- Keep existing `/api/pages/{name}` as aliases -> `node_id="default"`
- `ensure_default_pages(node_id)` — create `index.mu` per node
### `backend/graph.py`
- Same refactor: accept `node_id`, add `/api/nodes/{node_id}/graph`
- Keep `/api/graph` as alias for default
### `backend/docker_utils.py`
- Deprecate in favor of `nodes.py` restart endpoint
- Keep `/api/restart` as alias -> restart default node
### `backend/main.py`
- Import and include `nodes` router
- Startup: call `ensure_default_node()`, then start all node containers
- Remove direct `ensure_default_pages()` call (handled by node creation)
### `compose.yml`
- Remove static `nomadnet` service (backend manages containers dynamically)
- Replace `pages`/`sources`/`nomadnet-config` volumes with single `nodes` volume
- Add `NODES_DIR=/data/nodes`, `NOMADNET_IMAGE`, `RETICULUM_CONF` env vars
- Add `reticulum` bridge network for container communication
- Keep `reticulum.conf` bind-mount for backend to pass to spawned containers
### `frontend/src/stores/pagesStore.ts`
- Read `activeNodeId` from `nodesStore`
- All fetch calls go to `/api/nodes/{activeNodeId}/pages/...`
### `frontend/src/routes/DashboardView.tsx`
- Show active node name in header
- Restart button restarts active node
- Page list scoped to active node via store
### `frontend/src/routes/EditorView.tsx`
- Save/publish calls use `/api/nodes/{activeNodeId}/pages/{slug}`
- Load page from `/api/nodes/{activeNodeId}/pages/{name}`
- Show active node name in toolbar
### `frontend/src/routes/GraphView.tsx`
- Fetch from `/api/nodes/{activeNodeId}/graph`
### `frontend/src/App.tsx`
- Add `/nodes` route -> `NodesView`
### `frontend/src/components/shared/AppShell.tsx` (or equivalent layout)
- Add `NodeSelector` to header/nav area
## Backward Compatibility
- Existing `/api/pages/...`, `/api/restart`, `/api/graph` remain as aliases for `node_id="default"`
- `ensure_default_node()` migrates old flat `PAGES_DIR`/`SOURCES_DIR` into `NODES_DIR/default/`
- Frontend defaults `activeNodeId` to `"default"` — single-node users see no change
- `NodeSelector` only shows when >1 node exists (or shows as subtle indicator for single node)
## Implementation Order
1. `backend/nodes.py` — core model, storage, Docker container management, API
2. `backend/pages.py` — refactor to accept `node_id`, add node-scoped endpoints
3. `backend/graph.py` — refactor to accept `node_id`
4. `backend/main.py` — wire in nodes router, startup logic
5. `compose.yml` — restructure (remove static nomadnet, add nodes volume + network)
6. `frontend/src/stores/nodesStore.ts` — new store
7. `frontend/src/stores/pagesStore.ts` — parameterize by active node
8. `frontend/src/components/shared/NodeSelector.tsx` — node switcher
9. `frontend/src/routes/NodesView.tsx` — management UI
10. `frontend/src/App.tsx` + layout — add routes, add selector to shell
11. `frontend/src/routes/DashboardView.tsx` — node-aware
12. `frontend/src/routes/EditorView.tsx` — node-aware save/publish/load
13. `frontend/src/routes/GraphView.tsx` — node-aware
## Verification
1. `docker compose up --build -d` — starts backend only (no static nomadnet)
2. Hit `GET /api/nodes` — should return `[{id: "default", display_name: "Micronomicon", status: "running", ...}]`
3. Open web IDE — pages dashboard shows default node's pages, NodeSelector shows "Micronomicon"
4. Create a page, publish — appears in default node's container at `/root/.nomadnetwork/storage/pages/`
5. `POST /api/nodes` with `{id: "relay", display_name: "Relay Alpha"}` — new container starts, `GET /api/nodes` shows 2 nodes
6. Switch to "Relay Alpha" in NodeSelector — empty page list, create and publish `index.mu`
7. From remote NomadNet client, both nodes visible on network with separate identities and pages
8. Delete "Relay Alpha" — container stops, files removed, back to single node
9. Run backend tests: `python -m pytest uframe/tests/ -v` — all 91 pass (DSL unchanged)

30
nomadnet.conf Normal file
View File

@@ -0,0 +1,30 @@
[logging]
loglevel = 4
destination = file
[client]
enable_client = yes
user_interface = text
announce_at_start = yes
[textui]
intro_time = 1
theme = dark
colormode = 256
glyphs = unicode
[node]
# Enable page-serving node
enable_node = yes
node_name = Micronomicon
# Announce on the network
announce_interval = 360
announce_at_start = yes
# Rescan pages directory every minute so newly
# published pages are picked up automatically
page_refresh_interval = 1
# Not running as a propagation node
disable_propagation = yes

21
reticulum.conf Normal file
View File

@@ -0,0 +1,21 @@
[reticulum]
enable_transport = True
share_instance = Yes
[logging]
loglevel = 4
[interfaces]
# Local AutoInterface for LAN discovery
[[Default Interface]]
type = AutoInterface
enabled = Yes
# TCP Server — allows other Reticulum peers to connect
# Expose this port to reach the node from outside Docker
[[TCP Server Interface]]
type = TCPServerInterface
enabled = Yes
listen_ip = 0.0.0.0
listen_port = 4242