diff --git a/html/portal.html b/html/portal.html index 98601cd..a4e9681 100644 --- a/html/portal.html +++ b/html/portal.html @@ -17,7 +17,7 @@

Configure your DeskHog

WiFi

-
+
@@ -54,7 +54,12 @@

API configuration

- + +
+ + +
+
@@ -64,7 +69,7 @@

API configuration

Insights

- +
diff --git a/html/portal.js b/html/portal.js index 649cee8..db8e8e8 100644 --- a/html/portal.js +++ b/html/portal.js @@ -19,50 +19,54 @@ function showScreen(screenId) { } // Handle WiFi form submission -function saveWifiConfig() { - const form = document.getElementById('wifi-form'); - const formData = new FormData(form); +async function saveWifiConfig(event) { + event.preventDefault(); // Prevent form submission immediately - fetch('/save-wifi', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { + try { + const form = document.getElementById('wifi-form'); + const formData = new FormData(form); + + const response = await fetch('/save-wifi', { + method: 'POST', + body: formData + }); + const data = await response.json(); + if (data.success) { showScreen('success-screen'); } else { showScreen('error-screen'); } - }) - .catch(() => { + } catch (error) { + console.error('Error saving WiFi config:', error); showScreen('error-screen'); - }); - - return false; // Prevent default form submission + } + return false; } // Handle device config form submission -function saveDeviceConfig() { - const form = document.getElementById('device-form'); - const formData = new FormData(form); +async function saveDeviceConfig(event) { + event.preventDefault(); // Prevent form submission immediately - fetch('/save-device-config', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { + try { + const form = document.getElementById('device-form'); + const formData = new FormData(form); + + const response = await fetch('/save-device-config', { + method: 'POST', + body: formData + }); + const data = await response.json(); + if (data.success) { showScreen('success-screen'); } else { showScreen('error-screen'); } - }) - .catch(() => { + } catch (error) { + console.error('Error saving device config:', error); showScreen('error-screen'); - }); - + } return false; } @@ -73,61 +77,65 @@ function toggleApiKeyVisibility() { } // Add new insight -function addInsight() { - const form = document.getElementById('insight-form'); - const formData = new FormData(form); +async function addInsight(event) { + event.preventDefault(); // Prevent form submission immediately - fetch('/save-insight', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { + try { + const form = document.getElementById('insight-form'); + const formData = new FormData(form); + + const response = await fetch('/save-insight', { + method: 'POST', + body: formData + }); + const data = await response.json(); + if (data.success) { form.reset(); - loadInsights(); + await loadInsights(); } else { showScreen('error-screen'); } - }) - .catch(() => { + } catch (error) { + console.error('Error adding insight:', error); showScreen('error-screen'); - }); - + } return false; } // Delete insight -function deleteInsight(id) { +async function deleteInsight(id) { if (!confirm('Are you sure you want to delete this insight?')) { return; } - fetch('/delete-insight', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: id }) - }) - .then(response => response.json()) - .then(data => { + try { + const response = await fetch('/delete-insight', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: id }) + }); + const data = await response.json(); + if (data.success) { - loadInsights(); + await loadInsights(); } else { showScreen('error-screen'); } - }) - .catch(() => { + } catch (error) { + console.error('Error deleting insight:', error); showScreen('error-screen'); - }); + } } // Load insights list -function loadInsights() { - fetch('/get-insights') - .then(response => response.json()) - .then(data => { +async function loadInsights() { + try { + const response = await fetch('/get-insights'); + const data = await response.json(); + const container = document.getElementById('insights-list'); container.innerHTML = ''; @@ -149,17 +157,20 @@ function loadInsights() { }); container.appendChild(list); - }) - .catch(error => { + } catch (error) { console.error('Error loading insights:', error); - }); + const container = document.getElementById('insights-list'); + container.innerHTML = '

Error loading insights

