Update CLAUDE.md with comprehensive NUC infrastructure docs
Expand from initial setup notes to full operational manual covering OpenClaw gateway, Palmr file sharing, MinIO storage, Deepgram MCP, Gitea auto-deploy workflows, Tailscale Funnel architecture, JSX artifact publishing, and OpenWrt router management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
579
CLAUDE.md
579
CLAUDE.md
@@ -14,28 +14,32 @@ ssh nuc
|
|||||||
|
|
||||||
## DNS & Tailscale Setup
|
## DNS & Tailscale Setup
|
||||||
|
|
||||||
### Why Tailscale IP for DNS
|
### DNS Strategy: Local IP + Tailscale Subnet Routing
|
||||||
|
|
||||||
All `.nuc.lan` domains resolve to the **Tailscale IP** (`100.113.153.45`) instead of the local IP (`192.168.1.3`). This ensures services work from **anywhere** regardless of your current network's subnet.
|
All `.nuc.lan` domains resolve to the **local LAN IP** (`192.168.1.3`). This allows any device on the LAN to access services without Tailscale.
|
||||||
|
|
||||||
**Problem solved:** When connecting from a remote network that also uses `192.168.x.x`, traffic to `192.168.1.3` stays local instead of going through Tailscale. Using Tailscale IP (`100.x.x.x`) avoids this conflict.
|
**Remote access (Tailscale)** works via two mechanisms:
|
||||||
|
1. **Split DNS**: Tailscale forwards `*.nuc.lan` queries to the OpenWrt router (`192.168.1.1`) through the tunnel
|
||||||
|
2. **Subnet routing**: The NUC advertises `192.168.1.0/24` as a Tailscale subnet route, so `192.168.1.3` is reachable from any Tailscale device remotely
|
||||||
|
|
||||||
### Configured Domains (OpenWrt Router DNS)
|
### Configured Domains (OpenWrt Router DNS)
|
||||||
|
|
||||||
| Domain | Resolves To | Service |
|
| Domain | Resolves To | Service |
|
||||||
|--------|-------------|---------|
|
|--------|-------------|---------|
|
||||||
| `nuc.lan` | `100.113.153.45` | NUC Portal |
|
| `nuc.lan` | `192.168.1.3` | NUC Portal |
|
||||||
| `nuc.local` | `100.113.153.45` | NUC Portal |
|
| `nuc.local` | `192.168.1.3` | NUC Portal |
|
||||||
| `coolify.nuc.lan` | `100.113.153.45` | Coolify |
|
| `coolify.nuc.lan` | `192.168.1.3` | Coolify |
|
||||||
| `gitea.nuc.lan` | `100.113.153.45` | Gitea |
|
| `gitea.nuc.lan` | `192.168.1.3` | Gitea |
|
||||||
| `outline.nuc.lan` | `100.113.153.45` | Outline Wiki |
|
| `outline.nuc.lan` | `192.168.1.3` | Outline Wiki |
|
||||||
| `files.nuc.lan` | `100.113.153.45` | FileBrowser |
|
| `files.nuc.lan` | `192.168.1.3` | FileBrowser |
|
||||||
| `mail.nuc.lan` | `100.113.153.45` | Snappymail |
|
| `mail.nuc.lan` | `192.168.1.3` | Snappymail |
|
||||||
| `vault.nuc.lan` | `100.113.153.45` | Vaultwarden |
|
| `vault.nuc.lan` | `192.168.1.3` | Vaultwarden |
|
||||||
| `homepage.nuc.lan` | `100.113.153.45` | NUC Portal |
|
| `homepage.nuc.lan` | `192.168.1.3` | NUC Portal |
|
||||||
| `brand.nuc.lan` | `100.113.153.45` | Whyrating Brand |
|
| `brand.nuc.lan` | `192.168.1.3` | Whyrating Brand |
|
||||||
| `templates.nuc.lan` | `100.113.153.45` | Whyrating Templates |
|
| `templates.nuc.lan` | `192.168.1.3` | Whyrating Templates |
|
||||||
| `whyrating.nuc.lan` | `100.113.153.45` | Whyrating Hub |
|
| `whyrating.nuc.lan` | `192.168.1.3` | Whyrating Hub |
|
||||||
|
| `whyops.nuc.lan` | `192.168.1.3` | WhyOps (scraping, pipelines, testing) |
|
||||||
|
| `arrio.nuc.lan` | `192.168.1.3` | Arrio (hotel check-in) |
|
||||||
|
|
||||||
### Traefik Routing (Dynamic Config)
|
### Traefik Routing (Dynamic Config)
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@ http:
|
|||||||
ssh nuc "ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 '
|
ssh nuc "ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 '
|
||||||
uci add dhcp domain
|
uci add dhcp domain
|
||||||
uci set dhcp.@domain[-1].name=\"newservice.nuc.lan\"
|
uci set dhcp.@domain[-1].name=\"newservice.nuc.lan\"
|
||||||
uci set dhcp.@domain[-1].ip=\"100.113.153.45\"
|
uci set dhcp.@domain[-1].ip=\"192.168.1.3\"
|
||||||
uci commit dhcp
|
uci commit dhcp
|
||||||
/etc/init.d/dnsmasq restart
|
/etc/init.d/dnsmasq restart
|
||||||
'"
|
'"
|
||||||
@@ -200,6 +204,7 @@ Task(subagent_type="general-purpose", prompt="Add services to Homepage...", desc
|
|||||||
| `mcp__ssh-manager__*` | Direct SSH commands, file transfers |
|
| `mcp__ssh-manager__*` | Direct SSH commands, file transfers |
|
||||||
| `mcp__n8n__*` | Workflow automation (if configured) |
|
| `mcp__n8n__*` | Workflow automation (if configured) |
|
||||||
| `mcp__playwriter__*` | Browser automation fallback (see below) |
|
| `mcp__playwriter__*` | Browser automation fallback (see below) |
|
||||||
|
| `mcp__deepgram__*` | Audio transcription (STT) and text-to-speech (TTS) |
|
||||||
|
|
||||||
### Stalwart Mail MCP (Quick Guide)
|
### Stalwart Mail MCP (Quick Guide)
|
||||||
|
|
||||||
@@ -453,6 +458,10 @@ ssh nuc "docker exec <container_name> <command>"
|
|||||||
| Kopia | - | `http://100.113.153.45:51515` | kopia |
|
| Kopia | - | `http://100.113.153.45:51515` | kopia |
|
||||||
| Dozzle | - | `http://100.113.153.45:9999` | dozzle |
|
| Dozzle | - | `http://100.113.153.45:9999` | dozzle |
|
||||||
| CloudBeaver | - | `http://100.113.153.45:8978` | cloudbeaver-* |
|
| CloudBeaver | - | `http://100.113.153.45:8978` | cloudbeaver-* |
|
||||||
|
| WhyOps | `http://whyops.nuc.lan` | `http://100.113.153.45:3002` | whyrating-dashboard |
|
||||||
|
| Palmr | `https://alezmad-nuc.tail58f5ad.ts.net:8443` | `http://100.113.153.45:3334` | palmr |
|
||||||
|
| Arrio | `http://arrio.nuc.lan` | `http://100.113.153.45:3335` | web-tgksg0s8gocko4csggs0808c |
|
||||||
|
| Deepgram MCP | - | `http://100.113.153.45:8009` | deepgram-mcp |
|
||||||
|
|
||||||
**Note:** Use Tailscale IP (`100.113.153.45`) instead of `192.168.1.3` to avoid subnet conflicts when remote.
|
**Note:** Use Tailscale IP (`100.113.153.45`) instead of `192.168.1.3` to avoid subnet conflicts when remote.
|
||||||
|
|
||||||
@@ -526,7 +535,11 @@ ssh nuc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep <service>"
|
|||||||
ssh nuc "docker exec <container> wget -qO- http://127.0.0.1:<port>/healthz"
|
ssh nuc "docker exec <container> wget -qO- http://127.0.0.1:<port>/healthz"
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **Creating API keys when no UI available** (e.g., n8n):
|
7. **localhost resolves to IPv6 on NUC**: Docker containers bind to IPv4 only. Always use `127.0.0.1` instead of `localhost` in Tailscale Funnel/Serve targets, curl commands inside SSH, etc.
|
||||||
|
|
||||||
|
8. **Tailscale Funnel port not working externally**: Funnel ONLY supports ports **443, 8443, and 10000**. Other ports will show as "Funnel on" in status but won't route internet traffic.
|
||||||
|
|
||||||
|
9. **Creating API keys when no UI available** (e.g., n8n):
|
||||||
```bash
|
```bash
|
||||||
# Stop container, insert directly into SQLite, restart
|
# Stop container, insert directly into SQLite, restart
|
||||||
ssh nuc "docker run --rm -v <volume>:/data keinos/sqlite3 sqlite3 /data/database.sqlite \"<INSERT_QUERY>\""
|
ssh nuc "docker run --rm -v <volume>:/data keinos/sqlite3 sqlite3 /data/database.sqlite \"<INSERT_QUERY>\""
|
||||||
@@ -966,6 +979,7 @@ Coolify's "Gitea Source" uses GitHub App-style OAuth with JWT - **this doesn't w
|
|||||||
| whyrating-brand | http://brand.nuc.lan | `alezmad/whyrating-brand` | `r80gk0ccgg0okos8cw848kkk` |
|
| whyrating-brand | http://brand.nuc.lan | `alezmad/whyrating-brand` | `r80gk0ccgg0okos8cw848kkk` |
|
||||||
| whyrating-templates | http://templates.nuc.lan | `alezmad/whyrating-templates` | `qw80g4sog0kk8cc4wkcs8sgc` |
|
| whyrating-templates | http://templates.nuc.lan | `alezmad/whyrating-templates` | `qw80g4sog0kk8cc4wkcs8sgc` |
|
||||||
| turbostarter | https://alezmad-nuc.tail58f5ad.ts.net | `alezmad/turbostarter` | `v4gogwwc8wkk4888ksscc4k4` (service) |
|
| turbostarter | https://alezmad-nuc.tail58f5ad.ts.net | `alezmad/turbostarter` | `v4gogwwc8wkk4888ksscc4k4` (service) |
|
||||||
|
| arrio | http://arrio.nuc.lan | `alezmad/arrio` | `tgksg0s8gocko4csggs0808c` (service) |
|
||||||
|
|
||||||
### Turbostarter (Knosia) - Build & Deploy
|
### Turbostarter (Knosia) - Build & Deploy
|
||||||
|
|
||||||
@@ -1059,15 +1073,24 @@ Cloudflare shared IPs get blocked by Spanish ISPs during LaLiga matches. Tailsca
|
|||||||
| **Tailscale IP** | `100.x.x.x` (stable, never changes) |
|
| **Tailscale IP** | `100.x.x.x` (stable, never changes) |
|
||||||
| **Status** | `ssh nuc "tailscale funnel status"` |
|
| **Status** | `ssh nuc "tailscale funnel status"` |
|
||||||
|
|
||||||
|
**⚠️ CRITICAL: Funnel only supports ports 443, 8443, and 10000.** Other ports will appear to work locally (`tailscale funnel status` shows them) but traffic will NOT route from the public internet. Always use one of these three ports.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL: Always use `127.0.0.1`, NOT `localhost`** in Funnel/Serve targets. On the NUC, `localhost` resolves to IPv6 (`::1`) but Docker containers bind to IPv4 only, causing connection resets.
|
||||||
|
|
||||||
**Start Funnel for a service:**
|
**Start Funnel for a service:**
|
||||||
```bash
|
```bash
|
||||||
# Expose port 3000 via Funnel
|
# Expose port 3000 via Funnel (use 127.0.0.1, NOT localhost!)
|
||||||
ssh nuc "tailscale funnel 3000"
|
ssh nuc "printf '<sudo_pass>\n' | sudo -S tailscale funnel --bg --https=8443 http://127.0.0.1:3000"
|
||||||
|
|
||||||
# Or with background (use screen/tmux)
|
|
||||||
ssh nuc "screen -dmS funnel tailscale funnel 3000"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Current Funnel allocation:**
|
||||||
|
|
||||||
|
| Port | Service | Target |
|
||||||
|
|------|---------|--------|
|
||||||
|
| 443 | Traefik (main) | `http://192.168.1.3:80` |
|
||||||
|
| 8443 | Palmr (file sharing) | `http://127.0.0.1:3334` |
|
||||||
|
| 10000 | Palmr MinIO (uploads) | `http://127.0.0.1:9379` |
|
||||||
|
|
||||||
### Current Domain Routes
|
### Current Domain Routes
|
||||||
|
|
||||||
| Domain | Destination | Method |
|
| Domain | Destination | Method |
|
||||||
@@ -1168,6 +1191,106 @@ Examples:
|
|||||||
- <Links to services, docs, or other artifacts>
|
- <Links to services, docs, or other artifacts>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Publishing JSX/React Artifacts Online
|
||||||
|
|
||||||
|
Single-file React components (JSX) can be published as standalone web pages via the NUC's artifacts infrastructure.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
Public Internet → Tailscale Funnel (:443) → Traefik → artifacts-web (nginx) → /opt/artifacts/
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Funnel URL:** `https://alezmad-nuc.tail58f5ad.ts.net/artifacts/<name>/`
|
||||||
|
- **LAN URL:** `https://artifacts.nuc.lan/<name>/`
|
||||||
|
- **Nginx container:** `artifacts-web` (image: `nginx:alpine`, read-only mount of `/opt/artifacts`)
|
||||||
|
- **Traefik public route:** `Host(alezmad-nuc.tail58f5ad.ts.net) && PathPrefix(/artifacts)` → strips `/artifacts` → `artifacts-web:80`
|
||||||
|
- **Config file:** `/traefik/dynamic/nuc-services-public.yaml` (inside `coolify-proxy` container)
|
||||||
|
|
||||||
|
### Quick Publish Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build self-contained HTML from JSX
|
||||||
|
# - Replace `import { useState, ... } from "react"` with `const { useState, ... } = React;`
|
||||||
|
# - Remove `export default ComponentName;`
|
||||||
|
# - Wrap in HTML with React 18 CDN + Babel standalone
|
||||||
|
# - Add `ReactDOM.createRoot(root).render(<Component />)` at the end
|
||||||
|
|
||||||
|
# 2. Copy to NUC artifacts directory
|
||||||
|
ssh nuc "echo '7vXHpSTD.' | sudo -S mkdir -p /opt/artifacts/<name>"
|
||||||
|
scp /tmp/build/index.html nuc:~/tmp-artifact.html
|
||||||
|
ssh nuc "echo '7vXHpSTD.' | sudo -S mv ~/tmp-artifact.html /opt/artifacts/<name>/index.html"
|
||||||
|
ssh nuc "echo '7vXHpSTD.' | sudo -S chmod 644 /opt/artifacts/<name>/index.html"
|
||||||
|
|
||||||
|
# 3. Done! No server restart needed — nginx serves it immediately.
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Template for JSX Files
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TITLE</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { background: #08090d; overflow-x: hidden; }
|
||||||
|
/* Add any @keyframes or global styles here */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect, useRef, useCallback } = React;
|
||||||
|
|
||||||
|
// ... paste JSX component code here (without import/export lines) ...
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(ComponentName));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Script (for large JSX files)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Automated: strips import/export, wraps in HTML
|
||||||
|
cat > /tmp/build.html << 'HEADER'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||||
|
</head><body><div id="root"></div>
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect, useRef, useCallback, useMemo, useReducer } = React;
|
||||||
|
HEADER
|
||||||
|
|
||||||
|
# Strip first line (import) and last line (export), append body
|
||||||
|
sed -n '2,$p' source.jsx | sed '$d' >> /tmp/build.html
|
||||||
|
|
||||||
|
# Add render footer (replace COMPONENT_NAME)
|
||||||
|
cat >> /tmp/build.html << 'FOOTER'
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(COMPONENT_NAME));
|
||||||
|
</script></body></html>
|
||||||
|
FOOTER
|
||||||
|
```
|
||||||
|
|
||||||
|
### Currently Published
|
||||||
|
|
||||||
|
| Path | Source | Public URL |
|
||||||
|
|------|--------|------------|
|
||||||
|
| `/opt/artifacts/checkin/` | `arrio/.scratch/checkin_demo_v1.jsx` | `https://alezmad-nuc.tail58f5ad.ts.net/artifacts/checkin/` |
|
||||||
|
|
||||||
## OpenWrt Interaction Methods (Quick Reference)
|
## OpenWrt Interaction Methods (Quick Reference)
|
||||||
|
|
||||||
| Method | When to Use | Example |
|
| Method | When to Use | Example |
|
||||||
@@ -1179,6 +1302,198 @@ Examples:
|
|||||||
|
|
||||||
**Priority Order:** SSH > OpenWrt MCP > Chrome DevTools > Manual UI
|
**Priority Order:** SSH > OpenWrt MCP > Chrome DevTools > Manual UI
|
||||||
|
|
||||||
|
## Palmr (File Sharing - Dropbox Alternative)
|
||||||
|
|
||||||
|
Self-hosted file sharing platform (like WeTransfer/Dropbox) for sending and receiving files.
|
||||||
|
|
||||||
|
### Access
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **Public URL** | `https://alezmad-nuc.tail58f5ad.ts.net:8443` |
|
||||||
|
| **Local URL** | `http://192.168.1.3:3334` |
|
||||||
|
| **Login page** | `/login` |
|
||||||
|
| **Container** | `palmr` |
|
||||||
|
| **Image** | `kyantech/palmr:latest` |
|
||||||
|
| **Network** | `coolify` (IP: `10.0.1.5`) |
|
||||||
|
| **Sudo pass** | `7vXHpSTD.` |
|
||||||
|
|
||||||
|
### Users
|
||||||
|
|
||||||
|
| Username | Email | Password | Admin |
|
||||||
|
|----------|-------|----------|-------|
|
||||||
|
| `alezmad` | `agutierrez@mineryreport.com` | *(set via UI)* | Yes |
|
||||||
|
| `michi` | `michi@nuc.lan` | `FlexiCar2025.` | No |
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
External Browser → Funnel :8443 → 127.0.0.1:3334 → container:5487 (Next.js frontend)
|
||||||
|
Funnel :10000 → 127.0.0.1:9379 → container:9379 (MinIO uploads)
|
||||||
|
↑ via port-fwd-palmr-minio
|
||||||
|
```
|
||||||
|
|
||||||
|
Palmr uses **presigned URLs** for file uploads — the browser uploads directly to MinIO, NOT through the backend. This means MinIO MUST be reachable from the client's browser.
|
||||||
|
|
||||||
|
### Key Environment Variables
|
||||||
|
|
||||||
|
| Variable | Value | Purpose |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| `STORAGE_URL` | `https://alezmad-nuc.tail58f5ad.ts.net:10000` | Public MinIO URL for presigned upload URLs |
|
||||||
|
| `API_BASE_URL` | `http://127.0.0.1:3333` | Internal backend API (Next.js → Fastify) |
|
||||||
|
|
||||||
|
### Volume Mounts
|
||||||
|
|
||||||
|
| Volume | Mount Point | Contents |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| `5273abd0c...` (anonymous) | `/app/server` | SQLite DB, MinIO data, credentials |
|
||||||
|
| `palmr_uploads` | `/app/uploads` | Uploaded files |
|
||||||
|
|
||||||
|
### Container Recreation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh nuc "docker stop palmr && docker rm palmr && docker run -d \
|
||||||
|
--name palmr \
|
||||||
|
--network coolify \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3334:5487 \
|
||||||
|
-v 5273abd0c536116056362397cdb568d2eab066b8289412dd91ecce58c174df68:/app/server \
|
||||||
|
-v palmr_uploads:/app/uploads \
|
||||||
|
-e STORAGE_URL=https://alezmad-nuc.tail58f5ad.ts.net:10000 \
|
||||||
|
kyantech/palmr:latest"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Forwarder (MinIO)
|
||||||
|
|
||||||
|
MinIO runs inside the Palmr container on port 9379 (not exposed). A socat forwarder bridges it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh nuc "docker run -d --name port-fwd-palmr-minio --network coolify -p 9379:9379 \
|
||||||
|
alpine/socat tcp-listen:9379,fork,reuseaddr tcp-connect:palmr:9379"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Access
|
||||||
|
|
||||||
|
Palmr uses SQLite at `/app/server/prisma/palmr.db`. Passwords are **bcrypt** hashed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy DB locally for queries
|
||||||
|
ssh nuc "docker cp palmr:/app/server/prisma/palmr.db /tmp/palmr.db"
|
||||||
|
scp nuc:/tmp/palmr.db /tmp/palmr.db
|
||||||
|
sqlite3 /tmp/palmr.db "SELECT username, email, isAdmin FROM users;"
|
||||||
|
|
||||||
|
# Generate bcrypt hash inside container
|
||||||
|
ssh nuc "docker exec palmr sh -c 'cd /app/palmr-app && node -e \"const bcrypt = require(\\\"bcryptjs\\\"); console.log(bcrypt.hashSync(\\\"PASSWORD\\\", 10));\"'"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
1. **Upload stuck on loader**: Check `docker logs palmr` for `STORAGE_URL` errors. MinIO must be reachable from the client browser via the presigned URL.
|
||||||
|
|
||||||
|
2. **Blank page externally**: Verify Funnel is on a supported port (443/8443/10000) and target uses `127.0.0.1` not `localhost`.
|
||||||
|
|
||||||
|
3. **JWT token invalid after restart**: Expected — users must log in again after container recreation (JWT secret regenerated).
|
||||||
|
|
||||||
|
4. **Config limits**: File size and storage limits are in `app_configs` table (values in bytes). Update via SQLite, copy DB back, restart.
|
||||||
|
|
||||||
|
## MinIO Object Storage (Mac ↔ NUC File Transfer)
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
MinIO runs on the NUC as S3-compatible object storage. Used for Mac backups, file transfers, and app storage.
|
||||||
|
|
||||||
|
```
|
||||||
|
Mac (mc CLI) → Tailscale → 100.113.153.45:9000 → port-fwd (socat) → minio-xwowg8kswwsocssgocs8ss40:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **API URL** | `http://100.113.153.45:9000` |
|
||||||
|
| **Console URL** | `http://100.113.153.45:9001` |
|
||||||
|
| **Access Key** | `minioadmin` |
|
||||||
|
| **Secret Key** | `minioadmin` |
|
||||||
|
| **Port Forwarder** | `minio-port-fwd` (socat, maps host :9000/:9001 → container) |
|
||||||
|
| **Main Container** | `minio-xwowg8kswwsocssgocs8ss40` |
|
||||||
|
|
||||||
|
### Buckets
|
||||||
|
|
||||||
|
| Bucket | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `mac-backups` | Database dumps, tar archives, important backups |
|
||||||
|
| `mac-downloads` | Files moved from ~/Downloads for archival |
|
||||||
|
| `mac-projects` | Archived project files |
|
||||||
|
| `nuc-portal-previews` | NUC Portal screenshot previews |
|
||||||
|
| `whyrating` | WhyRating app assets |
|
||||||
|
|
||||||
|
### Mac CLI (mc)
|
||||||
|
|
||||||
|
Installed at `/opt/homebrew/bin/mc`, alias `nuc` pre-configured.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List buckets
|
||||||
|
mc ls nuc/
|
||||||
|
|
||||||
|
# Upload file
|
||||||
|
mc cp file.tar nuc/mac-backups/
|
||||||
|
|
||||||
|
# Download file
|
||||||
|
mc cp nuc/mac-backups/file.tar ./
|
||||||
|
|
||||||
|
# Sync a directory
|
||||||
|
mc mirror ~/path/to/dir nuc/mac-projects/dirname/
|
||||||
|
|
||||||
|
# Check bucket size
|
||||||
|
mc du nuc/mac-backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### nuc-sync CLI Tool
|
||||||
|
|
||||||
|
Wrapper script at `~/.local/bin/nuc-sync` for daily operations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nuc-sync status # Check NUC + MinIO connectivity
|
||||||
|
nuc-sync upload file.tar # Upload to mac-backups bucket
|
||||||
|
nuc-sync upload file.sql mac-downloads # Upload to specific bucket
|
||||||
|
nuc-sync push ~/Desktop/project # rsync to NUC (excludes node_modules, .next, .git)
|
||||||
|
nuc-sync pull /opt/backups/mac/x . # rsync from NUC
|
||||||
|
nuc-sync list # List all buckets
|
||||||
|
nuc-sync list mac-backups # List bucket contents
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinIO MCP Server
|
||||||
|
|
||||||
|
MCP server for Claude Code integration: `minio-nuc`
|
||||||
|
- **Source:** `~/mcp-servers/minio-mcp-server/`
|
||||||
|
- **GitHub:** `https://github.com/Rafitis/minio-mcp-server`
|
||||||
|
- **Scope:** user (available in all projects)
|
||||||
|
- **Tools:** list buckets, upload/download objects, create/delete buckets
|
||||||
|
|
||||||
|
### rsync Backup Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NUC backup path (owned by alezmad)
|
||||||
|
/opt/backups/mac/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
1. **MinIO unreachable (connection refused)**: The port forwarder may be down:
|
||||||
|
```bash
|
||||||
|
ssh nuc "docker start minio-port-fwd"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **mc alias not working**: Reconfigure:
|
||||||
|
```bash
|
||||||
|
/opt/homebrew/bin/mc alias set nuc http://100.113.153.45:9000 minioadmin minioadmin
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Console not loading**: Port 9001 needs the port-fwd container running. Check with:
|
||||||
|
```bash
|
||||||
|
ssh nuc "docker ps | grep minio-port-fwd"
|
||||||
|
```
|
||||||
|
|
||||||
## Next Steps / Migration Candidates
|
## Next Steps / Migration Candidates
|
||||||
|
|
||||||
### Priority 1: Safe to Delete (Duplicates/Old Versions)
|
### Priority 1: Safe to Delete (Duplicates/Old Versions)
|
||||||
@@ -1240,3 +1555,223 @@ mcp__coolify__service(action="create", type="prometheus", ...)
|
|||||||
- [ ] Verify MySQL data before deleting
|
- [ ] Verify MySQL data before deleting
|
||||||
- [ ] Add CloudBeaver to Uptime Kuma monitoring
|
- [ ] Add CloudBeaver to Uptime Kuma monitoring
|
||||||
- [ ] Configure OpenWrt MCP MQTT broker (optional)
|
- [ ] Configure OpenWrt MCP MQTT broker (optional)
|
||||||
|
|
||||||
|
## OpenClaw (AI Assistant Gateway)
|
||||||
|
|
||||||
|
Self-hosted AI assistant gateway running on the NUC via Docker Compose. Connects to messaging platforms (WhatsApp, Telegram, Discord, etc.) and routes messages through Claude.
|
||||||
|
|
||||||
|
### Access
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **Control UI (HTTPS)** | `https://alezmad-nuc.tail58f5ad.ts.net:8443` |
|
||||||
|
| **Gateway WS** | `ws://192.168.1.3:18789` |
|
||||||
|
| **Gateway Token** | `3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee` |
|
||||||
|
| **Model** | `anthropic/claude-sonnet-4-5-20250929` |
|
||||||
|
| **Repo on NUC** | `~/openclaw/` |
|
||||||
|
| **Config** | `~/.openclaw/openclaw.json` |
|
||||||
|
| **Version** | `2026.2.10` |
|
||||||
|
|
||||||
|
**⚠️ HTTPS Required:** The Control UI requires a secure context (HTTPS or localhost). Access via Tailscale Serve on port 8443.
|
||||||
|
|
||||||
|
### Tailscale Serve (HTTPS access)
|
||||||
|
|
||||||
|
The gateway is exposed via Tailscale Serve (not Funnel - tailnet only, not public):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start HTTPS proxy (requires sudo, must run from NUC terminal)
|
||||||
|
ssh nuc
|
||||||
|
sudo tailscale serve --bg --https=8443 http://localhost:18789
|
||||||
|
# Password: 7vXHpSTD
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ The `--bg` flag makes it persistent.** Without it, Ctrl+C stops the proxy.
|
||||||
|
|
||||||
|
### Docker Compose Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start gateway
|
||||||
|
ssh nuc "cd ~/openclaw && docker compose up -d openclaw-gateway"
|
||||||
|
|
||||||
|
# Restart gateway
|
||||||
|
ssh nuc "cd ~/openclaw && docker compose restart openclaw-gateway"
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
ssh nuc "docker logs openclaw-openclaw-gateway-1 2>&1 | tail -30"
|
||||||
|
|
||||||
|
# Run CLI commands (use docker exec for commands that need gateway connection)
|
||||||
|
ssh nuc "docker exec openclaw-openclaw-gateway-1 node dist/index.js <command> --token 3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee --url ws://127.0.0.1:18789"
|
||||||
|
|
||||||
|
# Run CLI commands that don't need gateway (use docker compose run)
|
||||||
|
ssh nuc "cd ~/openclaw && script -qc 'docker compose run --rm openclaw-cli <command>' /dev/null"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Pairing
|
||||||
|
|
||||||
|
When the Control UI shows "pairing required", approve the pending device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List pending devices
|
||||||
|
ssh nuc "docker exec openclaw-openclaw-gateway-1 node dist/index.js devices list --token 3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee --url ws://127.0.0.1:18789"
|
||||||
|
|
||||||
|
# Approve a device (use requestId from the list)
|
||||||
|
ssh nuc "docker exec openclaw-openclaw-gateway-1 node dist/index.js devices approve <requestId> --token 3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee --url ws://127.0.0.1:18789"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dashboard URL with embedded token (auto-authenticates):**
|
||||||
|
```
|
||||||
|
https://alezmad-nuc.tail58f5ad.ts.net:8443/#token=3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee
|
||||||
|
```
|
||||||
|
|
||||||
|
### Channel Plugins
|
||||||
|
|
||||||
|
Channels are plugins that must be enabled before use. 35 available, 4 loaded by default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all plugins
|
||||||
|
ssh nuc "script -qc 'cd ~/openclaw && docker compose run --rm openclaw-cli plugins list' /dev/null"
|
||||||
|
|
||||||
|
# Enable a channel plugin
|
||||||
|
ssh nuc "script -qc 'cd ~/openclaw && docker compose run --rm openclaw-cli plugins enable <plugin-id>' /dev/null"
|
||||||
|
|
||||||
|
# Then restart gateway
|
||||||
|
ssh nuc "cd ~/openclaw && docker compose restart openclaw-gateway"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Currently enabled channels:** WhatsApp (linked and active)
|
||||||
|
|
||||||
|
**Available channel plugins:**
|
||||||
|
| Plugin ID | Channel |
|
||||||
|
|-----------|---------|
|
||||||
|
| `whatsapp` | WhatsApp |
|
||||||
|
| `telegram` | Telegram |
|
||||||
|
| `discord` | Discord |
|
||||||
|
| `slack` | Slack |
|
||||||
|
| `signal` | Signal |
|
||||||
|
| `matrix` | Matrix |
|
||||||
|
| `msteams` | Microsoft Teams |
|
||||||
|
| `googlechat` | Google Chat |
|
||||||
|
| `imessage` | iMessage |
|
||||||
|
| `irc` | IRC |
|
||||||
|
|
||||||
|
### Setting Up Channels That Require QR Codes (WhatsApp, etc.)
|
||||||
|
|
||||||
|
**⚠️ The CLI needs a TTY for QR display.** Cannot run directly via `ssh nuc "command"`. Use `script` to fake a TTY and capture output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Enable the plugin
|
||||||
|
ssh nuc "script -qc 'cd ~/openclaw && docker compose run --rm openclaw-cli plugins enable whatsapp' /dev/null"
|
||||||
|
|
||||||
|
# Step 2: Restart gateway
|
||||||
|
ssh nuc "cd ~/openclaw && docker compose restart openclaw-gateway"
|
||||||
|
|
||||||
|
# Step 3: Run login with TTY capture (captures QR to file)
|
||||||
|
ssh nuc "script -q /tmp/openclaw-qr.txt -c 'cd ~/openclaw && docker compose run --rm openclaw-cli channels login'"
|
||||||
|
|
||||||
|
# Step 4: If QR needs to be viewed remotely, copy and render as image
|
||||||
|
scp nuc:/tmp/openclaw-qr.txt /tmp/
|
||||||
|
# Then use Python + Pillow to convert Unicode block chars to PNG
|
||||||
|
```
|
||||||
|
|
||||||
|
**QR to PNG conversion (run locally on Mac):**
|
||||||
|
```python
|
||||||
|
from PIL import Image
|
||||||
|
import re
|
||||||
|
|
||||||
|
with open("/tmp/openclaw-qr.txt", "rb") as f:
|
||||||
|
content = f.read().decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
lines = content.split("\n")
|
||||||
|
qr_lines = []
|
||||||
|
for l in lines:
|
||||||
|
clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", l).replace(chr(0), "")
|
||||||
|
if any(c in clean for c in ["\u2584", "\u2588", "\u2580"]):
|
||||||
|
qr_lines.append(clean)
|
||||||
|
|
||||||
|
scale = 10
|
||||||
|
width = max(len(l) for l in qr_lines)
|
||||||
|
height = len(qr_lines) * 2
|
||||||
|
img = Image.new("RGB", (width * scale, height * scale), "white")
|
||||||
|
|
||||||
|
for row_idx, line in enumerate(qr_lines):
|
||||||
|
for col_idx, ch in enumerate(line):
|
||||||
|
top_black = ch in ["\u2588", "\u2580"]
|
||||||
|
bot_black = ch in ["\u2588", "\u2584"]
|
||||||
|
for dy in range(scale):
|
||||||
|
for dx in range(scale):
|
||||||
|
if top_black:
|
||||||
|
img.putpixel((col_idx*scale+dx, row_idx*2*scale+dy), (0,0,0))
|
||||||
|
if bot_black:
|
||||||
|
img.putpixel((col_idx*scale+dx, (row_idx*2+1)*scale+dy), (0,0,0))
|
||||||
|
|
||||||
|
img.save("/tmp/qr.png")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative: User scans directly** — If the user has SSH terminal access, they can run `channels login` interactively and scan the QR from their terminal.
|
||||||
|
|
||||||
|
### Anthropic Authentication
|
||||||
|
|
||||||
|
Uses a Claude Code OAuth token (valid 1 year). Set in both config and docker-compose env:
|
||||||
|
|
||||||
|
**Config (`~/.openclaw/openclaw.json`):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_API_KEY": "sk-ant-oat01-..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker env (`~/openclaw/.env`):**
|
||||||
|
```
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-oat01-...
|
||||||
|
```
|
||||||
|
|
||||||
|
**To regenerate token (run on Mac where `claude` CLI is installed):**
|
||||||
|
```bash
|
||||||
|
claude setup-token
|
||||||
|
# Copy the output token and update both config + .env on NUC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config Schema (current)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"model": {
|
||||||
|
"primary": "anthropic/claude-sonnet-4-5-20250929"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gateway": {
|
||||||
|
"port": 18789,
|
||||||
|
"mode": "local",
|
||||||
|
"bind": "lan",
|
||||||
|
"auth": {
|
||||||
|
"mode": "token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Config gotchas:**
|
||||||
|
- `agent.model` is **legacy** — use `agents.defaults.model.primary` instead
|
||||||
|
- Run `docker exec openclaw-openclaw-gateway-1 node dist/index.js doctor --fix` to migrate legacy keys
|
||||||
|
- The `gateway.pairing` key does NOT exist — device pairing is managed via the `devices` CLI, not config
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
1. **"control ui requires HTTPS or localhost"**: Access via `https://alezmad-nuc.tail58f5ad.ts.net:8443` (Tailscale Serve), NOT `http://192.168.1.3:18789`
|
||||||
|
|
||||||
|
2. **"pairing required"**: Approve the device via `devices approve` command (see Device Pairing section above)
|
||||||
|
|
||||||
|
3. **"unauthorized: gateway token missing"**: Use the dashboard URL with `#token=...` hash to auto-authenticate
|
||||||
|
|
||||||
|
4. **CLI commands fail with "gateway closed"**: Use `docker exec` into the running gateway container instead of `docker compose run` (the CLI container can't reach the gateway on its internal Docker IP)
|
||||||
|
|
||||||
|
5. **Config "invalid" after edit**: Run `doctor --fix` inside the gateway container to clean up
|
||||||
|
|
||||||
|
6. **Channel "unsupported"**: Enable the plugin first with `plugins enable <id>`, then restart gateway
|
||||||
|
|
||||||
|
7. **CLAUDE_AI_SESSION_KEY warnings**: Harmless — these are for Claude web session auth which isn't used when using API key
|
||||||
|
|||||||
Reference in New Issue
Block a user