chore: sync storyforge handoff state
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-05-02 17:50:21 +08:00
parent 6f0d944a75
commit 65db3cd336
20 changed files with 3780 additions and 250 deletions

View File

@@ -0,0 +1,125 @@
# StoryForge Next Thread Handoff - 2026-05-02
## Gitea
- Repository: https://git.hyzq.site/krisolo/storyforge
- Current branch: `codex/storyforge-live-orchestrator-sync-20260323`
- Public workbench: https://storyforge.hyzq.net/
- Public health endpoint: https://storyforge.hyzq.net/healthz
## Project Goal
StoryForge is being shaped into a multi-platform new-media operating workbench: project-first workspace, benchmark discovery, creator-center account analysis, production queue, live recording, AI video generation, review, and a OneLiner main Agent layer that can route unfinished flows into platform Agents.
## Current Progress
- The public web workbench is deployed at `storyforge.hyzq.net` and can auto-login with the configured web auto-session.
- The UI has been returned to the preferred current design direction and refined for mobile/workbench use. The dashboard keeps the `1 main + 2 secondary` action model.
- OneLiner now opens immediately. Context hydration happens inside the OneLiner panel instead of leaving the global header stuck on `正在打开 OneLiner`.
- Discovery/creator-center flows now support Douyin and Kuaishou style creator-center sync, account analysis, top-video analysis, similar-account state isolation, and selected-account cache cleanup.
- Production Center exposes intake entry points for creator-center sync, import homepage/video/text, upload video, AI video, real-cut, and live-recorder maintenance.
- Admin Model Access centralizes language model, ASR, image, image-to-image, video, Huobao, Seedance, and runtime integration configuration behind super-admin access.
- Seedance 2.0 is routed through Huobao/Volcengine style video config. AI video creation preflights Huobao video config before dispatch.
- Public deployment scripts and fnOS/NAS deployment scripts are present for web, collector, live-recorder, cutvideo tunnel, n8n, Huobao, and CLI proxy.
## Architecture Snapshot
- Frontend: static vanilla JS app under `web/storyforge-web-v4`, with runtime config, API client, session store, platform runtime, and large workbench renderer in `assets/app.js`.
- Backend: FastAPI collector under `collector-service/app`, with `core_main.py` as the main app surface and feature modules for Douyin, domestic platforms, OneLiner, integrations, and database access.
- Data: server-side SQLite under `/home/ubuntu/storyforge/data/collector/storyforge.db` in production.
- Public server: `https://storyforge.hyzq.net` proxies the static web and collector API.
- fnOS/NAS: local storage and optional service workloads live under `/vol1/docker/hyzq-stack/...` on the fnOS host.
- Windows ASR target: intended Windows host is `192.168.31.18`, using faster-whisper with GPU-capable auto mode and mixed Chinese/English recognition.
## Current Public Runtime Status
Fresh checks on 2026-05-02:
- `GET https://storyforge.hyzq.net/healthz`: OK.
- `POST https://storyforge.hyzq.net/v2/auth/auto-session`: OK, returns the `kris` super-admin session.
- `cutvideo`: configured and reachable at the server-local route.
- `n8n`: configured and reachable at the server-local route.
- `Huobao`: configured and reachable, but video config count is `0`; Seedance/AI video still needs an enabled Huobao video config.
- `local_model`: intentionally not configured because the project decision is to use public/cloud models rather than local models.
- `ASR`: configured as Windows deployment, but public collector currently reports `Connection refused` on `http://127.0.0.1:28088/health`.
- `live_recorder`: configured as NAS deployment, but public collector currently reports connection reset on `http://127.0.0.1:19106/api/healthz`.
## Important Files For The Next Thread
- `web/storyforge-web-v4/assets/app.js`: primary workbench UI, OneLiner runtime, admin model config, discovery, production, and mobile interaction logic.
- `web/storyforge-web-v4/assets/storyforge-platform-runtime.js`: platform route contract for Douyin/Kuaishou/Xiaohongshu/Bilibili/Video Account style workbenches.
- `web/storyforge-web-v4/tests/workbench-pages.test.mjs`: frontend contract tests; most UI workflow guarantees live here.
- `collector-service/app/core_main.py`: collector API, auth, integrations, runtime config, live recorder proxy, Huobao model access, AI video job creation.
- `collector-service/app/domestic_platform_features.py`: domestic-platform creator-center sync, analysis, relations, video persistence, and top-video followups.
- `collector-service/app/douyin_features.py`: Douyin-specific account and public fetch behavior.
- `collector-service/app/oneliner_features.py`: OneLiner main Agent, governance, run lifecycle, execution cards, and platform Agent routing.
- `tests/test_platform_contracts.py`: backend route contracts for platform sync/analysis flows.
- `tests/test_production_baseline.py`: production, model access, AI video, and integration baseline tests.
- `docs/superpowers/specs/*` and `docs/superpowers/plans/*`: design and implementation plans used during this build phase.
- `docs/FNOS_LAN_DELIVERY_RUNBOOK_2026-03-27.md`: fnOS/NAS deployment guide.
- `docs/WINDOWS_CUTVIDEO_OPERATIONS_2026-03-27.md`: Windows cutvideo operating notes.
- `deploy/STORYFORGE_PUBLIC_GATEWAY.md`: public gateway deployment notes.
## Recent Change Highlights
- OneLiner opening behavior:
- Added `onelinerHydrating` and `onelinerHydrationMessage`.
- `open-oneliner` opens the panel first, renders immediately, then hydrates control surfaces and messages.
- Loading text is panel-local (`正在同步 OneLiner 上下文...`) and clears after hydration.
- Creator-center and benchmark discovery:
- Kuaishou/Douyin creator-center sync can persist snapshots and creator works into video sources.
- Account analysis carries model profile, linked-account, recent-similar, creator-center, and top-video context.
- Similar-account search results are isolated by selected account to avoid stale/cross-account state.
- AI video and Seedance:
- AI video form exposes provider/model controls and points admins to Huobao video config.
- Backend validates that Huobao has active video config before AI video dispatch.
- Seedance 2.0 uses the Huobao/Volcengine config path, not a local model path.
- Runtime governance and admin config:
- Admin Model Access covers runtime config, system model config, Huobao AI config, quota, policy, and integration status.
- Local model is left blank by design; public/cloud model configuration is the intended path.
- Deployment:
- Added fnOS compose/deploy scripts for CLI proxy, Huobao, and n8n.
- LAN stack deployment now includes cutvideo tunnel, live recorder, CLI proxy, n8n, Huobao, collector, web, and smoke checks.
## Verification Commands
Run from repository root:
```bash
node --test web/storyforge-web-v4/tests/workbench-pages.test.mjs
python3 -m unittest tests.test_platform_contracts
python3 -m unittest tests.test_production_baseline
curl -fsS https://storyforge.hyzq.net/healthz
```
Useful public deploy commands:
```bash
STORYFORGE_PUBLIC_SYNC_COLLECTOR=0 ./scripts/deploy_public_storyforge.sh
STORYFORGE_PUBLIC_SYNC_COLLECTOR=1 ./scripts/deploy_public_storyforge.sh
```
Useful fnOS/NAS deploy commands:
```bash
SKIP_SMOKE=1 ./scripts/deploy_fnos_storyforge_lan_stack.sh
./scripts/deploy_fnos_storyforge_cliproxy.sh
./scripts/deploy_fnos_storyforge_n8n.sh
./scripts/deploy_fnos_storyforge_huobao.sh
```
## Known Follow-Up Work
- Restore ASR reachability from the public collector to the Windows ASR host. The intended host is `192.168.31.18`; check whether the server-side runtime config should point at the relay/tunnel URL rather than `127.0.0.1:28088`.
- Restore live-recorder health from the public collector to the NAS service. The current public probe reports connection reset.
- Configure at least one active Huobao video model config for Seedance 2.0 before expecting AI video jobs to dispatch successfully.
- The public deploy smoke can fail if ASR/live-recorder are offline even when the web and collector deploy succeeded; check the individual health results before assuming the deploy itself failed.
- Keep secrets out of Git: API keys, cookies, creator-center login cookies, and Gitea credentials must stay in runtime config, Keychain, or server-side storage.
## Handoff Recommendation
For the next thread, start by pulling this branch from Gitea, reading this document, then running the verification commands above. After that, focus first on the three runtime gaps: ASR, live-recorder, and Huobao Seedance video config. Once those are green, test the real creator-center account flow and AI video creation from the public site.

