From 18c213ebd76a9ebf9e23e2eeb0d01c90aa569e69 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 11 Sep 2026 12:37:46 -0400 Subject: [PATCH] Route hwmonDaemon's auto-reopen through Workflow Designer validation (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_ticket_api.php's dedup-reopen path wrote status = 'Open' via a raw SQL UPDATE, completely bypassing TicketModel::updateTicket() and WorkflowModel::isTransitionAllowed() — the one status-write path in the app that never consulted the workflow engine at all. If an admin configured the Workflow Designer to disallow a direct Closed->Open transition, this automated path still forced it unconditionally. Now checks isTransitionAllowed('Closed', 'Open', false) first (false since this is an unattended system account, not admin-elevated). If not allowed, falls back to any transition the Workflow Designer does allow from Closed that requires neither a comment nor admin privilege (both of which this unattended automation can't satisfy), and applies it via TicketModel::updateTicket() instead of raw SQL. If no such transition exists at all, the ticket is deliberately left Closed (rather than forcing an unconfigured state) with a comment and audit entry explaining why, so the recurrence is still visible to a human without silently violating workflow rules. Verified against real MariaDB across all three branches: direct Closed->Open allowed (reopens to Open), disallowed but Closed->'In Progress' available unattended (falls back correctly), and no usable transition configured at all (ticket correctly stays Closed). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv --- create_ticket_api.php | 93 ++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/create_ticket_api.php b/create_ticket_api.php index d8e8b21..a723104 100644 --- a/create_ticket_api.php +++ b/create_ticket_api.php @@ -42,6 +42,8 @@ try { require_once __DIR__ . '/middleware/ApiKeyAuth.php'; require_once __DIR__ . '/models/AuditLogModel.php'; require_once __DIR__ . '/models/StatsModel.php'; +require_once __DIR__ . '/models/TicketModel.php'; +require_once __DIR__ . '/models/WorkflowModel.php'; require_once __DIR__ . '/helpers/UrlHelper.php'; $apiKeyAuth = new ApiKeyAuth($conn); @@ -338,17 +340,52 @@ if ($existing) { exit; } - // Ticket was closed — reopen it and add a recurrence comment - $reopenStmt = $conn->prepare( - "UPDATE tickets SET status = 'Open', closed_at = NULL, updated_at = NOW(), updated_by = ? WHERE ticket_id = ?" - ); - $reopenStmt->bind_param("is", $userId, $existingId); - $reopenStmt->execute(); - $reopenStmt->close(); + // Ticket was closed — reopen it and add a recurrence comment. Route + // through the Workflow Designer like every other status-write path in + // the app, rather than forcing status='Open' via raw SQL regardless of + // configured transition rules. + $workflowModel = new WorkflowModel($conn); + $reopenStatus = 'Open'; + if (!$workflowModel->isTransitionAllowed('Closed', 'Open', false)) { + // Direct Closed->Open isn't configured — fall back to any transition + // the Workflow Designer does allow from Closed that this unattended, + // non-admin automation can actually satisfy (no comment prompt, no + // admin elevation). If even that doesn't exist, leave the ticket + // Closed rather than force an unconfigured state. + $reopenStatus = null; + foreach ($workflowModel->getAllowedTransitions('Closed') as $transition) { + if (!$transition['requires_comment'] && !$transition['requires_admin']) { + $reopenStatus = $transition['to_status']; + break; + } + } + } + + if ($reopenStatus !== null) { + $ticketModel = new TicketModel($conn); + $ticketModel->updateTicket([ + 'ticket_id' => $existingId, + 'title' => $title, + 'description' => $description, + 'category' => $category, + 'type' => $type, + 'status' => $reopenStatus, + 'priority' => $priority, + ], $userId); + } else { + error_log("create_ticket_api: hwmonDaemon recurrence for ticket $existingId — " + . "no admin-free, comment-free transition from Closed is configured; leaving ticket Closed"); + } $commentText = "**Issue recurred — ticket reopened automatically.**\n\n" . "hwmonDaemon detected this condition again. The ticket description reflects the " . "original report; see this comment's timestamp for when the issue recurred."; + if ($reopenStatus === null) { + $commentText = "**Issue recurred, but the ticket could not be reopened automatically.**\n\n" + . "hwmonDaemon detected this condition again. No Workflow Designer transition from " + . "Closed is configured that this automation can perform unattended (no comment/admin " + . "requirement); the ticket remains Closed. Please review and reopen manually if appropriate."; + } $commentStmt = $conn->prepare( "INSERT INTO ticket_comments (ticket_id, user_id, user_name, comment_text, markdown_enabled) VALUES (?, ?, 'hwmonDaemon', ?, 1)" ); @@ -356,30 +393,40 @@ if ($existing) { $commentStmt->execute(); $commentStmt->close(); - $auditLog->log($userId, 'update', 'ticket', $existingId, [ - 'status' => ['from' => 'Closed', 'to' => 'Open'], - 'reason' => 'auto-reopened by hwmonDaemon (issue recurred)', - ]); + if ($reopenStatus !== null) { + $auditLog->log($userId, 'update', 'ticket', $existingId, [ + 'status' => ['from' => 'Closed', 'to' => $reopenStatus], + 'reason' => 'auto-reopened by hwmonDaemon (issue recurred)', + ]); - // Ticket reopened (Closed → Open) — refresh dashboard stats. - (new StatsModel($conn))->invalidateCache(); + // Ticket reopened — refresh dashboard stats. + (new StatsModel($conn))->invalidateCache(); + } else { + $auditLog->log($userId, 'update', 'ticket', $existingId, [ + 'reason' => 'hwmonDaemon recurrence detected but no valid reopen transition configured; ticket left Closed', + ]); + } Database::close(); - require_once __DIR__ . '/helpers/NotificationHelper.php'; - NotificationHelper::sendTicketNotification($existingId, [ - 'title' => $title, - 'priority' => $priority, - 'category' => $category, - 'type' => $type, - 'status' => 'Open', - ], 'automated'); + if ($reopenStatus !== null) { + require_once __DIR__ . '/helpers/NotificationHelper.php'; + NotificationHelper::sendTicketNotification($existingId, [ + 'title' => $title, + 'priority' => $priority, + 'category' => $category, + 'type' => $type, + 'status' => $reopenStatus, + ], 'automated'); + } echo json_encode([ 'success' => true, 'ticket_id' => $existingId, - 'message' => 'Existing closed ticket reopened', - 'action' => 'reopened', + 'message' => $reopenStatus !== null + ? 'Existing closed ticket reopened' + : 'Recurrence noted; ticket left Closed (no valid workflow transition configured)', + 'action' => $reopenStatus !== null ? 'reopened' : 'recurrence_noted', ]); exit; }