From d913f3c0eb530d749abe510d03e540c5b41072ba Mon Sep 17 00:00:00 2001 From: perco Date: Tue, 12 May 2026 14:55:27 +0200 Subject: [PATCH] Todo: scheduled Discord notifications via cron MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cron/todo_notify.php: checks every minute for tasks due now and sends Discord reminder (⏰) only when due_date+due_time is reached - No immediate notification when due_time is set (cron handles it) - Immediate notification still fires for tasks without a due_time - notified column tracks whether reminder was already sent - Resets notified=0 when a task is edited (new time picked) - Cron runs via: docker exec househub php /var/www/html/cron/todo_notify.php Co-Authored-By: Claude Sonnet 4.6 --- cron/todo_notify.php | 98 ++++++++++++++++++++++++++++++++++++++++ docker/schema_family.sql | 1 + modules/todo/api.php | 8 +++- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 cron/todo_notify.php diff --git a/cron/todo_notify.php b/cron/todo_notify.php new file mode 100644 index 0000000..3123b88 --- /dev/null +++ b/cron/todo_notify.php @@ -0,0 +1,98 @@ + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] + ); + $families = $meta->query("SELECT id FROM families")->fetchAll(PDO::FETCH_COLUMN); +} catch (Exception $e) { + exit("Cannot connect to meta DB: " . $e->getMessage() . "\n"); +} + +foreach ($families as $family_id) { + $db_name = 'househub_f' . $family_id; + + try { + $pdo = new PDO( + "mysql:host=$DB_HOST;dbname=$db_name;charset=utf8mb4", + $DB_USER, $DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] + ); + } catch (Exception $e) { + continue; + } + + // Skip if todo tables don't exist yet + if (!$pdo->query("SHOW TABLES LIKE 'pf_todos'")->fetchColumn()) continue; + + // Get Discord webhook for this family + $ws = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='todo_settings' AND reference_id='webhook_discord'"); + $ws->execute(); + $webhook = $ws->fetchColumn(); + if (!$webhook) continue; + + // Tasks due now: due today, due_time reached, not done, not yet notified + $stmt = $pdo->prepare(" + SELECT t.id, t.title, t.due_date, t.due_time, + l.name AS list_name, l.icon AS list_icon + FROM pf_todos t + LEFT JOIN pf_todo_lists l ON l.id = t.list_id + WHERE t.due_date = CURDATE() + AND t.due_time IS NOT NULL + AND t.due_time <= CURTIME() + AND t.done = 0 + AND t.notified = 0 + "); + $stmt->execute(); + + foreach ($stmt->fetchAll() as $t) { + $time = substr($t['due_time'], 0, 5); + $list = $t['list_name'] ? " _({$t['list_icon']} {$t['list_name']})_" : ''; + $msg = "⏰ **Rappel** : {$t['title']}{$list} — prévu à {$time}"; + + $ch = curl_init($webhook); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_POSTFIELDS => json_encode(['username' => 'HouseHub Todo', 'content' => $msg]), + ]); + curl_exec($ch); + $ok = curl_getinfo($ch, CURLINFO_HTTP_CODE) < 300; + curl_close($ch); + + if ($ok) { + $pdo->prepare("UPDATE pf_todos SET notified=1 WHERE id=?")->execute([$t['id']]); + } + } +} diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 4fb9964..5c3e599 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -335,6 +335,7 @@ CREATE TABLE IF NOT EXISTS pf_todos ( notes TEXT DEFAULT NULL, due_date DATE DEFAULT NULL, due_time TIME DEFAULT NULL, + notified TINYINT(1) DEFAULT 0, priority ENUM('none','low','medium','high') DEFAULT 'none', done TINYINT(1) DEFAULT 0, done_at DATETIME DEFAULT NULL, diff --git a/modules/todo/api.php b/modules/todo/api.php index 6e27cbb..efd1a27 100644 --- a/modules/todo/api.php +++ b/modules/todo/api.php @@ -122,7 +122,10 @@ if ($action === 'todos') { $new = $pdo->prepare("SELECT t.*, l.name as list_name, l.color as list_color, l.icon as list_icon FROM pf_todos t LEFT JOIN pf_todo_lists l ON l.id = t.list_id WHERE t.id=?"); $new->execute([$id]); $row = $new->fetch(); - discordNotify($pdo, "📋 **Nouvelle tâche** : " . $title . ($row['list_name'] ? " _(". $row['list_name'] .")_" : "")); + // Only notify immediately if no due_time (otherwise cron sends it at the right time) + if (empty($d['due_time'])) { + discordNotify($pdo, "📋 **Nouvelle tâche** : " . $title . ($row['list_name'] ? " _(". $row['list_name'] .")_" : "")); + } tOk($row); } @@ -145,7 +148,8 @@ if ($action === 'todos') { // Full update $title = trim($d['title'] ?? ''); if (!$title) tErr('Titre requis'); - $pdo->prepare("UPDATE pf_todos SET list_id=?, title=?, notes=?, due_date=?, due_time=?, priority=?, updated_at=NOW() WHERE id=?") + // Reset notified if due_time changed + $pdo->prepare("UPDATE pf_todos SET list_id=?, title=?, notes=?, due_date=?, due_time=?, priority=?, notified=0, updated_at=NOW() WHERE id=?") ->execute([$d['list_id'] ?: null, $title, $d['notes'] ?? null, $d['due_date'] ?: null, $d['due_time'] ?: null, $d['priority'] ?? 'none', $id]); tOk(['updated' => true]); }