feat(offline): hoist sync UI to homepage, slow auto-sync to weekly

Move OfflineSyncIndicator (logo pip) and OfflineSyncBanner from the
[recipeLang] layout/page to (main)/+layout.svelte and (main)/+page.svelte.
Sync is an app-wide concern, not recipe-specific, and surfacing it on the
homepage gives the entry point users actually see when they install the
PWA. Indicator pulls language from languageStore since (main) doesn't
have data.lang from a recipe-scoped load.

Drop the now-unused .banner-wrap CSS and OfflineSyncIndicator/Banner
imports from the recipe routes.

Auto-sync cadence:
- AUTO_SYNC_INTERVAL 30 min -> 1 week. Recipes don't change often enough
  to justify a half-hourly background download (the user explicitly
  wanted this dialed back).
- Internal poll tick 5 min -> 1 hour. Polling 12x an hour for a weekly
  event is wasted work; hourly is fine and still responsive when the
  weekly window opens.

Bump 1.65.3 -> 1.66.0.
This commit is contained in:
2026-05-04 22:21:16 +02:00
parent 0372c50084
commit 585c03a11e
6 changed files with 27 additions and 41 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.65.3",
"version": "1.66.0",
"private": true,
"type": "module",
"scripts": {
+5 -4
View File
@@ -2,7 +2,7 @@ import { browser } from '$app/environment';
import { isOfflineDataAvailable, getLastSync, clearOfflineData } from '$lib/offline/db';
import { downloadAllRecipes, type SyncResult, type SyncProgress } from '$lib/offline/sync';
const AUTO_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes
const AUTO_SYNC_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 week
const LAST_SYNC_KEY = 'bocken-last-sync-time';
type PWAState = {
@@ -152,12 +152,13 @@ function createPWAStore() {
startAutoSync() {
if (autoSyncInterval) return; // Already running
// Check every 5 minutes if we should sync
// Check hourly if we should sync — actual sync only fires once
// AUTO_SYNC_INTERVAL has elapsed since the last sync.
autoSyncInterval = setInterval(() => {
autoSync();
}, 5 * 60 * 1000); // Check every 5 minutes
}, 60 * 60 * 1000);
console.log('[PWA] Auto-sync enabled (every 30 minutes)');
console.log('[PWA] Auto-sync enabled (weekly)');
},
stopAutoSync() {
+18
View File
@@ -2,6 +2,8 @@
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import OfflineSyncIndicator from '$lib/components/OfflineSyncIndicator.svelte';
import { languageStore } from '$lib/stores/language.svelte';
let { data, children } = $props();
let user = $derived(data.session?.user);
@@ -16,9 +18,25 @@ let user = $derived(data.session?.user);
<LanguageSelector />
{/snippet}
{#snippet logo_overlay()}
<div class="logo-pip">
<OfflineSyncIndicator lang={languageStore.value} />
</div>
{/snippet}
{#snippet right_side()}
<UserHeader {user}></UserHeader>
{/snippet}
{@render children()}
</Header>
<style>
:global(.logo-pip) {
position: absolute;
top: -8px;
right: -7px;
z-index: 2;
pointer-events: auto;
}
</style>
+3
View File
@@ -2,6 +2,7 @@
import { resolve } from '$app/paths';
import LinksGrid from "$lib/components/LinksGrid.svelte";
import Seo from '$lib/components/Seo.svelte';
import OfflineSyncBanner from '$lib/components/OfflineSyncBanner.svelte';
import { onMount } from 'svelte';
let { data } = $props();
@@ -143,6 +144,8 @@ section h2{
</section>
{/if}
<OfflineSyncBanner {lang} />
<section>
<h2>{labels.pages}</h2>
@@ -45,7 +45,6 @@ onNavigate((navigation) => {
});
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import OfflineSyncIndicator from '$lib/components/OfflineSyncIndicator.svelte';
import BookOpen from '@lucide/svelte/icons/book-open';
import Heart from '@lucide/svelte/icons/heart';
import Leaf from '@lucide/svelte/icons/leaf';
@@ -134,12 +133,6 @@ const recipeCanonicalPath = $derived(recipeAltPath(page.url.pathname, /** @type
<LanguageSelector lang={data.lang} />
{/snippet}
{#snippet logo_overlay()}
<div class="logo-pip">
<OfflineSyncIndicator lang={data.lang} />
</div>
{/snippet}
{#snippet right_side()}
<UserHeader {user} recipeLang={data.recipeLang} lang={data.lang}></UserHeader>
{/snippet}
@@ -147,12 +140,3 @@ const recipeCanonicalPath = $derived(recipeAltPath(page.url.pathname, /** @type
{@render children()}
</Header>
<style>
:global(.logo-pip) {
position: absolute;
top: -8px;
right: -7px;
z-index: 2;
pointer-events: auto;
}
</style>
@@ -4,7 +4,6 @@
import AddButton from '$lib/components/AddButton.svelte';
import Seo from '$lib/components/Seo.svelte';
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
import OfflineSyncBanner from '$lib/components/OfflineSyncBanner.svelte';
import Search from '$lib/components/recipes/Search.svelte';
import { getCategories } from '$lib/js/categories';
import { m, type RecipesLang } from '$lib/js/recipesI18n';
@@ -315,19 +314,6 @@
z-index: 10;
}
/* ─── Offline sync banner — between search and recipe grid ─── */
.banner-wrap {
max-width: 1200px;
margin: 0 auto;
padding: 0 2em;
position: relative;
z-index: 9;
}
.banner-wrap.fallback {
padding: 0 2em;
margin: 0 auto;
}
.sentinel {
height: 1px;
}
@@ -442,9 +428,6 @@
<div class="hero-search-wrap">
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
</div>
<div class="banner-wrap">
<OfflineSyncBanner lang={data.lang} />
</div>
<div class="recipe-grid">
{#each visibleRecipes as recipe, i (recipe._id)}
<CompactCard
@@ -472,9 +455,6 @@
<h1>{labels.title}</h1>
<p class="subheading">{labels.subheading}</p>
</div>
<div class="banner-wrap fallback">
<OfflineSyncBanner lang={data.lang} />
</div>
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
<div class="recipe-grid">