diff --git a/.claude/CLAUDE-KNOWLEDGE.md b/.claude/CLAUDE-KNOWLEDGE.md index 64f9aacb0..25bb9b3ea 100644 --- a/.claude/CLAUDE-KNOWLEDGE.md +++ b/.claude/CLAUDE-KNOWLEDGE.md @@ -499,3 +499,6 @@ A: Browser-only RDE endpoints should require RDE to be enabled, a local dashboar ## Q: How should development-environment project creation seed environment config? A: Seed the normal initial environment config before marking the project as `isDevelopmentEnvironment=true`. Existing development-environment projects should continue to reject environment config override writes, but creation needs to populate defaults like RBAC permissions, password sign-in, and installed apps first; otherwise the write guard throws during setup/restart-deps. + +## Q: What can cause React error #185 immediately on dashboard load? +A: React error #185 is a maximum update depth error. In the dashboard root, `useSyncExternalStore` snapshot getters must return cached referentially stable values. Returning a fresh object such as `{ status: "healthy" }` from `getSnapshot` on every call can make React think the external store changed on every render and loop immediately. Use module-level constants for stable snapshots. diff --git a/apps/dashboard/src/app/layout-client.tsx b/apps/dashboard/src/app/layout-client.tsx index ba3b3207b..c25d7147a 100644 --- a/apps/dashboard/src/app/layout-client.tsx +++ b/apps/dashboard/src/app/layout-client.tsx @@ -23,6 +23,9 @@ type DevEnvironmentHealthSnapshot = | { status: "checking" | "healthy" } | { status: "unhealthy", restartCommand: string }; +const CHECKING_DEV_ENVIRONMENT_HEALTH_SNAPSHOT: DevEnvironmentHealthSnapshot = { status: "checking" }; +const HEALTHY_DEV_ENVIRONMENT_HEALTH_SNAPSHOT: DevEnvironmentHealthSnapshot = { status: "healthy" }; + function isDevEnvironmentHealthResponse(value: unknown): value is { ok: boolean, restart_command: string } { return ( value != null && @@ -34,7 +37,7 @@ function isDevEnvironmentHealthResponse(value: unknown): value is { ok: boolean, ); } -let devEnvironmentHealthSnapshot: DevEnvironmentHealthSnapshot = { status: "checking" }; +let devEnvironmentHealthSnapshot: DevEnvironmentHealthSnapshot = CHECKING_DEV_ENVIRONMENT_HEALTH_SNAPSHOT; const devEnvironmentHealthSubscribers = new Set<() => void>(); let devEnvironmentHealthTimer: ReturnType | undefined; let devEnvironmentHealthRequestSequence = 0; @@ -67,7 +70,7 @@ async function refreshDevEnvironmentHealth() { } setSnapshotIfCurrent(body.ok && response.ok - ? { status: "healthy" } + ? HEALTHY_DEV_ENVIRONMENT_HEALTH_SNAPSHOT : { status: "unhealthy", restartCommand: body.restart_command }); } catch { setSnapshotIfCurrent({ @@ -80,7 +83,7 @@ async function refreshDevEnvironmentHealth() { function subscribeDevEnvironmentHealth(callback: () => void) { devEnvironmentHealthSubscribers.add(callback); if (devEnvironmentHealthSubscribers.size === 1) { - setDevEnvironmentHealthSnapshot({ status: "checking" }); + setDevEnvironmentHealthSnapshot(CHECKING_DEV_ENVIRONMENT_HEALTH_SNAPSHOT); runAsynchronouslyWithAlert(refreshDevEnvironmentHealth()); devEnvironmentHealthTimer = setInterval(() => { runAsynchronouslyWithAlert(refreshDevEnvironmentHealth()); @@ -101,7 +104,7 @@ function getDevEnvironmentHealthSnapshot() { } function getServerDevEnvironmentHealthSnapshot(): DevEnvironmentHealthSnapshot { - return { status: "checking" }; + return CHECKING_DEV_ENVIRONMENT_HEALTH_SNAPSHOT; } function subscribeHealthyDevEnvironment(_callback: () => void) { @@ -109,7 +112,7 @@ function subscribeHealthyDevEnvironment(_callback: () => void) { } function getHealthyDevEnvironmentSnapshot(): DevEnvironmentHealthSnapshot { - return { status: "healthy" }; + return HEALTHY_DEV_ENVIRONMENT_HEALTH_SNAPSHOT; } function DevEnvironmentStoppedScreen(props: { restartCommand: string }) {