false, 'error' => 'Configuration file not found' ]); exit; } $envVars = parse_ini_file($envFile, false, INI_SCANNER_TYPED); if (!$envVars) { echo json_encode([ 'success' => false, 'error' => 'Invalid configuration file' ]); exit; } // Strip quotes from values if present (parse_ini_file may include them) foreach ($envVars as $key => $value) { if (is_string($value)) { if ((substr($value, 0, 1) === '"' && substr($value, -1) === '"') || (substr($value, 0, 1) === "'" && substr($value, -1) === "'")) { $envVars[$key] = substr($value, 1, -1); } } } // Database connection with detailed error handling $conn = new mysqli( $envVars['DB_HOST'], $envVars['DB_USER'], $envVars['DB_PASS'], $envVars['DB_NAME'] ); if ($conn->connect_error) { echo json_encode([ 'success' => false, 'error' => 'Database connection failed: ' . $conn->connect_error ]); exit; } // Load application config so UrlHelper can resolve APP_DOMAIN require_once __DIR__ . '/config/config.php'; // Authenticate via API key require_once __DIR__ . '/middleware/ApiKeyAuth.php'; require_once __DIR__ . '/models/AuditLogModel.php'; require_once __DIR__ . '/helpers/UrlHelper.php'; $apiKeyAuth = new ApiKeyAuth($conn); try { $systemUser = $apiKeyAuth->authenticate(); } catch (Exception $e) { // Authentication failed - ApiKeyAuth already sent the response exit; } $userId = $systemUser['user_id']; // Create tickets table with hash column if not exists $createTableSQL = "CREATE TABLE IF NOT EXISTS tickets ( id INT AUTO_INCREMENT PRIMARY KEY, ticket_id VARCHAR(9) NOT NULL, title VARCHAR(255) NOT NULL, hash VARCHAR(64) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_hash (hash) )"; $conn->query($createTableSQL); // Parse input regardless of content-type header $rawInput = file_get_contents('php://input'); $data = json_decode($rawInput, true); // Validate required fields before any processing if (!is_array($data) || empty($data['title'])) { // Try URL-encoded fallback if (empty($data['title'])) { parse_str($rawInput, $urlData); if (!empty($urlData['title'])) { $data = $urlData; } } if (!is_array($data) || empty($data['title'])) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'title is required']); exit; } } // Generate hash from stable components function generateTicketHash($data) { $title = (string)($data['title'] ?? ''); // Extract device name if present (matches /dev/sdX, /dev/nvmeXnY patterns) preg_match('/\/dev\/(sd[a-z]+|nvme\d+n\d+)/', $title, $deviceMatches); $isDriveTicket = !empty($deviceMatches); // Extract first bracketed tag as hostname/source preg_match('/^\[([^\]]+)\]/', $title, $hostMatches); $hostname = $hostMatches[1] ?? ''; // Detect issue category and optional sub-type $issueCategory = ''; $issueSubtype = ''; $isClusterWide = false; if (stripos($title, 'SMART issues') !== false) { $issueCategory = 'smart'; } elseif (stripos($title, 'LXC') !== false || stripos($title, 'storage usage') !== false) { $issueCategory = 'storage'; // Include the LXC container ID so each container gets its own ticket if (preg_match('/LXC\s+(\d+)/i', $title, $lxcMatch)) { $issueSubtype = 'lxc_' . $lxcMatch[1]; } } elseif (stripos($title, 'memory') !== false) { $issueCategory = 'memory'; } elseif (stripos($title, 'cpu') !== false) { $issueCategory = 'cpu'; } elseif (stripos($title, 'network') !== false) { $issueCategory = 'network'; } elseif (stripos($title, 'Ceph') !== false || stripos($title, '[ceph]') !== false) { $issueCategory = 'ceph'; if (stripos($title, '[cluster-wide]') !== false || stripos($title, 'HEALTH_ERR') !== false || stripos($title, 'HEALTH_WARN') !== false || stripos($title, 'cluster usage') !== false) { $isClusterWide = true; } // Normalize the specific Ceph warning type so different warnings get distinct tickets if (stripos($title, 'slow') !== false && stripos($title, 'BlueStore') !== false) { $issueSubtype = 'bluestore_slow'; } elseif (stripos($title, 'clock skew') !== false) { $issueSubtype = 'clock_skew'; } elseif (stripos($title, 'cluster usage') !== false) { $issueSubtype = 'usage'; } elseif (stripos($title, 'OSD down') !== false) { $issueSubtype = 'osd_down'; } elseif (stripos($title, 'HEALTH_ERR') !== false) { $issueSubtype = 'health_err'; } } // Build stable components $stableComponents = [ 'issue_category' => $issueCategory, 'issue_subtype' => $issueSubtype, 'environment_tags' => array_values(array_filter( explode('][', $title), fn($tag) => in_array($tag, ['production', 'development', 'staging', 'single-node', 'cluster-wide']) )), ]; // Include hostname for node-specific issues if (!$isClusterWide) { $stableComponents['hostname'] = $hostname; } // Include device path for drive-specific tickets if ($isDriveTicket) { $stableComponents['device'] = $deviceMatches[0]; } sort($stableComponents['environment_tags']); return hash('sha256', json_encode($stableComponents, JSON_UNESCAPED_SLASHES)); } // Shared ticket data $title = (string)($data['title'] ?? ''); $description = (string)($data['description'] ?? ''); $status = (string)($data['status'] ?? 'Open'); $priority = $data['priority'] ?? '4'; $category = (string)($data['category'] ?? 'General'); $type = (string)($data['type'] ?? 'Issue'); $ticketHash = generateTicketHash($data); $auditLog = new AuditLogModel($conn); // 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->bind_param("s", $ticketHash); $checkStmt->execute(); $existing = $checkStmt->get_result()->fetch_assoc(); $checkStmt->close(); if ($existing) { $existingId = $existing['ticket_id']; $existingStatus = $existing['status']; if ($existingStatus !== 'Closed') { // Ticket is still active — do not create a duplicate echo json_encode([ 'success' => false, 'error' => 'Duplicate ticket', 'existing_ticket_id' => $existingId, ]); 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(); $commentText = "**Issue recurred — ticket reopened automatically.**\n\n" . "New report received from hwmonDaemon:\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, [ 'status' => ['from' => 'Closed', 'to' => 'Open'], 'reason' => 'auto-reopened by hwmonDaemon (issue recurred)', ]); $conn->close(); require_once __DIR__ . '/helpers/NotificationHelper.php'; NotificationHelper::sendTicketNotification($existingId, [ 'title' => $title, 'priority' => $priority, 'category' => $category, 'type' => $type, 'status' => 'Open', ], 'automated'); echo json_encode([ 'success' => true, 'ticket_id' => $existingId, 'message' => 'Existing closed ticket reopened', 'action' => 'reopened', ]); exit; } // No existing ticket — create a new one $ticket_id = sprintf('%09d', mt_rand(1, 999999999)); $insertStmt = $conn->prepare( "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, hash, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" ); $insertStmt->bind_param("ssssssssi", $ticket_id, $title, $description, $status, $priority, $category, $type, $ticketHash, $userId ); try { $inserted = $insertStmt->execute(); } catch (mysqli_sql_exception $e) { $insertStmt->close(); if ($e->getCode() === 1062) { // Race condition: another node inserted the same hash between our SELECT and INSERT echo json_encode(['success' => false, 'error' => 'Duplicate ticket']); } else { echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; } $insertStmt->close(); if ($inserted) { $auditLog->logTicketCreate($userId, $ticket_id, [ 'title' => $title, 'priority' => $priority, 'category' => $category, 'type' => $type, ]); $conn->close(); require_once __DIR__ . '/helpers/NotificationHelper.php'; NotificationHelper::sendTicketNotification($ticket_id, [ 'title' => $title, 'priority' => $priority, 'category' => $category, 'type' => $type, 'status' => $status, ], 'automated'); echo json_encode([ 'success' => true, 'ticket_id' => $ticket_id, 'message' => 'Ticket created successfully', ]); } else { echo json_encode(['success' => false, 'error' => $conn->error]); }