Route hwmonDaemon's auto-reopen through Workflow Designer validation (#68)
Lint / PHP (phpcs PSR-12) (push) Successful in 50s
Lint / JS (eslint) (push) Successful in 14s
Lint / PHP requirements (version + extensions) (push) Successful in 34s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 2m25s
Lint / Deploy (push) Successful in 8s
Lint / PHP (phpcs PSR-12) (push) Successful in 50s
Lint / JS (eslint) (push) Successful in 14s
Lint / PHP requirements (version + extensions) (push) Successful in 34s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 2m25s
Lint / Deploy (push) Successful in 8s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
This commit is contained in:
+70
-23
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user