Updated the task view and backend to handle more like a routine

This commit is contained in:
2026-03-08 22:10:46 -04:00
parent 4161ff9513
commit 9d3a277f45
14 changed files with 405 additions and 147 deletions

3
.gitignore vendored
View File

@@ -93,3 +93,6 @@ external/*
# Agent Tasks
Provider/AgentTasks/
# OTA files
*.bundle

View File

@@ -1,2 +1,2 @@
VITE_API_BASE=http://192.168.50.216
VITE_API_BASE=http://calendink.local/
MKLITTLEFS_PATH=W:\Classified\Calendink\Provider\build\littlefs_py_venv\Scripts\littlefs-python.exe

View File

@@ -32,7 +32,7 @@ function loadEnv() {
loadEnv();
const targetIP = process.env.VITE_API_BASE ? process.env.VITE_API_BASE.replace('http://', '') : null;
const targetIP = process.env.VITE_API_BASE ? process.env.VITE_API_BASE.replace('http://', '').replace(/\/+$/, '') : null;
if (!targetIP) {
console.error('Error: VITE_API_BASE not found in frontend/.env. Please set it to the target device IP.');

View File

@@ -75,6 +75,8 @@
}
let isFetching = false;
let lastKnownFirmware = null;
let lastKnownSlot = null;
async function fetchAll(silent = false) {
if (isFetching) return;
isFetching = true;
@@ -85,6 +87,16 @@
getOTAStatus(),
getUpcomingTasks().catch(() => ({ users: [] }))
]);
// Detect any OTA update: firmware version change OR www partition flip
const fwChanged = lastKnownFirmware && sys.firmware !== lastKnownFirmware;
const slotChanged = lastKnownSlot !== null && ota.active_slot !== lastKnownSlot;
if (fwChanged || slotChanged) {
console.log(`OTA detected (fw: ${fwChanged}, slot: ${slotChanged}). Reloading...`);
window.location.href = window.location.pathname + '?t=' + Date.now();
return;
}
lastKnownFirmware = sys.firmware;
lastKnownSlot = ota.active_slot;
systemInfo = sys;
otaStatus = ota;
upcomingData = upcoming;
@@ -180,7 +192,7 @@
<div class="w-full max-w-6xl mx-auto space-y-8">
<!-- Header -->
<div class="text-center">
<h1 class="text-2xl font-bold text-accent">Calendink Provider 📅📅🚀🚀⚡✨🌈</h1>
<h1 class="text-2xl font-bold text-accent">Calendink Provider 📅⚡✨🌈</h1>
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
<!-- Status Badge -->
@@ -214,29 +226,46 @@
<!-- Upcoming Tasks Section (top priority) -->
{#if upcomingData.users.length > 0}
{@const periodNames = ['Morning', 'Afternoon', 'Evening']}
{@const periodIcons = ['🌅', '☀️', '🌙']}
<div class="bg-bg-card border border-border rounded-xl overflow-hidden shadow-xl">
<div class="px-5 py-3 border-b border-border">
<h2 class="text-sm font-semibold text-text-primary uppercase tracking-wider">
📋 Upcoming Tasks
📋 Today's Routine
</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-0 divide-y md:divide-y-0 md:divide-x divide-border">
{#each upcomingData.users as user}
{@const routineTasks = user.tasks.filter(t => t.recurrence > 0)}
<div class="p-4">
<h3 class="text-xs font-bold text-accent mb-3 uppercase tracking-wider">{user.name}</h3>
{#if user.tasks.length === 0}
<p class="text-[11px] text-text-secondary italic">No pending tasks</p>
{#if routineTasks.length === 0}
<p class="text-[11px] text-text-secondary italic">No routine tasks</p>
{:else}
<div class="space-y-2">
{#each user.tasks as task}
<div class="flex items-start gap-2">
<span class="text-[10px] mt-0.5 {isOverdue(task.due_date) ? 'text-danger' : 'text-text-secondary'} font-mono whitespace-nowrap">
{formatRelativeDate(task.due_date)}
</span>
<span class="text-xs text-text-primary leading-tight">{task.title}</span>
{#each [0, 1, 2] as periodIdx}
{@const tasksForPeriod = routineTasks.filter(t => t.period & (1 << periodIdx))}
{#if tasksForPeriod.length > 0}
<div class="mb-2">
<div class="text-[10px] uppercase tracking-wider text-text-secondary font-semibold mb-1">
{periodIcons[periodIdx]} {periodNames[periodIdx]}
</div>
{#each tasksForPeriod as task}
<div class="flex items-center gap-2 py-0.5 pl-3">
<span class="text-xs text-text-primary leading-tight">{task.title}</span>
{#if task.recurrence > 0}
<span class="text-[9px] text-accent font-mono">
{task.recurrence === 0x7F ? '∞' : task.recurrence === 0x1F ? 'wk' : ''}
</span>
{:else if task.due_date > 0}
<span class="text-[9px] {isOverdue(task.due_date) ? 'text-danger' : 'text-text-secondary'} font-mono">
{formatRelativeDate(task.due_date)}
</span>
{/if}
</div>
{/each}
</div>
{/each}
</div>
{/if}
{/each}
{/if}
</div>
{/each}

View File

@@ -6,17 +6,61 @@
let tasks = $state([]);
let error = $state('');
// Period and day-of-week labels
const PERIODS = ['Morning', 'Afternoon', 'Evening'];
const PERIOD_ICONS = ['🌅', '☀️', '🌙'];
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// Shared bitmask helpers (used for both period and recurrence)
function toggleBit(current, idx) {
return current ^ (1 << idx);
}
function hasBit(mask, idx) {
return (mask & (1 << idx)) !== 0;
}
function formatPeriod(mask) {
if (mask === 0x07) return 'All Day';
const names = [];
for (let i = 0; i < 3; i++) {
if (mask & (1 << i)) names.push(PERIODS[i]);
}
return names.length > 0 ? names.join(', ') : 'None';
}
function periodIcons(mask) {
const icons = [];
for (let i = 0; i < 3; i++) {
if (mask & (1 << i)) icons.push(PERIOD_ICONS[i]);
}
return icons.join('');
}
function formatRecurrence(mask) {
if (mask === 0x7F) return 'Every day';
if (mask === 0x1F) return 'Weekdays';
if (mask === 0x60) return 'Weekends';
const names = [];
for (let i = 0; i < 7; i++) {
if (mask & (1 << i)) names.push(DAYS[i]);
}
return names.join(', ');
}
// Add task form state
let newTitle = $state('');
let newPeriod = $state(0x01);
let newRecurrence = $state(0);
let newDueDay = $state('');
let newDueTime = $state('');
let showAddForm = $state(false);
// Edit state
let editingTaskId = $state(null);
let editTitle = $state('');
let editPeriod = $state(0);
let editRecurrence = $state(0);
let editDueDay = $state('');
let editDueTime = $state('');
// Confirm delete
let confirmDeleteId = $state(null);
@@ -28,8 +72,13 @@
}
try {
tasks = await getTasks(selectedUserId);
// Sort by due_date ascending
tasks.sort((a, b) => a.due_date - b.due_date);
// Sort: recurrent first (by day), then one-off by due_date
tasks.sort((a, b) => {
if (a.recurrence && !b.recurrence) return -1;
if (!a.recurrence && b.recurrence) return 1;
if (a.recurrence && b.recurrence) return a.recurrence - b.recurrence;
return a.due_date - b.due_date;
});
error = '';
} catch (e) {
error = e.message;
@@ -47,14 +96,20 @@
async function handleAddTask(e) {
e.preventDefault();
if (!newTitle.trim() || !newDueDay || !newDueTime) return;
if (!newTitle.trim()) return;
// For non-recurrent tasks, require a date
if (newRecurrence === 0 && !newDueDay) return;
const dueTimestamp = newRecurrence > 0
? 0
: Math.floor(new Date(`${newDueDay}T00:00`).getTime() / 1000);
const dueTimestamp = Math.floor(new Date(`${newDueDay}T${newDueTime}`).getTime() / 1000);
try {
await addTask(selectedUserId, newTitle.trim(), dueTimestamp);
await addTask(selectedUserId, newTitle.trim(), dueTimestamp, newPeriod, newRecurrence);
newTitle = '';
newPeriod = 0x01;
newRecurrence = 0;
newDueDay = '';
newDueTime = '';
showAddForm = false;
await fetchTasks();
} catch (e) {
@@ -74,20 +129,34 @@
function startEditing(task) {
editingTaskId = task.id;
editTitle = task.title;
const parts = formatDateForInput(task.due_date);
editDueDay = parts.day;
editDueTime = parts.time;
editPeriod = task.period;
editRecurrence = task.recurrence;
if (task.recurrence === 0 && task.due_date) {
const d = new Date(task.due_date * 1000);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const dayNum = String(d.getDate()).padStart(2, '0');
editDueDay = `${year}-${month}-${dayNum}`;
} else {
editDueDay = '';
}
}
async function saveEdit(e) {
e.preventDefault();
if (!editTitle.trim()) return;
if (editRecurrence === 0 && !editDueDay) return;
const dueTimestamp = editRecurrence > 0
? 0
: Math.floor(new Date(`${editDueDay}T00:00`).getTime() / 1000);
const dueTimestamp = Math.floor(new Date(`${editDueDay}T${editDueTime}`).getTime() / 1000);
try {
await updateTask(editingTaskId, {
title: editTitle.trim(),
due_date: dueTimestamp
due_date: dueTimestamp,
period: editPeriod,
recurrence: editRecurrence
});
editingTaskId = null;
await fetchTasks();
@@ -106,20 +175,12 @@
}
}
function formatDateForInput(timestamp) {
const d = new Date(timestamp * 1000);
function setTodayForNewTask() {
const d = new Date();
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const dayNum = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const mins = String(d.getMinutes()).padStart(2, '0');
return { day: `${year}-${month}-${dayNum}`, time: `${hours}:${mins}` };
}
function setNowForNewTask() {
const parts = formatDateForInput(Date.now() / 1000);
newDueDay = parts.day;
newDueTime = parts.time;
newDueDay = `${year}-${month}-${dayNum}`;
}
function formatRelativeDate(timestamp) {
@@ -139,8 +200,9 @@
return diff < 0 ? `${days}d ago` : `in ${days}d`;
}
function isOverdue(timestamp) {
return timestamp < Date.now() / 1000;
function isOverdue(task) {
if (task.recurrence > 0) return false; // Recurrent tasks don't have overdue
return task.due_date < Date.now() / 1000;
}
</script>
@@ -151,7 +213,7 @@
<div class="task-header">
<h2 class="task-title">Tasks</h2>
{#if !showAddForm}
<button class="add-task-btn" onclick={() => { showAddForm = true; setNowForNewTask(); }}>+ Add Task</button>
<button class="add-task-btn" onclick={() => { showAddForm = true; setTodayForNewTask(); }}>+ Add Task</button>
{/if}
</div>
@@ -165,39 +227,99 @@
class="task-input"
autofocus
/>
<div class="date-time-row">
<label class="date-time-field">
<span class="field-label">Date</span>
<input type="date" bind:value={newDueDay} class="task-date-input" />
</label>
<label class="date-time-field">
<span class="field-label">Time</span>
<input type="time" bind:value={newDueTime} class="task-time-input" />
</label>
<div class="field-row">
<div class="field-group">
<span class="field-label">Period</span>
<div class="period-selector">
{#each PERIODS as p, i}
<button
type="button"
class="period-btn {hasBit(newPeriod, i) ? 'active' : ''}"
onclick={() => newPeriod = toggleBit(newPeriod, i)}
>{PERIOD_ICONS[i]} {p}</button>
{/each}
</div>
</div>
</div>
<div class="field-row">
<div class="field-group">
<span class="field-label">Recurrence</span>
<div class="recurrence-selector">
{#each DAYS as day, i}
<button
type="button"
class="rec-btn {hasBit(newRecurrence, i) ? 'active' : ''}"
onclick={() => newRecurrence = toggleBit(newRecurrence, i)}
>{day}</button>
{/each}
</div>
</div>
</div>
{#if newRecurrence === 0}
<div class="field-row">
<label class="field-group">
<span class="field-label">Date</span>
<input type="date" bind:value={newDueDay} class="task-date-input" />
</label>
</div>
{/if}
<div class="form-actions">
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDay || !newDueTime}>Add</button>
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newDueTime = ''; }}>Cancel</button>
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || (newRecurrence === 0 && !newDueDay)}>Add</button>
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newRecurrence = 0; newPeriod = 0x01; }}>Cancel</button>
</div>
</form>
{/if}
<div class="task-list">
{#each tasks as task}
<div class="task-item {task.completed ? 'completed' : ''} {isOverdue(task.due_date) && !task.completed ? 'overdue' : ''}">
<div class="task-item {task.completed ? 'completed' : ''} {isOverdue(task) && !task.completed ? 'overdue' : ''}">
{#if editingTaskId === task.id}
<form class="edit-form" onsubmit={saveEdit}>
<input type="text" bind:value={editTitle} class="task-input" />
<div class="date-time-row">
<label class="date-time-field">
<span class="field-label">Date</span>
<input type="date" bind:value={editDueDay} class="task-date-input" />
</label>
<label class="date-time-field">
<span class="field-label">Time</span>
<input type="time" bind:value={editDueTime} class="task-time-input" />
</label>
<div class="field-row">
<div class="field-group">
<span class="field-label">Period</span>
<div class="period-selector">
{#each PERIODS as p, i}
<button
type="button"
class="period-btn {hasBit(editPeriod, i) ? 'active' : ''}"
onclick={() => editPeriod = toggleBit(editPeriod, i)}
>{PERIOD_ICONS[i]} {p}</button>
{/each}
</div>
</div>
</div>
<div class="field-row">
<div class="field-group">
<span class="field-label">Recurrence</span>
<div class="recurrence-selector">
{#each DAYS as day, i}
<button
type="button"
class="rec-btn {hasBit(editRecurrence, i) ? 'active' : ''}"
onclick={() => editRecurrence = toggleBit(editRecurrence, i)}
>{day}</button>
{/each}
</div>
</div>
</div>
{#if editRecurrence === 0}
<div class="field-row">
<label class="field-group">
<span class="field-label">Date</span>
<input type="date" bind:value={editDueDay} class="task-date-input" />
</label>
</div>
{/if}
<div class="form-actions">
<button type="submit" class="btn-primary btn-sm">Save</button>
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
@@ -213,9 +335,16 @@
/>
<div class="task-info">
<span class="task-text">{task.title}</span>
<span class="task-due {isOverdue(task.due_date) && !task.completed ? 'text-overdue' : ''}">
{formatRelativeDate(task.due_date)}
</span>
<div class="task-meta">
<span class="task-period">{periodIcons(task.period)} {formatPeriod(task.period)}</span>
{#if task.recurrence > 0}
<span class="task-recurrence">🔁 {formatRecurrence(task.recurrence)}</span>
{:else}
<span class="task-due {isOverdue(task) && !task.completed ? 'text-overdue' : ''}">
{formatRelativeDate(task.due_date)}
</span>
{/if}
</div>
</div>
</div>
<div class="task-actions">
@@ -305,18 +434,15 @@
border-color: var(--color-accent);
}
.date-time-row {
.field-row {
display: flex;
gap: 8px;
}
.date-time-field {
.field-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.date-time-field:first-child {
flex: 1;
}
@@ -328,6 +454,65 @@
color: var(--color-text-secondary);
}
.period-selector {
display: flex;
gap: 4px;
}
.period-btn {
flex: 1;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--color-border);
background: var(--color-bg-primary);
color: var(--color-text-secondary);
cursor: pointer;
font-size: 11px;
transition: all 0.15s;
white-space: nowrap;
}
.period-btn:hover {
background: var(--color-bg-card-hover);
}
.period-btn.active {
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 15%, transparent);
color: var(--color-accent);
font-weight: 600;
}
.recurrence-selector {
display: flex;
gap: 3px;
flex-wrap: wrap;
}
.rec-btn {
padding: 5px 8px;
border-radius: 6px;
border: 1px solid var(--color-border);
background: var(--color-bg-primary);
color: var(--color-text-secondary);
cursor: pointer;
font-size: 11px;
transition: all 0.15s;
min-width: 32px;
text-align: center;
}
.rec-btn:hover {
background: var(--color-bg-card-hover);
}
.rec-btn.active {
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 15%, transparent);
color: var(--color-accent);
font-weight: 600;
}
.task-date-input {
padding: 8px 12px;
border-radius: 8px;
@@ -342,23 +527,8 @@
position: relative;
}
.task-time-input {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--color-border);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
outline: none;
color-scheme: dark;
width: 110px;
cursor: pointer;
position: relative;
}
/* Make the native picker icon cover the entire input (Chrome/Edge only) */
.task-date-input::-webkit-calendar-picker-indicator,
.task-time-input::-webkit-calendar-picker-indicator {
.task-date-input::-webkit-calendar-picker-indicator {
position: absolute;
top: 0;
left: 0;
@@ -370,7 +540,7 @@
cursor: pointer;
}
.task-date-input:focus, .task-time-input:focus {
.task-date-input:focus {
border-color: var(--color-accent);
}
@@ -430,6 +600,7 @@
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--color-border);
@@ -437,6 +608,11 @@
transition: all 0.15s;
}
.task-item .edit-form {
width: 100%;
margin: 0;
}
.task-item:hover {
background: var(--color-bg-card-hover);
}
@@ -485,6 +661,25 @@
color: var(--color-text-secondary);
}
.task-meta {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.task-period {
font-size: 10px;
color: var(--color-text-secondary);
font-weight: 500;
}
.task-recurrence {
font-size: 10px;
color: var(--color-accent);
font-weight: 500;
}
.task-due {
font-size: 10px;
color: var(--color-text-secondary);

View File

@@ -222,14 +222,16 @@ export async function getUpcomingTasks() {
* Create a new task.
* @param {number} userId
* @param {string} title
* @param {number} dueDate Unix timestamp in seconds
* @returns {Promise<{id: number, user_id: number, title: string, due_date: number, completed: boolean}>}
* @param {number} dueDate Unix timestamp in seconds (used for non-recurrent tasks)
* @param {number} period 0=Morning, 1=Afternoon, 2=Evening
* @param {number} recurrence 0=None, 1-7=Day of week (1=Mon)
* @returns {Promise<{id: number, user_id: number, title: string, due_date: number, period: number, recurrence: number, completed: boolean}>}
*/
export async function addTask(userId, title, dueDate) {
export async function addTask(userId, title, dueDate, period = 0, recurrence = 0) {
const res = await trackedFetch(`${API_BASE}/api/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: userId, title, due_date: dueDate })
body: JSON.stringify({ user_id: userId, title, due_date: dueDate, period, recurrence })
});
if (!res.ok) {
const errorText = await res.text();
@@ -241,7 +243,7 @@ export async function addTask(userId, title, dueDate) {
/**
* Update a task (partial update — only include fields you want to change).
* @param {number} id
* @param {Object} fields - { title?: string, due_date?: number, completed?: boolean }
* @param {Object} fields - { title?: string, due_date?: number, period?: number, recurrence?: number, completed?: boolean }
* @returns {Promise<{status: string}>}
*/
export async function updateTask(id, fields) {

View File

@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 1,
"revision": 23
"revision": 30
}

View File

@@ -1,5 +1,6 @@
// POST /api/tasks — Create a new task
// Body: {"user_id":1, "title":"...", "due_date":1741369200}
// Body: {"user_id":1, "title":"...", "due_date":1741369200, "period":0,
// "recurrence":0}
#include "cJSON.h"
#include "esp_http_server.h"
@@ -31,19 +32,26 @@ internal esp_err_t api_tasks_post_handler(httpd_req_t *req)
cJSON *user_id_item = cJSON_GetObjectItem(body, "user_id");
cJSON *title_item = cJSON_GetObjectItem(body, "title");
cJSON *due_date_item = cJSON_GetObjectItem(body, "due_date");
cJSON *period_item = cJSON_GetObjectItem(body, "period");
cJSON *recurrence_item = cJSON_GetObjectItem(body, "recurrence");
if (!cJSON_IsNumber(user_id_item) || !cJSON_IsString(title_item) ||
!cJSON_IsNumber(due_date_item))
if (!cJSON_IsNumber(user_id_item) || !cJSON_IsString(title_item))
{
cJSON_Delete(body);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Missing user_id, title, or due_date");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing user_id or title");
return ESP_FAIL;
}
int64 due_date =
cJSON_IsNumber(due_date_item) ? (int64)due_date_item->valuedouble : 0;
uint8 period = cJSON_IsNumber(period_item) ? (uint8)period_item->valueint
: (uint8)PERIOD_MORNING;
uint8 recurrence =
cJSON_IsNumber(recurrence_item) ? (uint8)recurrence_item->valueint : 0;
task_t *task =
add_task((uint8)user_id_item->valueint, title_item->valuestring,
(int64)due_date_item->valuedouble);
add_task((uint8)user_id_item->valueint, title_item->valuestring, due_date,
period, recurrence);
cJSON_Delete(body);
if (!task)
@@ -58,6 +66,8 @@ internal esp_err_t api_tasks_post_handler(httpd_req_t *req)
cJSON_AddNumberToObject(resp, "user_id", task->user_id);
cJSON_AddStringToObject(resp, "title", task->title);
cJSON_AddNumberToObject(resp, "due_date", (double)task->due_date);
cJSON_AddNumberToObject(resp, "period", task->period);
cJSON_AddNumberToObject(resp, "recurrence", task->recurrence);
cJSON_AddBoolToObject(resp, "completed", task->completed);
const char *json = cJSON_PrintUnformatted(resp);

View File

@@ -41,6 +41,8 @@ internal esp_err_t api_tasks_get_handler(httpd_req_t *req)
cJSON_AddNumberToObject(obj, "user_id", g_Tasks[i].user_id);
cJSON_AddStringToObject(obj, "title", g_Tasks[i].title);
cJSON_AddNumberToObject(obj, "due_date", (double)g_Tasks[i].due_date);
cJSON_AddNumberToObject(obj, "period", g_Tasks[i].period);
cJSON_AddNumberToObject(obj, "recurrence", g_Tasks[i].recurrence);
cJSON_AddBoolToObject(obj, "completed", g_Tasks[i].completed);
cJSON_AddItemToArray(arr, obj);
}

View File

@@ -19,7 +19,8 @@ task_t *find_task(uint16 id)
}
// Add a task, returns pointer to new task or nullptr if full
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, uint8 period,
uint8 recurrence)
{
// Verify user exists
if (find_user(user_id) == nullptr)
@@ -35,6 +36,8 @@ task_t *add_task(uint8 user_id, const char *title, int64 due_date)
g_Tasks[i].user_id = user_id;
strlcpy(g_Tasks[i].title, title, sizeof(g_Tasks[i].title));
g_Tasks[i].due_date = due_date;
g_Tasks[i].period = period;
g_Tasks[i].recurrence = recurrence;
g_Tasks[i].completed = false;
g_Tasks[i].active = true;
return &g_Tasks[i];
@@ -94,18 +97,19 @@ void seed_tasks()
{
int64 now = (int64)(esp_timer_get_time() / 1000000);
// Alice's tasks (user_id = 1)
add_task(1, "Buy groceries", now + 86400); // +1 day
add_task(1, "Review PR #42", now + 3600); // +1 hour
add_task(1, "Book dentist appointment", now + 172800); // +2 days
add_task(1, "Update resume", now + 604800); // +7 days
// Alice's tasks (user_id = 1) — mix of one-off and recurring
add_task(1, "Buy groceries", now + 86400, PERIOD_MORNING);
add_task(1, "Review PR #42", now + 3600, PERIOD_AFTERNOON);
add_task(1, "Book dentist appointment", now + 172800, PERIOD_MORNING);
add_task(1, "Update resume", now + 604800, PERIOD_EVENING);
// Bob's tasks (user_id = 2)
add_task(2, "Fix login bug", now + 7200); // +2 hours
add_task(2, "Deploy staging", now + 43200); // +12 hours
add_task(2, "Write unit tests", now + 259200); // +3 days
// Bob's tasks (user_id = 2) — some recurring routines
add_task(2, "Morning standup", 0, PERIOD_MORNING, 0x1F); // Mon-Fri
add_task(2, "Deploy staging", now + 43200, PERIOD_AFTERNOON);
add_task(2, "Write unit tests", now + 259200, PERIOD_MORNING);
// Charlie's tasks (user_id = 3)
add_task(3, "Water plants", now + 1800); // +30 min
add_task(3, "Call plumber", now + 86400); // +1 day
// Charlie's tasks (user_id = 3) — kid routine examples
add_task(3, "Breakfast", 0, PERIOD_MORNING, 0x1F); // Mon-Fri
add_task(3, "Homework", 0, PERIOD_AFTERNOON, 0x15); // Mon+Wed+Fri
add_task(3, "Bath time", 0, PERIOD_EVENING, 0x7F); // Every day
}

View File

@@ -5,7 +5,8 @@
// Data store operations for tasks
task_t *find_task(uint16 id);
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,
uint8 period = PERIOD_MORNING, uint8 recurrence = 0);
bool remove_task(uint16 id);
void remove_tasks_for_user(uint8 user_id);
void sort_tasks_by_due_date(task_t **arr, int count);

View File

@@ -1,4 +1,4 @@
// GET /api/tasks/upcoming — Top 3 upcoming tasks per user (for Dashboard)
// GET /api/tasks/upcoming — Today's tasks per user, grouped by period
#include "cJSON.h"
#include "esp_http_server.h"
@@ -20,36 +20,26 @@ internal esp_err_t api_tasks_upcoming_handler(httpd_req_t *req)
continue;
// Collect incomplete tasks for this user
task_t *user_tasks[MAX_TASKS];
int count = 0;
for (int t = 0; t < MAX_TASKS; t++)
{
if (g_Tasks[t].active && g_Tasks[t].user_id == g_Users[u].id &&
!g_Tasks[t].completed)
{
user_tasks[count++] = &g_Tasks[t];
}
}
// Sort by due_date ascending
sort_tasks_by_due_date(user_tasks, count);
// Build user object with top 3
// Include: recurring tasks (any day) and one-off tasks
cJSON *user_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(user_obj, "id", g_Users[u].id);
cJSON_AddStringToObject(user_obj, "name", g_Users[u].name);
cJSON *tasks_arr = cJSON_AddArrayToObject(user_obj, "tasks");
int limit = count < 3 ? count : 3;
for (int i = 0; i < limit; i++)
for (int t = 0; t < MAX_TASKS; t++)
{
if (!g_Tasks[t].active || g_Tasks[t].completed ||
g_Tasks[t].user_id != g_Users[u].id)
continue;
cJSON *t_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(t_obj, "id", user_tasks[i]->id);
cJSON_AddStringToObject(t_obj, "title", user_tasks[i]->title);
cJSON_AddNumberToObject(t_obj, "due_date",
(double)user_tasks[i]->due_date);
cJSON_AddBoolToObject(t_obj, "completed", user_tasks[i]->completed);
cJSON_AddNumberToObject(t_obj, "id", g_Tasks[t].id);
cJSON_AddStringToObject(t_obj, "title", g_Tasks[t].title);
cJSON_AddNumberToObject(t_obj, "due_date", (double)g_Tasks[t].due_date);
cJSON_AddNumberToObject(t_obj, "period", g_Tasks[t].period);
cJSON_AddNumberToObject(t_obj, "recurrence", g_Tasks[t].recurrence);
cJSON_AddBoolToObject(t_obj, "completed", g_Tasks[t].completed);
cJSON_AddItemToArray(tasks_arr, t_obj);
}

View File

@@ -1,6 +1,6 @@
// POST /api/tasks/update — Modify a task
// Body: {"id":1, "title":"...", "due_date":..., "completed":true}
// All fields except "id" are optional
// Body: {"id":1, "title":"...", "due_date":..., "period":0, "recurrence":0,
// "completed":true} All fields except "id" are optional
#include "cJSON.h"
#include "esp_http_server.h"
@@ -58,6 +58,18 @@ internal esp_err_t api_tasks_update_handler(httpd_req_t *req)
task->due_date = (int64)due_date_item->valuedouble;
}
cJSON *period_item = cJSON_GetObjectItem(body, "period");
if (cJSON_IsNumber(period_item))
{
task->period = (uint8)period_item->valueint;
}
cJSON *recurrence_item = cJSON_GetObjectItem(body, "recurrence");
if (cJSON_IsNumber(recurrence_item))
{
task->recurrence = (uint8)recurrence_item->valueint;
}
cJSON *completed_item = cJSON_GetObjectItem(body, "completed");
if (cJSON_IsBool(completed_item))
{

View File

@@ -5,14 +5,24 @@
#include "types.hpp"
#include "user.hpp"
enum task_period_t : uint8
{
PERIOD_MORNING = 0x01, // bit 0
PERIOD_AFTERNOON = 0x02, // bit 1
PERIOD_EVENING = 0x04, // bit 2
PERIOD_ALL_DAY = 0x07 // all bits
};
struct task_t
{
uint16 id; // Auto-assigned (165535, 0 = empty slot)
uint8 user_id; // Owner (matches user_t.id)
char title[64]; // Task description
int64 due_date; // Unix timestamp (seconds)
bool completed; // Done flag
bool active; // Slot in use
char title[64]; // Task description
int64 due_date; // Unix timestamp (seconds) - used when recurrence is 0
uint16 id; // Auto-assigned (165535, 0 = empty slot)
uint8 user_id; // Owner (matches user_t.id)
uint8 recurrence; // Bitmask: bit0=Mon, bit1=Tue, ..., bit6=Sun. 0=none
uint8 period : 3; // Bitmask: bit0=Morning, bit1=Afternoon, bit2=Evening
bool completed : 1; // Done flag
bool active : 1; // Slot in use
};
constexpr int MAX_TASKS = 32;