- Add helpers/NotificationHelper.php: shared Matrix webhook sender that reads MATRIX_WEBHOOK_URL and MATRIX_NOTIFY_USERS from config - Remove sendDiscordWebhook() from TicketController; call NotificationHelper::sendTicketNotification() instead - Replace 60-line Discord embed block in create_ticket_api.php with a single NotificationHelper call - config/config.php: DISCORD_WEBHOOK_URL → MATRIX_WEBHOOK_URL + new MATRIX_NOTIFY_USERS key (comma-separated Matrix user IDs) - .env.example: updated env var names and comments Payload sent to hookshot includes notify_users array so the JS transform can build proper @mention links for each user. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
239 lines
7.2 KiB
PHP
239 lines
7.2 KiB
PHP
<?php
|
|
header('Content-Type: application/json');
|
|
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
|
|
// Load environment variables with error check
|
|
$envFile = __DIR__ . '/.env';
|
|
if (!file_exists($envFile)) {
|
|
echo json_encode([
|
|
'success' => 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);
|
|
|
|
// Generate hash from stable components
|
|
function generateTicketHash($data) {
|
|
// Extract device name if present (matches /dev/sdX, /dev/nvmeXnY patterns)
|
|
preg_match('/\/dev\/(sd[a-z]|nvme\d+n\d+)/', $data['title'], $deviceMatches);
|
|
$isDriveTicket = !empty($deviceMatches);
|
|
|
|
// Extract hostname from title [hostname][tags]...
|
|
preg_match('/\[([\w\d-]+)\]/', $data['title'], $hostMatches);
|
|
$hostname = $hostMatches[1] ?? '';
|
|
|
|
// Detect issue category (not specific attribute values)
|
|
$issueCategory = '';
|
|
$isClusterWide = false; // Flag for cluster-wide issues (exclude hostname from hash)
|
|
|
|
if (stripos($data['title'], 'SMART issues') !== false) {
|
|
$issueCategory = 'smart';
|
|
} elseif (stripos($data['title'], 'LXC') !== false || stripos($data['title'], 'storage usage') !== false) {
|
|
$issueCategory = 'storage';
|
|
} elseif (stripos($data['title'], 'memory') !== false) {
|
|
$issueCategory = 'memory';
|
|
} elseif (stripos($data['title'], 'cpu') !== false) {
|
|
$issueCategory = 'cpu';
|
|
} elseif (stripos($data['title'], 'network') !== false) {
|
|
$issueCategory = 'network';
|
|
} elseif (stripos($data['title'], 'Ceph') !== false || stripos($data['title'], '[ceph]') !== false) {
|
|
$issueCategory = 'ceph';
|
|
// Ceph cluster-wide issues should deduplicate across all nodes
|
|
// Check if this is a cluster-wide issue (not node-specific like OSD down on this node)
|
|
if (stripos($data['title'], '[cluster-wide]') !== false ||
|
|
stripos($data['title'], 'HEALTH_ERR') !== false ||
|
|
stripos($data['title'], 'HEALTH_WARN') !== false ||
|
|
stripos($data['title'], 'cluster usage') !== false) {
|
|
$isClusterWide = true;
|
|
}
|
|
}
|
|
|
|
// Build stable components with only static data
|
|
$stableComponents = [
|
|
'issue_category' => $issueCategory, // Generic category, not specific errors
|
|
'environment_tags' => array_filter(
|
|
explode('][', $data['title']),
|
|
fn($tag) => in_array($tag, ['production', 'development', 'staging', 'single-node', 'cluster-wide'])
|
|
)
|
|
];
|
|
|
|
// Only include hostname for non-cluster-wide issues
|
|
// This allows cluster-wide issues to deduplicate across all nodes
|
|
if (!$isClusterWide) {
|
|
$stableComponents['hostname'] = $hostname;
|
|
}
|
|
|
|
// Only include device info for drive-specific tickets
|
|
if ($isDriveTicket) {
|
|
$stableComponents['device'] = $deviceMatches[0];
|
|
}
|
|
|
|
// Sort arrays for consistent hashing
|
|
sort($stableComponents['environment_tags']);
|
|
|
|
return hash('sha256', json_encode($stableComponents, JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
// Check for duplicate tickets
|
|
$ticketHash = generateTicketHash($data);
|
|
$checkDuplicateSQL = "SELECT ticket_id FROM tickets WHERE hash = ? AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)";
|
|
$checkStmt = $conn->prepare($checkDuplicateSQL);
|
|
$checkStmt->bind_param("s", $ticketHash);
|
|
$checkStmt->execute();
|
|
$result = $checkStmt->get_result();
|
|
|
|
if ($result->num_rows > 0) {
|
|
$existingTicket = $result->fetch_assoc();
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => 'Duplicate ticket',
|
|
'existing_ticket_id' => $existingTicket['ticket_id']
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// Force JSON content type for all incoming requests
|
|
header('Content-Type: application/json');
|
|
|
|
if (!$data) {
|
|
// Try parsing as URL-encoded data
|
|
parse_str($rawInput, $data);
|
|
}
|
|
|
|
// Generate ticket ID (9-digit format with leading zeros)
|
|
$ticket_id = sprintf('%09d', mt_rand(1, 999999999));
|
|
|
|
// Prepare insert query with created_by field
|
|
$sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, hash, created_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
// First, store all values in variables
|
|
$title = $data['title'];
|
|
$description = $data['description'];
|
|
$status = $data['status'] ?? 'Open';
|
|
$priority = $data['priority'] ?? '4';
|
|
$category = $data['category'] ?? 'General';
|
|
$type = $data['type'] ?? 'Issue';
|
|
|
|
// Then use the variables in bind_param
|
|
$stmt->bind_param(
|
|
"ssssssssi",
|
|
$ticket_id,
|
|
$title,
|
|
$description,
|
|
$status,
|
|
$priority,
|
|
$category,
|
|
$type,
|
|
$ticketHash,
|
|
$userId
|
|
);
|
|
|
|
if ($stmt->execute()) {
|
|
// Log ticket creation to audit log
|
|
$auditLog = new AuditLogModel($conn);
|
|
$auditLog->logTicketCreate($userId, $ticket_id, [
|
|
'title' => $title,
|
|
'priority' => $priority,
|
|
'category' => $category,
|
|
'type' => $type
|
|
]);
|
|
|
|
echo json_encode([
|
|
'success' => true,
|
|
'ticket_id' => $ticket_id,
|
|
'message' => 'Ticket created successfully'
|
|
]);
|
|
} else {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => $conn->error
|
|
]);
|
|
}
|
|
|
|
$stmt->close();
|
|
$conn->close();
|
|
|
|
// Matrix webhook notification
|
|
require_once __DIR__ . '/helpers/NotificationHelper.php';
|
|
NotificationHelper::sendTicketNotification($ticket_id, [
|
|
'title' => $title,
|
|
'priority' => $priority,
|
|
'category' => $category,
|
|
'type' => $type,
|
|
'status' => $status,
|
|
], 'automated');
|