Files
roboco/docs/frontend/components/page-refresh-provider.md
T
08c02e2251 [79d686f0] Add page-scoped refresh button to the navbar (#351)
* [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>
2026-07-09 05:27:14 +02:00

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 update loading until 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:

  1. Import usePageRefresh from @/hooks.
  2. In a useEffect, register a callback that refetches the page's data.
  3. 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 RefreshCallback type now live in components/providers/page-refresh-provider.tsx and are re-exported by components/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:

  • usePageRefresh throws when called outside a PageRefreshProvider.
  • The hook returns disabled: true when nothing is registered, and disabled: false once a callback is registered.
  • Registering callbacks makes refresh() invoke them and sets disabled: false.
  • Unregistering callbacks prevents the callback from being called and returns disabled: true when 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.
  • Completed prerequisite: Add public usePageRefresh hook — 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:

  1. Wrap tests for the page in PageRefreshProvider from @/components/providers if they render page-level components that call usePageRefresh.
  2. Register the page's refetch callbacks and unregister them on unmount.
  3. 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.