Log watch/unwatch actions to the audit trail (#93)

api/watch_ticket.php performed the ticket_watchers INSERT IGNORE/DELETE
directly with no AuditLogModel call, unlike every other ticket-adjacent
mutation (comments, attachments, dependencies, status/field changes),
so watching/unwatching never showed up in a ticket's timeline.

Added AuditLogModel::log() calls to both the watch and unwatch paths,
gated on the DB statement's affected_rows so a no-op (already watching,
already not watching) doesn't produce a duplicate timeline entry. Added
'watch'/'unwatch' to AuditLogModel's VALID_ACTION_TYPES, and timeline
rendering in views/TicketView.php ("started watching this ticket" /
"stopped watching this ticket").

Verified against real MariaDB: watch -> unwatch -> watch again produces
exactly 2 timeline entries (not 4) since the two no-op repeats correctly
produced zero rows changed and were not logged; confirmed formatAction()/
getEventIcon() render both action types correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117oBw2jN4kALYeS8HPq4zV
This commit is contained in:
2026-09-12 01:17:13 -04:00
co-authored by Claude Sonnet 5
parent 74544ac5b4
commit e91f4547b6
3 changed files with 18 additions and 1 deletions
+6
View File
@@ -32,6 +32,8 @@ function getEventIcon(string $actionType): string
'status_change' => '[!]',
'attachment' => '[^]',
'delete' => '[x]',
'watch' => '[o]',
'unwatch' => '[o]',
default => '[*]',
};
}
@@ -53,6 +55,10 @@ function formatAction(array $event): string
return 'uploaded a file';
case 'delete':
return 'deleted a comment';
case 'watch':
return 'started watching this ticket';
case 'unwatch':
return 'stopped watching this ticket';
case 'assign':
if (is_array($det) && isset($det['assigned_to']['to'])) {
$to = $det['assigned_to']['to'] ?: 'Unassigned';