Commit Graph
5 Commits
Author SHA1 Message Date
jaredandClaude Opus 5.5 c490d4f387 MCP: ticket links and similar-ticket search (#113)
Lint / PHP (phpcs PSR-12) (push) Successful in 48s
Lint / JS (eslint) (push) Successful in 17s
Lint / PHP requirements (version + extensions) (push) Successful in 49s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 3m15s
Lint / Deploy (push) Successful in 4s
- get_ticket now returns `links` (blocks / blocked_by / relates_to /
  duplicates / duplicated_by, phrased from this ticket's side, limited to
  linked tickets the user can see) and `blocked` (any open blocked_by).
- find_similar_tickets (tickets:read): the possible-duplicates finder, by
  title or by an existing ticket (which is excluded from the results).
- link_tickets / unlink_tickets (tickets:write). Marking a duplicate only
  records the link. unlink also finds a link stored from the other side
  ("B blocked_by A" for "A blocks B").

api/ticket_dependencies.php's list/add/remove logic moves to
services/DependencyService.php, used by both (same checks and messages).
DependencyModel's remove methods now return rows removed, so removing a
link that is already gone no longer writes a "deleted" audit row.

Also includes the port in ToolScopeMiddleware's resource_metadata URL
(matches the 401's; no effect on prod, which has no port).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-26 17:08:04 -04:00
jaredandClaude Opus 5.5 a9bc72fdcc Fix the possible-duplicates finder; share it as SimilarTicketService (#113)
check_duplicates.php's logic moves to services/SimilarTicketService.php
(used next by the MCP find_similar_tickets tool). Fixes found while
testing it:

- Non-admins never got matches: the query had no `t` alias but the
  visibility filter's SQL uses t.*, so it failed and the error was
  swallowed into "no duplicates".
- The word-overlap scoring could never fire, since candidates were only
  whole-title substring or SOUNDEX matches. Tickets sharing a significant
  word (4+ letters) are now candidates too; overlap needs 2+ shared words
  so one common word (e.g. every automated ticket's [problem] tag) isn't
  a match.
- SOUNDEX compared PHP's 4-char code (effectively the first word) with
  MariaDB's full-length code: the SQL side almost never matched, and the
  scorer marked any two titles sharing a first word as "sounds alike".
  Both sides now use SQL SOUNDEX on the full title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-26 17:08:04 -04:00
jaredandClaude Opus 5.5 f206bb5889 Extract ticket creation from TicketController into TicketCreationService (#111)
Required-field and required-custom-field validation, createTicket
(which enforces visibility rules), the audit log, stats-cache
invalidation, custom field values, the optional 'duplicates' link and
the new-ticket notification move into services/TicketCreationService.php
with the same order and error messages, so the MCP create_ticket tool
runs one code path with the web form. TicketController::create keeps
CSRF, the redirect, and re-rendering the form on error (the view never
read the removed locals). The service also accepts visibility_groups as
a string for API callers; the controller still passes only the form's
array, so web behaviour is unchanged.

Verified the real web form over HTTP (logged in via Remote-User, real
CSRF token): a valid submit redirects to the new ticket, created and
audit-logged as the user; a blank description re-renders the form with
'Description is required' and creates nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-24 19:13:31 -04:00
jaredandClaude Opus 5.5 9e462f7f00 Extract ticket assignment from assign_ticket.php into AssignmentService (#111)
The access check, the admin/creator/current-assignee permission rule,
unassign/assign, the audit log, the optional Matrix assignment
notification and the stats-cache invalidation move into
services/AssignmentService.php for reuse by the MCP assign_ticket tool.
Error messages and status codes are unchanged.

One deliberate difference: assign_ticket.php's early error responses
(400/403/404) used a bare echo and so omitted the CSRF token that
bootstrap had just rotated. Every response now goes through
apiRespond(), which includes it. The front end already resyncs its
token from any response body, so this is compatible, and a failed
assign can no longer leave the page holding a stale token.

Verified over real HTTP: the assignee can reassign (200); a user who can
see the ticket but isn't admin/creator/assignee gets 403 'Permission
denied'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-24 19:13:31 -04:00
jaredandClaude Opus 5.5 d5832fb58a Extract comment creation from add_comment.php into CommentService (#111)
Validation, the ticket access check, reply-parent validation, @mention
extraction (audit-logged, notified only to mentioned users who can see
the ticket), and comment/watcher notifications move into
services/CommentService.php, so the MCP add_comment tool runs one code
path with the web UI. add_comment.php keeps session, CSRF, JSON parsing
and response codes. Error messages and status codes are unchanged; the
extracted body diffs against the original only where each 'emit error
and exit' became a 'return [..., http_status]'.

Verified the web endpoint over real HTTP: a comment is trimmed, saved
with its @mention and the rotated CSRF token returned; a confidential
ticket the user can't see still gets 403 'Access denied'; empty text
still gets 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-24 19:13:31 -04:00