Compare commits

...

2 Commits

Author SHA1 Message Date
ruinivist ff2fee02ff fix: use inequality instead of greater than for timestamp check
Addresses PR feedback to rely on timestamp inequality since it is more robust to clock issues and timestamp resolution when saving on the backend.
2026-05-31 10:58:45 +00:00
ruinivist 0d0d4b8a85 fix: reload drawing when edited externally by MCP
Adds an active polling and reload mechanism. It tracks the `updatedAt` timestamp of the current drawing and polls `refreshList` every 5 seconds. If the server's drawing is newer, it clears pending autosaves and reloads the drawing to reflect the external changes.

Also tracks when a title save is in progress to avoid reloading when the current user renamed the drawing.
2026-05-31 01:21:08 +00:00
+42 -8
View File
@@ -108,6 +108,8 @@ export function App() {
const loadVersionRef = useRef(0);
const themeSyncFrameRef = useRef<number | null>(null);
const toastTimeoutRef = useRef<number | null>(null);
const loadedUpdatedAtRef = useRef<string | null>(null);
const inFlightTitleSaveRef = useRef<Promise<void> | null>(null);
const activeDrawing = useMemo(
() => drawings.find((drawing) => drawing.id === activeId) ?? null,
@@ -141,6 +143,7 @@ export function App() {
body: JSON.stringify(nextScene),
})
.then((body) => {
loadedUpdatedAtRef.current = body.drawing.updatedAt;
setDrawings((current) => replaceMeta(current, body.drawing));
if (isManual) {
setToastMessage("Saved");
@@ -230,6 +233,7 @@ export function App() {
currentIdRef.current = drawing.id;
currentTitleRef.current = drawing.title;
latestSceneRef.current = nextScene;
loadedUpdatedAtRef.current = drawing.updatedAt;
setDrawings(sortDrawings(list));
setActiveId(drawing.id);
@@ -323,18 +327,22 @@ export function App() {
return;
}
try {
const body = await requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
method: "PUT",
body: JSON.stringify({ title: currentTitleRef.current }),
});
const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
method: "PUT",
body: JSON.stringify({ title: currentTitleRef.current }),
}).then((body) => {
currentTitleRef.current = body.drawing.title;
loadedUpdatedAtRef.current = body.drawing.updatedAt;
setActiveTitle(body.drawing.title);
setDrawings((current) => replaceMeta(current, body.drawing));
} catch (renameError) {
}).catch((renameError) => {
setError(renameError instanceof Error ? renameError.message : "Rename failed");
}
}).finally(() => {
inFlightTitleSaveRef.current = null;
});
inFlightTitleSaveRef.current = promise;
await promise;
}, []);
const syncThemeTokens = useCallback(() => {
@@ -411,6 +419,32 @@ export function App() {
};
}, []);
useEffect(() => {
const interval = window.setInterval(() => {
void refreshList();
}, 5000);
return () => {
window.clearInterval(interval);
};
}, [refreshList]);
useEffect(() => {
if (
activeDrawing &&
loadedUpdatedAtRef.current &&
activeDrawing.updatedAt !== loadedUpdatedAtRef.current &&
!inFlightSaveRef.current &&
!inFlightTitleSaveRef.current
) {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
void loadDrawing(activeDrawing.id);
}
}, [activeDrawing, loadDrawing]);
useEffect(() => {
if (!scene || loading) {
return;