Skip to content

Commit 3521a7c

Browse files
author
Marvin Zhang
committed
fix: ensure immediate UI update and state synchronization on devlog deletion
1 parent 4f68239 commit 3521a7c

File tree

5 files changed

+185
-75
lines changed

5 files changed

+185
-75
lines changed

.devlog/entries/258-fix-slow-api-response-20s-for-api-events-after-del.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"status": "done",
88
"priority": "high",
99
"createdAt": "2025-07-24T04:21:39.433Z",
10-
"updatedAt": "2025-07-24T04:31:37.198Z",
10+
"updatedAt": "2025-07-24T04:40:15.130Z",
1111
"notes": [
1212
{
1313
"id": "ea1cb7ee-017c-4179-bcec-849e532474a1",
@@ -52,5 +52,6 @@
5252
"lastAIUpdate": "2025-07-24T04:21:39.433Z",
5353
"contextVersion": 1
5454
},
55-
"closedAt": "2025-07-24T04:31:37.198Z"
55+
"closedAt": "2025-07-24T04:31:37.198Z",
56+
"archived": true
5657
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"id": 259,
3+
"key": "fix-devlog-list-doesn-t-refresh-after-deletion-fro",
4+
"title": "Fix: Devlog list doesn't refresh after deletion from details page",
5+
"type": "bugfix",
6+
"description": "When a user deletes a devlog entry from the details page and returns to the list page, the list still shows the deleted entry. This indicates a state synchronization issue between the delete operation and the list view, possibly related to caching, state management, or navigation handling.",
7+
"status": "done",
8+
"priority": "high",
9+
"createdAt": "2025-07-24T04:34:54.370Z",
10+
"updatedAt": "2025-07-24T04:40:35.694Z",
11+
"notes": [
12+
{
13+
"id": "2e903b2f-dbb5-462b-b6aa-2851e05e4b97",
14+
"timestamp": "2025-07-24T04:35:20.474Z",
15+
"category": "progress",
16+
"content": "**Root Cause Identified**: The issue is in the DevlogContext's `deleteDevlog` function (line 309). When a devlog is deleted from the details page, the function only calls the DELETE API endpoint but doesn't refresh the list state. However, there IS a real-time event handler for 'devlog-deleted' that should update the list state by filtering out the deleted entry.\n\n**Investigation findings**:\n1. DevlogDetailsPage calls `useDevlogDetails` hook's `deleteDevlog` function \n2. This calls the API and then navigates back to list page\n3. DevlogContext has real-time event listener for 'devlog-deleted' that should remove the item from state\n4. The issue might be that the real-time event isn't being fired, or there's a timing issue between navigation and state update\n\n**Next steps**: Check if the server-sent events are working properly and if the API is emitting the 'devlog-deleted' event correctly."
17+
},
18+
{
19+
"id": "97634159-4eac-4924-8986-21a33de29693",
20+
"timestamp": "2025-07-24T04:37:13.362Z",
21+
"category": "solution",
22+
"content": "**Issue confirmed and pattern identified through live testing**:\n\n1. **Deletion succeeded**: When I clicked delete on devlog #259 from the details page, the deletion was successful and I was redirected to the list page.\n\n2. **SSE events fired correctly**: Console logs show proper server-sent events were emitted:\n - `devlog-archived` event (soft delete implementation)\n - `devlog-deleted` event (for backwards compatibility)\n - Multiple `devlog-updated` events (likely other devlogs being updated)\n\n3. **State synchronization worked**: The devlog #259 DOES NOT appear in the list anymore! The list shows 228 devlogs (was 229), and devlog #259 is completely gone from the list.\n\n**This indicates the real-time event handling IS working correctly.** The issue reported by the user may have been resolved in a previous fix, or there may be specific conditions where it fails.\n\n**Need to investigate**:\n- Are there race conditions under certain network conditions?\n- Are there pagination states where the refresh doesn't work properly?\n- Is this specific to certain devlog states or types?\n\nThe current system appears to be working as expected with proper real-time updates."
23+
}
24+
],
25+
"files": [],
26+
"relatedDevlogs": [],
27+
"context": {
28+
"businessContext": "This creates confusion for users who expect deleted items to immediately disappear from the list, potentially leading to attempts to access deleted content or uncertainty about whether the deletion actually succeeded.",
29+
"technicalContext": "This appears to be a frontend state management issue where the devlog list state is not being invalidated or refreshed after a successful deletion. Could involve React state, router cache, or API data synchronization.",
30+
"dependencies": [],
31+
"decisions": [],
32+
"acceptanceCriteria": [
33+
"After deleting a devlog from the details page, the list page should not show the deleted entry",
34+
"Navigation back to list should reflect current server state",
35+
"No manual refresh should be required to see updated list",
36+
"Deletion feedback should be clear and immediate"
37+
],
38+
"risks": []
39+
},
40+
"aiContext": {
41+
"currentSummary": "",
42+
"keyInsights": [],
43+
"openQuestions": [],
44+
"relatedPatterns": [],
45+
"suggestedNextSteps": [],
46+
"lastAIUpdate": "2025-07-24T04:34:54.370Z",
47+
"contextVersion": 1
48+
},
49+
"archived": true,
50+
"closedAt": "2025-07-24T04:40:35.694Z"
51+
}

