* [870467e6] Frontend: page-scoped refresh provider, hook, and navbar button (#347) * [55376b8a] Create page-scoped refresh provider and context (#327) * [55376b8a] feat(panel): add page-scoped refresh context and provider * [55376b8a] docs(frontend): add page-refresh-provider component documentation --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [a0c02d0f] Add public usePageRefresh hook (#332) * [a0c02d0f] test(hooks): assert usePageRefresh is exported from hooks barrel * [a0c02d0f] feat(hooks): add public usePageRefresh hook with provider and tests * [a0c02d0f] fix(panel): move hook test wrappers to components and rename providers.tsx to unshadow barrel * [a0c02d0f] docs(panel): document usePageRefresh hook and PageRefreshProvider API --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Renn F <rennf93@users.noreply.github.com> * [5f28dd9b] Add navbar refresh button and remove inline dashboard refresh buttons (#336) * [5f28dd9b] Align PageRefreshProvider with active hook API and remove inline dashboard refresh buttons * [5f28dd9b] Remove unused scope-keyed PageRefreshProvider, context, and associated tests * [5f28dd9b] Address QA revision: add header refresh tests, page-scoped label, remove dead provider code and .venv symlink, revert formatting-only changes * [5f28dd9b] Remove remaining inline dashboard refresh buttons and committed .venv symlink * [5f28dd9b] docs(frontend): update page-refresh provider docs and panel README for navbar refresh button * [5f28dd9b] fix(panel): remove .venv symlink, ignore root .venv entries, and thin task-detail page data fetch into useTaskDetail hook * [5f28dd9b] Extract GitBrowser data fetching into useGitBrowser hook and add tests; verify .venv cleanup and task-detail thin hook usage * [5f28dd9b] fix(panel): remove root .venv symlink, restore .gitignore anchored rule, and revert lifecycle.json formatting noise * Delete .venv --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Renn F <rennf93@users.noreply.github.com> * [b8e1de1b] Fix navbar refresh button disabled state when registry is empty (#356) (#358) * [b8e1de1b] fix(panel): derive navbar refresh disabled state from registry, not unused prop PageRefreshProvider now computes `disabled` from whether any refresh callback is currently registered (registry size > 0) instead of a static, never-passed `disabled` prop that left the button permanently enabled. header.tsx now destructures `disabled` from usePageRefresh() and disables the button on `disabled || loading`. Updated the tests that asserted the old always-enabled-by-default behavior and added a new header test asserting the button is disabled with zero registered callbacks. * [b8e1de1b] docs(panel): document PageRefreshProvider disabled state derived from registry Updated documentation to reflect the refactored PageRefreshProvider behavior: the `disabled` state is now derived from whether any refresh callbacks are currently registered (empty registry = disabled), rather than a static `disabled` prop. Clarified in both panel/README.md and the full component guide that the navbar refresh button disables when no callbacks are registered and when a refresh cycle is in progress. Updated API documentation to remove the now-removed `disabled` prop from PageRefreshProviderProps and updated code examples and test coverage descriptions to reflect the new callback-driven semantics. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * test(panel): mock usePageRefresh in tests predating the provider Merge-skew: the page-refresh feature makes CommandCenter and the agent detail page call usePageRefresh; three tests merged from master render them without the new provider. Mock the hook module, matching the files' stub-everything style. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Renn F <rennf93@users.noreply.github.com>
8.9 KiB
Page-scoped refresh provider
A React Context + provider that lets global UI chrome (the navbar refresh button) trigger a refresh that is scoped to the page the user is currently viewing.
Purpose
Several dashboard pages fetch their own data through TanStack Query. A global refresh button in the header needs to re-fetch data for the current page without invalidating every other page's cache. PageRefreshProvider maintains a simple callback registry: pages register their own refetch callbacks when mounted, and the navbar button invokes every registered callback when clicked.
This keeps refresh semantics page-local. Only the components that are currently mounted and have registered callbacks participate in a refresh cycle.
Files
| File | Role |
|---|---|
panel/src/components/providers/page-refresh-provider.tsx |
Provider component, context value, and RefreshCallback / PageRefreshState types. |
panel/src/components/providers/index.ts |
Barrel export (PageRefreshProvider, PageRefreshContext, types). |
panel/src/hooks/use-page-refresh.ts |
Public usePageRefresh hook that consumes the context. |
panel/src/components/app-providers.tsx |
Root provider stack; wraps the app in PageRefreshProvider. |
panel/src/components/layout/header.tsx |
Navbar refresh button that calls refresh() and reflects loading. |
API
PageRefreshState
interface PageRefreshState {
disabled: boolean;
loading: boolean;
register: (callback: RefreshCallback) => void;
unregister: (callback: RefreshCallback) => void;
refresh: () => Promise<void>;
}
disabled— whether there are no refresh callbacks currently registered (the registry is empty). When true, clicking refresh is a no-op.loading— whether a refresh cycle is currently running.register(callback)— add a callback to invoke on the next refresh.unregister(callback)— remove a previously registered callback.refresh()— invoke every registered callback concurrently and updateloadinguntil they settle. Does nothing if the registry is empty.
RefreshCallback
type RefreshCallback = () => void | Promise<void>;
May be sync or async; refresh always returns a Promise and awaits async callbacks with Promise.all.
PageRefreshProviderProps
interface PageRefreshProviderProps {
children: React.ReactNode;
}
children— React tree that can consume the context.
The provider does not expose a disabled prop; instead, disabled is derived from whether any refresh callbacks are currently registered (the registry size).
How to consume
Pages and panels that want to expose a refresh action should:
- Import
usePageRefreshfrom@/hooks. - In a
useEffect, register a callback that refetches the page's data. - Unregister the same callback on unmount.
"use client";
import { useEffect } from "react";
import { usePageRefresh } from "@/hooks";
import { useProducts } from "@/hooks/use-products";
export default function ProductsPage() {
const { data: products, error, refetch } = useProducts();
const { register, unregister, refresh } = usePageRefresh();
useEffect(() => {
const cb = () => {
void refetch();
};
register(cb);
return () => unregister(cb);
}, [register, unregister, refetch]);
if (error) {
return <OfflineState onRetry={() => void refresh()} />;
}
return <ProductTable products={products} />;
}
How the navbar button triggers refresh
panel/src/components/layout/header.tsx consumes the same context:
const { refresh, loading, disabled } = usePageRefresh();
<Button
variant="ghost"
size="icon"
onClick={() => void refresh()}
disabled={disabled || loading}
aria-label="Refresh only the current page"
title="Refresh only the current page"
>
<RefreshCw className={cn("h-5 w-5", loading && "animate-spin")} />
</Button>
The button sits between the connection-status badge and the theme toggle. It is disabled when:
- No page has registered a refresh callback (the registry is empty)
- A refresh cycle is currently running
When disabled, the button is grayed out and clicking has no effect. While loading, the icon shows a spinner. Concurrent clicks are coalesced: a second click while a cycle is in progress does nothing.
Pages wired to the navbar refresh
The following routes and dashboard components register refetch callbacks with usePageRefresh. New pages should follow the same pattern.
| Route / component | Registered refetch(es) |
|---|---|
app/(dashboard)/a2a/page.tsx |
refetchConversations, refetchPairs, refetchMessages |
app/(dashboard)/agents/page.tsx |
refetch (orchestrator status) |
app/(dashboard)/agents/[agentId]/page.tsx |
refetch (agent status) |
app/(dashboard)/journals/page.tsx |
refetch (agent list) |
app/(dashboard)/journals/[entryId]/page.tsx |
refetch (journal entry) |
app/(dashboard)/metrics/page.tsx |
refetchTasks, refetchStatus |
app/(dashboard)/notifications/page.tsx |
refetch (notifications list) |
app/(dashboard)/products/page.tsx |
refetch (products) |
app/(dashboard)/projects/page.tsx |
refetch (projects) |
app/(dashboard)/tasks/page.tsx |
refetch (tasks) |
app/(dashboard)/tasks/[taskId]/page.tsx |
refetch (task detail) |
app/(dashboard)/work-sessions/page.tsx |
refetch (work sessions) |
components/auditor/auditor-dashboard.tsx |
refetch (auditor dashboard) |
components/business/pitches-tab.tsx |
refetch (pitches) |
components/dashboard/command-center.tsx |
refetch (CEO overview) |
components/dashboard/release-proposal-card.tsx |
refetch (release proposal) |
components/dashboard/roadmap-review-queue.tsx |
refetch (roadmap cycles) |
components/dashboard/x-post-queue.tsx |
refetch (X post queue) |
components/git/git-browser.tsx |
project/status/log/branches refetches |
components/kanban/core/kanban-board.tsx |
refetch (kanban tasks) |
components/knowledge-base/knowledge-base-browser.tsx |
stats/health refetches |
Design decisions
- Callback-set registry, not query invalidation: the provider stores
Set<RefreshCallback>and lets each page decide how to refresh. This avoids spraying React Query cache invalidations across unrelated pages. - No scope keys: the previous implementation keyed callbacks by a scope string and tracked an "active scope". That was removed because React mount/unmount lifecycle already scopes callbacks to the visible page; extra scope bookkeeping added complexity without benefit.
- React Context over Zustand: the state is transient and tied to the React tree, so Context is the lighter fit.
- Coalesced concurrent refreshes:
refresh()ignores subsequent calls while a cycle is already running, preventing double-refetch and keeping the button disabled honestly. - Provider types live next to the provider: unlike the first implementation, the context value types and
RefreshCallbacktype now live incomponents/providers/page-refresh-provider.tsxand are re-exported bycomponents/providers/index.ts. This matches the current panel boundary that keeps provider primitives together.
Testing
Run the provider-related tests with the panel test suite:
cd panel
pnpm test page-refresh
Covered behaviors:
usePageRefreshthrows when called outside aPageRefreshProvider.- The hook returns
disabled: truewhen nothing is registered, anddisabled: falseonce a callback is registered. - Registering callbacks makes
refresh()invoke them and setsdisabled: false. - Unregistering callbacks prevents the callback from being called and returns
disabled: truewhen the registry is empty. refresh()returns a promise and awaits async callbacks.refresh()is a no-op when the registry is empty (disabled).- The navbar button renders between the connection-status badge and the theme toggle.
- The navbar button exposes an accessible page-scoped label.
- The navbar button is disabled when no callbacks are registered and shows a spinner while a refresh callback is running.
- Concurrent clicks do not start a second refresh cycle.
Related work
- Completed prerequisite: Add public
usePageRefreshhook — consumes this provider. - This task: Add navbar refresh button and remove inline dashboard refresh buttons — wires the header button and removes per-page inline buttons.
Migration / rollout
No consumer migration is needed for end users. For developers adding a new dashboard page:
- Wrap tests for the page in
PageRefreshProviderfrom@/components/providersif they render page-level components that callusePageRefresh. - Register the page's refetch callbacks and unregister them on unmount.
- Do not add a new inline "Refresh" button; use the shared navbar button instead.
Operation-specific refresh controls (for example, a "Reindex" button inside a knowledge-base card or a "Retry" on an OfflineState) are intentionally preserved when their action is local to a sub-component, not a whole-page refresh.