diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-09-07 17:29:53 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-09-07 17:29:53 +0200 |
| commit | 8182a08f60e88595984ba80b472f29ccf53c19bd (patch) | |
| tree | c377ad8a7d43b8794b5df0f7283f71ac24408210 /src | |
| parent | 577a6f00a0f006ca51276ae606835c2d892da872 (diff) | |
feat: Enhance development scripts and add Angular support
- Added new scripts for Angular development and formatting in package.json.
- Updated workspaces to include Angular frontend.
- Modified backend project file to exclude specific views from content inclusion.
- Updated logging settings in appsettings.json to include HttpClient warnings.
- Refactored TimetableTable component for cleaner rendering.
- Removed UpdateNotification component and related service worker management code.
- Simplified service worker registration in root component.
- Cleaned up settings page by removing update management functionality.
- Improved stoplist component structure for better readability.
- Updated PWA worker to streamline caching and response handling.
Diffstat (limited to 'src')
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj | 9 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/appsettings.json | 15 | ||||
| -rw-r--r-- | src/frontend/app/components/TimetableTable.tsx | 18 | ||||
| -rw-r--r-- | src/frontend/app/components/UpdateNotification.tsx | 63 | ||||
| -rw-r--r-- | src/frontend/app/root.tsx | 32 | ||||
| -rw-r--r-- | src/frontend/app/routes/estimates-$id.tsx | 35 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 108 | ||||
| -rw-r--r-- | src/frontend/app/routes/stoplist.tsx | 91 | ||||
| -rw-r--r-- | src/frontend/app/utils/serviceWorkerManager.ts | 155 | ||||
| -rw-r--r-- | src/frontend/index.html | 56 | ||||
| -rw-r--r-- | src/frontend/public/pwa-worker.js | 147 |
11 files changed, 140 insertions, 589 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj index 517a253..4f5747c 100644 --- a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj +++ b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj @@ -10,4 +10,13 @@ <ItemGroup> <PackageReference Include="Costasdev.VigoTransitApi" Version="0.1.0" /> </ItemGroup> + + <ItemGroup> + <_ContentIncludedByDefault Remove="Views\Shared\_Layout.cshtml" /> + <_ContentIncludedByDefault Remove="Views\StopEstimates\GetEstimates.cshtml" /> + </ItemGroup> + + <ItemGroup> + <Folder Include="wwwroot\" /> + </ItemGroup> </Project>
\ No newline at end of file diff --git a/src/Costasdev.Busurbano.Backend/appsettings.json b/src/Costasdev.Busurbano.Backend/appsettings.json index 23160a4..49830b4 100644 --- a/src/Costasdev.Busurbano.Backend/appsettings.json +++ b/src/Costasdev.Busurbano.Backend/appsettings.json @@ -1,9 +1,10 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + }, + "AllowedHosts": "*" } diff --git a/src/frontend/app/components/TimetableTable.tsx b/src/frontend/app/components/TimetableTable.tsx index d03ddf4..86896ca 100644 --- a/src/frontend/app/components/TimetableTable.tsx +++ b/src/frontend/app/components/TimetableTable.tsx @@ -144,16 +144,14 @@ export const TimetableTable: React.FC<TimetableTableProps> = ({ </div> </div> <div className="card-body"> - {!isPast && ( - <div className="route-streets"> - <span className="service-id"> - {parseServiceId(entry.trip.service_id)} - </span> - {entry.next_streets.length > 0 && ( - <span> — {entry.next_streets.join(' — ')}</span> - )} - </div> - )} + <div className="route-streets"> + <span className="service-id"> + {parseServiceId(entry.trip.service_id)} + </span> + {entry.next_streets.length > 0 && ( + <span> — {entry.next_streets.join(' — ')}</span> + )} + </div> </div> </div> ); diff --git a/src/frontend/app/components/UpdateNotification.tsx b/src/frontend/app/components/UpdateNotification.tsx deleted file mode 100644 index c8e21ea..0000000 --- a/src/frontend/app/components/UpdateNotification.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState, useEffect } from "react"; -import { Download, X } from "lucide-react"; -import { swManager } from "../utils/serviceWorkerManager"; -import "./UpdateNotification.css"; - -export function UpdateNotification() { - const [showUpdate, setShowUpdate] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - - useEffect(() => { - swManager.onUpdate(() => { - setShowUpdate(true); - }); - }, []); - - const handleUpdate = async () => { - setIsUpdating(true); - swManager.activateUpdate(); - - // Wait for the page to reload - setTimeout(() => { - window.location.reload(); - }, 500); - }; - - const handleDismiss = () => { - setShowUpdate(false); - }; - - if (!showUpdate) return null; - - return ( - <div className="update-notification"> - <div className="update-content"> - <div className="update-icon"> - <Download size={20} /> - </div> - <div className="update-text"> - <div className="update-title">Nueva versión disponible</div> - <div className="update-description"> - Actualiza para obtener las últimas mejoras - </div> - </div> - <div className="update-actions"> - <button - className="update-button" - onClick={handleUpdate} - disabled={isUpdating} - > - {isUpdating ? "Actualizando..." : "Actualizar"} - </button> - <button - className="update-dismiss" - onClick={handleDismiss} - aria-label="Cerrar notificación" - > - <X size={16} /> - </button> - </div> - </div> - </div> - ); -} diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx index 040494f..25a873f 100644 --- a/src/frontend/app/root.tsx +++ b/src/frontend/app/root.tsx @@ -18,9 +18,6 @@ import "maplibre-theme/modern.css"; import { Protocol } from "pmtiles"; import maplibregl, { type LngLatLike } from "maplibre-gl"; import { AppProvider } from "./AppContext"; -import { swManager } from "./utils/serviceWorkerManager"; -import { UpdateNotification } from "./components/UpdateNotification"; -import { useEffect } from "react"; const pmtiles = new Protocol(); maplibregl.addProtocol("pmtiles", pmtiles.tile); //#endregion @@ -30,7 +27,7 @@ import "./i18n"; export const links: Route.LinksFunction = () => []; export function HydrateFallback() { - return "Loading..."; + return "Cargando..."; } export function Layout({ children }: { children: React.ReactNode }) { @@ -90,32 +87,19 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } -// Helper: check if coordinates are within Vigo bounds -function isWithinVigo(lngLat: LngLatLike): boolean { - let lng: number, lat: number; - if (Array.isArray(lngLat)) { - [lng, lat] = lngLat; - } else if ("lng" in lngLat && "lat" in lngLat) { - lng = lngLat.lng; - lat = lngLat.lat; - } else { - return false; - } - // Rough bounding box for Vigo - return lat >= 42.18 && lat <= 42.3 && lng >= -8.78 && lng <= -8.65; -} - import NavBar from "./components/NavBar"; export default function App() { - useEffect(() => { - // Initialize service worker - swManager.initialize(); - }, []); + if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/pwa-worker.js') + .catch((error) => { + console.error('Error registering SW:', error); + }); + } return ( <AppProvider> - <UpdateNotification /> <main className="main-content"> <Outlet /> </main> diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx index 1582275..ab10c53 100644 --- a/src/frontend/app/routes/estimates-$id.tsx +++ b/src/frontend/app/routes/estimates-$id.tsx @@ -122,8 +122,9 @@ export default function Estimates() { } }; - if (data === null) + if (data === null) { return <h1 className="page-title">{t("common.loading")}</h1>; + } return ( <div className="page-container estimates-page"> @@ -139,24 +140,24 @@ export default function Estimates() { </h1> </div> - <div className="table-responsive"> - {tableStyle === "grouped" ? ( - <GroupedTable data={data} dataDate={dataDate} /> - ) : ( - <RegularTable data={data} dataDate={dataDate} /> - )} - </div> + <div className="table-responsive"> + {tableStyle === "grouped" ? ( + <GroupedTable data={data} dataDate={dataDate} /> + ) : ( + <RegularTable data={data} dataDate={dataDate} /> + )} + </div> - <div className="timetable-section"> - <TimetableTable - data={timetableData} - currentTime={new Date().toTimeString().slice(0, 8)} // HH:MM:SS - /> + <div className="timetable-section"> + <TimetableTable + data={timetableData} + currentTime={new Date().toTimeString().slice(0, 8)} // HH:MM:SS + /> - {timetableData.length > 0 && ( - <div className="timetable-actions"> - <Link - to={`/timetable/${params.id}`} + {timetableData.length > 0 && ( + <div className="timetable-actions"> + <Link + to={`/timetable/${params.id}`} className="view-all-link" > <ExternalLink className="external-icon" /> diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx index b75434d..3bc3492 100644 --- a/src/frontend/app/routes/settings.tsx +++ b/src/frontend/app/routes/settings.tsx @@ -2,8 +2,6 @@ import { useApp } from "../AppContext"; import "./settings.css"; import { useTranslation } from "react-i18next"; import { useState } from "react"; -import { swManager } from "../utils/serviceWorkerManager"; -import { RotateCcw, Download } from "lucide-react"; export default function Settings() { const { t, i18n } = useTranslation(); @@ -19,63 +17,6 @@ export default function Settings() { const [isCheckingUpdates, setIsCheckingUpdates] = useState(false); const [updateMessage, setUpdateMessage] = useState<string | null>(null); - const handleCheckForUpdates = async () => { - setIsCheckingUpdates(true); - setUpdateMessage(null); - - try { - // Check if service worker is supported - if (!("serviceWorker" in navigator)) { - setUpdateMessage(t("about.sw_not_supported", "Service Workers no son compatibles en este navegador")); - return; - } - - // Force check for updates - await swManager.checkForUpdates(); - - // Wait a moment for the update check to complete - setTimeout(() => { - if (swManager.isUpdateAvailable()) { - setUpdateMessage(t("about.update_available", "¡Nueva versión disponible! Aparecerá una notificación para actualizar.")); - } else { - setUpdateMessage(t("about.up_to_date", "Ya tienes la versión más reciente.")); - } - }, 2000); - - } catch (error) { - console.error("Error checking for updates:", error); - setUpdateMessage(t("about.update_error", "Error al comprobar actualizaciones. Intenta recargar la página.")); - } finally { - setIsCheckingUpdates(false); - } - }; - - const handleClearCache = async () => { - if (confirm(t("about.clear_cache_confirm", "¿Estás seguro de que quieres limpiar la caché? Esto eliminará todos los datos guardados localmente."))) { - try { - await swManager.clearCache(); - setUpdateMessage(t("about.cache_cleared", "Caché limpiada. La página se recargará para aplicar los cambios.")); - setTimeout(() => { - window.location.reload(); - }, 2000); - } catch (error) { - console.error("Error clearing cache:", error); - setUpdateMessage(t("about.cache_error", "Error al limpiar la caché.")); - } - } - }; - - const handleResetPWA = async () => { - if (confirm(t("about.reset_pwa_confirm", "¿Estás seguro? Esto eliminará TODOS los datos de la aplicación y la reiniciará completamente. Úsalo solo si hay problemas graves de caché."))) { - try { - await swManager.resetPWA(); - } catch (error) { - console.error("Error resetting PWA:", error); - setUpdateMessage(t("about.reset_pwa_error", "Error al reiniciar la PWA.")); - } - } - }; - return ( <div className="page-container"> <h1 className="page-title">{t("about.title")}</h1> @@ -153,55 +94,6 @@ export default function Settings() { <dd>{t("about.details_grouped")}</dd> </dl> </details> - - <div className="settings-section"> - <h3>{t("about.app_updates", "Actualizaciones de la aplicación")}</h3> - <div className="update-controls"> - <button - className="update-button" - onClick={handleCheckForUpdates} - disabled={isCheckingUpdates} - > - {isCheckingUpdates ? ( - <> - <RotateCcw className="spinning" size={18} /> - {t("about.checking_updates", "Comprobando...")} - </> - ) : ( - <> - <Download size={18} /> - {t("about.check_updates", "Comprobar actualizaciones")} - </> - )} - </button> - - <button - className="clear-cache-button" - onClick={handleClearCache} - > - <RotateCcw size={18} /> - {t("about.clear_cache", "Limpiar caché")} - </button> - - <button - className="reset-pwa-button" - onClick={handleResetPWA} - > - <RotateCcw size={18} /> - {t("about.reset_pwa", "Reiniciar PWA (Nuclear)")} - </button> - </div> - - {updateMessage && ( - <div className={`update-message ${updateMessage.includes("Error") || updateMessage.includes("error") ? 'error' : 'success'}`}> - {updateMessage} - </div> - )} - - <p className="update-help-text"> - {t("about.update_help", "Si tienes problemas con la aplicación o no ves las últimas funciones, usa estos botones para forzar una actualización o limpiar los datos guardados.")} - </p> - </div> </section> <h2>{t("about.credits")}</h2> <p> diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx index 1e55dc9..885a0da 100644 --- a/src/frontend/app/routes/stoplist.tsx +++ b/src/frontend/app/routes/stoplist.tsx @@ -69,8 +69,9 @@ export default function StopList() { return stopsList.reverse(); }, [data]); - if (data === null) + if (data === null) { return <h1 className="page-title">{t("common.loading")}</h1>; + } return ( <div className="page-container stoplist-page"> @@ -83,60 +84,60 @@ export default function StopList() { </label> <input className="form-input" - type="text" - placeholder={randomPlaceholder} - id="stopName" - onChange={handleStopSearch} - /> - </div> - </form> - - {searchResults && searchResults.length > 0 && ( - <div className="list-container"> - <h2 className="page-subtitle"> - {t("stoplist.search_results", "Resultados de la búsqueda")} - </h2> - <ul className="list"> - {searchResults.map((stop: Stop) => ( - <StopItem key={stop.stopId} stop={stop} /> - ))} - </ul> - </div> - )} + type="text" + placeholder={randomPlaceholder} + id="stopName" + onChange={handleStopSearch} + /> + </div> + </form> + {searchResults && searchResults.length > 0 && ( <div className="list-container"> - <h2 className="page-subtitle">{t("stoplist.favourites")}</h2> - - {favouritedStops?.length === 0 && ( - <p className="message"> - {t( - "stoplist.no_favourites", - "Accede a una parada y márcala como favorita para verla aquí.", - )} - </p> - )} - + <h2 className="page-subtitle"> + {t("stoplist.search_results", "Resultados de la búsqueda")} + </h2> <ul className="list"> - {favouritedStops - ?.sort((a, b) => a.stopId - b.stopId) - .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)} + {searchResults.map((stop: Stop) => ( + <StopItem key={stop.stopId} stop={stop} /> + ))} </ul> </div> + )} - {recentStops && recentStops.length > 0 && ( - <div className="list-container"> - <h2 className="page-subtitle">{t("stoplist.recents")}</h2> + <div className="list-container"> + <h2 className="page-subtitle">{t("stoplist.favourites")}</h2> - <ul className="list"> - {recentStops.map((stop: Stop) => ( - <StopItem key={stop.stopId} stop={stop} /> - ))} - </ul> - </div> + {favouritedStops?.length === 0 && ( + <p className="message"> + {t( + "stoplist.no_favourites", + "Accede a una parada y márcala como favorita para verla aquí.", + )} + </p> )} + <ul className="list"> + {favouritedStops + ?.sort((a, b) => a.stopId - b.stopId) + .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)} + </ul> + </div> + + {recentStops && recentStops.length > 0 && ( <div className="list-container"> - <h2 className="page-subtitle">{t("stoplist.all_stops", "Paradas")}</h2> + <h2 className="page-subtitle">{t("stoplist.recents")}</h2> + + <ul className="list"> + {recentStops.map((stop: Stop) => ( + <StopItem key={stop.stopId} stop={stop} /> + ))} + </ul> + </div> + )} + + <div className="list-container"> + <h2 className="page-subtitle">{t("stoplist.all_stops", "Paradas")}</h2> <ul className="list"> {data diff --git a/src/frontend/app/utils/serviceWorkerManager.ts b/src/frontend/app/utils/serviceWorkerManager.ts deleted file mode 100644 index a1ddbab..0000000 --- a/src/frontend/app/utils/serviceWorkerManager.ts +++ /dev/null @@ -1,155 +0,0 @@ -export class ServiceWorkerManager { - private registration: ServiceWorkerRegistration | null = null; - private updateAvailable = false; - private onUpdateCallback?: () => void; - - async initialize() { - if (!("serviceWorker" in navigator)) { - console.log("Service Workers not supported"); - return; - } - - try { - // First, unregister any old service workers to start fresh - const registrations = await navigator.serviceWorker.getRegistrations(); - for (const registration of registrations) { - if (registration.scope.includes(window.location.origin)) { - console.log("Unregistering old service worker:", registration.scope); - await registration.unregister(); - } - } - - // Register the new worker with a fresh name - this.registration = await navigator.serviceWorker.register("/pwa-worker.js", { - updateViaCache: 'none' // Disable caching for the SW file itself - }); - console.log("PWA Worker registered with scope:", this.registration.scope); - - // Implement proper updatefound detection (web.dev pattern) - await this.detectSWUpdate(); - - // Check for updates periodically - setInterval(() => { - this.checkForUpdates(); - }, 30 * 1000); - - // Check when page becomes visible - document.addEventListener("visibilitychange", () => { - if (!document.hidden) { - this.checkForUpdates(); - } - }); - - } catch (error) { - console.error("Service Worker registration failed:", error); - } - } - - private async detectSWUpdate() { - if (!this.registration) return; - - // Listen for new service worker discovery - this.registration.addEventListener("updatefound", () => { - const newSW = this.registration!.installing; - if (!newSW) return; - - console.log("New service worker found, monitoring installation..."); - - newSW.addEventListener("statechange", () => { - console.log("New SW state:", newSW.state); - - if (newSW.state === "installed") { - if (navigator.serviceWorker.controller) { - // New service worker is installed, but old one is still controlling - // This means an update is available - console.log("New service worker installed - update available!"); - this.updateAvailable = true; - this.onUpdateCallback?.(); - } else { - // First install, no controller yet - console.log("Service worker installed for the first time"); - } - } - - if (newSW.state === "activated") { - console.log("New service worker activated"); - // Optionally notify about successful update - } - }); - }); - - // Also listen for controller changes - navigator.serviceWorker.addEventListener("controllerchange", () => { - console.log("Service worker controller changed - reloading page"); - window.location.reload(); - }); - } - - async checkForUpdates() { - if (this.registration) { - try { - await this.registration.update(); - } catch (error) { - console.error("Failed to check for updates:", error); - } - } - } - - activateUpdate() { - if (this.registration && this.registration.waiting) { - this.registration.waiting.postMessage({ type: "SKIP_WAITING" }); - this.updateAvailable = false; - } - } - - onUpdate(callback: () => void) { - this.onUpdateCallback = callback; - } - - isUpdateAvailable() { - return this.updateAvailable; - } - - async clearCache(): Promise<void> { - try { - // Delete all caches - const cacheNames = await caches.keys(); - await Promise.all(cacheNames.map(name => caches.delete(name))); - console.log("All caches cleared"); - } catch (error) { - console.error("Failed to clear cache:", error); - throw error; - } - } - - // Nuclear option: completely reset the PWA - async resetPWA(): Promise<void> { - try { - console.log("Resetting PWA completely..."); - - // 1. Unregister ALL service workers - const registrations = await navigator.serviceWorker.getRegistrations(); - await Promise.all(registrations.map(reg => reg.unregister())); - - // 2. Clear all caches - await this.clearCache(); - - // 3. Clear local storage (optional) - localStorage.clear(); - sessionStorage.clear(); - - console.log("PWA reset complete - reloading..."); - - // 4. Force reload after a short delay - setTimeout(() => { - window.location.reload(); - }, 500); - - } catch (error) { - console.error("Failed to reset PWA:", error); - throw error; - } - } -} - -export const swManager = new ServiceWorkerManager(); diff --git a/src/frontend/index.html b/src/frontend/index.html deleted file mode 100644 index 24697c0..0000000 --- a/src/frontend/index.html +++ /dev/null @@ -1,56 +0,0 @@ -<!doctype html> -<html lang="es"> - <head> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta charset="UTF-8" /> - - <title>UrbanoVigo Web</title> - - <link rel="icon" type="image/jpg" href="/logo-512.jpg" /> - <link rel="icon" href="/favicon.ico" sizes="64x64" /> - <link rel="apple-touch-icon" href="/logo-512.jpg" sizes="512x512" /> - <meta name="theme-color" content="#007bff" /> - - <link rel="canonical" href="https://urbanovigo.costas.dev/" /> - - <meta - name="description" - content="Aplicación web para encontrar paradas y tiempos de llegada de los autobuses urbanos de Vigo, España." - /> - <meta - name="keywords" - content="Vigo, autobús, urbano, parada, tiempo, llegada, transporte, público, España" - /> - <meta name="author" content="Ariel Costas Guerrero" /> - - <meta property="og:title" content="UrbanoVigo Web" /> - <meta property="og:type" content="website" /> - <meta property="og:url" content="https://urbanovigo.costas.dev/" /> - <meta - property="og:image" - content="https://urbanovigo.costas.dev/logo-512.jpg" - /> - <meta - property="og:description" - content="Aplicación web para encontrar paradas y tiempos de llegada de los autobuses urbanos de Vigo, España." - /> - - <link rel="manifest" href="/manifest.webmanifest" /> - - <meta name="robots" content="noindex, nofollow" /> - <meta name="googlebot" content="noindex, nofollow" /> - - <style> - body { - margin: 0; - padding: 0; - box-sizing: border-box; - } - </style> - </head> - - <body> - <div id="root"></div> - <script type="module" src="/src/main.tsx"></script> - </body> -</html> diff --git a/src/frontend/public/pwa-worker.js b/src/frontend/public/pwa-worker.js index f47ede0..ef696fe 100644 --- a/src/frontend/public/pwa-worker.js +++ b/src/frontend/public/pwa-worker.js @@ -1,49 +1,45 @@ -const CACHE_VERSION = "2025-0806a"; -const API_CACHE_NAME = `api-cache-${CACHE_VERSION}`; +const CACHE_VERSION = "2025-0907a"; const STATIC_CACHE_NAME = `static-cache-${CACHE_VERSION}`; +const STATIC_CACHE_ASSETS = [ + "/favicon.ico", + "/logo-256.png", + "/logo-512.jpg", + "/stops.json" +]; + +const EXPR_CACHE_AFTER_FIRST_VIEW = /(\/assets\/.*)|(\/api\/GetStopTimetable.*)/; -const API_URL_PATTERN = /\/api\/(GetStopList|GetStopEstimates|GetStopTimetable)/; -const API_MAX_AGE = 24 * 60 * 60 * 1000; // 24h const ESTIMATES_MIN_AGE = 15 * 1000; const ESTIMATES_MAX_AGE = 30 * 1000; self.addEventListener("install", (event) => { - console.log("SW: Installing new version"); + console.log("SW: Install event in progress. Cache version: ", CACHE_VERSION); event.waitUntil( caches.open(STATIC_CACHE_NAME).then((cache) => - cache.addAll([ - "/favicon.ico", - "/logo-256.png", - "/logo-512.jpg", - "/stops.json" - ]) + cache.addAll(STATIC_CACHE_ASSETS) ).then(() => self.skipWaiting()) ); }); self.addEventListener("activate", (event) => { - console.log("SW: Activating new version"); - event.waitUntil( - (async () => { - const cacheNames = await caches.keys(); - await Promise.all( - cacheNames.map((name) => { - if (name !== API_CACHE_NAME && name !== STATIC_CACHE_NAME) { - console.log("SW: Deleting old cache:", name); - return caches.delete(name); - } - }) - ); - await self.clients.claim(); - const clients = await self.clients.matchAll(); - clients.forEach((client) => - client.postMessage({ type: "SW_UPDATED" }) - ); - })() - ); + const doCleanup = async () => { + // Cleans the old caches + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames.map((name) => { + if (name !== STATIC_CACHE_NAME) { + return caches.delete(name); + } + }) + ); + + await self.clients.claim(); + } + + event.waitUntil(doCleanup()); }); -self.addEventListener("fetch", (event) => { +self.addEventListener("fetch", async (event) => { const request = event.request; const url = new URL(request.url); @@ -52,94 +48,37 @@ self.addEventListener("fetch", (event) => { return; } - // API - if (request.method === "GET" && API_URL_PATTERN.test(url.pathname)) { - event.respondWith(handleApiRequest(request)); - return; - } - - // Navegación -> network first + // Navigating => we don't intercept anything, if it fails, good luck if (request.mode === "navigate") { - event.respondWith(handleNavigationRequest(request)); return; } - // Estáticos -> cache first - if (request.method === "GET") { - event.respondWith(handleStaticRequest(request)); - } -}); - -async function handleApiRequest(request) { - const url = new URL(request.url); - const isEstimates = url.pathname.includes("GetStopEstimates"); - const maxAge = isEstimates - ? ESTIMATES_MIN_AGE + Math.random() * (ESTIMATES_MAX_AGE - ESTIMATES_MIN_AGE) - : API_MAX_AGE; - - const cache = await caches.open(API_CACHE_NAME); - const cachedResponse = await cache.match(request); - - if (cachedResponse) { - const dateHeader = cachedResponse.headers.get("date"); - const age = dateHeader ? Date.now() - new Date(dateHeader).getTime() : 0; - if (age && age < maxAge) { - console.debug("SW: Cache HIT", request.url); - return cachedResponse; + // Static => cache first, if not, network; if not, fallback + const isAssetCacheable = STATIC_CACHE_ASSETS.includes(url.pathname) || EXPR_CACHE_AFTER_FIRST_VIEW.test(url.pathname); + if (request.method === "GET" && isAssetCacheable) { + const response = handleStaticRequest(request); + if (response !== null) { + event.respondWith(response); } - cache.delete(request); - } - - try { - const netResponse = await fetch(request); - if (netResponse.ok) cache.put(request, netResponse.clone()); - return netResponse; - } catch (error) { - if (cachedResponse) return cachedResponse; - throw error; + return; } -} +}); async function handleStaticRequest(request) { const cache = await caches.open(STATIC_CACHE_NAME); const cachedResponse = await cache.match(request); - if (cachedResponse) return cachedResponse; - - try { - const netResponse = await fetch(request); - if (netResponse.ok) cache.put(request, netResponse.clone()); - return netResponse; - } catch (err) { - return new Response("Offline asset not available", { - status: 503, - headers: { "Content-Type": "text/plain" } - }); + if (cachedResponse){ + console.log("SW handleStaticRequest: HIT for ", request.url); + return cachedResponse; } -} -async function handleNavigationRequest(request) { try { const netResponse = await fetch(request); + if (netResponse.ok) cache.put(request, netResponse.clone()); + console.log("SW handleStaticRequest: MISS for ", request.url); return netResponse; } catch (err) { - // Si no hay red, intenta fallback con caché estática - const cache = await caches.open(STATIC_CACHE_NAME); - const offline = await cache.match("/stops.json"); - return ( - offline || - new Response("App offline", { - status: 503, - headers: { "Content-Type": "text/plain" } - }) - ); + console.error("SW handleStaticRequest: FAIL for ", request.url, err); + return null; } } - -self.addEventListener("message", (event) => { - if (event.data?.type === "SKIP_WAITING") self.skipWaiting(); - if (event.data?.type === "CLEAR_CACHE") { - event.waitUntil( - caches.keys().then((names) => Promise.all(names.map((n) => caches.delete(n)))) - ); - } -}); |