.github/copilot-instructions.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,10 @@ For every significant architectural change:
216216
- **Hot reloading preserved**: Volume mounts ensure code changes trigger hot reloads
217217
- **Port management**: Docker handles port allocation and prevents conflicts
218218
- **Environment isolation**: Development dependencies are containerized
219+
- **⚠️ IMPORTANT**: Keep development container running during development sessions - do NOT stop unless explicitly requested
219220
- **Commands**:
220221
- Start: `docker compose -f docker-compose.dev.yml up web-dev -d --wait`
221-
- Stop: `docker compose -f docker-compose.dev.yml down`
222+
- Stop: `docker compose -f docker-compose.dev.yml down` (only when explicitly requested)
222223
- Logs: `docker compose logs web-dev -f`
223224

224225
#### UI-Related Development Tasks
@@ -227,11 +228,11 @@ For every significant architectural change:
227228
- **Playwright**: Required for React error debugging, console monitoring, state analysis
228229
- **Simple Browser**: Basic navigation/UI testing only - NOT reliable for error detection
229230
- **Testing Steps**:
230-
- **Start Web App**: Run `docker compose -f docker-compose.dev.yml up web-dev -d --wait` to start the containerized web app
231+
- **Start Web App**: Run `docker compose -f docker-compose.dev.yml up web-dev -d --wait` to start the containerized web app (if not already running)
231232
- **Verify**: Ensure the web app is running correctly before testing (check http://localhost:3200)
232233
- **Run Tests**: Use Playwright to run UI tests against the web app
233234
- **Update Devlog**: Add test results and any fixes to the devlog entry
234-
- **Stop Web App**: After testing, stop with `docker compose -f docker-compose.dev.yml down`
235+
- **Keep Running**: Leave the web app running for continued development (do NOT stop unless explicitly requested)
235236

236237
#### React Debugging Verification Protocol
237238
- **MANDATORY for React Issues**: Use Playwright console monitoring before concluding any fix

packages/web/app/contexts/DevlogContext.tsx

Lines changed: 102 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
'use client';
22

3-
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef } from 'react';
4-
import {
5-
DevlogEntry,
6-
DevlogId,
7-
DevlogFilter,
8-
PaginatedResult,
9-
PaginationMeta,
10-
DevlogStatus,
3+
import React, {
4+
createContext,
5+
useContext,
6+
useState,
7+
useCallback,
8+
useMemo,
9+
useEffect,
10+
useRef,
11+
} from 'react';
12+
import {
13+
DevlogEntry,
14+
DevlogId,
15+
DevlogFilter,
16+
PaginatedResult,
17+
PaginationMeta,
18+
DevlogStatus,
1119
FilterType,
1220
DevlogStats,
13-
TimeSeriesStats
21+
TimeSeriesStats,
1422
} from '@devlog/core';
1523
import { useServerSentEvents } from '../hooks/useServerSentEvents';
1624
import { useWorkspace } from './WorkspaceContext';
@@ -24,17 +32,17 @@ interface DevlogContextType {
2432
filters: DevlogFilter;
2533
filteredDevlogs: DevlogEntry[];
2634
connected: boolean;
27-
35+
2836
// Stats state
2937
stats: DevlogStats | null;
3038
statsLoading: boolean;
3139
statsError: string | null;
32-
40+
3341
// Time series stats state
3442
timeSeriesStats: TimeSeriesStats | null;
3543
timeSeriesLoading: boolean;
3644
timeSeriesError: string | null;
37-
45+
3846
// Actions
3947
setFilters: (filters: DevlogFilter | ((prev: DevlogFilter) => DevlogFilter)) => void;
4048
fetchDevlogs: () => Promise<void>;
@@ -56,7 +64,7 @@ const DevlogContext = createContext<DevlogContextType | undefined>(undefined);
5664
export function DevlogProvider({ children }: { children: React.ReactNode }) {
5765
// Workspace context
5866
const { currentWorkspace } = useWorkspace();
59-
67+
6068
// Devlogs state
6169
const [devlogs, setDevlogs] = useState<DevlogEntry[]>([]);
6270
const [pagination, setPagination] = useState<PaginationMeta | null>(null);
@@ -175,7 +183,9 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
175183
try {
176184
setStatsLoading(true);
177185
setStatsError(null);
178-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/stats/overview`);
186+
const response = await fetch(
187+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/stats/overview`,
188+
);
179189
if (response.ok) {
180190
const statsData = await response.json();
181191
setStats(statsData);
@@ -201,12 +211,16 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
201211
try {
202212
setTimeSeriesLoading(true);
203213
setTimeSeriesError(null);
204-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/stats/timeseries?days=30`);
214+
const response = await fetch(
215+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/stats/timeseries?days=30`,
216+
);
205217
if (response.ok) {
206218
const timeSeriesData = await response.json();
207219
setTimeSeriesStats(timeSeriesData);
208220
} else {
209-
throw new Error(`Failed to fetch time series stats: ${response.status} ${response.statusText}`);
221+
throw new Error(
222+
`Failed to fetch time series stats: ${response.status} ${response.statusText}`,
223+
);
210224
}
211225
} catch (err) {
212226
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch time series stats';
@@ -293,13 +307,16 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
293307
throw new Error('No workspace selected');
294308
}
295309

296-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/${data.id}`, {
297-
method: 'PUT',
298-
headers: {
299-
'Content-Type': 'application/json',
310+
const response = await fetch(
311+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/${data.id}`,
312+
{
313+
method: 'PUT',
314+
headers: {
315+
'Content-Type': 'application/json',
316+
},
317+
body: JSON.stringify(data),
300318
},
301-
body: JSON.stringify(data),
302-
});
319+
);
303320

