Skip to content

WIP: Offline usage of Ferdium (fallback when server is down)#2372

Draft
SpecialAro wants to merge 4 commits into
ferdium:developfrom
SpecialAro:offline-usage
Draft

WIP: Offline usage of Ferdium (fallback when server is down)#2372
SpecialAro wants to merge 4 commits into
ferdium:developfrom
SpecialAro:offline-usage

Conversation

@SpecialAro

@SpecialAro SpecialAro commented Mar 28, 2026

Copy link
Copy Markdown
Member

Pre-flight Checklist

Please ensure you've completed all of the following.

Description of Change

This is a WIP of a fallback whenever Ferdium Server (or any other server) is down.

Motivation and Context

Whenever the Server goes down, Ferdium cannot load user and service information, making the app unusable. With this feature, some functionality is preserved (workspace and recipe adding, editing and deleting is disabled). Even though this is not ideal, it is a starting point not to lock users out of using the services.

This fixes #2369 and fixes #1826 and partially addresses #83

Code was so far generated by Codex and rail-guarded by me as I don't have much time to implement this properly.

THIS PR IS NOT YET READY TO MERGE!

Screenshots (WIP)

Checklist

  • My pull request is properly named
  • The changes respect the code style of the project (pnpm prepare-code)
  • pnpm test passes
  • I tested/previewed my changes locally

Release Notes

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.

Pull request overview

Implements a “local backup / offline mode” fallback so the Ferdium app can still load previously synced user/services/workspaces when the configured server is down, while disabling configuration mutations until connectivity is restored.

Changes:

  • Add offline-state snapshot persistence + hydration, plus app-level offline mode state machine (enter/exit, reconnect probing).
  • Disable service/workspace/recipe configuration actions and related UI affordances while in offline mode.
  • Update error/retry UX (InfoBars, sidebar status) and tweak request auto-retry behavior.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/styles/layout.scss Adds styling for an offline/online status element in the sidebar (light/dark themes).
