Refactor frontend into ES modules with modern tooling
CI / lint (push) Successful in 25s
CI / security (push) Successful in 19s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 21s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Successful in 25s
CI / security (push) Successful in 19s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 21s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
/* ObsiGate — Sync: SSE client + PWA registration + Offline mode */
|
||||
import { state } from './state.js';
|
||||
import { t } from './i18n.js';
|
||||
import { showToast } from './ui.js';
|
||||
import { api, AuthManager } from './auth.js';
|
||||
import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js';
|
||||
import { loadRecentFiles, initAboutModal } from './config.js';
|
||||
import { openFile } from './viewer.js';
|
||||
import { initOffline, syncFileIndexFromServer } from './offline.js';
|
||||
import { initThemes } from './themes.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(t('sync.vault_added', { vault: data.vault, count: data.stats.file_count }), "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(t('sync.vault_removed', { vault: data.vault }), "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(t('sync.index_started', { count: data.total_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(t('sync.index_complete', { count: data.total_files }), "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 (state.currentVault && state.currentPath) {
|
||||
const changed = (data.changes || []).some((c) => c.vault === state.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(t("sync.index_reloaded"), "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: t('sync.disconnected'),
|
||||
connecting: "Connexion...",
|
||||
connected: t('sync.connected'),
|
||||
syncing: t('sync.updating'),
|
||||
};
|
||||
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();
|
||||
|
||||
// Initialize AI status tracking
|
||||
_initAIStatus();
|
||||
}
|
||||
|
||||
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: t('sync.disconnected'),
|
||||
connecting: "Connexion...",
|
||||
connected: t('sync.connected_short'),
|
||||
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">${t('sync.no_recent_events')}</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: t('sync.event_index_updated'),
|
||||
index_reloaded: "Rechargement",
|
||||
vault_added: t('sync.event_vault_added'),
|
||||
vault_removed: t('sync.event_vault_removed'),
|
||||
index_start: t('sync.event_index_start'),
|
||||
index_progress: t('sync.event_index_progress'),
|
||||
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 = t("sync.vault_files", {vault: ev.data.vault, files: ev.data.files});
|
||||
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;
|
||||
|
||||
// ── AI Status section ──
|
||||
_renderAIStatusSection(panel);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Status Indicator (inside sync panel + header badge animation)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let _aiProcessing = false;
|
||||
let _aiConfigured = null; // null = unknown, true/false
|
||||
let _aiAutocomplete = null; // null = unknown, {available, server_ok, model_loaded, model, error}
|
||||
|
||||
/** Check if AI is configured by calling the /api/ai/status endpoint.
|
||||
* Updates _aiConfigured, _aiAutocomplete and refreshes the panel if open. */
|
||||
async function _checkAIStatus() {
|
||||
try {
|
||||
const status = await api('/api/ai/status');
|
||||
_aiConfigured = status.configured === true;
|
||||
_aiAutocomplete = status.autocomplete || null;
|
||||
} catch {
|
||||
_aiConfigured = false;
|
||||
_aiAutocomplete = null;
|
||||
}
|
||||
_updateAIStatusInPanel();
|
||||
}
|
||||
|
||||
function _renderAIStatusSection(panel) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'sync-panel__ai-section';
|
||||
section.id = 'sync-panel-ai';
|
||||
|
||||
// ---- External provider status ----
|
||||
let extIcon, extText, extClass;
|
||||
if (_aiProcessing) {
|
||||
extIcon = 'loader';
|
||||
extText = t('sync.processing');
|
||||
extClass = 'sync-panel__ai--processing';
|
||||
} else if (_aiConfigured === true) {
|
||||
extIcon = 'check-circle';
|
||||
extText = 'Fournisseur externe actif';
|
||||
extClass = 'sync-panel__ai--active';
|
||||
} else if (_aiConfigured === false) {
|
||||
extIcon = 'x-circle';
|
||||
extText = t('sync.no_provider');
|
||||
extClass = 'sync-panel__ai--inactive';
|
||||
} else {
|
||||
extIcon = 'help-circle';
|
||||
extText = t('sync.verifying');
|
||||
extClass = 'sync-panel__ai--unknown';
|
||||
}
|
||||
|
||||
// ---- Autocomplete (Ollama) status ----
|
||||
let acIcon, acText, acClass;
|
||||
if (_aiAutocomplete && _aiAutocomplete.available) {
|
||||
acIcon = 'check-circle';
|
||||
acText = t('sync.autocomplete_active');
|
||||
acClass = 'sync-panel__ai--active';
|
||||
} else if (_aiAutocomplete && _aiAutocomplete.server_ok && !_aiAutocomplete.model_loaded) {
|
||||
acIcon = 'alert-triangle';
|
||||
acText = t('sync.model_not_loaded');
|
||||
acClass = 'sync-panel__ai--inactive';
|
||||
} else if (_aiAutocomplete && _aiAutocomplete.error) {
|
||||
acIcon = 'x-circle';
|
||||
acText = 'Serveur Ollama injoignable';
|
||||
acClass = 'sync-panel__ai--inactive';
|
||||
} else if (_aiAutocomplete === null) {
|
||||
acIcon = 'help-circle';
|
||||
acText = t('sync.verifying');
|
||||
acClass = 'sync-panel__ai--unknown';
|
||||
} else {
|
||||
acIcon = 'x-circle';
|
||||
acText = t('sync.not_configured');
|
||||
acClass = 'sync-panel__ai--inactive';
|
||||
}
|
||||
|
||||
section.innerHTML = '' +
|
||||
'<div class="sync-panel__ai-title">Intelligence Artificielle</div>' +
|
||||
'<div class="sync-panel__ai-row">' +
|
||||
'<span class="sync-panel__ai-label">Fournisseur</span>' +
|
||||
'<span class="sync-panel__ai-state ' + extClass + '" data-lucide-icon="' + extIcon + '">' +
|
||||
'<i data-lucide="' + extIcon + '" style="width:14px;height:14px"></i>' +
|
||||
extText +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="sync-panel__ai-row">' +
|
||||
'<span class="sync-panel__ai-label">' + t('sync.autocomplete') + '</span>' +
|
||||
'<span class="sync-panel__ai-state ' + acClass + '" data-lucide-icon="' + acIcon + '">' +
|
||||
'<i data-lucide="' + acIcon + '" style="width:14px;height:14px"></i>' +
|
||||
acText +
|
||||
'</span>' +
|
||||
'</div>';
|
||||
|
||||
panel.appendChild(section);
|
||||
// Trigger icon refresh via global
|
||||
if (window._obsigateRefreshIcons) window._obsigateRefreshIcons();
|
||||
}
|
||||
|
||||
/** Update the AI section in the open sync panel. */
|
||||
function _updateAIStatusInPanel() {
|
||||
const section = document.getElementById('sync-panel-ai');
|
||||
if (!section) return;
|
||||
section.innerHTML = '';
|
||||
const panel = document.getElementById('sync-panel');
|
||||
if (panel) {
|
||||
// Re-render just the AI section
|
||||
const oldSection = panel.querySelector('.sync-panel__ai-section');
|
||||
if (oldSection) oldSection.remove();
|
||||
_renderAIStatusSection(panel);
|
||||
}
|
||||
}
|
||||
|
||||
/** Flash the sync badge white during AI processing.
|
||||
* NOTE: now handled via CSS class on #sync-badge.
|
||||
* The visual flash in the editor itself is handled by ai.js
|
||||
* which adds a processing indicator inline. */
|
||||
function _setBadgeAIProcessing(active) {
|
||||
const badge = document.getElementById('sync-badge');
|
||||
if (!badge) return;
|
||||
if (active) {
|
||||
badge.classList.add('sync-badge--ai-processing');
|
||||
} else {
|
||||
badge.classList.remove('sync-badge--ai-processing');
|
||||
}
|
||||
}
|
||||
|
||||
/** Initialize AI status tracking. */
|
||||
function _initAIStatus() {
|
||||
// Check AI status immediately at startup
|
||||
_checkAIStatus();
|
||||
|
||||
// Listen for AI processing events from ai.js
|
||||
document.addEventListener('ai-processing-start', () => {
|
||||
_aiProcessing = true;
|
||||
_setBadgeAIProcessing(true);
|
||||
_updateAIStatusInPanel();
|
||||
});
|
||||
|
||||
document.addEventListener('ai-processing-end', () => {
|
||||
_aiProcessing = false;
|
||||
_setBadgeAIProcessing(false);
|
||||
_updateAIStatusInPanel();
|
||||
});
|
||||
|
||||
// Re-check AI status when sync panel opens (in case config changed)
|
||||
const badge = document.getElementById('sync-badge');
|
||||
if (badge) {
|
||||
badge.addEventListener('click', () => {
|
||||
_checkAIStatus();
|
||||
}, { once: false });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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(t("sync.install_success"), "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, initDashboardTabs };
|
||||
export function init() {
|
||||
registerServiceWorker();
|
||||
initOffline();
|
||||
initThemes();
|
||||
initAboutModal();
|
||||
// Sync file index after a short delay (let vaults load first).
|
||||
// Gate on auth at execution time (not scheduling time) so that
|
||||
// initAuth() has a chance to complete and set _authEnabled / token.
|
||||
// The login handler triggers syncFileIndexFromServer explicitly on
|
||||
// fresh login, so we only do it here for already-authenticated or
|
||||
// auth-disabled sessions.
|
||||
setTimeout(() => {
|
||||
if (!AuthManager._authEnabled || AuthManager.getToken()) {
|
||||
syncFileIndexFromServer();
|
||||
}
|
||||
}, 3000);
|
||||
initDashboardTabs();
|
||||
|
||||
// Listen for Forge editor rename events
|
||||
window.addEventListener('message', function(e) {
|
||||
if (!e.data || !e.data.type) return;
|
||||
if (e.data.type === 'forge-renamed') {
|
||||
var vault = e.data.vault;
|
||||
var oldPath = e.data.oldPath;
|
||||
var newPath = e.data.newPath;
|
||||
console.log('[Forge] File renamed:', vault, oldPath, '->', newPath);
|
||||
var titleEl = document.getElementById('editor-title');
|
||||
if (titleEl && titleEl.textContent.indexOf('Forge:') === 0) {
|
||||
titleEl.textContent = 'Forge: ' + newPath.split('/').pop();
|
||||
}
|
||||
refreshSidebarTreePreservingState(vault);
|
||||
if (state.currentVault === vault && state.currentPath === oldPath) {
|
||||
import('./viewer.js').then(function(m) { m.openFile(vault, newPath); });
|
||||
}
|
||||
}
|
||||
if (e.data.type === 'forge-close') {
|
||||
var modal = document.getElementById('editor-modal');
|
||||
var forgeFrame = document.getElementById('forge-iframe');
|
||||
if (forgeFrame) forgeFrame.remove();
|
||||
if (modal) modal.classList.remove('active');
|
||||
// Restore brand text
|
||||
var brand = modal.querySelector('.editor-brand');
|
||||
if (brand) brand.textContent = 'ObsiGate';
|
||||
// Restore header/footer
|
||||
var headerEl = modal.querySelector('.editor-header');
|
||||
var footerEl = modal.querySelector('.editor-footer');
|
||||
if (headerEl) headerEl.style.display = '';
|
||||
if (footerEl) footerEl.style.display = '';
|
||||
// Refresh current file content
|
||||
if (state.currentVault && state.currentPath) {
|
||||
import('./viewer.js').then(function(m) { m.openFile(state.currentVault, state.currentPath); });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user