Compare commits

..
Author SHA1 Message Date
jared 5b43e7d18f Merge development: per-warning Ceph dedup (#112)
Lint / PHP (phpcs PSR-12) (push) Successful in 18s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 21s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m46s
Lint / Deploy (push) Successful in 3s
2026-09-25 16:39:35 -04:00
jaredandClaude Opus 5.5 72feafb359 Give each Ceph HEALTH_WARN its own dedup ticket (#112)
Lint / PHP (phpcs PSR-12) (push) Successful in 51s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 39s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m36s
Lint / Deploy (push) Successful in 3s
Every HEALTH_WARN other than four known types hashed with an empty
subtype, so unrelated cluster warnings (noout, nearfull, mon space,
insecure keys, MDS standby, ...) all collapsed onto one ticket. Each run
the reported warnings overwrote its title in turn, writing ~47k
title-flip audit rows on ticket 221040637.

Unknown warnings now key on Ceph's health-check code when the reporter
sends one (optional `check_code` payload field, e.g. OSDMAP_FLAGS), else
on the warning text before its first colon with counts stripped, so
"3 pool(s) nearfull" and "4 pool(s) nearfull" stay one ticket. Known
subtypes hash exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-25 15:59:37 -04:00
jared da916bc5a6 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
2026-09-25 15:45:07 -04:00
jaredandClaude Opus 5.5 ad201691e4 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
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
2026-09-25 15:34:44 -04:00
5 changed files with 54 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();
+32 -4
View File
@@ -159,6 +159,23 @@ function generateTicketHash($data)
}
} elseif (stripos($title, 'HEALTH_ERR') !== false) {
$issueSubtype = 'health_err';
} else {
// Any other warning gets its own ticket. Previously every other
// HEALTH_WARN shared one empty subtype, so unrelated warnings
// (noout, nearfull, insecure keys, ...) collapsed onto a single
// ticket and overwrote its title in turn every run (#112).
// Prefer Ceph's own health-check code when the reporter sends
// it; otherwise key on the warning text with counts stripped.
$checkCode = isset($data['check_code']) && is_string($data['check_code']) ? $data['check_code'] : '';
if ($checkCode !== '' && preg_match('/^[A-Z0-9_]{1,64}$/', $checkCode)) {
$issueSubtype = 'check_' . strtolower($checkCode);
} elseif (preg_match('/HEALTH_WARN:\s*([^\[]+)/i', $title, $warnMatch)) {
$warnText = strtolower(explode(':', $warnMatch[1])[0]);
$warnText = trim(preg_replace('/[^a-z]+/', '_', $warnText), '_');
if ($warnText !== '') {
$issueSubtype = 'warn_' . $warnText;
}
}
}
}
@@ -328,10 +345,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 +605,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>