'; + } } // Refresh network list -function refreshNetworks() { - fetch('/scan-networks') - .then(response => response.json()) - .then(data => { +async function refreshNetworks(config) { + try { + const response = await fetch('/scan-networks'); + const data = await response.json(); + + const { ssid } = config; const select = document.getElementById('ssid'); select.innerHTML = ''; @@ -189,16 +200,19 @@ function refreshNetworks() { if (network.encrypted) { label += ' 🔒'; } + + if (ssid === network.ssid) { + option.selected = true; + } option.textContent = label; select.appendChild(option); }); - }) - .catch(error => { + } catch (error) { console.error('Error refreshing networks:', error); const select = document.getElementById('ssid'); select.innerHTML = ''; - }); + } } // Start countdown on success screen @@ -218,24 +232,29 @@ function startCountdown() { } // Load current configuration -function loadCurrentConfig() { - fetch('/get-device-config') - .then(response => response.json()) - .then(data => { +async function loadCurrentConfig() { + try { + const response = await fetch('/get-device-config'); + const data = await response.json(); + if (data.teamId !== undefined) { document.getElementById('teamId').value = data.teamId; } if (data.apiKey) { document.getElementById('apiKey').value = data.apiKey; } - }) - .catch(error => { + if (data.baseUrl) { + document.getElementById('baseUrl').value = data.baseUrl; + } + return data; + } catch (error) { console.error('Error loading device config:', error); - }); + throw error; + } } // Initialize page -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', async function() { // Check if we need to show a specific screen based on URL hash const hash = window.location.hash.substr(1); if (hash && ['config-screen', 'success-screen', 'error-screen'].includes(hash)) { @@ -243,11 +262,11 @@ document.addEventListener('DOMContentLoaded', function() { } // Load current configuration - loadCurrentConfig(); + const config = await loadCurrentConfig(); // Load insights list - loadInsights(); + await loadInsights(); // Populate the networks list - refreshNetworks(); + await refreshNetworks(config); }); \ No newline at end of file diff --git a/include/html_portal.h b/include/html_portal.h index 1b7c6e3..055aad1 100644 --- a/include/html_portal.h +++ b/include/html_portal.h @@ -215,50 +215,54 @@ static const char PORTAL_HTML[] PROGMEM = "\n" "}\n" "\n" "// Handle WiFi form submission\n" -"function saveWifiConfig() {\n" -" const form = document.getElementById('wifi-form');\n" -" const formData = new FormData(form);\n" +"async function saveWifiConfig(event) {\n" +" event.preventDefault(); // Prevent form submission immediately\n" " \n" -" fetch('/save-wifi', {\n" -" method: 'POST',\n" -" body: formData\n" -" })\n" -" .then(response => response.json())\n" -" .then(data => {\n" +" try {\n" +" const form = document.getElementById('wifi-form');\n" +" const formData = new FormData(form);\n" +" \n" +" const response = await fetch('/save-wifi', {\n" +" method: 'POST',\n" +" body: formData\n" +" });\n" +" const data = await response.json();\n" +" \n" " if (data.success) {\n" " showScreen('success-screen');\n" " } else {\n" " showScreen('error-screen');\n" " }\n" -" })\n" -" .catch(() => {\n" +" } catch (error) {\n" +" console.error('Error saving WiFi config:', error);\n" " showScreen('error-screen');\n" -" });\n" -" \n" -" return false; // Prevent default form submission\n" +" }\n" +" return false;\n" "}\n" "\n" "// Handle device config form submission\n" -"function saveDeviceConfig() {\n" -" const form = document.getElementById('device-form');\n" -" const formData = new FormData(form);\n" +"async function saveDeviceConfig(event) {\n" +" event.preventDefault(); // Prevent form submission immediately\n" " \n" -" fetch('/save-device-config', {\n" -" method: 'POST',\n" -" body: formData\n" -" })\n" -" .then(response => response.json())\n" -" .then(data => {\n" +" try {\n" +" const form = document.getElementById('device-form');\n" +" const formData = new FormData(form);\n" +" \n" +" const response = await fetch('/save-device-config', {\n" +" method: 'POST',\n" +" body: formData\n" +" });\n" +" const data = await response.json();\n" +" \n" " if (data.success) {\n" " showScreen('success-screen');\n" " } else {\n" " showScreen('error-screen');\n" " }\n" -" })\n" -" .catch(() => {\n" +" } catch (error) {\n" +" console.error('Error saving device config:', error);\n" " showScreen('error-screen');\n" -" });\n" -" \n" +" }\n" " return false;\n" "}\n" "\n" @@ -269,61 +273,65 @@ static const char PORTAL_HTML[] PROGMEM = "\n" "}\n" "\n" "// Add new insight\n" -"function addInsight() {\n" -" const form = document.getElementById('insight-form');\n" -" const formData = new FormData(form);\n" +"async function addInsight(event) {\n" +" event.preventDefault(); // Prevent form submission immediately\n" " \n" -" fetch('/save-insight', {\n" -" method: 'POST',\n" -" body: formData\n" -" })\n" -" .then(response => response.json())\n" -" .then(data => {\n" +" try {\n" +" const form = document.getElementById('insight-form');\n" +" const formData = new FormData(form);\n" +" \n" +" const response = await fetch('/save-insight', {\n" +" method: 'POST',\n" +" body: formData\n" +" });\n" +" const data = await response.json();\n" +" \n" " if (data.success) {\n" " form.reset();\n" -" loadInsights();\n" +" await loadInsights();\n" " } else {\n" " showScreen('error-screen');\n" " }\n" -" })\n" -" .catch(() => {\n" +" } catch (error) {\n" +" console.error('Error adding insight:', error);\n" " showScreen('error-screen');\n" -" });\n" -" \n" +" }\n" " return false;\n" "}\n" "\n" "// Delete insight\n" -"function deleteInsight(id) {\n" +"async function deleteInsight(id) {\n" " if (!confirm('Are you sure you want to delete this insight?')) {\n" " return;\n" " }\n" " \n" -" fetch('/delete-insight', {\n" -" method: 'POST',\n" -" headers: {\n" -" 'Content-Type': 'application/json',\n" -" },\n" -" body: JSON.stringify({ id: id })\n" -" })\n" -" .then(response => response.json())\n" -" .then(data => {\n" +" try {\n" +" const response = await fetch('/delete-insight', {\n" +" method: 'POST',\n" +" headers: {\n" +" 'Content-Type': 'application/json',\n" +" },\n" +" body: JSON.stringify({ id: id })\n" +" });\n" +" const data = await response.json();\n" +" \n" " if (data.success) {\n" -" loadInsights();\n" +" await loadInsights();\n" " } else {\n" " showScreen('error-screen');\n" " }\n" -" })\n" -" .catch(() => {\n" +" } catch (error) {\n" +" console.error('Error deleting insight:', error);\n" " showScreen('error-screen');\n" -" });\n" +" }\n" "}\n" "\n" "// Load insights list\n" -"function loadInsights() {\n" -" fetch('/get-insights')\n" -" .then(response => response.json())\n" -" .then(data => {\n" +"async function loadInsights() {\n" +" try {\n" +" const response = await fetch('/get-insights');\n" +" const data = await response.json();\n" +" \n" " const container = document.getElementById('insights-list');\n" " container.innerHTML = '';\n" " \n" @@ -345,17 +353,20 @@ static const char PORTAL_HTML[] PROGMEM = "\n" " });\n" " \n" " container.appendChild(list);\n" -" })\n" -" .catch(error => {\n" +" } catch (error) {\n" " console.error('Error loading insights:', error);\n" -" });\n" +" const container = document.getElementById('insights-list');\n" +" container.innerHTML = '

Error loading insights

';\n" +" }\n" "}\n" "\n" "// Refresh network list\n" -"function refreshNetworks() {\n" -" fetch('/scan-networks')\n" -" .then(response => response.json())\n" -" .then(data => {\n" +"async function refreshNetworks(config) {\n" +" try {\n" +" const response = await fetch('/scan-networks');\n" +" const data = await response.json();\n" +" \n" +" const { ssid } = config;\n" " const select = document.getElementById('ssid');\n" " select.innerHTML = '';\n" " \n" @@ -385,16 +396,19 @@ static const char PORTAL_HTML[] PROGMEM = "\n" " if (network.encrypted) {\n" " label += ' 🔒';\n" " }\n" +"\n" +" if (ssid === network.ssid) {\n" +" option.selected = true;\n" +" }\n" " \n" " option.textContent = label;\n" " select.appendChild(option);\n" " });\n" -" })\n" -" .catch(error => {\n" +" } catch (error) {\n" " console.error('Error refreshing networks:', error);\n" " const select = document.getElementById('ssid');\n" " select.innerHTML = '';\n" -" });\n" +" }\n" "}\n" "\n" "// Start countdown on success screen\n" @@ -414,24 +428,29 @@ static const char PORTAL_HTML[] PROGMEM = "\n" "}\n" "\n" "// Load current configuration\n" -"function loadCurrentConfig() {\n" -" fetch('/get-device-config')\n" -" .then(response => response.json())\n" -" .then(data => {\n" +"async function loadCurrentConfig() {\n" +" try {\n" +" const response = await fetch('/get-device-config');\n" +" const data = await response.json();\n" +" \n" " if (data.teamId !== undefined) {\n" " document.getElementById('teamId').value = data.teamId;\n" " }\n" " if (data.apiKey) {\n" " document.getElementById('apiKey').value = data.apiKey;\n" " }\n" -" })\n" -" .catch(error => {\n" +" if (data.baseUrl) {\n" +" document.getElementById('baseUrl').value = data.baseUrl;\n" +" }\n" +" return data;\n" +" } catch (error) {\n" " console.error('Error loading device config:', error);\n" -" });\n" +" throw error;\n" +" }\n" "}\n" "\n" "// Initialize page\n" -"document.addEventListener('DOMContentLoaded', function() {\n" +"document.addEventListener('DOMContentLoaded', async function() {\n" " // Check if we need to show a specific screen based on URL hash\n" " const hash = window.location.hash.substr(1);\n" " if (hash && ['config-screen', 'success-screen', 'error-screen'].includes(hash)) {\n" @@ -439,13 +458,13 @@ static const char PORTAL_HTML[] PROGMEM = "\n" " }\n" " \n" " // Load current configuration\n" -" loadCurrentConfig();\n" +" const config = await loadCurrentConfig();\n" " \n" " // Load insights list\n" -" loadInsights();\n" +" await loadInsights();\n" "\n" " // Populate the networks list\n" -" refreshNetworks();\n" +" await refreshNetworks(config);\n" "});\n" "\n" "\n" @@ -459,7 +478,7 @@ static const char PORTAL_HTML[] PROGMEM = "\n" "

WiFi

\n" "
\n" " \n" -" \n" +" \n" "
\n" " \n" @@ -496,7 +515,12 @@ static const char PORTAL_HTML[] PROGMEM = "\n" " \n" " \n" "
\n" -" \n" +"\n" +"
\n" +" \n" +" \n" +"
\n" +"\n" "
\n" " \n" "
\n" @@ -506,7 +530,7 @@ static const char PORTAL_HTML[] PROGMEM = "\n" "

Insights

\n" "\n" "
\n" -" \n" +" \n" "
\n" " \n" " \n" diff --git a/src/ConfigManager.cpp b/src/ConfigManager.cpp index d31a1ca..b129de1 100644 --- a/src/ConfigManager.cpp +++ b/src/ConfigManager.cpp @@ -242,5 +242,42 @@ void ConfigManager::clearApiKey() { // Commit changes commit(); + SystemController::setApiState(ApiState::API_AWAITING_CONFIG); +} + +bool ConfigManager::setBaseUrl(const String& baseUrl) { + if (baseUrl.length() == 0 || baseUrl.length() > MAX_BASE_URL_LENGTH) { + SystemController::setApiState(ApiState::API_CONFIG_INVALID); + return false; + } + + if (!baseUrl.endsWith(PROJECTS_ENDPOINT)) { + SystemController::setApiState(ApiState::API_CONFIG_INVALID); + return false; + } + + _preferences.putString(_baseUrlKey, baseUrl); + + // Commit changes + commit(); + + updateApiConfigurationState(); + return true; +} + +String ConfigManager::getBaseUrl() { + String baseUrl = String(DEFAULT_BASE_URL) + String(PROJECTS_ENDPOINT); + if (!_preferences.isKey(_baseUrlKey)) { + return baseUrl; + } + return _preferences.getString(_baseUrlKey, baseUrl); +} + +void ConfigManager::clearBaseUrl() { + _preferences.remove(_baseUrlKey); + + // Commit changes + commit(); + SystemController::setApiState(ApiState::API_AWAITING_CONFIG); } \ No newline at end of file diff --git a/src/ConfigManager.h b/src/ConfigManager.h index 1b28885..d7e56f4 100644 --- a/src/ConfigManager.h +++ b/src/ConfigManager.h @@ -22,6 +22,8 @@ class ConfigManager { public: static const int NO_TEAM_ID = -1; // Sentinel value for no team ID + static constexpr const char* DEFAULT_BASE_URL = "https://us.posthog.com"; // Sentinel value for no base url + static constexpr const char* PROJECTS_ENDPOINT = "/api/projects/"; // Endpoint for projects /** * @brief Default constructor @@ -113,6 +115,24 @@ class ConfigManager { */ void clearApiKey(); + /** + * @brief Store base url + * @param baseUrl The base url to store + * @return true if saved successfully, false otherwise + */ + bool setBaseUrl(const String& baseUrl); + + /** + * @brief Retrieve stored base url + * @return The base url or DEFAULT_BASE_URL if not set + */ + String getBaseUrl(); + + /** + * @brief Remove stored base url + */ + void clearBaseUrl(); + /** * @brief Store insight configuration * @param id Unique insight identifier @@ -188,6 +208,7 @@ class ConfigManager { // Storage keys for API configuration const char* _teamIdKey = "team_id"; ///< Key for stored team ID const char* _apiKeyKey = "api_key"; ///< Key for stored API key + const char* _baseUrlKey = "base_url"; ///< Key for stored base url // Storage size limits /** @brief Maximum length for WiFi SSID (per IEEE 802.11 spec) */ @@ -198,6 +219,8 @@ class ConfigManager { static const size_t MAX_INSIGHT_LENGTH = 1024; /** @brief Maximum length for API key */ static const size_t MAX_API_KEY_LENGTH = 64; + /** @brief Maximum length for base url */ + static const size_t MAX_BASE_URL_LENGTH = 1024; /** @brief Maximum length for insight identifier */ static const size_t MAX_INSIGHT_ID_LENGTH = 64; diff --git a/src/posthog/PostHogClient.cpp b/src/posthog/PostHogClient.cpp index 2cdb1be..999d722 100644 --- a/src/posthog/PostHogClient.cpp +++ b/src/posthog/PostHogClient.cpp @@ -1,8 +1,6 @@ #include "PostHogClient.h" #include "../ConfigManager.h" -const char* PostHogClient::BASE_URL = "https://us.posthog.com/api/projects/"; - PostHogClient::PostHogClient(ConfigManager& config, EventQueue& eventQueue) : _config(config) , _eventQueue(eventQueue) @@ -26,7 +24,8 @@ void PostHogClient::requestInsightData(const String& insight_id) { bool PostHogClient::isReady() const { return SystemController::isSystemFullyReady() && _config.getTeamId() != ConfigManager::NO_TEAM_ID && - _config.getApiKey().length() > 0; + _config.getApiKey().length() > 0 && + _config.getBaseUrl().length() > 0; } void PostHogClient::process() { @@ -127,7 +126,7 @@ void PostHogClient::checkRefreshes() { } String PostHogClient::buildInsightUrl(const String& insight_id, const char* refresh_mode) const { - String url = String(BASE_URL); + String url = String(_config.getBaseUrl()); url += String(_config.getTeamId()); url += "/insights/?refresh="; url += refresh_mode; diff --git a/src/posthog/PostHogClient.h b/src/posthog/PostHogClient.h index 61ac942..754749a 100644 --- a/src/posthog/PostHogClient.h +++ b/src/posthog/PostHogClient.h @@ -87,7 +87,6 @@ class PostHogClient { unsigned long last_refresh_check; ///< Last refresh timestamp // Constants - static const char* BASE_URL; ///< PostHog API base URL static const unsigned long REFRESH_INTERVAL = 30000; ///< Refresh every 30s static const uint8_t MAX_RETRIES = 3; ///< Max retry attempts static const unsigned long RETRY_DELAY = 1000; ///< Delay between retries diff --git a/src/ui/CaptivePortal.cpp b/src/ui/CaptivePortal.cpp index 7c754ec..016548c 100644 --- a/src/ui/CaptivePortal.cpp +++ b/src/ui/CaptivePortal.cpp @@ -86,6 +86,15 @@ void CaptivePortal::handleGetDeviceConfig() { truncatedKey = truncatedKey.substring(0, 12) + "..."; } doc["apiKey"] = truncatedKey; + doc["baseUrl"] = _configManager.getBaseUrl(); + + // Stored credentials + String _ssid; + String _password; + if (_configManager.getWiFiCredentials(_ssid, _password)) { + doc["ssid"] = _ssid; + doc["password"] = "********"; // Hide the password + } String response; serializeJson(doc, response); @@ -113,6 +122,12 @@ void CaptivePortal::handleSaveDeviceConfig() { message = "Invalid API key"; } } + + // Handle base url + if (_server.hasArg("baseUrl")) { + String baseUrl = _server.arg("baseUrl"); + _configManager.setBaseUrl(baseUrl); + } // Create JSON response StaticJsonDocument<128> doc; diff --git a/src/ui/ProvisioningCard.h b/src/ui/ProvisioningCard.h index c9656c4..bf67510 100644 --- a/src/ui/ProvisioningCard.h +++ b/src/ui/ProvisioningCard.h @@ -2,7 +2,7 @@ #include // LVGL core library #include // For String class (or could be Arduino's String) -#include "hardware/WiFiInterface.h" // Custom WiFi interface class +#include "hardware/WifiInterface.h" // Custom WiFi interface class /** * @class ProvisioningCard