View File

@@ -0,0 +1,692 @@
# Homepage Workbench Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rebuild the StoryForge homepage into the approved human-first `v6` structure while preserving the current visual language, reducing text density, surfacing `1 主 2 次` actions first, and moving system governance entry points into an explicit admin workbench flow.
**Architecture:** Keep the existing static-script frontend architecture, but pull homepage-specific rendering into a dedicated browser module so the dashboard layout can be tested without dragging the entire `app.js` file into every change. The existing `renderDashboardScreen()` function becomes an orchestrator: it gathers runtime data, delegates HTML generation to a dedicated homepage renderer, and wires click handlers through the existing global action system and quick-action modal.
**Tech Stack:** Vanilla browser JS (IIFE modules on `window`), HTML string rendering, CSS in `assets/styles.css`, Python baseline tests, Node built-in test runner for homepage markup contracts.
---
### Task 1: Extract Homepage Rendering Into a Dedicated Module
**Files:**
- Create: `web/storyforge-web-v4/assets/storyforge-dashboard-home.js`
- Create: `web/storyforge-web-v4/tests/dashboard-home.test.mjs`
- Modify: `web/storyforge-web-v4/index.html`
- Modify: `web/storyforge-web-v4/assets/app.js`
- [ ] **Step 1: Write the failing homepage renderer test**
Create `web/storyforge-web-v4/tests/dashboard-home.test.mjs`:
```js
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
const ROOT = path.resolve(process.cwd(), "web/storyforge-web-v4");
function loadHomepageModule() {
const source = fs.readFileSync(path.join(ROOT, "assets/storyforge-dashboard-home.js"), "utf8");
const context = {
window: {},
console,
escapeHtml: (value) => String(value ?? ""),
formatNumber: (value) => String(value ?? 0),
safeArray: (value) => Array.isArray(value) ? value : [],
button: (label, action, tone = "secondary") =>
`<button class="btn btn-${tone}" data-action="${action}">${label}</button>`
};
vm.createContext(context);
vm.runInContext(source, context);
return context.window.StoryForgeDashboardHome;
}
test("homepage v6 puts actions before overview and uses 1-primary-2-secondary structure", () => {
const mod = loadHomepageModule();
const html = mod.renderDashboardHome({
title: "项目总台",
workspaceLabel: "Kris",
currentProjectName: "品牌增长实验室",
summaryTabs: [
{ key: "project_progress", label: "项目进度", value: "3 / 5", hint: "2 项可继续推进", active: true },
{ key: "focus_accounts", label: "重点账号 / 对标", value: "2 个", hint: "1 个缺高分分析", active: false },
{ key: "production_jobs", label: "生产任务", value: "4 条", hint: "1 条待确认", active: false }
],
primaryAction: {
title: "先补抖音重点对标的高分作品分析",
reason: "最近有新作品,但还没形成高分样本。",
badges: ["最优先", "预计 10 分钟判断", "关联:重点账号"]
},
secondaryActions: [
{ title: "确认一个待执行的生产计划", reason: "素材和结论都在,只差最后确认。" },
{ title: "更新重点账号的跟踪摘要", reason: "有新动态,但不值得占据大块首页空间。" }
],
overviewDetail: {
title: "当前阶段",
body: "这里只展示当前 tab 的核心状态。"
}
});
assert.ok(html.includes("今天先做什么"));
assert.ok(html.includes("项目概览"));
assert.ok(html.indexOf("今天先做什么") < html.indexOf("项目概览"));
assert.match(html, /先补抖音重点对标的高分作品分析/);
assert.match(html, /确认一个待执行的生产计划/);
assert.match(html, /更新重点账号的跟踪摘要/);
});
```
- [ ] **Step 2: Run the new test and verify it fails**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
```
Expected: FAIL with `ENOENT` for `storyforge-dashboard-home.js`.
- [ ] **Step 3: Create the dedicated homepage renderer module**
Create `web/storyforge-web-v4/assets/storyforge-dashboard-home.js`:
```js
(function () {
function defaultEscapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function renderTags(items, escapeHtml) {
return (items || []).map((item) => `<span class="tag">${escapeHtml(item)}</span>`).join("");
}
function renderSecondaryAction(item, index, escapeHtml) {
return `
<div class="dashboard-action-secondary">
<div class="dashboard-action-index">${index + 2}</div>
<div>
<h5>${escapeHtml(item.title)}</h5>
<p>${escapeHtml(item.reason)}</p>
</div>
<div class="dashboard-action-buttons">
<button class="btn btn-ghost" data-action="${escapeHtml(item.reasonAction || "open-action-reason")}">原因</button>
<button class="btn btn-secondary" data-action="${escapeHtml(item.goAction || "goto-production")}">${escapeHtml(item.goLabel || "去处理")}</button>
</div>
</div>
`;
}
function renderDashboardHome(model, helpers = {}) {
const escapeHtml = helpers.escapeHtml || defaultEscapeHtml;
return `
<div class="dashboard-home">
<div class="dashboard-context-row">
<div class="dashboard-context-left">
<div class="dashboard-context-chip">
<strong>当前工作区</strong><span>${escapeHtml(model.workspaceLabel)}</span>
</div>
<button class="dashboard-context-chip" data-action="open-dashboard-project-switcher">
<strong>当前项目</strong><span>${escapeHtml(model.currentProjectName)}</span>
</button>
</div>
<div class="dashboard-context-right">
${model.contextLinks.map((item) => `
<button class="dashboard-context-chip" data-action="${escapeHtml(item.action)}">
<span>${escapeHtml(item.label)}</span><strong>${escapeHtml(item.value)}</strong>
</button>
`).join("")}
</div>
</div>
<div class="panel pad dashboard-priority-panel">
<div class="panel-head">
<div>
<h3>今天先做什么</h3>
<div class="panel-subtitle">先做决定,再看细节。</div>
</div>
<span class="tag blue">${escapeHtml(model.actionSourceLabel)}</span>
</div>
<div class="dashboard-action-primary">
<div>
<h4>${escapeHtml(model.primaryAction.title)}</h4>
<p>${escapeHtml(model.primaryAction.reason)}</p>
<div class="task-meta">${renderTags(model.primaryAction.badges, escapeHtml)}</div>
</div>
<div class="dashboard-action-buttons">
<button class="btn btn-ghost" data-action="open-action-reason">查看原因</button>
<button class="btn btn-secondary" data-action="${escapeHtml(model.primaryAction.goAction)}">${escapeHtml(model.primaryAction.goLabel)}</button>
<button class="btn btn-primary" data-action="open-oneliner">${escapeHtml(model.primaryAction.agentLabel)}</button>
</div>
</div>
<div class="dashboard-action-secondary-list">
${model.secondaryActions.map((item, index) => renderSecondaryAction(item, index, escapeHtml)).join("")}
</div>
</div>
<div class="panel pad dashboard-overview-panel">
<div class="panel-head">
<div>
<h3>项目概览</h3>
<div class="panel-subtitle">按需展开,不抢首页第一优先级。</div>
</div>
<span class="tag">${escapeHtml(model.activeTabLabel)}</span>
</div>
<div class="dashboard-overview-tabs">
${model.summaryTabs.map((item) => `
<button class="dashboard-overview-tab ${item.active ? "is-active" : ""}" data-action="select-dashboard-tab" data-dashboard-tab="${escapeHtml(item.key)}">
<small>${escapeHtml(item.label)}</small>
<strong>${escapeHtml(item.value)}</strong>
<span>${escapeHtml(item.hint)}</span>
</button>
`).join("")}
</div>
<div class="dashboard-overview-body">${model.overviewBodyHtml}</div>
</div>
</div>
`;
}
window.StoryForgeDashboardHome = {
renderDashboardHome
};
})();
```
- [ ] **Step 4: Wire the new module into the page**
Modify `web/storyforge-web-v4/index.html`:
```html
<script src="./assets/storyforge-dashboard-home.js"></script>
<script src="./assets/app.js"></script>
```
Modify `web/storyforge-web-v4/assets/app.js` near `renderDashboardScreen()`:
```js
const dashboardHomeRenderer = window.StoryForgeDashboardHome;
function renderDashboardScreen() {
// existing auth/loading guards stay in place
const homeModel = buildDashboardHomeModel();
return screenShell(
"项目总台",
"先做最能推进当前项目的事。",
`${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
dashboardHomeRenderer.renderDashboardHome(homeModel, { escapeHtml })
);
}
```
- [ ] **Step 5: Re-run the renderer test and syntax checks**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
node --check web/storyforge-web-v4/assets/storyforge-dashboard-home.js
node --check web/storyforge-web-v4/assets/app.js
```
Expected: all PASS with the Node test showing `ok 1`.
- [ ] **Step 6: Commit the extraction**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
git add web/storyforge-web-v4/assets/storyforge-dashboard-home.js web/storyforge-web-v4/tests/dashboard-home.test.mjs web/storyforge-web-v4/index.html web/storyforge-web-v4/assets/app.js
git commit -m "feat: extract homepage dashboard renderer"
```
### Task 2: Implement Human-First Dashboard Data Model and 1-Primary-2-Secondary Actions
**Files:**
- Modify: `web/storyforge-web-v4/assets/storyforge-dashboard-home.js`
- Modify: `web/storyforge-web-v4/assets/app.js`
- Modify: `web/storyforge-web-v4/tests/dashboard-home.test.mjs`
- [ ] **Step 1: Add failing tests for homepage model generation**
Append to `web/storyforge-web-v4/tests/dashboard-home.test.mjs`:
```js
test("homepage model builds one primary action, two secondary actions, and a rule fallback label", () => {
const mod = loadHomepageModule();
assert.equal(typeof mod.createDashboardHomeModel, "function");
const model = mod.createDashboardHomeModel({
workspaceLabel: "Kris",
currentProjectName: "品牌增长实验室",
trackedAccountsCount: 2,
assistantCount: 1,
jobCount: 4,
actionSourceLabel: "规则推荐",
dashboardOverviewTab: "project_progress"
});
assert.equal(model.actionSourceLabel, "规则推荐");
assert.equal(model.secondaryActions.length, 2);
assert.match(model.primaryAction.title, /高分作品分析|继续补高分对标/);
});
```
- [ ] **Step 2: Run the targeted Node tests**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
```
Expected: FAIL because the renderer does not yet expose the full `contextLinks` / `actionSourceLabel` model consistently.
- [ ] **Step 3: Add a reusable homepage model builder in `storyforge-dashboard-home.js`**
Modify `web/storyforge-web-v4/assets/storyforge-dashboard-home.js`:
```js
function createDashboardHomeModel(raw) {
const trackedAccountsCount = Number(raw.trackedAccountsCount || 0);
const assistantCount = Number(raw.assistantCount || 0);
const jobCount = Number(raw.jobCount || 0);
const actions = [];
if (trackedAccountsCount > 0) {
actions.push({
title: "先补抖音重点对标的高分作品分析",
reason: "最近有新作品,但还没形成高分样本。",
badges: ["最优先", "预计 10 分钟判断", "关联:重点账号"],
goAction: "goto-discovery",
goLabel: "去找对标",
agentLabel: "交给主 Agent"
});
}
if (jobCount > 0) {
actions.push({
title: "确认一个待执行的生产计划",
reason: "素材和结论都在,只差最后确认。",
goAction: "goto-production",
goLabel: "去处理"
});
}
actions.push({
title: "更新重点账号的跟踪摘要",
reason: "有新动态,但不值得占据大块首页空间。",
goAction: "goto-tracking",
goLabel: "去处理"
});
while (actions.length < 3) {
actions.push({
title: "继续补高分对标并安排生产",
reason: "当前项目没有更多高优先动作时,保持主流程推进。",
goAction: "goto-production",
goLabel: "去处理"
});
}
return {
workspaceLabel: raw.workspaceLabel,
currentProjectName: raw.currentProjectName,
actionSourceLabel: raw.actionSourceLabel,
contextLinks: [
{ label: "账号", value: String(trackedAccountsCount), action: "goto-owned" },
{ label: "任务", value: String(jobCount), action: "goto-production" },
{ label: "Agent", value: String(assistantCount), action: "goto-playbook" }
],
primaryAction: actions[0],
secondaryActions: actions.slice(1, 3)
};
}
window.StoryForgeDashboardHome = {
createDashboardHomeModel,
renderDashboardHome
};
```
- [ ] **Step 4: Add dashboard-specific state and wire the model builder from `app.js`**
Modify `web/storyforge-web-v4/assets/app.js` state setup:
```js
const appState = {
// existing fields...
dashboardOverviewTab: "project_progress",
dashboardActionReason: null
};
```
Build the raw dashboard inputs in `web/storyforge-web-v4/assets/app.js`:
```js
function getDashboardActionSourceLabel() {
return appState.onelinerProfile ? "主 Agent 优先推荐" : "规则推荐";
}
function buildDashboardHomeModel() {
const project = getSelectedProject();
const stats = project ? getProjectStats(project.id) : { assistants: [], jobs: [], sources: [], knowledgeBases: [] };
const trackedAccounts = getTrackingAccounts();
const baseModel = window.StoryForgeDashboardHome.createDashboardHomeModel({
workspaceLabel: appState.me?.display_name || appState.me?.username || "当前工作区",
currentProjectName: project?.name || "还没有项目",
trackedAccountsCount: trackedAccounts.length || appState.accounts.length,
assistantCount: stats.assistants.length,
jobCount: stats.jobs.length,
actionSourceLabel: getDashboardActionSourceLabel(),
dashboardOverviewTab: appState.dashboardOverviewTab
});
return {
...baseModel,
summaryTabs: buildDashboardOverviewTabs(project, stats),
activeTabLabel: dashboardTabLabel(appState.dashboardOverviewTab),
overviewBodyHtml: renderDashboardOverviewBody(appState.dashboardOverviewTab, { project, stats, trackedAccounts })
};
}
```
- [ ] **Step 5: Re-run tests and syntax checks**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
node --check web/storyforge-web-v4/assets/storyforge-dashboard-home.js
node --check web/storyforge-web-v4/assets/app.js
```
Expected: PASS with no missing-field errors.
- [ ] **Step 6: Commit the action hierarchy work**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
git add web/storyforge-web-v4/assets/storyforge-dashboard-home.js web/storyforge-web-v4/assets/app.js web/storyforge-web-v4/tests/dashboard-home.test.mjs
git commit -m "feat: redesign dashboard actions for human-first flow"
```
### Task 3: Implement Overview Tabs, Project Switcher, and Admin Workbench Entry
**Files:**
- Modify: `web/storyforge-web-v4/index.html`
- Modify: `web/storyforge-web-v4/assets/app.js`
- Modify: `web/storyforge-web-v4/assets/storyforge-dashboard-home.js`
- Modify: `web/storyforge-web-v4/tests/dashboard-home.test.mjs`
- [ ] **Step 1: Add failing tests for overview tab buttons and admin entry**
Append to `web/storyforge-web-v4/tests/dashboard-home.test.mjs`:
```js
test("homepage overview uses tab buttons and does not render legacy repeated sections", () => {
const mod = loadHomepageModule();
const html = mod.renderDashboardHome({
workspaceLabel: "Kris",
currentProjectName: "品牌增长实验室",
contextLinks: [],
actionSourceLabel: "主 Agent 优先推荐",
primaryAction: { title: "A", reason: "B", badges: [], goAction: "x", goLabel: "去处理", agentLabel: "交给主 Agent" },
secondaryActions: [],
summaryTabs: [
{ key: "project_progress", label: "项目进度", value: "3 / 5", hint: "2 项可继续推进", active: true }
],
activeTabLabel: "项目进度",
overviewBodyHtml: "<section>tab body</section>"
});
assert.ok(html.includes('data-action="select-dashboard-tab"'));
assert.ok(!html.includes("当前项目推进详情"));
assert.ok(!html.includes("重点账号 / 对标</h3><div class=\"panel-subtitle\">右栏保留"));
});
```
- [ ] **Step 2: Run the Node test and verify the new assertions fail**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
```
Expected: FAIL because the overview renderer and admin entry are not complete yet.
- [ ] **Step 3: Implement overview-tab state and project switcher reuse**
Modify `web/storyforge-web-v4/assets/app.js`:
```js
function dashboardTabLabel(value) {
return ({
project_progress: "项目进度",
focus_accounts: "重点账号 / 对标",
production_jobs: "生产任务"
})[value] || "项目进度";
}
function buildDashboardOverviewTabs(project, stats) {
return [
{ key: "project_progress", label: "项目进度", value: "3 / 5", hint: "2 项可继续推进", active: appState.dashboardOverviewTab === "project_progress" },
{ key: "focus_accounts", label: "重点账号 / 对标", value: formatNumber(getTrackingAccounts().length), hint: "重点对象", active: appState.dashboardOverviewTab === "focus_accounts" },
{ key: "production_jobs", label: "生产任务", value: formatNumber(stats.jobs.length), hint: "当前项目任务", active: appState.dashboardOverviewTab === "production_jobs" }
];
}
function openDashboardProjectSwitcher() {
openActionModal({
title: "切换当前项目",
description: "首页上下文与动作区会随当前项目一起切换。",
submitLabel: "切换项目",
fields: [
{ name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options: getProjectOptions() }
],
onSubmit: async (payload) => {
appState.selectedProjectId = payload.projectId;
await loadAgentControlSurfaces(appState.selectedProjectId || "");
renderAll();
}
});
}
```
Add click handling in `web/storyforge-web-v4/assets/app.js`:
```js
if (name === "select-dashboard-tab") {
appState.dashboardOverviewTab = action.dataset.dashboardTab || "project_progress";
renderAll();
return;
}
if (name === "open-dashboard-project-switcher") {
openDashboardProjectSwitcher();
return;
}
if (name === "goto-owned") {
setScreen("owned");
return;
}
if (name === "goto-tracking") {
setScreen("tracking");
return;
}
if (name === "goto-playbook") {
setScreen("playbook");
return;
}
```
- [ ] **Step 4: Add the explicit admin workbench entry and screen**
Modify `web/storyforge-web-v4/index.html` sidebar:
```html
<button class="nav-item hidden" data-screen-target="admin-workbench" data-role-gate="super_admin">
<span class="icon"></span>
<span>管理员配置台</span>
</button>
```
Modify `web/storyforge-web-v4/assets/app.js`:
```js
function syncRoleGatedNav() {
document.querySelectorAll("[data-role-gate]").forEach((element) => {
const gate = element.getAttribute("data-role-gate");
const visible = gate === "super_admin" ? isSuperAdmin() : true;
element.classList.toggle("hidden", !visible);
});
}
function renderAdminWorkbenchScreen() {
if (!isSuperAdmin()) {
return screenShell("管理员配置台", "仅超级管理员可见。", "", renderEmptyState("无权限", "请使用超级管理员账号访问。"));
}
return screenShell(
"管理员配置台",
"系统级依赖、存储、平台 Agent 与策略治理。",
"",
`
${renderIntegrationOverviewPanel()}
${renderStorageStatusPanel()}
${renderPlatformAgentPanel()}
${renderAdminOpsOverviewPanel()}
${renderAdminFixRunsPanel()}
`
);
}
```
Call `syncRoleGatedNav()` inside `renderAll()` after session/role state has updated.
- [ ] **Step 5: Re-run targeted tests and syntax checks**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
node --check web/storyforge-web-v4/assets/storyforge-dashboard-home.js
node --check web/storyforge-web-v4/assets/app.js
```
Expected: PASS, and homepage markup no longer contains legacy repeated panels.
- [ ] **Step 6: Commit the overview/admin interaction work**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
git add web/storyforge-web-v4/index.html web/storyforge-web-v4/assets/app.js web/storyforge-web-v4/assets/storyforge-dashboard-home.js web/storyforge-web-v4/tests/dashboard-home.test.mjs
git commit -m "feat: add dashboard tab flow and admin workbench entry"
```
### Task 4: Add Styles, Docs, and Regression Coverage
**Files:**
- Modify: `web/storyforge-web-v4/assets/styles.css`
- Modify: `web/storyforge-web-v4/README.md`
- Modify: `scripts/check_repo_baseline.sh`
- Modify: `tests/test_production_baseline.py`
- [ ] **Step 1: Add a failing baseline regression test for the homepage redesign wiring**
Append to `tests/test_production_baseline.py`:
```python
def test_baseline_script_covers_homepage_dashboard_node_test(self) -> None:
script = (ROOT / "scripts" / "check_repo_baseline.sh").read_text(encoding="utf-8")
self.assertIn("dashboard-home.test.mjs", script)
```
- [ ] **Step 2: Run the Python regression test and verify the current branch fails**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
python3 -m unittest tests.test_production_baseline.ProductionBaselineTests.test_baseline_script_covers_homepage_dashboard_node_test -v
```
Expected: FAIL before `scripts/check_repo_baseline.sh` is updated to run the homepage Node test.
- [ ] **Step 3: Add the new CSS and update docs/baseline script**
Modify `web/storyforge-web-v4/assets/styles.css` with homepage-specific classes:
```css
.dashboard-context-row { display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; }
.dashboard-context-chip { display:flex; align-items:center; gap:8px; border:1px solid var(--line); border-radius:14px; padding:10px 12px; background:var(--panel-soft); }
.dashboard-priority-panel { display:grid; gap:12px; }
.dashboard-action-primary { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:16px; align-items:center; }
.dashboard-action-secondary-list { display:grid; gap:10px; }
.dashboard-overview-tabs { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.dashboard-overview-tab.is-active { border-color: var(--accent); background: var(--accent-soft); }
```
Modify `web/storyforge-web-v4/README.md`:
```md
- 首页已切到“人类决策优先”结构:
- 先显示当前项目与今日动作
- 再显示项目概览 tab
- 管理员配置台通过独立导航进入
```
Modify `scripts/check_repo_baseline.sh`:
```sh
echo "[5/5] validate homepage dashboard tests"
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
```
- [ ] **Step 4: Run the full redesign verification**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
python3 -m unittest tests.test_platform_contracts tests.test_production_baseline -v
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
node --check web/storyforge-web-v4/assets/storyforge-dashboard-home.js
node --check web/storyforge-web-v4/assets/app.js
bash scripts/check_repo_baseline.sh
git diff --check
```
Expected:
- Python tests PASS
- Node homepage test PASS
- `baseline checks passed`
- `git diff --check` returns no output
- [ ] **Step 5: Commit the styling and regression coverage**
Run:
```bash
cd /Users/kris/code/StoryForge-gitea
git add web/storyforge-web-v4/assets/styles.css web/storyforge-web-v4/README.md scripts/check_repo_baseline.sh tests/test_production_baseline.py
git commit -m "test: cover homepage dashboard redesign"
```