fix: strip read_file line numbers accidentally injected into JS files
CI / lint (push) Successful in 13s
CI / security (push) Successful in 8s
CI / test (push) Successful in 16s
CI / build (push) Successful in 3s

This commit is contained in:
2026-05-28 16:40:14 -04:00
parent 7866f93778
commit 643a73e0f5
12 changed files with 8947 additions and 10017 deletions
+437 -438
View File
@@ -1,438 +1,437 @@
1|/* ObsiGate — Sync: SSE client + PWA registration */
2|import {
3| state.currentVault,
4| state.currentPath,
5| state.activeSidebarTab,
6| selectedContextVault
7|} from './state.js';
8|import { showToast } from './ui.js';
9|import { loadVaults, loadTags, refreshTagsForContext } from './sidebar.js';
10|
11|// ---------------------------------------------------------------------------
12|// SSE Client — IndexUpdateManager
13|// ---------------------------------------------------------------------------
14|export const IndexUpdateManager = (() => {
15| let eventSource = null;
16| let reconnectTimer = null;
17| let reconnectDelay = 1000;
18| const MAX_RECONNECT_DELAY = 30000;
19| let recentEvents = [];
20| const MAX_RECENT_EVENTS = 20;
21| let connectionState = "disconnected"; // disconnected | connecting | connected
22|
23| function connect() {
24| if (eventSource) {
25| eventSource.close();
26| }
27| connectionState = "connecting";
28| _updateBadge();
29|
30| eventSource = new EventSource("/api/events");
31|
32| eventSource.addEventListener("connected", (e) => {
33| connectionState = "connected";
34| reconnectDelay = 1000;
35| _updateBadge();
36| });
37|
38| eventSource.addEventListener("index_updated", (e) => {
39| try {
40| const data = JSON.parse(e.data);
41| _addEvent("index_updated", data);
42| _onIndexUpdated(data);
43| } catch (err) {
44| console.error("SSE parse error:", err);
45| }
46| });
47|
48| eventSource.addEventListener("index_reloaded", (e) => {
49| try {
50| const data = JSON.parse(e.data);
51| _addEvent("index_reloaded", data);
52| _onIndexReloaded(data);
53| } catch (err) {
54| console.error("SSE parse error:", err);
55| }
56| });
57|
58| eventSource.addEventListener("vault_added", (e) => {
59| try {
60| const data = JSON.parse(e.data);
61| _addEvent("vault_added", data);
62| showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
63| loadVaults();
64| loadTags();
65| } catch (err) {
66| console.error("SSE parse error:", err);
67| }
68| });
69|
70| eventSource.addEventListener("vault_removed", (e) => {
71| try {
72| const data = JSON.parse(e.data);
73| _addEvent("vault_removed", data);
74| showToast(`Vault "${data.vault}" supprimé`, "info");
75| loadVaults();
76| loadTags();
77| } catch (err) {
78| console.error("SSE parse error:", err);
79| }
80| });
81|
82| eventSource.addEventListener("index_start", (e) => {
83| try {
84| const data = JSON.parse(e.data);
85| _addEvent("index_start", data);
86| connectionState = "syncing";
87| _updateBadge();
88| showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
89| } catch (err) {
90| console.error("SSE parse error:", err);
91| }
92| });
93|
94| eventSource.addEventListener("index_progress", (e) => {
95| try {
96| const data = JSON.parse(e.data);
97| _addEvent("index_progress", data);
98| connectionState = "syncing";
99| _updateBadge();
100| loadVaults();
101| loadTags();
102| } catch (err) {
103| console.error("SSE parse error:", err);
104| }
105| });
106|
107| eventSource.addEventListener("index_complete", (e) => {
108| try {
109| const data = JSON.parse(e.data);
110| _addEvent("index_complete", data);
111| connectionState = "connected";
112| _updateBadge();
113| showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
114| loadVaults();
115| loadTags();
116| } catch (err) {
117| console.error("SSE parse error:", err);
118| }
119| });
120|
121| eventSource.onerror = () => {
122| connectionState = "disconnected";
123| _updateBadge();
124| eventSource.close();
125| eventSource = null;
126| _scheduleReconnect();
127| };
128| }
129|
130| function _scheduleReconnect() {
131| if (reconnectTimer) clearTimeout(reconnectTimer);
132| reconnectTimer = setTimeout(() => {
133| reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
134| connect();
135| }, reconnectDelay);
136| }
137|
138| function _addEvent(type, data) {
139| recentEvents.unshift({
140| type,
141| data,
142| timestamp: new Date().toISOString(),
143| });
144| if (recentEvents.length > MAX_RECENT_EVENTS) {
145| recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
146| }
147| }
148|
149| async function _onIndexUpdated(data) {
150| // Brief syncing state
151| connectionState = "syncing";
152| _updateBadge();
153|
154| const n = data.total_changes || 0;
155| const vaults = (data.vaults || []).join(", ");
156| // Toast removed: silent auto-indexing — no notification needed
157|
158| // Refresh sidebar and tags if affected vault matches current context
159| const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
160| if (affectsCurrentVault) {
161| try {
162| await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
163| // Refresh current file if it was updated
164| if (currentVault && state.currentPath) {
165| const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath);
166| if (changed) {
167| openFile(state.currentVault, state.currentPath);
168| }
169| }
170| } catch (err) {
171| console.error("Error refreshing after index update:", err);
172| }
173| }
174|
175| // Refresh recent tab if it is active
176| if (state.activeSidebarTab === "recent") {
177| const vaultFilter = document.getElementById("recent-vault-filter");
178| loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
179| }
180|
181| setTimeout(() => {
182| connectionState = "connected";
183| _updateBadge();
184| }, 1500);
185| }
186|
187| async function _onIndexReloaded(data) {
188| connectionState = "syncing";
189| _updateBadge();
190| showToast("Index complet rechargé", "info");
191| try {
192| await Promise.all([loadVaults(), loadTags()]);
193| } catch (err) {
194| console.error("Error refreshing after full reload:", err);
195| }
196| setTimeout(() => {
197| connectionState = "connected";
198| _updateBadge();
199| }, 1500);
200| }
201|
202| function _updateBadge() {
203| const badge = document.getElementById("sync-badge");
204| if (!badge) return;
205| badge.className = "sync-badge sync-badge--" + connectionState;
206| const labels = {
207| disconnected: "Déconnecté",
208| connecting: "Connexion...",
209| connected: "Synchronisé",
210| syncing: "Mise à jour...",
211| };
212| badge.title = labels[connectionState] || connectionState;
213| }
214|
215| function disconnect() {
216| if (eventSource) {
217| eventSource.close();
218| eventSource = null;
219| }
220| if (reconnectTimer) {
221| clearTimeout(reconnectTimer);
222| reconnectTimer = null;
223| }
224| connectionState = "disconnected";
225| _updateBadge();
226| }
227|
228| function getState() {
229| return connectionState;
230| }
231|
232| function getRecentEvents() {
233| return recentEvents;
234| }
235|
236| return { connect, disconnect, getState, getRecentEvents };
237|})();
238|
239|// ---------------------------------------------------------------------------
240|// Sync status badge and panel
241|// ---------------------------------------------------------------------------
242|
243|function initSyncStatus() {
244| const badge = document.getElementById("sync-badge");
245| if (!badge) return;
246|
247| badge.addEventListener("click", (e) => {
248| e.stopPropagation();
249| toggleSyncPanel();
250| });
251|
252| IndexUpdateManager.connect();
253|}
254|
255|function toggleSyncPanel() {
256| let panel = document.getElementById("sync-panel");
257| if (panel) {
258| panel.remove();
259| return;
260| }
261| // Auto reconnect if disconnected when user opens the panel
262| if (IndexUpdateManager.getState() === "disconnected") {
263| IndexUpdateManager.connect();
264| }
265| panel = document.createElement("div");
266| panel.id = "sync-panel";
267| panel.className = "sync-panel";
268| _renderSyncPanel(panel);
269| document.body.appendChild(panel);
270|
271| // Close on outside click
272| setTimeout(() => {
273| document.addEventListener("click", _closeSyncPanelOutside, { once: true });
274| }, 0);
275|}
276|
277|function _closeSyncPanelOutside(e) {
278| const panel = document.getElementById("sync-panel");
279| if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
280| panel.remove();
281| }
282|}
283|
284|function _renderSyncPanel(panel) {
285| const state = IndexUpdateManager.getState();
286| const events = IndexUpdateManager.getRecentEvents();
287|
288| const stateLabels = {
289| disconnected: "Déconnecté",
290| connecting: "Connexion...",
291| connected: "Connecté",
292| syncing: "Synchronisation...",
293| };
294|
295| let html = `<div class="sync-panel__header">
296| <span class="sync-panel__title">Synchronisation</span>
297| <span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
298| </div>`;
299|
300| if (events.length === 0) {
301| html += `<div class="sync-panel__empty">Aucun événement récent</div>`;
302| } else {
303| html += `<div class="sync-panel__events">`;
304| events.slice(0, 10).forEach((ev) => {
305| const time = new Date(ev.timestamp).toLocaleTimeString();
306| const typeLabels = {
307| index_updated: "Mise à jour",
308| index_reloaded: "Rechargement",
309| vault_added: "Vault ajouté",
310| vault_removed: "Vault supprimé",
311| index_start: "Démarrage index.",
312| index_progress: "Vault indexé",
313| index_complete: "Indexation tech.",
314| };
315| const label = typeLabels[ev.type] || ev.type;
316| let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
317| if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
318| if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
319| if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
320| html += `<div class="sync-panel__event">
321| <span class="sync-panel__event-type">${label}</span>
322| <span class="sync-panel__event-detail">${detail}</span>
323| <span class="sync-panel__event-time">${time}</span>
324| </div>`;
325| });
326| html += `</div>`;
327| }
328|
329| panel.innerHTML = html;
330|}
331|
332|// ---------------------------------------------------------------------------
333|// PWA Service Worker Registration
334|// ---------------------------------------------------------------------------
335|function registerServiceWorker() {
336| if ("serviceWorker" in navigator) {
337| window.addEventListener("load", () => {
338| navigator.serviceWorker
339| .register("/sw.js")
340| .then((registration) => {
341| console.log("[PWA] Service Worker registered successfully:", registration.scope);
342|
343| // Check for updates periodically
344| setInterval(() => {
345| registration.update();
346| }, 60000); // Check every minute
347|
348| // Handle service worker updates
349| registration.addEventListener("updatefound", () => {
350| const newWorker = registration.installing;
351| newWorker.addEventListener("statechange", () => {
352| if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
353| // New service worker available
354| showUpdateNotification();
355| }
356| });
357| });
358| })
359| .catch((error) => {
360| console.log("[PWA] Service Worker registration failed:", error);
361| });
362| });
363| }
364|}
365|
366|function showUpdateNotification() {
367| const message = document.createElement("div");
368| message.className = "pwa-update-notification";
369| message.innerHTML = `
370| <div class="pwa-update-content">
371| <span>Une nouvelle version d'ObsiGate est disponible !</span>
372| <button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
373| <button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
374| </div>
375| `;
376| document.body.appendChild(message);
377|
378| // Auto-dismiss after 30 seconds
379| setTimeout(() => {
380| if (message.parentElement) {
381| message.remove();
382| }
383| }, 30000);
384|}
385|
386|// Handle install prompt
387|let deferredPrompt;
388|window.addEventListener("beforeinstallprompt", (e) => {
389| e.preventDefault();
390| deferredPrompt = e;
391|
392| // Show install button if desired
393| const installBtn = document.getElementById("pwa-install-btn");
394| if (installBtn) {
395| installBtn.style.display = "block";
396| installBtn.addEventListener("click", async () => {
397| if (deferredPrompt) {
398| deferredPrompt.prompt();
399| const { outcome } = await deferredPrompt.userChoice;
400| console.log(`[PWA] User response to install prompt: ${outcome}`);
401| deferredPrompt = null;
402| installBtn.style.display = "none";
403| }
404| });
405| }
406|});
407|
408|// Log when app is installed
409|window.addEventListener("appinstalled", () => {
410| console.log("[PWA] ObsiGate has been installed");
411| showToast("ObsiGate installé avec succès !", "success");
412|});
413|
414|// ── Dashboard tab switching (runs on page load and after rebuild) ──
415|function initDashboardTabs() {
416| document.querySelectorAll(".dashboard-tab").forEach(tab => {
417| // Remove existing listeners by cloning
418| const newTab = tab.cloneNode(true);
419| tab.parentNode.replaceChild(newTab, tab);
420| newTab.addEventListener("click", function() {
421| document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
422| document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
423| this.classList.add("active");
424| const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
425| if (panel) panel.classList.add("active");
426| });
427| });
428|}
429|
430|// ---------------------------------------------------------------------------
431|// Init — called by app.js orchestrator
432|// ---------------------------------------------------------------------------
433|export { initSyncStatus };
434|export function init() {
435| registerServiceWorker();
436| initDashboardTabs();
437|}
438|
1|/* ObsiGate — Sync: SSE client + PWA registration */
import {
state.currentVault,
state.currentPath,
state.activeSidebarTab,
selectedContextVault
} from './state.js';
import { showToast } from './ui.js';
import { loadVaults, loadTags, refreshTagsForContext } from './sidebar.js';
// ---------------------------------------------------------------------------
// SSE Client — IndexUpdateManager
// ---------------------------------------------------------------------------
export const IndexUpdateManager = (() => {
let eventSource = null;
let reconnectTimer = null;
let reconnectDelay = 1000;
const MAX_RECONNECT_DELAY = 30000;
let recentEvents = [];
const MAX_RECENT_EVENTS = 20;
let connectionState = "disconnected"; // disconnected | connecting | connected
function connect() {
if (eventSource) {
eventSource.close();
}
connectionState = "connecting";
_updateBadge();
eventSource = new EventSource("/api/events");
eventSource.addEventListener("connected", (e) => {
connectionState = "connected";
reconnectDelay = 1000;
_updateBadge();
});
eventSource.addEventListener("index_updated", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_updated", data);
_onIndexUpdated(data);
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_reloaded", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_reloaded", data);
_onIndexReloaded(data);
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("vault_added", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("vault_added", data);
showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("vault_removed", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("vault_removed", data);
showToast(`Vault "${data.vault}" supprimé`, "info");
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_start", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_start", data);
connectionState = "syncing";
_updateBadge();
showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_progress", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_progress", data);
connectionState = "syncing";
_updateBadge();
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_complete", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_complete", data);
connectionState = "connected";
_updateBadge();
showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.onerror = () => {
connectionState = "disconnected";
_updateBadge();
eventSource.close();
eventSource = null;
_scheduleReconnect();
};
}
function _scheduleReconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
connect();
}, reconnectDelay);
}
function _addEvent(type, data) {
recentEvents.unshift({
type,
data,
timestamp: new Date().toISOString(),
});
if (recentEvents.length > MAX_RECENT_EVENTS) {
recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
}
}
async function _onIndexUpdated(data) {
// Brief syncing state
connectionState = "syncing";
_updateBadge();
const n = data.total_changes || 0;
const vaults = (data.vaults || []).join(", ");
// Toast removed: silent auto-indexing — no notification needed
// Refresh sidebar and tags if affected vault matches current context
const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
if (affectsCurrentVault) {
try {
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
// Refresh current file if it was updated
if (currentVault && state.currentPath) {
const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath);
if (changed) {
openFile(state.currentVault, state.currentPath);
}
}
} catch (err) {
console.error("Error refreshing after index update:", err);
}
}
// Refresh recent tab if it is active
if (state.activeSidebarTab === "recent") {
const vaultFilter = document.getElementById("recent-vault-filter");
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
}
setTimeout(() => {
connectionState = "connected";
_updateBadge();
}, 1500);
}
async function _onIndexReloaded(data) {
connectionState = "syncing";
_updateBadge();
showToast("Index complet rechargé", "info");
try {
await Promise.all([loadVaults(), loadTags()]);
} catch (err) {
console.error("Error refreshing after full reload:", err);
}
setTimeout(() => {
connectionState = "connected";
_updateBadge();
}, 1500);
}
function _updateBadge() {
const badge = document.getElementById("sync-badge");
if (!badge) return;
badge.className = "sync-badge sync-badge--" + connectionState;
const labels = {
disconnected: "Déconnecté",
connecting: "Connexion...",
connected: "Synchronisé",
syncing: "Mise à jour...",
};
badge.title = labels[connectionState] || connectionState;
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
connectionState = "disconnected";
_updateBadge();
}
function getState() {
return connectionState;
}
function getRecentEvents() {
return recentEvents;
}
return { connect, disconnect, getState, getRecentEvents };
})();
// ---------------------------------------------------------------------------
// Sync status badge and panel
// ---------------------------------------------------------------------------
function initSyncStatus() {
const badge = document.getElementById("sync-badge");
if (!badge) return;
badge.addEventListener("click", (e) => {
e.stopPropagation();
toggleSyncPanel();
});
IndexUpdateManager.connect();
}
function toggleSyncPanel() {
let panel = document.getElementById("sync-panel");
if (panel) {
panel.remove();
return;
}
// Auto reconnect if disconnected when user opens the panel
if (IndexUpdateManager.getState() === "disconnected") {
IndexUpdateManager.connect();
}
panel = document.createElement("div");
panel.id = "sync-panel";
panel.className = "sync-panel";
_renderSyncPanel(panel);
document.body.appendChild(panel);
// Close on outside click
setTimeout(() => {
document.addEventListener("click", _closeSyncPanelOutside, { once: true });
}, 0);
}
function _closeSyncPanelOutside(e) {
const panel = document.getElementById("sync-panel");
if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
panel.remove();
}
}
function _renderSyncPanel(panel) {
const state = IndexUpdateManager.getState();
const events = IndexUpdateManager.getRecentEvents();
const stateLabels = {
disconnected: "Déconnecté",
connecting: "Connexion...",
connected: "Connecté",
syncing: "Synchronisation...",
};
let html = `<div class="sync-panel__header">
<span class="sync-panel__title">Synchronisation</span>
<span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
</div>`;
if (events.length === 0) {
html += `<div class="sync-panel__empty">Aucun événement récent</div>`;
} else {
html += `<div class="sync-panel__events">`;
events.slice(0, 10).forEach((ev) => {
const time = new Date(ev.timestamp).toLocaleTimeString();
const typeLabels = {
index_updated: "Mise à jour",
index_reloaded: "Rechargement",
vault_added: "Vault ajouté",
vault_removed: "Vault supprimé",
index_start: "Démarrage index.",
index_progress: "Vault indexé",
index_complete: "Indexation tech.",
};
const label = typeLabels[ev.type] || ev.type;
let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
html += `<div class="sync-panel__event">
<span class="sync-panel__event-type">${label}</span>
<span class="sync-panel__event-detail">${detail}</span>
<span class="sync-panel__event-time">${time}</span>
</div>`;
});
html += `</div>`;
}
panel.innerHTML = html;
}
// ---------------------------------------------------------------------------
// PWA Service Worker Registration
// ---------------------------------------------------------------------------
function registerServiceWorker() {
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("[PWA] Service Worker registered successfully:", registration.scope);
// Check for updates periodically
setInterval(() => {
registration.update();
}, 60000); // Check every minute
// Handle service worker updates
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
// New service worker available
showUpdateNotification();
}
});
});
})
.catch((error) => {
console.log("[PWA] Service Worker registration failed:", error);
});
});
}
}
function showUpdateNotification() {
const message = document.createElement("div");
message.className = "pwa-update-notification";
message.innerHTML = `
<div class="pwa-update-content">
<span>Une nouvelle version d'ObsiGate est disponible !</span>
<button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
<button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
`;
document.body.appendChild(message);
// Auto-dismiss after 30 seconds
setTimeout(() => {
if (message.parentElement) {
message.remove();
}
}, 30000);
}
// Handle install prompt
let deferredPrompt;
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
deferredPrompt = e;
// Show install button if desired
const installBtn = document.getElementById("pwa-install-btn");
if (installBtn) {
installBtn.style.display = "block";
installBtn.addEventListener("click", async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`[PWA] User response to install prompt: ${outcome}`);
deferredPrompt = null;
installBtn.style.display = "none";
}
});
}
});
// Log when app is installed
window.addEventListener("appinstalled", () => {
console.log("[PWA] ObsiGate has been installed");
showToast("ObsiGate installé avec succès !", "success");
});
// ── Dashboard tab switching (runs on page load and after rebuild) ──
function initDashboardTabs() {
document.querySelectorAll(".dashboard-tab").forEach(tab => {
// Remove existing listeners by cloning
const newTab = tab.cloneNode(true);
tab.parentNode.replaceChild(newTab, tab);
newTab.addEventListener("click", function() {
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
this.classList.add("active");
const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
if (panel) panel.classList.add("active");
});
});
}
// ---------------------------------------------------------------------------
// Init — called by app.js orchestrator
// ---------------------------------------------------------------------------
export { initSyncStatus };
export function init() {
registerServiceWorker();
initDashboardTabs();
}