Saltar a contenido

📱 PWA & Mobile Optimizations

Overview

LibreFolio is installable as a Progressive Web App (PWA). This page documents the technical implementation for developers.


Architecture

┌─────────────────────────────────────────────────┐
│ app.html                                         │
│  ├── viewport meta (no-zoom, viewport-fit)       │
│  ├── manifest link                               │
│  ├── Apple meta tags                             │
│  ├── theme-color                                 │
│  ├── early beforeinstallprompt capture           │
│  └── Service Worker registration                 │
├─────────────────────────────────────────────────┤
│ static/manifest.json                             │
│  └── display: standalone, icons, colors          │
├─────────────────────────────────────────────────┤
│ static/sw.js                                     │
│  └── offline fallback (navigate fail → cache)    │
├─────────────────────────────────────────────────┤
│ static/offline.html                              │
│  └── branded fallback page (i18n, dark mode)     │
├─────────────────────────────────────────────────┤
│ static/icons/                                    │
│  ├── icon-192.png (generated by dev.py)          │
│  └── icon-512.png (generated by dev.py)          │
├─────────────────────────────────────────────────┤
│ HelpMenu.svelte                                  │
│  └── Install App button + platform detection     │
├─────────────────────────────────────────────────┤
│ app.css                                          │
│  └── Mobile CSS (overscroll, touch-action, etc)  │
└─────────────────────────────────────────────────┘

Key Files

File Purpose
frontend/src/app.html Viewport, manifest link, Apple meta tags, early prompt capture, SW registration
frontend/src/app.css Mobile CSS optimizations
frontend/static/manifest.json PWA manifest (display, icons, colors)
frontend/static/sw.js Service Worker (offline fallback only)
frontend/static/offline.html Offline fallback page (static, i18n, dark mode)
frontend/static/icons/icon-{192,512}.png App icons (generated)
frontend/src/lib/components/layout/HelpMenu.svelte Install button logic
dev.pygenerate_pwa_icons() Icon generation from logo_square.png

Mobile CSS

Applied globally via app.css:

html {
    overscroll-behavior-x: none;          /* Disable swipe-back on Android */
}

body {
    overscroll-behavior: contain;          /* No pull-to-refresh */
    -webkit-tap-highlight-color: transparent; /* No blue flash on tap */
}

/* Prevent iOS auto-zoom on input focus */
@media (max-width: 768px) {
    input, select, textarea {
        font-size: 16px !important;
    }
}

/* Disable double-tap zoom on interactive elements */
a, button, input, select, textarea, [role="button"] {
    touch-action: manipulation;
}

The viewport meta in app.html:

<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" name="viewport" />


PWA Manifest

{
    "name": "LibreFolio",
    "short_name": "LibreFolio",
    "display": "standalone",
    "theme_color": "#1a4031",
    "background_color": "#f5f4ef",
    "icons": [
        { "src": "/icons/icon-192.png", "sizes": "192x192", "purpose": "any" },
        { "src": "/icons/icon-512.png", "sizes": "512x512", "purpose": "any" },
        { "src": "/icons/icon-512.png", "sizes": "512x512", "purpose": "maskable" }
    ]
}

Icon Generation

Icons are generated via dev.py using PIL:

def generate_pwa_icons():
    """Generate PWA icons (192x192, 512x512) from logo_square.png with white padding."""
    # Source: frontend/static/logo_square.png (944×944 RGBA)
    # Adds ~12% white padding around the logo before resize
    # Output: frontend/static/icons/icon-{192,512}.png

Run with: ./dev.py docs copy-assets (called during build pipeline).

The 12% padding ensures the logo looks good when OS applies circular/squircle masks.


Install Button Logic

The HelpMenu.svelte component handles three platform scenarios:

1. Chrome/Edge/Android (beforeinstallprompt)

