- CreateTicketView: "Link as duplicate" button on each duplicate result;
stores chosen ticket ID in hidden field, auto-creates duplicates dependency
after ticket is saved (TicketController)
- migrations/004: ticket_watchers table (ticket_id, user_id primary key)
- migrations/005: FULLTEXT index on tickets(title, description) for fast
relevance search replacing LIKE scan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
Bug fixes:
- bulk-delete action called undefined bulkDelete() — wired to the
existing showBulkDeleteModal() so the confirmation modal actually shows
UX:
- Template loader now checks for existing title/description and asks
for confirmation before overwriting user-typed content
- Visibility select shows a dynamic hint paragraph that updates when
the user changes the selection (public/internal/confidential)
Architecture:
- TICKET_STATUSES added to config as single source of truth; all
hardcoded ['Open','Pending','In Progress','Closed'] arrays in
DashboardView now read from config; bulk-status modal in dashboard.js
reads window.TICKET_STATUSES (set from PHP) with array fallback
- ASSET_VERSION now auto-computed from max mtime of dashboard/ticket
CSS+JS files so browsers always pick up changes on deploy; manual
override still available via ASSET_VERSION in .env
- Removed 10 dead standalone stat methods from StatsModel (getOpenTicketCount,
getClosedTicketCount, getTicketsByPriority, etc.) — all superseded by
the consolidated fetchAllStats() queries, never called externally
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No route in index.php ever invokes this method — all ticket updates
go through api/update_ticket.php. The method also lacked authorization
checks, making its removal strictly safer.
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>
StatsModel queries used `FROM tickets WHERE` with no table alias, but
getVisibilityFilter() returns SQL referencing `t.visibility`. Admins
were unaffected because they get `1=1` with no column references.
Added `t` alias to all three tickets queries that use $visSQL.
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>
CSS:
- ticket.css: use combined .comment.thread-depth-N selectors to resolve the
margin-left conflict between .comment-reply and .thread-depth-N classes
dashboard.js:
- Remove legacy initStatusFilter() (superseded by TDS v1.2 sidebar filters)
- Remove initTableSorting() call (client-side sort conflicts with server ?sort=)
- Remove quickSave() + saveTicket() (old hamburger-menu ticket page functions)
- Remove global loadTemplate() (duplicate of IIFE-scoped version in CreateTicketView)
- Remove generateSkeletonRows/Comments/Stats helpers (never called, used
unregistered CSS class names like .skeleton-row-tr)
- Remove "force dark mode" lines that overrode the user theme preference
- Fix non-TDS CSS classes in modal templates: text-center → style, text-green →
lt-text-cyan, mb-half → lt-mb-xs, modal-warning-text → lt-text-danger
Admin views:
- RecurringTicketsView: replace innerHTML += loop with createElement/appendChild
(avoids serial DOM re-parsing on each iteration)
- AuditLogView: add htmlspecialchars() to action_type option values (consistency)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TicketController::create: validate csrf_token from POST before processing
- CreateTicketView: emit hidden csrf_token field; replace innerHTML duplicate
list with DOM methods to prevent any XSS path; guard checkDuplicates() with
lt.api availability check
- index.php audit-log: allowlist action_type; validate date_from/date_to as
YYYY-MM-DD before passing to query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
submitReply() built a replyDiv.innerHTML template literal using
data.user_name (API response) without escaping — an attacker-controlled
display name could inject arbitrary HTML. Fix: wrap all API-sourced
string values in lt.escHtml() within the template.
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>
showReplyForm() read userName from data-user attribute (decoded by
the browser from HTML entities) and injected it unsanitized into
insertAdjacentHTML() — any HTML special chars would be parsed as markup.
Fix: wrap with lt.escHtml() before interpolation.
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-main.lt-container combined selector (specificity 0,2,0)
to prevent responsive .lt-container padding shorthand from overriding
the fixed-header clearance padding-top — affected all viewports < 1280px
- base.css: add .is-hidden { display: none !important } globally; it was
only defined in ticket.css so dashboard ticketPreview popup rendered
as a green box at 0,0 on page load instead of being hidden
- CreateTicketView.php: add dashboard.css to pageStyles so create-ticket-
meta-grid, lt-form-hint, visibility-groups-list, duplicate-list classes
are available (they were undefined when only ticket.css was loaded)
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>
- base.css: add width:100%+min-width:0 to .lt-main so flex column body
doesn't shrink content due to margin:0 auto from .lt-container
- layout_header.php: restructure mobile nav drawer to match web_template
exactly (nav-drawer-links nav, direct <a> links, section div, no ul/li
wrapper, overlay after drawer); fix lt-nav-overlay id mismatch with
base.js; rename lt-header-username -> lt-header-user (matches CSS);
add JSON_HEX_TAG to all inline json_encode calls (closes </script> XSS)
- base.css: add lt-kv-row/label/value aliases (display:contents pattern
used in web_template v1.2 kv-grid); add lt-badge-sm variant
- Admin views: add missing .catch() on editField/editRecurring/loadUsers;
add JSON_HEX_TAG to json_encode in TemplatesView/WorkflowDesignerView
- TicketView: add JSON_HEX_TAG to all ticket-data json_encode calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- layout_footer.php: add lt-footer with context-sensitive keyboard hint bar
([ ~ ] HOME | [ / ] SEARCH | [ + ] NEW | [ * ] CFG | [ ? ] HELP)
Context adapts for dashboard, ticket, and admin pages
- layout_footer.php: wire show-keyboard-help and open-settings for all pages
- base.css: body { display:flex; flex-direction:column } + lt-main { flex:1 }
so footer sticks to bottom of viewport on short pages
- base.css: add lt-flex-gap-xs/sm/md/lg and lt-flex-align-start/center/end
(were used across all views but never defined — causing broken layouts)
- base.css: add --lt-danger/amber/cyan/success/text-primary CSS variables
(referenced in ticket.css and dashboard.css fallbacks but never declared)
- base.css: add lt-text-danger/warning/success/info/primary utility classes
(used in TicketView, DashboardView, admin views but not defined in base.css)
- DashboardView.php: remove ascii-banner.js (loaded but never called)
- TemplatesView.php: fix priority badge from lt-p* to lt-chip component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full application redesign using Terminal Design System v1.2 (lt-* class
system). Introduces shared layout_header/footer partials, upgrades
base.css/base.js to TDS v1.2, and rewrites all views (Dashboard, Ticket,
CreateTicket, and all 7 admin views) with lt-frame, lt-table, lt-modal,
lt-stats-grid, lt-kv-grid, and data-action event delegation patterns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DashboardView.php:
- Table status column: replace status-{slug} with lt-status lt-status-{slug} for consistent [● Status] bracket decoration from base.css
- Table priority column: replace raw number with lt-priority lt-p{N} empty span for [▲▲ P1 CRITICAL] style badges
dashboard.js:
- Kanban card priority badge: replace card-priority p{N} with lt-priority lt-p{N} to use the design system badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ticket.js: replace custom formatFileSize() with lt.bytes.format() from web_template base.js; remove the now-redundant local function
- DashboardView.php: add id="tickets-table" and wire lt.tableNav.init() for j/k/Enter keyboard row navigation
- DashboardView.php: add lt-stat-card class + data-filter-key/data-filter-val to open/critical/closed stat cards; wire lt.statsFilter.init() + window.lt_onStatFilter so clicking a stat card filters the ticket list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returning 403 Forbidden leaks the existence of tickets to users who
should not know about them. Use 404 Not Found consistently across all
access-controlled endpoints to prevent enumeration.
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>
utils.js is loaded on all pages (dashboard, ticket, admin views) before dashboard.js.
Moving the canonical definition there and removing the guard + the copy in dashboard.js
eliminates the redundant redefinition on every page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StatsModel.getAllStats() now accepts a user array and applies the same
getVisibilityFilter() logic used by ticket listings. Admins continue to
share a single cached result; non-admin users get per-user cache entries
so confidential ticket counts are not leaked in dashboard stats.
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>
- Replace style.display with .is-hidden classList in ApiKeysView, CustomFieldsView, RecurringTicketsView
- Convert boot overlay fade-out from style.opacity to .boot-overlay--fade-out CSS class
- Add .boot-overlay--fade-out rule to dashboard.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch tab show/hide from style.display to .tab-content.active CSS class
- Convert visibilityGroupsField, markdownPreview, uploadProgress to use .is-hidden class
- Replace comment text div style.display with classList.add/remove('is-hidden')
- Add .is-hidden utility class to ticket.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace stat-card cursor:pointer inline style with CSS rule
- Convert view toggle (table/card) to use .is-hidden CSS class
- Convert bulk-actions and export-dropdown to use .is-visible class
- Add .is-hidden/.is-visible utility rules to dashboard.css
- Remove duplicate lt.keys.initDefaults() call from dashboard.js
- Remove redundant setTimeout from view mode restore
- Add lt.keys.initDefaults() to dashboard.js (was missing entirely)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- htmlspecialchars() on category, type, status in table rows
- htmlspecialchars() on data-status attributes in quick-action buttons
- Restrict $currentDir to 'asc'|'desc' to prevent class injection
- htmlspecialchars() on all http_build_query URLs in pagination and sort headers
- htmlspecialchars() on AuditLogView pagination URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all 8 showToast() calls in ApiKeysView.php with lt.toast.*
— all toast calls in the codebase now use lt.toast directly
- Add .duplicate-list, .duplicate-meta, .duplicate-hint CSS classes to
dashboard.css; replace inline styles in duplicate detection JS with them
- Add dashboardAutoRefresh() using lt.autoRefresh — reloads page every
5 minutes, skipping if a modal is open or user is typing in an input
- Add REFRESH button to dashboard header that triggers lt.autoRefresh.now()
for immediate manual refresh with timer restart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .inline-error and .inline-warning utility classes to dashboard.css
with correctly-matched terminal palette rgba values (replaces off-palette
rgba(231,76,60,0.1) and rgba(241,196,15,0.1))
- Add .key-generated-alert class for the new API key display frame
- Add base .dependency-item, .dependency-group h4, .dependency-item a,
.dependency-title, .btn-small overrides to ticket.css
- Remove all inline styles from the dependency list template in ticket.js
— layout, colors, and sizing now come from CSS classes
- Update CreateTicketView.php and ApiKeysView.php to use the new classes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add data-ts attributes to TicketView.php: ticket created/updated
header, comment dates (inner span to preserve edited indicator),
and all activity timeline dates
- Add initRelativeTimes() to ticket.js using lt.time.ago(); runs on
DOMContentLoaded and every 60s to keep relative times current
- Attachment dates now use lt.time.ago() with full date in title attr
and ts-cell span for periodic refresh
- Replace all 11 showToast() calls in ticket.js with lt.toast.* directly,
removing reliance on the backwards-compat shim for these paths
- Add span.ts-cell and td.ts-cell CSS to both dashboard.css and ticket.css:
dotted underline + cursor:help signals the title tooltip is available
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add data-ts attributes to table and card view date cells so JS can
convert them to relative time ("2h ago") while keeping the full date
in the title attribute for hover tooltips
- Add initRelativeTimes() in dashboard.js using lt.time.ago(); runs on
DOMContentLoaded and refreshes every 60s so times stay current
- Fix table sort for date columns to read data-ts attribute instead of
text content (which is now relative and not sortable as a date)
- Update README: add base.css/base.js/utils.js to project structure,
fix ascii-banner.js description, expand keyboard shortcuts table,
add developer notes for lt.time and boot sequence behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>