Fix ticket page 500 from unbounded audit timeline (#112)
Lint / PHP (phpcs PSR-12) (push) Successful in 21s
Lint / JS (eslint) (push) Successful in 8s
Lint / PHP requirements (version + extensions) (push) Successful in 20s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m22s
Lint / Deploy (push) Successful in 4s
Lint / PHP (phpcs PSR-12) (push) Successful in 21s
Lint / JS (eslint) (push) Successful in 8s
Lint / PHP requirements (version + extensions) (push) Successful in 20s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m22s
Lint / Deploy (push) Successful in 4s
A ticket updated by hwmonDaemon had ~52k audit rows; getTicketTimeline() loaded all of them and exhausted PHP's 128MB memory limit, so the ticket page returned 500. Not related to the MCP server. - getTicketTimeline() takes a limit (newest first). The ticket page shows the latest 500 events with a note when older ones are omitted; the JSON export caps at 5000. - create_ticket_api.php: the description is refreshed on every run, and that alone wrote a reason-only audit row every few minutes per open ticket. Audit only real title/priority changes. - create_ticket_api.php: after creating a brand-new ticket the dedup retry loop fell through into a second iteration on a closed connection, appending a 500 error body after the success response. Exit instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
This commit is contained in:
@@ -192,7 +192,7 @@ try {
|
||||
|
||||
// Load flat comment list (no threading nesting in export)
|
||||
$rawComments = $commentModel->getCommentsByTicketId($ticket['ticket_id'], false);
|
||||
$timeline = $auditLogModel->getTicketTimeline((string)$ticket['ticket_id']);
|
||||
$timeline = $auditLogModel->getTicketTimeline((string)$ticket['ticket_id'], 5000);
|
||||
|
||||
$comments = array_map(function ($c) {
|
||||
return [
|
||||
|
||||
@@ -54,8 +54,14 @@ class TicketController
|
||||
$totalComments = $this->commentModel->getCommentCount((int)$id);
|
||||
$comments = $this->commentModel->getCommentsByTicketId($id, true, $commentPageSize, 0);
|
||||
|
||||
// Get timeline for this ticket
|
||||
$timeline = $this->auditLogModel->getTicketTimeline($id);
|
||||
// Get the newest timeline events for this ticket. One extra row is
|
||||
// fetched only to tell the view that older events were left out.
|
||||
$timelineLimit = 500;
|
||||
$timeline = $this->auditLogModel->getTicketTimeline($id, $timelineLimit + 1);
|
||||
$timelineTruncated = count($timeline) > $timelineLimit;
|
||||
if ($timelineTruncated) {
|
||||
array_pop($timeline);
|
||||
}
|
||||
|
||||
// Get all users for assignment dropdown
|
||||
$allUsers = $this->userModel->getAllUsers();
|
||||
|
||||
+15
-4
@@ -328,10 +328,17 @@ for ($dedupAttempt = 1; $dedupAttempt <= $maxDedupAttempts; $dedupAttempt++) {
|
||||
$commentStmt->close();
|
||||
}
|
||||
|
||||
$auditLog->log($userId, 'update', 'ticket', $existingId, array_merge(
|
||||
array_diff_key($changes, ['description_refreshed' => true]),
|
||||
['reason' => 'auto-updated by hwmonDaemon (condition worsened)']
|
||||
));
|
||||
// Audit only real title/priority changes. The description is
|
||||
// refreshed on every run, and logging that alone wrote a
|
||||
// reason-only row every few minutes per open ticket — 50k+ rows
|
||||
// on one ticket, which broke its page (tinker_tickets#112).
|
||||
$auditedChanges = array_diff_key($changes, ['description_refreshed' => true]);
|
||||
if (!empty($auditedChanges)) {
|
||||
$auditLog->log($userId, 'update', 'ticket', $existingId, array_merge(
|
||||
$auditedChanges,
|
||||
['reason' => 'auto-updated by hwmonDaemon (condition worsened)']
|
||||
));
|
||||
}
|
||||
|
||||
// Only notify on priority escalation — title-only updates (e.g. rising
|
||||
// Power_On_Hours counter) should not generate a Matrix ping every hour.
|
||||
@@ -581,4 +588,8 @@ for ($dedupAttempt = 1; $dedupAttempt <= $maxDedupAttempts; $dedupAttempt++) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Internal server error']);
|
||||
}
|
||||
// Done — only the deadlock `continue` above may run the loop again.
|
||||
// Falling through re-entered it on a closed connection, appending a
|
||||
// 500 error body after the success response for every new ticket.
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -561,20 +561,27 @@ class AuditLogModel
|
||||
* Get formatted timeline for a specific ticket
|
||||
* Includes all ticket updates and comments
|
||||
*
|
||||
* Returns only the newest $limit events: a ticket touched by an automated
|
||||
* reporter can accumulate tens of thousands of rows, and loading them all
|
||||
* exhausted PHP memory on the ticket page (tinker_tickets#112).
|
||||
*
|
||||
* @param string $ticketId Ticket ID
|
||||
* @param int $limit Maximum number of events (newest first)
|
||||
* @return array Timeline events
|
||||
*/
|
||||
public function getTicketTimeline($ticketId)
|
||||
public function getTicketTimeline($ticketId, $limit = self::DEFAULT_LIMIT)
|
||||
{
|
||||
$limit = $this->validateLimit((int)$limit, self::EXPORT_LIMIT);
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT al.*, u.username, u.display_name
|
||||
FROM audit_log al
|
||||
LEFT JOIN users u ON al.user_id = u.user_id
|
||||
WHERE (al.entity_type = 'ticket' AND al.entity_id = ?)
|
||||
OR (al.entity_type = 'comment' AND JSON_UNQUOTE(JSON_EXTRACT(al.details, '$.ticket_id')) = ?)
|
||||
ORDER BY al.created_at DESC"
|
||||
ORDER BY al.created_at DESC, al.audit_id DESC
|
||||
LIMIT ?"
|
||||
);
|
||||
$stmt->bind_param("ss", $ticketId, $ticketId);
|
||||
$stmt->bind_param("ssi", $ticketId, $ticketId, $limit);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
|
||||
@@ -819,6 +819,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php if (!empty($timelineTruncated)) : ?>
|
||||
<div class="lt-empty lt-text-xs">Showing the <?= count($timeline) ?> most recent events; older activity is in the audit log.</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user