PHP inline conditionals inside HTML context must use 4-space indentation
to satisfy PSR-12 Generic.WhiteSpace.ScopeIndent rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the lt-alert workaround with the new purpose-built SLA banner
component now in base.css:
- lt-sla-p1 (pulsing red) / lt-sla-p2 (static amber) wrapper classes
- Structured subcomponents: lt-sla-icon, lt-sla-info, lt-sla-title,
lt-sla-bar + lt-sla-fill (gradient fill), lt-sla-meta, lt-sla-dismiss
- Dismiss now uses banner.hidden + sessionStorage key lt_sla_dismissed_<id>
(aligns with web_template pattern; previous code used classList 'dismissed')
- Elapsed/remaining/breach state driven by same tick() interval, now updating
lt-sla-fill width instead of a separate lt-progress bar inside lt-alert-msg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Progress bars now use linear-gradient fills for a more dramatic terminal
readout appearance (matches web_template 39862fa):
- Default (orange), --cyan, --green, --red variants all upgraded from flat
accent colors to directional gradients with highlight endpoints
SLA banner component (lt-sla-p1 / lt-sla-p2) added to base.css, replacing
the lt-alert workaround previously used for P1/P2 SLA display:
- lt-sla-p1: pulsing red banner (animation: lt-sla-pulse 2s)
- lt-sla-p2: static amber banner
- Subcomponents: icon, info, title, bar, fill, meta, dismiss
- Both fills use gradients for visual consistency (P2 amber→#ffd740)
- lt-sla-dismiss includes transition + :focus-visible ring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add security.yml badge to header
- Replace stale 'npm audit' description with actual semgrep config
- Add deploy tagging and notify-failure rows that were missing
- Fix ESLint config location note
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tainted-filename: filenames in upload_attachment.php and user_avatar.php
are derived exclusively from (int)-cast integers; no user string reaches
the filesystem path. Semgrep's taint engine tracks all use-sites of the
variable, producing findings on every file_exists/readfile/unlink call.
- tainted-callable: index.php audit-log query passes \$sql to prepare();
\$sql is assembled from hardcoded SQL fragments with ? placeholders and
explicit (int) LIMIT/OFFSET casts. User values are bound via bind_param,
never interpolated. Semgrep cannot see through the WHERE-builder logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- index.php: replace SQL string interpolation with concatenation + explicit
(int) casts for LIMIT/OFFSET; add nosemgrep for tainted-sql false positive
(WHERE clause built from hardcoded fragments with bound params only)
- api/upload_attachment.php: add realpath() path-traversal guard after mkdir
- api/user_avatar.php: make (int) cast explicit at cache-path construction;
add nosemgrep for tainted-filename false positive (integer-only input)
- assets/js/ticket.js: add nosemgrep for insertAdjacentHTML — all dynamic
content already escaped via lt.escHtml() before insertion
- .gitea/workflows/security.yml: exclude echoed-request rule globally —
all echo in API context is json_encode() output, not HTML; htmlentities()
fix semgrep suggests would corrupt JSON responses
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spam fixes:
- Add ZFS pool category to hash with subtypes (pool_state, pool_usage,
pool_errors) so DEGRADED and usage-high on same pool get separate tickets
- Strip volatile percentages from LXC/ZFS usage titles ("usage high: 80.1%"
→ "usage high") and OSD counts from BlueStore slow-ops titles
("2 OSD(s) experiencing" → "OSD(s) experiencing") in hwmonDaemon.py
phpcs fix:
- Remove leftover merge conflict marker (<<<<<<< HEAD / >>>>>>>)
in create_ticket_api.php which caused phpcs to fail on bitshift
operator spacing
DB cleanup:
- Deleted 107 spam comments and 107 audit entries from tickets
357934698 (ZFS pool), 673679581 (BlueStore), 925498317 (LXC storage)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comments on worsening condition now only fire on priority escalation.
Title and description updates are silent — title changes (e.g. rising
Power_On_Hours counters) were generating a comment on every hourly run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- source_type (auto vs manual) added to dedup hash so automated
tickets never collide with manually created ones
- OSD-specific subtype (osd_down_N) so each OSD gets its own ticket
- Description refreshed on every automated update (current sensor data)
- Comments on worsening condition only fire on meaningful changes
- ASCII art descriptions wrapped in fenced code blocks in comments
- Reopen comment also uses fenced code block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Include source_type (auto vs manual) in dedup hash so automated
tickets never collide with manually created ones. This was causing
hwmonDaemon to hijack manual task tickets that shared the same
cluster/category/environment tags.
- Include specific OSD ID in hash subtype (osd_down_N) so each OSD
failure gets its own ticket instead of all colliding to osd_down.
- Wrap hwmonDaemon report descriptions in fenced code blocks in
comments so ASCII art box-drawing renders correctly instead of
collapsing into a paragraph blob.
- Refresh ticket description on every automated update so the ticket
body shows current sensor data, not stale values from first report.
- Only post a worsening-condition comment when title or priority
actually changed (not just a description refresh).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lint.yml: add notify-failure Matrix alert job; add Tag deployed commit
step (main branch only) with deploy-YYYY.MM.DD-N tagging via Gitea API;
add permissions: contents: write to deploy job
- security.yml: new workflow running semgrep with p/php and p/owasp-top-ten
configs on push, PR, and weekly schedule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a deploy job that runs only when both php-lint and js-lint succeed.
Calls the CT132 webhook directly with HMAC-SHA256 signature from the
WEBHOOK_SECRET repo secret. Disabled the direct push webhooks that
previously deployed on every push regardless of lint status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The menu was positioned top:calc(100%+4px), leaving a 4px dead zone
between the trigger and the menu that interrupted the :hover chain.
Changed to top:100% with padding-top:6px + margin-top:-2px so the
menu's hover area is contiguous with the trigger — no more needing
to mouse over quickly to keep the dropdown open.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
body lacked display:flex + flex-direction:column, so the existing
flex:1 on .lt-main had no effect. Error pages (404, 403) and any
page with little content showed the footer immediately after content
rather than pinned to the viewport bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the bare "Ticket not found" text response with the shared
views/error_404.php partial so users see the full TDS-styled error page.
Also collapsed the two identical 404 branches into one check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lt.copy() does not exist — the correct API is lt.clipboard.copy().
Also added ok-check since clipboardCopy() returns a boolean promise,
not a rejection on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added php_sapi_name() CLI guard matching the pattern used in migrate.php
and cleanup_ratelimit.php. Without this, the script was web-accessible
and could generate an API key without authentication if no keys existed yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /ticket.php?id=VALUE redirect did not validate the id parameter,
allowing path traversal (e.g. ?id=../admin) or other unexpected values
in the Location header. Added ctype_digit validation so only positive
numeric IDs are redirected to /ticket/N; anything else falls back to /.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NotificationHelper::notifyWatchers: excludeUserId parameter was
accepted but never used; actors were notified of their own actions.
Fix: add AND tw.user_id != ? clause to watcher query when exclusion
is requested.
- TicketView.php: formatAction() default case returned raw
$event['action_type'] unescaped into HTML context. Fix: wrap with
htmlspecialchars().
- Admin views: field_id, recurring_id, template_id, transition_id
in data-id attributes were uncast; field_type was unescaped in
CustomFieldsView; from/to_status slugs derived from DB values were
used directly in class attributes in WorkflowDesignerView.
Fix: (int) cast for IDs, htmlspecialchars for field_type,
preg_replace to sanitize DB-derived CSS class slugs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
api/export_tickets.php: getAllTickets() was called without $currentUser,
so visibility filtering was skipped — any authenticated user could export
all tickets including confidential/internal ones.
api/user_preferences.php: the single-preference setcookie() call was
missing httponly/secure flags (batch path had them correctly). Also cast
preference values to string before passing to setPreference(string).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The command palette 'P1 Critical Tickets' link uses ?priority=1, but
getAllTickets only accepted priority_min/priority_max. Resolve the single
?priority=N param to both priority_min and priority_max so it acts as
an exact priority match.
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>
PHP 8.2 strict mysqli mode throws mysqli_sql_exception on duplicate key
rather than returning false from execute(). Replace the old if/else errno
check with try/catch on mysqli_sql_exception, re-throw non-1062 errors,
and use random_int range 100000000-999999999 (no leading zeros) for the
retry ID.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PHP 8.2 raises mysqli_sql_exception on prepare() for non-existent tables
rather than returning false. Wrap each child-table delete in try/catch and
silently skip tables that don't exist in all deployments, re-throwing for
unexpected errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
parseInt(ticketId, 10) was stripping leading zeros before sending
to update_ticket.php. Switch to String(ticketId) for consistency
with all other ticket ID handling in the JS codebase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All ticket_id parameters were bound as integer ("i"), which stripped
leading zeros before insertion into ticket_attachments.ticket_id
(VARCHAR 9). This caused a mismatch: upload_attachment.php creates
the directory using the full string (e.g. /uploads/000123456/) but
the DB stored the integer form ("123456"), so download and delete
would look in the wrong path.
Changed getAttachments, addAttachment, getTotalSizeForTicket, and
getAttachmentCount to use string binding ("s") so the canonical
zero-padded ticket ID is stored and read back consistently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Use random_int(100000000-999999999) so IDs are always 9 digits without
a leading zero, matching the behaviour of TicketModel::createTicket().
The old sprintf('%09d', mt_rand(1, ...)) could produce IDs like
000123456 which broke PHP array key lookups elsewhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>