From a4828c1b7b3554c8e392e8e0a8cac3fb501205b5 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 11 Sep 2026 14:28:31 -0400 Subject: [PATCH] Alert and record a lost recurring-ticket occurrence on creation failure (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RecurringTicketModel::claimForRun() deliberately advances next_run_at before TicketModel::createTicket() runs, to prevent duplicate-ticket floods if creation fails partway and the cron retries. The tradeoff: if createTicket() then fails, that specific occurrence is gone forever with no record anywhere an admin would normally look — the catch block only wrote a line to stdout/the cron log. Added recordMissedOccurrence(), called from both the "createTicket() returned success:false" branch and the exception catch, which writes an audit_log entry (entity_type='recurring_ticket', action_type='error') and fires a new NotificationHelper::sendSystemAlert() — a generic operational alert (unlike the ticket-specific notification methods, it has no associated ticket) sent to the shared MATRIX_NOTIFY_USERS list regardless of any per-event toggle, so a silently-skipped recurring ticket surfaces immediately instead of requiring someone to grep cron logs. Verified against real MariaDB and a real webhook-capturing server: calling the recorder writes the audit_log row with the failure reason and schedule details, and fires the Matrix alert with the same information. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv --- cron/create_recurring_tickets.php | 40 ++++++++++++++++++++++++++++++- helpers/NotificationHelper.php | 17 +++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/cron/create_recurring_tickets.php b/cron/create_recurring_tickets.php index f2b851d..49ab526 100644 --- a/cron/create_recurring_tickets.php +++ b/cron/create_recurring_tickets.php @@ -29,6 +29,38 @@ function logMessage($message) echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n"; } +/** + * Record a recurring-ticket occurrence that was claimed (next_run_at already + * advanced to the next future run) but then failed to actually produce a + * ticket. That claim-then-fail ordering is deliberate — it stops a failing + * creation from re-firing and flooding duplicates on every subsequent cron + * tick — but means this specific occurrence has no other record anywhere an + * admin would normally look: no audit_log entry (nothing was created), no + * Matrix "ticket created" alert, no failure table. Without this, it's simply + * gone, silently, forever. + */ +function recordMissedOccurrence($auditLog, $recurring, $reason) +{ + $auditLog->log( + $recurring['created_by'], + 'error', + 'recurring_ticket', + (string)$recurring['recurring_id'], + [ + 'reason' => $reason, + 'title_template' => $recurring['title_template'], + 'schedule_type' => $recurring['schedule_type'], + ] + ); + + NotificationHelper::sendSystemAlert( + "Recurring ticket occurrence lost: schedule #{$recurring['recurring_id']} " + . "(\"{$recurring['title_template']}\") was claimed for this run but ticket " + . "creation failed, so this occurrence will not be created or retried.", + ['reason' => $reason, 'recurring_id' => $recurring['recurring_id']] + ); +} + logMessage("Starting recurring tickets cron job"); try { @@ -100,11 +132,17 @@ try { $created++; } else { - logMessage("ERROR: Failed to create ticket - " . ($result['error'] ?? 'Unknown error')); + $reason = $result['error'] ?? 'Unknown error'; + logMessage("ERROR: Failed to create ticket - " . $reason); + recordMissedOccurrence($auditLog, $recurring, $reason); $errors++; } } catch (Exception $e) { logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage()); + // claimForRun() already advanced next_run_at before this point, so + // this occurrence is permanently gone unless recorded somewhere an + // admin would actually look — a cron log line alone doesn't count. + recordMissedOccurrence($auditLog, $recurring, $e->getMessage()); $errors++; } } diff --git a/helpers/NotificationHelper.php b/helpers/NotificationHelper.php index 4ffa01a..f4590e5 100644 --- a/helpers/NotificationHelper.php +++ b/helpers/NotificationHelper.php @@ -59,6 +59,23 @@ class NotificationHelper // ─── Public event methods ───────────────────────────────────────────────── + /** + * Generic operational alert with no associated ticket (e.g. a recurring + * schedule whose ticket creation failed after its next_run_at was + * already advanced, so the missed occurrence has no other record an + * admin would normally see). Always sent to the shared + * MATRIX_NOTIFY_USERS list, regardless of any per-event notify toggle. + */ + public static function sendSystemAlert(string $message, array $context = []): void + { + self::fire(array_merge([ + 'event' => 'system_alert', + 'message' => $message, + ], $context, [ + 'notify_users' => self::notifyUsers(), + ])); + } + /** * New ticket created (manual or automated/API). *