Skip to content

fix(dashboard): keep service rows during refetch (#666) - #684

Merged
Hydralerne merged 2 commits into
oblien:mainfrom
Rish-it:fix/666-service-skeleton-flash
Aug 21, 2026
Merged

fix(dashboard): keep service rows during refetch (#666)#684
Hydralerne merged 2 commits into
oblien:mainfrom
Rish-it:fix/666-service-skeleton-flash

Conversation

@Rish-it

@Rish-it Rish-it commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #666

Problem

The services tab collapsed into its skeleton pulse-cards on every action (create service, launch, drift fix) and every tab switch, then popped back when the response landed. Reported as "poor state update… annoying not smooth".

Root cause

Two loading flags gated the entire tab (ServicesTab.tsx):

const loading = servicesData.isLoading || containersLoading;
  • refreshServices() set isLoading: true unconditionally on every refetch — and its error path wiped the list to [], blanking the tab on transient failures.
  • containersLoading flipped to true at the top of every fetchData() call and on every remount of the tab (the tab is conditionally rendered per activeTab, so switching back to Services re-skeletoned even with cached data).

The codebase already solved this exact class of bug for project info — begin-fetch-state.ts documents how "every invalidation unmounted the page's whole subtree" and fixed it with stale-while-revalidate. Services/containers never got that treatment.

Fix

Port the same semantics:

  • refreshServices reports isLoading only when the list is empty (nothing on screen to keep), and keeps the previous list when a refetch fails.
  • The tab's skeleton gate requires emptiness: servicesData.isLoading || (containersLoading && services.length === 0). Rows render fine without container data — status falls back per service (ct?.status ?? …) — so container chips simply populate when the read lands (~20ms locally).

Consumers of servicesData.isLoading (LogsSettings, OverviewTab, DomainSettings) inherit the calmer semantics automatically: stale data stays visible during revalidation instead of flashing placeholders.

Verification

  • tsc --noEmit clean
  • dashboard suite: 87 files / 1046 tests passing
  • Manual: services list persists through create/launch/drift actions and tab switches; first cold load still shows the skeleton; failed refetch keeps rows + shows error state

The services tab collapsed into its skeleton pulse-cards on every
action and every tab switch. Two loading flags gated the whole tab
and both flipped to true on every refetch:

- refreshServices() set isLoading: true unconditionally, and its
  error path wiped the list to [] on transient failures.
- ServicesTab OR-ed containersLoading (which flips on every fetchData
  call and every remount of the tab) straight into its skeleton gate.

Fix: stale-while-revalidate semantics, matching what useEndpoint/
beginFetchState already do for project info.

- refreshServices reports loading only when the list is empty and
  keeps the previous list when a refetch fails.
- The tab renders its skeleton only when there is nothing to show;
  service rows render without container data (status falls back per
  service) while the containers read is in flight.

Consumers of servicesData.isLoading (LogsSettings, OverviewTab,
DomainSettings) inherit the calmer semantics: stale data stays on
screen during revalidation instead of flashing placeholders.

Fixes oblien#666
Copilot AI lite review requested due to automatic review settings August 21, 2026 19:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Review follow-up on oblien#684 — port the documented beginFetchState rule
instead of sniffing loading off the data, and pair the tab's error
gate so kept rows are actually visible.

- New pure module src/context/services-fetch-state.ts: loading is
  loadedId !== id, not a property of the list. Same id is a REFRESH
  (keep rows, no skeleton); different id is a NAVIGATION (nothing of
  this project's to show — calling it loaded would render project A's
  services under project B).
- refreshServices tracks servicesLoadedIdRef; both failure paths
  (success:false and throw) collapse into one symmetric fail() that
  keeps rows only for a same-id refresh.
- ServicesTab: full-tab error card only when nothing is showable;
  failed refetch with rows keeps them and reports inline with retry.
  Fixes empty-project flash too: creating the first service no longer
  re-skeletons, since loadedId already matches.
- Tests: 7 cases pinning refresh/navigation/first-load/failure —
  fails if anyone re-reads loading off the data again.
@Hydralerne

Copy link
Copy Markdown
Member

thank you bro

@Hydralerne
Hydralerne merged commit c592d73 into oblien:main Aug 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

service page had poor state update, showing the skelton for every action which is annoying not smoth

3 participants