diff --git a/b2f.md b/b2f.md
index 985b7fc..3e7532a 100644
--- a/b2f.md
+++ b/b2f.md
@@ -1,6 +1,6 @@
# b2f
-> 文档版本:v1.19
-> 最后更新:2026-04-03 16:43:25
+> 文档版本:v1.26
+> 最后更新:2026-04-03 19:18:34
说明:
@@ -12,6 +12,177 @@
## 待确认
+### B2F-038
+
+- 时间:2026-04-03 19:13:57
+- 谁提的:backend
+- 当前事实:
+ - backend 已按“活动卡片列表最小产品化第一刀”补齐以下返回中的活动卡片最小摘要字段:
+ - `GET /cards`
+ - `GET /home`
+ - `GET /me/entry-home`
+ - 当前最小摘要字段为:
+ - `summary`
+ - `status`
+ - `statusCode`
+ - `timeWindow`
+ - `ctaText`
+ - `isDefaultExperience`
+ - `eventType`
+ - `currentPresentation`
+ - `currentContentBundle`
+ - backend 当前希望 frontend 这轮优先做的是:
+ - 活动列表页按这组字段完成最小接线
+ - 详情页继续沿用:
+ - `play.canLaunch`
+ - `currentPresentation`
+ - `currentContentBundle`
+ 这组已发布 release 语义
+ - 联调时继续通过 frontend 调试日志回传以下事实:
+ - 列表页实际拿到的 `cardEventIds`
+ - 点击卡片后的 `eventId`
+ - 详情页实际显示的 `status / canLaunch / currentPresentation / currentContentBundle`
+- 需要对方确认什么:
+ - frontend 请按这组字段完成活动卡片列表最小实现,并回写:
+ - 当前字段是否足够
+ - 列表页是否还缺必需字段
+ - 最新日志里是否已能稳定看到:
+ - `cardEventIds`
+ - `clickedEventId`
+ - `detail.status`
+ - `detail.canLaunch`
+- 是否已解决:否
+
+### B2F-037
+
+- 时间:2026-04-03 22:52:10
+- 谁提的:backend
+- 当前事实:
+ - backend 已根据 frontend 在 `F2B-013` 的结构化日志,确认 manual 多赛道当前不显示赛道选择区的根因不在 frontend 展示层
+ - 当前 frontend 日志事实为:
+ - `event-play.pageEventId = evt_demo_variant_manual_001`
+ - `event-play.variantCount = 0`
+ - `event-prepare.variantCount = 0`
+ - `event-prepare.selectableVariantCount = 0`
+ - `event-prepare.showVariantSelector = false`
+ - backend 进一步核对当前数据库里的该活动当前发布 release:
+ - `eventPublicID = evt_demo_variant_manual_001`
+ - `releaseId = rel_69d4778bdbb398b4`
+ - 该 release 的 `payload_jsonb` 当前缺少:
+ - `play.assignmentMode`
+ - `play.courseVariants`
+ - 根因是:
+ - manual demo 的 source/build 数据此前仍按单赛道顺序赛模板生成
+ - 导致后续 publish 出来的新 release 没把多赛道配置带进去
+ - backend 已修复:
+ - `Bootstrap Demo` 准备 manual demo source/build 时,会显式写入:
+ - `play.assignmentMode = manual`
+ - `play.courseVariants = [variant_a, variant_b]`
+- 需要对方确认什么:
+ - 无,当前这条已通过本轮联调日志确认
+- 是否已解决:是
+
+### B2F-036
+
+- 时间:2026-04-03 22:34:08
+- 谁提的:backend
+- 当前事实:
+ - backend 已按活动卡片列表最小产品化第一刀,统一补齐以下返回里的卡片摘要字段:
+ - `GET /cards`
+ - `GET /home`
+ - `GET /me/entry-home`
+ - 当前新增/补齐字段为:
+ - `summary`
+ - `status`
+ - `statusCode`
+ - `timeWindow`
+ - `ctaText`
+ - `isDefaultExperience`
+ - `eventType`
+ - `currentPresentation`
+ - `currentContentBundle`
+ - 当前口径固定如下:
+ - `summary` 缺失时回退:`当前暂无活动摘要`
+ - `timeWindow` 缺失时回退:`时间待公布`
+ - `ctaText` 当前由 backend 派生:
+ - 默认体验活动:`进入体验`
+ - 进行中:`进入活动`
+ - 已结束:`查看回顾`
+ - 其余:`查看详情`
+ - `currentPresentation / currentContentBundle` 继续表示当前已发布 release 摘要,不是 event 草稿默认值
+ - backend 已给 `cards` 落显式字段:
+ - `is_default_experience`
+ - 当前 demo 数据已标记:
+ - 顺序赛为默认体验活动
+ - 积分赛、多赛道为普通活动
+- 需要对方确认什么:
+ - frontend 可按以上字段和降级规则开始活动卡片列表最小产品化第一刀
+ - frontend 请回写:
+ - 当前字段是否足够启动列表页最小实现
+ - 是否还缺列表页必需名称摘要
+- 是否已解决:否
+
+### B2F-035
+
+- 时间:2026-04-03 18:16:19
+- 谁提的:backend
+- 当前事实:
+ - backend 已根据 frontend 在 `F2B-012` 的反馈,正式收紧 `play.canLaunch` 和 `POST /events/{eventPublicID}/launch` 的前置条件
+ - 当前规则已改为:
+ - 仅当当前 event 满足以下条件时,`play.canLaunch = true`
+ - event `status = active`
+ - 已存在当前发布 release
+ - 当前发布 release 有 `manifest`
+ - 当前发布 release 已绑定 `runtime`
+ - 当前发布 release 已绑定 `presentation`
+ - 当前发布 release 已绑定 `content bundle`
+ - 当前若缺任一项,backend 会返回更明确原因,例如:
+ - `current published release is missing runtime binding`
+ - `current published release is missing presentation binding`
+ - `current published release is missing content bundle binding`
+ - `launch` 当前也已按同一套规则阻断,避免出现:
+ - `play.canLaunch = false`
+ - 但直接调用 `launch` 仍能进局
+- 需要对方确认什么:
+ - frontend 请在 backend 重启后复验:
+ - 当 `currentPresentation / currentContentBundle / runtime` 任意缺失时,`play.canLaunch` 是否已变为 `false`
+ - `play.reason` 是否已返回更具体缺失原因
+ - frontend 页面当前可继续沿用:
+ - `canLaunch=false` 时禁用进入动作
+ - 同时展示 backend 返回的 `reason`
+- 是否已解决:否
+
+### B2F-034
+
+- 时间:2026-04-03 18:05:19
+- 谁提的:backend
+- 当前事实:
+ - backend 当前已确认一个需要 frontend 明确区分的语义:
+ - `currentPresentation`
+ - `currentContentBundle`
+ 当前表示的是“当前已发布 release 上实际绑定的展示版本 / 内容包版本摘要”
+ - 它们当前不是:
+ - 活动草稿默认值
+ - event 默认绑定草稿态
+ - 这也解释了为什么:
+ - 后台未完成导入 + 默认绑定 + publish 之前,这两项可能为空
+ - 一旦跑过后台发布链,它们就会开始显示
+ - backend 当前正式规则也已明确:
+ - 玩家进入游戏必须基于“已发布 release”
+ - 不能基于未发布默认配置直接放行
+- 需要对方确认什么:
+ - frontend 请按以下口径调整页面语义:
+ - 文案优先改成:
+ - `当前发布展示版本`
+ - `当前发布内容包版本`
+ - 玩家能否继续进入,优先只看:
+ - `play.canLaunch`
+ - 当这两项为空时,优先解释为:
+ - 当前发布 release 未绑定
+ - 或当前尚未发布
+ - 不要把它们展示成“活动默认配置已存在,只是未显示”
+- 是否已解决:否
+
### B2F-032
- 时间:2026-04-03 16:43:25
@@ -269,6 +440,23 @@
## 已确认
+### B2F-033
+
+- 时间:2026-04-03 17:25:35
+- 谁提的:backend
+- 当前事实:
+ - backend 已把玩法切换对应的联调资源补齐到 workbench:
+ - `presentation schema`
+ - `content manifest`
+ - `asset manifest`
+ - 玩法切换现在会自动填真实 dev 资源地址,不再继续保留 `example.com` 占位:
+ - `GET /dev/demo-assets/presentations/{demoKey}`
+ - `GET /dev/demo-assets/content-manifests/{demoKey}`
+ - 当前联调样例文案也已统一成中文活动样例,便于 frontend 直接核对页面显示与日志事实
+- 需要对方确认什么:
+ - frontend 如需核对当前玩法对应的展示/内容输入,可直接对照 workbench 当前表单值与上述两条 dev 资源地址
+- 是否已解决:是
+
### B2F-027
- 时间:2026-04-03 14:37:00
diff --git a/b2t.md b/b2t.md
index 57b412e..f575d05 100644
--- a/b2t.md
+++ b/b2t.md
@@ -1,6 +1,6 @@
# B2T 协作清单
-> 文档版本:v1.18
-> 最后更新:2026-04-03 16:16:38
+> 文档版本:v1.22
+> 最后更新:2026-04-03 19:21:23
说明:
@@ -36,6 +36,63 @@
## 已确认
+### B2T-031
+
+- 时间:2026-04-03 19:21:23
+- 谁提的:backend
+- 当前事实:
+ - frontend 本轮已通过结构化调试日志确认以下链路正常:
+ - 活动列表页当前能稳定拿到 3 张 demo 卡片
+ - 多赛道入口点击后能进入正确活动:
+ - `evt_demo_variant_manual_001`
+ - 多赛道详情当前已拿到:
+ - `assignmentMode = manual`
+ - `variantCount = 2`
+ - `detailCanLaunch = true`
+ - 当前发布 `presentation / content bundle` 摘要正常
+ - backend 当前可确认:
+ - manual 多赛道 demo 的当前发布 release 已切到正确版本
+ - 活动列表最小产品化第一刀与前端当前接线口径一致
+ - frontend 最新 `f2b.md` 当前也已将:
+ - `F2B-014`
+ 标记为已确认
+ - 当前仍留一条未完全收口的旧项:
+ - `F2B-011`
+ - 即 demo 活动历史 ongoing session 回收口径,需要后续单独收掉
+- 需要对方确认什么:
+ - 无
+- 是否已解决:是
+
+### B2T-029
+
+- 时间:2026-04-03 22:34:08
+- 谁提的:backend
+- 当前事实:
+ - backend 已按 `活动卡片列表最小产品化配合阶段` 落完第一刀最小摘要字段
+ - 当前以下返回已统一补齐活动卡片最小摘要:
+ - `GET /cards`
+ - `GET /home`
+ - `GET /me/entry-home`
+ - 当前摘要字段包括:
+ - `summary`
+ - `status`
+ - `statusCode`
+ - `timeWindow`
+ - `ctaText`
+ - `isDefaultExperience`
+ - `eventType`
+ - `currentPresentation`
+ - `currentContentBundle`
+ - 当前实现继续保持:
+ - 不新增对象层级
+ - 不改活动详情页现有语义
+ - `currentPresentation / currentContentBundle` 仍表示当前已发布 release 摘要
+ - backend 同时已通过 `0011_card_summary.sql` 给 `cards` 落了显式字段:
+ - `is_default_experience`
+- 需要对方确认什么:
+ - 无
+- 是否已解决:是
+
### B2T-028
- 时间:2026-04-03 16:16:38
@@ -184,6 +241,67 @@
## 已完成
+### B2T-030
+
+- 时间:2026-04-03 19:08:55
+- 谁提的:backend
+- 当前事实:
+ - backend 当前已完成并稳定运行的主线可概括为三段:
+ - 联调标准化阶段
+ - 真实输入替换第一刀
+ - 活动卡片列表最小产品化第一刀
+ - 联调标准化阶段当前已具备:
+ - 一键测试链
+ - 详细日志
+ - 稳定 demo 数据
+ - workbench 回归结果汇总
+ - frontend 调试日志通道
+ - 真实输入替换第一刀当前已完成:
+ - 真实 KML
+ - 真实地图 URL
+ - demo content manifest / presentation schema 通过 backend dev 资源入口提供
+ - 中文活动文案样例
+ - 活动卡片列表最小产品化第一刀当前已完成:
+ - `GET /cards`
+ - `GET /home`
+ - `GET /me/entry-home`
+ 统一补齐活动卡片摘要字段
+ - 当前卡片最小摘要字段包括:
+ - `summary`
+ - `status`
+ - `statusCode`
+ - `timeWindow`
+ - `ctaText`
+ - `isDefaultExperience`
+ - `eventType`
+ - `currentPresentation`
+ - `currentContentBundle`
+ - 当前阶段 backend 仍保持:
+ - 不扩新对象层级
+ - 不推翻现有 `Event / EventRelease / Session`
+ - 继续以标准联调链为唯一基线
+- 需要对方确认什么:
+ - 无
+- 是否已解决:是
+
+### B2T-029
+
+- 时间:2026-04-03 17:25:35
+- 谁提的:backend
+- 当前事实:
+ - backend 已把“真实输入替换第一刀”继续推进到:
+ - `content manifest`
+ - `presentation schema`
+ - 中文活动文案样例
+ - 当前 workbench 的玩法切换会自动填充 backend 内置 demo 资源:
+ - `GET /dev/demo-assets/presentations/{demoKey}`
+ - `GET /dev/demo-assets/content-manifests/{demoKey}`
+ - 这两条路由只服务联调,不进入正式客户端运行链路
+ - `Bootstrap Demo` 当前准备的联调样例文案已统一为中文活动样例,不再继续暴露一批 `Demo ...` 名称
+- 需要对方确认什么:
+ - 无
+- 是否已解决:是
+
### B2T-024
- 时间:2026-04-03 14:21:24
diff --git a/backend/README.md b/backend/README.md
index 76d51bd..f4c4f59 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -1,6 +1,6 @@
# Backend
-> 文档版本:v1.17
-> 最后更新:2026-04-03 16:16:38
+> 文档版本:v1.22
+> 最后更新:2026-04-03 18:56:46
这套后端现在已经能支撑一条完整主链:
@@ -14,6 +14,22 @@
- 真正进入游戏时客户端消费的是 `manifest_url`
- `session` 会固化当时实际绑定的 `release`
+当前还要明确一条业务规则:
+
+- 玩家进入游戏,必须基于“已发布 release”
+- `event` 默认绑定、活动草稿配置、未发布 presentation / content bundle 都不能直接作为玩家正式进入依据
+- 当前 `currentPresentation` / `currentContentBundle` 在玩家链路里表示的是:
+ - 当前已发布 release 实际绑定的展示版本摘要
+ - 当前已发布 release 实际绑定的内容包摘要
+- 它们不是 event 草稿默认值摘要
+- 当前 `play.canLaunch` 和 `launch` 也已按同一套规则收口:
+ - 只有当当前发布 release 同时具备:
+ - `manifest`
+ - `runtime`
+ - `presentation`
+ - `content bundle`
+ 时,玩家才允许正式进入
+
当前 workbench 里新增的“当前 Launch 实际配置摘要”仅用于调试:
- 它会由 backend 代读当前 launch 对应的 manifest
@@ -35,6 +51,51 @@
- backend 会临时保留最近 200 条日志,供 workbench 查看与清空
- 这块只用于联调排查,不替代正式生产日志体系
+当前 demo 真实输入第一刀也已经接入:
+
+- workbench 的玩法切换会自动填入 backend 内置的:
+ - `presentation schema`
+ - `content manifest`
+- 这些 demo 资源通过 backend 提供的 dev 路由读取:
+ - `GET /dev/demo-assets/presentations/{demoKey}`
+ - `GET /dev/demo-assets/content-manifests/{demoKey}`
+
+当前 workbench 的 `Bootstrap` 语义也已经拆开:
+
+- `Bootstrap Demo(只准备数据)`
+ - 只准备 demo 测试数据,不额外重新发布当前玩法
+- `Bootstrap + 发布当前玩法`
+ - 先准备 demo,再对当前选中的玩法执行一遍“发布活动配置(自动补 Runtime)”
+- 这两条路由只服务联调,不进入正式客户端发布链
+- 当前联调样例文案也已从 `Demo ...` 收口为中文活动样例,便于前端和总控直接对口排查
+
+当前活动卡片列表最小产品化第一刀也已经进入 backend:
+
+- `/cards`
+- `/home`
+- `/me/entry-home`
+
+这三处当前已统一补齐最小活动卡片摘要字段:
+
+- `summary`
+- `status`
+- `statusCode`
+- `timeWindow`
+- `ctaText`
+- `isDefaultExperience`
+- `eventType`
+- `currentPresentation`
+- `currentContentBundle`
+
+当前口径:
+
+- 卡片摘要与详情页继续共用同一套“当前发布 release 摘要”语义
+- `currentPresentation / currentContentBundle` 仍表示:
+ - 当前已发布 release 实际绑定的展示版本摘要
+ - 当前已发布 release 实际绑定的内容包摘要
+- `isDefaultExperience` 当前由卡片显式字段控制
+- `timeWindow / ctaText` 当前先按后端派生规则提供,允许后续继续演进
+
## 文档导航
- [文档索引](D:/dev/cmr-mini/backend/docs/README.md)
diff --git a/backend/docs/开发说明.md b/backend/docs/开发说明.md
index 78e942d..01d7514 100644
--- a/backend/docs/开发说明.md
+++ b/backend/docs/开发说明.md
@@ -1,6 +1,6 @@
# 开发说明
-> 文档版本:v1.20
-> 最后更新:2026-04-03 16:16:38
+> 文档版本:v1.25
+> 最后更新:2026-04-03 18:56:46
## 1. 环境变量
@@ -45,6 +45,13 @@ cd D:\dev\cmr-mini\backend
- `Bootstrap Demo`
- `Use Classic Demo / Use Score-O Demo / Use Manual Variant Demo`
- `整条链一键验收`
+- 当前玩法切换除了切 `event / release / source / build`,还会自动切换:
+ - `presentation schema`
+ - `content manifest`
+ - `asset manifest`
+- 这些 demo 资源现在由 backend 提供,避免继续在 workbench 里保留 `example.com` 占位地址:
+ - `GET /dev/demo-assets/presentations/{demoKey}`
+ - `GET /dev/demo-assets/content-manifests/{demoKey}`
- 如果 frontend 需要把页面侧调试日志直接打到 backend,优先使用:
- `POST /dev/client-logs`
- 然后在 workbench 的 `前端调试日志` 面板里查看
@@ -67,6 +74,54 @@ cd D:\dev\cmr-mini\backend
- backend 当前只在内存里保留最近 200 条
- 适合前端把关键事实直接打进来,避免只靠截图和口头描述
- 不替代正式生产日志体系
+- `Bootstrap Demo` 准备出的联调文案也已换成中文样例:
+ - `领秀城公园顺序赛`
+ - `领秀城公园积分赛`
+ - `领秀城公园多赛道挑战`
+
+## 4. 活动卡片列表最小摘要
+
+当前 backend 已为以下入口统一补齐活动卡片最小摘要字段:
+
+- `/cards`
+- `/home`
+- `/me/entry-home`
+
+当前字段集:
+
+- `title`
+- `subtitle`
+- `summary`
+- `status`
+- `statusCode`
+- `timeWindow`
+- `ctaText`
+- `coverUrl`
+- `isDefaultExperience`
+- `eventType`
+- `currentPresentation`
+- `currentContentBundle`
+
+当前派生规则:
+
+- `summary`
+ - 无值时回退为:`当前暂无活动摘要`
+- `status`
+ - `running` -> `进行中`
+ - `upcoming` -> `即将开始`
+ - `ended` -> `已结束`
+ - 其余 -> `状态待确认`
+- `timeWindow`
+ - 由 `cards.starts_at / ends_at` 派生
+ - 缺失时回退为:`时间待公布`
+- `ctaText`
+ - 默认体验活动:`进入体验`
+ - 进行中:`进入活动`
+ - 已结束:`查看回顾`
+ - 其余:`查看详情`
+- `currentPresentation / currentContentBundle`
+ - 当前继续表示已发布 release 实际绑定摘要
+ - 不是 event 草稿默认值
默认会设置:
@@ -100,13 +155,26 @@ cd D:\dev\cmr-mini\backend
当前推荐顺序:
-1. `Bootstrap Demo`
+1. `Bootstrap Demo(只准备数据)`
2. 选择一种玩法入口:
- `Use Classic Demo`
- `Use Score-O Demo`
- `Use Manual Variant Demo`
-3. `一键补齐 Runtime 并发布`
-4. `一键标准回归`
+3. 如果只是想看发布过程,点 `Bootstrap + 发布当前玩法`
+4. 如果想只测发布链,点 `一键补齐 Runtime 并发布`
+5. 如果想直接验整条链,点 `一键标准回归`
+
+当前这几个按钮的职责已经拆开:
+
+- `Bootstrap Demo(只准备数据)`
+ - 只负责准备 demo event / source / build / release / runtime 等测试数据
+ - 不会基于当前玩法再额外重新发布一版
+- `Bootstrap + 发布当前玩法`
+ - 会先执行一遍 `Bootstrap Demo`
+ - 然后对当前选中的玩法执行“发布活动配置(自动补 Runtime)”
+- `一键补齐 Runtime 并发布`
+ - 不再隐式 bootstrap
+ - 只基于当前已选玩法和当前表单上下文执行发布链
当前这条一键链会自动完成:
@@ -220,6 +288,48 @@ dev 环境下,frontend 可直接把关键调试事实发到 backend:
## 3. 当前开发约定
+### 3.0 玩家进入规则
+
+当前要明确一条玩家链路规则:
+
+- 玩家进入游戏,必须基于“已发布 release”
+- 不能基于:
+ - event 草稿默认绑定
+ - 未发布 presentation
+ - 未发布 content bundle
+ - 未发布 runtime
+
+当前接口中的:
+
+- `currentPresentation`
+- `currentContentBundle`
+
+在玩家链路里表示的是:
+
+- 当前已发布 release 上实际绑定的展示版本摘要
+- 当前已发布 release 上实际绑定的内容包摘要
+
+不是:
+
+- event 草稿默认值摘要
+
+所以如果当前 release 还没绑定这些对象,玩家页看到空值是正常行为。前端页面应优先:
+
+- 用 `play.canLaunch` 判定是否允许进入
+- 把空值解释成“当前未发布或当前发布未绑定”
+
+当前 `canLaunch` 已按正式进入规则收紧:
+
+- 只有当当前 event 满足以下条件时,`play.canLaunch = true`
+ - event `status = active`
+ - 已存在当前发布 release
+ - 当前发布 release 有 `manifest`
+ - 当前发布 release 已绑定 `runtime`
+ - 当前发布 release 已绑定 `presentation`
+ - 当前发布 release 已绑定 `content bundle`
+
+当前 `POST /events/{eventPublicID}/launch` 也已与 `canLaunch` 保持同一套前置条件。
+
### 3.1 开发阶段先不用 Redis
当前第一版全部依赖:
diff --git a/backend/docs/接口清单.md b/backend/docs/接口清单.md
index 8db180b..4bfffde 100644
--- a/backend/docs/接口清单.md
+++ b/backend/docs/接口清单.md
@@ -1,6 +1,6 @@
# API 清单
-> 文档版本:v1.9
-> 最后更新:2026-04-03 16:16:38
+> 文档版本:v1.12
+> 最后更新:2026-04-03 22:34:08
本文档只记录当前 backend 已实现接口,不写未来规划接口。
@@ -95,12 +95,23 @@
用途:
- 返回入口首页卡片
+- 当前卡片摘要字段已统一补齐:
+ - `summary`
+ - `status`
+ - `statusCode`
+ - `timeWindow`
+ - `ctaText`
+ - `isDefaultExperience`
+ - `eventType`
+ - `currentPresentation`
+ - `currentContentBundle`
### `GET /cards`
用途:
- 只返回卡片列表
+- 当前与 `/home` 使用同一套卡片摘要语义
### `GET /me/entry-home`
@@ -111,6 +122,7 @@
用途:
- 首页聚合接口
+- 当前 `cards` 也已统一使用活动卡片最小摘要字段
返回重点:
@@ -179,6 +191,15 @@
- `play.ongoingSession`
- `play.recentSession`
+当前 `play.canLaunch=true` 的最小前置条件为:
+
+- event `status = active`
+- 当前已发布 release 存在
+- 当前已发布 release 有 `manifest`
+- 当前已发布 release 已绑定 `runtime`
+- 当前已发布 release 已绑定 `presentation`
+- 当前已发布 release 已绑定 `content bundle`
+
当前摘要字段最少包括:
- `currentPresentation.presentationId`
@@ -210,6 +231,12 @@
- 如果当前 release 声明了 `play.courseVariants[]`
- `launch` 会返回最终绑定的 `launch.variant`
- 当前为兼容旧调用方,`assignmentMode=manual` 且未传 `variantId` 时,backend 会先回退到首个可选 variant
+- 当前 `launch` 与 `play.canLaunch` 使用同一套前置条件
+- 若当前发布 release 缺少:
+ - `runtime`
+ - `presentation`
+ - `content bundle`
+ 之一,`launch` 会直接返回 `409`
返回重点:
@@ -530,6 +557,38 @@
- `playfield.kind`
- `game.mode`
+### `GET /dev/demo-assets/presentations/{demoKey}`
+
+环境:
+
+- 仅 non-production
+
+用途:
+
+- 返回联调用的示例展示定义 schema
+- 给 workbench 的玩法切换自动填充真实 `presentation schema` 资源地址
+
+路径参数:
+
+- `demoKey`
+ - 当前支持:`classic`、`score-o`、`manual-variant`
+
+### `GET /dev/demo-assets/content-manifests/{demoKey}`
+
+环境:
+
+- 仅 non-production
+
+用途:
+
+- 返回联调用的示例内容 manifest
+- 给 workbench 的玩法切换自动填充真实 `content manifest` 资源地址
+
+路径参数:
+
+- `demoKey`
+ - 当前支持:`classic`、`score-o`、`manual-variant`
+
补充说明:
- 只用于 workbench 联调排查
@@ -1357,3 +1416,36 @@
- 查看单个运行绑定详情
+### `GET /home`
+
+用途:
+
+- 返回入口首页摘要
+- 当前卡片摘要字段已统一补齐:
+ - `summary`
+ - `status`
+ - `statusCode`
+ - `timeWindow`
+ - `ctaText`
+ - `isDefaultExperience`
+ - `eventType`
+ - `currentPresentation`
+ - `currentContentBundle`
+
+### `GET /cards`
+
+用途:
+
+- 按入口返回活动卡片列表
+- 当前与 `/home` 使用同一套卡片摘要语义
+
+### `GET /me/entry-home`
+
+鉴权:
+
+- Bearer token
+
+用途:
+
+- 返回“我的首页”聚合
+- 当前 `cards` 也已统一使用活动卡片最小摘要字段
diff --git a/backend/docs/数据模型.md b/backend/docs/数据模型.md
index 6d7b9af..4992944 100644
--- a/backend/docs/数据模型.md
+++ b/backend/docs/数据模型.md
@@ -1,8 +1,8 @@
# 数据模型
-> 文档版本:v1.3
-> 最后更新:2026-04-03 12:36:15
+> 文档版本:v1.4
+> 最后更新:2026-04-03 22:34:08
-当前 migration 共 10 版。
+当前 migration 共 11 版。
## 1. 迁移清单
@@ -16,6 +16,7 @@
- [0008_production_skeleton.sql](D:/dev/cmr-mini/backend/migrations/0008_production_skeleton.sql)
- [0009_event_ops_phase2.sql](D:/dev/cmr-mini/backend/migrations/0009_event_ops_phase2.sql)
- [0010_event_default_bindings.sql](D:/dev/cmr-mini/backend/migrations/0010_event_default_bindings.sql)
+- [0011_card_summary.sql](D:/dev/cmr-mini/backend/migrations/0011_card_summary.sql)
## 2. 表分组
@@ -78,6 +79,16 @@
- 支撑首页卡片
- 运营入口聚合
- tenant/channel 维度展示控制
+- 默认体验活动标记
+
+当前补充字段:
+
+- `cards.is_default_experience`
+
+当前说明:
+
+- 活动卡片列表第一刀先通过卡片显式字段承接“默认体验活动 / 普通活动”区分
+- `timeWindow / ctaText / status` 当前先由 backend 摘要层派生,不再额外新增对象层级
### 2.5 运行态
diff --git a/backend/docs/核心流程.md b/backend/docs/核心流程.md
index cacb2db..f2b171d 100644
--- a/backend/docs/核心流程.md
+++ b/backend/docs/核心流程.md
@@ -1,6 +1,6 @@
# 核心流程
-> 文档版本:v1.2
-> 最后更新:2026-04-03 11:22:50
+> 文档版本:v1.3
+> 最后更新:2026-04-03 18:16:19
## 1. 总流程
@@ -104,6 +104,17 @@ APP 当前主链是手机号验证码:
- 是否有 ongoing session
- 当前推荐动作是什么
+补充规则:
+
+- `play.canLaunch` 不是“有 event 就能进”
+- 它当前表示“当前发布 release 已完整可启动”
+- 最小要求为:
+ - 已发布 release 存在
+ - manifest 存在
+ - runtime 已绑定
+ - presentation 已绑定
+ - content bundle 已绑定
+
当前聚合接口:
- `GET /events/{eventPublicID}/play`
diff --git a/backend/internal/httpapi/handlers/dev_handler.go b/backend/internal/httpapi/handlers/dev_handler.go
index eeedb3b..fd6e842 100644
--- a/backend/internal/httpapi/handlers/dev_handler.go
+++ b/backend/internal/httpapi/handlers/dev_handler.go
@@ -137,6 +137,36 @@ func (h *DevHandler) ManifestSummary(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": summary})
}
+func (h *DevHandler) DemoPresentationSchema(w http.ResponseWriter, r *http.Request) {
+ if !h.devService.Enabled() {
+ http.NotFound(w, r)
+ return
+ }
+
+ key := r.PathValue("demoKey")
+ payload, ok := demoPresentationAssets[key]
+ if !ok {
+ http.NotFound(w, r)
+ return
+ }
+ httpx.WriteJSON(w, http.StatusOK, payload)
+}
+
+func (h *DevHandler) DemoContentManifest(w http.ResponseWriter, r *http.Request) {
+ if !h.devService.Enabled() {
+ http.NotFound(w, r)
+ return
+ }
+
+ key := r.PathValue("demoKey")
+ payload, ok := demoContentAssets[key]
+ if !ok {
+ http.NotFound(w, r)
+ return
+ }
+ httpx.WriteJSON(w, http.StatusOK, payload)
+}
+
func pickString(v any) string {
switch t := v.(type) {
case string:
@@ -160,6 +190,150 @@ func pickNestedString(m map[string]any, parent, child string) string {
return pickString(nested[child])
}
+var demoPresentationAssets = map[string]map[string]any{
+ "classic": {
+ "templateKey": "event.detail.city-run",
+ "sourceType": "schema",
+ "version": "v2026-04-03",
+ "title": "雪熊领秀城区顺序赛展示定义",
+ "event": map[string]any{
+ "title": "雪熊领秀城区顺序赛",
+ "subtitle": "沿河绿道 6 点经典路线",
+ },
+ "card": map[string]any{
+ "heroTitle": "今日推荐路线",
+ "heroSubtitle": "城区步道顺序挑战",
+ "badge": "顺序赛",
+ },
+ "detail": map[string]any{
+ "sections": []map[string]any{
+ {"type": "hero", "title": "顺序打卡", "subtitle": "沿河绿道 6 点路线"},
+ {"type": "summary", "items": []string{"预计时长 35 分钟", "适合首次联调与新手体验", "默认使用标准 6 点线路"}},
+ {"type": "safety", "items": []string{"注意路口减速", "夜间建议结伴测试"}},
+ },
+ },
+ },
+ "score-o": {
+ "templateKey": "event.detail.score-o",
+ "sourceType": "schema",
+ "version": "v2026-04-03",
+ "title": "雪熊领秀城区积分赛展示定义",
+ "event": map[string]any{
+ "title": "雪熊领秀城区积分赛",
+ "subtitle": "20 分钟自由取点积分挑战",
+ },
+ "card": map[string]any{
+ "heroTitle": "自由取点",
+ "heroSubtitle": "在限定时间内尽量拿高分",
+ "badge": "积分赛",
+ },
+ "detail": map[string]any{
+ "sections": []map[string]any{
+ {"type": "hero", "title": "20 分钟自由取点", "subtitle": "控制点分值不同,自由规划路线"},
+ {"type": "summary", "items": []string{"推荐热身后再开局", "适合熟悉地图后做效率测试", "默认接入 score-o 玩法"}},
+ {"type": "result", "items": []string{"展示积分、完成点数、路线效率"}},
+ },
+ },
+ },
+ "manual-variant": {
+ "templateKey": "event.detail.variant-selector",
+ "sourceType": "schema",
+ "version": "v2026-04-03",
+ "title": "雪熊领秀城区多赛道挑战展示定义",
+ "event": map[string]any{
+ "title": "雪熊领秀城区多赛道挑战",
+ "subtitle": "A / B 线手动选择联调活动",
+ },
+ "card": map[string]any{
+ "heroTitle": "多赛道选择",
+ "heroSubtitle": "同一地点,不同路线长度与难度",
+ "badge": "多赛道",
+ },
+ "detail": map[string]any{
+ "sections": []map[string]any{
+ {"type": "hero", "title": "先选赛道再开始", "subtitle": "A 线偏短,B 线偏长"},
+ {"type": "variants", "items": []string{"A 线:短线体验版", "B 线:长线挑战版"}},
+ {"type": "summary", "items": []string{"适合验证 variant 选择与回流链", "默认推荐 B 线做联调"}},
+ },
+ },
+ },
+}
+
+var demoContentAssets = map[string]map[string]any{
+ "classic": {
+ "manifestVersion": "1",
+ "bundleType": "route_content",
+ "version": "v2026-04-03",
+ "title": "雪熊领秀城区顺序赛内容包",
+ "locale": "zh-CN",
+ "event": map[string]any{
+ "title": "雪熊领秀城区顺序赛",
+ "subtitle": "沿河绿道 6 点经典路线",
+ },
+ "hero": map[string]any{
+ "title": "绿道顺序挑战",
+ "subtitle": "按照既定顺序依次完成 6 个控制点",
+ },
+ "sections": []map[string]any{
+ {"type": "intro", "title": "活动说明", "body": "适合首次联调与基础顺序赛流程验证。"},
+ {"type": "tips", "title": "路线提示", "body": "默认路线沿河绿道展开,注意桥下拐点。"},
+ {"type": "result", "title": "结果页文案", "body": "完成后展示用时、配速与打卡完成率。"},
+ },
+ "assets": map[string]any{
+ "cover": "https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg",
+ "entryHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
+ },
+ },
+ "score-o": {
+ "manifestVersion": "1",
+ "bundleType": "result_media",
+ "version": "v2026-04-03",
+ "title": "雪熊领秀城区积分赛内容包",
+ "locale": "zh-CN",
+ "event": map[string]any{
+ "title": "雪熊领秀城区积分赛",
+ "subtitle": "20 分钟自由取点积分挑战",
+ },
+ "hero": map[string]any{
+ "title": "自由规划路线",
+ "subtitle": "在限定时间内尽量争取更高积分",
+ },
+ "sections": []map[string]any{
+ {"type": "intro", "title": "玩法说明", "body": "每个控制点分值不同,优先测试路径规划与效率。"},
+ {"type": "tips", "title": "策略建议", "body": "建议先拿近点,再视剩余时间冲刺高分点。"},
+ {"type": "result", "title": "结果页文案", "body": "结果页重点展示总积分、完成点位与平均速度。"},
+ },
+ "assets": map[string]any{
+ "cover": "https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg",
+ "entryHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
+ },
+ },
+ "manual-variant": {
+ "manifestVersion": "1",
+ "bundleType": "route_content",
+ "version": "v2026-04-03",
+ "title": "雪熊领秀城区多赛道挑战内容包",
+ "locale": "zh-CN",
+ "event": map[string]any{
+ "title": "雪熊领秀城区多赛道挑战",
+ "subtitle": "A / B 线手动选择联调活动",
+ },
+ "hero": map[string]any{
+ "title": "同图多赛道",
+ "subtitle": "先选路线,再验证 launch / result / history 回流",
+ },
+ "sections": []map[string]any{
+ {"type": "intro", "title": "玩法说明", "body": "A 线适合短线体验,B 线适合长线挑战与数据对比。"},
+ {"type": "variants", "title": "赛道差异", "body": "两条赛道使用不同 KML,用于验证 variant 选择与恢复链。"},
+ {"type": "result", "title": "结果页文案", "body": "结果页需展示所选赛道名、routeCode 与成绩摘要。"},
+ },
+ "assets": map[string]any{
+ "cover": "https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg",
+ "entryHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
+ },
+ },
+}
+
const devWorkbenchHTML = `
@@ -319,6 +493,14 @@ const devWorkbenchHTML = `
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
align-items: start;
}
+ .masonry {
+ position: relative;
+ margin-top: 16px;
+ min-height: 0;
+ }
+ .masonry > .panel {
+ margin: 0;
+ }
.stack {
display: grid;
gap: 16px;
@@ -379,6 +561,31 @@ const devWorkbenchHTML = `
color: #062419;
font-weight: 700;
cursor: pointer;
+ transition: transform .14s ease, opacity .14s ease, filter .14s ease, box-shadow .14s ease;
+ }
+ button:hover:not(:disabled) {
+ transform: translateY(-1px);
+ filter: brightness(1.02);
+ }
+ button:disabled {
+ cursor: not-allowed;
+ opacity: 0.72;
+ }
+ button.is-running {
+ position: relative;
+ box-shadow: 0 0 0 1px rgba(79, 209, 165, 0.35), 0 0 0 4px rgba(79, 209, 165, 0.08);
+ }
+ button.is-running::after {
+ content: "";
+ width: 12px;
+ height: 12px;
+ margin-left: 10px;
+ border-radius: 999px;
+ border: 2px solid rgba(6, 36, 25, 0.32);
+ border-top-color: currentColor;
+ display: inline-block;
+ vertical-align: -2px;
+ animation: spin .8s linear infinite;
}
button.secondary {
background: var(--accent-2);
@@ -576,17 +783,97 @@ const devWorkbenchHTML = `
font-weight: 600;
}
.status {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
color: var(--accent);
font-weight: 700;
+ min-height: 24px;
+ }
+ .status::before {
+ content: "";
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ background: currentColor;
+ box-shadow: 0 0 0 0 rgba(79, 209, 165, 0.35);
+ }
+ .status.running::before {
+ animation: pulse 1.1s ease infinite;
}
.status.error {
color: var(--danger);
}
+ .progress-card {
+ display: grid;
+ gap: 8px;
+ padding: 12px;
+ border-radius: 14px;
+ background: rgba(255,255,255,0.03);
+ border: 1px solid rgba(255,255,255,0.05);
+ }
+ .progress-meta {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: center;
+ color: var(--muted);
+ font-size: 12px;
+ }
+ .progress-label {
+ color: var(--text);
+ font-weight: 700;
+ }
+ .progress-track {
+ width: 100%;
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.08);
+ overflow: hidden;
+ }
+ .progress-fill {
+ height: 100%;
+ width: 0%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(79, 209, 165, 0.95), rgba(125, 211, 252, 0.95));
+ transition: width .18s ease;
+ }
+ .progress-note {
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.5;
+ }
+ @keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+ @keyframes pulse {
+ 0% { box-shadow: 0 0 0 0 rgba(79, 209, 165, 0.38); }
+ 70% { box-shadow: 0 0 0 8px rgba(79, 209, 165, 0); }
+ 100% { box-shadow: 0 0 0 0 rgba(79, 209, 165, 0); }
+ }
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.sidebar { position: static; }
.row.two { grid-template-columns: 1fr; }
.shell { padding: 20px 16px 32px; }
+ .masonry {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 16px;
+ height: auto !important;
+ }
+ .masonry > .panel {
+ position: static !important;
+ width: auto !important;
+ left: auto !important;
+ top: auto !important;
+ }
+ }
+ @media (max-width: 640px) {
+ .masonry {
+ grid-template-columns: 1fr;
+ }
}
@@ -643,9 +930,10 @@ const devWorkbenchHTML = `
第一步:选玩法
- 先在这里选玩法入口。顺序赛、积分赛、多赛道各有一套独立 demo 数据,后面一键流程都会复用这里选中的 event。
+ 先在这里准备 demo 数据并选择玩法入口。顺序赛、积分赛、多赛道各有一套独立 demo 数据,后面一键流程都会复用这里选中的 event。
-
+
+
@@ -655,6 +943,7 @@ const devWorkbenchHTML = `
积分赛入口 tenant_demo / mini-demo / evt_demo_score_o_001
多赛道入口 tenant_demo / mini-demo / evt_demo_variant_manual_001
+ 说明:Bootstrap Demo(只准备数据) 只负责把三种玩法的 demo 对象和默认样例准备好;Bootstrap + 发布当前玩法 会先准备 demo,再对当前选中的玩法执行一遍“发布活动配置(自动补 Runtime)”。
@@ -716,7 +1005,9 @@ const devWorkbenchHTML = `
+
+
短信登录 / 绑定
@@ -914,7 +1205,7 @@ const devWorkbenchHTML = `
3. 想只测发布链,就点 发布活动配置(自动补 Runtime)
4. 想只测局内流程,就点 快速进一局、结束并看结果
- 这些流程会复用当前表单里的手机号、设备、event、channel 等输入。发布活动配置(默认绑定)会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。发布活动配置(自动补 Runtime)会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。整条链一键验收会继续执行:play -> launch -> start -> finish -> result -> history。
+ 这些流程会复用当前表单里的手机号、设备、event、channel 等输入。发布活动配置(默认绑定)会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。发布活动配置(自动补 Runtime)会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。整条链一键验收会先准备并发布当前玩法,再继续执行:play -> launch -> start -> finish -> result -> history。
预期结果
@@ -949,6 +1240,18 @@ const devWorkbenchHTML = `
判定 待执行
+
+
当前玩法关键状态
+
+
Event ID -
+
Release ID -
+
Can Launch -
+
Assignment Mode -
+
Variant Count -
+
Game Mode -
+
Playfield Kind -
+
+
@@ -1462,7 +1765,7 @@ const devWorkbenchHTML = `
@@ -1494,18 +1797,18 @@ const devWorkbenchHTML = `
@@ -1516,7 +1819,7 @@ const devWorkbenchHTML = `
@@ -1626,6 +1929,14 @@ const devWorkbenchHTML = `
响应日志
最后一次请求的结果会记录在这里,便于后续做请求回放和用例保存。
ready
+
+
+ 当前进度:待执行
+ 0 / 0
+
+
+
长流程会在这里显示当前步骤。
+
@@ -1875,6 +2186,24 @@ const devWorkbenchHTML = `
+
+
GET/dev/demo-assets/presentations/{demoKey}
+
读取联调用的示例展示定义 schema,给 workbench 快速导入。
+
+
+
+
+
GET/dev/demo-assets/content-manifests/{demoKey}
+
读取联调用的示例内容 manifest,给 workbench 快速导入。
+
+
+
GET/dev/config/local-files
列出本地配置目录中的 JSON 文件,作为 source config 导入入口。
@@ -2279,6 +2608,11 @@ const devWorkbenchHTML = `
const HISTORY_KEY = 'cmr-backend-workbench-history-v1';
const SCENARIO_KEY = 'cmr-backend-workbench-scenarios-v1';
const MODE_KEY = 'cmr-backend-workbench-mode-v1';
+ const FLOW_STEP_PLANS = {
+ 'flow-admin-default-publish': ['get-event', 'import-presentation', 'import-bundle', 'save-defaults', 'build-source', 'publish-build', 'get-release'],
+ 'flow-admin-runtime-publish': ['bootstrap-demo', 'get-event', 'import-presentation', 'import-bundle', 'create-runtime-binding', 'save-defaults', 'build-source', 'publish-build', 'get-release'],
+ 'flow-standard-regression': ['prepare-release', 'login-wechat', 'event-play', 'event-launch', 'session-start', 'session-finish', 'session-result', 'history-check']
+ };
const state = {
accessToken: '',
refreshToken: '',
@@ -2289,12 +2623,25 @@ const devWorkbenchHTML = `
sessionToken: '',
lastCurl: ''
};
+ const currentFlowStatus = {
+ eventId: '-',
+ releaseId: '-',
+ canLaunch: '-',
+ assignmentMode: '-',
+ variantCount: '-',
+ gameMode: '-',
+ playfieldKind: '-'
+ };
const $ = (id) => document.getElementById(id);
const logEl = $('log');
const curlEl = $('curl');
const historyEl = $('history');
const statusEl = $('status');
+ const progressLabelEl = $('progress-label');
+ const progressStepEl = $('progress-step');
+ const progressFillEl = $('progress-fill');
+ const progressNoteEl = $('progress-note');
const modeNodes = Array.from(document.querySelectorAll('[data-modes]'));
const modeButtons = Array.from(document.querySelectorAll('[data-mode-btn]'));
const navLinks = Array.from(document.querySelectorAll('[data-nav-target]'));
@@ -2448,9 +2795,68 @@ const devWorkbenchHTML = `
$('session-id').value = state.sessionId || '';
$('session-token').value = state.sessionToken || '';
curlEl.textContent = state.lastCurl || '-';
+ syncCurrentFlowStatusFromForms();
persistState();
}
+ function renderCurrentFlowStatus() {
+ $('current-flow-event-id').textContent = currentFlowStatus.eventId || '-';
+ $('current-flow-release-id').textContent = currentFlowStatus.releaseId || '-';
+ $('current-flow-can-launch').textContent = currentFlowStatus.canLaunch || '-';
+ $('current-flow-assignment-mode').textContent = currentFlowStatus.assignmentMode || '-';
+ $('current-flow-variant-count').textContent = currentFlowStatus.variantCount || '-';
+ $('current-flow-game-mode').textContent = currentFlowStatus.gameMode || '-';
+ $('current-flow-playfield-kind').textContent = currentFlowStatus.playfieldKind || '-';
+ }
+
+ function syncCurrentFlowStatusFromForms() {
+ currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || '-';
+ currentFlowStatus.releaseId = trimmedOrUndefined($('event-release-id').value) || state.releaseId || '-';
+ renderCurrentFlowStatus();
+ }
+
+ function resetCurrentFlowStatus() {
+ currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || '-';
+ currentFlowStatus.releaseId = trimmedOrUndefined($('event-release-id').value) || state.releaseId || '-';
+ currentFlowStatus.canLaunch = '-';
+ currentFlowStatus.assignmentMode = '-';
+ currentFlowStatus.variantCount = '-';
+ currentFlowStatus.gameMode = '-';
+ currentFlowStatus.playfieldKind = '-';
+ renderCurrentFlowStatus();
+ }
+
+ function setCurrentFlowStatusFromPlayResponse(result) {
+ const payload = result && result.data ? result.data : (result || {});
+ const play = payload.play || {};
+ const resolvedRelease = payload.resolvedRelease || {};
+ const variants = Array.isArray(play.courseVariants) ? play.courseVariants : [];
+ currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || currentFlowStatus.eventId || '-';
+ currentFlowStatus.releaseId = resolvedRelease.releaseId || trimmedOrUndefined($('event-release-id').value) || state.releaseId || '-';
+ currentFlowStatus.canLaunch = typeof play.canLaunch === 'boolean' ? String(play.canLaunch) : '-';
+ currentFlowStatus.assignmentMode = play.assignmentMode || '-';
+ currentFlowStatus.variantCount = variants.length ? String(variants.length) : '0';
+ renderCurrentFlowStatus();
+ }
+
+ function setCurrentFlowStatusFromLaunch(summary, launchData) {
+ const payload = launchData && launchData.launch ? launchData.launch : {};
+ const resolvedRelease = payload.resolvedRelease || {};
+ const variant = payload.variant || {};
+ currentFlowStatus.eventId = trimmedOrUndefined($('event-id').value) || currentFlowStatus.eventId || '-';
+ currentFlowStatus.releaseId = summary && summary.releaseId ? summary.releaseId : (resolvedRelease.releaseId || currentFlowStatus.releaseId || '-');
+ if (summary && summary.gameMode) {
+ currentFlowStatus.gameMode = summary.gameMode;
+ }
+ if (summary && summary.playfieldKind) {
+ currentFlowStatus.playfieldKind = summary.playfieldKind;
+ }
+ if (variant && variant.assignmentMode) {
+ currentFlowStatus.assignmentMode = variant.assignmentMode;
+ }
+ renderCurrentFlowStatus();
+ }
+
function setDefaultPublishExpectation(result) {
const release = result || {};
const releaseId = release.id || '-';
@@ -2507,6 +2913,64 @@ const devWorkbenchHTML = `
$('launch-config-playfield-kind').textContent = '-';
$('launch-config-game-mode').textContent = '-';
$('launch-config-verdict').textContent = '待执行';
+ resetCurrentFlowStatus();
+ }
+
+ function buildDemoPresentationSchemaURL(demoKind) {
+ return window.location.origin + '/dev/demo-assets/presentations/' + encodeURIComponent(demoKind);
+ }
+
+ function buildDemoContentManifestURL(demoKind) {
+ return window.location.origin + '/dev/demo-assets/content-manifests/' + encodeURIComponent(demoKind);
+ }
+
+ function buildDemoContentAssetManifest(demoKind) {
+ return JSON.stringify({
+ manifestUrl: buildDemoContentManifestURL(demoKind),
+ coverUrl: 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
+ entryHtml: 'https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html'
+ }, null, 2);
+ }
+
+ function applyDemoImportInputs(demoKind) {
+ const configs = {
+ classic: {
+ presentationTitle: '领秀城公园顺序赛展示定义',
+ templateKey: 'event.detail.standard',
+ presentationVersion: 'v2026-04-03-classic',
+ contentTitle: '领秀城公园顺序赛内容包',
+ contentVersion: 'v2026-04-03-classic',
+ bundleType: 'result_media'
+ },
+ 'score-o': {
+ presentationTitle: '领秀城公园积分赛展示定义',
+ templateKey: 'event.detail.score-o',
+ presentationVersion: 'v2026-04-03-score-o',
+ contentTitle: '领秀城公园积分赛内容包',
+ contentVersion: 'v2026-04-03-score-o',
+ bundleType: 'result_media'
+ },
+ 'manual-variant': {
+ presentationTitle: '领秀城公园多赛道挑战展示定义',
+ templateKey: 'event.detail.multi-variant',
+ presentationVersion: 'v2026-04-03-manual',
+ contentTitle: '领秀城公园多赛道挑战内容包',
+ contentVersion: 'v2026-04-03-manual',
+ bundleType: 'result_media'
+ }
+ };
+ const config = configs[demoKind] || configs.classic;
+ $('admin-presentation-import-title').value = config.presentationTitle;
+ $('admin-presentation-import-template-key').value = config.templateKey;
+ $('admin-presentation-import-source-type').value = 'schema';
+ $('admin-presentation-import-version').value = config.presentationVersion;
+ $('admin-presentation-import-schema-url').value = buildDemoPresentationSchemaURL(demoKind);
+ $('admin-content-import-title').value = config.contentTitle;
+ $('admin-content-import-bundle-type').value = config.bundleType;
+ $('admin-content-import-source-type').value = 'manifest';
+ $('admin-content-import-version').value = config.contentVersion;
+ $('admin-content-import-manifest-url').value = buildDemoContentManifestURL(demoKind);
+ $('admin-content-import-asset-manifest-json').value = buildDemoContentAssetManifest(demoKind);
}
async function resolveLaunchConfigSummary(launchPayload) {
@@ -2560,10 +3024,62 @@ const devWorkbenchHTML = `
$('launch-config-verdict').textContent = data.verdict || '待执行';
}
+ function scheduleMasonryLayout() {
+ if (window.__cmrMasonryFrame) {
+ cancelAnimationFrame(window.__cmrMasonryFrame);
+ }
+ window.__cmrMasonryFrame = requestAnimationFrame(function() {
+ document.querySelectorAll('.masonry').forEach(function(container) {
+ const panels = Array.from(container.querySelectorAll(':scope > .panel')).filter(function(panel) {
+ return !panel.classList.contains('mode-hidden');
+ });
+ container.style.height = '';
+ panels.forEach(function(panel) {
+ panel.style.position = '';
+ panel.style.width = '';
+ panel.style.left = '';
+ panel.style.top = '';
+ });
+ if (window.innerWidth <= 900 || panels.length <= 1) {
+ return;
+ }
+ const gap = 16;
+ const minWidth = 320;
+ const width = container.clientWidth;
+ if (!width) {
+ return;
+ }
+ const columnCount = Math.max(1, Math.floor((width + gap) / (minWidth + gap)));
+ if (columnCount <= 1) {
+ return;
+ }
+ const columnWidth = Math.floor((width - gap * (columnCount - 1)) / columnCount);
+ const heights = Array(columnCount).fill(0);
+ panels.forEach(function(panel) {
+ let targetColumn = 0;
+ for (let i = 1; i < heights.length; i += 1) {
+ if (heights[i] < heights[targetColumn]) {
+ targetColumn = i;
+ }
+ }
+ panel.style.position = 'absolute';
+ panel.style.width = columnWidth + 'px';
+ panel.style.left = ((columnWidth + gap) * targetColumn) + 'px';
+ panel.style.top = heights[targetColumn] + 'px';
+ heights[targetColumn] += panel.offsetHeight + gap;
+ });
+ container.style.height = Math.max.apply(null, heights.map(function(height) {
+ return Math.max(0, height - gap);
+ })) + 'px';
+ });
+ });
+ }
+
function renderClientLogs(items) {
const logs = Array.isArray(items) ? items : [];
if (!logs.length) {
$('client-logs').textContent = '暂无前端调试日志';
+ scheduleMasonryLayout();
return;
}
$('client-logs').textContent = logs.map(function(item) {
@@ -2587,6 +3103,7 @@ const devWorkbenchHTML = `
}
return lines.join('\n');
}).join('\n\n---\n\n');
+ scheduleMasonryLayout();
}
async function refreshClientLogs() {
@@ -2612,11 +3129,11 @@ const devWorkbenchHTML = `
return {
eventId: data.variantManualEventId,
releaseId: data.variantManualReleaseId || '',
- sourceId: data.sourceId || '',
- buildId: data.buildId || '',
- courseSetId: data.courseSetId || '',
- courseVariantId: data.courseVariantId || '',
- runtimeBindingId: data.runtimeBindingId || ''
+ sourceId: data.variantManualSourceId || '',
+ buildId: data.variantManualBuildId || '',
+ courseSetId: data.variantManualCourseSetId || '',
+ courseVariantId: data.variantManualCourseVariantId || '',
+ runtimeBindingId: data.variantManualRuntimeBindingId || ''
};
}
return {
@@ -2691,21 +3208,22 @@ const devWorkbenchHTML = `
async function runAdminDefaultPublishFlow(options) {
const ensureRuntime = options && options.ensureRuntime === true;
+ const bootstrapDemo = options && options.bootstrapDemo === true;
const flowTitle = ensureRuntime ? 'flow-admin-runtime-publish' : 'flow-admin-default-publish';
const eventId = $('admin-event-ref-id').value || $('event-id').value;
if (!trimmedOrUndefined(eventId)) {
throw new Error('admin event id is required');
}
- if (ensureRuntime) {
- writeLog(flowTitle + '.step', { step: 'bootstrap-demo' });
+ if (bootstrapDemo) {
+ markFlowStep(flowTitle, 'bootstrap-demo');
const bootstrap = await request('POST', '/dev/bootstrap-demo');
if (bootstrap.data) {
applyBootstrapContext(bootstrap.data, eventId);
}
}
- writeLog(flowTitle + '.step', { step: 'get-event', eventId: eventId });
+ markFlowStep(flowTitle, 'get-event', { eventId: eventId });
const eventDetail = await request('GET', '/admin/events/' + encodeURIComponent(eventId), undefined, true);
if (eventDetail.data && eventDetail.data.event) {
$('admin-event-ref-id').value = eventDetail.data.event.id || $('admin-event-ref-id').value;
@@ -2724,7 +3242,7 @@ const devWorkbenchHTML = `
}
}
- writeLog(flowTitle + '.step', { step: 'import-presentation', eventId: eventId });
+ markFlowStep(flowTitle, 'import-presentation', { eventId: eventId });
const importedPresentation = await request('POST', '/admin/events/' + encodeURIComponent(eventId) + '/presentations/import', {
title: $('admin-presentation-import-title').value,
templateKey: $('admin-presentation-import-template-key').value,
@@ -2740,7 +3258,7 @@ const devWorkbenchHTML = `
$('config-presentation-id').value = importedPresentation.data.id || $('config-presentation-id').value;
}
- writeLog(flowTitle + '.step', { step: 'import-bundle', eventId: eventId });
+ markFlowStep(flowTitle, 'import-bundle', { eventId: eventId });
const importedBundle = await request('POST', '/admin/events/' + encodeURIComponent(eventId) + '/content-bundles/import', {
title: $('admin-content-import-title').value,
bundleType: $('admin-content-import-bundle-type').value,
@@ -2778,8 +3296,7 @@ const devWorkbenchHTML = `
throw new Error('创建 runtime binding 前缺少字段: ' + missing.join(', '));
}
- writeLog(flowTitle + '.step', {
- step: 'create-runtime-binding',
+ markFlowStep(flowTitle, 'create-runtime-binding', {
eventId: eventId,
placeId: $('prod-place-id').value,
mapAssetId: $('prod-map-asset-id').value,
@@ -2803,7 +3320,7 @@ const devWorkbenchHTML = `
}
}
- writeLog(flowTitle + '.step', { step: 'save-defaults', eventId: eventId });
+ markFlowStep(flowTitle, 'save-defaults', { eventId: eventId });
const defaults = await request('POST', '/admin/events/' + encodeURIComponent(eventId) + '/defaults', {
presentationId: trimmedOrUndefined($('admin-release-presentation-id').value),
contentBundleId: trimmedOrUndefined($('admin-release-content-bundle-id').value),
@@ -2828,14 +3345,14 @@ const devWorkbenchHTML = `
if (!trimmedOrUndefined(sourceId)) {
throw new Error('no source id available for build');
}
- writeLog(flowTitle + '.step', { step: 'build-source', sourceId: sourceId });
+ markFlowStep(flowTitle, 'build-source', { sourceId: sourceId });
const build = await request('POST', '/admin/sources/' + encodeURIComponent(sourceId) + '/build', undefined, true);
state.sourceId = build.data.sourceId || state.sourceId;
state.buildId = build.data.id || state.buildId;
$('admin-pipeline-build-id').value = build.data.id || $('admin-pipeline-build-id').value;
$('admin-pipeline-source-id').value = build.data.sourceId || $('admin-pipeline-source-id').value;
- writeLog(flowTitle + '.step', { step: 'publish-build', buildId: $('admin-pipeline-build-id').value || state.buildId });
+ markFlowStep(flowTitle, 'publish-build', { buildId: $('admin-pipeline-build-id').value || state.buildId });
const published = await request('POST', '/admin/builds/' + encodeURIComponent($('admin-pipeline-build-id').value || state.buildId) + '/publish', {}, true);
state.releaseId = published.data.release.releaseId || state.releaseId;
$('admin-pipeline-release-id').value = published.data.release.releaseId || $('admin-pipeline-release-id').value;
@@ -2854,7 +3371,7 @@ const devWorkbenchHTML = `
$('config-content-bundle-id').value = published.data.contentBundle.contentBundleId;
}
- writeLog(flowTitle + '.step', { step: 'get-release', releaseId: $('admin-pipeline-release-id').value || state.releaseId });
+ markFlowStep(flowTitle, 'get-release', { releaseId: $('admin-pipeline-release-id').value || state.releaseId });
const releaseDetail = await request('GET', '/admin/releases/' + encodeURIComponent($('admin-pipeline-release-id').value || state.releaseId), undefined, true);
setDefaultPublishExpectation(releaseDetail.data);
writeLog(flowTitle + '.expected', {
@@ -2877,12 +3394,11 @@ const devWorkbenchHTML = `
}
resetStandardRegressionExpectation();
- writeLog(flowTitle + '.step', { step: 'prepare-release', eventId: eventId });
- const releaseDetail = await runAdminDefaultPublishFlow({ ensureRuntime: true });
+ markFlowStep(flowTitle, 'prepare-release', { eventId: eventId });
+ const releaseDetail = await runAdminDefaultPublishFlow({ ensureRuntime: true, bootstrapDemo: true });
const publishPass = $('flow-admin-verdict').textContent.indexOf('通过') === 0;
- writeLog(flowTitle + '.step', {
- step: 'login-wechat',
+ markFlowStep(flowTitle, 'login-wechat', {
code: $('wechat-code').value,
deviceKey: $('wechat-device').value
});
@@ -2894,15 +3410,14 @@ const devWorkbenchHTML = `
state.accessToken = login.data.tokens.accessToken;
state.refreshToken = login.data.tokens.refreshToken;
- writeLog(flowTitle + '.step', {
- step: 'event-play',
+ markFlowStep(flowTitle, 'event-play', {
eventId: eventId
});
const play = await request('GET', '/events/' + encodeURIComponent(eventId) + '/play', undefined, true);
+ setCurrentFlowStatusFromPlayResponse(play);
const playPass = !!(play.data && play.data.play && play.data.resolvedRelease && play.data.resolvedRelease.manifestUrl);
- writeLog(flowTitle + '.step', {
- step: 'event-launch',
+ markFlowStep(flowTitle, 'event-launch', {
eventId: eventId,
releaseId: $('event-release-id').value || state.releaseId,
variantId: trimmedOrUndefined($('event-variant-id').value)
@@ -2926,18 +3441,17 @@ const devWorkbenchHTML = `
);
const launchConfigSummary = await resolveLaunchConfigSummary(launch.data);
setLaunchConfigSummary(launchConfigSummary);
+ setCurrentFlowStatusFromLaunch(launchConfigSummary, launch.data);
writeLog(flowTitle + '.launch-summary', launchConfigSummary);
- writeLog(flowTitle + '.step', {
- step: 'session-start',
+ markFlowStep(flowTitle, 'session-start', {
sessionId: state.sessionId
});
await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/start', {
sessionToken: state.sessionToken
});
- writeLog(flowTitle + '.step', {
- step: 'session-finish',
+ markFlowStep(flowTitle, 'session-finish', {
sessionId: state.sessionId,
status: $('finish-status').value
});
@@ -2947,8 +3461,7 @@ const devWorkbenchHTML = `
summary: buildFinishSummary()
});
- writeLog(flowTitle + '.step', {
- step: 'session-result',
+ markFlowStep(flowTitle, 'session-result', {
sessionId: state.sessionId
});
const sessionResult = await request('GET', '/sessions/' + encodeURIComponent(state.sessionId) + '/result', undefined, true);
@@ -2959,8 +3472,7 @@ const devWorkbenchHTML = `
sessionResult.data.result
);
- writeLog(flowTitle + '.step', {
- step: 'history-check',
+ markFlowStep(flowTitle, 'history-check', {
sessionId: state.sessionId
});
const mySessions = await request('GET', '/me/sessions?limit=10', undefined, true);
@@ -3027,6 +3539,7 @@ const devWorkbenchHTML = `
$('admin-release-runtime-binding-id').value = options.runtimeBindingId || '';
$('admin-pipeline-source-id').value = options.sourceId || '';
$('admin-pipeline-build-id').value = options.buildId || '';
+ applyDemoImportInputs(options.demoKind || 'classic');
state.sourceId = options.sourceId || '';
state.buildId = options.buildId || '';
state.releaseId = options.releaseId || state.releaseId;
@@ -3041,9 +3554,76 @@ const devWorkbenchHTML = `
setStatus(options.statusText);
}
- function setStatus(text, isError = false) {
+ function setStatus(text, isError = false, isRunning = false) {
statusEl.textContent = text;
- statusEl.className = isError ? 'status error' : 'status';
+ statusEl.className = 'status' + (isError ? ' error' : '') + (isRunning ? ' running' : '');
+ }
+
+ function resetProgress() {
+ progressLabelEl.textContent = '当前进度:待执行';
+ progressStepEl.textContent = '0 / 0';
+ progressFillEl.style.width = '0%';
+ progressNoteEl.textContent = '长流程会在这里显示当前步骤。';
+ }
+
+ function updateFlowProgress(flowTitle, stepKey, detailText) {
+ const plan = FLOW_STEP_PLANS[flowTitle];
+ if (!plan || !plan.length) {
+ return;
+ }
+ const index = stepKey ? plan.indexOf(stepKey) : -1;
+ const current = index >= 0 ? index + 1 : 0;
+ const total = plan.length;
+ const percent = current > 0 ? Math.max(8, Math.round(current / total * 100)) : 0;
+ progressLabelEl.textContent = '当前进度:' + flowTitle;
+ progressStepEl.textContent = current + ' / ' + total;
+ progressFillEl.style.width = percent + '%';
+ progressNoteEl.textContent = detailText || (stepKey ? ('正在执行:' + stepKey) : '长流程执行中');
+ }
+
+ function completeFlowProgress(flowTitle, success, detailText) {
+ const plan = FLOW_STEP_PLANS[flowTitle];
+ if (!plan || !plan.length) {
+ return;
+ }
+ progressLabelEl.textContent = '当前进度:' + flowTitle;
+ progressStepEl.textContent = plan.length + ' / ' + plan.length;
+ progressFillEl.style.width = success ? '100%' : progressFillEl.style.width;
+ progressNoteEl.textContent = detailText || (success ? '长流程已完成。' : '长流程执行失败。');
+ }
+
+ function markFlowStep(flowTitle, stepKey, payload) {
+ const detail = payload && payload.eventId
+ ? (stepKey + ' · ' + payload.eventId)
+ : (payload && payload.sessionId
+ ? (stepKey + ' · ' + payload.sessionId)
+ : ('正在执行:' + stepKey));
+ updateFlowProgress(flowTitle, stepKey, detail);
+ writeLog(flowTitle + '.step', Object.assign({ step: stepKey }, payload || {}));
+ }
+
+ function setButtonRunning(button, running, label) {
+ if (!button) {
+ return;
+ }
+ if (!button.dataset.originalLabel) {
+ button.dataset.originalLabel = button.innerHTML;
+ }
+ button.disabled = !!running;
+ button.classList.toggle('is-running', !!running);
+ if (running) {
+ button.innerHTML = label || '执行中';
+ } else if (button.dataset.originalLabel) {
+ button.innerHTML = button.dataset.originalLabel;
+ }
+ }
+
+ function getActiveTriggerButton() {
+ const active = document.activeElement;
+ if (active && active.tagName === 'BUTTON') {
+ return active;
+ }
+ return null;
}
function getWorkbenchMode() {
@@ -3060,6 +3640,7 @@ const devWorkbenchHTML = `
modeButtons.forEach(function(button) {
button.classList.toggle('active', button.dataset.modeBtn === mode);
});
+ scheduleMasonryLayout();
}
function normalizeLogPayload(payload) {
@@ -3475,6 +4056,7 @@ const devWorkbenchHTML = `
historyEl.innerHTML = '';
if (!history.length) {
historyEl.innerHTML = '
No requests yet.
';
+ scheduleMasonryLayout();
return;
}
history.forEach(function(item) {
@@ -3487,6 +4069,7 @@ const devWorkbenchHTML = `
'url=' + item.url;
historyEl.appendChild(node);
});
+ scheduleMasonryLayout();
}
function applyAPIFilter() {
@@ -3709,7 +4292,17 @@ const devWorkbenchHTML = `
}
async function run(title, fn) {
- setStatus('running: ' + title);
+ const triggerButton = getActiveTriggerButton();
+ setButtonRunning(triggerButton, true, '执行中');
+ setStatus('running: ' + title, false, true);
+ if (FLOW_STEP_PLANS[title]) {
+ resetProgress();
+ updateFlowProgress(title, null, '长流程已开始,等待第一步...');
+ }
+ writeLog(title + '.start', {
+ startedAt: new Date().toLocaleString(),
+ note: 'request accepted and running'
+ });
try {
const result = await fn();
setStatus('ok: ' + title);
@@ -3723,6 +4316,9 @@ const devWorkbenchHTML = `
syncState();
} catch (err) {
setStatus('error: ' + title + ' -> ' + (err && err.message ? err.message : 'unknown error'), true);
+ if (FLOW_STEP_PLANS[title]) {
+ completeFlowProgress(title, false, '失败:' + (err && err.message ? err.message : 'unknown error'));
+ }
writeLog(title, {
error: err,
lastCurl: state.lastCurl || null
@@ -3733,6 +4329,12 @@ const devWorkbenchHTML = `
status: 'error',
url: state.lastCurl
});
+ } finally {
+ if (FLOW_STEP_PLANS[title] && statusEl.className.indexOf('error') < 0) {
+ completeFlowProgress(title, true, '长流程已完成。');
+ }
+ setButtonRunning(triggerButton, false);
+ scheduleMasonryLayout();
}
}
@@ -3746,6 +4348,7 @@ const devWorkbenchHTML = `
state.sessionToken = '';
state.lastCurl = '';
syncState();
+ resetProgress();
writeLog('clear-state', { ok: true });
setStatus('ready');
};
@@ -3895,9 +4498,11 @@ const devWorkbenchHTML = `
request('GET', '/events/' + encodeURIComponent($('event-id').value))
);
- $('btn-event-play').onclick = () => run('event-play', () =>
- request('GET', '/events/' + encodeURIComponent($('event-id').value) + '/play', undefined, true)
- );
+ $('btn-event-play').onclick = () => run('event-play', async () => {
+ const result = await request('GET', '/events/' + encodeURIComponent($('event-id').value) + '/play', undefined, true);
+ setCurrentFlowStatusFromPlayResponse(result);
+ return result;
+ });
$('btn-launch').onclick = () => run('event-launch', async () => {
const result = await request('POST', '/events/' + encodeURIComponent($('event-id').value) + '/launch', {
@@ -3911,6 +4516,7 @@ const devWorkbenchHTML = `
syncState();
const configSummary = await resolveLaunchConfigSummary(result.data);
setLaunchConfigSummary(configSummary);
+ setCurrentFlowStatusFromLaunch(configSummary, result.data);
writeLog('event-launch.summary', configSummary);
return result;
});
@@ -4685,6 +5291,10 @@ const devWorkbenchHTML = `
return result;
});
+ $('btn-bootstrap-publish').onclick = () => run('bootstrap-publish-current', async () => {
+ return await runAdminDefaultPublishFlow({ ensureRuntime: true, bootstrapDemo: true });
+ });
+
$('btn-use-classic-demo').onclick = () => run('use-classic-demo', async () => {
const result = await request('POST', '/dev/bootstrap-demo');
applyFrontendDemoSelection({
@@ -4692,6 +5302,7 @@ const devWorkbenchHTML = `
releaseId: result.data.releaseId || 'rel_demo_001',
localConfigFile: 'classic-sequential.json',
gameModeCode: 'classic-sequential',
+ demoKind: 'classic',
sourceId: result.data.sourceId || '',
buildId: result.data.buildId || '',
courseSetId: result.data.courseSetId || '',
@@ -4710,6 +5321,7 @@ const devWorkbenchHTML = `
releaseId: result.data.scoreOReleaseId || 'rel_demo_score_o_001',
localConfigFile: 'score-o.json',
gameModeCode: 'score-o',
+ demoKind: 'score-o',
sourceId: result.data.scoreOSourceId || '',
buildId: result.data.scoreOBuildId || '',
courseSetId: result.data.scoreOCourseSetId || '',
@@ -4729,11 +5341,12 @@ const devWorkbenchHTML = `
variantId: 'variant_b',
localConfigFile: 'classic-sequential.json',
gameModeCode: 'classic-sequential',
- sourceId: result.data.sourceId || '',
- buildId: result.data.buildId || '',
- courseSetId: result.data.courseSetId || '',
- courseVariantId: result.data.courseVariantId || '',
- runtimeBindingId: '',
+ demoKind: 'manual-variant',
+ sourceId: result.data.variantManualSourceId || '',
+ buildId: result.data.variantManualBuildId || '',
+ courseSetId: result.data.variantManualCourseSetId || '',
+ courseVariantId: result.data.variantManualCourseVariantId || '',
+ runtimeBindingId: result.data.variantManualRuntimeBindingId || '',
logTitle: 'variant-manual-demo-ready',
statusText: 'ok: manual variant demo loaded'
});
@@ -4795,11 +5408,11 @@ const devWorkbenchHTML = `
});
$('btn-flow-admin-default-publish').onclick = () => run('flow-admin-default-publish', async () => {
- return await runAdminDefaultPublishFlow({ ensureRuntime: false });
+ return await runAdminDefaultPublishFlow({ ensureRuntime: false, bootstrapDemo: false });
});
$('btn-flow-admin-runtime-publish').onclick = () => run('flow-admin-runtime-publish', async () => {
- return await runAdminDefaultPublishFlow({ ensureRuntime: true });
+ return await runAdminDefaultPublishFlow({ ensureRuntime: true, bootstrapDemo: false });
});
$('btn-flow-standard-regression').onclick = () => run('flow-standard-regression', async () => {
@@ -4842,21 +5455,38 @@ const devWorkbenchHTML = `
'admin-pipeline-build-id', 'admin-pipeline-release-id', 'admin-release-runtime-binding-id',
'admin-release-presentation-id', 'admin-release-content-bundle-id', 'admin-rollback-release-id'
].forEach(function(id) {
- $(id).addEventListener('change', persistState);
- $(id).addEventListener('input', persistState);
+ $(id).addEventListener('change', function() {
+ persistState();
+ syncCurrentFlowStatusFromForms();
+ });
+ $(id).addEventListener('input', function() {
+ persistState();
+ syncCurrentFlowStatusFromForms();
+ });
});
$('api-filter').addEventListener('input', applyAPIFilter);
restoreState();
+ if (
+ !$('admin-presentation-import-schema-url').value ||
+ $('admin-presentation-import-schema-url').value.indexOf('example.com') >= 0 ||
+ !$('admin-content-import-manifest-url').value ||
+ $('admin-content-import-manifest-url').value.indexOf('example.com') >= 0
+ ) {
+ applyDemoImportInputs('classic');
+ }
syncWorkbenchMode();
syncState();
renderHistory();
renderScenarioOptions();
applyAPIFilter();
syncAPICounts();
+ resetProgress();
renderClientLogs([]);
writeLog('workbench-ready', { ok: true, hint: 'Use Bootstrap Demo first on a fresh database.' });
+ window.addEventListener('resize', scheduleMasonryLayout);
+ scheduleMasonryLayout();