homelab_automation/app/favorites_manager.js
Bruno Charest 661d005fc7
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Add favorites feature with toggle filters, group management, color/icon pickers, dashboard widget, and star buttons across containers/docker sections with persistent storage and real-time UI updates
2025-12-23 14:56:31 -05:00

226 lines
6.8 KiB
JavaScript

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;