Use drive serial for dedup hash; update title/priority on worsened conditions
Two changes to the external ticket API: 1. Serial-based dedup: generateTicketHash() now uses the `serial` field from the hwmonDaemon payload as the stable drive identifier instead of extracting /dev/sdX from the title. Device path is kept as a fallback for payloads without a serial field (backwards compatible). Hash key renamed from `device` to `drive` to reflect this. 2. Active-ticket updates: when a duplicate is detected and the ticket is still open, the API now compares the incoming title and priority against the existing ticket. If the title changed or priority escalated (lower number), the ticket is updated and a comment is added explaining what changed. Previously the API silently returned "Duplicate ticket" with no update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+86
-10
@@ -104,9 +104,15 @@ if (!is_array($data) || empty($data['title'])) {
|
|||||||
function generateTicketHash($data) {
|
function generateTicketHash($data) {
|
||||||
$title = (string)($data['title'] ?? '');
|
$title = (string)($data['title'] ?? '');
|
||||||
|
|
||||||
|
// Prefer explicit serial from payload; fall back to extracting device path from title
|
||||||
|
// for backwards compatibility with older hwmonDaemon versions.
|
||||||
|
$serial = isset($data['serial']) && $data['serial'] !== null && $data['serial'] !== ''
|
||||||
|
? (string)$data['serial']
|
||||||
|
: null;
|
||||||
|
|
||||||
// Extract device name if present (matches /dev/sdX, /dev/nvmeXnY patterns)
|
// Extract device name if present (matches /dev/sdX, /dev/nvmeXnY patterns)
|
||||||
preg_match('/\/dev\/(sd[a-z]+|nvme\d+n\d+)/', $title, $deviceMatches);
|
preg_match('/\/dev\/(sd[a-z]+|nvme\d+n\d+)/', $title, $deviceMatches);
|
||||||
$isDriveTicket = !empty($deviceMatches);
|
$isDriveTicket = !empty($deviceMatches) || $serial !== null;
|
||||||
|
|
||||||
// Extract first bracketed tag as hostname/source
|
// Extract first bracketed tag as hostname/source
|
||||||
preg_match('/^\[([^\]]+)\]/', $title, $hostMatches);
|
preg_match('/^\[([^\]]+)\]/', $title, $hostMatches);
|
||||||
@@ -168,9 +174,11 @@ function generateTicketHash($data) {
|
|||||||
$stableComponents['hostname'] = $hostname;
|
$stableComponents['hostname'] = $hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include device path for drive-specific tickets
|
// Include drive identifier for drive-specific tickets.
|
||||||
|
// Use serial when available (stable across reboots/reshuffles); fall back to
|
||||||
|
// device path for tickets created before serial was added to the payload.
|
||||||
if ($isDriveTicket) {
|
if ($isDriveTicket) {
|
||||||
$stableComponents['device'] = $deviceMatches[0];
|
$stableComponents['drive'] = $serial ?? ($deviceMatches[0] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($stableComponents['environment_tags']);
|
sort($stableComponents['environment_tags']);
|
||||||
@@ -190,22 +198,90 @@ $ticketHash = generateTicketHash($data);
|
|||||||
$auditLog = new AuditLogModel($conn);
|
$auditLog = new AuditLogModel($conn);
|
||||||
|
|
||||||
// Look up any existing ticket with this hash (open OR closed)
|
// Look up any existing ticket with this hash (open OR closed)
|
||||||
$checkStmt = $conn->prepare("SELECT ticket_id, status FROM tickets WHERE hash = ? ORDER BY created_at DESC LIMIT 1");
|
$checkStmt = $conn->prepare("SELECT ticket_id, status, title, priority FROM tickets WHERE hash = ? ORDER BY created_at DESC LIMIT 1");
|
||||||
$checkStmt->bind_param("s", $ticketHash);
|
$checkStmt->bind_param("s", $ticketHash);
|
||||||
$checkStmt->execute();
|
$checkStmt->execute();
|
||||||
$existing = $checkStmt->get_result()->fetch_assoc();
|
$existing = $checkStmt->get_result()->fetch_assoc();
|
||||||
$checkStmt->close();
|
$checkStmt->close();
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
$existingId = $existing['ticket_id'];
|
$existingId = $existing['ticket_id'];
|
||||||
$existingStatus = $existing['status'];
|
$existingStatus = $existing['status'];
|
||||||
|
$existingTitle = $existing['title'];
|
||||||
|
$existingPriority = (int)$existing['priority'];
|
||||||
|
$newPriority = (int)$priority;
|
||||||
|
|
||||||
if ($existingStatus !== 'Closed') {
|
if ($existingStatus !== 'Closed') {
|
||||||
// Ticket is still active — do not create a duplicate
|
// Ticket is still active — update title and escalate priority if the new
|
||||||
|
// report is more severe (lower number = higher severity).
|
||||||
|
$changes = [];
|
||||||
|
$updateSql = "UPDATE tickets SET updated_at = NOW(), updated_by = ?";
|
||||||
|
$bindTypes = "i";
|
||||||
|
$bindVals = [$userId];
|
||||||
|
|
||||||
|
if ($title !== $existingTitle) {
|
||||||
|
$updateSql .= ", title = ?";
|
||||||
|
$bindTypes .= "s";
|
||||||
|
$bindVals[] = $title;
|
||||||
|
$changes['title'] = ['from' => $existingTitle, 'to' => $title];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newPriority < $existingPriority) {
|
||||||
|
$updateSql .= ", priority = ?";
|
||||||
|
$bindTypes .= "i";
|
||||||
|
$bindVals[] = $newPriority;
|
||||||
|
$changes['priority'] = ['from' => $existingPriority, 'to' => $newPriority];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($changes)) {
|
||||||
|
$updateSql .= " WHERE ticket_id = ?";
|
||||||
|
$bindTypes .= "s";
|
||||||
|
$bindVals[] = $existingId;
|
||||||
|
|
||||||
|
$updStmt = $conn->prepare($updateSql);
|
||||||
|
$updStmt->bind_param($bindTypes, ...$bindVals);
|
||||||
|
$updStmt->execute();
|
||||||
|
$updStmt->close();
|
||||||
|
|
||||||
|
// Comment summarising what changed
|
||||||
|
$changeLines = [];
|
||||||
|
if (isset($changes['title'])) {
|
||||||
|
$changeLines[] = "- **Title updated** to reflect current issue";
|
||||||
|
}
|
||||||
|
if (isset($changes['priority'])) {
|
||||||
|
$changeLines[] = "- **Priority escalated** from P{$changes['priority']['from']} to P{$changes['priority']['to']}";
|
||||||
|
}
|
||||||
|
$commentText = "**hwmonDaemon reported a worsened condition — ticket updated automatically.**\n\n" .
|
||||||
|
implode("\n", $changeLines) . "\n\nLatest report:\n\n" . $description;
|
||||||
|
$commentStmt = $conn->prepare(
|
||||||
|
"INSERT INTO ticket_comments (ticket_id, user_id, user_name, comment_text, markdown_enabled) VALUES (?, ?, 'hwmonDaemon', ?, 1)"
|
||||||
|
);
|
||||||
|
$commentStmt->bind_param("sis", $existingId, $userId, $commentText);
|
||||||
|
$commentStmt->execute();
|
||||||
|
$commentStmt->close();
|
||||||
|
|
||||||
|
$auditLog->log($userId, 'update', 'ticket', $existingId, array_merge(
|
||||||
|
$changes,
|
||||||
|
['reason' => 'auto-updated by hwmonDaemon (condition worsened)']
|
||||||
|
));
|
||||||
|
|
||||||
|
require_once __DIR__ . '/helpers/NotificationHelper.php';
|
||||||
|
NotificationHelper::sendTicketNotification($existingId, [
|
||||||
|
'title' => $title,
|
||||||
|
'priority' => $newPriority < $existingPriority ? $newPriority : $existingPriority,
|
||||||
|
'category' => $category,
|
||||||
|
'type' => $type,
|
||||||
|
'status' => $existingStatus,
|
||||||
|
], 'automated');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'success' => true,
|
||||||
'error' => 'Duplicate ticket',
|
'ticket_id' => $existingId,
|
||||||
'existing_ticket_id' => $existingId,
|
'message' => empty($changes) ? 'Duplicate — no change' : 'Existing ticket updated',
|
||||||
|
'action' => empty($changes) ? 'deduplicated' : 'updated',
|
||||||
|
'changes' => $changes,
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user