The filter action used ?assigned_to=none but DashboardController only
recognises 'unassigned' as the sentinel value for that filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The command palette 'My Open Tickets' action navigates to
?assigned_to=me, but DashboardController only handled numeric IDs
and 'unassigned', silently ignoring 'me'. Resolve 'me' to the
current user's ID. Also update the active filter chip to display
'Me' instead of 'User #me'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
A '?' button next to the MD/Preview toggles opens a reference modal
covering all supported syntax: inline formatting, headings, lists,
task lists, tables, code blocks, footnotes, emoji shortcodes, and
ticket references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The #markdownPreview div lacked the lt-markdown class, so CSS rules for
list-style (ul bullets, ol numbers), mark, del, task items etc. never
applied during live preview while typing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
comment-text divs with data-markdown were never getting the lt-markdown
class, so all scoped CSS (ul/ol/li bullets, mark, del, task items, etc.)
had no effect. Fixed in PHP template, JS comment builder, and
renderMarkdownComments().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
toggle-sidebar action was only in the DashboardView inline script,
not in dashboard.js where toggleSidebar() is defined. Move it into
the dashboard.js event delegation switch so it's guaranteed to fire.
Also fix beta webhook: was using a different secret than production
so Gitea pushes to development never triggered the beta deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JS was toggling .collapsed on the wrong element (dashboardSidebar div
instead of lt-sidebar aside), and the expand button was permanently
display:none. When collapsed, users had no way to re-expand.
- toggleSidebar now targets lt-sidebar (the aside)
- Toggle button flips ◀ ↔ ▶ to indicate state and serve as the expand button
- Collapsed CSS hides the body and label, centers the ▶ button in the strip
- Remove the dead sidebarExpandBtn element from HTML
- Persist and restore state correctly on page load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tile click was omitting status param so controller applied default
Open/Pending/In Progress filter, hiding closed/other-status tickets
created today. Pass show_all=1 instead to match the stat count which
includes all statuses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Created Today tile: no longer limits to open statuses (count is all statuses)
- Closed Today tile: filters by closed_at range, not updated_at
- Add closed_from/closed_to support to TicketModel and DashboardController
- Add Created/Updated/Closed date range inputs to sidebar filter panel
- Apply button collects date inputs; Clear All removes them
- removeFilter handles date chip removal (clears both _from and _to)
- Active filter chips shown for date ranges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
Removes inline max-width/nowrap from title td, moves to CSS with
width:99% so the title column absorbs all available space freed by
hiding other columns. max-width:0 trick ensures overflow ellipsis
still works correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TicketView: ticket age was measuring from last update not creation;
fixed to always use created_at
- dashboard.js: bulk assign used non-existent onSelect callback (no
selection was ever stored); fixed to onChange with selected[0],
added max:1 to enforce single-select
- base.js: lt.combobox Enter key only fired when focusedIdx >= 0;
now falls back to first filtered result when no arrow key used
- DashboardView + dashboard.js + dashboard.css: add COLS ▾ button on
table header that opens a checkbox panel to show/hide optional
columns (Ticket ID, Category, Type, Created By, Assigned To,
Created, Updated); state persisted in localStorage, Reset button
restores all; core columns (Priority, Title, Status, Actions) always
visible; data-col attributes added to all th/td for CSS targeting
Notifications bell: was functional all along — was broken by the
notifications.php 500 error (now fixed). Avg resolution: correct,
tickets genuinely take ~158 days average on this dataset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ticket.js: status change requiring a comment now shows an inline
modal with a textarea — comment is actually posted before the status
changes, instead of just warning the user and changing anyway
- layout_header.php: add ⌘K button in header so users can discover
the command palette; also removes inline onclick in favor of JS
(CSP-safe via nonce script block already present)
- TicketView.php: upgrade breadcrumb to lt-breadcrumb markup with
ticket title preview (truncated at 45 chars) and aria-current
- ticket.js + ticket.css: image attachments now render as clickable
thumbnails (3rem×3rem) that open in lt.lightbox; non-image files
keep the icon display unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds lt-cmd-overlay HTML to layout_header.php and initializes
lt.cmdPalette with commands for: navigation (Dashboard, New Ticket),
filters (My Tickets, Unassigned, P1 Critical), admin pages (if admin),
and recent tickets (last 5 viewed, stored in localStorage).
TicketView.php records each viewed ticket ID to localStorage under
lt_recent_tickets so the command palette can surface them as Recent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
- Remove duplicate edit-comment/delete-comment cases from TicketView.php inline
script — ticket.js already handles them. Double-call of editComment() would
immediately open then close the edit form (second call sees .editing → cancels)
- Fix keyboard shortcut 1-4 status change: dispatchEvent(new Event('change'))
was non-bubbling (default), so the document-level change delegation in TicketView
never received it. Now uses { bubbles: true } so updateTicketStatus() fires correctly
- Fix saved filter status type: getCurrentFilterCriteria() was saving status as a
joined string "Open,Pending" but pill-click handler called .join() expecting an array
(TypeError swallowed by try/catch → status filter silently not applied). Now saves
as array; applySavedFilterCriteria handles both arrays and legacy strings
- Pill-click handler also updated to handle both array and string status formats
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add nonce to charts and ticket-preview drawer inline <script> blocks in
DashboardView.php (both were CSP-blocked — charts never rendered)
- Add .lt-modal-xs (280px) to base.css — used by quickStatus/quickAssign
modals but was undefined, causing them to use full modal width
- Fix showConfirmModal in utils.js: class="text-center" → "lt-text-center"
(undefined class); escape newlines as <br> so multi-line messages render
- Remove duplicate click-handler cases from DashboardView.php inline script
that were already handled by dashboard.js, preventing double-firing
(export-tickets, open-settings, remove-filter, etc. were all called twice)
- Fix manual-refresh action to use lt.autoRefresh.now() instead of bare
window.location.reload() so modal/focus guards are respected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .lt-modal-sm (max 360px) and .lt-modal-header--danger variant used
in JS-generated bulk delete confirmation modal (no CSS = unstyled header)
- Add .lt-badge-sm for compact inline badges (comment counts, group tags)
- Add .lt-kv-row { display:contents } with .lt-kv-label/.lt-kv-value rules
(was missing from previous commit — added in base.css)
- Replace style="text-align:center" with .lt-text-center in JS modal body
- Replace style="flex-direction:column" with .lt-flex-col on .lt-btn-group
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Avatar img tag was missing closing > — the endif fired before the tag
closed, causing the initials span to be parsed as an attribute value;
this would silently break the avatar fallback when image fails to load
- Replace style="width:100%;text-align:center" on notif footer link with
lt-w-full lt-text-center utility classes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Remove all onerror="this.style.display='none'" from avatar imgs in
layout_header.php, DashboardView.php, and TicketView.php (PHP + JS)
- Replace onclick SLA dismiss with data-action="dismiss-priority-banner"
attribute; handler wired via existing click delegation in TicketView.php
- Global capture-phase error delegation in layout_footer.php handles all
avatar image failures by adding .lt-avatar-img-err class (CSS display:none)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avatar bug:
- base.css: .lt-avatar now position:relative; img is position:absolute inset:0
so a loaded image covers the initials span (fixes img+initials shown together)
- base.css: .lt-avatar img.lt-avatar-img-err { display:none } — CSS hook for error state
- layout_footer.php: capture-phase error event delegation on .lt-avatar imgs
replaces blocked inline onerror handlers (CSP has no unsafe-inline in script-src)
Chart bug:
- DashboardView: replaced display:flex section-body containers with a
position:relative; width:100%; height:170px div wrapper for each canvas
(Chart.js responsive:true reads parentNode dimensions; flex containers
give canvas zero intrinsic width causing 0×0 render = empty charts)
- Removed has-lt-overlay from chart frames (no overlay div was injected)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DashboardView: Charts row with 3 panels (priority donut, status donut, category bar)
using Chart.js from CDN; data passed inline from PHP stats; TDS color palette
- DashboardView: Flatpickr date picker on advanced search date fields with TDS theme overrides
- dashboard.js: showTableSkeleton() shows lt-skeleton-row during filter-triggered reloads
and auto-refresh; called before all location.reload() with delay
- dashboard.css: Flatpickr TDS theme overrides (dark BG, monospace font, TDS accent colors)
- SecurityHeadersMiddleware: Added cdn.jsdelivr.net to script-src and style-src CSP
to allow Chart.js and Flatpickr from CDN
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dependencies tab: auto-loads potential duplicates via /api/check_duplicates.php
on first activation; shows 'Mark duplicate' button per result which POSTs to
ticket_dependencies with type=duplicates and refreshes the dependencies list
- Settings modal: replaced checkboxes with lt-toggle switches for
notifications_enabled and sound_effects; loads current user prefs on modal open
and saves via /api/user_preferences.php on SAVE button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- updateTicketField() now targets .lt-frame-ticket[data-priority] (TDS v1.2)
instead of old .priority-indicator / .ticket-container selectors
- All 7 admin views: keyboard-shortcuts.js now uses dynamic ?v={$_v}
instead of hardcoded unversioned path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dashboard: saved filter pills row above active filters bar — loads from API,
click applies criteria as URL params, hidden when no saved filters exist
- ticket.css: add TDS-styled CSS for @mention autocomplete dropdown (was unstyled)
- Dashboard table: data-tooltip on Title and Assigned To columns for truncated text
(lt.tooltip.init() auto-called by lt.init(), zero extra JS needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dashboard stat cards now show lt-dot trend indicators (up/warn/idle) based on
created_today vs closed_today flow — no extra DB query needed
- Add collapsible Team Workload panel showing assignee open ticket counts with
progress bars (green/cyan/red by load), avatar, and name
- StatsModel.getTicketsByAssignee() now returns proper objects with user_id,
display_name, open_count (was name-keyed flat array); limit raised to 8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Fix DashboardView asset version (was hardcoded 20260327, now uses config ASSET_VERSION)
- Add lt-dot status indicators on dashboard table rows and ticket view toolbar
- Add lt-tag display for Category/Type in ticket read mode (swaps to select in edit mode)
- Add P1/P2 SLA alert banner with elapsed time, progress bar, per-session dismiss
- Wire command palette (Ctrl+K): global nav + admin links via lt.cmdPalette.init()
- Fix cmdPalette.init() call format (flat array, not nested group objects)
- Improve activity timeline: richer formatAction(), better color coding by event type,
inline status transitions shown in meta row, icon column added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS cascade fixes were correct but browser was serving cached base.css.
Inline style cannot be cached separately and bypasses all cascade issues.
CSS variables still respect media query :root overrides so --header-height
resolves to the correct value (50px SM, 46px XS) at each breakpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In the SM (≤767px) and XS (≤479px) media queries, .lt-container { padding }
shorthand appeared after .lt-main { padding-top } with equal specificity,
causing the shorthand to clobber the header-clearance padding-top. Swap order
so .lt-main always wins.
Also remove redundant lt-scanlines div — body::before in base.css already
renders the scanline overlay globally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
body::before and body::after are used for background grid/gradient effects.
Adding lt-scanlines to body caused ::after conflict (higher specificity) and
put the scanline overlay at z-index 9998, above the header at z-index 300.
Move lt-scanlines to a dedicated fixed div so pseudo-elements don't conflict
and the header remains fully visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sync base.css + base.js from web_template (adds lt-scanlines,
lt-cursor, lt-radar, lt-display-field, --font-crt/VT323 token)
- Add VT323 to Google Fonts link in layout_header.php
- Add lt-scanlines to <body> — CRT scanline overlay, light-mode suppressed
- Replace custom .editable-metadata:disabled CSS override in ticket.css
with the canonical .lt-display-field class from base.css
- Switch Priority/Category/Type/Visibility selects and visibility-group
checkboxes in TicketView.php from disabled attribute to lt-display-field
- Update toggleEditMode() in ticket.js to add/remove lt-display-field
instead of toggling the disabled attribute
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: DashboardView.php and dashboard.js both had a global
document.addEventListener('click') handler handling the same bulk-assign
and quick-assign actions. Every click fired both handlers, creating two
modals and two API fetches that both appended to the same select element.
Fix: Remove duplicate cases (bulk-*, navigate, view-ticket, quick-*,
set-view-mode, toggle-*, clear-selection) from DashboardView.php's inline
handler. dashboard.js already handles all of these correctly.
Also replace <select> with lt.combobox in both bulk-assign and
quick-assign modals so large user lists are searchable instead of a
long scrolling dropdown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use white-space:pre-wrap on description view div so newlines and multiple
spaces are preserved natively — no <br> replacement, ASCII art aligns
correctly since body is already monospace (JetBrains Mono).
Override opacity:1 on readonly API key input so generated keys are fully
readable instead of being faded to 0.45 by base.css [readonly] rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: disabled textarea gets opacity:0.45 + color:var(--text-muted) from
base.css, making it near-invisible on OLED (true-black background).
Fix:
- TicketView: add #ticketDescriptionView (div.lt-markdown) alongside the textarea;
textarea is now hidden by default (style="display:none"), view div is shown
- ticket.js: renderDescriptionView() renders raw text via parseMarkdown() or nl2br;
showDescriptionView() / showDescriptionEdit() swap between them;
toggleEditMode() calls showDescriptionEdit() when entering edit, and
renderDescriptionView() + showDescriptionView() when returning to read mode
- ticket.css: .ticket-description-view sets full-contrast text-primary/secondary
colors, min-height, and line-height for comfortable reading
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
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>
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>