feat: v6.0.0 PWA offline support
- manifest + icones, service worker (precache, network-first, Background Sync)
- module client FlowOffline (IndexedDB, queue, delta, flush) + hook editeur
- endpoints /api/v2/sync/{delta,batch,status} + moteur de sync (conflits LWW/orpheline/copie offline)
- migrations offline_sync_queue + sync_version (triggers)
- UI offline (banner, badge sync, toasts, icone dirty) + doc /help
- tests pytest (sync, migrations, SW, offline) + E2E Playwright; bump 6.0.0
This commit is contained in:
@@ -2350,12 +2350,21 @@ applyAIBlocks(text){
|
||||
if(this.saving){ if(cb) cb(); return; }
|
||||
this.sync();
|
||||
this.saving=true;
|
||||
const blocksArr=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','first_col_header','colsW','meeting','automation_id','automation_name'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;});
|
||||
// v6.0.0 PWA: hors ligne (ou échec réseau) → file IndexedDB rejouée au retour du réseau
|
||||
const queueOffline=()=>{
|
||||
if(!window.FlowOffline){this.saving=false;return;}
|
||||
window.FlowOffline.savePageOffline({id:this.pid,title:this.pageTitle,content:JSON.stringify(blocksArr),content_format:'blocks'})
|
||||
.then(()=>{this.saving=false;this.dirty=false;this.lastSaved='offline';if(cb)cb();})
|
||||
.catch(()=>{this.saving=false;});
|
||||
};
|
||||
if(!navigator.onLine){queueOffline();return;}
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','first_col_header','colsW','meeting','automation_id','automation_name'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
|
||||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:blocksArr})})
|
||||
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
|
||||
this._syncTitleUI();
|
||||
if(cb)cb();
|
||||
}).catch(()=>{this.saving=false;});
|
||||
}).catch(()=>{this.saving=false;queueOffline();});
|
||||
},
|
||||
runAutomationButton(bid){
|
||||
const idx=this.getIdx(bid);if(idx<0)return;
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FlowDeck — {% block title_prefix %}Home{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<meta name="theme-color" content="#191919">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="stylesheet" href="/static/css/app.css?v=5.1.1">
|
||||
<link rel="stylesheet" href="/static/css/design-tokens.css?v=5.2.0">
|
||||
<link rel="stylesheet" href="/static/css/components.css?v=5.2.0">
|
||||
@@ -750,6 +755,11 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ============ PWA SYNC BADGE (v6.0.0) ============ -->
|
||||
<div id="fd-sync-badge" class="fd-sync-badge" style="display:none" role="status" aria-live="polite">
|
||||
<span class="fd-sync-spinner"></span><span>Synchronisation…</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script data-cfasync="false">
|
||||
@@ -2271,5 +2281,55 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# ─── PWA: sync indicators (v6.0.0) ─── #}
|
||||
<style>
|
||||
.fd-sync-badge{position:fixed;top:10px;right:14px;z-index:600;display:flex;align-items:center;gap:8px;
|
||||
background:var(--bg-secondary,#262626);border:1px solid var(--border,rgba(255,255,255,.1));
|
||||
color:var(--text-secondary,#b8b8b8);border-radius:20px;padding:5px 12px;font-size:12px;
|
||||
box-shadow:0 4px 16px rgba(0,0,0,.35);}
|
||||
.fd-sync-spinner{width:12px;height:12px;border:2px solid rgba(255,255,255,.25);
|
||||
border-top-color:var(--accent,#2383E2);border-radius:50%;display:inline-block;
|
||||
animation:fd-spin .8s linear infinite;}
|
||||
@keyframes fd-spin{to{transform:rotate(360deg)}}
|
||||
.sidebar-item.fd-dirty .page-name::after{content:'⟳';margin-left:6px;font-size:11px;
|
||||
color:var(--accent,#2383E2);opacity:.85;}
|
||||
</style>
|
||||
|
||||
{# ─── PWA: offline client module + service worker registration (v6.0.0) ─── #}
|
||||
<script src="/static/js/offline.js?v=6.0.0" defer data-cfasync="false"></script>
|
||||
<script data-cfasync="false">
|
||||
(function() {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/sw.js').then(function(reg) {
|
||||
if ('sync' in reg && reg.sync) {
|
||||
reg.sync.register('sync-flowdeck').catch(function() {});
|
||||
}
|
||||
}).catch(function() {});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# ─── PWA: sync badge + toasts wiring (v6.0.0) ─── #}
|
||||
<script data-cfasync="false">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.FlowOffline) return;
|
||||
window.FlowOffline.onChange(function(s) {
|
||||
var badge = document.getElementById('fd-sync-badge');
|
||||
if (badge) badge.style.display = s.isSyncing ? 'flex' : 'none';
|
||||
});
|
||||
window.addEventListener('flowdeck:sync-done', function(ev) {
|
||||
var d = ev.detail || {};
|
||||
if (d.failed) {
|
||||
if (window.showToast) window.showToast(d.failed + ' modification(s) non synchronisée(s)', 'error');
|
||||
} else if (d.conflicts) {
|
||||
if (window.showToast) window.showToast('Synchronisé — ' + d.conflicts + ' conflit(s) résolu(s)', 'info');
|
||||
} else if (d.synced) {
|
||||
if (window.showToast) window.showToast('Modifications synchronisées', 'success');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
{% from '_icons.html' import fd_icon %}
|
||||
<title>FlowDeck — All-in-one workspace</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<meta name="theme-color" content="#191919">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #191919;
|
||||
@@ -201,5 +206,14 @@
|
||||
FlowDeck v4.0.1 — Open source · Self-hosted · Notion-compatible
|
||||
</footer>
|
||||
|
||||
<script data-cfasync="false">
|
||||
(function() {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -212,7 +212,8 @@
|
||||
.modified-dot.show{display:block;}
|
||||
|
||||
/* Offline banner */
|
||||
.offline-banner{position:fixed;top:0;left:0;right:0;background:var(--danger);color:#fff;text-align:center;padding:6px 12px;font-size:12px;z-index:500;font-weight:500;}
|
||||
.offline-banner{position:fixed;top:0;left:0;right:0;background:var(--danger);color:#fff;text-align:center;padding:6px 12px;font-size:12px;z-index:500;font-weight:500;display:flex;align-items:center;justify-content:center;gap:12px;}
|
||||
.offline-badge{background:rgba(255,255,255,.22);border-radius:10px;padding:1px 8px;font-size:11px;font-weight:600;}
|
||||
|
||||
/* Pinned indicator */
|
||||
.pin-icon{font-size:10px;color:var(--text-tertiary);cursor:pointer;margin-left:4px;opacity:0;transition:opacity 150ms;}
|
||||
@@ -595,6 +596,7 @@ window._wsInitData = (function() {
|
||||
|
||||
// Online/offline
|
||||
online: navigator.onLine,
|
||||
pendingSyncCount: 0,
|
||||
pinnedIds: (function() {
|
||||
try { return JSON.parse(localStorage.getItem('fd_ws_pinned') || '[]'); }
|
||||
catch(e) { return []; }
|
||||
@@ -648,6 +650,13 @@ window._wsInitData = (function() {
|
||||
var self = this;
|
||||
window.addEventListener('online', function() { self.online = true; });
|
||||
window.addEventListener('offline', function() { self.online = false; });
|
||||
// v6.0.0 PWA: reflect offline queue state from the client module
|
||||
if (window.FlowOffline && window.FlowOffline.onChange) {
|
||||
window.FlowOffline.onChange(function(s) {
|
||||
self.online = s.online;
|
||||
self.pendingSyncCount = s.pendingSyncCount;
|
||||
});
|
||||
}
|
||||
// Side-peek editor posts renames here so the tree/peek title stay in sync.
|
||||
window.addEventListener('message', function(ev) {
|
||||
if (ev.origin !== window.location.origin) return;
|
||||
@@ -3015,6 +3024,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<!-- Offline banner -->
|
||||
<div class="offline-banner" x-show="!online" x-transition>
|
||||
<span>{{ fd_icon('alert-triangle',14) }} You are offline. Changes will sync when connection is restored.</span>
|
||||
<span class="offline-badge" x-show="pendingSyncCount > 0" x-text="pendingSyncCount + ' modification(s) en attente'"></span>
|
||||
</div>
|
||||
|
||||
<!-- Move-to modal -->
|
||||
|
||||
Reference in New Issue
Block a user