aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/app/components/TimetableTable.tsx18
-rw-r--r--src/frontend/app/components/UpdateNotification.tsx63
-rw-r--r--src/frontend/app/root.tsx32
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx35
-rw-r--r--src/frontend/app/routes/settings.tsx108
-rw-r--r--src/frontend/app/routes/stoplist.tsx91
-rw-r--r--src/frontend/app/utils/serviceWorkerManager.ts155
-rw-r--r--src/frontend/index.html56
-rw-r--r--src/frontend/public/pwa-worker.js147
9 files changed, 123 insertions, 582 deletions
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))))
- );
- }
-});