-
Calendink Provider 🚀👑🥸
+
Calendink Provider 📅📅🚀🚀
ESP32-S3 System Dashboard v{__APP_VERSION__}
@@ -323,6 +324,11 @@
+ {:else if currentView === 'users'}
+
+
+
+
{/if}
diff --git a/Provider/frontend/src/lib/Sidebar.svelte b/Provider/frontend/src/lib/Sidebar.svelte
index 07e63ee..840ac83 100644
--- a/Provider/frontend/src/lib/Sidebar.svelte
+++ b/Provider/frontend/src/lib/Sidebar.svelte
@@ -5,6 +5,7 @@
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: '🏠' },
{ id: 'tasks', label: 'Tasks', icon: '📋' },
+ { id: 'users', label: 'Users', icon: '👥' },
];
diff --git a/Provider/frontend/src/lib/UserManager.svelte b/Provider/frontend/src/lib/UserManager.svelte
index dabb036..82ab6fb 100644
--- a/Provider/frontend/src/lib/UserManager.svelte
+++ b/Provider/frontend/src/lib/UserManager.svelte
@@ -1,274 +1,436 @@
+
+
+ {#if mode === 'selector'}
+
+
+ {#each users as user}
+
+
selectedUserId = user.id}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => { if (e.key === 'Enter') selectedUserId = user.id; }}
+ >
+ {user.name}
+
+ {/each}
+
+
+ {:else}
+
+
+
+ {#if showAddForm}
+
+ {/if}
+
+
+ {#each users as user}
+
+ {#if editingUserId === user.id}
+
+ {:else}
+
+ {user.name}
+ ID: {user.id}
+
+
+
+ {#if confirmDeleteId === user.id}
+
+
+ {:else}
+
+ {/if}
+
+ {/if}
+
+ {:else}
+
No users found. Create one to get started!
+ {/each}
+
+ {/if}
+
+ {#if error}
+
{error}
+ {/if}
+
- let { selectedUserId = $bindable(null), onUsersChanged = () => {} } = $props();
-
- let users = $state([]);
- let newUserName = $state('');
- let showAddForm = $state(false);
- let error = $state('');
- let confirmDeleteId = $state(null);
-
- async function fetchUsers() {
- try {
- users = await getUsers();
- if (users.length > 0 && !selectedUserId) {
- selectedUserId = users[0].id;
- }
- // If selected user was deleted, select first available
- if (selectedUserId && !users.find(u => u.id === selectedUserId)) {
- selectedUserId = users.length > 0 ? users[0].id : null;
- }
- error = '';
- } catch (e) {
- error = e.message;
- }
- }
-
- async function handleAddUser() {
- if (!newUserName.trim()) return;
- try {
- const user = await addUser(newUserName.trim());
- newUserName = '';
- showAddForm = false;
- await fetchUsers();
- selectedUserId = user.id;
- onUsersChanged();
- } catch (e) {
- error = e.message;
- }
- }
-
- async function handleRemoveUser(id) {
- try {
- await removeUser(id);
- confirmDeleteId = null;
- await fetchUsers();
- onUsersChanged();
- } catch (e) {
- error = e.message;
- }
- }
-
- $effect(() => {
- fetchUsers();
- });
-
-
-
-
-
- {#each users as user}
-
-
selectedUserId = user.id}
- role="button"
- tabindex="0"
- onkeydown={(e) => { if (e.key === 'Enter') selectedUserId = user.id; }}
- >
- {user.name}
- {#if confirmDeleteId === user.id}
-
-
-
-
- {:else}
-
- {/if}
-
- {/each}
-
- {#if showAddForm}
-
- {:else}
-
- {/if}
-
-
-
- {#if error}
-
{error}
- {/if}
-
-
-
+
diff --git a/Provider/frontend/src/lib/api.js b/Provider/frontend/src/lib/api.js
index 82783a3..8d11e30 100644
--- a/Provider/frontend/src/lib/api.js
+++ b/Provider/frontend/src/lib/api.js
@@ -163,6 +163,25 @@ export async function removeUser(id) {
return res.json();
}
+/**
+ * Update a user's name.
+ * @param {number} id
+ * @param {string} name
+ * @returns {Promise<{status: string}>}
+ */
+export async function updateUser(id, name) {
+ const res = await fetch(`${API_BASE}/api/users/update`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id, name })
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed (${res.status}): ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
// ─── Task Management ─────────────────────────────────────────────────────────
/**
diff --git a/Provider/frontend/version.json b/Provider/frontend/version.json
index daebab9..0325e63 100644
--- a/Provider/frontend/version.json
+++ b/Provider/frontend/version.json
@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 1,
- "revision": 4
+ "revision": 13
}
\ No newline at end of file
diff --git a/Provider/main/api/tasks/store.cpp b/Provider/main/api/tasks/store.cpp
index edd6f06..4aef8ab 100644
--- a/Provider/main/api/tasks/store.cpp
+++ b/Provider/main/api/tasks/store.cpp
@@ -6,7 +6,7 @@
#include "api/users/store.hpp"
// Find a task by ID, returns nullptr if not found
-internal task_t *find_task(uint16 id)
+task_t *find_task(uint16 id)
{
for (int i = 0; i < MAX_TASKS; i++)
{
@@ -19,7 +19,7 @@ internal task_t *find_task(uint16 id)
}
// Add a task, returns pointer to new task or nullptr if full
-internal task_t *add_task(uint8 user_id, const char *title, int64 due_date)
+task_t *add_task(uint8 user_id, const char *title, int64 due_date)
{
// Verify user exists
if (find_user(user_id) == nullptr)
@@ -44,7 +44,7 @@ internal task_t *add_task(uint8 user_id, const char *title, int64 due_date)
}
// Remove a task by ID, returns true if found and removed
-internal bool remove_task(uint16 id)
+bool remove_task(uint16 id)
{
for (int i = 0; i < MAX_TASKS; i++)
{
@@ -59,7 +59,7 @@ internal bool remove_task(uint16 id)
}
// Remove all tasks belonging to a user
-internal void remove_tasks_for_user(uint8 user_id)
+void remove_tasks_for_user(uint8 user_id)
{
for (int i = 0; i < MAX_TASKS; i++)
{
@@ -73,7 +73,7 @@ internal void remove_tasks_for_user(uint8 user_id)
// Simple insertion sort for small arrays — sort task pointers by due_date
// ascending
-internal void sort_tasks_by_due_date(task_t **arr, int count)
+void sort_tasks_by_due_date(task_t **arr, int count)
{
for (int i = 1; i < count; i++)
{
@@ -90,7 +90,7 @@ internal void sort_tasks_by_due_date(task_t **arr, int count)
// Populate dummy tasks on boot for development iteration.
// Uses relative offsets from current time so due dates always make sense.
-internal void seed_tasks()
+void seed_tasks()
{
int64 now = (int64)(esp_timer_get_time() / 1000000);
diff --git a/Provider/main/api/tasks/store.hpp b/Provider/main/api/tasks/store.hpp
index a900661..d456c38 100644
--- a/Provider/main/api/tasks/store.hpp
+++ b/Provider/main/api/tasks/store.hpp
@@ -4,9 +4,9 @@
#include "types.hpp"
// Data store operations for tasks
-internal task_t *find_task(uint16 id);
-internal task_t *add_task(uint8 user_id, const char *title, int64 due_date);
-internal bool remove_task(uint16 id);
-internal void remove_tasks_for_user(uint8 user_id);
-internal void sort_tasks_by_due_date(task_t **arr, int count);
-internal void seed_tasks();
+task_t *find_task(uint16 id);
+task_t *add_task(uint8 user_id, const char *title, int64 due_date);
+bool remove_task(uint16 id);
+void remove_tasks_for_user(uint8 user_id);
+void sort_tasks_by_due_date(task_t **arr, int count);
+void seed_tasks();
diff --git a/Provider/main/api/users/store.cpp b/Provider/main/api/users/store.cpp
index a49ce71..e8857fe 100644
--- a/Provider/main/api/users/store.cpp
+++ b/Provider/main/api/users/store.cpp
@@ -3,7 +3,7 @@
#include "api/users/store.hpp"
// Find a user by ID, returns nullptr if not found
-internal user_t *find_user(uint8 id)
+user_t *find_user(uint8 id)
{
for (int i = 0; i < MAX_USERS; i++)
{
@@ -16,7 +16,7 @@ internal user_t *find_user(uint8 id)
}
// Add a user, returns pointer to new user or nullptr if full
-internal user_t *add_user(const char *name)
+user_t *add_user(const char *name)
{
for (int i = 0; i < MAX_USERS; i++)
{
@@ -32,7 +32,7 @@ internal user_t *add_user(const char *name)
}
// Remove a user by ID, returns true if found and removed
-internal bool remove_user(uint8 id)
+bool remove_user(uint8 id)
{
for (int i = 0; i < MAX_USERS; i++)
{
@@ -47,8 +47,19 @@ internal bool remove_user(uint8 id)
return false;
}
+// Update a user's name, returns pointer to user or nullptr if not found
+user_t *update_user(uint8 id, const char *name)
+{
+ user_t *user = find_user(id);
+ if (user)
+ {
+ strlcpy(user->name, name, sizeof(user->name));
+ }
+ return user;
+}
+
// Populate dummy users on boot for development iteration
-internal void seed_users()
+void seed_users()
{
add_user("Alice");
add_user("Bob");
diff --git a/Provider/main/api/users/store.hpp b/Provider/main/api/users/store.hpp
index 63ce26f..90312b4 100644
--- a/Provider/main/api/users/store.hpp
+++ b/Provider/main/api/users/store.hpp
@@ -3,9 +3,9 @@
#include "types.hpp"
#include "user.hpp"
-
// Data store operations for users
-internal user_t *find_user(uint8 id);
-internal user_t *add_user(const char *name);
-internal bool remove_user(uint8 id);
-internal void seed_users();
+user_t *find_user(uint8 id);
+user_t *add_user(const char *name);
+bool remove_user(uint8 id);
+user_t *update_user(uint8 id, const char *name);
+void seed_users();
diff --git a/Provider/main/api/users/unity.cpp b/Provider/main/api/users/unity.cpp
index e1792ed..90c715c 100644
--- a/Provider/main/api/users/unity.cpp
+++ b/Provider/main/api/users/unity.cpp
@@ -3,4 +3,5 @@
#include "api/users/list.cpp"
#include "api/users/add.cpp"
#include "api/users/remove.cpp"
+#include "api/users/update.cpp"
// clang-format on
diff --git a/Provider/main/api/users/update.cpp b/Provider/main/api/users/update.cpp
new file mode 100644
index 0000000..19013c7
--- /dev/null
+++ b/Provider/main/api/users/update.cpp
@@ -0,0 +1,68 @@
+// POST /api/users/update — Update an existing user's name
+// Body: {"id": 1, "name": "Bob"}
+
+#include "cJSON.h"
+#include "esp_http_server.h"
+
+#include "api/users/store.hpp"
+#include "types.hpp"
+#include "user.hpp"
+
+internal esp_err_t api_users_update_handler(httpd_req_t *req)
+{
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+
+ char buf[128];
+ int received = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (received <= 0)
+ {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body");
+ return ESP_FAIL;
+ }
+ buf[received] = '\0';
+
+ cJSON *body = cJSON_Parse(buf);
+ if (!body)
+ {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
+ return ESP_FAIL;
+ }
+
+ cJSON *id_item = cJSON_GetObjectItem(body, "id");
+ cJSON *name_item = cJSON_GetObjectItem(body, "name");
+
+ if (!cJSON_IsNumber(id_item) || !cJSON_IsString(name_item) ||
+ strlen(name_item->valuestring) == 0)
+ {
+ cJSON_Delete(body);
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing 'id' or 'name'");
+ return ESP_FAIL;
+ }
+
+ user_t *user = update_user((uint8)id_item->valueint, name_item->valuestring);
+ cJSON_Delete(body);
+
+ if (!user)
+ {
+ httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "User not found");
+ return ESP_FAIL;
+ }
+
+ cJSON *resp = cJSON_CreateObject();
+ cJSON_AddStringToObject(resp, "status", "ok");
+
+ const char *json = cJSON_PrintUnformatted(resp);
+ httpd_resp_sendstr(req, json);
+
+ free((void *)json);
+ cJSON_Delete(resp);
+
+ return ESP_OK;
+}
+
+internal const httpd_uri_t api_users_update_uri = {.uri = "/api/users/update",
+ .method = HTTP_POST,
+ .handler =
+ api_users_update_handler,
+ .user_ctx = NULL};
diff --git a/Provider/main/appstate.hpp b/Provider/main/appstate.hpp
index 5fb8584..a1b7609 100644
--- a/Provider/main/appstate.hpp
+++ b/Provider/main/appstate.hpp
@@ -2,7 +2,7 @@
#include "types.hpp"
-// Shared Application State (Unity Build)
-internal bool g_Ethernet_Initialized = false;
-internal bool g_Wifi_Initialized = false;
-internal uint8_t g_Active_WWW_Partition = 0;
+// Shared Application State
+extern bool g_Ethernet_Initialized;
+extern bool g_Wifi_Initialized;
+extern uint8_t g_Active_WWW_Partition;
diff --git a/Provider/main/http_server.cpp b/Provider/main/http_server.cpp
index ea98a0a..cca72c7 100644
--- a/Provider/main/http_server.cpp
+++ b/Provider/main/http_server.cpp
@@ -186,6 +186,7 @@ internal httpd_handle_t start_webserver(void)
esp_vfs_littlefs_conf_t conf = {};
conf.base_path = "/www";
conf.partition_label = g_Active_WWW_Partition == 0 ? "www_0" : "www_1";
+ ESP_LOGI(TAG, "Mounting LittleFS partition: %s", conf.partition_label);
conf.format_if_mount_failed = false;
conf.dont_mount = false;
esp_err_t ret = esp_vfs_littlefs_register(&conf);
@@ -244,6 +245,7 @@ internal httpd_handle_t start_webserver(void)
// Register todo list API routes
httpd_register_uri_handler(server, &api_users_get_uri);
httpd_register_uri_handler(server, &api_users_post_uri);
+ httpd_register_uri_handler(server, &api_users_update_uri);
httpd_register_uri_handler(server, &api_users_delete_uri);
httpd_register_uri_handler(server, &api_tasks_upcoming_uri);
httpd_register_uri_handler(server, &api_tasks_get_uri);
@@ -272,14 +274,13 @@ internal httpd_handle_t start_webserver(void)
return NULL;
}
-internal void stop_webserver(httpd_handle_t server)
+internal void stop_webserver(httpd_handle_t server, uint8_t partition_index)
{
if (server)
{
httpd_stop(server);
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
- esp_vfs_littlefs_unregister(g_Active_WWW_Partition == 0 ? "www_0"
- : "www_1");
+ esp_vfs_littlefs_unregister(partition_index == 0 ? "www_0" : "www_1");
#endif
}
}
diff --git a/Provider/main/main.cpp b/Provider/main/main.cpp
index d4e7c69..06f6afb 100644
--- a/Provider/main/main.cpp
+++ b/Provider/main/main.cpp
@@ -1,5 +1,6 @@
// STD Lib
#include
+#include
// SDK
#include "esp_log.h"
@@ -23,7 +24,12 @@
#include "http_server.cpp"
// clang-format on
-internal constexpr bool kBlockUntilEthernetEstablished = false;
+// Global Application State Definitions
+bool g_Ethernet_Initialized = false;
+bool g_Wifi_Initialized = false;
+uint8_t g_Active_WWW_Partition = 0;
+
+constexpr bool kBlockUntilEthernetEstablished = false;
extern "C" void app_main()
{
@@ -46,6 +52,10 @@ extern "C" void app_main()
{
// Read active www partition from NVS
err = nvs_get_u8(my_handle, "www_part", &g_Active_WWW_Partition);
+ if (err == ESP_OK)
+ {
+ printf("NVS: Found active www partition: %d\n", g_Active_WWW_Partition);
+ }
if (err == ESP_ERR_NVS_NOT_FOUND)
{
@@ -74,6 +84,76 @@ extern "C" void app_main()
printf("Error opening NVS handle!\n");
}
+ // Detect if this is the first boot after a new flash (Firmware or Frontend)
+ bool is_new_flash = false;
+ if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK)
+ {
+ // 1. Check Firmware Compile Time
+ char last_time[64] = {0};
+ size_t time_size = sizeof(last_time);
+ const char *current_time = __DATE__ " " __TIME__;
+ if (nvs_get_str(my_handle, "last_fw_time", last_time, &time_size) !=
+ ESP_OK ||
+ strcmp(last_time, current_time) != 0)
+ {
+ printf("New firmware detected! (Last: %s, Current: %s)\n",
+ last_time[0] ? last_time : "None", current_time);
+ is_new_flash = true;
+ nvs_set_str(my_handle, "last_fw_time", current_time);
+ }
+
+ // 2. Check Frontend Partition Fingerprint (www_0)
+ const esp_partition_t *www0_p = esp_partition_find_first(
+ ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_LITTLEFS, "www_0");
+ if (www0_p)
+ {
+ uint8_t current_sha[32];
+ if (esp_partition_get_sha256(www0_p, current_sha) == ESP_OK)
+ {
+ uint8_t last_sha[32] = {0};
+ size_t sha_size = sizeof(last_sha);
+ if (nvs_get_blob(my_handle, "www0_sha", last_sha, &sha_size) !=
+ ESP_OK ||
+ memcmp(last_sha, current_sha, 32) != 0)
+ {
+ printf("New frontend partition detected via SHA256!\n");
+ is_new_flash = true;
+ nvs_set_blob(my_handle, "www0_sha", current_sha, 32);
+ }
+ }
+ }
+
+ if (is_new_flash)
+ {
+ nvs_commit(my_handle);
+ }
+ nvs_close(my_handle);
+ }
+
+ // If we are running from FACTORY and a new flash was detected, override to
+ // www_0
+ {
+ const esp_partition_t *running = esp_ota_get_running_partition();
+ if (running != NULL &&
+ running->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY)
+ {
+ if (is_new_flash && g_Active_WWW_Partition != 0)
+ {
+ printf("FACTORY APP + NEW FLASH: Overriding www_part to 0 (was %d)\n",
+ g_Active_WWW_Partition);
+ g_Active_WWW_Partition = 0;
+
+ // Persist the override
+ if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK)
+ {
+ nvs_set_u8(my_handle, "www_part", 0);
+ nvs_commit(my_handle);
+ nvs_close(my_handle);
+ }
+ }
+ }
+ }
+
ESP_ERROR_CHECK(esp_event_loop_create_default());
setup_led();
@@ -168,6 +248,9 @@ extern "C" void app_main()
printf("Connected!\n");
+ // Start the webserver
+ web_server = start_webserver();
+
// Mark the current app as valid to cancel rollback, only if it's an OTA app
{
const esp_partition_t *running = esp_ota_get_running_partition();
@@ -179,9 +262,6 @@ extern "C" void app_main()
}
}
- // Start the webserver
- web_server = start_webserver();
-
// Keep the main task alive indefinitely
while (true)
{
@@ -193,7 +273,7 @@ shutdown:
if (web_server)
{
- stop_webserver(web_server);
+ stop_webserver(web_server, g_Active_WWW_Partition);
web_server = NULL;
}