{"id":3634,"date":"2026-03-18T01:46:27","date_gmt":"2026-03-18T01:46:27","guid":{"rendered":"https:\/\/mboprinters.com\/?page_id=3634"},"modified":"2026-03-18T01:46:29","modified_gmt":"2026-03-18T01:46:29","slug":"app-crea-posts","status":"publish","type":"page","link":"https:\/\/mboprinters.com\/de\/app-crea-posts","title":{"rendered":"app crea posts"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/>\n  <title>Bomedia Content Dashboard<\/title>\n  <link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\" \/>\n  <link href=\"https:\/\/fonts.googleapis.com\/css2?family=DM+Sans:wght@300;400;500;600&#038;family=DM+Mono:wght@400;500&#038;display=swap\" rel=\"stylesheet\" \/>\n  <style>\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    :root {\n      --bg:       #f4f3ef;\n      --surface:  #ffffff;\n      --surface2: #edece8;\n      --border:   #e0dfd9;\n      --text:     #1a1917;\n      --muted:    #6b6962;\n      --accent:   #d4540a;\n      --accent2:  #f0e9df;\n      --green:    #1a6645;\n      --green-bg: #e3f3ec;\n      --amber:    #7a4a00;\n      --amber-bg: #fff0d4;\n      --red:      #9e1c1c;\n      --red-bg:   #fdeaea;\n      --shadow:   0 2px 12px rgba(0,0,0,.07);\n      --radius:   14px;\n      --radius-sm:8px;\n    }\n\n    body {\n      font-family: 'DM Sans', sans-serif;\n      background: var(--bg);\n      color: var(--text);\n      min-height: 100vh;\n      padding: 28px 20px 60px;\n    }\n\n    \/* \u2500\u2500 Layout \u2500\u2500 *\/\n    .container { max-width: 1100px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }\n\n    \/* \u2500\u2500 Card \u2500\u2500 *\/\n    .card {\n      background: var(--surface);\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      box-shadow: var(--shadow);\n    }\n    .card-body  { padding: 24px; }\n    .card-title { font-size: 1.1rem; font-weight: 600; display: flex; align-items: center; gap: 8px; }\n\n    \/* \u2500\u2500 Header \u2500\u2500 *\/\n    .header-top { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 20px; }\n    .header-top h1 {\n      font-size: clamp(1.6rem, 4vw, 2.1rem);\n      font-weight: 600;\n      letter-spacing: -.02em;\n      line-height: 1.15;\n    }\n    .logo-dot { color: var(--accent); }\n    .header-desc { font-size: .875rem; color: var(--muted); line-height: 1.6; max-width: 680px; margin-top: 6px; }\n\n    .stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-top: 20px; }\n    .stat-box { background: var(--surface2); border-radius: var(--radius-sm); padding: 14px 16px; }\n    .stat-label { font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }\n    .stat-value { font-size: 1.45rem; font-weight: 600; margin-top: 4px; font-family: 'DM Mono', monospace; }\n    .stat-value.sm { font-size: .85rem; font-weight: 500; font-family: 'DM Sans', sans-serif; color: var(--muted); line-height: 1.4; margin-top: 6px; }\n\n    \/* \u2500\u2500 Alerts \u2500\u2500 *\/\n    .alert { border-radius: var(--radius-sm); padding: 12px 16px; font-size: .875rem; line-height: 1.5; display: flex; gap: 10px; align-items: flex-start; }\n    .alert-green { background: var(--green-bg); color: var(--green); border: 1px solid #b5dfc9; }\n    .alert-red   { background: var(--red-bg);   color: var(--red);   border: 1px solid #f3c0c0; }\n    .alert svg   { flex-shrink: 0; margin-top: 1px; }\n\n    \/* \u2500\u2500 Tabs \u2500\u2500 *\/\n    .tabs-nav { display: flex; gap: 4px; background: var(--surface2); border-radius: 10px; padding: 4px; width: fit-content; }\n    .tab-btn {\n      background: none; border: none; padding: 8px 22px;\n      font-size: .875rem; font-weight: 500; font-family: inherit;\n      color: var(--muted); cursor: pointer; border-radius: 8px;\n      transition: background .15s, color .15s;\n    }\n    .tab-btn.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 4px rgba(0,0,0,.1); }\n\n    \/* \u2500\u2500 Controls bar \u2500\u2500 *\/\n    .controls { display: grid; grid-template-columns: 1fr 110px auto; gap: 10px; align-items: center; }\n    @media (max-width: 600px) { .controls { grid-template-columns: 1fr; } }\n\n    .input-wrap { position: relative; }\n    .input-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--muted); pointer-events: none; }\n    input, textarea {\n      width: 100%; font-family: inherit; font-size: .875rem;\n      border: 1px solid var(--border); border-radius: var(--radius-sm);\n      padding: 9px 12px; background: var(--surface);\n      color: var(--text); outline: none;\n      transition: border-color .15s, box-shadow .15s;\n    }\n    input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(212,84,10,.12); }\n    .input-wrap input { padding-left: 34px; }\n    textarea { min-height: 160px; resize: vertical; line-height: 1.6; }\n\n    \/* \u2500\u2500 Buttons \u2500\u2500 *\/\n    .btn {\n      display: inline-flex; align-items: center; gap: 7px;\n      padding: 9px 18px; border-radius: var(--radius-sm);\n      font-family: inherit; font-size: .875rem; font-weight: 500;\n      cursor: pointer; border: none; white-space: nowrap;\n      transition: opacity .15s, transform .1s;\n    }\n    .btn:active { transform: scale(.97); }\n    .btn:disabled { opacity: .5; cursor: not-allowed; }\n    .btn-primary { background: var(--text); color: #fff; }\n    .btn-primary:hover:not(:disabled) { background: #333; }\n    .btn-outline { background: var(--surface); color: var(--text); border: 1px solid var(--border); }\n    .btn-outline:hover:not(:disabled) { background: var(--surface2); }\n    .btn-accent  { background: var(--accent); color: #fff; }\n    .btn-accent:hover:not(:disabled) { background: #b84408; }\n    .btn-green   { background: var(--green); color: #fff; }\n    .btn-green:hover:not(:disabled) { background: #145536; }\n\n    \/* \u2500\u2500 Post Cards \u2500\u2500 *\/\n    .posts-grid { display: flex; flex-direction: column; gap: 16px; }\n\n    .post-card { border-radius: var(--radius); overflow: hidden; transition: box-shadow .2s; }\n    .post-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,.1); }\n\n    .post-card-head { padding: 18px 20px 0; display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }\n    .post-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }\n\n    .badge {\n      display: inline-flex; align-items: center;\n      font-size: .72rem; font-weight: 600;\n      padding: 3px 9px; border-radius: 20px; border: 1px solid transparent;\n    }\n    .badge-approved  { background: var(--green-bg); color: var(--green); border-color: #b5dfc9; }\n    .badge-published { background: var(--surface2); color: var(--muted); border-color: var(--border); }\n    .badge-draft, .badge-pending, .badge-default { background: var(--amber-bg); color: var(--amber); border-color: #f5d88a; }\n    .badge-outline { background: transparent; border-color: var(--border); color: var(--muted); }\n\n    .post-headline { font-size: 1.05rem; font-weight: 600; line-height: 1.35; }\n    .post-date     { font-size: .8rem; color: var(--muted); margin-top: 4px; }\n\n    .post-card-body { padding: 14px 20px 20px; display: flex; flex-direction: column; gap: 12px; }\n    .post-text { font-size: .875rem; line-height: 1.7; color: #3a3730; white-space: pre-wrap; }\n    .post-hashtags { background: var(--surface2); border-radius: var(--radius-sm); padding: 10px 14px; font-size: .82rem; color: var(--muted); line-height: 1.5; }\n\n    .images-label { font-size: .8rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; display: flex; align-items: center; gap: 6px; }\n    .images-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }\n    .img-thumb { border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--border); background: var(--surface2); aspect-ratio: 4\/3; display: block; }\n    .img-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .2s; }\n    .img-thumb:hover img { transform: scale(1.03); }\n    .images-empty { border: 1.5px dashed var(--border); border-radius: var(--radius-sm); padding: 14px; font-size: .82rem; color: var(--muted); }\n\n    \/* \u2500\u2500 Generate tab \u2500\u2500 *\/\n    .gen-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }\n    .gen-desc { font-size: .875rem; color: var(--muted); line-height: 1.6; margin-bottom: 16px; }\n\n    \/* \u2500\u2500 Empty state \u2500\u2500 *\/\n    .empty-state { text-align: center; padding: 48px 20px; color: var(--muted); font-size: .9rem; }\n    .empty-icon  { font-size: 2.2rem; margin-bottom: 10px; }\n\n    \/* \u2500\u2500 Spinner \u2500\u2500 *\/\n    @keyframes spin { to { transform: rotate(360deg); } }\n    .spin { animation: spin .7s linear infinite; }\n\n    \/* \u2500\u2500 Fade in \u2500\u2500 *\/\n    @keyframes fadeUp { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }\n    .fade-up { animation: fadeUp .3s ease both; }\n\n    \/* \u2500\u2500 Divider \u2500\u2500 *\/\n    .divider { border: none; border-top: 1px solid var(--border); margin: 4px 0; }\n\n    \/* \u2500\u2500 Webhook input \u2500\u2500 *\/\n    .webhook-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n    .webhook-row input { flex: 1; min-width: 200px; font-family: 'DM Mono', monospace; font-size: .8rem; }\n  <\/style>\n<\/head>\n<body>\n\n<div class=\"container fade-up\" id=\"app\">\n\n  <!-- Header -->\n  <div class=\"card\">\n    <div class=\"card-body\">\n      <div class=\"header-top\">\n        <div style=\"flex:1\">\n          <h1>Bomedia<span class=\"logo-dot\">.<\/span> Content Dashboard<\/h1>\n          <p class=\"header-desc\">Panel para consultar posts programados, aprobarlos y lanzar nuevas generaciones autom\u00e1ticas o desde prompt contra tu escenario de Make.<\/p>\n        <\/div>\n      <\/div>\n\n      <div class=\"stats-row\">\n        <div class=\"stat-box\">\n          <div class=\"stat-label\">Webhook Make<\/div>\n          <div class=\"webhook-row\" style=\"margin-top:6px\">\n            <input type=\"text\" id=\"webhookInput\" value=\"https:\/\/hook.eu2.make.com\/6xfh7ui8sdyahint8kgx9m77vhb10ee4\" \/>\n            <button class=\"btn btn-outline\" onclick=\"saveWebhook()\" title=\"Guardar\" style=\"padding:8px 12px\">\n              <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"\/><polyline points=\"17 21 17 13 7 13 7 21\"\/><polyline points=\"7 3 7 8 15 8\"\/><\/svg>\n            <\/button>\n          <\/div>\n        <\/div>\n        <div class=\"stat-box\">\n          <div class=\"stat-label\">Posts cargados<\/div>\n          <div class=\"stat-value\" id=\"postCount\">0<\/div>\n        <\/div>\n        <div class=\"stat-box\">\n          <div class=\"stat-label\">Acciones disponibles<\/div>\n          <div class=\"stat-value sm\">listar \u00b7 aprobar<br>generar \u00b7 prompt<\/div>\n        <\/div>\n      <\/div>\n\n      <div id=\"globalMsg\" style=\"margin-top:14px;display:none\"><\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Tabs -->\n  <div class=\"tabs-nav\">\n    <button class=\"tab-btn active\" onclick=\"switchTab('posts', this)\">Posts<\/button>\n    <button class=\"tab-btn\" onclick=\"switchTab('generate', this)\">Generar<\/button>\n  <\/div>\n\n  <!-- TAB: Posts -->\n  <div id=\"tab-posts\">\n    <div class=\"card\">\n      <div class=\"card-body\">\n        <div class=\"controls\">\n          <div class=\"input-wrap\">\n            <svg class=\"input-icon\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"\/><\/svg>\n            <input type=\"text\" id=\"searchInput\" placeholder=\"Buscar por titular, texto, estado o hashtags\" oninput=\"renderPosts()\" \/>\n          <\/div>\n          <input type=\"number\" id=\"limitInput\" value=\"10\" min=\"1\" max=\"200\" placeholder=\"L\u00edmite\" \/>\n          <button class=\"btn btn-primary\" id=\"loadBtn\" onclick=\"loadPosts()\">\n            <svg id=\"refreshIcon\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><path d=\"M23 4v6h-6\"\/><path d=\"M20.49 15a9 9 0 11-2.12-9.36L23 10\"\/><\/svg>\n            Cargar posts\n          <\/button>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"posts-grid\" id=\"postsGrid\">\n      <div class=\"card\"><div class=\"empty-state\"><div class=\"empty-icon\">\ud83d\udccb<\/div>No hay posts cargados todav\u00eda.<\/div><\/div>\n    <\/div>\n  <\/div>\n\n  <!-- TAB: Generate -->\n  <div id=\"tab-generate\" style=\"display:none\">\n    <div class=\"gen-grid\">\n\n      <!-- Auto -->\n      <div class=\"card\">\n        <div class=\"card-body\">\n          <div class=\"card-title\">\n            <svg width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"\/><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"\/><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"\/><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"\/><\/svg>\n            Generaci\u00f3n autom\u00e1tica\n          <\/div>\n          <hr class=\"divider\" style=\"margin:12px 0\" \/>\n          <p class=\"gen-desc\">Lanza la generaci\u00f3n del siguiente post usando la l\u00f3gica de calendario y rotaci\u00f3n del escenario en Make.<\/p>\n          <button class=\"btn btn-accent\" id=\"genAutoBtn\" onclick=\"generateAutomatic()\">\n            <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"\/><\/svg>\n            Generar post autom\u00e1tico\n          <\/button>\n        <\/div>\n      <\/div>\n\n      <!-- Prompt -->\n      <div class=\"card\">\n        <div class=\"card-body\">\n          <div class=\"card-title\">\n            <svg width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" viewBox=\"0 0 24 24\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"\/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"\/><\/svg>\n            Generaci\u00f3n por prompt\n          <\/div>\n          <hr class=\"divider\" style=\"margin:12px 0\" \/>\n          <textarea id=\"promptInput\">Create a LinkedIn post in English about UV flatbed printing for retail displays, with a professional and technical tone.<\/textarea>\n          <div style=\"margin-top:10px\">\n            <button class=\"btn btn-accent\" id=\"genPromptBtn\" onclick=\"generateFromPrompt()\">\n              <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"\/><\/svg>\n              Generar desde prompt\n            <\/button>\n          <\/div>\n        <\/div>\n      <\/div>\n\n    <\/div>\n  <\/div>\n\n<\/div><!-- \/app -->\n\n<script>\n  \/\/ \u2500\u2500 Config \u2500\u2500\n  let WEBHOOK_URL = document.getElementById('webhookInput').value;\n\n  function saveWebhook() {\n    WEBHOOK_URL = document.getElementById('webhookInput').value.trim();\n    showMsg('Webhook actualizado.', 'green');\n  }\n\n  \/\/ \u2500\u2500 State \u2500\u2500\n  let allPosts = [];\n\n  \/\/ \u2500\u2500 Normalize \u2500\u2500\n  function normalizePost(raw) {\n    if (!raw || typeof raw !== 'object') return null;\n    if (raw.headline || raw.publish_date || raw.row_number) {\n      return {\n        row_number: raw.row_number ?? raw.__ROW_NUMBER__ ?? null,\n        publish_date: raw.publish_date ?? '',\n        status: raw.status ?? '',\n        topic_type: raw.topic_type ?? '',\n        headline: raw.headline ?? '',\n        body_text: raw.body_text ?? '',\n        hashtags: raw.hashtags ?? '',\n        image_1_drive_file_id: raw.image_1_drive_file_id ?? '',\n        image_2_drive_file_id: raw.image_2_drive_file_id ?? '',\n        image_3_drive_file_id: raw.image_3_drive_file_id ?? '',\n        image_1_drive_url: raw.image_1_drive_url ?? '',\n        image_2_drive_url: raw.image_2_drive_url ?? '',\n        image_3_drive_url: raw.image_3_drive_url ?? '',\n      };\n    }\n    return {\n      row_number: raw.__ROW_NUMBER__ ?? null,\n      publish_date: raw['0'] ?? '',\n      status: raw['1'] ?? '',\n      topic_type: raw['2'] ?? '',\n      headline: raw['3'] ?? '',\n      body_text: raw['4'] ?? '',\n      hashtags: raw['5'] ?? '',\n      image_1_drive_file_id: raw['7'] ?? '',\n      image_2_drive_file_id: raw['8'] ?? '',\n      image_3_drive_file_id: raw['9'] ?? '',\n      image_1_drive_url: raw['11'] ?? '',\n      image_2_drive_url: raw['12'] ?? '',\n      image_3_drive_url: raw['13'] ?? '',\n    };\n  }\n\n  function normalizeList(data) {\n    if (!data) return [];\n    if (Array.isArray(data)) return data.map(normalizePost).filter(Boolean);\n    if (Array.isArray(data.posts)) return data.posts.map(normalizePost).filter(Boolean);\n    if (typeof data.posts === 'object' && data.posts !== null && data.posts.__ROW_NUMBER__)\n      return [normalizePost(data.posts)].filter(Boolean);\n    if (typeof data === 'object' && data.__ROW_NUMBER__)\n      return [normalizePost(data)].filter(Boolean);\n    return [];\n  }\n\n  \/\/ \u2500\u2500 API \u2500\u2500\n  async function callWebhook(payload) {\n    const res = await fetch(WEBHOOK_URL, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify(payload),\n    });\n    const text = await res.text();\n    try { return JSON.parse(text); } catch { return { raw: text, ok: res.ok }; }\n  }\n\n  \/\/ \u2500\u2500 Tabs \u2500\u2500\n  function switchTab(name, btn) {\n    document.getElementById('tab-posts').style.display = name === 'posts' ? '' : 'none';\n    document.getElementById('tab-generate').style.display = name === 'generate' ? '' : 'none';\n    document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));\n    btn.classList.add('active');\n  }\n\n  \/\/ \u2500\u2500 Helpers \u2500\u2500\n  function extractDriveId(url) {\n    if (!url) return null;\n    \/\/ \/d\/FILE_ID\/ format\n    const m1 = url.match(\/\\\/d\\\/([a-zA-Z0-9_-]{10,})\/);\n    if (m1) return m1[1];\n    \/\/ id=FILE_ID format\n    const m2 = url.match(\/[?&]id=([a-zA-Z0-9_-]{10,})\/);\n    if (m2) return m2[1];\n    return null;\n  }\n\n  function driveImgSrc(url, fileId) {\n    const id = fileId || extractDriveId(url);\n    if (id) return `https:\/\/drive.google.com\/thumbnail?id=${id}&sz=w600`;\n    return url || '';\n  }\n\n  function driveHref(url, fileId) {\n    const id = fileId || extractDriveId(url);\n    if (id) return `https:\/\/drive.google.com\/file\/d\/${id}\/view`;\n    return url || '#';\n  }\n\n  function badgeClass(status) {\n    const v = (status || '').toLowerCase();\n    if (v === 'approved')  return 'badge-approved';\n    if (v === 'published') return 'badge-published';\n    return 'badge-default';\n  }\n\n  function esc(str) {\n    return String(str ?? '').replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;').replace(\/\"\/g,'&quot;');\n  }\n\n  function showMsg(msg, type) {\n    const el = document.getElementById('globalMsg');\n    el.style.display = 'block';\n    el.innerHTML = `<div class=\"alert alert-${type === 'green' ? 'green' : 'red'}\">\n      <svg width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n        ${type === 'green'\n          ? '<path d=\"M22 11.08V12a10 10 0 11-5.93-9.14\"\/><polyline points=\"22 4 12 14.01 9 11.01\"\/>'\n          : '<circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"\/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"\/>'}\n      <\/svg>\n      ${esc(msg)}\n    <\/div>`;\n    setTimeout(() => { el.style.display = 'none'; }, 5000);\n  }\n\n  function setLoading(btnId, iconId, loading, spinSvg) {\n    const btn = document.getElementById(btnId);\n    if (!btn) return;\n    btn.disabled = loading;\n    if (iconId && spinSvg) {\n      const icon = document.getElementById(iconId);\n      if (icon) icon.classList.toggle('spin', loading);\n    }\n  }\n\n  \/\/ \u2500\u2500 Render posts \u2500\u2500\n  function renderPosts() {\n    const q = document.getElementById('searchInput').value.trim().toLowerCase();\n    const filtered = q\n      ? allPosts.filter(p =>\n          [p.headline, p.body_text, p.topic_type, p.status, p.hashtags]\n            .join(' ').toLowerCase().includes(q))\n      : allPosts;\n\n    document.getElementById('postCount').textContent = allPosts.length;\n\n    const grid = document.getElementById('postsGrid');\n    if (!filtered.length) {\n      grid.innerHTML = '<div class=\"card\"><div class=\"empty-state\"><div class=\"empty-icon\">\ud83d\udccb<\/div>No hay posts que coincidan.<\/div><\/div>';\n      return;\n    }\n\n    grid.innerHTML = filtered.map(post => {\n      const images = [\n        { url: post.image_1_drive_url, fileId: post.image_1_drive_file_id },\n        { url: post.image_2_drive_url, fileId: post.image_2_drive_file_id },\n        { url: post.image_3_drive_url, fileId: post.image_3_drive_file_id },\n      ].filter(img => img.url || img.fileId);\n\n      const imagesHtml = images.length\n        ? `<div class=\"images-grid\">${images.map((img, i) => {\n            const src = driveImgSrc(img.url, img.fileId);\n            const href = driveHref(img.url, img.fileId);\n            return `<a class=\"img-thumb\" href=\"${esc(href)}\" target=\"_blank\" rel=\"noreferrer\">\n              <img decoding=\"async\" src=\"${esc(src)}\" alt=\"Imagen ${i+1}\" loading=\"lazy\" onerror=\"this.style.display='none'\" \/>\n            <\/a>`;\n          }).join('')}<\/div>`\n        : `<div class=\"images-empty\">No hay im\u00e1genes en esta fila.<\/div>`;\n\n      const isApproved = (post.status || '').toLowerCase() === 'approved';\n\n      return `<div class=\"card post-card fade-up\" id=\"post-${esc(post.row_number)}\">\n        <div class=\"post-card-head\">\n          <div style=\"flex:1\">\n            <div class=\"post-meta\">\n              <span class=\"badge ${badgeClass(post.status)}\">${esc(post.status || 'unknown')}<\/span>\n              ${post.topic_type ? `<span class=\"badge badge-outline\">${esc(post.topic_type)}<\/span>` : ''}\n              ${post.row_number != null ? `<span class=\"badge badge-outline\">fila ${esc(post.row_number)}<\/span>` : ''}\n            <\/div>\n            <div class=\"post-headline\">${esc(post.headline || 'Sin titular')}<\/div>\n            <div class=\"post-date\">${esc(post.publish_date || 'Sin fecha')}<\/div>\n          <\/div>\n          <button\n            class=\"btn ${isApproved ? 'btn-outline' : 'btn-green'}\"\n            id=\"approve-btn-${esc(post.row_number)}\"\n            onclick=\"approvePost(${JSON.stringify(post.row_number)})\"\n            ${!post.row_number || isApproved ? 'disabled' : ''}\n          >\n            <svg width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><path d=\"M22 11.08V12a10 10 0 11-5.93-9.14\"\/><polyline points=\"22 4 12 14.01 9 11.01\"\/><\/svg>\n            ${isApproved ? 'Aprobado' : 'Aprobar'}\n          <\/button>\n        <\/div>\n        <div class=\"post-card-body\">\n          <p class=\"post-text\">${esc(post.body_text)}<\/p>\n          ${post.hashtags ? `<div class=\"post-hashtags\">${esc(post.hashtags)}<\/div>` : ''}\n          <div>\n            <div class=\"images-label\">\n              <svg width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"\/><polyline points=\"21 15 16 10 5 21\"\/><\/svg>\n              Im\u00e1genes candidatas\n            <\/div>\n            <div style=\"margin-top:8px\">${imagesHtml}<\/div>\n          <\/div>\n        <\/div>\n      <\/div>`;\n    }).join('');\n  }\n\n  \/\/ \u2500\u2500 Actions \u2500\u2500\n  async function loadPosts() {\n    setLoading('loadBtn', 'refreshIcon', true, true);\n    try {\n      const limit = parseInt(document.getElementById('limitInput').value) || 10;\n      const data = await callWebhook({ action: 'list_posts', limit });\n      allPosts = normalizeList(data);\n      renderPosts();\n      showMsg(`Cargados ${allPosts.length} posts.`, 'green');\n    } catch (e) {\n      showMsg(e.message || 'No se pudieron cargar los posts.', 'red');\n    } finally {\n      setLoading('loadBtn', 'refreshIcon', false, true);\n    }\n  }\n\n  async function generateAutomatic() {\n    const btn = document.getElementById('genAutoBtn');\n    btn.disabled = true;\n    btn.textContent = 'Generando\u2026';\n    try {\n      await callWebhook({ action: 'generate_post' });\n      showMsg('Post generado correctamente.', 'green');\n      await loadPosts();\n    } catch (e) {\n      showMsg(e.message || 'No se pudo generar el post.', 'red');\n    } finally {\n      btn.disabled = false;\n      btn.innerHTML = `<svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"\/><\/svg> Generar post autom\u00e1tico`;\n    }\n  }\n\n  async function generateFromPrompt() {\n    const promptVal = document.getElementById('promptInput').value.trim();\n    if (!promptVal) return;\n    const btn = document.getElementById('genPromptBtn');\n    btn.disabled = true;\n    btn.textContent = 'Generando\u2026';\n    try {\n      await callWebhook({ action: 'generate_post_from_prompt', prompt: promptVal });\n      showMsg('Post generado desde prompt correctamente.', 'green');\n      await loadPosts();\n    } catch (e) {\n      showMsg(e.message || 'No se pudo generar el post desde prompt.', 'red');\n    } finally {\n      btn.disabled = false;\n      btn.innerHTML = `<svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"\/><\/svg> Generar desde prompt`;\n    }\n  }\n\n  async function approvePost(rowNumber) {\n    if (!rowNumber) return;\n    const btn = document.getElementById(`approve-btn-${rowNumber}`);\n    if (btn) { btn.disabled = true; btn.textContent = '\u2026'; }\n    try {\n      await callWebhook({ action: 'approve_post', row_number: rowNumber, status: 'approved' });\n      showMsg(`Fila ${rowNumber} aprobada.`, 'green');\n      allPosts = allPosts.map(p => p.row_number === rowNumber ? { ...p, status: 'approved' } : p);\n      renderPosts();\n    } catch (e) {\n      showMsg(e.message || 'No se pudo aprobar el post.', 'red');\n      if (btn) { btn.disabled = false; btn.textContent = 'Aprobar'; }\n    }\n  }\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>Bomedia Content Dashboard Bomedia. Content Dashboard Panel para consultar posts programados, aprobarlos y lanzar nuevas generaciones autom\u00e1ticas o desde prompt contra tu escenario de Make. Webhook Make Posts cargados 0 Acciones disponibles listar \u00b7 aprobargenerar \u00b7 prompt Posts Generar Cargar<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-3634","page","type-page","status-publish","hentry"],"translation":{"provider":"WPGlobus","version":"3.0.0","language":"de","enabled_languages":["es","en","de","fr"],"languages":{"es":{"title":true,"content":true,"excerpt":false},"en":{"title":false,"content":false,"excerpt":false},"de":{"title":false,"content":false,"excerpt":false},"fr":{"title":false,"content":false,"excerpt":false}}},"_links":{"self":[{"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/pages\/3634","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/comments?post=3634"}],"version-history":[{"count":1,"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/pages\/3634\/revisions"}],"predecessor-version":[{"id":3636,"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/pages\/3634\/revisions\/3636"}],"wp:attachment":[{"href":"https:\/\/mboprinters.com\/de\/wp-json\/wp\/v2\/media?parent=3634"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}