diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a000f25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,29 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+
+# Virtual environments
+venv/
+env/
+ENV/
+.venv
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Project specific
+*.log
+*.db
+*.sqlite
+
diff --git a/src/static/profiles.js b/src/static/profiles.js
index 4b514c0..33e7b82 100644
--- a/src/static/profiles.js
+++ b/src/static/profiles.js
@@ -26,7 +26,11 @@ document.addEventListener("DOMContentLoaded", () => {
if (Array.isArray(profiles)) {
entries = profiles.map((profileId) => [profileId, {}]);
} else if (profiles && typeof profiles === "object") {
- entries = Object.entries(profiles);
+ // Make sure we're iterating over profile entries, not metadata
+ entries = Object.entries(profiles).filter(([key]) => {
+ // Skip metadata keys like 'current_profile_id' if they exist
+ return key !== 'current_profile_id' && key !== 'profiles';
+ });
}
if (entries.length === 0) {
@@ -113,19 +117,10 @@ document.addEventListener("DOMContentLoaded", () => {
if (!response.ok) {
throw new Error("Failed to load profiles");
}
- const profiles = await response.json();
- let currentProfileId = null;
- try {
- const currentResponse = await fetch("/profiles/current", {
- headers: { Accept: "application/json" },
- });
- if (currentResponse.ok) {
- const currentData = await currentResponse.json();
- currentProfileId = currentData.id || null;
- }
- } catch (error) {
- console.warn("Failed to load current profile:", error);
- }
+ const data = await response.json();
+ // Handle both old format (just profiles object) and new format (with current_profile_id)
+ const profiles = data.profiles || data;
+ const currentProfileId = data.current_profile_id || null;
renderProfiles(profiles, currentProfileId);
} catch (error) {
console.error("Load profiles failed:", error);
diff --git a/src/static/tabs.js b/src/static/tabs.js
new file mode 100644
index 0000000..5a59147
--- /dev/null
+++ b/src/static/tabs.js
@@ -0,0 +1,471 @@
+// Tab management JavaScript
+let currentTabId = null;
+
+// Get current tab from cookie
+function getCurrentTabFromCookie() {
+ const cookies = document.cookie.split(';');
+ for (let cookie of cookies) {
+ const [name, value] = cookie.trim().split('=');
+ if (name === 'current_tab') {
+ return value;
+ }
+ }
+ return null;
+}
+
+// Load tabs list
+async function loadTabs() {
+ try {
+ const response = await fetch('/tabs');
+ const data = await response.json();
+
+ // Get current tab from cookie first, then from server response
+ currentTabId = getCurrentTabFromCookie() || data.current_tab_id;
+ renderTabsList(data.tabs, data.tab_order, currentTabId);
+
+ // Load current tab content if available
+ if (currentTabId) {
+ loadTabContent(currentTabId);
+ } else if (data.tab_order && data.tab_order.length > 0) {
+ // Set first tab as current if none is set
+ await setCurrentTab(data.tab_order[0]);
+ }
+ } catch (error) {
+ console.error('Failed to load tabs:', error);
+ const container = document.getElementById('tabs-list');
+ if (container) {
+ container.innerHTML = '
Failed to load tabs
';
+ }
+ }
+}
+
+// Render tabs list in the main UI
+function renderTabsList(tabs, tabOrder, currentTabId) {
+ const container = document.getElementById('tabs-list');
+ if (!container) return;
+
+ if (!tabOrder || tabOrder.length === 0) {
+ container.innerHTML = 'No tabs available
';
+ return;
+ }
+
+ let html = '';
+ for (const tabId of tabOrder) {
+ const tab = tabs[tabId];
+ if (tab) {
+ const activeClass = tabId === currentTabId ? 'active' : '';
+ const tabName = tab.name || `Tab ${tabId}`;
+ html += `
+
+ `;
+ }
+ }
+ html += '
';
+ container.innerHTML = html;
+}
+
+// Render tabs list in modal (like profiles)
+function renderTabsListModal(tabs, tabOrder, currentTabId) {
+ const container = document.getElementById('tabs-list-modal');
+ if (!container) return;
+
+ container.innerHTML = "";
+ let entries = [];
+
+ if (Array.isArray(tabOrder)) {
+ entries = tabOrder.map((tabId) => [tabId, tabs[tabId] || {}]);
+ } else if (tabs && typeof tabs === "object") {
+ entries = Object.entries(tabs).filter(([key]) => {
+ return key !== 'current_tab_id' && key !== 'tabs' && key !== 'tab_order';
+ });
+ }
+
+ if (entries.length === 0) {
+ const empty = document.createElement("p");
+ empty.className = "muted-text";
+ empty.textContent = "No tabs found.";
+ container.appendChild(empty);
+ return;
+ }
+
+ entries.forEach(([tabId, tab]) => {
+ const row = document.createElement("div");
+ row.className = "profiles-row";
+
+ const label = document.createElement("span");
+ label.textContent = (tab && tab.name) || tabId;
+ if (String(tabId) === String(currentTabId)) {
+ label.textContent = `✓ ${label.textContent}`;
+ label.style.fontWeight = "bold";
+ label.style.color = "#FFD700";
+ }
+
+ const applyButton = document.createElement("button");
+ applyButton.className = "btn btn-secondary btn-small";
+ applyButton.textContent = "Select";
+ applyButton.addEventListener("click", async () => {
+ await selectTab(tabId);
+ document.getElementById('tabs-modal').classList.remove('active');
+ });
+
+ const editButton = document.createElement("button");
+ editButton.className = "btn btn-secondary btn-small";
+ editButton.textContent = "Edit";
+ editButton.addEventListener("click", () => {
+ openEditTabModal(tabId, tab);
+ });
+
+ const deleteButton = document.createElement("button");
+ deleteButton.className = "btn btn-danger btn-small";
+ deleteButton.textContent = "Delete";
+ deleteButton.addEventListener("click", async () => {
+ const confirmed = confirm(`Delete tab "${label.textContent}"?`);
+ if (!confirmed) {
+ return;
+ }
+ try {
+ const response = await fetch(`/tabs/${tabId}`, {
+ method: "DELETE",
+ headers: { Accept: "application/json" },
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ error: "Failed to delete tab" }));
+ throw new Error(errorData.error || "Failed to delete tab");
+ }
+ // Clear cookie if deleted tab was current
+ if (tabId === currentTabId) {
+ document.cookie = 'current_tab=; path=/; max-age=0';
+ currentTabId = null;
+ }
+ await loadTabsModal();
+ await loadTabs(); // Reload main tabs list
+ } catch (error) {
+ console.error("Delete tab failed:", error);
+ alert("Failed to delete tab: " + error.message);
+ }
+ });
+
+ row.appendChild(label);
+ row.appendChild(applyButton);
+ row.appendChild(editButton);
+ row.appendChild(deleteButton);
+ container.appendChild(row);
+ });
+}
+
+// Load tabs in modal
+async function loadTabsModal() {
+ const container = document.getElementById('tabs-list-modal');
+ if (!container) return;
+
+ container.innerHTML = "";
+ const loading = document.createElement("p");
+ loading.className = "muted-text";
+ loading.textContent = "Loading tabs...";
+ container.appendChild(loading);
+
+ try {
+ const response = await fetch("/tabs", {
+ headers: { Accept: "application/json" },
+ });
+ if (!response.ok) {
+ throw new Error("Failed to load tabs");
+ }
+ const data = await response.json();
+ const tabs = data.tabs || data;
+ const currentTabId = getCurrentTabFromCookie() || data.current_tab_id || null;
+ renderTabsListModal(tabs, data.tab_order || [], currentTabId);
+ } catch (error) {
+ console.error("Load tabs failed:", error);
+ container.innerHTML = "";
+ const errorMessage = document.createElement("p");
+ errorMessage.className = "muted-text";
+ errorMessage.textContent = "Failed to load tabs.";
+ container.appendChild(errorMessage);
+ }
+}
+
+// Select a tab
+async function selectTab(tabId) {
+ // Update active state
+ document.querySelectorAll('.tab-button').forEach(btn => {
+ btn.classList.remove('active');
+ });
+ const btn = document.querySelector(`[data-tab-id="${tabId}"]`);
+ if (btn) {
+ btn.classList.add('active');
+ }
+
+ // Set as current tab
+ await setCurrentTab(tabId);
+ // Load tab content
+ loadTabContent(tabId);
+}
+
+// Set current tab in cookie
+async function setCurrentTab(tabId) {
+ try {
+ const response = await fetch(`/tabs/${tabId}/set-current`, {
+ method: 'POST'
+ });
+ const data = await response.json();
+ if (response.ok) {
+ currentTabId = tabId;
+ // Also set cookie on client side
+ document.cookie = `current_tab=${tabId}; path=/; max-age=31536000`;
+ } else {
+ console.error('Failed to set current tab:', data.error);
+ }
+ } catch (error) {
+ console.error('Error setting current tab:', error);
+ }
+}
+
+// Load tab content
+async function loadTabContent(tabId) {
+ const container = document.getElementById('tab-content');
+ if (!container) return;
+
+ try {
+ const response = await fetch(`/tabs/${tabId}`);
+ const tab = await response.json();
+
+ if (tab.error) {
+ container.innerHTML = `${tab.error}
`;
+ return;
+ }
+
+ // Render tab content (presets section)
+ const tabName = tab.name || `Tab ${tabId}`;
+ container.innerHTML = `
+
+
Presets
+
+
+
+
+
+
+
+ `;
+
+ // Trigger presets loading if the function exists
+ if (typeof renderTabPresets === 'function') {
+ renderTabPresets(tabId);
+ }
+ } catch (error) {
+ console.error('Failed to load tab content:', error);
+ container.innerHTML = 'Failed to load tab content
';
+ }
+}
+
+// Open edit tab modal
+function openEditTabModal(tabId, tab) {
+ const modal = document.getElementById('edit-tab-modal');
+ const idInput = document.getElementById('edit-tab-id');
+ const nameInput = document.getElementById('edit-tab-name');
+ const idsInput = document.getElementById('edit-tab-ids');
+
+ if (idInput) idInput.value = tabId;
+ if (nameInput) nameInput.value = tab ? (tab.name || '') : '';
+ if (idsInput) idsInput.value = tab && tab.names ? tab.names.join(', ') : '1';
+
+ if (modal) modal.classList.add('active');
+}
+
+// Update an existing tab
+async function updateTab(tabId, name, ids) {
+ try {
+ const names = ids ? ids.split(',').map(id => id.trim()) : ['1'];
+ const response = await fetch(`/tabs/${tabId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ name: name,
+ names: names
+ })
+ });
+
+ const data = await response.json();
+ if (response.ok) {
+ // Reload tabs list
+ await loadTabsModal();
+ await loadTabs();
+ // Close modal
+ document.getElementById('edit-tab-modal').classList.remove('active');
+ return true;
+ } else {
+ alert(`Error: ${data.error || 'Failed to update tab'}`);
+ return false;
+ }
+ } catch (error) {
+ console.error('Failed to update tab:', error);
+ alert('Failed to update tab');
+ return false;
+ }
+}
+
+// Create a new tab
+async function createTab(name, ids) {
+ try {
+ const names = ids ? ids.split(',').map(id => id.trim()) : ['1'];
+ const response = await fetch('/tabs', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ name: name,
+ names: names
+ })
+ });
+
+ const data = await response.json();
+ if (response.ok) {
+ // Reload tabs list
+ await loadTabsModal();
+ await loadTabs();
+ // Select the new tab
+ if (data && Object.keys(data).length > 0) {
+ const newTabId = Object.keys(data)[0];
+ await selectTab(newTabId);
+ }
+ return true;
+ } else {
+ alert(`Error: ${data.error || 'Failed to create tab'}`);
+ return false;
+ }
+ } catch (error) {
+ console.error('Failed to create tab:', error);
+ alert('Failed to create tab');
+ return false;
+ }
+}
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', () => {
+ loadTabs();
+
+ // Set up tabs modal
+ const tabsButton = document.getElementById('tabs-btn');
+ const tabsModal = document.getElementById('tabs-modal');
+ const tabsCloseButton = document.getElementById('tabs-close-btn');
+ const newTabNameInput = document.getElementById('new-tab-name');
+ const newTabIdsInput = document.getElementById('new-tab-ids');
+ const createTabButton = document.getElementById('create-tab-btn');
+
+ // Set up edit tab button in header
+ const editTabBtn = document.getElementById('edit-tab-btn');
+ if (editTabBtn) {
+ editTabBtn.addEventListener('click', async () => {
+ if (!currentTabId) {
+ alert('No tab selected. Please select a tab first.');
+ return;
+ }
+ try {
+ const response = await fetch(`/tabs/${currentTabId}`);
+ if (response.ok) {
+ const tab = await response.json();
+ openEditTabModal(currentTabId, tab);
+ } else {
+ alert('Failed to load tab for editing');
+ }
+ } catch (error) {
+ console.error('Failed to load tab:', error);
+ alert('Failed to load tab for editing');
+ }
+ });
+ }
+
+ if (tabsButton && tabsModal) {
+ tabsButton.addEventListener('click', () => {
+ tabsModal.classList.add('active');
+ loadTabsModal();
+ });
+ }
+
+ if (tabsCloseButton) {
+ tabsCloseButton.addEventListener('click', () => {
+ tabsModal.classList.remove('active');
+ });
+ }
+
+ if (tabsModal) {
+ tabsModal.addEventListener('click', (event) => {
+ if (event.target === tabsModal) {
+ tabsModal.classList.remove('active');
+ }
+ });
+ }
+
+ // Set up create tab
+ const createTabHandler = async () => {
+ if (!newTabNameInput) return;
+ const name = newTabNameInput.value.trim();
+ const ids = (newTabIdsInput && newTabIdsInput.value.trim()) || '1';
+
+ if (name) {
+ await createTab(name, ids);
+ if (newTabNameInput) newTabNameInput.value = '';
+ if (newTabIdsInput) newTabIdsInput.value = '1';
+ }
+ };
+
+ if (createTabButton) {
+ createTabButton.addEventListener('click', createTabHandler);
+ }
+
+ if (newTabNameInput) {
+ newTabNameInput.addEventListener('keypress', (event) => {
+ if (event.key === 'Enter') {
+ createTabHandler();
+ }
+ });
+ }
+
+ // Set up edit tab form
+ const editTabForm = document.getElementById('edit-tab-form');
+ if (editTabForm) {
+ editTabForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const idInput = document.getElementById('edit-tab-id');
+ const nameInput = document.getElementById('edit-tab-name');
+ const idsInput = document.getElementById('edit-tab-ids');
+
+ const tabId = idInput ? idInput.value : null;
+ const name = nameInput ? nameInput.value.trim() : '';
+ const ids = idsInput ? idsInput.value.trim() : '1';
+
+ if (tabId && name) {
+ await updateTab(tabId, name, ids);
+ editTabForm.reset();
+ }
+ });
+ }
+
+ // Close edit modal when clicking outside
+ const editTabModal = document.getElementById('edit-tab-modal');
+ if (editTabModal) {
+ editTabModal.addEventListener('click', (event) => {
+ if (event.target === editTabModal) {
+ editTabModal.classList.remove('active');
+ }
+ });
+ }
+});
+
+// Export for use in other scripts
+window.tabsManager = {
+ loadTabs,
+ selectTab,
+ createTab,
+ updateTab,
+ openEditTabModal,
+ getCurrentTabId: () => currentTabId
+};
diff --git a/src/templates/index.html b/src/templates/index.html
index ea67a95..3cf444d 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -5,29 +5,13 @@
LED Controller - Tab Mode
-
LED Controller - Tab Mode