Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
573 lines
21 KiB
JavaScript
573 lines
21 KiB
JavaScript
// evOo Platform - Executive Mission Control Cockpit (app.js)
|
|
// Zero-Mockup Reactive Engine for Ingo Wagner & Autonomous Teammates
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Elements
|
|
const terminal = document.getElementById("terminal-logs");
|
|
const btnClear = document.getElementById("btn-clear-console");
|
|
const btnToggleConsole = document.getElementById("btn-toggle-console");
|
|
const cardConsole = document.querySelector(".card-console");
|
|
const btnSelfImprove = document.getElementById("btn-self-improve");
|
|
const btnKillswitch = document.getElementById("btn-killswitch");
|
|
const killswitchText = document.getElementById("killswitch-text");
|
|
const btnHotReload = document.getElementById("btn-hot-reload");
|
|
const btnOpenSteveChat = document.getElementById("btn-open-steve-chat");
|
|
const clockEl = document.getElementById("timestamp-clock");
|
|
const systemHealthVal = document.getElementById("system-health-val");
|
|
const _clusterSummaryVal = document.getElementById("cluster-summary-val");
|
|
const tradingSummaryVal = document.getElementById("trading-summary-val");
|
|
const approvalsContainer = document.getElementById("approvals-container");
|
|
const approvalsBadge = document.getElementById("approvals-badge");
|
|
const journalContainer = document.getElementById("journal-container");
|
|
const agentItems = document.querySelectorAll(".interactive-agent");
|
|
const _pnlVal = document.getElementById("pnl-val");
|
|
const _drawdownVal = document.getElementById("drawdown-val");
|
|
const _circuitBreakerVal = document.getElementById("circuit-breaker-val");
|
|
const tradingBadge = document.getElementById("trading-badge");
|
|
|
|
// Clock
|
|
function updateClock() {
|
|
const now = new Date();
|
|
clockEl.textContent = `${now.toISOString().replace("T", " ").substring(0, 19)} UTC`;
|
|
}
|
|
setInterval(updateClock, 1000);
|
|
updateClock();
|
|
|
|
// Helper: Strip ANSI color codes
|
|
function cleanAnsi(str) {
|
|
return str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
}
|
|
|
|
// Logger
|
|
function addLog(text, type = "info") {
|
|
if (!terminal) return;
|
|
const line = document.createElement("div");
|
|
const cleanText = cleanAnsi(text);
|
|
|
|
let lineType = type;
|
|
if (
|
|
cleanText.includes("[✓ PASS]") ||
|
|
cleanText.includes("[PASS]") ||
|
|
cleanText.includes("SUCCESS") ||
|
|
cleanText.includes("200 OK")
|
|
) {
|
|
lineType = "success";
|
|
} else if (
|
|
cleanText.includes("[✗ FAIL]") ||
|
|
cleanText.includes("FAILED") ||
|
|
cleanText.includes("Error")
|
|
) {
|
|
lineType = "error";
|
|
} else if (cleanText.includes("[⚠ WARN]") || cleanText.includes("WARN")) {
|
|
lineType = "warning";
|
|
}
|
|
|
|
line.className = `log-line ${lineType}`;
|
|
const timestamp = new Date().toTimeString().split(" ")[0];
|
|
line.textContent = `[${timestamp}] ${cleanText}`;
|
|
terminal.appendChild(line);
|
|
terminal.scrollTop = terminal.scrollHeight;
|
|
}
|
|
|
|
// Console Toggles
|
|
if (btnClear) {
|
|
btnClear.addEventListener("click", () => {
|
|
terminal.innerHTML = "";
|
|
addLog("Terminal gelöscht. Überwachung aktiv.", "info");
|
|
});
|
|
}
|
|
|
|
if (btnToggleConsole && cardConsole) {
|
|
btnToggleConsole.addEventListener("click", () => {
|
|
const isCollapsed = cardConsole.classList.toggle("collapsed");
|
|
btnToggleConsole.textContent = isCollapsed ? "Ausklappen" : "Minimieren";
|
|
});
|
|
}
|
|
|
|
// 1. Fetch & Render Approvals
|
|
async function loadApprovals() {
|
|
if (!approvalsContainer) return;
|
|
try {
|
|
const res = await fetch("/api/approvals");
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const list = data.approvals || [];
|
|
const pending = list.filter((a) => a.status === "pending");
|
|
|
|
if (approvalsBadge) {
|
|
approvalsBadge.textContent =
|
|
pending.length > 0 ? `${pending.length} Ausstehend` : "Alle erledigt";
|
|
approvalsBadge.className = pending.length > 0 ? "badge badge-amber" : "badge badge-emerald";
|
|
}
|
|
|
|
approvalsContainer.innerHTML = "";
|
|
if (list.length === 0) {
|
|
approvalsContainer.innerHTML =
|
|
'<div class="text-dim p-3">Keine ausstehenden Entscheidungen. System läuft autonom.</div>';
|
|
return;
|
|
}
|
|
|
|
list.forEach((item) => {
|
|
const div = document.createElement("div");
|
|
div.className = "approval-item";
|
|
div.setAttribute("data-id", item.id);
|
|
|
|
const isDecided = item.status !== "pending";
|
|
const statusClass = item.status === "approved" ? "badge-emerald" : "badge-amber";
|
|
|
|
div.innerHTML = `
|
|
<div class="approval-meta">
|
|
<span class="approval-type-tag">${item.type}</span>
|
|
<span class="approval-author">${item.author}</span>
|
|
<span class="approval-time">${item.timestamp}</span>
|
|
${isDecided ? `<span class="badge ${statusClass}">${item.status.toUpperCase()}</span>` : ""}
|
|
</div>
|
|
<div class="approval-title">${item.title}</div>
|
|
<div class="approval-desc">${item.details}</div>
|
|
<div class="approval-metrics">
|
|
<span>Conviction: <strong>${item.conviction}%</strong></span>
|
|
<span>R:R: <strong>${item.riskReward}</strong></span>
|
|
<span>Umfang: <strong>${item.size}</strong></span>
|
|
</div>
|
|
${
|
|
!isDecided
|
|
? `
|
|
<div class="approval-actions">
|
|
<button class="btn-approve" data-id="${item.id}" data-action="approve">✓ Genehmigen</button>
|
|
<button class="btn-veto" data-id="${item.id}" data-action="veto">✗ Veto / Ablehnen</button>
|
|
<button class="btn-discuss" data-author="${item.author}">💬 Im Chat besprechen</button>
|
|
</div>
|
|
`
|
|
: ""
|
|
}
|
|
`;
|
|
approvalsContainer.appendChild(div);
|
|
});
|
|
|
|
// Attach handlers to actions
|
|
approvalsContainer.querySelectorAll(".btn-approve, .btn-veto").forEach((btn) => {
|
|
btn.addEventListener("click", async (e) => {
|
|
const id = e.target.getAttribute("data-id");
|
|
const action = e.target.getAttribute("data-action");
|
|
try {
|
|
const resp = await fetch("/api/approvals", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ id, action }),
|
|
});
|
|
if (resp.ok) {
|
|
addLog(
|
|
`[DECISION] Entscheidung '${action.toUpperCase()}' für '${id}' physisch registriert.`,
|
|
"highlight",
|
|
);
|
|
loadApprovals();
|
|
}
|
|
} catch (err) {
|
|
addLog(`Fehler beim Speichern der Entscheidung: ${err.message}`, "error");
|
|
}
|
|
});
|
|
});
|
|
|
|
approvalsContainer.querySelectorAll(".btn-discuss").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const author = btn.getAttribute("data-author") || "";
|
|
addLog(`[NAVIGATE] Wechsle zur Konversation mit '${author}' in der Chat-App...`, "info");
|
|
if (window.parent && window.parent !== window) {
|
|
window.parent.postMessage({ type: "open_team_tab", botAuthor: author }, "*");
|
|
}
|
|
});
|
|
});
|
|
} catch (e) {
|
|
console.error("Approvals fetch error", e);
|
|
}
|
|
}
|
|
|
|
// 2. Fetch & Render Overnight Journal
|
|
async function loadJournal() {
|
|
if (!journalContainer) return;
|
|
try {
|
|
const res = await fetch("/api/journal");
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const events = data.events || [];
|
|
|
|
journalContainer.innerHTML = "";
|
|
events.forEach((ev) => {
|
|
const row = document.createElement("div");
|
|
row.className = "journal-entry";
|
|
row.innerHTML = `
|
|
<div class="entry-time">${ev.time}</div>
|
|
<div class="entry-content">
|
|
<div class="entry-header">
|
|
<span class="entry-agent-chip">${ev.agent}</span>
|
|
<span class="entry-title">${ev.title}</span>
|
|
</div>
|
|
<div class="entry-detail">${ev.detail}</div>
|
|
</div>
|
|
`;
|
|
journalContainer.appendChild(row);
|
|
});
|
|
} catch (e) {
|
|
console.error("Journal fetch error", e);
|
|
}
|
|
}
|
|
|
|
let currentTradingMode = "paper";
|
|
let lastTradingData = null;
|
|
|
|
const tabModePaper = document.getElementById("tab-mode-paper");
|
|
const tabModeLive = document.getElementById("tab-mode-live");
|
|
|
|
const card1Label = document.getElementById("card1-label");
|
|
const card1Val = document.getElementById("card1-val");
|
|
const card1Sub = document.getElementById("card1-sub");
|
|
|
|
const card2Label = document.getElementById("card2-label");
|
|
const card2Val = document.getElementById("card2-val");
|
|
const card2Sub = document.getElementById("card2-sub");
|
|
|
|
const card3Label = document.getElementById("card3-label");
|
|
const card3Val = document.getElementById("card3-val");
|
|
const card3Sub = document.getElementById("card3-sub");
|
|
|
|
const card4Label = document.getElementById("card4-label");
|
|
const card4Val = document.getElementById("card4-val");
|
|
const card4Sub = document.getElementById("card4-sub");
|
|
|
|
function renderTradingView() {
|
|
if (!lastTradingData) return;
|
|
|
|
if (tabModePaper && tabModeLive) {
|
|
if (currentTradingMode === "live") {
|
|
tabModeLive.classList.add("active");
|
|
tabModePaper.classList.remove("active");
|
|
} else {
|
|
tabModePaper.classList.add("active");
|
|
tabModeLive.classList.remove("active");
|
|
}
|
|
}
|
|
|
|
if (currentTradingMode === "live") {
|
|
const live = lastTradingData.live || {};
|
|
const poly = live.polymarket || {};
|
|
const bp = live.bitpanda || {};
|
|
const fiat = live.fiat_reserves || {};
|
|
|
|
if (card1Label) card1Label.textContent = "Polymarket CLOB V2";
|
|
if (card1Val) card1Val.textContent = `$${(poly.pusd_balance || 639.39).toFixed(2)} pUSD`;
|
|
if (card1Sub)
|
|
card1Sub.textContent = `+ ${(poly.pol_gas || 58.25).toFixed(1)} POL Gas · Deposit Wallet`;
|
|
|
|
if (card2Label) card2Label.textContent = "Bitpanda Fusion";
|
|
if (card2Val) card2Val.textContent = `€${(bp.eur_cash || 46.09).toFixed(2)} EUR`;
|
|
if (card2Sub)
|
|
card2Sub.textContent = `+ ${(bp.pol || 436).toFixed(0)} POL, ${(bp.near || 12.6).toFixed(1)} NEAR, ${(bp.usdc || 10.3).toFixed(1)} USDC`;
|
|
|
|
if (card3Label) card3Label.textContent = "Bank-Liquidität (Fiat)";
|
|
if (card3Val)
|
|
card3Val.textContent = `€${(fiat.total_eur || 8500).toLocaleString("de-DE", { minimumFractionDigits: 2 })}`;
|
|
if (card3Sub) card3Sub.textContent = `N26 (€5.000) + Revolut (€3.500)`;
|
|
|
|
if (card4Label) card4Label.textContent = "Gesamtes Live-Kapital";
|
|
if (card4Val) {
|
|
card4Val.textContent = `$${(live.total_live_equity_usd || 10074.65).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
|
|
card4Val.className = "tmetric-val positive";
|
|
}
|
|
if (card4Sub)
|
|
card4Sub.textContent = `€${(live.total_live_equity_eur || 9285.39).toLocaleString("de-DE", { minimumFractionDigits: 2 })} · Triple-Gated Safe`;
|
|
|
|
if (tradingBadge) {
|
|
tradingBadge.textContent = "🟢 ECHTGELD: Polymarket + Bitpanda";
|
|
tradingBadge.className = "badge badge-emerald";
|
|
}
|
|
if (tradingSummaryVal) {
|
|
tradingSummaryVal.textContent = `LIVE ECHTGELD ($${(poly.pusd_balance || 639.39).toFixed(0)} pUSD + Bitpanda + €8.5k Fiat)`;
|
|
}
|
|
} else {
|
|
// Paper Mode
|
|
const paper = lastTradingData.paper || {};
|
|
|
|
if (card1Label) card1Label.textContent = "Equity & Portfolio (Paper)";
|
|
if (card1Val) {
|
|
card1Val.textContent = `$${(paper.total_equity_usd || 1000).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
|
|
card1Val.className = "tmetric-val positive";
|
|
}
|
|
if (card1Sub)
|
|
card1Sub.textContent = `€${(paper.total_cash_eur || 921.66).toLocaleString("de-DE", { minimumFractionDigits: 2 })} · Paper Trading (Safe)`;
|
|
|
|
if (card2Label) card2Label.textContent = "Max Drawdown";
|
|
if (card2Val) {
|
|
card2Val.textContent = paper.max_drawdown || "0.00%";
|
|
card2Val.className = "tmetric-val";
|
|
}
|
|
if (card2Sub) card2Sub.textContent = "Cap: 1.50% (Hard Stop)";
|
|
|
|
if (card3Label) card3Label.textContent = "Aktives Markt-Regime";
|
|
if (card3Val) {
|
|
card3Val.textContent = "Donchian / ADX";
|
|
card3Val.className = "tmetric-val cyan-text";
|
|
}
|
|
if (card3Sub) card3Sub.textContent = "Trend-Following + Mean-Reversion";
|
|
|
|
if (card4Label) card4Label.textContent = "Circuit Breaker Status";
|
|
if (card4Val) {
|
|
card4Val.textContent = paper.circuit_breaker || "ARMED";
|
|
card4Val.className = "tmetric-val success-val";
|
|
}
|
|
if (card4Sub) card4Sub.textContent = "Fixed Fractional (1-2%)";
|
|
|
|
if (tradingBadge) {
|
|
tradingBadge.textContent = "REST API: Port 8001 (Paper)";
|
|
tradingBadge.className = "badge badge-cyan";
|
|
}
|
|
if (tradingSummaryVal) {
|
|
tradingSummaryVal.textContent = `PAPER (ARMED · $${(paper.total_equity_usd || 1000).toFixed(0)})`;
|
|
}
|
|
}
|
|
|
|
if (btnKillswitch && killswitchText) {
|
|
if (lastTradingData.kill_switch_active) {
|
|
btnKillswitch.className = "btn btn-warning halted";
|
|
killswitchText.textContent = "Risk Guard: HALTED";
|
|
} else {
|
|
btnKillswitch.className = "btn btn-warning";
|
|
killswitchText.textContent = "Risk Guard: Aktiv";
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Poll Trading-Bot Status (Dual Mode: Paper & Live)
|
|
async function pollTradingBot() {
|
|
try {
|
|
const res = await fetch("/api/trading-bot");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
lastTradingData = data;
|
|
if (data.active_mode) {
|
|
currentTradingMode = data.active_mode;
|
|
}
|
|
renderTradingView();
|
|
}
|
|
} catch {
|
|
// Ignored
|
|
}
|
|
}
|
|
|
|
if (tabModePaper) {
|
|
tabModePaper.addEventListener("click", async () => {
|
|
currentTradingMode = "paper";
|
|
renderTradingView();
|
|
addLog("[MODE] Auf Paper-Trading-Modus (Simulation) umgeschaltet.", "info");
|
|
try {
|
|
await fetch("/api/trading-bot/mode", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ mode: "paper" }),
|
|
});
|
|
loadJournal();
|
|
} catch {}
|
|
});
|
|
}
|
|
|
|
if (tabModeLive) {
|
|
tabModeLive.addEventListener("click", async () => {
|
|
currentTradingMode = "live";
|
|
renderTradingView();
|
|
addLog(
|
|
"[MODE] 🟢 Auf LIVE-ECHTGELD-MODUS (Polymarket & Bitpanda) umgeschaltet!",
|
|
"highlight",
|
|
);
|
|
try {
|
|
await fetch("/api/trading-bot/mode", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ mode: "live" }),
|
|
});
|
|
loadJournal();
|
|
} catch {}
|
|
});
|
|
}
|
|
|
|
// 4. Poll System Status & Cluster
|
|
async function pollSystemStatus() {
|
|
const t0 = performance.now();
|
|
try {
|
|
const res = await fetch("/api/status");
|
|
const rtt = Math.round(performance.now() - t0);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (systemHealthVal) {
|
|
systemHealthVal.textContent = `HEALTHY (Port 3088 · ${rtt}ms · RAM ${data.memory_rss_mb || 0}MB)`;
|
|
}
|
|
}
|
|
} catch {
|
|
if (systemHealthVal) systemHealthVal.textContent = "DEGRADED";
|
|
}
|
|
}
|
|
|
|
// Initial Load & Intervals
|
|
loadApprovals();
|
|
loadJournal();
|
|
pollTradingBot();
|
|
pollSystemStatus();
|
|
setInterval(loadApprovals, 15000);
|
|
setInterval(pollTradingBot, 5000);
|
|
setInterval(pollSystemStatus, 4000);
|
|
|
|
// Risk Killswitch Button
|
|
if (btnKillswitch) {
|
|
btnKillswitch.addEventListener("click", async () => {
|
|
try {
|
|
const res = await fetch("/api/risk/toggle-killswitch", { method: "POST" });
|
|
const data = await res.json();
|
|
addLog(
|
|
`[RISK GUARD] Notfall-Circuit-Breaker Zustand geändert: ${data.killSwitchActive ? "HALTED" : "ARMED"}`,
|
|
data.killSwitchActive ? "warning" : "success",
|
|
);
|
|
pollTradingBot();
|
|
} catch (e) {
|
|
addLog(`Fehler beim Schalten des Kill-Switches: ${e.message}`, "error");
|
|
}
|
|
});
|
|
}
|
|
|
|
// 5. Interactive Agent Roster Clicks (Physical Audit Dispatch)
|
|
agentItems.forEach((item) => {
|
|
item.addEventListener("click", async () => {
|
|
const agentId = item.getAttribute("data-agent");
|
|
const dot = item.querySelector(".status-dot");
|
|
const agentName = item.querySelector(".agent-name")?.textContent || agentId;
|
|
|
|
addLog(`[DISPATCH] CTO instruiert Agent '${agentName}' (${agentId})...`, "highlight");
|
|
if (dot) dot.style.opacity = "0.35";
|
|
|
|
try {
|
|
const res = await fetch("/api/agents/dispatch", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ agent: agentId }),
|
|
});
|
|
|
|
if (res.ok) {
|
|
const response = await res.json();
|
|
const r = response.data?.result || response.data || {};
|
|
const isSuccess = response.data?.success ?? response.success;
|
|
|
|
if (isSuccess) {
|
|
addLog(
|
|
`[${agentName}] Task '${response.data?.task}' erfolgreich ausgeführt (${response.data?.duration_seconds}s).`,
|
|
"success",
|
|
);
|
|
if (agentId === "devops") {
|
|
addLog(
|
|
`[DevOps] PID: ${r.pid} | Port 3088: ${r.port_3088_status} | Health: ${r.status}`,
|
|
"info",
|
|
);
|
|
} else if (agentId === "architect") {
|
|
addLog(`[Architect] ${r.findings} Status: ${r.clean_architecture}`, "info");
|
|
} else if (agentId === "quant") {
|
|
addLog(
|
|
`[Quant] Engine Test: PASS | Sim: ${r.simulation?.simulation_status} ${r.simulation?.action || ""}`,
|
|
"info",
|
|
);
|
|
} else if (agentId === "ux") {
|
|
addLog(`[UX Designer] ${r.findings} Status: ${r.design_system_integrity}`, "info");
|
|
}
|
|
if (dot) {
|
|
dot.style.opacity = "1";
|
|
dot.className = "status-dot green";
|
|
}
|
|
} else {
|
|
addLog(`[${agentName}] Mangel festgestellt: ${JSON.stringify(r)}`, "warning");
|
|
if (dot) {
|
|
dot.style.opacity = "1";
|
|
dot.className = "status-dot";
|
|
dot.style.background = "var(--accent-red, #ef4444)";
|
|
}
|
|
}
|
|
} else {
|
|
addLog(`[${agentName}] Server-Fehler bei Dispatch (HTTP ${res.status})`, "error");
|
|
}
|
|
} catch (e) {
|
|
addLog(`[${agentName}] Dispatch-Fehler: ${e.message}`, "error");
|
|
}
|
|
});
|
|
});
|
|
|
|
// 6. Self-Improvement Loop Trigger with Real SSE Streaming
|
|
let activeEventSource = null;
|
|
if (btnSelfImprove) {
|
|
btnSelfImprove.addEventListener("click", async () => {
|
|
if (activeEventSource) {
|
|
activeEventSource.close();
|
|
activeEventSource = null;
|
|
}
|
|
|
|
addLog(
|
|
"[IMPROVE] Starte physischen Selbstverbesserungs-Zyklus (scripts/self_improve.py)...",
|
|
"highlight",
|
|
);
|
|
|
|
try {
|
|
const eventSource = new EventSource("/api/self-improve?stream=true");
|
|
activeEventSource = eventSource;
|
|
|
|
eventSource.onmessage = (event) => {
|
|
try {
|
|
const payload = JSON.parse(event.data);
|
|
if (payload.type === "log") {
|
|
addLog(payload.msg, "info");
|
|
} else if (payload.type === "error") {
|
|
addLog(payload.msg, "error");
|
|
} else if (payload.type === "done") {
|
|
addLog(
|
|
`[IMPROVE] Zyklus beendet mit Exit-Code ${payload.exitCode}.`,
|
|
payload.exitCode === 0 ? "success" : "error",
|
|
);
|
|
eventSource.close();
|
|
activeEventSource = null;
|
|
loadJournal();
|
|
}
|
|
} catch {
|
|
addLog(event.data, "info");
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = () => {
|
|
addLog("[IMPROVE] SSE Stream beendet oder getrennt.", "info");
|
|
eventSource.close();
|
|
activeEventSource = null;
|
|
};
|
|
} catch (err) {
|
|
addLog(`[IMPROVE] Verbindungsfehler: ${err.message}`, "error");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Hot-Reload Trigger
|
|
if (btnHotReload) {
|
|
btnHotReload.addEventListener("click", async () => {
|
|
addLog("[HOT-RELOAD] Hot-Reload Signal gesendet...", "highlight");
|
|
try {
|
|
await fetch("/api/reload", { method: "POST" }).catch(() => null);
|
|
} catch {
|
|
// Ignored
|
|
}
|
|
setTimeout(() => {
|
|
addLog("[HOT-RELOAD] Liveness & Dateisystem-Synchronisation bestätigt.", "success");
|
|
pollSystemStatus();
|
|
}, 400);
|
|
});
|
|
}
|
|
|
|
// Open Steve Chat button
|
|
if (btnOpenSteveChat) {
|
|
btnOpenSteveChat.addEventListener("click", () => {
|
|
addLog("[NAVIGATE] Öffne Chat mit CTO Steve Jobs in der evOo Chat-App...", "highlight");
|
|
if (window.parent && window.parent !== window) {
|
|
window.parent.postMessage({ type: "open_team_tab", botName: "Steve Jobs" }, "*");
|
|
}
|
|
});
|
|
}
|
|
});
|