// dashboard-composition-detail-page-phase-.js — extracted verbatim from dashboard.js (pure move).

// ── Composition detail page (Phase 7 — observability) ─────────────

async function renderWorkflowDetail() {
  const id = dashboardState.composerDraft?.id || dashboardState.currentParams?.id;
  if (!id) {
    setContentNode(
      el("div", { class: "empty-state" }, [
        el("div", { class: "empty-state-icon" }, "🧩"),
        el("div", { class: "empty-state-title" }, "No composition selected"),
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => navigate("workflows"),
          },
          "← Back to workflows",
        ),
      ]),
    );
    return;
  }
  const [comp, runsResp, funnelResp] = await Promise.all([
    api("composition-get", { id }).catch((e) => ({ error: e?.message })),
    api("composition-runs", { id, limit: 20 }).catch((e) => ({ error: e?.message })),
    api("composition-funnel", { id }).catch(() => null),
  ]);
  if (comp?.error) {
    setContentNode(
      el("div", { class: "empty-state" }, [
        el("div", {}, "Composition not found"),
        el("div", { class: "muted", style: "font-size: 11px;" }, esc(comp.error)),
      ]),
    );
    return;
  }
  const c = comp.composition;
  const runs = runsResp?.runs || [];

  const root = el("div", { style: "max-width: 1100px; margin: 0 auto; padding: 24px;" });

  // Header
  const statusPill = el(
    "span",
    {
      class: "pill",
      style: `background: ${c.status === "active" ? "#1e3a1e" : c.status === "paused" ? "#3a2a1e" : "#222"}; color: ${c.status === "active" ? "#5ada5a" : c.status === "paused" ? "#daaa5a" : "#aaa"};`,
    },
    c.status,
  );

  root.appendChild(
    el(
      "div",
      {
        class: "row",
        style: "justify-content: space-between; align-items: flex-start; margin-bottom: 18px;",
      },
      [
        el("div", { style: "flex: 1;" }, [
          el("div", { class: "row", style: "gap: 8px; align-items: center;" }, [
            el("h1", { style: "margin: 0; font-size: 22px;" }, c.name),
            statusPill,
          ]),
          el(
            "div",
            { class: "muted", style: "font-size: 12px; margin-top: 4px;" },
            c.description || "",
          ),
        ]),
        el(
          "div",
          { class: "row", style: "gap: 6px;" },
          [
            c.status === "active"
              ? el(
                  "button",
                  {
                    class: "btn btn-tiny btn-secondary",
                    type: "button",
                    onclick: async () => {
                      await api("composition-pause", { id: c.id });
                      renderWorkflowDetail();
                    },
                  },
                  "⏸ Pause",
                )
              : c.status !== "archived"
                ? el(
                    "button",
                    {
                      class: "btn btn-tiny btn-primary",
                      type: "button",
                      onclick: async () => {
                        await api("composition-activate", { id: c.id });
                        renderWorkflowDetail();
                      },
                    },
                    "▶ Activate",
                  )
                : null,
            // Phase 9: killswitch — visible from detail page on any non-archived composition
            c.status !== "archived"
              ? el(
                  "button",
                  {
                    class: "btn btn-tiny btn-danger",
                    type: "button",
                    title: "Killswitch — pauses + cancels in-flight runs",
                    onclick: async () => {
                      if (await _composerKillswitch(c)) renderWorkflowDetail();
                    },
                  },
                  "🛑 Kill",
                )
              : null,
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                onclick: async () => {
                  const r = await api("composition-run", { id: c.id, body: {} }).catch((e) => ({
                    error: e?.message,
                  }));
                  if (r?.error) toast(`Run failed: ${r.error}`, "err");
                  else toast(`Run · ${r.playsSucceeded || 0}/${r.playsExecuted || 0} plays`, "ok");
                  renderWorkflowDetail();
                },
              },
              "▶ Run now",
            ),
            // Phase 33: 🧪 Test on 3 — uses test_mode (Phase 31d). Picks first 3
            // audience contacts; doesn't pollute funnel; doesn't fire chain events.
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title:
                  "Test mode — runs against the first 3 audience contacts only. Doesn't pollute the funnel or chain to downstream compositions.",
                onclick: async () => {
                  const r = await api("composition-run", {
                    id: c.id,
                    body: { test_mode: true },
                  }).catch((e) => ({ error: e?.message }));
                  if (r?.error) toast(`Test run failed: ${r.error}`, "err");
                  else
                    toast(
                      `🧪 Test run · ${r.playsSucceeded || 0}/${r.playsExecuted || 0} plays on ${r.audienceSize || 0} contacts`,
                      "ok",
                      5000,
                    );
                  renderWorkflowDetail();
                },
              },
              "🧪 Test on 3",
            ),
            // Phase 34b: open the n8n-style canvas (default editor going forward)
            el(
              "button",
              {
                class: "btn btn-tiny btn-primary",
                type: "button",
                title: "Open the canvas editor (drag-droppable nodes, n8n-style)",
                onclick: () => {
                  dashboardState.composerDraft = { ...c };
                  navigate("workflows/canvas");
                },
              },
              "🔀 Canvas",
            ),
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "Open the legacy 4-step wizard editor",
                onclick: () => {
                  dashboardState.composerDraft = { ...c };
                  navigate("workflows/edit");
                },
              },
              "✏ Wizard",
            ),
            // Phase 15: clone composition into a new draft variant
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title:
                  "Duplicate this composition into a new draft (e.g., for A/B testing a different DM template).",
                onclick: async () => {
                  const name = prompt("Name for the cloned variant:", `${c.name} (variant)`);
                  if (!name) return;
                  const r = await api("composition-clone", { id: c.id, body: { name } }).catch(
                    (e) => ({ error: e?.message }),
                  );
                  if (r?.error) {
                    toast(`Clone failed: ${r.error}`, "err");
                    return;
                  }
                  toast(`📋 Cloned to "${r.composition.name}" (#${r.composition.id})`, "ok", 3500);
                  dashboardState.composerDraft = { ...r.composition };
                  navigate("workflows/edit");
                },
              },
              "📋 Clone",
            ),
            // Phase 15: open compare picker against this composition
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "Compare this composition's funnel side-by-side with another.",
                onclick: () => _composerOpenComparePicker(c),
              },
              "⚖ Compare",
            ),
            // Phase 12: link to budget allocator
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "See per-account daily-cap utilization across all your compositions",
                onclick: () => navigate("workflows/budgets"),
              },
              "💰 Account budgets",
            ),
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                onclick: () => navigate("workflows"),
              },
              "← Back",
            ),
          ].filter(Boolean),
        ),
      ],
    ),
  );

  // Funnel + stats
  const totalReplyRate =
    c.total_runs > 0 && c.total_audience_reached > 0
      ? Math.round((100 * c.total_replies) / Math.max(c.total_audience_reached, 1))
      : 0;
  root.appendChild(
    el("div", { class: "row", style: "gap: 16px; margin-bottom: 20px;" }, [
      _statCard("Total runs", String(c.total_runs || 0)),
      _statCard("Audience reached", String(c.total_audience_reached || 0)),
      _statCard("Replies attributed", String(c.total_replies || 0)),
      _statCard("Reply rate", `${totalReplyRate}%`),
      _statCard("Last run", c.last_run_at ? new Date(c.last_run_at).toLocaleString() : "Never"),
    ]),
  );

  // Phase 33: per-template performance card — when this composition was
  // cloned from a template, show how the template performs across ALL the
  // user's compositions vs this one specifically. Helps spot "this comp is
  // outperforming the baseline" or "I should clone the better-performing
  // sibling".
  if (c.source_template_id) {
    api("composition-performance-templates")
      .then((r) => {
        const allTpl = r?.templates || [];
        const tplStats = allTpl.find((t) => t.template_id === c.source_template_id);
        if (!tplStats) return;
        const f = tplStats.funnel || {};
        const totalCompCount = tplStats.composition_count || 1;
        // Skip if this is the only composition cloned from the template — no comparison to make
        if (totalCompCount <= 1) return;
        const tplReplyRate = f.sent > 0 ? f.replied / f.sent : 0;
        const myReplyRate =
          c.total_audience_reached > 0 ? c.total_replies / c.total_audience_reached : 0;
        const isBeating = myReplyRate > tplReplyRate;
        const card = el(
          "div",
          { class: "card", style: "margin-bottom: 16px; padding: 12px 14px;" },
          [
            el(
              "div",
              {
                class: "row",
                style: "justify-content: space-between; align-items: center; margin-bottom: 8px;",
              },
              [
                el(
                  "div",
                  { style: "font-weight: 500; font-size: 13px;" },
                  `📋 Template baseline: ${c.source_template_id}`,
                ),
                el(
                  "div",
                  { class: "muted", style: "font-size: 10px;" },
                  `${totalCompCount} compositions cloned from this template`,
                ),
              ],
            ),
            el("div", { class: "row", style: "gap: 16px; font-size: 11px; line-height: 1.6;" }, [
              el("div", { style: "flex: 1;" }, [
                el("div", { class: "muted" }, "Across all clones"),
                el(
                  "div",
                  {},
                  `attempted: ${f.attempted || 0} · sent: ${f.sent || 0} · replied: ${f.replied || 0}`,
                ),
                el("div", {}, `reply rate (sent → replied): ${(tplReplyRate * 100).toFixed(1)}%`),
              ]),
              el("div", { style: "flex: 1;" }, [
                el("div", { class: "muted" }, "This composition"),
                el(
                  "div",
                  {},
                  `reached: ${c.total_audience_reached || 0} · replied: ${c.total_replies || 0}`,
                ),
                el(
                  "div",
                  { style: `color: ${isBeating ? "#5ada5a" : "#faaa5a"};` },
                  `reply rate (reached → replied): ${(myReplyRate * 100).toFixed(1)}% ${isBeating ? "↑ beating baseline" : "↓ below baseline"}`,
                ),
              ]),
            ]),
          ],
        );
        // Insert after the stats row (find it and append)
        const beforeShape = root.querySelector(".card");
        if (beforeShape) root.insertBefore(card, beforeShape);
        else root.appendChild(card);
      })
      .catch(() => {});
  }

  // Composition body summary — Phase 34: Diagram / Body toggle
  const summaryCard = el("div", { class: "card", style: "margin-bottom: 16px;" });
  let viewMode = "diagram"; // default to diagram view
  const titleRow = el("div", {
    class: "row",
    style: "justify-content: space-between; align-items: center; margin-bottom: 10px;",
  });
  titleRow.appendChild(el("div", { class: "card-title", style: "margin: 0;" }, "Shape"));
  const toggleRow = el("div", { class: "row", style: "gap: 4px;" });
  const diagBtn = el("button", { type: "button", class: "btn btn-tiny btn-primary" }, "🔀 Diagram");
  const bodyBtn = el("button", { type: "button", class: "btn btn-tiny btn-secondary" }, "📋 Body");
  toggleRow.appendChild(diagBtn);
  toggleRow.appendChild(bodyBtn);
  titleRow.appendChild(toggleRow);
  summaryCard.appendChild(titleRow);

  const shapeContainer = el("div", {});
  summaryCard.appendChild(shapeContainer);

  const renderShapeView = async () => {
    shapeContainer.innerHTML = "";
    if (viewMode === "diagram") {
      // Phase 34: pull recent run audit + audience-count to populate
      // diagram badges + per-play status. Best-effort; fall back to plain
      // diagram if API calls fail.
      let audiencePreview = null;
      let lastRunStatus = {};
      try {
        const ar = await api("composition-audience-count", { body: { audience: c.audience } });
        if (ar?.count != null) audiencePreview = { count: ar.count };
      } catch {}
      // Use the most recent run's audit (if any) to color-code per-play status
      try {
        if (runs.length > 0) {
          const latestRun = runs[0];
          const auditR = await api("composition-run-audit", { runId: latestRun.id }).catch(
            () => null,
          );
          if (auditR?.events && Array.isArray(auditR.events)) {
            // auditR shape: events with play_idx + status (best-effort mapping)
            for (const ev of auditR.events) {
              if (ev.play_idx != null && ev.status) lastRunStatus[ev.play_idx] = ev.status;
            }
          }
        }
      } catch {}
      const lockedAccounts = (c.share_info || []).filter((s) => s.locked).map((s) => s.account_id);
      const svg = _renderCompositionDiagram(c, {
        audiencePreview,
        lockedAccounts,
        lastRunStatus,
        readonly: c.status === "archived",
        onEdit:
          c.status === "archived"
            ? null
            : (section) => {
                dashboardState.composerDraft = { ...c };
                dashboardState.composerStartStep =
                  section === "trigger"
                    ? 0
                    : section === "audience"
                      ? 1
                      : section === "channels"
                        ? 2
                        : section === "plays"
                          ? 3
                          : 0;
                navigate("workflows/edit");
              },
      });
      const wrapper = el("div", { style: "padding: 12px; overflow-x: auto;" });
      wrapper.appendChild(svg);
      shapeContainer.appendChild(wrapper);
      shapeContainer.appendChild(
        el(
          "div",
          { class: "muted", style: "font-size: 10px; padding: 6px 12px; line-height: 1.5;" },
          "Click any node to edit that section in the wizard. ⚠ = warning (locked account, empty audience). Status badges (✓/✗/↷) reflect the most recent run.",
        ),
      );
    } else {
      shapeContainer.appendChild(_renderCompositionShape(c));
    }
  };

  diagBtn.onclick = () => {
    viewMode = "diagram";
    diagBtn.className = "btn btn-tiny btn-primary";
    bodyBtn.className = "btn btn-tiny btn-secondary";
    renderShapeView();
  };
  bodyBtn.onclick = () => {
    viewMode = "body";
    diagBtn.className = "btn btn-tiny btn-secondary";
    bodyBtn.className = "btn btn-tiny btn-primary";
    renderShapeView();
  };

  renderShapeView();
  root.appendChild(summaryCard);

  // Phase 11: pending canary banner — surfaces "main batch will fire at <time>"
  if (c.pending_canary) {
    const pc = c.pending_canary;
    const dueAt = new Date(pc.scheduled_for);
    const sec = Math.max(0, Math.round((dueAt.getTime() - Date.now()) / 1000));
    const human = sec > 60 ? `${Math.round(sec / 60)} min` : `${sec}s`;
    root.appendChild(
      el(
        "div",
        {
          style:
            "padding: 10px 14px; background: #2a2a0a; border: 1px solid #5a5a1a; border-radius: 4px; font-size: 12px; margin-bottom: 16px; color: #faaa5a;",
        },
        [
          el(
            "div",
            { style: "font-weight: 500; margin-bottom: 4px;" },
            `🐦 Canary running — main batch (${pc.audience_remaining_count} contacts) fires in ~${human}`,
          ),
          el(
            "div",
            { class: "muted", style: "font-size: 11px;" },
            `Original run #${pc.original_run_id} on canary sample · max error rate ${(pc.max_error_rate * 100).toFixed(0)}%. ` +
              `Above threshold → composition auto-pauses. Otherwise → main batch runs automatically.`,
          ),
        ],
      ),
    );
  }

  // Phase 20: share-weight info — when this composition shares an
  // account with other active comps, show the daily-cap split.
  if (Array.isArray(c.share_info) && c.share_info.length > 0) {
    const contested = c.share_info.filter((s) => s.total_weight > s.weight);
    if (contested.length > 0) {
      const card = el("div", { class: "card", style: "margin-bottom: 16px;" });
      card.appendChild(el("div", { class: "card-title" }, "⚖ Account share"));
      for (const s of c.share_info) {
        const isContested = s.total_weight > s.weight;
        card.appendChild(
          el(
            "div",
            {
              style: `display: grid; grid-template-columns: 1fr auto auto auto; gap: 12px; padding: 6px 8px; border-bottom: 1px solid #1a1a1a; align-items: center; font-size: 11px;`,
            },
            [
              el("div", {}, `${s.platform}@${s.account_id}`),
              el("div", { class: "muted" }, `weight ${s.weight}/${s.total_weight}`),
              el(
                "div",
                { style: isContested ? "color: #daaa5a;" : "color: #5ada5a;" },
                `${Math.round(s.share * 100)}% share`,
              ),
              el(
                "div",
                { class: "muted" },
                `→ ${s.effective_daily_cap}/day of ${s.platform_cap} cap`,
              ),
            ],
          ),
        );
      }
      root.appendChild(card);
    }
  }

  // Phase 8: funnel card
  const funnel = funnelResp?.funnel;
  const funnelCard = el("div", { class: "card", style: "margin-bottom: 16px;" });
  funnelCard.appendChild(el("div", { class: "card-title" }, "Funnel"));
  if (funnel && Object.values(funnel).some((n) => n > 0)) {
    funnelCard.appendChild(_composerRenderFunnelBar(funnel));
    const replyRate = funnel.sent > 0 ? Math.round((100 * funnel.replied) / funnel.sent) : 0;
    funnelCard.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin-top: 8px;" },
        `Reply rate: ${replyRate}% (${funnel.replied} of ${funnel.sent} sent)${funnel.converted > 0 ? ` · Converted: ${funnel.converted}` : ""}`,
      ),
    );

    // Phase 21: surface channel mix (Phase 20 backend exposes this on the
    // composition GET response). Shows how attempts spread across the
    // fallback chain — useful to see if "use only if has X" filters are
    // routing contacts as expected.
    const mix = c.channel_mix;
    if (mix && mix.total_attempted > 0 && Object.keys(mix.mix || {}).length > 0) {
      const platforms = Object.keys(mix.mix);
      const total = platforms.reduce((s, p) => s + (mix.mix[p] || 0), 0);
      if (total > 0) {
        const platformColors = {
          telegram: "#5a8aff",
          linkedin: "#5aaaff",
          twitter: "#5acaaa",
          email: "#5ada5a",
        };
        const segments = platforms
          .map((p) => {
            const n = mix.mix[p] || 0;
            const pct = total > 0 ? Math.round((100 * n) / total) : 0;
            return { p, n, pct, color: platformColors[p] || "#888" };
          })
          .filter((s) => s.n > 0);
        const stackBar = el(
          "div",
          {
            style:
              "display: flex; height: 14px; border-radius: 3px; overflow: hidden; background: #1a1a1a; margin-top: 12px;",
          },
          segments.map((s) =>
            el("div", {
              style: `width: ${s.pct}%; background: ${s.color}; transition: width 200ms;`,
              title: `${s.p}: ${s.n} attempts (${s.pct}%)`,
            }),
          ),
        );
        funnelCard.appendChild(
          el(
            "div",
            { class: "muted", style: "font-size: 11px; margin-top: 12px;" },
            "Channel mix:",
          ),
        );
        funnelCard.appendChild(stackBar);
        funnelCard.appendChild(
          el(
            "div",
            { class: "row", style: "gap: 12px; margin-top: 4px; font-size: 10px;" },
            segments.map((s) =>
              el("span", {}, [
                el("span", {
                  style: `display: inline-block; width: 8px; height: 8px; background: ${s.color}; border-radius: 50%; margin-right: 4px;`,
                }),
                `${s.p}: ${s.n} (${s.pct}%)`,
              ]),
            ),
          ),
        );
      }
    }
  } else {
    funnelCard.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 12px; padding: 8px;" },
        "No funnel data yet. Activate + run the composition; outcomes are tracked from this point forward.",
      ),
    );
  }
  root.appendChild(funnelCard);

  // Runs table
  const runsCard = el("div", { class: "card", style: "margin-bottom: 16px;" });
  runsCard.appendChild(el("div", { class: "card-title" }, "Recent runs"));
  if (runs.length === 0) {
    runsCard.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 12px; padding: 8px;" },
        "No runs yet. Click ▶ Run now or activate the composition.",
      ),
    );
  } else {
    const table = el("div", { style: "display: flex; flex-direction: column; gap: 4px;" });
    for (const r of runs) {
      const stats = (() => {
        try {
          return JSON.parse(r.stats_json || "{}");
        } catch {
          return {};
        }
      })();
      const dur =
        r.finished_at && r.started_at
          ? Math.round((new Date(r.finished_at) - new Date(r.started_at)) / 1000)
          : null;
      const statusColor =
        r.status === "success"
          ? "#5ada5a"
          : r.status === "partial"
            ? "#daaa5a"
            : r.status === "error"
              ? "#ff6666"
              : "#aaa";
      table.appendChild(
        el(
          "div",
          {
            style:
              "display: grid; grid-template-columns: 130px 80px 60px 1fr 100px 60px; gap: 12px; padding: 6px 8px; border-bottom: 1px solid #1a1a1a; font-size: 11px; align-items: center;",
          },
          [
            el("div", {}, new Date(r.started_at).toLocaleString()),
            el("div", { style: `color: ${statusColor}; font-weight: 500;` }, r.status),
            el("div", { class: "muted" }, r.triggered_by || "manual"),
            el(
              "div",
              { class: "muted" },
              `audience: ${r.audience_size || 0} · plays: ${r.plays_succeeded || 0}✓/${r.plays_failed || 0}✗${r.plays_executed ? ` (${r.plays_executed} total)` : ""}${r.error ? ` · err: ${String(r.error).slice(0, 40)}` : ""}`,
            ),
            el("div", { class: "muted" }, dur != null ? `${dur}s` : "—"),
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "Why these contacts?",
                onclick: () => _composerOpenRunAudit(r.id),
              },
              "🔍",
            ),
          ],
        ),
      );
    }
    runsCard.appendChild(table);
  }
  root.appendChild(runsCard);

  setContentNode(root);
}

