Commit Graph

82 Commits

Author SHA1 Message Date
jared 60f23051a9 Fix notifications not detecting comment events
AuditLogModel::logCommentCreate logs comments with action_type='comment'
not 'create'. The notification query was filtering on action_type='create'
only, so comment events on watched/owned tickets were never surfaced.

Widen the filter to IN ('comment', 'create') to match the actual logged
values while staying compatible with any legacy entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 13:45:04 -04:00
jared 47b70b0ee8 Fix ticket ID handling in assign and delete_attachment APIs
assign_ticket.php: preserve string ticket ID (ctype_digit validation)
  instead of (int) cast for consistent audit logging and URL generation.

delete_attachment.php: use string ticket_id from DB for the upload
  directory path — (int) cast was stripping leading zeros, causing
  the wrong path (/uploads/123456/) instead of /uploads/000123456/.
  Also pass raw string to getTicketById() to let TicketModel handle
  type coercion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 13:31:10 -04:00
jared d0c889a594 Fix leading-zero ticket ID in clone_ticket.php
- Preserve source ticket ID as string (ctype_digit validation)
  instead of int-casting with (int)
- Use $sourceTicket['ticket_id'] (canonical DB form) when creating
  the relates_to dependency and audit log entry; avoids storing
  "123456" instead of "000123456" which breaks string-based
  depends_on_id lookups in DependencyModel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 13:02:49 -04:00
jared ab0edd1325 Implement bulk_delete operation; validate operation types
- TicketModel: add deleteTicket() that removes all child records
  (comments, watchers, dependencies, attachments, custom fields)
  then deletes the ticket and cleans up physical attachment files
- BulkOperationsModel: add bulk_delete case to processBulkOperation()
  so the "Bulk Delete" UI button actually works instead of silently
  failing with N failures
- bulk_operation.php: validate operation_type against whitelist to
  reject unknown operations early with a proper error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 12:53:53 -04:00
jared d295d64f85 Fix leading-zero ticket ID handling across API and UI
- dashboard.js: use String(cb.value) instead of parseInt() in
  getSelectedTicketIds() so zero-padded IDs like 000123456 are
  preserved when sent to bulk_operation.php
- DashboardView.php: remove (int) cast on data-ticket-id attribute
  for quick-status button; was stripping leading zeros
- TicketView.php: remove (int) cast on export URL ticket_id param
- update_ticket.php: preserve ticket_id as string via trim((string)...)
- add_comment.php: preserve ticket_id as string; validate with
  ctype_digit instead of (int) cast so comments are stored with the
  canonical zero-padded ID matching the tickets table
- export_tickets.php: validate singleId as string to avoid stripping
  leading zeros in the export endpoint
- notifications.php: preserve ticket_id strings in URLs and ticket
  ownership checks; index myTicketIds by both int and string forms
  for robust lookup regardless of how audit_log stored the ID
- TicketController.php: fix inline dependency insert — column was
  wrong (depends_on_ticket_id → depends_on_id) and bind types were
  wrong ("iii" → "ssi"); feature was silently broken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 12:43:18 -04:00
jared d6603d07f2 Fix bulk operation dropping tickets with leading-zero IDs, add query null-check
bulk_operation.php: ticket ID validation was converting IDs to int then back
to string, so '000123456' became '123456' which never matched the DB VARCHAR
key, silently rejecting ~11% of tickets from bulk operations. Now validates
with ctype_digit() to preserve leading zeros.

TicketModel::getTicketsByIds(): changed intval() to strval() and bind type
'i' to 's' so VARCHAR ticket_id columns are queried consistently as strings.

DashboardController::getCategoriesAndTypes(): added null check on query
result before calling fetch_assoc() to prevent TypeError if query fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:29:37 -04:00
jared 55a3d2945c Fix comment avatar, activity log labels, and ticket update permissions
- add_comment.php: include user_id in response for avatar rendering
- ticket.js: add buildCommentElement() helper that matches server-rendered
  comment structure (avatar, edit/delete buttons, textarea); use it in
  addComment() and submitReply() so new comments show the avatar immediately
