From d47aa2fe90db61994cfaa71bec14cd6464f08fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Sun, 16 Nov 2025 21:12:48 +0100 Subject: [PATCH] PWA offline error page + login page cleanup (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add friendly PWA offline error page When the PWA fails to connect to the server, users now see a branded offline page with a friendly "technical difficulties" message, the app logo, and a reload button. The page automatically attempts to reload when connectivity is restored. Changes: - Created public/offline.html with branded offline experience - Updated service worker to cache and serve offline page on network failures - Added service worker registration in application.js - Service worker now handles navigation requests with offline fallback * Extract PWA offline logo to separate cached asset Move the inline SVG logo from offline.html to a separate file at public/logo-offline.svg. This makes the logo asset easily identifiable and maintainable, as it may diverge from other logo versions in the future. Changes: - Created public/logo-offline.svg with the offline page logo - Updated service worker to cache logo as part of OFFLINE_ASSETS array - Updated fetch handler to serve cached offline assets - Updated offline.html to reference logo file instead of inline SVG * Update offline message for better readability Signed-off-by: Juan José Mata * CodeRabbit comments * Keep 40x and 50x flowing * Dark mode * Logo tweaks * Login/sign up cleanup --------- Signed-off-by: Juan José Mata Co-authored-by: Claude --- app/assets/images/logomark.svg | 6 +- app/javascript/application.js | 13 +++ app/views/layouts/auth.html.erb | 5 +- app/views/pwa/service-worker.js | 65 +++++++++++++- app/views/sessions/new.html.erb | 4 - config/locales/views/layout/ca.yml | 1 - config/locales/views/layout/en.yml | 1 - config/locales/views/layout/es.yml | 1 - config/locales/views/layout/nb.yml | 1 - config/locales/views/layout/tr.yml | 1 - config/locales/views/sessions/en.yml | 2 +- public/logo-offline.svg | 6 ++ public/offline.html | 121 +++++++++++++++++++++++++++ test/application_system_test_case.rb | 2 +- 14 files changed, 208 insertions(+), 21 deletions(-) create mode 100644 public/logo-offline.svg create mode 100644 public/offline.html diff --git a/app/assets/images/logomark.svg b/app/assets/images/logomark.svg index a8f2dc7d2..80e043546 100644 --- a/app/assets/images/logomark.svg +++ b/app/assets/images/logomark.svg @@ -1,6 +1,6 @@ - - - \ No newline at end of file + + + diff --git a/app/javascript/application.js b/app/javascript/application.js index 12751637b..44dcb4a84 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -5,3 +5,16 @@ import "controllers"; Turbo.StreamActions.redirect = function () { Turbo.visit(this.target); }; + +// Register service worker for PWA offline support +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-worker') + .then(registration => { + console.log('Service Worker registered with scope:', registration.scope); + }) + .catch(error => { + console.log('Service Worker registration failed:', error); + }); + }); +} diff --git a/app/views/layouts/auth.html.erb b/app/views/layouts/auth.html.erb index 2cf1df595..f12f5b2a0 100644 --- a/app/views/layouts/auth.html.erb +++ b/app/views/layouts/auth.html.erb @@ -4,12 +4,9 @@
- <%= image_tag "logo-color.png", class: "w-16 mb-6" %> + <%= image_tag "logomark.svg", class: "w-16 mb-6" %>
-

- <%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %> -

<% if (controller_name == "sessions" && action_name == "new") || (controller_name == "registrations" && action_name == "new") %>
diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js index 68d5c2ee6..4108a2065 100644 --- a/app/views/pwa/service-worker.js +++ b/app/views/pwa/service-worker.js @@ -1,10 +1,69 @@ +const CACHE_VERSION = 'v1'; +const OFFLINE_ASSETS = [ + '/offline.html', + '/logo-offline.svg' +]; + +// Install event - cache the offline page and assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_VERSION).then((cache) => { + return cache.addAll(OFFLINE_ASSETS); + }) + ); + // Activate immediately + self.skipWaiting(); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_VERSION) { + return caches.delete(cacheName); + } + }) + ); + }).then(() => { + // Take control of all pages immediately + return self.clients.claim(); + }) + ); +}); + +// Fetch event - serve offline page when network fails +self.addEventListener('fetch', (event) => { + // Handle navigation requests (page loads) + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request).catch((error) => { + // Only show offline page for network errors + if (error.name === 'TypeError' || !navigator.onLine) { + return caches.match('/offline.html'); + } + throw error; + }) + ); + } + // Handle offline assets (logo, etc.) + else if (OFFLINE_ASSETS.some(asset => new URL(event.request.url).pathname === asset)) { + event.respondWith( + caches.match(event.request).then((response) => { + return response || fetch(event.request); + }) + ); + } +}); + // Add a service worker for processing Web Push notifications: // // self.addEventListener("push", async (event) => { // const { title, options } = await event.data.json() // event.waitUntil(self.registration.showNotification(title, options)) // }) -// +// // self.addEventListener("notificationclick", function(event) { // event.notification.close() // event.waitUntil( @@ -12,12 +71,12 @@ // for (let i = 0; i < clientList.length; i++) { // let client = clientList[i] // let clientPath = (new URL(client.url)).pathname -// +// // if (clientPath == event.notification.data.path && "focus" in client) { // return client.focus() // } // } -// +// // if (clients.openWindow) { // return clients.openWindow(event.notification.data.path) // } diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 336547490..72afcaf49 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,7 +1,3 @@ -<% - header_title t(".title") -%> - <% if @prefill_demo_credentials %>
diff --git a/config/locales/views/layout/ca.yml b/config/locales/views/layout/ca.yml index 4741f505a..66e260d7a 100644 --- a/config/locales/views/layout/ca.yml +++ b/config/locales/views/layout/ca.yml @@ -6,7 +6,6 @@ ca: no_account: Nou a %{product_name}? sign_in: Inicia sessió sign_up: Crea un compte - your_account: El teu compte shared: footer: privacy_policy: Política de privacitat diff --git a/config/locales/views/layout/en.yml b/config/locales/views/layout/en.yml index 96445f376..1a6ac3bb4 100644 --- a/config/locales/views/layout/en.yml +++ b/config/locales/views/layout/en.yml @@ -6,7 +6,6 @@ en: no_account: New to %{product_name}? sign_in: Sign in sign_up: Create account - your_account: Your account shared: footer: privacy_policy: Privacy Policy diff --git a/config/locales/views/layout/es.yml b/config/locales/views/layout/es.yml index 6178965e3..37ffbe463 100644 --- a/config/locales/views/layout/es.yml +++ b/config/locales/views/layout/es.yml @@ -6,7 +6,6 @@ es: no_account: ¿Nuevo en %{product_name}? sign_in: Iniciar sesión sign_up: Crear cuenta - your_account: Tu cuenta shared: footer: privacy_policy: Política de privacidad diff --git a/config/locales/views/layout/nb.yml b/config/locales/views/layout/nb.yml index f6833b442..0dde7d43d 100644 --- a/config/locales/views/layout/nb.yml +++ b/config/locales/views/layout/nb.yml @@ -6,7 +6,6 @@ nb: no_account: Ny hos %{product_name}? sign_in: Logg inn sign_up: Opprett konto - your_account: Din konto shared: footer: privacy_policy: Personvernerklæring diff --git a/config/locales/views/layout/tr.yml b/config/locales/views/layout/tr.yml index fe2b6634b..eb60b1a27 100644 --- a/config/locales/views/layout/tr.yml +++ b/config/locales/views/layout/tr.yml @@ -6,7 +6,6 @@ tr: no_account: "%{product_name}'ye yeni misiniz?" sign_in: Giriş yap sign_up: Hesap oluştur - your_account: Hesabınız shared: footer: privacy_policy: Gizlilik Politikası diff --git a/config/locales/views/sessions/en.yml b/config/locales/views/sessions/en.yml index 98a6ecd9a..88afdafec 100644 --- a/config/locales/views/sessions/en.yml +++ b/config/locales/views/sessions/en.yml @@ -15,7 +15,7 @@ en: forgot_password: Forgot your password? password: Password submit: Log in - title: Sign in to your account + title: Sure password_placeholder: Enter your password openid_connect: Sign in with OpenID Connect google_auth_connect: Sign in with Google diff --git a/public/logo-offline.svg b/public/logo-offline.svg new file mode 100644 index 000000000..e4d8f87c5 --- /dev/null +++ b/public/logo-offline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 000000000..e87823fcd --- /dev/null +++ b/public/offline.html @@ -0,0 +1,121 @@ + + + + + + + Offline - Maybe + + + +
+ + +

We're experiencing
technical difficulties

+

It looks like you're offline or we can't reach our servers right now. Please check your connection and try again.

+ + +
+ + + + diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index b756c30ce..a32c04b49 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -30,7 +30,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase click_button "Logout" # Trigger Capybara's wait mechanism to avoid timing issues with logout - find("h2", text: "Sign in to your account") + find("a", text: "Sign in") end def within_testid(testid)