diff --git a/Claude.md b/Claude.md index 7cc43af..fd93c4e 100644 --- a/Claude.md +++ b/Claude.md @@ -23,14 +23,15 @@ Tinker Tickets is a feature-rich, self-hosted ticket management system built for - Backend: PHP 7.4+ with MySQLi - Frontend: Vanilla JavaScript, CSS3 - Database: MariaDB on separate LXC (10.10.10.50) -- Web Server: Apache on production (10.10.10.45) +- Web Server: nginx with PHP-FPM on production (10.10.10.45) - Authentication: Authelia SSO with LLDAP backend **Production Environment:** -- **Primary URL**: http://t.lotusguild.org -- **Web Server**: Apache at 10.10.10.45 (`/root/code/tinker_tickets`) +- **Primary URL**: https://t.lotusguild.org +- **Web Server**: nginx at 10.10.10.45 (`/var/www/html/tinkertickets`) - **Database**: MariaDB at 10.10.10.50 (`ticketing_system` database) - **Authentication**: Authelia provides SSO via headers +- **Dev Environment**: `/root/code/tinker_tickets` (not production) ## Architecture @@ -105,7 +106,8 @@ Controllers → Models → Database │ ├── UserPreferencesModel.php # User preferences │ └── WorkflowModel.php # Status transition workflows ├── scripts/ -│ └── cleanup_orphan_uploads.php # Clean orphaned uploads +│ ├── cleanup_orphan_uploads.php # Clean orphaned uploads +│ └── create_dependencies_table.php # Create ticket_dependencies table ├── uploads/ # File attachment storage ├── views/ │ ├── admin/ @@ -195,6 +197,10 @@ All admin pages are accessible via the **Admin dropdown** in the dashboard heade 7. **Cache busting**: Use `?v=YYYYMMDD` query params on JS/CSS files 8. **Ticket linking**: Use `#123456789` in markdown-enabled comments 9. **User groups**: Stored in `users.groups` as comma-separated values +10. **API routing**: All API endpoints must be added to `index.php` router +11. **Session in APIs**: RateLimitMiddleware starts session; don't call session_start() again +12. **Database collation**: Use `utf8mb4_general_ci` (not unicode_ci) for new tables +13. **Ticket ID extraction**: Use `getTicketIdFromUrl()` helper in JS files ## File Reference Quick Guide diff --git a/README.md b/README.md index 397cdd5..2e73e60 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,18 @@ Access all admin pages via the **Admin dropdown** in the dashboard header. | `/admin/api-keys` | Generate and manage API keys | ### Notifications -- **Discord Integration**: Webhook notifications for ticket creation +- **Discord Integration**: Webhook notifications for ticket creation and updates - **Rich Embeds**: Color-coded priority indicators and ticket links +- **Dynamic URLs**: Ticket links adapt to the server hostname + +### Keyboard Shortcuts +| Shortcut | Action | +|----------|--------| +| `Ctrl/Cmd + E` | Toggle edit mode (ticket page) | +| `Ctrl/Cmd + S` | Save changes (ticket page) | +| `Ctrl/Cmd + K` | Focus search box (dashboard) | +| `ESC` | Cancel edit / close modal | +| `?` | Show keyboard shortcuts help | ### Security Features - **CSRF Protection**: Token-based protection on all forms @@ -150,13 +160,20 @@ Access all admin pages via the **Admin dropdown** in the dashboard header. ### 1. Environment Configuration -Create `.env` file in project root: +Copy the example file and edit with your values: +```bash +cp .env.example .env +nano .env +``` + +Required environment variables: ```env DB_HOST=10.10.10.50 DB_USER=tinkertickets DB_PASS=your_password DB_NAME=ticketing_system DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... +TIMEZONE=America/New_York ``` ### 2. Cron Jobs diff --git a/api/get_users.php b/api/get_users.php index bc2fe1a..5b4d467 100644 --- a/api/get_users.php +++ b/api/get_users.php @@ -37,6 +37,10 @@ try { $sql = "SELECT user_id, username, display_name FROM users ORDER BY display_name, username"; $result = $conn->query($sql); + if (!$result) { + throw new Exception("Failed to query users"); + } + $users = []; while ($row = $result->fetch_assoc()) { $users[] = [ diff --git a/api/update_ticket.php b/api/update_ticket.php index c0c4ad6..f83eb23 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -204,7 +204,9 @@ try { } // Create ticket URL - $ticketUrl = "http://t.lotusguild.org/ticket/$ticketId"; + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? 't.lotusguild.org'; + $ticketUrl = "{$protocol}://{$host}/ticket/{$ticketId}"; // Determine embed color based on priority $colors = [ diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 975204b..6ac14ef 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -5,6 +5,14 @@ function escapeHtml(text) { return div.innerHTML; } +// Get ticket ID from URL (handles both /ticket/123 and ?id=123 formats) +function getTicketIdFromUrl() { + const pathMatch = window.location.pathname.match(/\/ticket\/(\d+)/); + if (pathMatch) return pathMatch[1]; + const params = new URLSearchParams(window.location.search); + return params.get('id'); +} + /** * Toggle sidebar visibility on desktop */ @@ -32,19 +40,13 @@ document.addEventListener('DOMContentLoaded', function() { // Main initialization document.addEventListener('DOMContentLoaded', function() { - console.log('DOM loaded, initializing dashboard...'); - // Check if we're on the dashboard page const hasTable = document.querySelector('table'); - const isTicketPage = window.location.pathname.includes('/ticket/') || + const isTicketPage = window.location.pathname.includes('/ticket/') || window.location.href.includes('ticket.php') || document.querySelector('.ticket-details') !== null; const isDashboard = hasTable && !isTicketPage; - console.log('Has table:', hasTable); - console.log('Is ticket page:', isTicketPage); - console.log('Is dashboard:', isDashboard); - if (isDashboard) { // Dashboard-specific initialization initStatusFilter(); @@ -324,8 +326,6 @@ function quickSave() { priority: parseInt(prioritySelect.value) }; - console.log('Saving ticket data:', data); - fetch('/api/update_ticket.php', { method: 'POST', headers: { @@ -335,9 +335,7 @@ function quickSave() { body: JSON.stringify(data) }) .then(response => { - console.log('Response status:', response.status); return response.text().then(text => { - console.log('Raw response:', text); try { return JSON.parse(text); } catch (e) { @@ -346,7 +344,6 @@ function quickSave() { }); }) .then(result => { - console.log('Update result:', result); if (result.success) { // Update the hamburger menu display const hamburgerStatus = document.getElementById('hamburger-status'); @@ -365,9 +362,7 @@ function quickSave() { statusDisplay.className = `status-${statusSelect.value}`; statusDisplay.textContent = statusSelect.value; } - - console.log('Ticket updated successfully'); - + // Close hamburger menu after successful save const hamburgerContent = document.querySelector('.hamburger-content'); if (hamburgerContent) { @@ -391,15 +386,8 @@ function quickSave() { function saveTicket() { const editables = document.querySelectorAll('.editable'); const data = {}; - - let ticketId; - if (window.location.href.includes('?id=')) { - ticketId = window.location.href.split('id=')[1]; - } else { - const matches = window.location.pathname.match(/\/ticket\/(\d+)/); - ticketId = matches ? matches[1] : null; - } - + const ticketId = getTicketIdFromUrl(); + editables.forEach(field => { if (field.dataset.field) { data[field.dataset.field] = field.value; @@ -477,8 +465,6 @@ function loadTemplate() { if (template.default_priority) { document.getElementById('priority').value = template.default_priority; } - - console.log('Template loaded:', template.template_name); } else { console.error('Failed to load template:', data.error); toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000); diff --git a/assets/js/ticket.js b/assets/js/ticket.js index a4f7dce..e78c593 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -5,6 +5,18 @@ function escapeHtml(text) { return div.innerHTML; } +// Get ticket ID from URL (handles both /ticket/123 and ?id=123 formats) +function getTicketIdFromUrl() { + // Try new URL format first: /ticket/123456789 + const pathMatch = window.location.pathname.match(/\/ticket\/(\d+)/); + if (pathMatch) { + return pathMatch[1]; + } + // Fall back to query param: ?id=123456789 + const params = new URLSearchParams(window.location.search); + return params.get('id'); +} + /** * Toggle visibility groups field based on visibility selection */ @@ -28,14 +40,7 @@ function saveTicket() { const editables = document.querySelectorAll('.editable'); const data = {}; - // Extract ticket ID from URL (works with both old and new URL formats) - let ticketId; - if (window.location.href.includes('?id=')) { - ticketId = window.location.href.split('id=')[1]; - } else { - const matches = window.location.pathname.match(/\/ticket\/(\d+)/); - ticketId = matches ? matches[1] : null; - } + const ticketId = getTicketIdFromUrl(); if (!ticketId) { console.error('Could not determine ticket ID'); @@ -157,14 +162,7 @@ function addComment() { return; } - // Extract ticket ID from URL (works with both old and new URL formats) - let ticketId; - if (window.location.href.includes('?id=')) { - ticketId = window.location.href.split('id=')[1]; - } else { - const matches = window.location.pathname.match(/\/ticket\/(\d+)/); - ticketId = matches ? matches[1] : null; - } + const ticketId = getTicketIdFromUrl(); if (!ticketId) { console.error('Could not determine ticket ID'); @@ -346,8 +344,6 @@ function handleAssignmentChange() { if (!data.success) { toast.error('Error updating assignment'); console.error(data.error); - } else { - console.log('Assignment updated successfully'); } }) .catch(error => { @@ -386,8 +382,6 @@ function handleMetadataChanges() { toast.error(`Error updating ${fieldName}`); console.error(data.error); } else { - console.log(`${fieldName} updated successfully to:`, newValue); - // Update window.ticketData window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue; @@ -470,17 +464,9 @@ function updateTicketStatus() { // Extract status change logic into reusable function function performStatusChange(statusSelect, selectedOption, newStatus) { - // Extract ticket ID - let ticketId; - if (window.location.href.includes('?id=')) { - ticketId = window.location.href.split('id=')[1]; - } else { - const matches = window.location.pathname.match(/\/ticket\/(\d+)/); - ticketId = matches ? matches[1] : null; - } + const ticketId = getTicketIdFromUrl(); if (!ticketId) { - console.error('Could not determine ticket ID'); statusSelect.selectedIndex = 0; return; } @@ -531,8 +517,6 @@ function performStatusChange(statusSelect, selectedOption, newStatus) { statusSelect.insertBefore(selectedOption, statusSelect.firstChild); statusSelect.selectedIndex = 0; - console.log('Status updated successfully to:', newStatus); - // Reload page to refresh activity timeline setTimeout(() => { window.location.reload(); diff --git a/controllers/TicketController.php b/controllers/TicketController.php index 5a9fab1..e8c9cbf 100644 --- a/controllers/TicketController.php +++ b/controllers/TicketController.php @@ -212,7 +212,9 @@ class TicketController { $webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL']; // Create ticket URL - $ticketUrl = "http://t.lotusguild.org/ticket/$ticketId"; + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? 't.lotusguild.org'; + $ticketUrl = "{$protocol}://{$host}/ticket/{$ticketId}"; // Map priorities to Discord colors $priorityColors = [ diff --git a/create_ticket_api.php b/create_ticket_api.php index 9ca5b51..9c45fd8 100644 --- a/create_ticket_api.php +++ b/create_ticket_api.php @@ -234,12 +234,16 @@ $priorityColors = [ "4" => 6736490 // --priority-4: #66bb6a ]; +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$host = $_SERVER['HTTP_HOST'] ?? 't.lotusguild.org'; +$ticketUrl = "{$protocol}://{$host}/ticket/{$ticket_id}"; + $discord_data = [ "content" => "", "embeds" => [[ "title" => "New Ticket Created: #" . $ticket_id, "description" => $title, - "url" => "http://t.lotusguild.org/ticket/" . $ticket_id, + "url" => $ticketUrl, "color" => $priorityColors[$priority], "fields" => [ ["name" => "Priority", "value" => $priority, "inline" => true],