Skip to content

Commit 90331f3

Browse files
Merge pull request #7363 from christianbeeznest/GH-7351
Internal: Fix multiple browser loads of header-logo on homepage - refs #7351
2 parents 759fe0d + 2d5c953 commit 90331f3

3 files changed

Lines changed: 274 additions & 95 deletions

File tree

assets/vue/components/layout/PlatformLogo.vue

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,50 @@ const securityStore = useSecurityStore()
1010
const siteName = platformConfigStore.getSetting("platform.site_name")
1111

1212
const theme = computed(() => platformConfigStore.visualTheme || "chamilo")
13-
const DEFAULT_THEME = "chamilo"
1413
const bust = ref(Date.now())
1514

16-
function themeUrl(name, path, { strict = false } = {}) {
17-
const base = `/themes/${encodeURIComponent(name)}/${path}`
18-
const qs = []
19-
if (strict) qs.push("strict=1")
20-
qs.push(`t=${bust.value}`)
21-
return `${base}?${qs.join("&")}`
22-
}
23-
24-
const sources = computed(() => [
25-
themeUrl(theme.value, "images/header-logo.svg", { strict: true }),
26-
themeUrl(theme.value, "images/header-logo.png", { strict: true }),
27-
themeUrl(theme.value, "images/header-logo.svg"),
28-
themeUrl(theme.value, "images/header-logo.png"),
29-
themeUrl(DEFAULT_THEME, "images/header-logo.svg"),
30-
themeUrl(DEFAULT_THEME, "images/header-logo.png"),
31-
])
15+
/**
16+
* It will always serve the best available logo (svg/png) and fallback to default theme.
17+
* This avoids strict=1 probing and prevents 404 noise in the console.
18+
*/
19+
const logoUrl = computed(() => {
20+
return `/themes/${encodeURIComponent(theme.value)}/logo/header?t=${bust.value}`
21+
})
3222

33-
const idx = ref(0)
34-
const currentSrc = computed(() => sources.value[idx.value] || "")
23+
const showImg = ref(true)
3524

3625
watch(
3726
() => platformConfigStore.visualTheme,
3827
() => {
39-
idx.value = 0
4028
bust.value = Date.now()
41-
}
29+
showImg.value = true
30+
},
4231
)
4332

