Scope folder realtime refreshes by device

This commit is contained in:
kris
2026-04-10 12:55:43 +08:00
parent d1e5a1ac5e
commit 0cba837ed3
11 changed files with 357 additions and 9 deletions

View File

@@ -0,0 +1,197 @@
# SSE Refresh Noise Reduction 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:** Reduce unnecessary realtime reloads on scoped conversation folder pages without breaking global conversation refresh behavior.
**Architecture:** Keep server-side event names stable and narrow consumption at scoped clients. The shared Web realtime refresh utility accepts optional device scope, while the Android folder activity applies the same project-first, device-only fallback filter.
**Tech Stack:** Next.js App Router, TypeScript `node:test`, Android Java native activities, Server-Sent Events.
---
### Task 1: Add Device Scope To Shared Web Refresh Filtering
**Files:**
- Modify: `src/lib/realtime-refresh.ts`
- Test: `tests/realtime-refresh-utils.test.ts`
- [ ] **Step 1: Write the failing tests**
```ts
assert.equal(
shouldRefreshRealtimeEvent({
eventType: "conversation.updated",
eventData: JSON.stringify({ deviceId: "mac-studio" }),
projectIds: ["project-a", "project-b"],
deviceId: "mac-studio",
}),
true,
);
assert.equal(
shouldRefreshRealtimeEvent({
eventType: "conversation.updated",
eventData: JSON.stringify({ deviceId: "windows-box" }),
projectIds: ["project-a", "project-b"],
deviceId: "mac-studio",
}),
false,
);
```
- [ ] **Step 2: Run the focused test and verify the new device-scope test fails**
Run: `npx tsx --test tests/realtime-refresh-utils.test.ts`
Expected: the new device-scope case fails before `deviceId` support exists in `RealtimeRefreshScope`.
- [ ] **Step 3: Add minimal device scope support**
```ts
export interface RealtimeRefreshScope {
projectId?: string;
projectIds?: string[];
deviceId?: string;
deviceIds?: string[];
conversationUpdatedNotes?: string[];
}
```
Then parse `deviceId` from the JSON payload and require scoped listeners to match at least one scoped project or device identifier before refreshing.
- [ ] **Step 4: Run the focused test and verify it passes**
Run: `npx tsx --test tests/realtime-refresh-utils.test.ts`
Expected: all realtime refresh utility tests pass.
### Task 2: Wire Web Folder Device Scope
**Files:**
- Modify: `src/components/app-runtime.tsx`
- Modify: `src/app/conversations/folders/[folderKey]/page.tsx`
- Test: `tests/project-scoped-realtime-refresh.test.ts`
- [ ] **Step 1: Write the failing route wiring assertion**
```ts
assert.match(
folderPage,
/deviceId=\{folder\.deviceId\}/,
"expected folder page to scope device-only refreshes to the folder device",
);
```
- [ ] **Step 2: Run the route test and verify the assertion fails**
Run: `npx tsx --test tests/project-scoped-realtime-refresh.test.ts`
Expected: the assertion fails until the folder page passes `deviceId`.
- [ ] **Step 3: Pass device scope through the runtime component**
```tsx
<RealtimeRefresh
projectIds={folder.threads.map((thread) => thread.projectId)}
deviceId={folder.deviceId}
events={["conversation.updated", "project.messages.updated"]}
/>
```
Also pass `deviceId` and `deviceIds` from `RealtimeRefresh` into `shouldRefreshRealtimeEvent`.
- [ ] **Step 4: Run the route test and verify it passes**
Run: `npx tsx --test tests/project-scoped-realtime-refresh.test.ts`
Expected: the folder route wiring test passes.
### Task 3: Mirror Filtering In Android Folder Activity
**Files:**
- Modify: `android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java`
- Test: `tests/android-folder-realtime-refresh.test.ts`
- [ ] **Step 1: Write the failing Android source assertion**
```ts
assert.match(
source,
/String payloadDeviceId = event\.payload\.optString\("deviceId", ""\)\.trim\(\);/,
"expected folder activity to inspect device-scoped realtime payloads",
);
```
- [ ] **Step 2: Run the Android static test and verify it fails**
Run: `npx tsx --test tests/android-folder-realtime-refresh.test.ts`
Expected: the test fails until the folder activity reads `deviceId`.
- [ ] **Step 3: Add project-first and device-only filtering**
```java
String payloadProjectId = event.payload.optString("projectId", "").trim();
if (!payloadProjectId.isEmpty()) {
return trackedProjectIds.contains(payloadProjectId)
|| (!targetProjectIds.isEmpty() && targetProjectIds.contains(payloadProjectId))
|| (targetProjectId != null && targetProjectId.equals(payloadProjectId));
}
String payloadDeviceId = event.payload.optString("deviceId", "").trim();
if (payloadDeviceId.isEmpty() || folderDeviceId == null || folderDeviceId.isEmpty()) {
return false;
}
return payloadDeviceId.equals(folderDeviceId);
```
- [ ] **Step 4: Run the Android static test and verify it passes**
Run: `npx tsx --test tests/android-folder-realtime-refresh.test.ts`
Expected: the Android folder realtime filtering test passes.
### Task 4: Verify Integration
**Files:**
- Verify: `src/lib/realtime-refresh.ts`
- Verify: `src/components/app-runtime.tsx`
- Verify: `src/app/conversations/folders/[folderKey]/page.tsx`
- Verify: `android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java`
- Verify: `scripts/deploy-server.sh`
- [ ] **Step 1: Run focused realtime tests**
Run: `npx tsx --test tests/realtime-refresh-utils.test.ts tests/project-scoped-realtime-refresh.test.ts tests/android-folder-realtime-refresh.test.ts`
Expected: all focused realtime tests pass.
- [ ] **Step 2: Run deployment script guard test**
Run: `npx tsx --test tests/deploy-server-script.test.ts`
Expected: the deploy script tests pass and assert `.project`, `.classpath`, and `.settings` are excluded from rsync.
- [ ] **Step 3: Run broader realtime page tests**
Run: `npx tsx --test tests/realtime-refresh-utils.test.ts tests/project-scoped-realtime-refresh.test.ts tests/android-folder-realtime-refresh.test.ts tests/config-pages-realtime-refresh.test.ts tests/config-state-events.test.ts tests/settings-page-realtime-refresh.test.ts tests/settings-state-events.test.ts tests/status-pages-realtime-refresh.test.ts`
Expected: all selected realtime and event contract tests pass.
- [ ] **Step 4: Run project gates**
Run: `npm run lint`
Expected: lint exits 0.
Run: `npm run build`
Expected: production build exits 0.
- [ ] **Step 5: Stage only intended files**
```bash
git add src/lib/realtime-refresh.ts src/components/app-runtime.tsx 'src/app/conversations/folders/[folderKey]/page.tsx' tests/realtime-refresh-utils.test.ts tests/project-scoped-realtime-refresh.test.ts tests/android-folder-realtime-refresh.test.ts android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java scripts/deploy-server.sh tests/deploy-server-script.test.ts docs/superpowers/specs/2026-04-10-sse-refresh-noise-reduction-design.md docs/superpowers/plans/2026-04-10-sse-refresh-noise-reduction.md
```
Expected: generated IDE metadata under `android/.project`, `android/.settings`, `android/app/.project`, `android/app/.settings`, and `android/app/.classpath` remains untracked.