Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 86 additions & 11 deletions dashboard/web/src/components/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { Info } from '@phosphor-icons/react';
import { useRef, type ReactNode } from 'react';
import { Info, WarningCircle } from '@phosphor-icons/react';

import type { Result } from '../types';

Expand Down Expand Up @@ -69,6 +69,58 @@ export function PanelEmpty({ result }: { result: { reason: string; hint?: string
);
}

/**
* Shown above a panel's content when the newest response was unavailable but an
* earlier one wasn't.
*
* An upstream declining once is not a reason to throw away data that is seconds
* old — but it is a reason to say so, because silently showing stale numbers is
* worse than showing none. Carries the same reason and hint `PanelEmpty` would,
* so the fix stays discoverable without the panel going blank.
*/
export function PanelStale({ result }: { result: { reason: string; hint?: string } }) {
return (
<div
style={{
display: 'flex',
gap: 'var(--space-2)',
alignItems: 'flex-start',
padding: 'var(--space-2) var(--space-3)',
borderRadius: 'var(--radius-sm)',
background: 'color-mix(in srgb, var(--ap-amber) 12%, transparent)',
}}
>
<WarningCircle
size={14}
color="var(--ap-amber)"
weight="regular"
style={{ flex: 'none', marginTop: 2 }}
/>
<div style={{ minWidth: 0, fontSize: 12 }}>
<span>Showing last known data — {result.reason}</span>
{result.hint && (
<div className="text-muted" style={{ marginTop: 2 }}>
{result.hint}
</div>
)}
</div>
</div>
);
}

/**
* Remembers the most recent payload that was actually available.
*
* Assigning during render is safe here because it's derived purely from props
* and is idempotent — the same `data` always produces the same assignment, so a
* double render under StrictMode can't skew it.
*/
function useLastAvailable<T extends object>(data: Result<T> | null): T | null {
const lastAvailable = useRef<T | null>(null);
if (data?.available) lastAvailable.current = data;
return lastAvailable.current;
Comment on lines +118 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant component and hook usage.
fd -a 'Panel\.tsx$|.*' | sed 's#^\./##' | rg 'dashboard/web/src/components/Panel\.tsx|src/components/Panel\.tsx' || true
if [ -f dashboard/web/src/components/Panel.tsx ]; then
  echo '--- outline ---'
  ast-grep outline dashboard/web/src/components/Panel.tsx || true
  echo '--- relevant lines ---'
  sed -n '1,180p' dashboard/web/src/components/Panel.tsx | nl -ba
fi

# Search for Result type and usages of useLastAvailable to understand scope/types.
echo '--- useLastAvailable usages ---'
rg -n "useLastAvailable|Result<|interface Result|type Result" dashboard/web/src -S || true

# If not in specified path, search repo.
echo '--- repo-wide usages ---'
rg -n "useLastAvailable|interface Result|type Result" -S || true

Repository: joshdev8/AutoPlexx

Length of output: 867


🌐 Web query:

React concurrent rendering abandoned render may leave refs mutated useEffect layout effect documentation

💡 Result:

In React's concurrent rendering architecture, the distinction between the render phase and the commit phase is fundamental to maintaining application stability. Render Phase (Interruptible) The render phase, where React calls your component functions to build the new Fiber tree, is explicitly designed to be pure and interruptible [1][2][3]. React may invoke your component function multiple times, pause work to handle higher-priority updates, or completely abandon a render cycle if the work is deemed no longer relevant [4][5][3]. Because of this, it is critical that rendering logic remains free of side effects [5][6][3]. This includes: - Avoiding direct DOM mutations. - Avoiding writing to mutable refs during the render execution [5]. - Avoiding triggering asynchronous operations or other external side effects [5][3]. If you mutate a ref directly in the render body, that mutation could be persisted even if React discards the current rendering work, leading to inconsistent state or "phantom" effects [5]. Commit Phase (Uninterruptible) Once the render phase is complete and React has determined the final UI state, it moves to the commit phase [1][3]. This phase is synchronous and uninterruptible [1][3]. It is only during this stage that React applies changes to the host environment (the DOM) [1][3]. Because useLayoutEffect and useEffect are scheduled during the commit phase, they are guaranteed to run only after the render is finalized and committed [1][7]. - Refs are attached and updated as part of the commit phase (typically in the mutation or layout stage) [1]. - useLayoutEffect runs synchronously after DOM mutations but before the browser paints [8][1]. - useEffect runs asynchronously after the browser has painted [1][9][3]. By isolating side effects, ref updates, and DOM interactions within these effect hooks, you ensure that these operations only occur when React is certain that the component's output will indeed be reflected in the user interface [5][7]. Attempts to perform these actions during rendering can lead to bugs where effects observe inconsistent trees or perform redundant, incorrect operations [10][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Panel.tsx imports and hook/body section ---'
sed -n '1,170p' dashboard/web/src/components/Panel.tsx

echo '--- useLastAvailable usages ---'
rg -n "useLastAvailable|type Result|interface Result" dashboard/web/src -S || true

echo '--- repository-wide useLastAvailable/type Result usages ---'
rg -n "useLastAvailable|type Result|interface Result" -S || true

Repository: joshdev8/AutoPlexx

Length of output: 6149


Track last available payloads after commit, not during render.

Assigning lastAvailable.current while the component function runs can leave the ref pointing to data that was produced during a concurrent render cycle that React later discarded. Move this assignment into useLayoutEffect(..., [data]) so it only records available payloads that are part of the committed render.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/web/src/components/Panel.tsx` around lines 118 - 121, Update
useLastAvailable so the lastAvailable ref is assigned only inside a
useLayoutEffect dependent on data, recording data when it is available after the
render commits. Remove the render-time assignment while preserving the existing
T | null return behavior.

}

/** Placeholder while a panel's first request is in flight. */
export function PanelLoading() {
return (
Expand Down Expand Up @@ -98,6 +150,20 @@ export function PanelBody<T extends object>({
empty?: string;
children: (value: T) => ReactNode;
}) {
const lastAvailable = useLastAvailable(data);

const render = (value: T) => {
const rendered = children(value);
if (empty && Array.isArray(rendered) && rendered.length === 0) {
return (
<span className="text-muted" style={{ fontSize: 13 }}>
{empty}
</span>
);
}
return <>{rendered}</>;
};

if (loading && !data) return <PanelLoading />;
// A transport failure leaves `data` null with `loading` false. Reporting that
// as "No response yet" would imply the request is still coming.
Expand All @@ -112,15 +178,24 @@ export function PanelBody<T extends object>({
/>
);
}
if (!data.available) return <PanelEmpty result={data} />;

const rendered = children(data);
if (empty && Array.isArray(rendered) && rendered.length === 0) {
return (
<span className="text-muted" style={{ fontSize: 13 }}>
{empty}
</span>
);
if (!data.available) {
// An upstream blip arrives as a *successful* response carrying
// `available: false`, so `usePolled` can't tell it from real data and
// replaces the last good payload with it. Without this branch a single
// failed poll blanks the panel until the server-side TTL lapses — up to a
// minute for the calendar — which is exactly what "a failed refresh keeps
// the last good data on screen" is supposed to prevent.
if (lastAvailable) {
return (
<>
<PanelStale result={data} />
{render(lastAvailable)}
</>
);
}
return <PanelEmpty result={data} />;
}
return <>{rendered}</>;

return render(data);
}
Loading