44-
const onError = () => {
45-
if (idx.value < sources.value.length - 1) {
46-
idx.value++
47-
} else {
48-
idx.value = sources.value.length
33+
function onError(e) {
34+
showImg.value = false
35+
if (e?.target) {
36+
e.target.style.display = "none"
4937
}
5038
}
5139
</script>
5240
<template>
5341
<div class="platform-logo">
5442
<BaseAppLink :to="securityStore.user ? { name: 'Home' } : { name: 'Index' }">
5543
<img
56-
v-if="currentSrc"
44+
v-if="showImg"
5745
:alt="siteName"
58-
:src="currentSrc"
46+
:src="logoUrl"
5947
:title="siteName"
6048
decoding="async"
6149
fetchpriority="high"
6250
@error="onError"
6351
/>
64-
<span v-else class="font-semibold text-primary" aria-label="logo">
52+
<span
53+
v-else
54+
class="font-semibold text-primary"
55+
aria-label="logo"
56+
>
6557
{{ siteName }}
6658
</span>
6759
</BaseAppLink>

public/service-worker.js

Lines changed: 135 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,159 @@
33
* Handles:
44
* - Offline caching
55
* - Push notifications
6+
*
7+
* Notes:
8+
* - We avoid caching or intercepting dynamic endpoints (/api, /themes) to prevent noisy console logs
9+
* when responses are 404 (expected in strict checks) or 202 (async endpoints).
10+
* - We only cache successful GET responses (res.ok).
611
*/
712

13+
const CACHE_NAME = "chamilo-cache-v1"
14+
const PRECACHE_URLS = [
15+
"/",
16+
"/manifest.json",
17+
"/img/pwa-icons/icon-192.png",
18+
"/img/pwa-icons/icon-512.png",
19+
]
20+
821
// PWA: Cache basic files on install
9-
self.addEventListener('install', (event) => {
10-
console.log('[Service Worker] Install event');
22+
self.addEventListener("install", (event) => {
23+
console.log("[Service Worker] Install event")
1124

1225
event.waitUntil(
13-
caches.open('chamilo-cache-v1').then((cache) => {
14-
return cache.addAll([
15-
'/',
16-
'/manifest.json',
17-
'/img/pwa-icons/icon-192.png',
18-
'/img/pwa-icons/icon-512.png',
19-
]);
26+
caches.open(CACHE_NAME).then((cache) => {
27+
return cache.addAll(PRECACHE_URLS)
2028
})
21-
);
22-
});
23-
24-
//PWA: Serve from cache if available
25-
self.addEventListener('fetch', (event) => {
26-
const url = new URL(event.request.url);
27-
if (url.pathname.startsWith('/r/')) {
28-
return;
29+
)
30+
31+
// Activate updated SW ASAP
32+
self.skipWaiting()
33+
})
34+
35+
self.addEventListener("activate", (event) => {
36+
console.log("[Service Worker] Activate event")
37+
38+
event.waitUntil(
39+
(async () => {
40+
// Cleanup old caches if cache name changes in the future
41+
const keys = await caches.keys()
42+
await Promise.all(
43+
keys.map((key) => {
44+
if (key !== CACHE_NAME) {
45+
return caches.delete(key)
46+
}
47+
return Promise.resolve()
48+
})
49+
)
50+
51+
await self.clients.claim()
52+
})()
53+
)
54+
})
55+
56+
/**
57+
* Decide whether we should bypass SW for this request.
58+
* Keep logic explicit and conservative.
59+
*/
60+
function shouldBypass(request) {
61+
const url = new URL(request.url)
62+
63+
// Ignore internal redirect helper routes
64+
if (url.pathname.startsWith("/r/")) return true
65+
66+
// Avoid intercepting theme assets (prevents console noise for strict 404 checks)
67+
if (url.pathname.startsWith("/themes/")) return true
68+
69+
// Avoid intercepting API calls (can return 202 or other statuses that shouldn't be cached)
70+
if (url.pathname.startsWith("/api/")) return true
71+
72+
// Avoid strict probing calls anywhere (expected 404 should not produce SW noise)
73+
if (url.searchParams.has("strict")) return true
74+
75+
// Do not handle non-GET requests
76+
if (request.method !== "GET") return true
77+
78+
return false
79+
}
80+
81+
/**
82+
* Cache-first for static navigations/assets, network fallback.
83+
* - Only caches successful responses (res.ok).
84+
* - If network fails, returns cache if available.
85+
*/
86+
self.addEventListener("fetch", (event) => {
87+
const request = event.request
88+
89+
if (shouldBypass(request)) {
90+
// Let the browser handle it directly
91+
return
2992
}
3093

3194
event.respondWith(
32-
caches.match(event.request).then((cachedResponse) => {
33-
return cachedResponse || fetch(event.request);
34-
})
35-
);
36-
});
95+
(async () => {
96+
try {
97+
const cachedResponse = await caches.match(request)
98+
if (cachedResponse) {
99+
return cachedResponse
100+
}
101+
102+
const response = await fetch(request)
103+
104+
// Cache only successful responses
105+
if (response && response.ok) {
106+
const cache = await caches.open(CACHE_NAME)
107+
await cache.put(request, response.clone())
108+
}
109+
110+
return response
111+
} catch (err) {
112+
// Network failure: try cache
113+
const cachedResponse = await caches.match(request)
114+
if (cachedResponse) {
115+
return cachedResponse
116+
}
117+
118+
// As last resort, provide a friendly offline response for navigations
119+
const accept = request.headers.get("accept") || ""
120+
if (accept.includes("text/html")) {
121+
return new Response(
122+
"<h1>Offline</h1><p>The application is currently offline.</p>",
123+
{ headers: { "Content-Type": "text/html; charset=utf-8" }, status: 503 }
124+
)
125+
}
126+
127+
// For other assets, just fail gracefully
128+
return new Response("Offline", { status: 503, statusText: "Service Unavailable" })
129+
}
130+
})()
131+
)
132+
})
37133

38134
// PUSH NOTIFICATIONS
39-
self.addEventListener('push', function (event) {
40-
console.log('[Service Worker] Push received.');
135+
self.addEventListener("push", function (event) {
136+
console.log("[Service Worker] Push received.")
41137

42-
let data = {};
138+
let data = {}
43139
if (event.data) {
44-
data = event.data.json();
140+
data = event.data.json()
45141
}
46142

47143
const options = {
48-
body: data.message || 'No message payload',
49-
icon: '/img/pwa-icons/icon-192.png',
144+
body: data.message || "No message payload",
145+
icon: "/img/pwa-icons/icon-192.png",
50146
data: {
51-
url: data.url || '/',
147+
url: data.url || "/",
52148
},
53-
};
149+
}
54150

55151
event.waitUntil(
56-
self.registration.showNotification(data.title || 'Chamilo', options)
57-
);
58-
});
152+
self.registration.showNotification(data.title || "Chamilo", options)
153+
)
154+
})
59155

60-
self.addEventListener('notificationclick', function (event) {
61-
event.notification.close();
62-
const url = event.notification.data.url || '/';
63-
event.waitUntil(
64-
clients.openWindow(url)
65-
);
66-
});
156+
self.addEventListener("notificationclick", function (event) {
157+
event.notification.close()
158+
const url = (event.notification && event.notification.data && event.notification.data.url) || "/"
159+
160+
event.waitUntil(clients.openWindow(url))
161+
})

0 commit comments

Comments
 (0)