package handlers

import "net/http"

type OpsWorkbenchHandler struct{}

func NewOpsWorkbenchHandler() *OpsWorkbenchHandler {
	return &OpsWorkbenchHandler{}
}

func (h *OpsWorkbenchHandler) Get(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(opsWorkbenchHTML))
}

const opsWorkbenchHTML = `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CMR 运维后台</title>
  <style>
    :root {
      --bg: #071218;
      --panel: #12212c;
      --panel-2: #172a36;
      --line: #284355;
      --text: #ecf6ff;
      --muted: #92acbf;
      --brand: #52d6b3;
      --warn: #ffd36a;
      --danger: #ff7b7b;
      --accent: #4fa3ff;
    }
    * { box-sizing: border-box; }
    html { scroll-behavior: smooth; }
    body {
      margin: 0;
      color: var(--text);
      font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
      background:
        radial-gradient(circle at top left, rgba(82,214,179,.12), transparent 28%),
        radial-gradient(circle at top right, rgba(79,163,255,.12), transparent 24%),
        var(--bg);
    }
    .shell {
      width: 100%;
      max-width: none;
      margin: 0;
      padding: 24px 28px 32px;
      display: grid;
      grid-template-columns: 280px minmax(0,1fr) 400px;
      gap: 20px;
      align-items: start;
    }
    .sidebar, .aside, .panel {
      background: rgba(18,33,44,.92);
      border: 1px solid var(--line);
      border-radius: 24px;
      box-shadow: 0 16px 60px rgba(0,0,0,.22);
    }
    .sidebar, .aside {
      position: sticky;
      top: 20px;
      padding: 18px;
      display: grid;
      gap: 18px;
      align-content: start;
    }
    .sidebar { background: linear-gradient(180deg, rgba(13,27,36,.96), rgba(18,33,44,.94)); }
    .aside { background: linear-gradient(180deg, rgba(18,33,44,.94), rgba(13,23,31,.96)); }
    .content { display: grid; gap: 20px; min-width: 0; }
    .panel { padding: 20px; display: grid; gap: 16px; align-content: start; }
    .hero {
      background: linear-gradient(135deg, rgba(82,214,179,.15), rgba(79,163,255,.10));
      padding: 22px 24px;
    }
    .brand h1, .hero h2, .panel h3 { margin: 0; line-height: 1.2; }
    .brand h1 { font-size: 26px; }
    .hero h2 { font-size: 30px; }
    .panel h3 { font-size: 24px; }
    .brand p, .hero p, .panel p, .hint { margin: 0; color: var(--muted); line-height: 1.7; }
    .tag, .eyebrow {
      display: inline-flex;
      width: fit-content;
      padding: 4px 10px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 700;
    }
    .tag { background: rgba(82,214,179,.18); color: var(--brand); }
    .eyebrow { background: rgba(79,163,255,.16); color: var(--accent); }
    .nav-group { display: grid; gap: 10px; }
    .nav-title {
      color: var(--muted);
      font-size: 12px;
      letter-spacing: .08em;
      text-transform: uppercase;
      padding: 0 4px;
    }
    .nav-link {
      display: grid;
      gap: 4px;
      width: 100%;
      text-align: left;
      color: var(--text);
      padding: 13px 14px;
      border-radius: 18px;
      border: 1px solid var(--line);
      background: rgba(255,255,255,.02);
      cursor: pointer;
    }
    .nav-link.active {
      border-color: rgba(82,214,179,.45);
      background: rgba(82,214,179,.10);
      box-shadow: inset 0 0 0 1px rgba(82,214,179,.16);
    }
    .nav-link span { color: var(--muted); font-size: 13px; line-height: 1.45; }
    .hero-points, .metrics, .grid-2, .grid-3, .split { display: grid; gap: 14px; }
    .toolbar { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; justify-content: space-between; }
    .toolbar .actions { justify-content: flex-end; }
    .hero-points, .metrics { grid-template-columns: repeat(3, minmax(0,1fr)); }
    .metrics { grid-template-columns: repeat(4, minmax(0,1fr)); }
    .grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
    .grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
    .split { grid-template-columns: 1.05fr .95fr; align-items: start; }
    .hero-card, .metric, .item, .token-box, .statusbox, .logbox, .list {
      border-radius: 18px;
      border: 1px solid var(--line);
      background: rgba(255,255,255,.03);
    }
    .hero-card, .metric, .item { padding: 14px; display: grid; gap: 6px; }
    .item.selectable { cursor: pointer; transition: border-color .18s ease, background .18s ease, transform .18s ease; }
    .item.selectable:hover { border-color: rgba(82,214,179,.35); background: rgba(82,214,179,.08); transform: translateY(-1px); }
    .item.active { border-color: rgba(82,214,179,.45); background: rgba(82,214,179,.10); box-shadow: inset 0 0 0 1px rgba(82,214,179,.14); }
    .metric strong { font-size: 28px; line-height: 1; }
    .metric span, .item .meta { color: var(--muted); font-size: 13px; line-height: 1.5; }
    .field { display: grid; gap: 8px; min-width: 0; }
    .field label { color: var(--muted); font-size: 14px; }
    input, textarea, select {
      width: 100%;
      border-radius: 16px;
      border: 1px solid var(--line);
      background: var(--panel-2);
      color: var(--text);
      padding: 13px 14px;
      font: inherit;
    }
    textarea { min-height: 110px; resize: vertical; line-height: 1.55; }
    .tall textarea { min-height: 190px; }
    .actions { display: flex; gap: 12px; flex-wrap: wrap; }
    button {
      border: 0;
      border-radius: 16px;
      padding: 13px 18px;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
      background: var(--brand);
      color: #03231d;
    }
    button.nav-link {
      background: rgba(255,255,255,.02);
      color: var(--text);
      border: 1px solid var(--line);
      padding: 13px 14px;
    }
    button.nav-link.active {
      border-color: rgba(82,214,179,.45);
      background: rgba(82,214,179,.10);
      color: var(--text);
    }
    button.secondary { background: var(--warn); color: #3f2d00; }
    button.ghost { background: transparent; border: 1px solid var(--line); color: var(--text); }
    .token-box, .statusbox, .logbox {
      padding: 14px;
      white-space: pre-wrap;
      word-break: break-word;
      font-family: "Consolas", "SFMono-Regular", monospace;
    }
    .statusbox { min-height: 84px; }
    .logbox { min-height: 220px; max-height: 420px; overflow: auto; }
    .list { padding: 10px; display: grid; gap: 10px; max-height: 320px; overflow: auto; }
    .ops-view { display: none; }
    .ops-view.active { display: grid; }
    [hidden] { display: none !important; }
    .modal-backdrop {
      position: fixed;
      inset: 0;
      background: rgba(3, 10, 14, .68);
      backdrop-filter: blur(4px);
      display: grid;
      place-items: center;
      padding: 24px;
      z-index: 30;
    }
    .modal-card {
      width: min(1180px, calc(100vw - 48px));
      max-height: calc(100vh - 48px);
      overflow: auto;
      background: linear-gradient(180deg, rgba(18,33,44,.98), rgba(12,23,31,.98));
      border: 1px solid var(--line);
      border-radius: 24px;
      box-shadow: 0 20px 80px rgba(0,0,0,.35);
      padding: 22px;
      display: grid;
      gap: 16px;
    }
    .status-ok { color: var(--brand); }
    .status-error { color: var(--danger); }
    @media (max-width: 1560px) {
      .shell { grid-template-columns: 250px minmax(0,1fr) 360px; padding: 22px 24px 28px; }
    }
    @media (max-width: 1360px) {
      .shell { grid-template-columns: 220px minmax(0,1fr); }
      .aside { grid-column: 2; position: static; }
    }
    @media (max-width: 1120px) {
      .shell { grid-template-columns: 1fr; }
      .sidebar, .aside { position: static; }
      .hero-points, .metrics, .grid-2, .grid-3, .split { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <div class="shell">
    <aside class="sidebar">
      <div class="brand">
        <span class="tag">运维后台第一期</span>
        <h1>资源录入与发布中心</h1>
        <p>这里不做联调回归，只做资源纳管、活动绑定和发布回滚。调试问题继续去 <code>/dev/workbench</code>。</p>
      </div>
      <div class="nav-group">
        <div class="nav-title">主要流程</div>
        <button class="nav-link active" type="button" data-view="overview"><strong>资源总览</strong><span>先看已有资源、活动和当前发布状态。</span></button>
        <button class="nav-link" type="button" data-view="maps"><strong>地图 / 地点管理</strong><span>先管地图列表、地点、当前瓦片版本和地图预览。</span></button>
        <button class="nav-link" type="button" data-view="courses"><strong>路线资源管理</strong><span>围绕地图管理 KML、路线组、默认路线和预览。</span></button>
        <button class="nav-link" type="button" data-view="events"><strong>活动管理</strong><span>活动列表、基础信息、状态和当前发布概况。</span></button>
        <button class="nav-link" type="button" data-view="compose"><strong>活动编排</strong><span>绑定 runtime、展示定义、内容包，准备发布。</span></button>
        <button class="nav-link" type="button" data-view="publish"><strong>发布中心</strong><span>查看 pipeline、build、publish、rollback。</span></button>
      </div>
      <div class="nav-group">
        <div class="nav-title">辅助入口</div>
        <button class="nav-link" type="button" data-view="ingest"><strong>资源录入</strong><span>上传文件或登记正式链接，统一纳管资源对象。</span></button>
      </div>
      <div class="nav-group">
        <div class="nav-title">调试工具</div>
        <a class="nav-link" href="/dev/workbench"><strong>返回调试工作台</strong><span>去一键回归、配置摘要、前端日志面板。</span></a>
      </div>
    </aside>

    <main class="content">
      <section class="panel hero">
        <h2>先录资源，再绑活动，最后发布</h2>
        <p>运维平台第一版的目标很单一：把正式资源稳定录入系统，沉淀成资源对象，再通过统一发布链形成可追溯的 release。这里不混调试按钮，也不直接暴露玩家链路。</p>
        <div class="hero-points">
          <div class="hero-card"><strong>资源录入</strong><span>文件上传和外链登记统一收口，不再依赖手工改代码或散脚本。</span></div>
          <div class="hero-card"><strong>活动绑定</strong><span>活动侧只绑定默认 runtime、presentation、content bundle 三元组。</span></div>
          <div class="hero-card"><strong>发布中心</strong><span>继续走统一 build / publish / rollback，不开第二套发布逻辑。</span></div>
        </div>
      </section>

      <section class="panel ops-view active" id="overview">
        <div class="eyebrow">资源总览</div>
        <h3>先看关键统计，再看当前运行时信息</h3>
        <p>资源总览先回答两个问题：系统里现在有多少正式对象；你当前选中的活动此刻绑定了哪套 release / runtime / 展示定义 / 内容包。</p>
        <input id="managed-place-id" type="hidden" value="">
        <div class="split">
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">资源与路线统计</div>
            <div class="metrics">
              <div class="metric"><strong id="metric-places">0</strong><span>地点</span></div>
              <div class="metric"><strong id="metric-map-assets">0</strong><span>地图</span></div>
              <div class="metric"><strong id="metric-tile-releases">0</strong><span>瓦片版本</span></div>
              <div class="metric"><strong id="metric-assets">0</strong><span>受管资源</span></div>
              <div class="metric"><strong id="metric-course-sets">0</strong><span>路线组</span></div>
              <div class="metric"><strong id="metric-course-variants">0</strong><span>路线变体</span></div>
              <div class="metric"><strong id="metric-runtime-bindings">0</strong><span>运行绑定</span></div>
              <div class="metric"><strong id="metric-config-sources">0</strong><span>配置源</span></div>
            </div>
          </div>
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">活动与发布统计</div>
            <div class="metrics">
              <div class="metric"><strong id="metric-events">0</strong><span>活动数</span></div>
              <div class="metric"><strong id="metric-default-events">0</strong><span>默认体验活动</span></div>
              <div class="metric"><strong id="metric-published-events">0</strong><span>已发布活动</span></div>
              <div class="metric"><strong id="metric-pipeline-releases">0</strong><span>发布版本</span></div>
              <div class="metric"><strong id="metric-presentations">0</strong><span>展示定义</span></div>
              <div class="metric"><strong id="metric-content-bundles">0</strong><span>内容包</span></div>
              <div class="metric"><strong id="metric-ops-users">0</strong><span>运维账号</span></div>
            </div>
          </div>
        </div>
        <div class="split">
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">当前运行时信息</div>
            <div class="field"><label>当前活动</label><div class="token-box" id="overview-current-event">-</div></div>
            <div class="field"><label>当前发布版本</label><div class="token-box" id="overview-current-release">-</div></div>
            <div class="field"><label>当前 runtime</label><div class="token-box" id="overview-current-runtime">-</div></div>
            <div class="field"><label>当前展示定义</label><div class="token-box" id="overview-current-presentation">-</div></div>
            <div class="field"><label>当前内容包</label><div class="token-box" id="overview-current-content-bundle">-</div></div>
          </div>
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">操作提示</div>
            <div class="item"><strong>1. 先看资源总览</strong><div class="meta">确认地点、地图、路线组、活动、已发布数量是否符合预期。</div></div>
            <div class="item"><strong>2. 再选主流程</strong><div class="meta">地图 / 地点管理 -> 路线资源管理 -> 活动管理 / 活动编排 -> 发布中心。</div></div>
            <div class="item"><strong>3. 关联活动只看摘要</strong><div class="meta">地图页只看数量和跳转，活动详情统一放到“活动管理”。</div></div>
          </div>
        </div>
        <div class="grid-3">
          <div class="field"><label>运维手机号</label><input id="ops-mobile" value="13800138000"></div>
          <div class="field"><label>短信验证码</label><input id="ops-code" value=""></div>
          <div class="field"><label>运维显示名</label><input id="ops-display-name" value="开发运维"></div>
          <div class="field"><label>国家区号</label><input id="ops-country-code" value="86"></div>
          <div class="field"><label>设备标识</label><input id="ops-device-key" value="ops-console-001"></div>
          <div class="field"><label>当前角色</label><input id="ops-role-code" value="开发态自动放行" readonly></div>
          <div class="field"><label>活动 ID（总览 / pipeline）</label><input id="event-id" value="evt_demo_variant_manual_001"></div>
          <div class="field"><label>当前发布版本 ID</label><input id="release-id" value=""></div>
          <div class="field"><label>当前构建 ID</label><input id="build-id" value=""></div>
        </div>
        <div class="actions">
          <button id="btn-send-ops-code">发送验证码</button>
          <button id="btn-register-ops" class="secondary">注册运维账号</button>
          <button id="btn-login-ops" class="secondary">手机号登录</button>
          <button class="secondary" id="btn-refresh-overview">刷新总览</button>
          <button class="ghost" id="btn-clear-token">清空令牌</button>
        </div>
        <div class="hint">开发环境默认免登录放行，生产环境请使用手机号验证码注册/登录的独立运维账号。运维账号和前端玩家账号完全分离。</div>
      </section>

      <section class="panel ops-view" id="maps">
        <div class="eyebrow">地图 / 地点管理</div>
        <h3>先进入地图列表，再做新增、编辑和预览</h3>
        <p>地点是地图的归属容器，不是主入口。一个地点可挂多张地图，一张地图只属于一个地点。关联活动在这里先只看数量和摘要，详情统一去“活动管理”。</p>
        <div class="toolbar">
          <div class="grid-2" style="min-width:min(100%,720px);">
            <div class="field"><label>地图关键字</label><input id="map-search" value="" placeholder="按地图名称、编码、地点筛选"></div>
            <div class="field"><label>地点关键字</label><input id="place-search" value="" placeholder="按地点名称、省市、编码筛选"></div>
          </div>
          <div class="actions">
            <button id="btn-open-create-map">添加地图</button>
            <button class="ghost" id="btn-open-create-place">添加地点</button>
            <button class="secondary" id="btn-refresh-map-area">刷新地图区</button>
          </div>
        </div>
        <div class="panel" style="padding:18px;">
          <div class="eyebrow">地图列表</div>
          <div class="hint">默认只看地图列表。点一张地图，再进入地图详情；新增地图和新增地点都走独立弹出层。</div>
          <div class="list" id="map-library-list"><div class="item"><strong>暂无地图</strong><div class="meta">打开页面后会自动拉取地图列表，也可以手动点“刷新地图区”。</div></div></div>
        </div>

        <div hidden>
          <div id="place-list"></div>
          <div id="map-list"></div>
        </div>

        <div class="modal-backdrop" id="map-detail-modal" hidden>
          <div class="modal-card">
            <div class="eyebrow">地图详情</div>
            <h3 style="margin:0;">当前地图详情 / 预览</h3>
            <div class="split">
              <div class="panel" style="padding:18px;">
                <div class="field"><label>当前地点</label><div class="token-box" id="map-preview-place">-</div></div>
                <div class="field"><label>当前地图</label><div class="token-box" id="map-preview-map">-</div></div>
                <div class="field"><label>当前瓦片版本</label><div class="token-box" id="map-preview-tile-version">-</div></div>
                <div class="field"><label>Tile Base URL</label><div class="token-box" id="map-preview-tile-base">-</div></div>
                <div class="field"><label>Meta URL</label><div class="token-box" id="map-preview-meta">-</div></div>
              </div>
              <div class="panel" style="padding:18px;">
                <div class="field"><label>默认活动数量</label><div class="token-box" id="map-preview-default-count">0</div></div>
                <div class="field"><label>默认活动摘要</label><div class="token-box" id="map-preview-default-events">-</div></div>
                <div class="field"><label>关联活动数量</label><div class="token-box" id="map-preview-linked-count">0</div></div>
                <div class="field"><label>关联活动摘要</label><div class="token-box" id="map-preview-linked-summary">当前未关联活动</div></div>
              </div>
            </div>
            <div class="actions">
              <button id="btn-open-map-editor-from-detail">编辑地图</button>
              <button class="secondary" id="btn-open-map-tile-from-detail">导入瓦片版本</button>
              <button class="ghost" id="btn-open-events-view">前往活动管理</button>
              <button class="ghost" id="btn-close-map-detail">关闭详情</button>
            </div>
          </div>
        </div>

        <div class="modal-backdrop" id="map-editor-panel" hidden>
          <div class="modal-card">
            <div class="eyebrow">添加 / 编辑地图</div>
            <div class="grid-2">
              <div class="field"><label>管理地图 ID</label><input id="managed-map-id" value=""></div>
              <div class="field"><label>所属地点</label><select id="map-place-select"><option value="">请先录入地点</option></select></div>
              <div class="field"><label>地图编码</label><input id="map-code" value="lxcb-map-001"></div>
              <div class="field"><label>地图名称</label><input id="map-name" value="领秀城公园底图"></div>
              <div class="field"><label>地图类型</label><input id="map-type" value="raster"></div>
              <div class="field"><label>地图封面 URL</label><input id="map-cover-url" value=""></div>
              <div class="field"><label>地图摘要</label><input id="map-summary" value="领秀城公园地图资源，包含默认体验活动与当前瓦片版本。"></div>
            </div>
            <div class="panel" style="padding:18px;">
              <div class="eyebrow">上传 / 导入首个瓦片版本</div>
              <div class="grid-3">
                <div class="field"><label>瓦片版本号</label><input id="tile-version" value="2026-04-07"></div>
                <div class="field"><label>瓦片根地址</label><input id="tile-base-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/"></div>
                <div class="field"><label>元数据地址</label><input id="tile-meta-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json"></div>
              </div>
            </div>
            <div class="actions">
              <button id="btn-create-map-asset">新增地图</button>
              <button class="secondary" id="btn-update-map-asset">保存地图修改</button>
              <button class="secondary" id="btn-import-tile">导入瓦片版本</button>
              <button class="ghost" id="btn-get-map-asset">重新读取地图详情</button>
              <button class="ghost" id="btn-close-map-editor">关闭</button>
            </div>
          </div>
        </div>

        <div class="modal-backdrop" id="place-editor-panel" hidden>
          <div class="modal-card">
            <div class="eyebrow">添加 / 编辑地点</div>
            <div class="grid-2">
              <div class="field"><label>管理地点 ID</label><input id="place-manage-id" value=""></div>
              <div class="field"><label>地点编码</label><input id="place-code" value="lxcb-001"></div>
              <div class="field"><label>地点名称</label><input id="place-name" value="领秀城公园"></div>
              <div class="field"><label>省份</label><select id="place-province"><option value="">加载中...</option></select></div>
              <div class="field"><label>城市</label><select id="place-city"><option value="">请先选择省份</option></select></div>
              <div class="field"><label>地点区域</label><input id="place-region" value="" readonly></div>
            </div>
            <div class="actions">
              <button id="btn-create-place">保存地点</button>
              <button class="ghost" id="btn-get-place">重新读取地点详情</button>
              <button class="ghost" id="btn-close-place-editor">关闭</button>
            </div>
          </div>
        </div>
      </section>

      <section class="panel ops-view" id="ingest">
        <div class="eyebrow">资源录入</div>
        <h3>统一纳管资源</h3>
        <p>运维只需要关心“录一个资源”，不需要关心它最终是 OSS 上传还是已有外链。录完之后都落成统一资源对象。</p>
        <div class="split">
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">上传文件</div>
            <p>适合 KML、schema、manifest、本地静态包。backend 负责上传 OSS 并纳管。</p>
            <div class="grid-2">
              <div class="field"><label>资源类型</label><select id="asset-type"><option value="kml">kml</option><option value="tiles">tiles</option><option value="presentation">presentation</option><option value="content_bundle">content_bundle</option><option value="static_bundle">static_bundle</option></select></div>
              <div class="field"><label>资源编码</label><input id="asset-code" value="lxcb-route-pack-2026-04-07"></div>
              <div class="field"><label>版本</label><input id="asset-version" value="2026-04-07"></div>
              <div class="field"><label>标题</label><input id="asset-title" value="领秀城多赛道 KML 包"></div>
              <div class="field"><label>状态</label><select id="asset-status"><option value="active">active</option><option value="draft">draft</option></select></div>
              <div class="field"><label>对象目录（可选）</label><input id="asset-object-dir" value="gotomars/kml/lxcb-001/2026-04-07"></div>
              <div class="field"><label>内容类型（可选）</label><input id="asset-content-type" value="application/vnd.google-earth.kml+xml"></div>
              <div class="field"><label>上传文件</label><input id="asset-file" type="file"></div>
            </div>
            <div class="field"><label>Metadata JSON（可选）</label><textarea id="asset-metadata">{}</textarea></div>
            <div class="actions"><button id="btn-upload-asset">上传并纳管资源</button></div>
          </div>

          <div class="panel" style="padding:18px;">
            <div class="eyebrow">登记外链</div>
            <p>适合正式 OSS / CDN 上已经存在的资源。登记后，活动侧和发布链统一只认受管资源对象。</p>
            <div class="field"><label>外链 URL</label><input id="asset-public-url" value="https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route01.kml"></div>
            <div class="actions">
              <button class="secondary" id="btn-register-link">登记外链资源</button>
              <button class="ghost" id="btn-list-assets">查看受管资源</button>
            </div>
            <div class="hint">建议优先纳管：地图瓦片目录、KML、presentation schema、content bundle manifest。</div>
          </div>
        </div>
      </section>

      <section class="panel ops-view" id="courses">
        <div class="eyebrow">KML / 赛道管理</div>
        <h3>围绕当前地图管理 KML、赛道集和默认路线</h3>
        <p>KML 不是独立漂在外面的资源。运维上应该先选地图，再导入一批 KML，形成赛道集，然后查看默认路线和预览数据。</p>
        <div class="split">
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">导入瓦片版本</div>
            <div class="grid-2">
              <div class="field"><label>当前地点编码</label><input value="复用上方“地图资源管理”的地点编码" readonly></div>
              <div class="field"><label>当前地图编码</label><input value="复用上方“地图资源管理”的地图编码" readonly></div>
              <div class="field"><label>瓦片版本号</label><input id="tile-version" value="2026-04-07"></div>
              <div class="field"><label>瓦片根地址</label><input id="tile-base-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/"></div>
              <div class="field"><label>元数据地址</label><input id="tile-meta-url" value="https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json"></div>
            </div>
            <div class="actions"><button id="btn-import-tile">导入瓦片版本</button></div>
          </div>

          <div class="panel tall" style="padding:18px;">
            <div class="eyebrow">批量导入 KML</div>
            <div class="grid-2">
              <div class="field"><label>Course Set Code</label><input id="course-set-code" value="lxcb-manual-2026-04-07"></div>
              <div class="field"><label>Course Set Name</label><input id="course-set-name" value="领秀城公园多赛道 2026-04-07"></div>
              <div class="field"><label>Mode</label><input id="course-mode" value="classic-sequential"></div>
              <div class="field"><label>Default Route Code</label><input id="default-route-code" value="route-variant-d"></div>
            </div>
            <div class="field">
              <label>KML Batch JSON</label>
              <textarea id="routes-json">[
  {"name":"路线 01","routeCode":"route-variant-a","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route01.kml","sourceType":"kml","controlCount":10,"status":"active"},
  {"name":"路线 02","routeCode":"route-variant-b","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route02.kml","sourceType":"kml","controlCount":10,"status":"active"},
  {"name":"路线 03","routeCode":"route-variant-c","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route03.kml","sourceType":"kml","controlCount":10,"status":"active"},
  {"name":"路线 04","routeCode":"route-variant-d","fileUrl":"https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/2026-04-07/route04.kml","sourceType":"kml","controlCount":10,"status":"active"}
]</textarea>
            </div>
            <div class="actions"><button id="btn-import-kml-batch">批量导入 KML</button></div>
          </div>
        </div>
        <div class="split">
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">当前地图下赛道集</div>
            <div class="list" id="course-set-list"><div class="item"><strong>暂无赛道集</strong><div class="meta">读取地图详情或完成一轮 KML 导入后，这里会显示当前地图的赛道集。</div></div></div>
          </div>
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">KML / 变体预览</div>
            <div class="field"><label>当前赛道集</label><div class="token-box" id="course-preview-course-set">-</div></div>
            <div class="field"><label>默认路线</label><div class="token-box" id="course-preview-default-route">-</div></div>
            <div class="field"><label>路线数量</label><div class="token-box" id="course-preview-variant-count">0</div></div>
            <div class="field"><label>路线摘要</label><div class="token-box" id="course-preview-variants">-</div></div>
          </div>
        </div>
      </section>

      <section class="panel ops-view" id="events">
        <div class="eyebrow">活动管理</div>
        <h3>先看活动列表和基础信息</h3>
        <p>活动管理先处理业务壳：名称、状态、是否默认体验、是否出现在活动列表，以及当前发布概况。资源绑定与发布准备统一去“活动编排”。</p>
        <div class="grid-3">
          <div class="field"><label>租户编码</label><input id="event-tenant-code" value="tenant_demo"></div>
          <div class="field"><label>活动标识 Slug</label><input id="event-slug" value="city-park-manual-variant"></div>
          <div class="field"><label>活动名称</label><input id="event-display-name" value="领秀城公园多赛道挑战"></div>
          <div class="field"><label>活动摘要</label><input id="event-summary" value="多赛道联调体验活动"></div>
          <div class="field"><label>活动状态</label><input id="event-status" value="active"></div>
          <div class="field"><label>活动 ID</label><input id="binding-event-id" value="evt_demo_variant_manual_001"></div>
        </div>
        <div class="actions">
          <button class="secondary" id="btn-list-events">读取活动列表</button>
          <button class="secondary" id="btn-create-event">新建活动</button>
          <button class="secondary" id="btn-update-event">更新活动</button>
          <button class="secondary" id="btn-get-event">读取活动</button>
          <button class="ghost" id="btn-open-compose-view">前往活动编排</button>
        </div>
        <div class="split">
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">活动列表</div>
            <div class="list" id="event-list-main"><div class="item"><strong>暂无活动</strong><div class="meta">先点“读取活动列表”。</div></div></div>
          </div>
          <div class="panel" style="padding:18px;">
            <div class="eyebrow">当前活动概况</div>
            <div class="field"><label>当前发布版本</label><div class="token-box" id="event-preview-release">-</div></div>
            <div class="field"><label>当前 runtime</label><div class="token-box" id="event-preview-runtime">-</div></div>
            <div class="field"><label>当前展示定义</label><div class="token-box" id="event-preview-presentation">-</div></div>
            <div class="field"><label>当前内容包</label><div class="token-box" id="event-preview-content-bundle">-</div></div>
          </div>
        </div>
      </section>

      <section class="panel ops-view" id="compose">
        <div class="eyebrow">活动编排</div>
        <h3>绑定运行对象、展示定义和内容包</h3>
        <p>这里才进入发布前准备：给当前活动绑定 runtime、presentation、content bundle，确认默认 active 三元组，然后交给发布中心 build / publish。</p>
        <div class="grid-3">
          <div class="field"><label>Presentation ID</label><input id="presentation-id" value=""></div>
          <div class="field"><label>Content Bundle ID</label><input id="content-bundle-id" value=""></div>
          <div class="field"><label>Runtime Binding ID</label><input id="runtime-binding-id" value=""></div>
          <div class="field"><label>Presentation Title</label><input id="presentation-title" value="多赛道详情展示模板"></div>
          <div class="field"><label>Presentation Template Key</label><input id="presentation-template-key" value="event.detail.multi-variant"></div>
          <div class="field"><label>Presentation Schema URL</label><input id="presentation-schema-url" value="https://oss-mbh5.colormaprun.com/gotomars/presentations/event-detail-standard/v2026-04-07/schema.json"></div>
          <div class="field"><label>Presentation Version</label><input id="presentation-version" value="v2026-04-07"></div>
          <div class="field"><label>Bundle Title</label><input id="bundle-title" value="多赛道结果内容包"></div>
          <div class="field"><label>Bundle Type</label><input id="bundle-type" value="result_media"></div>
          <div class="field"><label>Bundle Manifest URL</label><input id="bundle-manifest-url" value="https://oss-mbh5.colormaprun.com/gotomars/content-bundles/result-media-manual/v2026-04-07/manifest.json"></div>
          <div class="field"><label>Bundle Version</label><input id="bundle-version" value="v2026-04-07"></div>
        </div>
        <div class="actions">
          <button id="btn-import-presentation">导入展示定义</button>
          <button id="btn-import-bundle">导入内容包</button>
          <button class="ghost" id="btn-save-defaults">保存活动默认绑定</button>
          <button class="ghost" id="btn-open-publish-view">前往发布中心</button>
        </div>
      </section>

      <section class="panel ops-view" id="publish">
        <div class="eyebrow">发布中心</div>
        <h3>统一 build / publish / rollback</h3>
        <p>运维后台不造第二条发布链，仍然复用现在这套 source、build、release 流程。</p>
        <div class="grid-3">
          <div class="field"><label>Pipeline Event ID</label><input id="pipeline-event-id" value="evt_demo_variant_manual_001"></div>
          <div class="field"><label>配置源 ID</label><input id="source-id" value=""></div>
          <div class="field"><label>回滚发布版本 ID</label><input id="rollback-release-id" value=""></div>
        </div>
        <div class="actions">
          <button class="secondary" id="btn-get-pipeline">读取发布链</button>
          <button id="btn-build-source">构建配置源</button>
          <button id="btn-publish-build">发布构建</button>
          <button class="ghost" id="btn-get-release">读取发布版本</button>
          <button class="ghost" id="btn-rollback-release">回滚发布</button>
        </div>
      </section>
    </main>

    <aside class="aside">
      <div class="field">
        <label>当前 Bearer Token</label>
        <div class="token-box" id="token-box">当前无令牌</div>
      </div>
      <section class="panel" style="padding:18px;">
        <div class="eyebrow">当前状态</div>
        <p>当前动作结果会写到这里，方便你快速判断到底卡在资源录入、活动绑定还是发布流程。</p>
        <div class="statusbox" id="status-box">待执行</div>
      </section>
      <section class="panel" style="padding:18px;">
        <div class="eyebrow">响应日志</div>
        <p>保留最后一次响应。运维排查时先看这里，不用再回调试工作台翻日志。</p>
        <div class="logbox" id="log-box">等待操作...</div>
      </section>
      <section class="panel" style="padding:18px;">
        <div class="eyebrow">最近受管资源</div>
        <div class="list" id="asset-list"><div class="item"><strong>暂无数据</strong><div class="meta">先执行刷新总览或查看受管资源</div></div></div>
      </section>
      <section class="panel" style="padding:18px;">
        <div class="eyebrow">最近活动</div>
        <div class="list" id="event-list"><div class="item"><strong>暂无数据</strong><div class="meta">先执行刷新总览</div></div></div>
      </section>
    </aside>
  </div>
  <script>
    const STORAGE_KEY = 'cmr-ops-workbench-v5';
    const state = {
      accessToken: '',
      activeView: 'overview',
      placeItems: [],
      mapItems: [],
      placeMapItems: [],
      regionOptions: [],
      mapEditorMode: 'none'
    };

    function $(id) { return document.getElementById(id); }

    function setActiveView(view) {
      state.activeView = view || 'overview';
      document.querySelectorAll('.ops-view').forEach(section => {
        section.classList.toggle('active', section.id === state.activeView);
      });
      document.querySelectorAll('.nav-link[data-view]').forEach(button => {
        button.classList.toggle('active', button.dataset.view === state.activeView);
      });
    }

    function setStatus(text, isError) {
      const el = $('status-box');
      el.textContent = text;
      el.className = 'statusbox ' + (isError ? 'status-error' : 'status-ok');
    }

    function writeLog(title, payload) {
      $('log-box').textContent = '[' + new Date().toLocaleString() + '] ' + title + '\n' + JSON.stringify(payload, null, 2);
    }

    function syncToken() {
      $('token-box').textContent = state.accessToken
        ? ('Bearer ' + state.accessToken.slice(0, 36) + '...')
        : '开发环境默认免登录，可直接操作；如需验证运维账号链路，再执行手机号登录。';
    }

    function setMapEditorMode(mode) {
      state.mapEditorMode = mode || 'none';
      $('map-editor-panel').hidden = state.mapEditorMode !== 'map';
      $('place-editor-panel').hidden = state.mapEditorMode !== 'place';
      persist();
    }

    function getFieldMap() {
      return {
        opsMobile: $('ops-mobile'),
        opsCode: $('ops-code'),
        opsDisplayName: $('ops-display-name'),
        opsCountryCode: $('ops-country-code'),
        opsDeviceKey: $('ops-device-key'),
        opsRoleCode: $('ops-role-code'),
        eventId: $('event-id'),
        releaseId: $('release-id'),
        buildId: $('build-id'),
        managedPlaceId: $('managed-place-id'),
        managedMapId: $('managed-map-id'),
        mapPlaceSelect: $('map-place-select'),
        placeManageId: $('place-manage-id'),
        assetType: $('asset-type'),
        assetCode: $('asset-code'),
        assetVersion: $('asset-version'),
        assetTitle: $('asset-title'),
        assetStatus: $('asset-status'),
        assetObjectDir: $('asset-object-dir'),
        assetPublicUrl: $('asset-public-url'),
        assetContentType: $('asset-content-type'),
        assetMetadata: $('asset-metadata'),
        placeCode: $('place-code'),
        placeName: $('place-name'),
        placeProvince: $('place-province'),
        placeCity: $('place-city'),
        placeRegion: $('place-region'),
        mapCode: $('map-code'),
        mapName: $('map-name'),
        mapType: $('map-type'),
        mapCoverUrl: $('map-cover-url'),
        mapSummary: $('map-summary'),
        tileVersion: $('tile-version'),
        tileBaseUrl: $('tile-base-url'),
        tileMetaUrl: $('tile-meta-url'),
        courseSetCode: $('course-set-code'),
        courseSetName: $('course-set-name'),
        courseMode: $('course-mode'),
        defaultRouteCode: $('default-route-code'),
        routesJson: $('routes-json'),
        eventTenantCode: $('event-tenant-code'),
        eventSlug: $('event-slug'),
        eventDisplayName: $('event-display-name'),
        eventSummary: $('event-summary'),
        eventStatus: $('event-status'),
        bindingEventId: $('binding-event-id'),
        presentationId: $('presentation-id'),
        contentBundleId: $('content-bundle-id'),
        runtimeBindingId: $('runtime-binding-id'),
        presentationTitle: $('presentation-title'),
        presentationTemplateKey: $('presentation-template-key'),
        presentationSchemaUrl: $('presentation-schema-url'),
        presentationVersion: $('presentation-version'),
        bundleTitle: $('bundle-title'),
        bundleType: $('bundle-type'),
        bundleManifestUrl: $('bundle-manifest-url'),
        bundleVersion: $('bundle-version'),
        pipelineEventId: $('pipeline-event-id'),
        sourceId: $('source-id'),
        rollbackReleaseId: $('rollback-release-id')
      };
    }

    function persist() {
      const payload = { accessToken: state.accessToken, activeView: state.activeView };
      Object.entries(getFieldMap()).forEach(([key, node]) => payload[key] = node.value);
      localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
    }

    function restore() {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) {
        syncToken();
        return;
      }
      try {
        const p = JSON.parse(raw);
        state.accessToken = p.accessToken || '';
        state.activeView = p.activeView || 'overview';
        Object.entries(getFieldMap()).forEach(([key, node]) => {
          if (p[key] !== undefined && p[key] !== null && p[key] !== '') node.value = p[key];
        });
      } catch (_) {}
      syncToken();
      setActiveView(state.activeView);
    }

    function syncPlaceRegion() {
      const provinceText = $('place-province').selectedOptions[0] ? $('place-province').selectedOptions[0].textContent : '';
      const cityText = $('place-city').selectedOptions[0] ? $('place-city').selectedOptions[0].textContent : '';
      $('place-region').value = [provinceText, cityText].filter(Boolean).join(' / ');
      persist();
    }

    function renderCityOptions(provinceCode, preferredCityCode) {
      const province = state.regionOptions.find(item => item.code === provinceCode);
      const cities = province ? province.cities || [] : [];
      $('place-city').innerHTML = cities.length
        ? cities.map(item => '<option value="' + item.code + '">' + item.name + '</option>').join('')
        : '<option value="">暂无城市</option>';
      if (preferredCityCode && cities.some(item => item.code === preferredCityCode)) {
        $('place-city').value = preferredCityCode;
      } else if (cities[0]) {
        $('place-city').value = cities[0].code;
      } else {
        $('place-city').value = '';
      }
      syncPlaceRegion();
    }

    function renderProvinceOptions(preferredProvinceCode, preferredCityCode) {
      $('place-province').innerHTML = state.regionOptions.length
        ? state.regionOptions.map(item => '<option value="' + item.code + '">' + item.name + '</option>').join('')
        : '<option value="">暂无省份</option>';
      if (preferredProvinceCode && state.regionOptions.some(item => item.code === preferredProvinceCode)) {
        $('place-province').value = preferredProvinceCode;
      } else if (state.regionOptions[0]) {
        $('place-province').value = state.regionOptions[0].code;
      } else {
        $('place-province').value = '';
      }
      renderCityOptions($('place-province').value, preferredCityCode);
    }

    async function loadRegionOptions() {
      const result = await request('GET', '/ops/admin/region-options');
      state.regionOptions = result.data || [];
      renderProvinceOptions($('place-province').value, $('place-city').value);
      return result;
    }

    function applyRegionText(regionText) {
      if (!regionText || !state.regionOptions.length) {
        return;
      }
      const [provinceText, cityText] = String(regionText).split('/').map(item => item.trim()).filter(Boolean);
      const province = state.regionOptions.find(item => item.name === provinceText);
      if (!province) {
        $('place-region').value = regionText;
        return;
      }
      $('place-province').value = province.code;
      renderCityOptions(province.code);
      const city = (province.cities || []).find(item => item.name === cityText);
      if (city) {
        $('place-city').value = city.code;
      }
      syncPlaceRegion();
    }

    async function request(method, url, body, isJSON = true) {
      const headers = {};
      let payload = body;
      if (state.accessToken) headers['Authorization'] = 'Bearer ' + state.accessToken;
      if (isJSON && body !== undefined) {
        headers['Content-Type'] = 'application/json';
        payload = JSON.stringify(body);
      }
      const resp = await fetch(url, { method, headers, body: payload });
      const text = await resp.text();
      let data = {};
      try { data = text ? JSON.parse(text) : {}; } catch (_) { data = { raw: text }; }
      if (!resp.ok) throw { status: resp.status, body: data, method, url };
      return data;
    }

    function renderItems(rootId, items, render) {
      const root = $(rootId);
      if (!Array.isArray(items) || !items.length) {
        root.innerHTML = '<div class="item"><strong>暂无数据</strong><div class="meta">当前还没有可展示对象</div></div>';
        return;
      }
      root.innerHTML = items.map(render).join('');
    }

    function renderAssets(items) {
      renderItems('asset-list', items, item =>
        '<div class="item"><strong>' + (item.title || item.assetCode || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.assetType || '-') + ' / ' + (item.assetCode || '-') + ' / ' + (item.version || '-') + '</div>' +
        '<div class="meta">' + (item.publicUrl || '-') + '</div></div>'
      );
    }

    function renderEvents(items) {
      renderItems('event-list', items, item =>
        '<div class="item"><strong>' + (item.displayName || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.status || '-') + '</div>' +
        '<div class="meta">' + (item.summary || '暂无摘要') + '</div></div>'
      );
    }

    function renderEventListMain(items) {
      renderItems('event-list-main', items, item =>
        '<div class="item"><strong>' + (item.displayName || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.status || '-') + '</div>' +
        '<div class="meta">' + ((item.currentRelease && item.currentRelease.id) || '当前未发布') + '</div></div>'
      );
    }

    function renderPlaces(items) {
      const keyword = ($('place-search') && $('place-search').value || '').trim().toLowerCase();
      const filtered = !keyword ? items : (items || []).filter(item =>
        [item.name, item.code, item.region].filter(Boolean).join(' ').toLowerCase().includes(keyword)
      );
      renderItems('place-list', items, item =>
        '<div class="item selectable' + ((item.id || '') === $('managed-place-id').value ? ' active' : '') + '" data-place-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.code || '-') + '</div>' +
        '<div class="meta">' + (item.region || '区域待补充') + '</div></div>'
      );
      renderItems('place-list', filtered, item =>
        '<div class="item selectable' + ((item.id || '') === $('managed-place-id').value ? ' active' : '') + '" data-place-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.code || '-') + '</div>' +
        '<div class="meta">' + (item.region || '区域待补充') + '</div></div>'
      );
    }

    function renderMapAssets(items) {
      const keyword = ($('map-search') && $('map-search').value || '').trim().toLowerCase();
      const filtered = !keyword ? items : (items || []).filter(item =>
        [item.name, item.code, item.placeName].filter(Boolean).join(' ').toLowerCase().includes(keyword)
      );
      renderItems('map-list', filtered, item =>
        '<div class="item selectable' + ((item.id || '') === $('managed-map-id').value ? ' active' : '') + '" data-map-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.code || '-') + ' / ' + (item.mapType || '-') + '</div>' +
        '<div class="meta">' + ((item.currentTileRelease && item.currentTileRelease.versionCode) || '当前无瓦片版本') + '</div></div>'
      );
    }

    function renderMapLibrary(items) {
      const keyword = ($('map-search') && $('map-search').value || '').trim().toLowerCase();
      const filtered = !keyword ? items : (items || []).filter(item =>
        [item.name, item.code, item.placeName].filter(Boolean).join(' ').toLowerCase().includes(keyword)
      );
      renderItems('map-library-list', filtered, item =>
        '<div class="item selectable' + ((item.id || '') === $('managed-map-id').value ? ' active' : '') + '" data-map-id="' + (item.id || '') + '"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.placeName || '未关联地点') + '</div>' +
        '<div class="meta">' + ((item.currentTileRelease && item.currentTileRelease.versionCode) || '当前无瓦片版本') + '</div></div>'
      );
    }

    function renderMapPlaceSelect() {
      const items = Array.isArray(state.placeItems) ? state.placeItems : [];
      $('map-place-select').innerHTML = items.length
        ? items.map(item => '<option value="' + item.id + '">' + (item.name || item.code || item.id) + ' / ' + (item.region || '未设置区域') + '</option>').join('')
        : '<option value="">请先录入地点</option>';
      if ($('managed-place-id').value && items.some(item => item.id === $('managed-place-id').value)) {
        $('map-place-select').value = $('managed-place-id').value;
      } else if (items[0]) {
        $('map-place-select').value = items[0].id;
        $('managed-place-id').value = items[0].id;
      } else {
        $('map-place-select').value = '';
      }
      persist();
    }

    function renderLinkedEvents(items) {
      renderItems('linked-event-list', items, item =>
        '<div class="item"><strong>' + (item.title || item.eventId || '-') + '</strong>' +
        '<div class="meta">' + (item.eventId || '-') + ' / ' + (item.status || '-') + (item.isDefaultExperience ? ' / 默认体验' : '') + '</div>' +
        '<div class="meta">' + (item.currentReleaseId || '当前未发布') + ' / ' + (item.currentPresentation || '无展示定义') + ' / ' + (item.currentContentBundle || '无内容包') + '</div></div>'
      );
    }

    function renderCourseSets(items) {
      renderItems('course-set-list', items, item =>
        '<div class="item"><strong>' + (item.name || item.code || item.id || '-') + '</strong>' +
        '<div class="meta">' + (item.id || '-') + ' / ' + (item.mode || '-') + ' / ' + (item.status || '-') + '</div>' +
        '<div class="meta">' + ((item.currentVariant && (item.currentVariant.name || item.currentVariant.routeCode)) || '当前无默认路线') + '</div></div>'
      );
    }

    async function refreshOverviewAndLog(reason) {
      try {
        const result = await refreshOverview();
        writeLog(reason || 'refresh-overview', result);
        setStatus('已刷新：资源总览', false);
      } catch (error) {
        writeLog(reason || 'refresh-overview', { error });
        setStatus('失败：资源总览自动刷新', true);
      }
    }

    function setPreviewValue(id, value) {
      $(id).textContent = (value !== undefined && value !== null && value !== '') ? String(value) : '-';
    }

    function applyPipelineSummary(pipeline) {
      const sources = Array.isArray(pipeline.sources) ? pipeline.sources : [];
      const releases = Array.isArray(pipeline.releases) ? pipeline.releases : [];
      if (pipeline.currentRelease && pipeline.currentRelease.id) $('release-id').value = pipeline.currentRelease.id;
      if (sources[0] && sources[0].id) $('source-id').value = sources[0].id;
      if (releases[0] && releases[0].id) $('rollback-release-id').value = releases[0].id;
      setPreviewValue('overview-current-release', pipeline.currentRelease ? (pipeline.currentRelease.id || '-') : '当前未发布');
      setPreviewValue('overview-current-runtime', pipeline.currentRelease && pipeline.currentRelease.runtime ? ((pipeline.currentRelease.runtime.name || pipeline.currentRelease.runtime.runtimeBindingId || pipeline.currentRelease.runtime.id || '-') + ' / ' + (pipeline.currentRelease.runtime.runtimeBindingId || pipeline.currentRelease.runtime.id || '-')) : '当前未绑定');
      setPreviewValue('overview-current-presentation', pipeline.currentRelease && pipeline.currentRelease.presentation ? ((pipeline.currentRelease.presentation.name || pipeline.currentRelease.presentation.templateKey || '-') + ' / ' + (pipeline.currentRelease.presentation.presentationId || pipeline.currentRelease.presentation.id || '-')) : '当前未绑定');
      setPreviewValue('overview-current-content-bundle', pipeline.currentRelease && pipeline.currentRelease.contentBundle ? ((pipeline.currentRelease.contentBundle.name || pipeline.currentRelease.contentBundle.bundleType || '-') + ' / ' + (pipeline.currentRelease.contentBundle.contentBundleId || pipeline.currentRelease.contentBundle.id || '-')) : '当前未绑定');
      persist();
    }

    function applyEventDetail(detail) {
      if (!detail) return;
      if (detail.event && detail.event.id) {
        $('event-id').value = detail.event.id;
        $('binding-event-id').value = detail.event.id;
        $('pipeline-event-id').value = detail.event.id;
        setPreviewValue('overview-current-event', (detail.event.displayName || detail.event.id || '-') + ' / ' + (detail.event.id || '-'));
      }
      if (detail.event && detail.event.tenantCode) $('event-tenant-code').value = detail.event.tenantCode;
      if (detail.event && detail.event.slug) $('event-slug').value = detail.event.slug;
      if (detail.event && detail.event.displayName) $('event-display-name').value = detail.event.displayName;
      if (detail.event && detail.event.summary) $('event-summary').value = detail.event.summary;
      if (detail.event && detail.event.status) $('event-status').value = detail.event.status;
      if (detail.event && detail.event.currentRelease && detail.event.currentRelease.id) $('release-id').value = detail.event.currentRelease.id;
      if (detail.latestSource && detail.latestSource.id) $('source-id').value = detail.latestSource.id;
      if (detail.currentPresentation && detail.currentPresentation.id) $('presentation-id').value = detail.currentPresentation.id;
      if (detail.currentContentBundle && detail.currentContentBundle.id) $('content-bundle-id').value = detail.currentContentBundle.id;
      if (detail.currentRuntime && detail.currentRuntime.id) $('runtime-binding-id').value = detail.currentRuntime.id;
      setPreviewValue('event-preview-runtime', detail.currentRuntime ? (detail.currentRuntime.runtimeBindingId || detail.currentRuntime.id || '-') : '当前未绑定');
      setPreviewValue('event-preview-presentation', detail.currentPresentation ? ((detail.currentPresentation.name || '-') + ' / ' + (detail.currentPresentation.presentationId || detail.currentPresentation.id || '-')) : '当前未绑定');
      setPreviewValue('event-preview-content-bundle', detail.currentContentBundle ? ((detail.currentContentBundle.name || '-') + ' / ' + (detail.currentContentBundle.contentBundleId || detail.currentContentBundle.id || '-')) : '当前未绑定');
      setPreviewValue('event-preview-release', detail.event && detail.event.currentRelease ? (detail.event.currentRelease.id || '-') : '当前未发布');
      persist();
    }

    function applyPlaceDetail(detail) {
      if (!detail || !detail.place) return;
      $('managed-place-id').value = detail.place.id || '';
      $('place-manage-id').value = detail.place.id || '';
      $('place-code').value = detail.place.code || $('place-code').value;
      $('place-name').value = detail.place.name || $('place-name').value;
      $('place-region').value = detail.place.region || $('place-region').value;
      applyRegionText(detail.place.region || '');
      state.placeMapItems = detail.mapAssets || [];
      renderMapAssets(state.placeMapItems);
      if (Array.isArray(detail.mapAssets) && detail.mapAssets[0] && detail.mapAssets[0].id) {
        $('managed-map-id').value = detail.mapAssets[0].id;
      }
      setPreviewValue('map-preview-place', (detail.place.name || '-') + ' / ' + (detail.place.code || '-'));
      renderMapPlaceSelect();
      persist();
    }

    function applyMapAssetDetail(detail) {
      if (!detail || !detail.mapAsset) return;
      $('managed-map-id').value = detail.mapAsset.id || '';
      $('map-code').value = detail.mapAsset.code || $('map-code').value;
      $('map-name').value = detail.mapAsset.name || $('map-name').value;
      $('map-type').value = detail.mapAsset.mapType || $('map-type').value;
      $('map-cover-url').value = detail.mapAsset.coverUrl || $('map-cover-url').value;
      $('map-summary').value = detail.mapAsset.description || $('map-summary').value;
      const currentTile = detail.mapAsset.currentTileRelease || (Array.isArray(detail.tileReleases) ? detail.tileReleases[0] : null);
      if (currentTile && currentTile.id) {
        $('tile-version').value = currentTile.versionCode || $('tile-version').value;
        $('tile-base-url').value = currentTile.tileBaseUrl || $('tile-base-url').value;
        $('tile-meta-url').value = currentTile.metaUrl || $('tile-meta-url').value;
      }
      setPreviewValue('map-preview-map', (detail.mapAsset.name || '-') + ' / ' + (detail.mapAsset.code || '-'));
      setPreviewValue('map-preview-tile-version', currentTile ? (currentTile.versionCode || '-') : '-');
      setPreviewValue('map-preview-tile-base', currentTile ? (currentTile.tileBaseUrl || '-') : '-');
      setPreviewValue('map-preview-meta', currentTile ? (currentTile.metaUrl || '-') : '-');
      renderCourseSets(detail.courseSets || []);
      const linked = Array.isArray(detail.linkedEvents) ? detail.linkedEvents : [];
      const defaultCount = linked.filter(item => !!item.isDefaultExperience).length;
      setPreviewValue('map-preview-linked-count', linked.length);
      setPreviewValue('map-preview-linked-summary', linked.length ? linked.slice(0, 4).map(item => (item.title || item.eventId || '-') + (item.isDefaultExperience ? ' / 默认体验' : '')).join('\n') : '当前未关联活动');
      setPreviewValue('map-preview-default-count', defaultCount || $('map-preview-default-count').textContent || 0);
      persist();
    }

    function applyMapExperienceDetail(detail) {
      if (!detail) return;
      setPreviewValue('map-preview-default-count', detail.defaultExperienceCount || 0);
      const items = Array.isArray(detail.defaultExperiences) ? detail.defaultExperiences : [];
      setPreviewValue(
        'map-preview-default-events',
        items.length
          ? items.map(item => (item.title || item.eventId || '-') + ' / ' + (item.status || item.statusCode || '-')).join('\n')
          : '当前无默认活动'
      );
    }

    function applyCourseImportDetail(result) {
      if (!result) return;
      if (result.courseSet && result.courseSet.id) {
        setPreviewValue('course-preview-course-set', (result.courseSet.name || '-') + ' / ' + (result.courseSet.id || '-'));
        setPreviewValue('course-preview-default-route', (result.courseSet.currentVariant && (result.courseSet.currentVariant.routeCode || result.courseSet.currentVariant.name)) || $('default-route-code').value || '-');
      }
      const variants = Array.isArray(result.variants) ? result.variants : [];
      setPreviewValue('course-preview-variant-count', variants.length);
      setPreviewValue('course-preview-variants', variants.length ? variants.map(item => (item.name || '-') + ' / ' + (item.routeCode || '-') + ' / ' + (item.status || '-')).join('\n') : '暂无路线');
    }

    async function run(title, fn) {
      setStatus('执行中：' + title, false);
      try {
        const result = await fn();
        writeLog(title, result);
        setStatus('完成：' + title, false);
        persist();
        return result;
      } catch (error) {
        const serializedError = (error && typeof error === 'object')
          ? {
              name: error.name || 'Error',
              message: error.message || '',
              stack: error.stack || '',
              status: error.status,
              body: error.body,
              method: error.method,
              url: error.url
            }
          : { message: String(error) };
        writeLog(title, { error: serializedError });
        setStatus('失败：' + title, true);
        return null;
      }
    }

    async function refreshOverview() {
      const [summaryResp, assetsResp, eventsResp, pipelineResp] = await Promise.all([
        request('GET', '/ops/admin/summary'),
        request('GET', '/ops/admin/assets'),
        request('GET', '/ops/admin/events?limit=12'),
        request('GET', '/ops/admin/events/' + encodeURIComponent($('event-id').value) + '/pipeline')
      ]);
      const summary = summaryResp.data || {};
      $('metric-assets').textContent = String(summary.managedAssets || 0);
      $('metric-places').textContent = String(summary.places || 0);
      $('metric-map-assets').textContent = String(summary.mapAssets || 0);
      $('metric-tile-releases').textContent = String(summary.tileReleases || 0);
      $('metric-course-sets').textContent = String(summary.courseSets || 0);
      $('metric-course-variants').textContent = String(summary.courseVariants || 0);
      $('metric-events').textContent = String(summary.events || 0);
      $('metric-default-events').textContent = String(summary.defaultEvents || 0);
      $('metric-published-events').textContent = String(summary.publishedEvents || 0);
      $('metric-config-sources').textContent = String(summary.configSources || 0);
      $('metric-runtime-bindings').textContent = String(summary.runtimeBindings || 0);
      $('metric-pipeline-releases').textContent = String(summary.releases || 0);
      $('metric-presentations').textContent = String(summary.presentations || 0);
      $('metric-content-bundles').textContent = String(summary.contentBundles || 0);
      $('metric-ops-users').textContent = String(summary.opsUsers || 0);
      renderAssets(assetsResp.data || []);
      renderEvents(eventsResp.data || []);
      applyPipelineSummary(pipelineResp.data || {});
      return {
        summary,
        assets: (assetsResp.data || []).length,
        events: (eventsResp.data || []).length,
        pipeline: pipelineResp.data || {}
      };
    }

    async function loadPlaces() {
      const result = await request('GET', '/ops/admin/places?limit=20');
      state.placeItems = result.data || [];
      renderPlaces(state.placeItems);
      renderMapPlaceSelect();
      if (Array.isArray(state.placeItems) && state.placeItems[0] && !($('managed-place-id').value)) {
        $('managed-place-id').value = state.placeItems[0].id;
      }
      persist();
      return result;
    }

    async function loadMapAssets() {
      const result = await request('GET', '/ops/admin/map-assets?limit=30');
      state.mapItems = result.data || [];
      renderMapLibrary(state.mapItems);
      if (Array.isArray(state.mapItems) && state.mapItems[0] && !($('managed-map-id').value)) {
        $('managed-map-id').value = state.mapItems[0].id;
      }
      persist();
      return result;
    }

    async function loadPlaceDetail(placeID) {
      const id = placeID || $('place-manage-id').value || $('managed-place-id').value;
      if (!id) throw new Error('请先提供管理地点 ID');
      $('place-manage-id').value = id;
      $('managed-place-id').value = id;
      const result = await request('GET', '/ops/admin/places/' + encodeURIComponent(id));
      applyPlaceDetail(result.data || {});
      renderPlaces(state.placeItems);
      renderMapAssets(state.placeMapItems);
      persist();
      return result;
    }

    async function loadMapAssetDetail(mapID) {
      const id = mapID || $('managed-map-id').value;
      if (!id) throw new Error('请先提供管理地图 ID');
      $('managed-map-id').value = id;
      setMapEditorMode('none');
      const [mapResult, experienceResult] = await Promise.all([
        request('GET', '/ops/admin/map-assets/' + encodeURIComponent(id)),
        request('GET', '/experience-maps/' + encodeURIComponent(id))
      ]);
      applyMapAssetDetail(mapResult.data || {});
      applyMapExperienceDetail(experienceResult.data || {});
      renderMapLibrary(state.mapItems);
      renderMapAssets(state.placeMapItems);
      $('map-detail-modal').hidden = false;
      persist();
      return { map: mapResult.data || {}, experience: experienceResult.data || {} };
    }

    $('btn-open-create-map').onclick = () => {
      $('map-detail-modal').hidden = true;
      $('managed-map-id').value = '';
      $('map-code').value = 'map-' + Date.now();
      if ($('map-place-select').value) {
        $('managed-place-id').value = $('map-place-select').value;
      }
      setMapEditorMode('map');
      persist();
      setStatus('已打开：添加地图', false);
    };

    $('btn-open-create-place').onclick = () => {
      $('map-detail-modal').hidden = true;
      $('place-manage-id').value = '';
      $('managed-place-id').value = '';
      setMapEditorMode('place');
      persist();
      setStatus('已打开：添加地点', false);
    };

    $('btn-close-map-editor').onclick = () => {
      setMapEditorMode('none');
      setStatus('已收起：地图编辑区', false);
    };

    $('btn-close-place-editor').onclick = () => {
      setMapEditorMode('none');
      setStatus('已收起：地点编辑区', false);
    };

    $('btn-close-map-detail').onclick = () => {
      $('map-detail-modal').hidden = true;
      setStatus('已关闭：地图详情', false);
    };

    $('btn-open-map-editor-from-detail').onclick = () => {
      $('map-detail-modal').hidden = true;
      setMapEditorMode('map');
      setStatus('已打开：地图编辑', false);
    };

    $('btn-open-map-tile-from-detail').onclick = () => {
      $('map-detail-modal').hidden = true;
      setMapEditorMode('map');
      setStatus('已打开：瓦片版本导入', false);
    };

    $('btn-create-place').onclick = () => run('create-place', async () => {
      const result = await request('POST', '/ops/admin/places', {
        code: $('place-code').value,
        name: $('place-name').value,
        region: $('place-region').value || undefined,
        status: 'active'
      });
      if (result && result.data && result.data.id) {
        $('managed-place-id').value = result.data.id;
        $('place-manage-id').value = result.data.id;
      }
      await loadPlaces();
      await refreshOverview();
      setMapEditorMode('none');
      persist();
      return result;
    });

    $('btn-get-place').onclick = () => run('get-place', () => loadPlaceDetail());

    $('btn-create-map-asset').onclick = () => run('create-map-asset', async () => {
      const placeId = $('map-place-select').value || $('managed-place-id').value;
      if (!placeId) throw new Error('请先选择地点');
      $('managed-place-id').value = placeId;
      const result = await request('POST', '/ops/admin/places/' + encodeURIComponent(placeId) + '/map-assets', {
        code: $('map-code').value,
        name: $('map-name').value,
        mapType: $('map-type').value,
        coverUrl: $('map-cover-url').value || undefined,
        description: $('map-summary').value || undefined,
        status: 'active'
      });
      if (result && result.data && result.data.id) {
        $('managed-map-id').value = result.data.id;
      }
      await loadMapAssets();
      if ($('managed-map-id').value) {
        await loadMapAssetDetail($('managed-map-id').value);
      }
      setMapEditorMode('none');
      persist();
      return result;
    });

    $('btn-update-map-asset').onclick = () => run('update-map-asset', async () => {
      if (!$('managed-map-id').value) throw new Error('请先读取地图详情，拿到管理地图 ID');
      const result = await request('PUT', '/ops/admin/map-assets/' + encodeURIComponent($('managed-map-id').value), {
        code: $('map-code').value,
        name: $('map-name').value,
        mapType: $('map-type').value,
        coverUrl: $('map-cover-url').value || undefined,
        description: $('map-summary').value || undefined,
        status: 'active'
      });
      await refreshOverview();
      await loadMapAssetDetail($('managed-map-id').value);
      setMapEditorMode('none');
      return result;
    });

    $('btn-get-map-asset').onclick = () => run('get-map-asset', () => loadMapAssetDetail());

    $('btn-refresh-map-area').onclick = () => run('refresh-map-area', async () => {
      const [regionsResult, placesResult, mapsResult] = await Promise.all([loadRegionOptions(), loadPlaces(), loadMapAssets()]);
      $('map-detail-modal').hidden = true;
      if ($('place-manage-id').value || $('managed-place-id').value) {
        await loadPlaceDetail($('managed-place-id').value);
      }
      if ($('managed-map-id').value) {
        await loadMapAssetDetail($('managed-map-id').value);
      }
      return {
        regions: ((regionsResult && regionsResult.data) || []).length,
        places: ((placesResult && placesResult.data) || []).length,
        maps: ((mapsResult && mapsResult.data) || []).length,
      };
    });

    $('btn-send-ops-code').onclick = () => run('ops-send-sms-code', async () => {
      return await request('POST', '/ops/auth/sms/send', {
        countryCode: $('ops-country-code').value,
        mobile: $('ops-mobile').value,
        deviceKey: $('ops-device-key').value,
        scene: 'ops_login'
      });
    });

    $('btn-register-ops').onclick = () => run('ops-register', async () => {
      const result = await request('POST', '/ops/auth/register', {
        countryCode: $('ops-country-code').value,
        mobile: $('ops-mobile').value,
        code: $('ops-code').value,
        deviceKey: $('ops-device-key').value,
        displayName: $('ops-display-name').value
      });
      state.accessToken = result.data.tokens.accessToken;
      $('ops-role-code').value = result.data.user.roleCode || '';
      syncToken();
      return result;
    });

    $('btn-open-events-view').onclick = () => {
      setActiveView('events');
      persist();
      setStatus('已切到：活动管理', false);
    };

    $('btn-open-compose-view').onclick = () => {
      setActiveView('compose');
      persist();
      setStatus('已切到：活动编排', false);
    };

    $('btn-open-publish-view').onclick = () => {
      setActiveView('publish');
      persist();
      setStatus('已切到：发布中心', false);
    };

    $('btn-login-ops').onclick = () => run('ops-login-sms', async () => {
      const result = await request('POST', '/ops/auth/login/sms', {
        countryCode: $('ops-country-code').value,
        mobile: $('ops-mobile').value,
        code: $('ops-code').value,
        deviceKey: $('ops-device-key').value
      });
      state.accessToken = result.data.tokens.accessToken;
      $('ops-role-code').value = result.data.user.roleCode || '';
      syncToken();
      return result;
    });

    $('btn-clear-token').onclick = () => {
      state.accessToken = '';
      syncToken();
      persist();
      setStatus('已清空 token', false);
    };

    $('btn-refresh-overview').onclick = () => run('refresh-overview', refreshOverview);

    $('btn-upload-asset').onclick = () => run('upload-asset', async () => {
      const file = $('asset-file').files[0];
      if (!file) throw new Error('请选择文件');
      const form = new FormData();
      form.append('assetType', $('asset-type').value);
      form.append('assetCode', $('asset-code').value);
      form.append('version', $('asset-version').value);
      form.append('title', $('asset-title').value);
      form.append('objectDir', $('asset-object-dir').value);
      form.append('status', $('asset-status').value);
      form.append('metadataJson', $('asset-metadata').value || '{}');
      form.append('file', file);
      const result = await request('POST', '/ops/admin/assets/upload', form, false);
      await refreshOverview();
      return result;
    });

    $('btn-register-link').onclick = () => run('register-link', async () => {
      const result = await request('POST', '/ops/admin/assets/register-link', {
        assetType: $('asset-type').value,
        assetCode: $('asset-code').value,
        version: $('asset-version').value,
        title: $('asset-title').value,
        publicUrl: $('asset-public-url').value,
        contentType: $('asset-content-type').value || undefined,
        status: $('asset-status').value,
        metadata: JSON.parse($('asset-metadata').value || '{}')
      });
      await refreshOverview();
      return result;
    });

    $('btn-list-assets').onclick = () => run('list-assets', async () => {
      const result = await request('GET', '/ops/admin/assets');
      renderAssets(result.data || []);
      return result;
    });

    $('btn-import-tile').onclick = () => run('import-tile-release', async () => {
      const result = await request('POST', '/ops/admin/ops/tile-releases/import', {
        placeCode: $('place-code').value,
        placeName: $('place-name').value,
        mapAssetCode: $('map-code').value,
        mapAssetName: $('map-name').value,
        mapType: $('map-type').value,
        versionCode: $('tile-version').value,
        status: 'active',
        tileBaseUrl: $('tile-base-url').value,
        metaUrl: $('tile-meta-url').value,
        setAsCurrent: true
      });
      if (result && result.data) {
        if (result.data.place && result.data.place.id) $('managed-place-id').value = result.data.place.id;
        if (result.data.mapAsset && result.data.mapAsset.id) $('managed-map-id').value = result.data.mapAsset.id;
        applyMapAssetDetail({
          mapAsset: result.data.mapAsset,
          tileReleases: result.data.tileRelease ? [result.data.tileRelease] : []
        });
      }
      $('map-detail-modal').hidden = false;
      setMapEditorMode('none');
      persist();
      return result;
    });

    $('btn-import-kml-batch').onclick = () => run('import-kml-batch', async () => {
      const result = await request('POST', '/ops/admin/ops/course-sets/import-kml-batch', {
        placeCode: $('place-code').value,
        placeName: $('place-name').value,
        mapAssetCode: $('map-code').value,
        mapAssetName: $('map-name').value,
        mapType: $('map-type').value,
        courseSetCode: $('course-set-code').value,
        courseSetName: $('course-set-name').value,
        mode: $('course-mode').value,
        status: 'active',
        defaultRouteCode: $('default-route-code').value,
        routes: JSON.parse($('routes-json').value || '[]')
      });
      applyCourseImportDetail(result.data || {});
      return result;
    });

    $('btn-list-events').onclick = () => run('list-events', async () => {
      const result = await request('GET', '/ops/admin/events?limit=30');
      renderEvents(result.data || []);
      renderEventListMain(result.data || []);
      if (Array.isArray(result.data) && result.data[0] && result.data[0].id) {
        $('binding-event-id').value = result.data[0].id;
        $('pipeline-event-id').value = result.data[0].id;
      }
      persist();
      return result;
    });

    $('btn-create-event').onclick = () => run('create-event', async () => {
      const result = await request('POST', '/ops/admin/events', {
        tenantCode: $('event-tenant-code').value || null,
        slug: $('event-slug').value,
        displayName: $('event-display-name').value,
        summary: $('event-summary').value || null,
        status: $('event-status').value
      });
      if (result && result.data) {
        applyEventDetail({ event: result.data });
      }
      persist();
      await refreshOverview();
      return result;
    });

    $('btn-update-event').onclick = () => run('update-event', async () => {
      const eventId = $('binding-event-id').value.trim();
      if (!eventId) throw new Error('请先填写活动 ID');
      const result = await request('PUT', '/ops/admin/events/' + encodeURIComponent(eventId), {
        tenantCode: $('event-tenant-code').value || null,
        slug: $('event-slug').value,
        displayName: $('event-display-name').value,
        summary: $('event-summary').value || null,
        status: $('event-status').value
      });
      if (result && result.data) {
        applyEventDetail({ event: result.data });
      }
      persist();
      await refreshOverview();
      return result;
    });

    $('btn-get-event').onclick = () => run('get-event', async () => {
      const result = await request('GET', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value));
      applyEventDetail(result.data || {});
      return result;
    });

    $('btn-import-presentation').onclick = () => run('import-presentation', async () => {
      const result = await request('POST', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value) + '/presentations/import', {
        title: $('presentation-title').value,
        templateKey: $('presentation-template-key').value,
        sourceType: 'schema',
        schemaUrl: $('presentation-schema-url').value,
        version: $('presentation-version').value,
        status: 'active',
        isDefault: true
      });
      if (result && result.data && result.data.id) $('presentation-id').value = result.data.id;
      persist();
      return result;
    });

    $('btn-import-bundle').onclick = () => run('import-content-bundle', async () => {
      const manifestUrl = $('bundle-manifest-url').value;
      const result = await request('POST', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value) + '/content-bundles/import', {
        title: $('bundle-title').value,
        bundleType: $('bundle-type').value,
        sourceType: 'manifest',
        manifestUrl: manifestUrl,
        version: $('bundle-version').value,
        status: 'active',
        isDefault: true,
        assetManifest: { manifestUrl }
      });
      if (result && result.data && result.data.id) $('content-bundle-id').value = result.data.id;
      persist();
      return result;
    });

    $('btn-save-defaults').onclick = () => run('save-event-defaults', async () => {
      return await request('POST', '/ops/admin/events/' + encodeURIComponent($('binding-event-id').value) + '/defaults', {
        presentationId: $('presentation-id').value || undefined,
        contentBundleId: $('content-bundle-id').value || undefined,
        runtimeBindingId: $('runtime-binding-id').value || undefined
      });
    });

    $('btn-get-pipeline').onclick = () => run('get-pipeline', async () => {
      const result = await request('GET', '/ops/admin/events/' + encodeURIComponent($('pipeline-event-id').value) + '/pipeline');
      applyPipelineSummary(result.data || {});
      return result;
    });

    $('btn-build-source').onclick = () => run('build-source', async () => {
      if (!$('source-id').value) throw new Error('请先提供配置源 ID');
      const result = await request('POST', '/ops/admin/sources/' + encodeURIComponent($('source-id').value) + '/build');
      if (result && result.data && result.data.id) $('build-id').value = result.data.id;
      persist();
      return result;
    });

    $('btn-publish-build').onclick = () => run('publish-build', async () => {
      if (!$('build-id').value) throw new Error('请先提供构建 ID');
      const result = await request('POST', '/ops/admin/builds/' + encodeURIComponent($('build-id').value) + '/publish', {
        runtimeBindingId: $('runtime-binding-id').value || undefined,
        presentationId: $('presentation-id').value || undefined,
        contentBundleId: $('content-bundle-id').value || undefined
      });
      if (result && result.data && result.data.id) {
        $('release-id').value = result.data.id;
        $('rollback-release-id').value = result.data.id;
      }
      persist();
      return result;
    });

    $('btn-get-release').onclick = () => run('get-release', async () => {
      if (!$('release-id').value) throw new Error('请先提供发布版本 ID');
      return await request('GET', '/ops/admin/releases/' + encodeURIComponent($('release-id').value));
    });

    $('btn-rollback-release').onclick = () => run('rollback-release', async () => {
      if (!$('rollback-release-id').value) throw new Error('请先提供待回滚的发布版本 ID');
      return await request('POST', '/ops/admin/events/' + encodeURIComponent($('pipeline-event-id').value) + '/rollback', {
        releaseId: $('rollback-release-id').value
      });
    });

    document.querySelectorAll('input, textarea, select').forEach(node => {
      node.addEventListener('change', persist);
      node.addEventListener('input', persist);
    });

    ['place-search', 'map-search'].forEach(id => {
      $(id).addEventListener('input', () => {
        renderPlaces(state.placeItems);
        renderMapLibrary(state.mapItems);
        renderMapAssets(state.placeMapItems);
      });
    });

    $('place-province').addEventListener('change', () => {
      renderCityOptions($('place-province').value);
    });
    $('place-city').addEventListener('change', syncPlaceRegion);
    $('map-place-select').addEventListener('change', () => {
      $('managed-place-id').value = $('map-place-select').value || '';
      persist();
    });

    $('place-list').addEventListener('click', event => {
      const item = event.target.closest('[data-place-id]');
      if (!item) return;
      void run('get-place', () => loadPlaceDetail(item.dataset.placeId));
    });

    ['map-list', 'map-library-list'].forEach(id => {
      $(id).addEventListener('click', event => {
        const item = event.target.closest('[data-map-id]');
        if (!item) return;
        void run('get-map-asset', () => loadMapAssetDetail(item.dataset.mapId));
      });
    });

    ['map-detail-modal', 'map-editor-panel', 'place-editor-panel'].forEach(id => {
      $(id).addEventListener('click', event => {
        if (event.target !== $(id)) return;
        if (id === 'map-detail-modal') {
          $('map-detail-modal').hidden = true;
          return;
        }
        setMapEditorMode('none');
      });
    });

    document.querySelectorAll('.nav-link[data-view]').forEach(button => {
      button.addEventListener('click', () => {
        setActiveView(button.dataset.view);
        persist();
        if (button.dataset.view === 'overview') {
          void refreshOverviewAndLog('refresh-overview');
        } else if (button.dataset.view === 'maps') {
          void run('refresh-map-area', async () => {
            const [regionsResult, placesResult, mapsResult] = await Promise.all([loadRegionOptions(), loadPlaces(), loadMapAssets()]);
            return {
              regions: ((regionsResult && regionsResult.data) || []).length,
              places: ((placesResult && placesResult.data) || []).length,
              maps: ((mapsResult && mapsResult.data) || []).length,
            };
          });
        }
      });
    });

    restore();
    setActiveView(state.activeView);
    setMapEditorMode('none');
    renderPlaces([]);
    renderMapAssets([]);
    renderMapLibrary([]);
    renderCourseSets([]);
    renderAssets([]);
    renderEvents([]);
    renderEventListMain([]);
    writeLog('ops-workbench-ready', {
      ok: true,
      hint: '开发环境默认免登录。建议顺序：先看资源总览 -> 地图/地点管理 -> 路线资源管理 -> 活动管理 / 活动编排 -> 发布中心。'
    });
    void refreshOverviewAndLog('refresh-overview');
    void loadRegionOptions();
    if (state.activeView === 'maps') {
      void run('refresh-map-area', async () => {
        const [regionsResult, placesResult, mapsResult] = await Promise.all([loadRegionOptions(), loadPlaces(), loadMapAssets()]);
        return {
          regions: ((regionsResult && regionsResult.data) || []).length,
          places: ((placesResult && placesResult.data) || []).length,
          maps: ((mapsResult && mapsResult.data) || []).length,
        };
      });
    }
  </script>
</body>
</html>`
