+
bluetooth_searching
Not connected Pair your sign
@@ -244,12 +243,13 @@
-
+
@@ -298,7 +298,7 @@
diff --git a/js/app.js b/js/app.js
index e98b2cb..56a9502 100644
--- a/js/app.js
+++ b/js/app.js
@@ -1190,6 +1190,7 @@ setupPwaInstall({
bannerTitle: document.getElementById('install-banner-title'),
bannerSubtitle: document.getElementById('install-banner-subtitle'),
btnInstall: document.getElementById('btn-install'),
+ btnInstallHelp: document.getElementById('btn-install-help'),
btnDismiss: document.getElementById('btn-dismiss-install'),
settingsBtn: document.getElementById('btn-settings-install'),
modal: document.getElementById('install-modal'),
diff --git a/js/color-picker.js b/js/color-picker.js
index 802972d..27d96a5 100644
--- a/js/color-picker.js
+++ b/js/color-picker.js
@@ -379,8 +379,8 @@ export function createColorPicker({ root, initialHex = '#ff0000', onColorChange,
suppressEvents = true;
setHsl(hexToHsl(n), { commit: false });
suppressEvents = false;
- onColorChange?.(currentHex);
if (commit) {
+ onColorChange?.(currentHex);
pushRecent(currentHex);
recent = loadRecent();
renderRecent();
diff --git a/js/pwa-install.js b/js/pwa-install.js
index 2e44d7e..0b993a2 100644
--- a/js/pwa-install.js
+++ b/js/pwa-install.js
@@ -1,8 +1,7 @@
-/** Health Hub-style PWA install: native prompt() only when Chromium offers it. */
+/** Native PWA install with honest manual and already-installed fallbacks. */
-export const DISMISS_KEY = 'floro_install_dismissed_v5';
+export const DISMISS_KEY = 'floro_install_dismissed_v6';
const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000;
-const ANDROID_FALLBACK_DELAY_MS = 2500;
export function isStandalone() {
return (
@@ -44,12 +43,27 @@ function shouldSuppressBanner() {
return isStandalone() || isDismissed();
}
-function installHelpText(hasNativePrompt) {
+async function detectInstalledPwa() {
+ if (isStandalone()) return true;
+ if (!('getInstalledRelatedApps' in navigator)) return false;
+
+ try {
+ const apps = await navigator.getInstalledRelatedApps();
+ return apps.some((app) => app.platform === 'webapp');
+ } catch {
+ return false;
+ }
+}
+
+function installHelpText(hasNativePrompt, installedPwa) {
+ if (installedPwa) {
+ return 'FloRo Sign is installed. Find it in your Android app drawer; Chrome may not add a Home screen icon.';
+ }
if (isIOS() && !hasNativePrompt) {
return 'Open this page in Safari, tap Share, then Add to Home Screen.';
}
if (isAndroid() && !hasNativePrompt) {
- return 'Stay on this page in Chrome. When Chrome offers install, use that. A bookmark keeps the browser bar.';
+ return 'Chrome has not offered one-tap install yet. You can still install from the Chrome menu.';
}
return 'Install FloRo so it opens full screen, not as a browser tab.';
}
@@ -59,6 +73,7 @@ export function setupPwaInstall({
bannerTitle,
bannerSubtitle,
btnInstall,
+ btnInstallHelp,
btnDismiss,
settingsBtn,
modal,
@@ -70,40 +85,42 @@ export function setupPwaInstall({
log,
}) {
let deferredPrompt = window.__floroDeferredInstall || null;
- let androidFallbackTimer = null;
+ let installedPwa = isStandalone();
function showBanner() {
if (!bannerEl || shouldSuppressBanner()) return;
bannerEl.classList.remove('hidden');
bannerEl.setAttribute('aria-hidden', 'false');
- document.querySelector('.app-shell')?.classList.add('install-banner-visible');
syncBanner();
}
function hideBanner() {
bannerEl?.classList.add('hidden');
bannerEl?.setAttribute('aria-hidden', 'true');
- document.querySelector('.app-shell')?.classList.remove('install-banner-visible');
}
function syncBanner() {
const hasNativePrompt = Boolean(deferredPrompt);
- if (bannerTitle) bannerTitle.textContent = 'Install FloRo';
- if (bannerSubtitle) bannerSubtitle.textContent = installHelpText(hasNativePrompt);
+ if (bannerTitle) {
+ bannerTitle.textContent = installedPwa ? 'FloRo Sign is installed' : 'Install FloRo Sign';
+ }
+ if (bannerSubtitle) {
+ bannerSubtitle.textContent = installHelpText(hasNativePrompt, installedPwa);
+ }
if (btnInstall) {
btnInstall.textContent = 'Install';
- btnInstall.classList.toggle('hidden', !hasNativePrompt);
+ btnInstall.classList.toggle('hidden', !hasNativePrompt || installedPwa);
+ }
+ if (btnInstallHelp) {
+ btnInstallHelp.textContent = installedPwa ? 'Find app' : 'Install help';
+ btnInstallHelp.classList.toggle(
+ 'hidden',
+ hasNativePrompt || (!installedPwa && !isAndroid() && !isIOS())
+ );
}
btnDismiss?.classList.remove('hidden');
}
- function scheduleAndroidFallback() {
- if (shouldSuppressBanner() || !isAndroid() || deferredPrompt) return;
- androidFallbackTimer = window.setTimeout(() => {
- if (!deferredPrompt && !shouldSuppressBanner()) showBanner();
- }, ANDROID_FALLBACK_DELAY_MS);
- }
-
function renderModalSteps(items) {
if (!modalSteps) return;
modalSteps.innerHTML = '';
@@ -120,6 +137,18 @@ export function setupPwaInstall({
modalSteps?.classList.remove('hidden');
modalAction?.classList.add('hidden');
+ if (installedPwa) {
+ modalTitle.textContent = 'Find FloRo Sign';
+ modalDesc.textContent =
+ 'Android installs the app in your app drawer, but your launcher may not add it to the Home screen.';
+ renderModalSteps([
+ 'Swipe up from your Home screen to open the app drawer',
+ 'Search for FloRo Sign (older installs may appear as FloRo)',
+ 'Long-press the app icon and drag it to your Home screen',
+ ]);
+ return;
+ }
+
if (isIOS()) {
modalTitle.textContent = 'Add to Home Screen';
modalDesc.textContent = 'iPhone and iPad cannot auto-install. In Safari:';
@@ -131,13 +160,14 @@ export function setupPwaInstall({
return;
}
- modalTitle.textContent = 'Install FloRo';
+ modalTitle.textContent = 'Install FloRo Sign';
modalDesc.textContent =
- 'Chrome and Edge only show a one-tap install when this page qualifies as an app. Until then:';
+ 'Chrome can delay or temporarily suppress its one-tap prompt. Manual installation is always available:';
renderModalSteps([
- 'Open https://rawshn.com/sign-controller/ in Chrome or Edge (not a WebView or in-app browser)',
- 'Wait until the address bar or browser menu offers Install / Install app',
- 'If you only see Add to Home screen, that is a bookmark, not the standalone app. Reload and try again after the service worker is active',
+ 'Open this page directly in Chrome or Edge, not an in-app browser',
+ 'Tap the three-dot Chrome menu',
+ 'Tap Add to Home screen (or Install and create shortcut), then choose Install app',
+ 'After installation, swipe up and search your app drawer for FloRo Sign',
]);
}
@@ -147,31 +177,32 @@ export function setupPwaInstall({
async function triggerNativeInstall() {
if (!deferredPrompt) return false;
+ let accepted = false;
try {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
- log?.('User installed FloRo Controller app.', 'success');
+ accepted = true;
+ installedPwa = true;
+ log?.('User installed FloRo Sign.', 'success');
}
} catch (err) {
log?.(`Install prompt failed: ${err?.message || err}`, 'error');
return false;
}
- hideBanner();
deferredPrompt = null;
window.__floroDeferredInstall = null;
closeHelpSheet();
+ if (accepted) showBanner();
+ else hideBanner();
return true;
}
function capturePrompt(event) {
event.preventDefault();
deferredPrompt = event;
+ installedPwa = false;
window.__floroDeferredInstall = event;
- if (androidFallbackTimer) {
- window.clearTimeout(androidFallbackTimer);
- androidFallbackTimer = null;
- }
showBanner();
}
@@ -181,25 +212,26 @@ export function setupPwaInstall({
hideBanner();
} else if (deferredPrompt) {
showBanner();
- } else if (isIOS()) {
+ } else if (isIOS() || isAndroid()) {
showBanner();
} else {
hideBanner();
- scheduleAndroidFallback();
}
window.addEventListener('appinstalled', () => {
- log?.('FloRo Controller installed.', 'success');
- hideBanner();
+ installedPwa = true;
+ log?.('FloRo Sign installed.', 'success');
deferredPrompt = null;
window.__floroDeferredInstall = null;
- settingsBtn?.closest('.settings-section')?.classList.add('hidden');
+ showBanner();
});
btnInstall?.addEventListener('click', async () => {
await triggerNativeInstall();
});
+ btnInstallHelp?.addEventListener('click', openHelpSheet);
+
btnDismiss?.addEventListener('click', () => {
try {
localStorage.setItem(DISMISS_KEY, String(Date.now()));
@@ -227,4 +259,12 @@ export function setupPwaInstall({
if (settingsBtn && isStandalone()) {
settingsBtn.closest('.settings-section')?.classList.add('hidden');
}
+
+ detectInstalledPwa().then((installed) => {
+ if (!installed || isStandalone()) return;
+ installedPwa = true;
+ deferredPrompt = null;
+ window.__floroDeferredInstall = null;
+ showBanner();
+ });
}
diff --git a/js/state.js b/js/state.js
index 2f1ef2b..a2f50e7 100644
--- a/js/state.js
+++ b/js/state.js
@@ -6,7 +6,7 @@ const SCENE_DEFAULTS = {
brightness: 8,
speed: 50,
color: '#ff0000',
- activeMode: 1,
+ activeMode: 32,
lastAnimationMode: 32,
};
diff --git a/js/ui.js b/js/ui.js
index 8066348..7e5b4c2 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1,6 +1,6 @@
import { formatModeLabel, getModeName, MODE_COUNT } from './mode-names.js';
-export const APP_VERSION = '2.2.0';
+export const APP_VERSION = '2.2.1';
export function createLogger(consoleBody) {
return function log(message, type = 'info') {
diff --git a/manifest.webmanifest b/manifest.webmanifest
index 3529800..a75b601 100644
--- a/manifest.webmanifest
+++ b/manifest.webmanifest
@@ -1,16 +1,22 @@
{
"name": "FloRo Sign Controller",
- "short_name": "FloRo",
+ "short_name": "FloRo Sign",
"description": "Control your FloRo neon sign over Bluetooth",
"start_url": "./",
"scope": "./",
- "id": "https://rawshn.com/sign-controller/",
+ "id": "./",
"display": "standalone",
"display_override": ["standalone"],
"orientation": "portrait",
"background_color": "#020D0A",
"theme_color": "#020D0A",
"prefer_related_applications": false,
+ "related_applications": [
+ {
+ "platform": "webapp",
+ "url": "./manifest.webmanifest"
+ }
+ ],
"launch_handler": {
"client_mode": ["navigate-existing", "auto"]
},
diff --git a/scripts/check-pwa.mjs b/scripts/check-pwa.mjs
index 26ab9ac..68dc8ea 100644
--- a/scripts/check-pwa.mjs
+++ b/scripts/check-pwa.mjs
@@ -1,9 +1,9 @@
#!/usr/bin/env node
/**
- * Contracts for the installable PWA. Fail CI if the Health Hub-aligned
- * install path regresses (fake Install button, JS-injected manifest, etc.).
+ * Contracts for the installable PWA. Fail CI if the native install path,
+ * honest fallback, manifest, or startup performance regresses.
*/
-import { readFileSync, existsSync } from 'node:fs';
+import { readFileSync, existsSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -41,6 +41,21 @@ if (!html.includes('beforeinstallprompt')) {
if (!html.includes('id="btn-install"') || !html.includes('install-banner-action-primary hidden')) {
fail('Install button must start hidden until a native prompt exists');
}
+if (!html.includes('id="btn-install-help"')) {
+ fail('Android needs an honest manual-install fallback when Chrome withholds prompt()');
+}
+if (!html.includes("updateViaCache: 'none'")) {
+ fail('service worker registration must bypass stale HTTP caches');
+}
+if (html.includes("window.addEventListener('load'") && html.includes('serviceWorker.register')) {
+ fail('service worker registration must not wait for slow page assets to finish loading');
+}
+if (html.includes('fonts.googleapis.com') || html.includes('fonts.gstatic.com')) {
+ fail('startup must not depend on third-party web fonts');
+}
+if (/id="connect-prompt"\s+class="[^"]*\bhidden\b/.test(html)) {
+ fail('default disconnected banner must render in the initial shell to avoid layout shift');
+}
let manifest;
try {
@@ -53,8 +68,8 @@ if (manifest) {
if (manifest.start_url !== './' || manifest.scope !== './') {
fail('manifest start_url and scope must be "./" (portable across localhost and /sign-controller/)');
}
- if (manifest.id !== 'https://rawshn.com/sign-controller/') {
- fail('manifest id must be the canonical https://rawshn.com/sign-controller/');
+ if (manifest.id !== './') {
+ fail('manifest id must be "./" so it resolves consistently on production and localhost');
}
if (manifest.display !== 'standalone') {
fail('manifest display must be standalone');
@@ -74,6 +89,11 @@ if (manifest) {
if (!icons.some((i) => i.sizes === '192x192') || !icons.some((i) => i.sizes === '512x512')) {
fail('manifest needs 192x192 and 512x512 icons');
}
+ if (!manifest.related_applications?.some(
+ (app) => app.platform === 'webapp' && app.url === './manifest.webmanifest'
+ )) {
+ fail('manifest must self-reference so Android can detect an existing installation');
+ }
}
if (!/const CACHE_NAME = 'floro-controller-v\d+'/.test(sw)) {
@@ -85,18 +105,37 @@ if (!sw.includes('./js/pwa-install.js')) {
if (!sw.includes("addEventListener('fetch'") && !sw.includes('addEventListener("fetch"')) {
fail('sw.js must have a fetch handler (Chrome installability)');
}
+if (!sw.includes("event.request.mode === 'navigate'") || !sw.includes("caches.match('./index.html')")) {
+ fail('service worker must serve cached navigation immediately for fast installed-app launch');
+}
+if (!sw.includes("new Request(url, { cache: 'reload' })")) {
+ fail('precache must bypass the HTTP cache so a version bump cannot reinstall stale assets');
+}
+
+const symbolsFont = join(root, 'fonts/MaterialSymbolsOutlined.woff2');
+if (!existsSync(symbolsFont)) {
+ fail('missing self-hosted Material Symbols font');
+} else if (statSync(symbolsFont).size > 50_000) {
+ fail('Material Symbols font must stay subset below 50 KB');
+}
if (existsSync(join(root, 'manifest.json'))) {
fail('do not ship manifest.json; use manifest.webmanifest');
}
const pwaSrc = readFileSync(join(root, 'js/pwa-install.js'), 'utf8');
-if (pwaSrc.includes("btnInstall?.addEventListener('click'") &&
- /btnInstall\?\.addEventListener\('click'[\s\S]{0,180}openHelpSheet/.test(pwaSrc)) {
+const stateSrc = readFileSync(join(root, 'js/state.js'), 'utf8');
+if (!/activeMode:\s*32/.test(stateSrc) || !/lastAnimationMode:\s*32/.test(stateSrc)) {
+ fail('first-run Dynamic state must start on specified mode 32');
+}
+const nativeInstallHandler = pwaSrc.match(
+ /btnInstall\?\.addEventListener\('click',[\s\S]*?\n \}\);/
+)?.[0] || '';
+if (!nativeInstallHandler.includes('triggerNativeInstall') ||
+ nativeInstallHandler.includes('openHelpSheet')) {
fail('Install button click must call prompt() only; do not open the help sheet');
}
-if (!pwaSrc.includes('classList.toggle(\'hidden\', !hasNativePrompt)') &&
- !pwaSrc.includes('classList.toggle("hidden", !hasNativePrompt)')) {
+if (!pwaSrc.includes("btnInstall.classList.toggle('hidden', !hasNativePrompt || installedPwa)")) {
fail('Install button visibility must follow hasNativePrompt');
}
diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs
index 1ea5c24..4606856 100644
--- a/scripts/generate-icons.mjs
+++ b/scripts/generate-icons.mjs
@@ -1,5 +1,5 @@
-import { execSync } from "node:child_process";
-import { mkdirSync } from "node:fs";
+import { execFileSync } from "node:child_process";
+import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -16,8 +16,24 @@ const sizes = [
];
for (const { name, size } of sizes) {
- execSync(`sips -z ${size} ${size} "${src}" --out "${join(outDir, name)}"`, {
- stdio: "inherit",
- });
- console.log(`Wrote icons/${name}`);
+ const output = join(outDir, name);
+ if (process.platform === "darwin") {
+ execFileSync("sips", ["-z", String(size), String(size), src, "--out", output], {
+ stdio: "inherit",
+ });
+ console.log(`Wrote icons/${name}`);
+ continue;
+ }
+
+ if (!existsSync(output)) {
+ throw new Error(`Missing icons/${name}; regenerate icons on macOS with sips`);
+ }
+ const png = readFileSync(output);
+ const isPng = png.length >= 24 && png.subarray(1, 4).toString("ascii") === "PNG";
+ const width = isPng ? png.readUInt32BE(16) : 0;
+ const height = isPng ? png.readUInt32BE(20) : 0;
+ if (width !== size || height !== size) {
+ throw new Error(`icons/${name} must be ${size}x${size}, got ${width}x${height}`);
+ }
+ console.log(`Verified icons/${name} (${size}x${size})`);
}
diff --git a/specs/pwa.md b/specs/pwa.md
index acca97c..024251a 100644
--- a/specs/pwa.md
+++ b/specs/pwa.md
@@ -28,14 +28,15 @@ No ` ` tag. Relative URLs resolve against the page URL (`/` locally, `/sign
| Field | Value |
|-------|--------|
-| `name` / `short_name` | FloRo Sign Controller / FloRo |
+| `name` / `short_name` | FloRo Sign Controller / FloRo Sign |
| `start_url` / `scope` | `./` |
-| `id` | `https://rawshn.com/sign-controller/` (canonical, matches Health Hub style) |
+| `id` | `./` (resolves to the canonical scope in production and localhost in development) |
| `display` | `standalone` |
| `display_override` | `["standalone"]` |
| `launch_handler.client_mode` | `["navigate-existing", "auto"]` (array, not a string) |
| `icons` | `./icons/icon-192.png` (`any`), `./icons/icon-512.png` (`any` + `maskable`) |
| `theme_color` / `background_color` | `#020D0A` |
+| `related_applications` | Self-reference used to detect an existing Android installation |
`prefer_related_applications` stays `false` so Chrome does not skip the install UI looking for a Play Store app.
@@ -47,11 +48,11 @@ No ` ` tag. Relative URLs resolve against the page URL (`/` locally, `/sign
navigator.serviceWorker.register('./sw.js');
```
-Register on `window` `load` so the first paint is not blocked. `skipWaiting` + `clientsClaim` remain. Precache the app shell including `js/pwa-install.js`. Bump `CACHE_NAME` whenever cached assets change.
+Register immediately—the API is asynchronous and waiting for `window.load` can delay installability behind slow images or fonts. Use `updateViaCache: "none"` so a stale CDN header cannot pin an old worker. `skipWaiting` + `clientsClaim` remain. Precache the app shell including `js/pwa-install.js` with `cache: "reload"` requests; otherwise a new cache version can be populated with old files from the browser’s HTTP cache. Bump `CACHE_NAME` whenever cached assets change.
Production (portfolio `vercel.json`): `/sign-controller/sw.js` must be `Cache-Control: no-cache` so clients are not stuck on a 4-hour cached worker. Optional `Service-Worker-Allowed: /sign-controller/`.
-Navigate and manifest/sw requests are network-first so an installed app picks up updates.
+Navigations return the cached app shell immediately and refresh it in the background. Manifest requests are network-first. Worker updates bypass the HTTP cache.
## Install UI
@@ -60,14 +61,15 @@ Module: `js/pwa-install.js`. Early capture in `index.html` (before other scripts
| Situation | Banner | Install button | Click |
|-----------|--------|----------------|-------|
| `beforeinstallprompt` fired | Show | Show | `event.prompt()` only |
-| Android, no event after 2.5s | Show (honest copy) | Hidden | n/a |
-| iOS, not standalone | Show (Share → A2HS copy) | Hidden | n/a |
+| Android, no native event | Show immediately (honest copy) | **Install help** | Chrome menu instructions |
+| Android, PWA already installed | Show find-app copy | **Find app** | App-drawer instructions |
+| iOS, not standalone | Show (Share → A2HS copy) | **Install help** | Share → Add to Home Screen instructions |
| Desktop, no event | Hidden | Hidden | Settings help sheet only |
| Already standalone | Hidden | Hidden | Settings install row hidden |
-Dismiss **Not now** writes `localStorage` `floro_install_dismissed_v5` with a 7-day TTL. Settings → **Install App** ignores dismiss: native `prompt()` if held, otherwise the help sheet (iOS A2HS or Chromium menu copy). The help sheet is never the primary action for a visible **Install** button.
+Dismiss **Not now** writes `localStorage` `floro_install_dismissed_v6` with a 7-day TTL. Settings → **Install App** ignores dismiss: native `prompt()` if held, otherwise the help sheet. A self-reference in `related_applications` lets supporting Chrome versions distinguish “prompt unavailable” from “already installed.” Android launchers may install into the app drawer without pinning a Home screen icon, so the installed help names both **FloRo Sign** and the old **FloRo** short name.
-`preventDefault` on `beforeinstallprompt` is required to call `prompt()` later. It also suppresses Chrome’s default ⋮ **Install app** item. Therefore the in-page **Install** button must be the path that calls `prompt()`. Never tell the user to use ⋮ after we have consumed the event.
+`preventDefault` on `beforeinstallprompt` is required to call `prompt()` later. The in-page **Install** button must call that event directly. If Chrome does not emit the event (for example, after a recent dismissal), the separately labeled **Install help** action may explain the manual menu route; it must never pretend to invoke native installation.
## Verification
@@ -81,4 +83,5 @@ Manual: Chrome or Edge on Android, https://rawshn.com/sign-controller/, tap **In
| Date | Change |
|------|--------|
+| 2026-09-05 | Register SW immediately, detect existing installs, add honest manual fallback, and make cached launches fast |
| 2026-09-05 | Reimplemented to match Health Hub: native prompt only, static manifest, relative scope |
diff --git a/sw.js b/sw.js
index 4a49e42..faa3c22 100644
--- a/sw.js
+++ b/sw.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'floro-controller-v57';
+const CACHE_NAME = 'floro-controller-v62';
const ASSETS = [
'./',
'./index.html',
@@ -18,20 +18,20 @@ const ASSETS = [
'./icons/icon-192.png',
'./icons/icon-512.png',
'./icons/apple-touch-icon.png',
- './logo.png',
];
function isNetworkFirstRequest(request) {
- if (request.mode === 'navigate') return true;
const path = new URL(request.url).pathname;
return path.endsWith('/manifest.webmanifest') || path.endsWith('/sw.js');
}
self.addEventListener('install', (event) => {
event.waitUntil(
- caches.open(CACHE_NAME).then((cache) =>
- Promise.allSettled(ASSETS.map((url) => cache.add(url))).then(() => self.skipWaiting())
- )
+ caches.open(CACHE_NAME)
+ .then((cache) =>
+ cache.addAll(ASSETS.map((url) => new Request(url, { cache: 'reload' })))
+ )
+ .then(() => self.skipWaiting())
);
});
@@ -46,6 +46,22 @@ self.addEventListener('activate', (event) => {
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;
+ if (event.request.mode === 'navigate') {
+ const networkResponse = fetch(event.request).then((response) => {
+ if (!response.ok) return response;
+ const copy = response.clone();
+ return caches.open(CACHE_NAME)
+ .then((cache) => cache.put('./index.html', copy))
+ .then(() => response);
+ });
+
+ event.waitUntil(networkResponse.then(() => undefined).catch(() => undefined));
+ event.respondWith(
+ caches.match('./index.html').then((cached) => cached || networkResponse)
+ );
+ return;
+ }
+
if (isNetworkFirstRequest(event.request)) {
event.respondWith(
fetch(event.request)
@@ -57,9 +73,6 @@ self.addEventListener('fetch', (event) => {
return response;
})
.catch(() => {
- if (event.request.mode === 'navigate') {
- return caches.match('./index.html');
- }
return caches.match(event.request);
})
);
Install FloRo
Install FloRo so it opens full screen, not as a browser tab.
+
Activity Log
FloRo Sign Controller
-v2.2.0
+v2.2.1