mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-17 11:02:06 +00:00
fix(backup): pause revoked backups and tell users to reconnect
Google answers a revoked or expired refresh token with invalid_grant. Every save then retried the dead token, and reconnecting never cleared the old error, so it kept showing on Backup Settings afterwards. - The Google Drive and Dropbox OAuth callbacks clear lastError when a provider is reconnected. - An invalid_grant, or a missing refresh token, now pauses the integration with a readable "reconnect" message instead of retrying it on every catch update. Other failures still retry as before. - A banner on every page and an alert on the Pokédex page point to Backup Settings, which shows a "Reconnect needed" badge. The Pokédex page re-checks backup status after each export, because saving a catch record also exports on the server and may pause a provider first. - Offline sync status and the "Save all artwork" link move from every page to a new /offline-guide page, linked from the user menu and the home and welcome pages. Only the offline read-only banner stays sitewide. - Unit tests cover every export path and the backup status store. BDD covers revocation and reconnecting for both providers, and the offline guide. The mock provider can now reject token refreshes, and mock control calls fail loudly if a stale mock is reused. Coverage thresholds are raised to the new baseline.
This commit is contained in:
+28
-39
@@ -6,14 +6,14 @@
|
||||
import SignIn from '$lib/components/SignIn.svelte';
|
||||
import SignOut from '$lib/components/SignOut.svelte';
|
||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { claimOfflineData, requestOfflineSync, startOfflineSync } from '$lib/stores/offlineSync';
|
||||
import {
|
||||
artworkDownloadStatus,
|
||||
claimOfflineData,
|
||||
downloadAllArtwork,
|
||||
offlineSyncStatus,
|
||||
requestOfflineSync,
|
||||
startOfflineSync
|
||||
} from '$lib/stores/offlineSync';
|
||||
PROVIDER_LABELS,
|
||||
backupsNeedingReconnect,
|
||||
clearBackupStatus,
|
||||
refreshBackupStatus
|
||||
} from '$lib/stores/backupStatus';
|
||||
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
|
||||
@@ -57,7 +57,10 @@
|
||||
updateOnlineState();
|
||||
void getUser()
|
||||
.then(async () => {
|
||||
if (localUser) await claimOfflineData(localUser.id);
|
||||
if (localUser) {
|
||||
void refreshBackupStatus();
|
||||
await claimOfflineData(localUser.id);
|
||||
}
|
||||
stopOfflineSync = startOfflineSync(() => localUser?.id ?? null);
|
||||
})
|
||||
.catch((error) => console.error('Unable to claim offline data', error));
|
||||
@@ -75,12 +78,14 @@
|
||||
void claimOfflineData(session.user.id)
|
||||
.then(requestOfflineSync)
|
||||
.catch((error) => console.error('Unable to claim offline data', error));
|
||||
void refreshBackupStatus();
|
||||
}
|
||||
} else {
|
||||
// Offline data is only cleared by the Sign Out button (or another account claiming it).
|
||||
// An expired or rejected session must not throw away artwork that would then have to be
|
||||
// downloaded again after signing back in.
|
||||
localUser = null;
|
||||
clearBackupStatus();
|
||||
}
|
||||
user.set(localUser);
|
||||
});
|
||||
@@ -98,10 +103,7 @@
|
||||
};
|
||||
});
|
||||
|
||||
function formatMegabytes(bytes: number) {
|
||||
const megabytes = bytes / 1048576;
|
||||
return megabytes < 1 ? '<1 MB' : `≈${Math.round(megabytes)} MB`;
|
||||
}
|
||||
$: reconnectLabels = $backupsNeedingReconnect.map((provider) => PROVIDER_LABELS[provider]);
|
||||
|
||||
async function getUser() {
|
||||
const {
|
||||
@@ -192,6 +194,9 @@
|
||||
<li>
|
||||
<a href="/backup-settings"> Backup Settings </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/offline-guide"> Using Offline </a>
|
||||
</li>
|
||||
<li>
|
||||
<SignOut
|
||||
{supabase}
|
||||
@@ -222,34 +227,18 @@
|
||||
<div class="alert rounded-none" role="status">
|
||||
<span>Offline read-only mode: saved data remains available, but changes are disabled.</span>
|
||||
</div>
|
||||
{:else if localUser && $offlineSyncStatus.state === 'error'}
|
||||
<div class="alert alert-warning rounded-none" role="status">
|
||||
<span>Offline copy could not be refreshed: {$offlineSyncStatus.message}</span>
|
||||
<button class="btn btn-sm" on:click={requestOfflineSync}>Retry</button>
|
||||
</div>
|
||||
{:else if localUser && $artworkDownloadStatus.state === 'error'}
|
||||
<div class="alert alert-warning rounded-none" role="status">
|
||||
<span
|
||||
>Offline data is saved, but artwork could not be saved: {$artworkDownloadStatus.message}.</span
|
||||
>
|
||||
<button class="btn btn-sm" on:click={downloadAllArtwork}>Retry</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isOnline && localUser && $offlineSyncStatus.state === 'syncing'}
|
||||
<p class="bg-base-200 px-4 py-1 text-center text-xs" role="status">Updating offline copy…</p>
|
||||
{:else if isOnline && localUser && $offlineSyncStatus.generatedAt}
|
||||
<p class="bg-base-200 px-4 py-1 text-center text-xs" role="status">
|
||||
Offline copy updated {new Date($offlineSyncStatus.generatedAt).toLocaleString()}.
|
||||
{#if $artworkDownloadStatus.state === 'downloading'}
|
||||
Saving all artwork for offline…
|
||||
{:else if $artworkDownloadStatus.state === 'missing'}
|
||||
<button class="link" on:click={downloadAllArtwork}>
|
||||
Save all artwork for offline{#if $artworkDownloadStatus.missingBytes}{' '}({formatMegabytes(
|
||||
$artworkDownloadStatus.missingBytes
|
||||
)}){/if}
|
||||
</button>
|
||||
{/if}
|
||||
</p>
|
||||
{#if localUser && reconnectLabels.length > 0 && $page.url.pathname !== '/backup-settings'}
|
||||
<div
|
||||
class="alert alert-warning rounded-none"
|
||||
role="alert"
|
||||
data-testid="backup-reconnect-banner"
|
||||
>
|
||||
<span>
|
||||
Your {reconnectLabels.join(' and ')} backup has stopped because access expired or was revoked.
|
||||
</span>
|
||||
<a class="btn btn-sm" href="/backup-settings">Reconnect</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<main class="flex-grow">
|
||||
|
||||
Reference in New Issue
Block a user