app.html (early) → captures event on window.__pwaInstallPrompt
HelpMenu onMount → reads window.__pwaInstallPrompt as fallback
User clicks "Install App" → calls deferredPrompt.prompt()
Chrome shows native install banner

Race Condition

beforeinstallprompt can fire BEFORE Svelte components mount. That's why we capture it early in app.html on window.__pwaInstallPrompt, then read it in onMount.

2. iOS (Safari only)

  • Detected via user agent: /iPad|iPhone|iPod/ or MacIntel + maxTouchPoints > 1
  • Shows localized instructions: "Tap Share → Add to Home Screen"
  • No programmatic install possible on iOS

3. Desktop Fallback

  • If deferredPrompt not captured (e.g. Firefox, or event missed)
  • Shows hint: "Look for ⊕ in address bar"

Standalone Detection

isStandalone = window.matchMedia('(display-mode: standalone)').matches
    || (window.navigator as any).standalone === true; // iOS property

When isStandalone === true, the Install button is hidden entirely.


Service Worker (Offline Fallback)

LibreFolio uses a minimal Service Worker for offline fallback only — it does NOT cache app assets.

How It Works

User opens PWA → browser checks sw.js for updates (byte-diff)
SW install event → pre-caches /offline.html (~5KB)
User navigates → SW intercepts (mode === 'navigate')
fetch(request) succeeds → normal response (no caching)
fetch(request) fails → serve cached /offline.html

Key Design Decisions

Decision Rationale
No app caching Updates are instant — no stale cache issues
Only navigate intercept API calls, CSS, JS pass through untouched
skipWaiting() + clients.claim() New SW activates immediately
Cache versioning (offline-v1) Increment to force offline.html refresh
Auto-retry (10s interval) Page reloads automatically when server returns

Files

File Purpose
frontend/static/sw.js Service Worker (~25 lines)
frontend/static/offline.html Fallback page (static, inline styles, i18n)

Offline Page Features

  • Inline CSS (no external dependencies — works without network)
  • Dark mode via prefers-color-scheme media query
  • i18n: detects navigator.language → EN/IT/FR/ES
  • Auto-retry: pings server every 10s, reloads on success
  • LibreFolio branding (colors, leaf icon)

Updating the Offline Page

  1. Edit frontend/static/offline.html
  2. Increment cache version in sw.js: offline-v1offline-v2
  3. Deploy — browser auto-updates SW on next PWA open

iOS Notes

  • Service Workers supported since iOS 11.3 (2018)
  • Cache eviction possible after 7 days of non-use — irrelevant here (1 file, re-cached on next visit)
  • All iOS browsers use WebKit → same SW behavior everywhere

HTTP vs HTTPS Behavior

Feature HTTPS HTTP localhost HTTP LAN
Manifest loaded
display: standalone
Service Worker
beforeinstallprompt
Auto-install banner
Manual "Add to Home" ✅ (Android)
iOS Add to Home

Future: Share Target (File Receiving)

Not Implemented

This section documents a potential future feature.

The Web Share Target API would allow LibreFolio to appear in the OS "Share" menu when sharing files (e.g., broker CSV exports).

Requirements: - Service Worker (to handle the POST request) - HTTPS mandatory - Works on: Android Chrome ✅, iOS ❌ (Apple doesn't support share_target)

iOS Alternative: iOS Shortcuts automation — user creates a Shortcut that accepts files from Share Sheet and POSTs them to the LibreFolio API (POST /api/v1/files/upload). See user docs for setup guide.


Updating the PWA

Updates flow automatically:

  1. Admin updates Docker container (new sw.js and/or app code)
  2. User reopens the installed PWA
  3. Browser detects sw.js changed (byte-diff) → installs new SW
  4. New SW activates → pre-caches updated offline.html
  5. App loads fresh assets from server (no caching layer)

No manual "update app" action needed. If offline.html content changes, increment the cache version in sw.js (offline-v1offline-v2) to force a refresh.