Refresh settings page in realtime
This commit is contained in:
99
docs/superpowers/plans/2026-04-07-settings-page-realtime.md
Normal file
99
docs/superpowers/plans/2026-04-07-settings-page-realtime.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Settings Page Realtime Refresh 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:** 让 `/me/settings` 在用户设置变更后自动刷新。
|
||||
|
||||
**Architecture:** 复用现有 `boss-events -> /api/v1/events -> RealtimeRefresh` SSE 链路,新增一个语义明确的 `settings.updated` 事件。`updateUserSettings` 写入文件状态后发布事件,设置页只监听这个事件。
|
||||
|
||||
**Tech Stack:** Next.js App Router, TypeScript, Node test runner, SSE
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 写红灯测试
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/settings-page-realtime-refresh.test.ts`
|
||||
- Create: `tests/settings-state-events.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写设置页接线测试**
|
||||
|
||||
```ts
|
||||
assert.match(source, /<RealtimeRefresh/, "expected settings page to render RealtimeRefresh");
|
||||
assert.match(source, /events=\{\["settings\.updated"\]\}/, "expected settings page to refresh on settings.updated");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑页面测试确认失败**
|
||||
|
||||
Run: `npx tsx --test tests/settings-page-realtime-refresh.test.ts`
|
||||
Expected: FAIL,因为设置页还没有接 `RealtimeRefresh`。
|
||||
|
||||
- [ ] **Step 3: 写设置事件测试**
|
||||
|
||||
```ts
|
||||
await updateUserSettings({ liveUpdates: false });
|
||||
assert.equal(events.at(-1)?.event, "settings.updated");
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑事件测试确认失败**
|
||||
|
||||
Run: `npx tsx --test tests/settings-state-events.test.ts`
|
||||
Expected: FAIL,因为 `updateUserSettings` 还没有发布 `settings.updated`。
|
||||
|
||||
### Task 2: 补实现
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/boss-events.ts`
|
||||
- Modify: `src/lib/boss-data.ts`
|
||||
- Modify: `src/app/me/settings/page.tsx`
|
||||
|
||||
- [ ] **Step 1: 扩展 `BossEventName`**
|
||||
|
||||
```ts
|
||||
| "settings.updated"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 `updateUserSettings` 发布事件**
|
||||
|
||||
```ts
|
||||
publishBossEvent("settings.updated");
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 给设置页接入刷新**
|
||||
|
||||
```tsx
|
||||
<RealtimeRefresh events={["settings.updated"]} />
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑本轮测试确认转绿**
|
||||
|
||||
Run: `npx tsx --test tests/settings-page-realtime-refresh.test.ts tests/settings-state-events.test.ts`
|
||||
Expected: PASS
|
||||
|
||||
### Task 3: 验证与提交
|
||||
|
||||
**Files:**
|
||||
- Test: `tests/settings-page-realtime-refresh.test.ts`
|
||||
- Test: `tests/settings-state-events.test.ts`
|
||||
|
||||
- [ ] **Step 1: 跑相关 realtime 测试**
|
||||
|
||||
Run: `npx tsx --test tests/settings-page-realtime-refresh.test.ts tests/settings-state-events.test.ts tests/config-pages-realtime-refresh.test.ts tests/config-state-events.test.ts tests/realtime-refresh-utils.test.ts tests/project-scoped-realtime-refresh.test.ts tests/status-pages-realtime-refresh.test.ts`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: 跑 lint**
|
||||
|
||||
Run: `npm run lint`
|
||||
Expected: exit code 0
|
||||
|
||||
- [ ] **Step 3: 跑 build**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: exit code 0
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-04-07-settings-page-realtime-design.md docs/superpowers/plans/2026-04-07-settings-page-realtime.md tests/settings-page-realtime-refresh.test.ts tests/settings-state-events.test.ts src/lib/boss-events.ts src/lib/boss-data.ts src/app/me/settings/page.tsx
|
||||
git commit -m "Refresh settings page in realtime"
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# 设置页实时刷新 Design
|
||||
|
||||
## 目标
|
||||
|
||||
让 Web 设置页 `/me/settings` 在用户设置被其他窗口或设备修改后自动刷新。
|
||||
|
||||
## 范围
|
||||
|
||||
本轮只处理设置页:
|
||||
|
||||
- 新增 `settings.updated` SSE 事件
|
||||
- `updateUserSettings` 写入成功后发布 `settings.updated`
|
||||
- `/me/settings` 页面通过 `RealtimeRefresh` 监听 `settings.updated`
|
||||
|
||||
不处理 `devices/add`、`me/security` 和根重定向页;这些页面没有同等价值的实时刷新需求。
|
||||
|
||||
## 验证
|
||||
|
||||
- 页面接线测试确认设置页渲染 `RealtimeRefresh` 并监听 `settings.updated`
|
||||
- 状态事件测试确认 `updateUserSettings` 会发布 `settings.updated`
|
||||
- 跑相关 realtime 测试、`npm run lint`、`npm run build`
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RealtimeRefresh } from "@/components/app-runtime";
|
||||
import { AppShell, PageNav, SettingsForm, StatusBar } from "@/components/app-ui";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
@@ -10,6 +11,7 @@ export default async function SettingsPage() {
|
||||
|
||||
return (
|
||||
<AppShell bottomNav={false}>
|
||||
<RealtimeRefresh events={["settings.updated"]} />
|
||||
<StatusBar />
|
||||
<PageNav title="设置" backHref="/me" />
|
||||
<div className="space-y-3 px-[18px] pb-6">
|
||||
|
||||
@@ -9630,7 +9630,7 @@ export async function updateUserSettings(settings: Partial<UserSettings>) {
|
||||
};
|
||||
return state.user.settings;
|
||||
});
|
||||
publishBossEvent("conversation.updated", { deviceId: PRIMARY_CODEX_NODE_ID });
|
||||
publishBossEvent("settings.updated");
|
||||
return nextSettings;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export type BossEventName =
|
||||
| "ai_accounts.updated"
|
||||
| "devices.updated"
|
||||
| "devices.skills.updated"
|
||||
| "settings.updated"
|
||||
| "storage.updated"
|
||||
| "ota.updated";
|
||||
|
||||
|
||||
18
tests/settings-page-realtime-refresh.test.ts
Normal file
18
tests/settings-page-realtime-refresh.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
test("settings page refreshes when user settings change", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/app/me/settings/page.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(source, /import \{ RealtimeRefresh \}/, "expected settings page to import RealtimeRefresh");
|
||||
assert.match(source, /<RealtimeRefresh/, "expected settings page to render RealtimeRefresh");
|
||||
assert.match(
|
||||
source,
|
||||
/events=\{\["settings\.updated"\]\}/,
|
||||
"expected settings page to refresh on settings.updated",
|
||||
);
|
||||
});
|
||||
62
tests/settings-state-events.test.ts
Normal file
62
tests/settings-state-events.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
|
||||
let runtimeRoot = "";
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
||||
let updateUserSettings: (typeof import("../src/lib/boss-data"))["updateUserSettings"];
|
||||
let subscribeBossEvents: (typeof import("../src/lib/boss-events"))["subscribeBossEvents"];
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-settings-events-"));
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [data, events] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-events.ts"),
|
||||
]);
|
||||
readState = data.readState;
|
||||
writeState = data.writeState;
|
||||
updateUserSettings = data.updateUserSettings;
|
||||
subscribeBossEvents = events.subscribeBossEvents;
|
||||
}
|
||||
|
||||
async function resetSettingsState() {
|
||||
const state = await readState();
|
||||
state.user.settings = {
|
||||
liveUpdates: true,
|
||||
showRiskBadges: true,
|
||||
confirmDangerousActions: true,
|
||||
preferredEntryPoint: "conversations",
|
||||
};
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await setup();
|
||||
await resetSettingsState();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("updateUserSettings publishes settings refresh event", async () => {
|
||||
const events: Array<{ event: string }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event) => {
|
||||
events.push({ event });
|
||||
});
|
||||
|
||||
await updateUserSettings({ liveUpdates: false });
|
||||
unsubscribe();
|
||||
|
||||
assert.equal(events.at(-1)?.event, "settings.updated");
|
||||
});
|
||||
Reference in New Issue
Block a user