status_transitions already has a DB-level UNIQUE KEY on
(from_status, to_status), so a genuine duplicate pair was never
actually possible to insert — but hitting that constraint raw
surfaced as an opaque "An internal error occurred" to the admin
instead of a clear message, since manage_workflows.php only validated
from_status !== to_status before attempting the insert/update.
Added an explicit existence check before insert/update in both the
POST and PUT handlers (excluding the row's own ID on update), so the
common case — an admin re-adding or renaming into a pair that already
exists — gets a specific 409 with the conflicting pair named, instead
of a generic 500. Also added ORDER BY transition_id to
WorkflowModel::getAllTransitions() as a defense-in-depth backstop:
since it collapses rows into a PHP array keyed by
[from_status][to_status] with no defined winner otherwise, if the DB
constraint were ever weakened or bypassed, this at least makes which
row wins deterministic (most recently created).
Verified against a real running server + real MariaDB: creating a
duplicate active pair, a duplicate inactive pair, and updating a
different row into an existing pair are all correctly rejected with
the friendly message; updating a row to keep its own existing pair
succeeds; and a genuinely different pair still creates normally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
- CustomFieldModel: assign ?? fallbacks to variables before bind_param
(by-reference args cannot be ?? expressions; fatal on PHP 8.2, custom
fields were uncreatable/uneditable)
- RecurringTicketModel::create: fix swapped bind type for schedule_type
(enum bound as int coerced 'daily' to 0, breaking the cron)
- TicketModel/CommentModel: bind varchar ticket_id as string not int so
the unique index is usable and leading-zero IDs match; ticket_watchers
(int column) left as integer
- TicketModel::deleteTicket: delete from custom_field_values (real table)
not the nonexistent ticket_custom_fields
- TicketModel search: honor literal '0'; never emit AGAINST('*') on
all-special-char input (fall back to LIKE)
- TicketModel::updateTicket: disambiguate not-found vs no-op vs genuine
optimistic-lock conflict on zero affected rows
- WorkflowModel: do not cache transitions/statuses on DB failure (a
transient error no longer blocks all status changes for the TTL)
- DependencyModel: filter linked tickets by visibility (new optional user
context params) to stop confidential metadata leaking via dependencies
- BulkOperationsModel: validate status/priority/assignee before mutating
- AuditLogModel: gate getClientIP forwarded headers on trusted proxies;
add missing action/entity types so audit-log filters work
- WorkflowModel: add transitionRequiresComment() accessor for enforcement
- CommentModel: stop leaking raw DB errors to clients (log instead)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add CacheHelper for file-based caching with TTL support
- Add Database helper for centralized connection management
- Update WorkflowModel to cache status transitions (10 min TTL)
- Update UserPreferencesModel to cache user prefs (5 min TTL)
- Update manage_workflows.php to clear cache on changes
- Update get_users.php to use Database helper (example migration)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive workflow management system for ticket status transitions:
- Created WorkflowModel.php for managing status transition rules
- Updated TicketController.php to load allowed transitions for each ticket
- Modified TicketView.php to display dynamic status dropdown with only allowed transitions
- Enhanced api/update_ticket.php with server-side workflow validation
- Added updateTicketStatus() JavaScript function for client-side status changes
- Included CSS styling for status select dropdown with color-coded states
- Transitions can require comments or admin privileges
- Status changes are validated against status_transitions table
This feature enforces proper ticket workflows and prevents invalid status changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>