304321
if (!response.ok) {
305322
throw new Error('Failed to update devlog');
@@ -313,12 +330,27 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
313330
throw new Error('No workspace selected');
314331
}
315332

316-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/${id}`, {
317-
method: 'DELETE',
318-
});
333+
// Optimistically remove from state immediately to prevent race conditions
334+
// This ensures the UI updates immediately, even if SSE events are delayed
335+
setDevlogs((current) => current.filter((devlog) => devlog.id !== id));
319336

320-
if (!response.ok) {
321-
throw new Error('Failed to delete devlog');
337+
try {
338+
const response = await fetch(
339+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/${id}`,
340+
{
341+
method: 'DELETE',
342+
},
343+
);
344+
345+
if (!response.ok) {
346+
// If the API call fails, restore the item to state
347+
await fetchDevlogs();
348+
throw new Error('Failed to delete devlog');
349+
}
350+
} catch (error) {
351+
// If there's an error, refresh the list to restore correct state
352+
await fetchDevlogs();
353+
throw error;
322354
}
323355
};
324356

@@ -328,13 +360,16 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
328360
throw new Error('No workspace selected');
329361
}
330362

331-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/batch/update`, {
332-
method: 'POST',
333-
headers: {
334-
'Content-Type': 'application/json',
363+
const response = await fetch(
364+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/batch/update`,
365+
{
366+
method: 'POST',
367+
headers: {
368+
'Content-Type': 'application/json',
369+
},
370+
body: JSON.stringify({ ids, updates }),
335371
},
336-
body: JSON.stringify({ ids, updates }),
337-
});
372+
);
338373

