Optimistic locking:
- TicketView now includes updated_at in window.ticketData
- ticket.js saveTicket() sends expected_updated_at on every save so
the server can detect concurrent edits
- On conflict response, shows a clear toast: "ticket was modified by
someone else while you were editing — reload to see latest version"
- On success, syncs window.ticketData.updated_at from server response
so subsequent saves use the correct lock key
- update_ticket.php now returns updated_at in success response
Visibility audit log:
- updateVisibility() result is now checked; on success, logs a delta
entry to the audit trail with from/to visibility and groups so the
timeline shows who changed visibility and when
Full ticket export:
- export_tickets.php now accepts format=full with a single ticket_id
- Produces a JSON file containing ticket fields, flat comment list
(with author, timestamps, text), and the full audit timeline
- Access-controlled: respects canUserAccessTicket() before exporting
- EXPORT button added to ticket toolbar linking directly to the endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /ticket_dependencies: verify user can access both the source
ticket and the target ticket before creating a dependency
- DELETE by ticket IDs: verify user can access source ticket; also
validate dependency_type against the allowed whitelist
- DELETE by dependency_id: look up dependency's ticket before deletion
and verify user can access it, preventing IDOR
- custom_fields.php: validate json_decode returns an array on POST/PUT;
add http_response_code(400) to all error responses
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TicketModel::getAllTickets() now accepts optional $user param and applies
getVisibilityFilter() so non-admin users cannot see internal/confidential
tickets they lack access to from the dashboard listing
- DashboardController passes $GLOBALS['currentUser'] to getAllTickets()
- clone_ticket.php: move Content-Type header to top so all error paths send
correct JSON content type
- AuthMiddleware: filter group names from HTTP header to [a-z0-9_-] only,
preventing header injection via malformed group names
- add_comment.php: return HTTP 201 on success, 500 in catch block
- update_comment.php, delete_comment.php: return 500 in catch blocks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- upload_attachment.php: derive stored file extension from validated MIME type
instead of user-supplied filename, preventing executable extension attacks
(e.g. a PHP file renamed to evil.txt would now be stored as .txt)
- CustomFieldModel.php: fix bind_param type string in updateDefinition()
'sssssiiiii' (10 chars) → 'sssssiiii' (9 chars) to match 9 SQL placeholders
- RateLimitMiddleware.php: replace MD5 with SHA256 for rate limit file hashing
- user_preferences.php: add httponly, secure, samesite=Lax flags to ticketsPerPage
cookie to prevent XSS/CSRF cookie theft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
after successful validation and stores in $GLOBALS['_new_csrf_token']; add
apiRespond() helper to append token to responses; lt.api interceptor in
layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
'?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bulk_operation.php: replace is_numeric() with strict int cast+equality to reject scientific notation
- AttachmentModel.php: fix bind_param type strings (s→i for integer ticket IDs)
- CommentModel.php: use strict !== comparison with (int) cast for user_id ownership checks
- ticket.js: replace all non-TDS class names (text-amber→lt-text-amber, btn→lt-btn variants, etc.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- delete_attachment.php: add realpath() path traversal check before
unlink() — mirrors the defense-in-depth already in download_attachment.php;
also cast ticket_id to int when building the path
- manage_templates.php: add input validation to POST and PUT handlers:
required field checks, max length caps (name 100, title 255, desc 64KB),
allowlist validation for category/type, priority clamped to 1-5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before: entire ticket data was logged and shown in the activity tab.
After: compare old vs new values before saving; log only fields that
actually changed as { field: { from: '...', to: '...' } } pairs.
- TicketController.php: fetch old ticket before update, compute delta
- api/update_ticket.php: same fix for the API endpoint (currentTicket
already fetched for auth, reuse it for delta comparison)
- TicketView.php: render delta format as "Field: old → new" with color;
truncate long values (description) at 60 chars; keep legacy flat format
as fallback for older log entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base.css: add --lt-border/--lt-surface aliases so dashboard.css respects
theme instead of using hardcoded fallback colors
- base.css: add lt-select-sm/lt-input-sm compact size variants (used in 15+
places), lt-msg-danger alias for lt-msg-error, lt-form-hint--warn,
lt-font-mono utility class
- audit_log.php: cap ?limit= at 500 to prevent DoS via oversized queries
- ApiKeysView.php: replace deprecated execCommand('copy') with lt.copy();
add integer casts on api_key_id in id attr and data-id
- AuditLogView.php: rebuild pagination with windowed prev/next/ellipsis
pattern matching DashboardView; integer cast on user_id select option
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- clone_ticket.php: replace custom visibility check with centralized canUserAccessTicket(); return 404 (not 403) for inaccessible tickets
- README.md: remove bootstrap.php from the API endpoints table (it's a shared include, not a public endpoint); correct its project structure description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- delete_attachment.php: check canUserAccessTicket() before allowing deletion; return 404 (not 403) for inaccessible tickets to prevent existence leakage
- upload_attachment.php: verify ticket access on both GET (list) and POST (upload) before processing
- update_ticket.php: pass currentUser to controller; add canUserAccessTicket() check before permission check; return 404 for inaccessible tickets instead of leaking existence via 403
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security fixes:
- add_comment.php: verify canUserAccessTicket() before allowing comment creation
- assign_ticket.php: use canUserAccessTicket() to prevent info leakage via 403 vs 404
- check_duplicates.php: apply getVisibilityFilter() so confidential ticket titles are not exposed in duplicate search results
- ticket_dependencies.php: verify ticket access on GET before returning dependency data
Route registration:
- Register 7 previously missing API endpoints in index.php: custom_fields, saved_filters, audit_log, user_preferences, download_attachment, clone_ticket, health
Frontend:
- ticket.js: fill empty catch block and empty else block in addComment() with proper error toasts
Documentation:
- README.md: document all API endpoints and update project structure listing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS fixes:
- Fix [ ] brackets appearing below button text by replacing display:inline-flex
with display:inline-block + white-space:nowrap on .btn — removes cross-browser
flex pseudo-element inconsistency as root cause
- Remove conflicting .btn::before ripple block (position:absolute was overriding
bracket content positioning)
- Remove overflow:hidden from .btn which was clipping bracket content
- Fix body::after duplicate rule causing GPU layer blink (second position:fixed
rule re-created compositor layer, overriding display:none suppression)
- Replace all transition:all with scoped property transitions in dashboard.css,
ticket.css, base.css (prevents full CSS property evaluation on every hover)
- Convert pulse-warning/pulse-critical keyframes from box-shadow to opacity
animation (GPU-composited, eliminates CPU repaints at 60fps)
- Fix mobile *::before/*::after blanket content:none rule — now targets only
decorative frame glyphs, preserving button brackets and status indicators
- Remove --terminal-green-dim override that broke .lt-btn hover backgrounds
JS fixes:
- Fix all lt.lt.toast.* double-prefix instances in dashboard.js
- Add null guard before .appendChild() on bulkAssignUser select
- Replace all remaining emoji with terminal bracket notation (dashboard.js,
ticket.js, markdown.js)
- Migrate all toast.*() shim calls to lt.toast.* across all JS files
View fixes:
- Remove hardcoded [ ] brackets from .btn buttons (CSS now adds them)
- Replace all emoji with terminal bracket notation in all views and admin views
- Add missing CSP nonces to AuditLogView.php and UserActivityView.php script tags
- Bump CSS version strings to ?v=20260319b for cache busting
Security fixes:
- update_ticket.php: add authorization check (non-admins can only edit their own
or assigned tickets)
- add_comment.php: validate and cast ticket_id to integer with 400 response
- clone_ticket.php: fix unconditional session_start(), add ticket ID validation,
add internal ticket access check
- bulk_operation.php: add HTTP 401/403 status codes on auth failures
- upload_attachment.php: fix missing $conn arg in AttachmentModel constructor
- assign_ticket.php: add ticket existence check and permission verification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security:
- Fix IDOR in delete/update comment (add ticket visibility check)
- XSS defense-in-depth in DashboardView active filters
- Replace innerHTML with DOM construction in toast.js
- Remove redundant real_escape_string in check_duplicates
- Add rate limiting to get_template, download_attachment, audit_log,
saved_filters, user_preferences endpoints
Bug fixes:
- Session timeout now reads from config instead of hardcoded 18000
- TicketController uses $GLOBALS['config'] instead of duplicate .env parsing
- Add DISCORD_WEBHOOK_URL to centralized config
- Cleanup script uses hashmap for O(1) ticket ID lookups
Dead code removal (~100 lines):
- Remove dead getTicketComments() from TicketModel (wrong bind_param type)
- Remove dead getCategories()/getTypes() from DashboardController
- Remove ~80 lines dead Discord webhook code from update_ticket API
Consolidation:
- Create api/bootstrap.php for shared API setup (auth, CSRF, rate limit)
- Convert 6 API endpoints to use bootstrap
- Extract escapeHtml/getTicketIdFromUrl into shared utils.js
- Batch save for user preferences (1 request instead of 7)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keyboard Navigation:
- Add J/K keys for Gmail-style ticket list navigation
- Add N key for new ticket, C for comment focus
- Add G then D for go to dashboard (vim-style)
- Add 1-4 number keys for quick status changes on ticket page
- Add Enter to open selected ticket
- Update keyboard help modal with all new shortcuts
Ticket Age Indicator:
- Show "Last activity: X days ago" on ticket view
- Visual warning (yellow pulse) for tickets idle >5 days
- Critical warning (red pulse) for tickets idle >10 days
Ticket Clone Feature:
- Add "Clone" button on ticket view
- Creates copy with [CLONE] prefix in title
- Preserves description, priority, category, type, visibility
- Automatically creates "relates_to" dependency to original
Active Filter Badges:
- Show visual badges above ticket table for active filters
- Click X on badge to remove individual filter
- "Clear All" button to reset all filters
- Color-coded by filter type (status, priority, search)
Visual Enhancements:
- Add keyboard-selected row highlighting for J/K navigation
- Smooth animations for filter badges
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace exception getMessage() exposure with generic error messages
to prevent internal information disclosure. Errors are now logged
with full details while clients receive sanitized responses.
Affected endpoints:
- add_comment, update_comment, delete_comment
- update_ticket, export_tickets
- generate_api_key, revoke_api_key
- manage_templates, manage_workflows, manage_recurring
- custom_fields, get_users
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Consolidate all 20 API files to use centralized Database helper
- Add optimistic locking to ticket updates to prevent concurrent conflicts
- Add caching to StatsModel (60s TTL) for dashboard performance
- Add health check endpoint (api/health.php) for monitoring
- Improve rate limit cleanup with cron script and efficient DirectoryIterator
- Enable rate limit response headers (X-RateLimit-*)
- Add audit logging for workflow transitions
- Log Discord webhook failures instead of silencing
- Fix visibility check on export_tickets.php
- Add database migration system with performance indexes
- Fix cron recurring tickets to use assignTicket method
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ErrorHandler class for consistent error handling and logging
- Provides methods for common error responses (401, 403, 404, 422, 500)
- Includes error logging to temp directory
- Update get_template.php to use ErrorHandler (example migration)
Co-Authored-By: Claude Opus 4.5 <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>
Template fixes:
- Fixed column name mismatch: use 'default_priority' instead of 'priority'
- Updated manage_templates.php API INSERT and UPDATE queries
- Updated TemplatesView.php to use correct field name in PHP and JS
CSS improvements for .setting-row:
- Better flexbox layout with flex-wrap for responsiveness
- Proper styling for inputs, selects, and textareas in setting rows
- Labels now align to top (better for textareas)
- Added focus states with amber glow effect
- Improved checkbox styling within setting rows
- Better mobile responsive behavior (stacked layout)
- Updated cache version to 20260126a
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add comment edit/delete functionality (owner or admin can modify)
- Add edit/delete buttons to comments in TicketView
- Create update_comment.php and delete_comment.php API endpoints
- Add updateComment() and deleteComment() methods to CommentModel
- Show "(edited)" indicator on modified comments
- Add migration script for updated_at column
- Auto-link URLs in plain text comments (non-markdown)
- Add markdown table support with proper HTML rendering
- Preserve code blocks during markdown parsing
- Fix mobile UI elements showing on desktop (add display:none defaults)
- Add mobile styles for CreateTicketView form elements
- Stack status-priority-row on mobile devices
- Update cache busters to v20260124e
- Update Claude.md and README.md documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bug fixes:
- Fix ticket ID extraction using URLSearchParams instead of split()
- Add error handling for query result in get_users.php
- Make Discord webhook URLs dynamic (use HTTP_HOST)
Code cleanup:
- Remove debug console.log statements from dashboard.js and ticket.js
- Add getTicketIdFromUrl() helper function to both JS files
Documentation:
- Update Claude.md: fix web server (nginx not Apache), add new notes
- Update README.md: add keyboard shortcuts, update setup instructions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add admin dropdown menu in dashboard header with links to all admin pages
- Fix template modal: larger size (800px), responsive grid, type/priority dropdowns
- Fix recurring tickets modal: add Type and Assign To fields, larger size
- Make dashboard stat cards clickable for quick filtering
- Fix user-activity query (remove is_active requirement)
- Add table existence check in ticket_dependencies API
- Fix table overflow on dashboard
- Update Claude.md and README.md with current project status
- Remove migrations directory (all migrations completed)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Completely rewrote README with all new features and admin routes
- Cleaned up remaining migration files
- Added detailed PHP error/exception handlers to dependencies API
to help debug the 500 error
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add detailed error handling in DependencyModel (throw exceptions on failure)
- Add try-catch in ticket_dependencies.php to catch query errors
- Remove all old migrations (001-014) that have already been run
- Keep only new feature migrations (015-018) for reference
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix AuditLogModel instantiation with proper $conn parameter
- Fix log() call parameter order (details should be array, not ipAddress)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add session status check
- Remove broken AuditLogModel call without $conn in CSRF check
- Fix AuditLogModel instantiation with proper $conn parameter
- Fix log() call to pass array instead of JSON string for details
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add session status check before starting session
- Add error reporting settings for debugging
- Prevents potential session conflicts with RateLimitMiddleware
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The AuditLogModel was being instantiated without required $conn parameter
when logging CSRF failures, causing a 500 error.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove is_active filter from get_users.php (column doesn't exist)
- Fix ticket ID validation regex in upload_attachment.php (9-digit format)
- Fix createSettingsModal reference to use openSettingsModal from settings.js
- Add error handling for dependencies tab to prevent infinite loading
- Add try-catch wrapper to ticket_dependencies.php API
- Make export dropdown visible only when tickets are selected
- Export only selected tickets instead of all filtered tickets
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CSRF validation to user_preferences.php
- Protects POST and DELETE methods
- Completes CSRF protection for all API endpoints
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add CSRF validation to assign_ticket.php
- Add CSRF validation to saved_filters.php
- Supports POST, PUT, and DELETE methods
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed multiple critical issues reported by user:
1. **API Configuration Errors:**
- Fixed all API files to use correct config path (config/config.php instead of config/db.php)
- Fixed: get_users.php (bulk assign dropdown now loads users)
- Fixed: get_template.php (templates now load correctly)
- Fixed: bulk_operation.php (bulk operations now work)
- Fixed: assign_ticket.php (manual assignment now works)
2. **Dark Mode Improvements:**
- Added dark mode support for Activity tab content
- Ensured proper text and background colors in dark mode
All APIs now properly:
- Load configuration from config/config.php
- Use correct session variables ($_SESSION['user']['user_id'])
- Create and close database connections properly
- Return proper JSON responses
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed critical bug in assign_ticket.php that was causing JSON parsing errors:
- Fixed authentication check to use correct session variable ($_SESSION['user']['user_id'])
- Added missing database connection initialization
- Added proper connection cleanup (close)
- Updated all references to use correct session variable
- Changed require paths to use dirname(__DIR__) for consistency
This resolves the "Failed to execute 'json' on 'Response': Unexpected end of JSON input" error that occurred when assigning tickets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add comprehensive bulk operations system for admins:
- Created BulkOperationsModel.php with operation tracking and processing
- Added bulk_operation.php API endpoint for bulk operations
- Created get_users.php API endpoint for user dropdown in bulk assign
- Updated DashboardView.php with checkboxes and bulk actions toolbar
- Added JavaScript functions for:
- Select all/clear selection
- Bulk close tickets
- Bulk assign tickets
- Bulk change priority
- Added comprehensive CSS for bulk actions toolbar and modals
- All bulk operations are admin-only (enforced server-side)
- Operations tracked in bulk_operations table with audit logging
- Supports bulk_close, bulk_assign, and bulk_priority operations
Admins can now select multiple tickets and perform batch operations, significantly improving workflow efficiency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>