fix(rnetwork): add auth headers to graph viewer API calls; add Blender multi-user
- Fix 401 errors on rNetwork by passing encryptid-token as Bearer auth on /api/info, /api/graph, /api/workspaces fetch calls - Add blender-multiuser replication server (multi-user-server:0.5.8) to docker-compose with health check and resource limits - Add Multiplayer tab to folk-blender shape with connection info, server status check, and setup instructions - Add /api/blender-multiuser/status endpoint for TCP health probe Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dc30b8f2e6
commit
70cb919541
|
|
@ -283,6 +283,33 @@ services:
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
|
# ── Blender Multi-User replication server (always-on, persistent TCP) ──
|
||||||
|
blender-multiuser:
|
||||||
|
image: registry.gitlab.com/slumber/multi-user/multi-user-server:0.5.8
|
||||||
|
container_name: blender-multiuser
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 512m
|
||||||
|
cpus: 1
|
||||||
|
ports:
|
||||||
|
- "5555:5555"
|
||||||
|
- "5556:5556"
|
||||||
|
- "5557:5557"
|
||||||
|
- "5558:5558"
|
||||||
|
environment:
|
||||||
|
- port=5555
|
||||||
|
- password=${BLENDER_MULTIUSER_PASSWORD}
|
||||||
|
- timeout=5000
|
||||||
|
- log_level=INFO
|
||||||
|
- log_file=multiuser_server.log
|
||||||
|
networks:
|
||||||
|
- rspace-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python3 -c 'import socket; s=socket.socket(); s.settimeout(2); s.connect((\"localhost\",5555)); s.close()' || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
# ── On-demand sidecars (started/stopped by server/sidecar-manager.ts) ──
|
# ── On-demand sidecars (started/stopped by server/sidecar-manager.ts) ──
|
||||||
# Build: docker compose --profile sidecar build
|
# Build: docker compose --profile sidecar build
|
||||||
# Create: docker compose --profile sidecar create
|
# Create: docker compose --profile sidecar create
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,94 @@ const styles = css`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiplayer-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-dot.online { background: #22c55e; }
|
||||||
|
.mp-dot.offline { background: #ef4444; }
|
||||||
|
|
||||||
|
.mp-field {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-conn {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-conn input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-copy-btn {
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f8fafc;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-copy-btn:hover { background: #e2e8f0; }
|
||||||
|
|
||||||
|
.mp-instructions {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-instructions ol {
|
||||||
|
margin: 4px 0 0 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-instructions a {
|
||||||
|
color: #ea580c;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -318,11 +406,12 @@ export class FolkBlender extends FolkShape {
|
||||||
#renderUrl: string | null = null;
|
#renderUrl: string | null = null;
|
||||||
#script: string | null = null;
|
#script: string | null = null;
|
||||||
#blendUrl: string | null = null;
|
#blendUrl: string | null = null;
|
||||||
#activeTab: "preview" | "code" = "preview";
|
#activeTab: "preview" | "code" | "multiplayer" = "preview";
|
||||||
#promptInput: HTMLTextAreaElement | null = null;
|
#promptInput: HTMLTextAreaElement | null = null;
|
||||||
#generateBtn: HTMLButtonElement | null = null;
|
#generateBtn: HTMLButtonElement | null = null;
|
||||||
#previewArea: HTMLElement | null = null;
|
#previewArea: HTMLElement | null = null;
|
||||||
#codeArea: HTMLElement | null = null;
|
#codeArea: HTMLElement | null = null;
|
||||||
|
#multiplayerArea: HTMLElement | null = null;
|
||||||
#downloadRow: HTMLElement | null = null;
|
#downloadRow: HTMLElement | null = null;
|
||||||
|
|
||||||
override createRenderRoot() {
|
override createRenderRoot() {
|
||||||
|
|
@ -350,6 +439,7 @@ export class FolkBlender extends FolkShape {
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" data-tab="preview">🖼️ Render</button>
|
<button class="tab active" data-tab="preview">🖼️ Render</button>
|
||||||
<button class="tab" data-tab="code">📜 Script</button>
|
<button class="tab" data-tab="code">📜 Script</button>
|
||||||
|
<button class="tab" data-tab="multiplayer">🌐 Multiplayer</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<div class="render-preview">
|
<div class="render-preview">
|
||||||
|
|
@ -361,6 +451,28 @@ export class FolkBlender extends FolkShape {
|
||||||
<div class="code-area" style="display:none">
|
<div class="code-area" style="display:none">
|
||||||
<pre>// Blender Python script will appear here</pre>
|
<pre>// Blender Python script will appear here</pre>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="multiplayer-area">
|
||||||
|
<div class="mp-status">
|
||||||
|
<span class="mp-dot"></span>
|
||||||
|
<span class="mp-status-text">Checking...</span>
|
||||||
|
</div>
|
||||||
|
<div class="mp-field">
|
||||||
|
<label>Connection</label>
|
||||||
|
<div class="mp-conn">
|
||||||
|
<input class="mp-conn-input" readonly value="" />
|
||||||
|
<button class="mp-copy-btn" title="Copy">📋</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mp-instructions">
|
||||||
|
<strong>How to connect:</strong>
|
||||||
|
<ol>
|
||||||
|
<li>Install <a href="https://extensions.blender.org/add-ons/multi-user/" target="_blank" rel="noopener">Multi-User addon</a> in Blender 4.3+</li>
|
||||||
|
<li>Open sidebar → Multi-User tab</li>
|
||||||
|
<li>Paste the connection address above</li>
|
||||||
|
<li>Click Connect — you're in the shared session!</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-row" style="display:none">
|
<div class="download-row" style="display:none">
|
||||||
<a class="download-btn blend-dl" href="#" download>⬇️ .blend</a>
|
<a class="download-btn blend-dl" href="#" download>⬇️ .blend</a>
|
||||||
|
|
@ -378,6 +490,7 @@ export class FolkBlender extends FolkShape {
|
||||||
this.#generateBtn = wrapper.querySelector(".generate-btn");
|
this.#generateBtn = wrapper.querySelector(".generate-btn");
|
||||||
this.#previewArea = wrapper.querySelector(".render-preview");
|
this.#previewArea = wrapper.querySelector(".render-preview");
|
||||||
this.#codeArea = wrapper.querySelector(".code-area");
|
this.#codeArea = wrapper.querySelector(".code-area");
|
||||||
|
this.#multiplayerArea = wrapper.querySelector(".multiplayer-area");
|
||||||
this.#downloadRow = wrapper.querySelector(".download-row");
|
this.#downloadRow = wrapper.querySelector(".download-row");
|
||||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||||
|
|
||||||
|
|
@ -385,12 +498,14 @@ export class FolkBlender extends FolkShape {
|
||||||
wrapper.querySelectorAll(".tab").forEach((tab) => {
|
wrapper.querySelectorAll(".tab").forEach((tab) => {
|
||||||
tab.addEventListener("click", (e) => {
|
tab.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const tabName = (tab as HTMLElement).dataset.tab as "preview" | "code";
|
const tabName = (tab as HTMLElement).dataset.tab as "preview" | "code" | "multiplayer";
|
||||||
this.#activeTab = tabName;
|
this.#activeTab = tabName;
|
||||||
wrapper.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
|
wrapper.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
|
||||||
tab.classList.add("active");
|
tab.classList.add("active");
|
||||||
if (this.#previewArea) this.#previewArea.style.display = tabName === "preview" ? "flex" : "none";
|
if (this.#previewArea) this.#previewArea.style.display = tabName === "preview" ? "flex" : "none";
|
||||||
if (this.#codeArea) this.#codeArea.style.display = tabName === "code" ? "block" : "none";
|
if (this.#codeArea) this.#codeArea.style.display = tabName === "code" ? "block" : "none";
|
||||||
|
if (this.#multiplayerArea) this.#multiplayerArea.style.display = tabName === "multiplayer" ? "block" : "none";
|
||||||
|
if (tabName === "multiplayer") this.#fetchMultiplayerStatus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -430,9 +545,42 @@ export class FolkBlender extends FolkShape {
|
||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// Multiplayer copy button
|
||||||
|
const copyBtn = wrapper.querySelector(".mp-copy-btn") as HTMLButtonElement;
|
||||||
|
const connInput = wrapper.querySelector(".mp-conn-input") as HTMLInputElement;
|
||||||
|
copyBtn?.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (connInput?.value) {
|
||||||
|
navigator.clipboard.writeText(connInput.value).then(() => {
|
||||||
|
copyBtn.textContent = "✅";
|
||||||
|
setTimeout(() => { copyBtn.textContent = "📋"; }, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #fetchMultiplayerStatus() {
|
||||||
|
const dot = this.#multiplayerArea?.querySelector(".mp-dot");
|
||||||
|
const statusText = this.#multiplayerArea?.querySelector(".mp-status-text");
|
||||||
|
const connInput = this.#multiplayerArea?.querySelector(".mp-conn-input") as HTMLInputElement | null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/blender-multiuser/status");
|
||||||
|
const data = await res.json() as { available: boolean; host: string; port: number };
|
||||||
|
if (dot) {
|
||||||
|
dot.classList.toggle("online", data.available);
|
||||||
|
dot.classList.toggle("offline", !data.available);
|
||||||
|
}
|
||||||
|
if (statusText) statusText.textContent = data.available ? "Server Online" : "Server Offline";
|
||||||
|
if (connInput) connInput.value = `${data.host}:${data.port}`;
|
||||||
|
} catch {
|
||||||
|
if (dot) { dot.classList.remove("online"); dot.classList.add("offline"); }
|
||||||
|
if (statusText) statusText.textContent = "Unable to check";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async #generate() {
|
async #generate() {
|
||||||
const prompt = this.#promptInput?.value.trim();
|
const prompt = this.#promptInput?.value.trim();
|
||||||
if (!prompt || this.#isLoading) return;
|
if (!prompt || this.#isLoading) return;
|
||||||
|
|
|
||||||
|
|
@ -280,10 +280,12 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
}
|
}
|
||||||
} catch { /* storage unavailable */ }
|
} catch { /* storage unavailable */ }
|
||||||
|
|
||||||
|
const token = localStorage.getItem("encryptid-token");
|
||||||
|
const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
const [wsRes, infoRes, graphRes] = await Promise.all([
|
const [wsRes, infoRes, graphRes] = await Promise.all([
|
||||||
fetch(`${base}/api/workspaces`),
|
fetch(`${base}/api/workspaces`, { headers }),
|
||||||
fetch(`${base}/api/info`),
|
fetch(`${base}/api/info`, { headers }),
|
||||||
graphData ? Promise.resolve(null) : fetch(`${base}/api/graph${trustParam}`),
|
graphData ? Promise.resolve(null) : fetch(`${base}/api/graph${trustParam}`, { headers }),
|
||||||
]);
|
]);
|
||||||
if (wsRes.ok) this.workspaces = await wsRes.json();
|
if (wsRes.ok) this.workspaces = await wsRes.json();
|
||||||
if (infoRes.ok) this.info = await infoRes.json();
|
if (infoRes.ok) this.info = await infoRes.json();
|
||||||
|
|
|
||||||
|
|
@ -2121,6 +2121,33 @@ Output ONLY the Python code, no explanations or comments outside the code.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Blender Multi-User server status ──
|
||||||
|
const MULTIUSER_HOST = process.env.MULTIUSER_HOST || "159.195.32.209";
|
||||||
|
const MULTIUSER_PORT = 5555;
|
||||||
|
|
||||||
|
app.get("/api/blender-multiuser/status", async (c) => {
|
||||||
|
let available = false;
|
||||||
|
try {
|
||||||
|
const net = await import("node:net");
|
||||||
|
available = await new Promise<boolean>((resolve) => {
|
||||||
|
const sock = net.createConnection({ host: "blender-multiuser", port: MULTIUSER_PORT }, () => {
|
||||||
|
sock.destroy();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
sock.setTimeout(2000);
|
||||||
|
sock.on("timeout", () => { sock.destroy(); resolve(false); });
|
||||||
|
sock.on("error", () => resolve(false));
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
available,
|
||||||
|
host: MULTIUSER_HOST,
|
||||||
|
port: MULTIUSER_PORT,
|
||||||
|
instructions: "Install Multi-User addon from extensions.blender.org, connect to host:port in Blender",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Design Agent (Scribus DTP via Gemini tool loop) ──
|
// ── Design Agent (Scribus DTP via Gemini tool loop) ──
|
||||||
const SCRIBUS_BRIDGE_URL = process.env.SCRIBUS_BRIDGE_URL || "http://scribus-novnc:8765";
|
const SCRIBUS_BRIDGE_URL = process.env.SCRIBUS_BRIDGE_URL || "http://scribus-novnc:8765";
|
||||||
const SCRIBUS_BRIDGE_SECRET = process.env.SCRIBUS_BRIDGE_SECRET || "";
|
const SCRIBUS_BRIDGE_SECRET = process.env.SCRIBUS_BRIDGE_SECRET || "";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue