| <!doctype html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Vista previa del app</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| </head> |
| <body class="bg-slate-100 text-slate-900"> |
| <div class="max-w-5xl mx-auto p-6 space-y-6"> |
| <header class="flex items-center justify-between gap-4"> |
| <div> |
| <h1 class="text-3xl font-bold">Vista previa del app</h1> |
| <p class="text-slate-600"> |
| Aquí puedes ver cómo se despliega el visualizador con una API key de |
| ejemplo. |
| </p> |
| </div> |
| <a |
| href="/admin" |
| class="px-4 py-2 bg-slate-800 text-white rounded-xl hover:bg-slate-900" |
| >Ir al panel</a |
| > |
| </header> |
|
|
| <div |
| class="rounded-3xl overflow-hidden border border-slate-200 shadow-lg" |
| > |
| <iframe |
| id="preview-iframe" |
| src="" |
| class="w-full min-h-[600px] border-0" |
| ></iframe> |
| </div> |
| <div class="rounded-3xl bg-white p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-3">Puente de desarrollo</h2> |
| <p class="text-slate-700 mb-4"> |
| Esta vista previa se conecta con el app React embebido. Puedes enviar |
| comandos y ver las respuestas del iframe. |
| </p> |
| <div class="grid gap-4 md:grid-cols-2 mb-4"> |
| <div> |
| <label class="block text-sm font-medium text-slate-700 mb-2"> |
| Cliente de prueba |
| </label> |
| <select |
| id="client-select" |
| class="w-full rounded-2xl border border-slate-300 px-4 py-3" |
| > |
| <option value="ID_UNICO_DEL_CLIENTE_001"> |
| ID_UNICO_DEL_CLIENTE_001 |
| </option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-slate-700 mb-2"> |
| Comando personalizado |
| </label> |
| <input |
| id="custom-command" |
| type="text" |
| placeholder="Ej. set-color:#f97316" |
| class="w-full rounded-2xl border border-slate-300 px-4 py-3" |
| /> |
| </div> |
| </div> |
| <div class="flex flex-wrap gap-3 mb-4"> |
| <button |
| id="load-client" |
| class="px-4 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700" |
| > |
| Cargar cliente seleccionado |
| </button> |
| <button |
| id="send-ping" |
| class="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700" |
| > |
| Enviar ping |
| </button> |
| <button |
| id="request-status" |
| class="px-4 py-2 bg-slate-200 text-slate-900 rounded-xl hover:bg-slate-300" |
| > |
| Pedir estado al app |
| </button> |
| <button |
| id="send-custom" |
| class="px-4 py-2 bg-green-600 text-white rounded-xl hover:bg-green-700" |
| > |
| Enviar comando |
| </button> |
| </div> |
| <div |
| id="bridge-log" |
| class="rounded-2xl border border-slate-200 bg-slate-50 p-4 text-slate-700 h-52 overflow-y-auto" |
| ></div> |
| </div> |
| <script> |
| const iframe = document.getElementById("preview-iframe"); |
| const bridgeLog = document.getElementById("bridge-log"); |
| const clientSelect = document.getElementById("client-select"); |
| const customCommand = document.getElementById("custom-command"); |
| |
| function addLog(message) { |
| const item = document.createElement("div"); |
| item.className = "text-sm mb-2"; |
| item.textContent = message; |
| bridgeLog.appendChild(item); |
| bridgeLog.scrollTop = bridgeLog.scrollHeight; |
| } |
| |
| async function loadClients() { |
| try { |
| const res = await fetch("/api/keys"); |
| const data = await res.json(); |
| if (!res.ok) { |
| throw new Error(data.error || "No se pudo cargar los clientes."); |
| } |
| clientSelect.innerHTML = data.keys |
| .map( |
| (key) => |
| `<option value="${key.client_id}">${key.client_id} - ${key.nombre}</option>`, |
| ) |
| .join(""); |
| addLog(`Clientes cargados: ${data.keys.length}`); |
| } catch (error) { |
| addLog(`Error al cargar clientes: ${error.message || error}`); |
| } |
| } |
| |
| async function checkEnvironment() { |
| try { |
| const res = await fetch("/health"); |
| const data = await res.json(); |
| if (!res.ok || !data.frontend_ready) { |
| addLog( |
| "Ambiente no listo: asegúrate de tener backend y frontend levantados.", |
| ); |
| return false; |
| } |
| addLog("Ambiente OK: backend y frontend están levantados."); |
| return true; |
| } catch (error) { |
| addLog( |
| "No se pudo verificar el ambiente. El backend debe estar corriendo.", |
| ); |
| return false; |
| } |
| } |
| |
| async function loadPreviewToken() { |
| const backendOk = await checkEnvironment(); |
| if (!backendOk) return; |
| const clientId = clientSelect.value || "ID_UNICO_DEL_CLIENTE_001"; |
| try { |
| const res = await fetch("/api/token", { |
| method: "POST", |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, |
| body: `client_id=${encodeURIComponent(clientId)}`, |
| }); |
| const data = await res.json(); |
| if (!res.ok) { |
| throw new Error(data.error || "No se pudo generar token."); |
| } |
| iframe.src = `/app?token=${encodeURIComponent(data.token)}`; |
| addLog(`Token generado para ${clientId}`); |
| } catch (error) { |
| addLog(`Error generando token: ${error.message || error}`); |
| } |
| } |
| |
| function sendBridgeCommand(command, payload) { |
| if (!iframe.contentWindow) { |
| addLog("El iframe aún no está listo."); |
| return; |
| } |
| iframe.contentWindow.postMessage( |
| { type: "preview-command", command, payload }, |
| "*", |
| ); |
| addLog( |
| `Enviado comando al app: ${command}${payload ? ` ${JSON.stringify(payload)}` : ""}`, |
| ); |
| } |
| |
| window.addEventListener("message", (event) => { |
| if (event.source !== iframe.contentWindow) return; |
| const data = event.data; |
| if (!data || typeof data !== "object") return; |
| |
| if (data.type === "saas-session-active") { |
| addLog(`React indicó sesión activa: ${data.nombreCliente}`); |
| } |
| if (data.type === "app-loaded") { |
| addLog(`React cargado con cliente: ${data.clientId}`); |
| } |
| if (data.type === "preview-response") { |
| addLog(`React responde: ${JSON.stringify(data)}`); |
| } |
| }); |
| |
| document.getElementById("load-client").addEventListener("click", () => { |
| loadPreviewToken(); |
| }); |
| |
| document.getElementById("send-ping").addEventListener("click", () => { |
| sendBridgeCommand("ping"); |
| }); |
| |
| document |
| .getElementById("request-status") |
| .addEventListener("click", () => { |
| sendBridgeCommand("status"); |
| }); |
| |
| document.getElementById("send-custom").addEventListener("click", () => { |
| const commandText = customCommand.value.trim(); |
| if (!commandText) { |
| addLog("Ingresa un comando personalizado."); |
| return; |
| } |
| const [command, payloadString] = commandText.split(":", 2); |
| let payload; |
| if (payloadString) { |
| payload = { value: payloadString }; |
| } |
| sendBridgeCommand(command, payload); |
| }); |
| |
| loadClients().then(loadPreviewToken); |
| </script> |
|
|
| <div class="rounded-3xl bg-white p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-3">Nota</h2> |
| <p class="text-slate-700"> |
| Esta vista previa genera un token de acceso para el cliente |
| <strong>ID_UNICO_DEL_CLIENTE_001</strong> y carga el app con él. |
| </p> |
| </div> |
| </div> |
| </body> |
| </html> |
|
|