- AuditLogModel: logCommentCreate uses action_type='comment' not 'create'
- TicketView: formatAction handles entity_type='comment' with action_type='create'
  for existing DB records; prevents "created this ticket" showing for comments
- update_ticket.php: remove owner/assignee restriction so any authenticated
  team member can update ticket status and fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 22:37:53 -04:00
jared 6e1ae01cac Fix recurring ticket schedule edge cases in API and model
- manage_recurring.php calculateNextRun(): expand monthly cap from 28→31
  with proper last-day-of-month clamping (matches model fix); use split
  with ':00' append to handle malformed time strings without crashing;
  fix weekly day array to start at index 1 (not 0) so day=0 never maps
  to empty string and blows up DateTime
- RecurringTicketModel::calculateNextRunTime(): same weekly day array fix
  (start at index 1) to eliminate '' → DateTime exception on day=0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:16:41 -04:00
jared 538baadd57 Add comment skeleton loaders, workflow validation, monthly schedule fix
- TicketView.php: Show 3 lt-skeleton-card placeholders in the comment list
  while "Load more" fetches; skeletons are removed on resolve or error
- ticket.css: Add .comment-skeleton margin spacing
- WorkflowDesignerView.php + manage_workflows.php: Prevent creating/editing
  status transitions where from_status === to_status (client + server check)
- RecurringTicketsView.php: Expand monthly day picker from 28 to 31 days
  (days 29-31 labelled "last day in short months")
- RecurringTicketModel.php: Clamp monthly schedule day to last day of target
  month using format('t') instead of hard-capping at 28

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:09:53 -04:00
jared fbda618fbb Fix path traversal, closed-connection, and ticket ID validation bugs
- download_attachment.php: path traversal check used strpos() without
  trailing DIRECTORY_SEPARATOR, allowing /uploads_evil/* to pass when
  upload dir is /uploads — now checks realPath + DIRECTORY_SEPARATOR prefix
- bulk_operation.php: $conn->close() was called before StatsModel($conn)
  construction; moved close() inside each branch to after all DB use
- upload_attachment.php: ticket ID validated as /^\d{9}$/ (exactly 9
  digits) breaking all tickets below ID 1,000,000,000 — changed to
  /^\d+$/ for any positive integer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 17:57:36 -04:00
jared 01f2dac2d6 Fix session_start guards, add missing API routes, rewrite README
- Added session_status() === PHP_SESSION_NONE guard to six API files
  (custom_fields, revoke_api_key, manage_templates, generate_api_key,
  get_template, manage_recurring) that called bare session_start() after
  RateLimitMiddleware had already started the session
- Registered /api/notifications.php and /api/user_avatar.php in index.php
  router (were missing, served only by direct file access)
- Complete README rewrite: remove all Discord references (Matrix/hookshot
  is the only external notification method), add hwmonDaemon API docs,
  document all TDS v1.2 features (kanban, charts, SLA, command palette,
  notification bell, watcher avatars, @mention, etc.), fix keyboard
  shortcuts table, add Matrix/LDAP env vars to setup section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 17:52:07 -04:00
jared 4433bad2ce Fix manage_workflows bind_param by-reference errors and duplicate session_start
- Extract expression args to local variables before bind_param (PHP 8 requirement)
- Guard session_start with session_status check in manage_workflows
- Remove redundant session_start from bulk_operation (RateLimitMiddleware starts it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 17:27:02 -04:00
jared 1761f41943 Invalidate stats cache after any ticket-modifying API call
StatsModel::invalidateCache() was never called from any API, so the
60s cached stats persisted after bulk assign/status/priority changes,
ticket updates, assignments, and clones. Dashboard tiles showed stale
counts until the TTL expired.

Added invalidation to the four APIs that affect dashboard stat tiles:
- bulk_operation.php: after successful bulk assign/status/priority
- assign_ticket.php: after successful reassignment
- update_ticket.php: after any successful ticket update
- clone_ticket.php: after successful clone (open_tickets changes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 12:39:24 -04:00
jared bc88ba3612 Fix notifications 500 (audit_id column), smart resolution time units
- notifications.php: audit_log PK is audit_id not log_id; alias all
  three queries with audit_id AS log_id to fix 500 error
- DashboardView: avg resolution time now picks best unit automatically
  (min < 1h, hr < 48h, days < 14d, wks otherwise) with full hours
  shown in title tooltip; adds lt-stat-unit CSS for the suffix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 11:32:02 -04:00
jared 5e04478586 Fix parse error in notifications.php: escape inner quotes in LIKE string
The $statusSql double-quoted string contained '%"status":%' which caused
PHP to terminate the string at the inner double quotes, resulting in a
parse error (unexpected identifier 'status') on the beta server.

Also cleared stale stats cache that stored by_assignee in old name=>count
map format instead of the current array-of-objects format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 11:25:26 -04:00
jared 9494df2bf9 Add timezone and notif_last_seen to user_preferences valid keys whitelist
Both keys were silently dropped on batch save (the for-loop just
continued on unknown keys). timezone is sent by saveSettings() and
notif_last_seen is written by the notifications mark-read endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 11:01:38 -04:00
jared ac05b212b2 Fix performAdvancedSearch ReferenceError, settings save, sort reset, notifications 500, CSP
DashboardView.php: wrap performAdvancedSearch in a closure so it is
resolved at event-fire time rather than listener-registration time
(advanced-search.js loads later via pageScripts so the bare identifier
reference caused ReferenceError).

DashboardView.php: reset sort URL to page=1 so sorting all pages
instead of staying on the current page.

dashboard.js: add missing save-settings and close-settings cases to
the click delegation handler (were removed in a prior session under
the assumption they were in dashboard.js, but they were not).

notifications.php: replace JSON_EXTRACT-based comment join (not
universally supported) with a two-step PHP filter: fetch owner/watcher
ticket IDs first, then filter raw comment rows in PHP. Also fix the
status change LIKE pattern to match the actual logTicketUpdate format
{"status": {"from": ..., "to": ...}}.

SecurityHeadersMiddleware.php: add https://cdn.jsdelivr.net to
connect-src so Chart.js source maps load without CSP violations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 10:53:06 -04:00
jared df6c4de196 Fix notification comment query, status title, and is-hidden visibility
notifications.php: comment notifications never fired because the query
used action_type='comment'/entity_type='ticket' but logCommentCreate
logs action_type='create'/entity_type='comment'. Fix query to match
actual log format and extract ticket_id from details JSON.

notifications.php: status change notification titles always showed
"? → ?" because code read details.old_value/new_value but logTicketUpdate
stores the delta as {"status": {"from": ..., "to": ...}}.

base.css: move .is-hidden to base.css (global) — it was only defined in
ticket.css, so on the dashboard the ticket-preview popup had no hide
rule applied and was visible in the DOM at all times.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 10:47:39 -04:00
jared 84b104a501 fix: various inline style cleanup, a11y improvements, and bind_param bug
- Replace style="text-align:center" with .lt-text-center utility class in
  WorkflowDesignerView, CustomFieldsView, error_403, error_404, DashboardView JS string
- Replace style="margin-top:..." with .lt-mt-sm utility in WorkflowDesignerView
- Switch comment-edit-raw data-store textareas to .is-hidden class (TicketView PHP
  + JS-rendered; ticket.js template literal) — these are never shown, only read via .value
- Add aria-describedby="visibilityGroupsHint" + id on hint <p> in CreateTicketView
- Fix bind_param type string bug in manage_workflows.php PUT handler: 'ssiiiii' → 'ssiiii'
  (7 type chars for 6 params caused binding error on workflow transition updates)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:29:52 -04:00
jared 3c29c6ee6f feat: SLA live timer, notification bell, lt-toggle MD, right drawer, kanban drag-drop
- TicketView: SLA banner now shows live HH:MM:SS elapsed + countdown via JS setInterval
  (previously showed static hours from PHP)
- TicketView: Markdown toggles in comment form replaced with lt-toggle switches
- layout_header: In-app notification bell (🔔) with dropdown panel for all users
- layout_footer: Notification JS — polls /api/notifications.php every 60s, badge count,
  mark-all-read, panel open/close with Escape/outside-click
- api/notifications.php (new): Returns assign/comment/status-change events from audit_log
  for current user's tickets and watched tickets; mark-read via user_preferences
- DashboardView: Ticket preview right drawer — Ctrl+click title or ⊙ peek button
  opens lt-drawer-right with ticket summary extracted from table row DOM
- DashboardView: lt.sortable wired on all 4 kanban columns (group='kanban')
  Cross-column drag = status change via POST /api/update_ticket.php with optimistic UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 17:21:21 -04:00
jared fca4896e0d fix: watcher avatars, dependency TDS styling, asset versions, nav dropdown light theme
- watch_ticket.php GET now returns watcher list (up to 6 users) for avatar group
- TicketView: watcher avatar group rendered next to WATCH button, refreshes on toggle
- Rewrite renderDependencies/renderDependents to use TDS lt-kv-grid/lt-badge/lt-btn classes
- renderDependencies: show lt-alert--warning blocker banner when blocked_by has open tickets
- Fix ALL hardcoded ?v=20260327 asset version strings in CreateTicketView + all admin views
- base.css: fix .lt-nav-dropdown-menu hardcoded background → var(--bg-overlay)
- base.css: add light-theme overrides for nav dropdown menu (background, links, hover)
- ticket.css: add .lt-avatar-group and .lt-avatar--overflow styles for watcher display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:02:30 -04:00
jared cfb88d9c88 fix: CSRF token staleness causing intermittent 403 on POST actions
Root cause: bootstrap.php rotates the CSRF token on every successful POST,
but most API endpoints called echo json_encode() directly instead of
apiRespond() — so the rotated token was never returned to the client.
The next POST from the same page sent the now-invalid old token → 403.
Refreshing the page loaded a fresh token, making it work once.

Fixes:
- assign_ticket.php, watch_ticket.php: switch to apiRespond()
- saved_filters.php, user_preferences.php: replace all echo json_encode
  calls with apiRespond() (19 and 12 call sites respectively)
- base.js: both apiFetch() and _apiFetchAuth() now update window.CSRF_TOKEN
  whenever a response includes a csrf_token field, keeping the client
  permanently in sync with server-side rotations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 19:01:18 -04:00
jared ade1a70214 feat: ticket watchers, fulltext search, single-query pagination, watcher notifications
Ticket watchers:
- api/watch_ticket.php: GET (watch state) + POST (watch/unwatch toggle)
- index.php: route for /api/watch_ticket.php
- TicketView: WATCH/UNWATCH button with live state fetch and toggle
- NotificationHelper::notifyWatchers(): fetches watchers from DB, resolves
  Matrix IDs via Synapse, fires notification to watchers + global list
- add_comment.php, update_ticket.php: call notifyWatchers on comment and
  status-change events respectively

Fulltext search:
- TicketModel::hasFulltextIndex(): detects FULLTEXT index via information_schema
- getAllTickets(): uses MATCH...AGAINST when fulltext index exists, LIKE fallback
  when not yet applied — zero-downtime rollout

Single-query pagination:
- getAllTickets() replaces separate COUNT + SELECT with COUNT(*) OVER() window
  function — one round trip to DB per page load instead of two

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 22:00:32 -04:00
jared c8181e8076 feat: comment pagination, Matrix integration, Synapse mention resolution
Comment pagination:
- CommentModel: add getCommentCount(), paginated getCommentsByTicketId()
  with getThreadedCommentsPaged() for threading + LIMIT/OFFSET
- TicketController: load first 50 root comments + total count on page load
- api/get_comments.php: new AJAX endpoint for Load More (index.php routed)
- TicketView: Load More button + buildCommentEl() JS renderer for AJAX comments;
  passes totalComments/commentOffset/isAdmin to window.ticketData

Matrix integration:
- NotificationHelper: add sendStatusChangeNotification(), sendCommentNotification(),
  sendMentionNotification(), sendAssignmentNotification() alongside existing
  sendTicketNotification(); internal fire() helper replaces duplicated cURL logic
- SynapseHelper: new helper that resolves SSO usernames → Matrix IDs by querying
  Synapse Admin REST API directly (no caching, no stale data)
- config.php: add SYNAPSE_ADMIN_URL, SYNAPSE_ADMIN_TOKEN, MATRIX_NOTIFY_COMMENTS,
  MATRIX_NOTIFY_ASSIGNMENTS config keys (all from .env)
- api/update_ticket.php: fire status-change notification after successful save
- api/add_comment.php: resolve @mentioned usernames via SynapseHelper and fire
  mention notification; fire general comment notification when MATRIX_NOTIFY_COMMENTS=1
- api/assign_ticket.php: fire assignment notification (resolves assignee via Synapse)
  when MATRIX_NOTIFY_ASSIGNMENTS=1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:34:16 -04:00
jared cc3f667d4c Wire optimistic locking, visibility audit log, full ticket export
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>
2026-03-29 21:19:01 -04:00
jared f709e98bd3 Security: add authorization checks to ticket_dependencies API
- 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>
2026-03-29 18:26:01 -04:00
jared e6b6a2a88c Security/correctness: visibility filtering, Content-Type headers, group validation
- 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>
2026-03-29 18:23:16 -04:00
jared f983269f93 Fix file upload security, bind_param mismatch, and cookie flags
- 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>
2026-03-29 18:14:18 -04:00
jared 7be283423a Fix loose comparisons, missing response codes, and session handling
- ticket.js: escape dependency_id with lt.escHtml() in data attribute
- assign_ticket.php: strict (int) cast for ticket_id (> 0 check), authorization
  comparisons, and add missing http_response_code(400) on invalid user ID
- TicketView.php: strict (int) cast for priority select, assigned_to select,
  and comment ownership check
- CommentModel.php: strict (int) cast for parent_comment_id thread comparison
- UserModel.php: strict (int) cast for is_admin check
- export_tickets.php: conditional session_start() to avoid double-start warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 17:39:46 -04:00
jared 2e450dc01d Apply web_template gap analysis improvements (P1-P3)
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>
2026-03-29 17:02:40 -04:00
jared d33f761a55 Fix loose comparisons in authorization checks
- TicketModel.php: fix bind_param "sssi"→"issi" for ticketId in addComment()
- TicketModel.php: use strict (int) cast === for confidential ticket access check
- update_ticket.php: use strict (int) cast !== for creator/assignee auth check
- AttachmentModel.php: use strict (int) cast === for upload ownership check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:35:48 -04:00
jared cfbef029cb Fix bind_param type mismatches and integer validation
- TemplateModel.php: fix bind_param "ssssiii" -> "sssssii" (5 strings not 4)
- manage_workflows.php: fix bind_param 'ssiiii' -> 'ssiiiii' (4 int columns)
- download_attachment.php, delete_attachment.php, get_template.php: replace is_numeric()
  with strict int cast+equality check to reject floats and scientific notation
- manage_recurring.php: validate JSON input before accessing schedule_type key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:33:48 -04:00
jared 5242d42fa7 Fix type safety and TDS class naming issues
- 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>
2026-03-28 22:29:28 -04:00
jared b40c404828 fix: ldap_get_entries returns raw binary, remove incorrect base64 decode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 20:53:49 -04:00
jared 18bf1fde0e feat: LDAP avatar support via lldap
- Create tinker-tickets service account in lldap (lldap_strict_readonly)
- Add /api/user_avatar.php: binds to lldap, fetches avatar attribute,
  caches JPEG to uploads/avatars/, returns 404 sentinel for missing photos
- Install php8.2-ldap on LXC 132 (beta) and LXC coding server
- Update layout_header.php: show lt-avatar with photo overlay + initials fallback
- Update TicketView.php: comment avatars use photo overlay pattern
- Add .lt-avatar-img / .lt-avatar-initials CSS for photo-over-initials layout
- Add LDAP_* config keys to config.php and .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 20:47:08 -04:00
jared 82aa4bf5de Harden attachment deletion and template CRUD validation
- 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>
2026-03-28 13:41:22 -04:00
jared 170bd86aa6 Show only changed fields (delta) in ticket activity timeline
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>
2026-03-28 13:35:01 -04:00
jared b42597c927 Fix CSS variables, missing utility classes, API hardening, and audit log UX
- 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>
2026-03-28 13:22:12 -04:00
jared a403e49537 Use canUserAccessTicket() in clone_ticket.php; fix README bootstrap entry
- 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>
2026-03-20 21:47:03 -04:00
jared 84cc023bc4 Enforce ticket visibility on attachment and update endpoints
- 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>
2026-03-20 21:42:47 -04:00
jared 164c2d231a Fix visibility enforcement and register missing API routes
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>
2026-03-20 21:39:02 -04:00
jared 27075a62ee Fix bracket buttons rendering below text + UI/security improvements
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>
2026-03-19 22:20:43 -04:00
jared 89a685a502 Integrate web_template design system and fix security/quality issues
Security fixes:
- Add HTTP method validation to delete_comment.php (block CSRF via GET)
- Remove $_GET fallback in comment deletion (was CSRF bypass vector)
- Guard session_start() with session_status() check across API files
- Escape json_encode() data attributes with htmlspecialchars in views
- Escape inline APP_TIMEZONE config values in DashboardView/TicketView
- Validate timezone param against DateTimeZone::listIdentifiers() in index.php
- Remove Database::escape() (was using real_escape_string, not safe)
- Fix AttachmentModel hardcoded connection; inject via constructor

Backend fixes:
- Fix CommentModel bind_param type for ticket_id (s→i)
- Fix buildCommentThread orphan parent guard
- Fix StatsModel JOIN→LEFT JOIN so unassigned tickets aren't excluded
- Add ticket ID validation in BulkOperationsModel before implode()
- Add duplicate key retry in TicketModel::createTicket() for race conditions
- Wrap SavedFiltersModel default filter changes in transactions
- Add null result guards in WorkflowModel query methods

Frontend JS:
- Rewrite toast.js as lt.toast shim (base.js dependency)
- Delegate escapeHtml() to lt.escHtml()
- Rewrite keyboard-shortcuts.js using lt.keys.on()
- Migrate settings.js to lt.api.* and lt.modal.open/close()
- Migrate advanced-search.js to lt.api.* and lt.modal.open/close()
- Migrate dashboard.js fetch calls to lt.api.*; update all dynamic
  modals (bulk ops, quick actions, confirm/input) to lt-modal structure
- Migrate ticket.js fetchMentionUsers to lt.api.get()
- Remove console.log/error/warn calls from JS files

Views:
- Add /web_template/base.css and base.js to all 10 view files
- Call lt.keys.initDefaults() in DashboardView, TicketView, admin views
- Migrate all modal HTML from settings-modal/settings-content to
  lt-modal-overlay/lt-modal/lt-modal-header/lt-modal-body/lt-modal-footer
- Replace style="display:none" with aria-hidden="true" on all modals
- Replace modal open/close style.display with lt.modal.open/close()
- Update modal buttons to lt-btn lt-btn-primary/lt-btn-ghost classes
- Remove manual ESC keydown handlers (replaced by lt.keys.initDefaults)
- Fix unescaped timezone values in TicketView inline script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:22:24 -04:00
jared bcc163bc77 Audit fixes: security, dead code removal, API consolidation, JS dedup
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>
2026-02-11 14:50:06 -05:00
jared 1c1eb19876 Add UI enhancements and new features
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>
2026-01-30 19:21:36 -05:00
jared ed9c2a39d1 Fix error message disclosure in API endpoints
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>
2026-01-30 18:56:29 -05:00
jared 5b2a2c271e Add security logging, domain validation, and output helpers
- Add authentication failure logging to AuthMiddleware (session expiry,
  access denied, unauthenticated access attempts)
- Add UrlHelper for secure URL generation with host validation against
  configurable ALLOWED_HOSTS whitelist
- Add OutputHelper with consistent XSS-safe escaping functions (h, attr,
  json, url, css, truncate, date, cssClass)
- Add validation to AuditLogModel query parameters (pagination limits,
  date format validation, action/entity type validation, IP sanitization)
- Add APP_DOMAIN and ALLOWED_HOSTS configuration options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 18:51:16 -05:00
jared 7575d6a277 Add performance, security, and reliability improvements
- 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>
2026-01-30 14:39:13 -05:00
jared 8a8b1b0258 Add centralized error handler
- 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>
2026-01-29 10:55:15 -05:00
jared d2a8c73e2c Add caching layer and database helper
- 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>
2026-01-29 10:53:26 -05:00