function _statCard(label, value) {
  return el(
    "div",
    {
      style:
        "flex: 1; padding: 10px 12px; background: #0c0c0c; border: 1px solid #222; border-radius: 6px; min-width: 120px;",
    },
    [
      el(
        "div",
        {
          class: "muted",
          style: "font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;",
        },
        label,
      ),
      el("div", { style: "font-size: 18px; font-weight: 600; margin-top: 4px;" }, value),
    ],
  );
}

function _renderCompositionShape(c) {
  const audSummary = (c.audience?.resolvers || []).map((r) => r.type).join(" + ") || "—";
  const chSummary =
    (c.channels || []).map((ch) => `${ch.platform}@${ch.account_id || "?"}`).join(" → ") || "—";
  const playSummary = (c.plays || []).map((p) => p.type).join(" · ") || "—";
  const trig =
    c.trigger?.type === "schedule"
      ? `schedule (${c.trigger.cron})`
      : c.trigger?.type === "schedule_simple"
        ? `schedule (every ${c.trigger.every})`
        : c.trigger?.type === "event"
          ? `event (${c.trigger.source})`
          : "manual";
  return el(
    "div",
    { style: "display: grid; grid-template-columns: 100px 1fr; gap: 8px; font-size: 12px;" },
    [
      el("div", { class: "muted" }, "Trigger"),
      el("div", {}, trig),
      el("div", { class: "muted" }, "Audience"),
      el("div", {}, audSummary),
      el("div", { class: "muted" }, "Channels"),
      el("div", {}, chSummary),
      el("div", { class: "muted" }, "Plays"),
      el("div", {}, `${(c.plays || []).length} total — ${playSummary}`),
    ],
  );
}

/**
 * Phase 34 — n8n-style diagram renderer for compositions.
 *
 * Renders a vertical SVG flowchart: trigger → audience → channels → plays.
 * Branches fork into sub-trees. Each node is clickable (opens edit dialog
 * for that section). Hovering shows hint warnings (locked accounts, empty
 * audience, missing params).
 *
 * Input: composition body (cloned or live row)
 * Optional opts:
 *   - audiencePreview: { count, sample } from from-template response or
 *     audience-count API (renders as count badge on audience node)
 *   - lockedAccounts: array of account ids that are locked (shows ⚠ on channel nodes)
 *   - lastRunStatus: per-play map of "ok"/"fail"/"skip" from a recent run
 *   - onEdit(section, idx?): click-handler invoked with section name
 *     ("trigger", "audience", "channels", "plays") and optional index for plays
 *   - readonly: boolean (no click-to-edit when true)
 */