src/stores/ServicesStore.ts Guards service add/edit/delete/reorder actions when offline mode is active.
src/stores/RequestStore.ts Adjusts required-request retry logic and suppresses required-request failure state in offline mode.
src/stores/GlobalErrorStore.ts Updates auth failure handling in offline mode and triggers offline-backup refresh on successful requests.
src/stores/AppStore.ts Introduces offline mode state, backup meta loading, offline activation/hydration, and reconnect checks.
src/helpers/offline-state.ts New helper to load/save/serialize/hydrate the offline snapshot file.
src/features/workspaces/store.ts Disables workspace create/update/delete/reorder actions in offline mode.
src/features/workspaces/containers/WorkspacesScreen.tsx Threads isOfflineMode into workspace dashboard UI.
src/features/workspaces/containers/EditWorkspaceScreen.tsx Threads isOfflineMode into the edit workspace form.
src/features/workspaces/components/WorkspacesDashboard.tsx Hides/blocks workspace creation/edit navigation and shows an offline warning.
src/features/workspaces/components/WorkspaceServiceListItem.tsx Adds a disabled mode for toggling workspace service membership.
src/features/workspaces/components/WorkspaceDrawer.tsx Disables workspace settings entry points and add-new workspace UI in offline mode.
src/features/workspaces/components/EditWorkspaceForm.tsx Disables inputs/actions and shows offline warning when offline mode is active.
src/features/workspaces/components/CreateWorkspaceForm.tsx Adds a disabled state to prevent workspace creation submission.
src/containers/settings/SettingsWindow.tsx Removes unused nested <Outlet /> rendering from Settings portal container.
src/containers/settings/ServicesScreen.tsx Passes offline mode flag down to services settings dashboard.
src/containers/settings/RecipesScreen.tsx Passes offline mode flag down to recipes settings dashboard.
src/containers/settings/EditSettingsScreen.tsx Passes offline mode flag down to settings form.
src/containers/settings/EditServiceScreen.tsx Passes offline mode flag down to edit service form.
src/containers/layout/AppLayoutContainer.tsx Wires offline mode state/actions and health status into AppLayout and Sidebar.
src/containers/auth/AuthLayoutContainer.tsx Wires offline mode entry action/state into auth layout when API is unhealthy.
src/components/ui/imageUpload/index.tsx Adds a disabled state that prevents uploads and disables the delete button.
src/components/settings/settings/EditSettingsForm.tsx Adds an offline-mode warning banner in settings.
src/components/settings/services/ServicesDashboard.tsx Shows offline warning, disables navigation to recipes/add service FAB while offline.
src/components/settings/services/ServiceItem.tsx Adds disabled navigation behavior for service rows.
src/components/settings/services/EditServiceForm.tsx Disables all mutation controls and shows an offline warning when offline.
src/components/settings/recipes/RecipesDashboard.tsx Shows offline warning and disables add-service interactions while offline.
src/components/settings/recipes/RecipeItem.tsx Adds disabled support for recipe teaser buttons.
src/components/settings/SettingsLayout.tsx Minor import formatting adjustment.
src/components/services/tabs/Tabbar.tsx Prevents drag-sort and forwards offline mode state into sortable list.
src/components/services/tabs/TabItem.tsx Disables context menu actions that mutate service state while offline.
src/components/services/tabs/TabBarSortableList.tsx Threads offline mode flag into tab items.
src/components/layout/Sidebar.tsx Adds offline/online status indicator with “switch back online” action when ready; hides recipes button offline.
src/components/layout/AppLayout.tsx Adds “Run in offline mode” CTA to error/unhealthy API InfoBars and new props for API health/offline state.
src/components/auth/AuthLayout.tsx Adds “Run in offline mode” CTA on auth screen when API is unhealthy and a backup is available.
src/actions/app.ts Adds enterOfflineMode / exitOfflineMode actions.
src/@types/stores.types.ts Adds isOfflineMode to AppStore and corrects allRecipesRequest typing to CachedRequest.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/stores/AppStore.ts
Comment on lines +706 to +709
let recipes = this.stores.recipes.all;
if (recipes.length === 0) {
recipes = await this.stores.recipes.allRecipesRequest.execute().promise;
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

_activateOfflineMode awaits this.stores.recipes.allRecipesRequest.execute().promise. allRecipesRequest is a CachedRequest, and its implementation never resolves/rejects the promise on error, so if getInstalledRecipes() throws this await can hang indefinitely and leave isEnteringOfflineMode stuck true. Avoid awaiting a CachedRequest.promise here (e.g., call execute() and read .result/.isError, or update CachedRequest to always settle its promise on error).

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
export function saveOfflineState(snapshot: OfflineStateSnapshot): void {
try {
ensureDirSync(userDataPath('config'));
writeJsonSync(BACKUP_FILE, snapshot, { spaces: 2 });
} catch (error) {
console.warn('Could not write offline backup state', error);
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

saveOfflineState uses ensureDirSync/writeJsonSync, which perform blocking filesystem I/O on the renderer thread. With the current integration this can run often, so it risks noticeable UI stutters. Prefer the async fs-extra variants (or move the persistence to the main process via IPC) and consider debouncing writes to reduce churn.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +60
updatedAt: new Date().toISOString(),
user: {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
organization: user.organization,
accountType: user.accountType,
beta: user.beta,
locale: user.locale,
isSubscriptionOwner: user.isSubscriptionOwner,
team: user.team,
},

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

The offline snapshot writes personally identifiable info (e.g., email, firstname/lastname, organization, team data) to offline-state.json in plaintext. Please consider minimizing the persisted fields to only what the offline UI strictly needs, and/or encrypting the snapshot (e.g., using OS keychain/credential store) to reduce local data exposure risk.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +122
export function serializeOfflineState({
user,
services,
workspaces,
}: {
user: Record<string, any>;
services: ServiceModel[];
workspaces: Workspace[];
}): OfflineStateSnapshot {
return {
updatedAt: new Date().toISOString(),
user: {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
organization: user.organization,
accountType: user.accountType,
beta: user.beta,
locale: user.locale,
isSubscriptionOwner: user.isSubscriptionOwner,
team: user.team,
},
services: services.map(service => ({
id: service.id,
recipeId: service.recipe.id,
name: service.name,
team: service.team,
customUrl: service.customUrl,
iconUrl: service.iconUrl,
order: service.order,
isEnabled: service.isEnabled,
isNotificationEnabled: service.isNotificationEnabled,
isBadgeEnabled: service.isBadgeEnabled,
isMediaBadgeEnabled: service.isMediaBadgeEnabled,
trapLinkClicks: service.trapLinkClicks,
isIndirectMessageBadgeEnabled: service.isIndirectMessageBadgeEnabled,
isMuted: service.isMuted,
isDarkModeEnabled: service.isDarkModeEnabled,
darkReaderSettings: service.darkReaderSettings,
isProgressbarEnabled: service.isProgressbarEnabled,
onlyShowFavoritesInUnreadCount: service.onlyShowFavoritesInUnreadCount,
spellcheckerLanguage: service.spellcheckerLanguage,
userAgentPref: service.userAgentPref,
isHibernationEnabled: service.isHibernationEnabled,
isWakeUpEnabled: service.isWakeUpEnabled,
useFavicon: service.useFavicon,
iconId: service.hasCustomUploadedIcon ? 'offline-backup' : '',
})),
workspaces: workspaces.map(workspace => ({
id: workspace.id,
name: workspace.name,
order: workspace.order,
services: [...workspace.services],
userId: workspace.userId,
})),
};
}

export function hydrateOfflineState(
snapshot: OfflineStateSnapshot,
recipes: any[],
) {
const services = snapshot.services
.map(service => {
const recipe = recipes.find(r => r.id === service.recipeId);
if (!recipe) {
return null;
}

try {
return new ServiceModel(service, recipe);
} catch (error) {
console.warn('Could not hydrate offline service', service.id, error);
return null;
}
})
.filter(Boolean);

return {
user: new UserModel(snapshot.user as any),
services,
workspaces: snapshot.workspaces.map(workspace => new Workspace(workspace)),
};
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

This new helper contains non-trivial serialization/hydration logic that will be security/compatibility sensitive over time (e.g., handling missing recipes, schema evolution). The repo already has Jest coverage for other helpers, so adding unit tests for serializeOfflineState/hydrateOfflineState (round-trip, missing recipe handling, corrupt snapshot handling) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +116 to 118
appStore.authRequestFailed = false;
appStore.refreshOfflineBackup();
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

_handleRequests calls appStore.refreshOfflineBackup() for every successful Request. Since this hook runs for all requests (including frequent ones like healthCheckRequest), this can trigger repeated synchronous disk writes (see saveOfflineState), causing UI jank and unnecessary I/O. Consider only refreshing the backup after the specific “source-of-truth” requests succeed (user info/services/workspaces), and debounce/throttle the backup write (e.g., at most once every N minutes or when the serialized snapshot actually changes).

Copilot uses AI. Check for mistakes.
@SpecialAro SpecialAro marked this pull request as draft March 28, 2026 16:39
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.

Automatic service configuration backup / "Offline" mode [Feature Request] Offline PWA support.

2 participants