Merge development: fix ticket page 500 (#112)
Lint / PHP (phpcs PSR-12) (push) Successful in 33s
Lint / JS (eslint) (push) Successful in 8s
Lint / PHP requirements (version + extensions) (push) Successful in 24s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m38s
Lint / Deploy (push) Successful in 5s

This commit is contained in:
2026-09-25 15:45:07 -04:00
5 changed files with 37 additions and 10 deletions
+1 -1
View File
@@ -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 [
+8 -2
View File
@@ -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
View File
@@ -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;
}
+10 -3
View File
@@ -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();
+3
View File
@@ -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>