const favoritesManager = { apiBase: window.location.origin, _initialized: false, _loadPromise: null, groups: [], favoritesByKey: new Map(), favoritesById: new Map(), listeners: new Set(), getAuthHeaders() { const token = localStorage.getItem('accessToken'); const apiKey = localStorage.getItem('apiKey'); const headers = { 'Content-Type': 'application/json' }; if (token) { headers['Authorization'] = `Bearer ${token}`; } else if (apiKey) { headers['X-API-Key'] = apiKey; } return headers; }, async fetchAPI(endpoint, options = {}) { const response = await fetch(`${this.apiBase}${endpoint}`, { ...options, headers: { ...this.getAuthHeaders(), ...(options.headers || {}) } }); if (!response.ok) { if (response.status === 401) { if (window.dashboard?.logout) { window.dashboard.logout(); } return null; } throw new Error(`API Error: ${response.status}`); } if (response.status === 204) return null; return response.json(); }, setData(groups, favorites) { this.groups = Array.isArray(groups) ? groups : []; this.favoritesByKey.clear(); this.favoritesById.clear(); (favorites || []).forEach(f => { const dc = f?.docker_container; if (!dc?.host_id || !dc?.container_id) return; const key = `${dc.host_id}::${dc.container_id}`; this.favoritesByKey.set(key, f); this.favoritesById.set(f.id, f); }); this.refreshStarsInDom(); this._emitChange(); }, async load() { const [groupsRes, favsRes] = await Promise.all([ this.fetchAPI('/api/favorites/groups').catch(() => ({ groups: [] })), this.fetchAPI('/api/favorites/containers').catch(() => ({ containers: [] })), ]); this.setData(groupsRes?.groups || [], favsRes?.containers || []); }, async ensureInit() { if (this._initialized) return; if (this._loadPromise) { await this._loadPromise; return; } this._loadPromise = (async () => { try { await this.load(); } finally { this._initialized = true; } })(); await this._loadPromise; }, onChange(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); }, _emitChange() { for (const cb of this.listeners) { try { cb(); } catch (e) { console.error('favoritesManager listener error:', e); } } }, _key(hostId, containerId) { return `${hostId}::${containerId}`; }, isFavorite(hostId, containerId) { return this.favoritesByKey.has(this._key(hostId, containerId)); }, getFavoriteId(hostId, containerId) { const fav = this.favoritesByKey.get(this._key(hostId, containerId)); return fav ? fav.id : null; }, async addOrUpdateFavorite(hostId, containerId, groupId = null) { const created = await this.fetchAPI('/api/favorites/containers', { method: 'POST', body: JSON.stringify({ host_id: hostId, container_id: containerId, group_id: groupId }) }); if (!created) return null; const key = this._key(hostId, containerId); this.favoritesByKey.set(key, created); this.favoritesById.set(created.id, created); this.refreshStarsInDom(); this._emitChange(); return created; }, async moveFavoriteToGroup(hostId, containerId, groupId = null) { return await this.addOrUpdateFavorite(hostId, containerId, groupId); }, async listGroups() { const res = await this.fetchAPI('/api/favorites/groups'); this.groups = res?.groups || []; this._emitChange(); return this.groups; }, async createGroup({ name, sort_order = 0, color = null, icon_key = null }) { const created = await this.fetchAPI('/api/favorites/groups', { method: 'POST', body: JSON.stringify({ name, sort_order, color, icon_key }) }); await this.listGroups(); return created; }, async updateGroup(groupId, { name = null, sort_order = null, color = null, icon_key = null }) { const updated = await this.fetchAPI(`/api/favorites/groups/${groupId}`, { method: 'PATCH', body: JSON.stringify({ name, sort_order, color, icon_key }) }); await this.listGroups(); return updated; }, async deleteGroup(groupId) { await this.fetchAPI(`/api/favorites/groups/${groupId}`, { method: 'DELETE' }); await this.listGroups(); await this.load(); }, async removeFavoriteById(favoriteId) { await this.fetchAPI(`/api/favorites/containers/${favoriteId}`, { method: 'DELETE' }); const existing = this.favoritesById.get(favoriteId); if (existing?.docker_container?.host_id && existing?.docker_container?.container_id) { const key = this._key(existing.docker_container.host_id, existing.docker_container.container_id); this.favoritesByKey.delete(key); } this.favoritesById.delete(favoriteId); this.refreshStarsInDom(); this._emitChange(); }, async toggleFavorite(hostId, containerId) { const key = this._key(hostId, containerId); const existing = this.favoritesByKey.get(key); if (existing?.id) { await this.removeFavoriteById(existing.id); return { favorited: false }; } await this.addOrUpdateFavorite(hostId, containerId, null); return { favorited: true }; }, refreshStarsInDom() { const buttons = document.querySelectorAll('[data-fav-key]'); buttons.forEach(btn => { const key = btn.getAttribute('data-fav-key'); const isFav = this.favoritesByKey.has(key); const icon = btn.querySelector('i'); if (icon) { icon.classList.toggle('fas', isFav); icon.classList.toggle('far', !isFav); } btn.classList.toggle('text-purple-400', isFav); btn.classList.toggle('text-gray-400', !isFav); btn.setAttribute('title', isFav ? 'Retirer des favoris' : 'Ajouter aux favoris'); btn.setAttribute('aria-pressed', isFav ? 'true' : 'false'); }); }, }; window.favoritesManager = favoritesManager;