Commit Graph

326 Commits

Author SHA1 Message Date
jared fdc6d3d463 Fix ASCII art alignment, readonly input opacity, api key visibility
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>
2026-03-31 19:43:18 -04:00
jared 72d5061867 Fix description line breaks and disabled-field readability
Ticket descriptions are plain text — renderDescriptionView() now always
uses nl2br instead of parseMarkdown(), preventing markdown from mangling
single newlines into run-on paragraphs.

Override base.css opacity:0.45 on disabled .editable-metadata selects
(Priority, Category, Type) so they remain legible at full contrast on
dark/OLED screens in read mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 19:36:10 -04:00
jared 1d721eecb4 fix: description unreadable in dark mode / OLED — swap disabled textarea for lt-markdown div
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>
2026-03-31 18:10:39 -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 a89095cbcc chore: delete applied migrations (001-002, 004-005 all applied to DB)
All SQL migration files have been applied and recorded in the migrations
tracking table. Folder intentionally empty — migrate.php kept as runner
for future one-time schema changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 22:03:16 -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 0acf5e84c3 feat: duplicate link action, watcher migration, fulltext search migration
- 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>
2026-03-29 21:54:00 -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 2fdd42b45b UX and architecture fixes: bulk-delete, template guard, statuses config
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>
2026-03-29 21:09:29 -04:00
jared 277daf6f00 Remove dead TicketController::update() method
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>
2026-03-29 18:32:55 -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 f0abadfc57 Fix 500 error for non-admin users on dashboard
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>
2026-03-28 22:42:39 -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 d8e6dcf7fa fix: CSS nesting conflict, dashboard.js dead code removal, admin view escaping
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>
2026-03-28 21:34:34 -04:00
jared 6b76496640 fix: CSRF on ticket create form, DOM-safe duplicate list, audit-log param validation
- 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>
2026-03-28 21:26:52 -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 87f878ee6b Fix XSS: escape user_name and created_at in reply DOM injection
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>
2026-03-28 13:42:27 -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 e2c23d0405 Fix XSS: escape userName in reply form insertAdjacentHTML template
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>
2026-03-28 13:38:30 -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 3bb4792635 Fix header overlap, is-hidden missing globally, and CreateTicketView CSS
- 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>
2026-03-28 13:30:00 -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 e721b33911 Align UI with web_template TDS v1.2 standards
- Replace lt-chip priority badges with lt-badge lt-badge-p[1-4] across
  DashboardView, TemplatesView (matches web_template sticky table pattern)
- Add lt-theme-btn theme toggle to header-right; wire lt.theme.toggle()
- Replace ASCII art empty state with lt-empty-state component in dashboard
- Standardize tab wrapper lt-tabs → lt-tab-bar in Dashboard and TicketView
- Add missing lt-keys-help modal to layout_footer (fixes ? key doing nothing)
- Add lt-cmd-overlay command palette container + lt.cmdPalette.init() nav
- Add .lt-timeline-action CSS rule (used in TicketView, was undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:06:40 -04:00
jared d7775e62ec Fix layout regressions, nav drawer structure, and security issues
- 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>
2026-03-28 12:43:24 -04:00
jared 51f6991f9d feat: nano-style footer bar, missing utility classes, CSS semantic vars
- 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>
2026-03-27 20:16:05 -04:00
jared 9bdeaf7731 fix: deep audit — wire TDS v1.2 components, fix kanban/tabs/bulk/avatar
- ticket.js: fix showTab() early return preventing attachments/deps from loading
- ticket.js: fix performStatusChange() overwriting lt-status-* classes
- dashboard.js: fix updateSelectionCount() using is-visible instead of style.display
- dashboard.js: fix populateKanbanCards() to use #kanban-col-* IDs (TDS v1.2)
- dashboard.js: fix setViewMode() removing references to old non-TDS elements
- dashboard.js: remove mobile-bottom-nav injection (no CSS existed for it)
- dashboard.css: add full lt-kanban-card component styles with priority accents
- dashboard.css: add mobile sidebar overlay, filter toggle, ticket preview popup CSS
- DashboardView.php: replace priority badges with lt-chip component
- TicketView.php: add lt-avatar with initials to comment author display
- ApiKeysView.php: enhance API usage section with lt-code-block component + curl example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 19:58:14 -04:00
jared 79c2d2b513 feat: complete TDS v1.2 redesign across all views
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>
2026-03-27 19:05:42 -04:00
jared 1989bcb8c8 Migrate status and priority display to lt-status/lt-priority design system classes
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>
2026-03-20 22:25:49 -04:00
jared 0a2214bfaf Improve web_template compliance: lt.bytes.format, lt.tableNav, lt.statsFilter
- 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>
2026-03-20 22:07:49 -04:00
jared e7d01ef576 Return 404 (not 403) for inaccessible tickets in TicketController
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>
2026-03-20 21:47:28 -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 06b7a8f59b Consolidate showConfirmModal into utils.js, remove duplicate from dashboard.js
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>
2026-03-20 21:44:46 -04:00
jared 9f1a375e5a Apply visibility filtering to dashboard statistics
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>
2026-03-20 21:44:01 -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 ce95e555d5 CSS class migrations: admin views and boot overlay fade-out
- 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>
2026-03-20 21:20:55 -04:00
jared f45ec9b0f7 CSS class migrations in CreateTicketView: duplicate warning, visibility groups
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:18:16 -04:00
jared 5a41ebf180 Convert ticket preview popup visibility to use .is-hidden CSS class
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:16:49 -04:00
jared e35401d54e CSS class migrations for ticket page: tabs, visibility, markdown preview, uploads
- 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>
2026-03-20 21:13:55 -04:00
jared 913e294f9d CSS class migrations: stat-card cursor, view toggle, bulk actions visibility
- 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>
2026-03-20 21:08:28 -04:00
jared 28aa9e33ea Fix XSS: escape table data and sanitize sort/pagination URL params
- 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>
2026-03-20 20:40:51 -04:00
jared 31aa7d1b81 Fix JS SyntaxError breaking tabs, textarea scrolling, and XSS escaping
Bug fixes:
- ticket.js: Remove duplicate const textarea declaration inside showMentionSuggestions()
  (was redeclaring a parameter, causing SyntaxError that broke all tab switching)
- ticket.css: Add overflow:hidden + resize:none to disabled textarea so description
  shows full height without internal scrollbar (page scrolls instead)
- ticket.js: Trigger height recalculation when entering edit mode on description

XSS/escaping fixes:
- TicketView.php: htmlspecialchars() on description textarea content (closes </textarea> injection risk)
- TicketView.php: htmlspecialchars() on ticket status and workflow transition status strings
- DashboardView.php: htmlspecialchars() on $cat/$type in input value= attributes
- RecurringTicketsView.php: htmlspecialchars() on composed schedule string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 20:34:55 -04:00