25 KiB
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:
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:
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:
(function () {
function defaultEscapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
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:
<script src="./assets/storyforge-dashboard-home.js"></script>
<script src="./assets/app.js"></script>
Modify web/storyforge-web-v4/assets/app.js near renderDashboardScreen():
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:
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:
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:
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:
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:
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:
const appState = {
// existing fields...
dashboardOverviewTab: "project_progress",
dashboardActionReason: null
};
Build the raw dashboard inputs in web/storyforge-web-v4/assets/app.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:
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:
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:
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:
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:
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:
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:
<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:
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:
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:
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:
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:
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:
.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:
- 首页已切到“人类决策优先”结构:
- 先显示当前项目与今日动作
- 再显示项目概览 tab
- 管理员配置台通过独立导航进入
Modify scripts/check_repo_baseline.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:
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 --checkreturns no output -
Step 5: Commit the styling and regression coverage
Run:
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"