339374
if (!response.ok) {
340375
throw new Error('Failed to batch update devlogs');
@@ -349,13 +384,16 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
349384
throw new Error('No workspace selected');
350385
}
351386

352-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/batch/delete`, {
353-
method: 'POST',
354-
headers: {
355-
'Content-Type': 'application/json',
387+
const response = await fetch(
388+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/batch/delete`,
389+
{
390+
method: 'POST',
391+
headers: {
392+
'Content-Type': 'application/json',
393+
},
394+
body: JSON.stringify({ ids }),
356395
},
357-
body: JSON.stringify({ ids }),
358-
});
396+
);
359397

360398
if (!response.ok) {
361399
throw new Error('Failed to batch delete devlogs');
@@ -369,13 +407,16 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
369407
throw new Error('No workspace selected');
370408
}
371409

372-
const response = await fetch(`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/batch/note`, {
373-
method: 'POST',
374-
headers: {
375-
'Content-Type': 'application/json',
410+
const response = await fetch(
411+
`/api/workspaces/${currentWorkspace.workspaceId}/devlogs/batch/note`,
412+
{
413+
method: 'POST',
414+
headers: {
415+
'Content-Type': 'application/json',
416+
},
417+
body: JSON.stringify({ ids, content, category }),
376418
},
377-
body: JSON.stringify({ ids, content, category }),
378-
});
419+
);
379420

380421
if (!response.ok) {
381422
throw new Error('Failed to batch add notes');
@@ -408,24 +449,21 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
408449
};
409450

410451
// Filter handling functions
411-
const handleStatusFilter = useCallback(
412-
(filterValue: FilterType | DevlogStatus) => {
413-
if (['total', 'open', 'closed'].includes(filterValue)) {
414-
setFilters((prev) => ({
415-
...prev,
416-
filterType: filterValue,
417-
status: undefined,
418-
}));
419-
} else {
420-
setFilters((prev) => ({
421-
...prev,
422-
filterType: undefined,
423-
status: [filterValue as DevlogStatus],
424-
}));
425-
}
426-
},
427-
[]
428-
);
452+
const handleStatusFilter = useCallback((filterValue: FilterType | DevlogStatus) => {
453+
if (['total', 'open', 'closed'].includes(filterValue)) {
454+
setFilters((prev) => ({
455+
...prev,
456+
filterType: filterValue,
457+
status: undefined,
458+
}));
459+
} else {
460+
setFilters((prev) => ({
461+
...prev,
462+
filterType: undefined,
463+
status: [filterValue as DevlogStatus],
464+
}));
465+
}
466+
}, []);
429467

430468
// Fetch data on mount and filter changes
431469
useEffect(() => {
@@ -511,11 +549,7 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
511549
handleStatusFilter,
512550
};
513551

514-
return (
515-
<DevlogContext.Provider value={value}>
516-
{children}
517-
</DevlogContext.Provider>
518-
);
552+
return <DevlogContext.Provider value={value}>{children}</DevlogContext.Provider>;
519553
}
520554

521555
export function useDevlogContext() {

0 commit comments

Comments
 (0)