refactor: Code cleanup and documentation updates

Bug fixes:
- Fix ticket ID extraction using URLSearchParams instead of split()
- Add error handling for query result in get_users.php
- Make Discord webhook URLs dynamic (use HTTP_HOST)

Code cleanup:
- Remove debug console.log statements from dashboard.js and ticket.js
- Add getTicketIdFromUrl() helper function to both JS files

Documentation:
- Update Claude.md: fix web server (nginx not Apache), add new notes
- Update README.md: add keyboard shortcuts, update setup instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 22:01:20 -05:00
parent 6e569c8918
commit 11a593a7dd
8 changed files with 71 additions and 66 deletions

View File

@@ -23,14 +23,15 @@ Tinker Tickets is a feature-rich, self-hosted ticket management system built for
- Backend: PHP 7.4+ with MySQLi - Backend: PHP 7.4+ with MySQLi
- Frontend: Vanilla JavaScript, CSS3 - Frontend: Vanilla JavaScript, CSS3
- Database: MariaDB on separate LXC (10.10.10.50) - 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 - Authentication: Authelia SSO with LLDAP backend
**Production Environment:** **Production Environment:**
- **Primary URL**: http://t.lotusguild.org - **Primary URL**: https://t.lotusguild.org
- **Web Server**: Apache at 10.10.10.45 (`/root/code/tinker_tickets`) - **Web Server**: nginx at 10.10.10.45 (`/var/www/html/tinkertickets`)
- **Database**: MariaDB at 10.10.10.50 (`ticketing_system` database) - **Database**: MariaDB at 10.10.10.50 (`ticketing_system` database)
- **Authentication**: Authelia provides SSO via headers - **Authentication**: Authelia provides SSO via headers
- **Dev Environment**: `/root/code/tinker_tickets` (not production)
## Architecture ## Architecture
@@ -105,7 +106,8 @@ Controllers → Models → Database
│ ├── UserPreferencesModel.php # User preferences │ ├── UserPreferencesModel.php # User preferences
│ └── WorkflowModel.php # Status transition workflows │ └── WorkflowModel.php # Status transition workflows
├── scripts/ ├── 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 ├── uploads/ # File attachment storage
├── views/ ├── views/
│ ├── admin/ │ ├── 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 7. **Cache busting**: Use `?v=YYYYMMDD` query params on JS/CSS files
8. **Ticket linking**: Use `#123456789` in markdown-enabled comments 8. **Ticket linking**: Use `#123456789` in markdown-enabled comments
9. **User groups**: Stored in `users.groups` as comma-separated values 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 ## File Reference Quick Guide

View File

@@ -88,8 +88,18 @@ Access all admin pages via the **Admin dropdown** in the dashboard header.
| `/admin/api-keys` | Generate and manage API keys | | `/admin/api-keys` | Generate and manage API keys |
### Notifications ### 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 - **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 ### Security Features
- **CSRF Protection**: Token-based protection on all forms - **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 ### 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 ```env
DB_HOST=10.10.10.50 DB_HOST=10.10.10.50
DB_USER=tinkertickets DB_USER=tinkertickets
DB_PASS=your_password DB_PASS=your_password
DB_NAME=ticketing_system DB_NAME=ticketing_system
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
TIMEZONE=America/New_York
``` ```
### 2. Cron Jobs ### 2. Cron Jobs

View File

@@ -37,6 +37,10 @@ try {
$sql = "SELECT user_id, username, display_name FROM users ORDER BY display_name, username"; $sql = "SELECT user_id, username, display_name FROM users ORDER BY display_name, username";
$result = $conn->query($sql); $result = $conn->query($sql);
if (!$result) {
throw new Exception("Failed to query users");
}
$users = []; $users = [];
while ($row = $result->fetch_assoc()) { while ($row = $result->fetch_assoc()) {
$users[] = [ $users[] = [

View File

@@ -204,7 +204,9 @@ try {
} }
// Create ticket 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}";
// Determine embed color based on priority // Determine embed color based on priority
$colors = [ $colors = [

View File

@@ -5,6 +5,14 @@ function escapeHtml(text) {
return div.innerHTML; 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 * Toggle sidebar visibility on desktop
*/ */
@@ -32,19 +40,13 @@ document.addEventListener('DOMContentLoaded', function() {
// Main initialization // Main initialization
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded, initializing dashboard...');
// Check if we're on the dashboard page // Check if we're on the dashboard page
const hasTable = document.querySelector('table'); 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') || window.location.href.includes('ticket.php') ||
document.querySelector('.ticket-details') !== null; document.querySelector('.ticket-details') !== null;
const isDashboard = hasTable && !isTicketPage; const isDashboard = hasTable && !isTicketPage;
console.log('Has table:', hasTable);
console.log('Is ticket page:', isTicketPage);
console.log('Is dashboard:', isDashboard);
if (isDashboard) { if (isDashboard) {
// Dashboard-specific initialization // Dashboard-specific initialization
initStatusFilter(); initStatusFilter();
@@ -324,8 +326,6 @@ function quickSave() {
priority: parseInt(prioritySelect.value) priority: parseInt(prioritySelect.value)
}; };
console.log('Saving ticket data:', data);
fetch('/api/update_ticket.php', { fetch('/api/update_ticket.php', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -335,9 +335,7 @@ function quickSave() {
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
.then(response => { .then(response => {
console.log('Response status:', response.status);
return response.text().then(text => { return response.text().then(text => {
console.log('Raw response:', text);
try { try {
return JSON.parse(text); return JSON.parse(text);
} catch (e) { } catch (e) {
@@ -346,7 +344,6 @@ function quickSave() {
}); });
}) })
.then(result => { .then(result => {
console.log('Update result:', result);
if (result.success) { if (result.success) {
// Update the hamburger menu display // Update the hamburger menu display
const hamburgerStatus = document.getElementById('hamburger-status'); const hamburgerStatus = document.getElementById('hamburger-status');
@@ -365,9 +362,7 @@ function quickSave() {
statusDisplay.className = `status-${statusSelect.value}`; statusDisplay.className = `status-${statusSelect.value}`;
statusDisplay.textContent = statusSelect.value; statusDisplay.textContent = statusSelect.value;
} }
console.log('Ticket updated successfully');
// Close hamburger menu after successful save // Close hamburger menu after successful save
const hamburgerContent = document.querySelector('.hamburger-content'); const hamburgerContent = document.querySelector('.hamburger-content');
if (hamburgerContent) { if (hamburgerContent) {
@@ -391,15 +386,8 @@ function quickSave() {
function saveTicket() { function saveTicket() {
const editables = document.querySelectorAll('.editable'); const editables = document.querySelectorAll('.editable');
const data = {}; const data = {};
const ticketId = getTicketIdFromUrl();
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;
}
editables.forEach(field => { editables.forEach(field => {
if (field.dataset.field) { if (field.dataset.field) {
data[field.dataset.field] = field.value; data[field.dataset.field] = field.value;
@@ -477,8 +465,6 @@ function loadTemplate() {
if (template.default_priority) { if (template.default_priority) {
document.getElementById('priority').value = template.default_priority; document.getElementById('priority').value = template.default_priority;
} }
console.log('Template loaded:', template.template_name);
} else { } else {
console.error('Failed to load template:', data.error); console.error('Failed to load template:', data.error);
toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000); toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);

View File

@@ -5,6 +5,18 @@ function escapeHtml(text) {
return div.innerHTML; 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 * Toggle visibility groups field based on visibility selection
*/ */
@@ -28,14 +40,7 @@ function saveTicket() {
const editables = document.querySelectorAll('.editable'); const editables = document.querySelectorAll('.editable');
const data = {}; const data = {};
// Extract ticket ID from URL (works with both old and new URL formats) const ticketId = getTicketIdFromUrl();
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;
}
if (!ticketId) { if (!ticketId) {
console.error('Could not determine ticket ID'); console.error('Could not determine ticket ID');
@@ -157,14 +162,7 @@ function addComment() {
return; return;
} }
// Extract ticket ID from URL (works with both old and new URL formats) const ticketId = getTicketIdFromUrl();
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;
}
if (!ticketId) { if (!ticketId) {
console.error('Could not determine ticket ID'); console.error('Could not determine ticket ID');
@@ -346,8 +344,6 @@ function handleAssignmentChange() {
if (!data.success) { if (!data.success) {
toast.error('Error updating assignment'); toast.error('Error updating assignment');
console.error(data.error); console.error(data.error);
} else {
console.log('Assignment updated successfully');
} }
}) })
.catch(error => { .catch(error => {
@@ -386,8 +382,6 @@ function handleMetadataChanges() {
toast.error(`Error updating ${fieldName}`); toast.error(`Error updating ${fieldName}`);
console.error(data.error); console.error(data.error);
} else { } else {
console.log(`${fieldName} updated successfully to:`, newValue);
// Update window.ticketData // Update window.ticketData
window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue; window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue;
@@ -470,17 +464,9 @@ function updateTicketStatus() {
// Extract status change logic into reusable function // Extract status change logic into reusable function
function performStatusChange(statusSelect, selectedOption, newStatus) { function performStatusChange(statusSelect, selectedOption, newStatus) {
// Extract ticket ID const ticketId = getTicketIdFromUrl();
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;
}
if (!ticketId) { if (!ticketId) {
console.error('Could not determine ticket ID');
statusSelect.selectedIndex = 0; statusSelect.selectedIndex = 0;
return; return;
} }
@@ -531,8 +517,6 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
statusSelect.insertBefore(selectedOption, statusSelect.firstChild); statusSelect.insertBefore(selectedOption, statusSelect.firstChild);
statusSelect.selectedIndex = 0; statusSelect.selectedIndex = 0;
console.log('Status updated successfully to:', newStatus);
// Reload page to refresh activity timeline // Reload page to refresh activity timeline
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();

View File

@@ -212,7 +212,9 @@ class TicketController {
$webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL']; $webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL'];
// Create ticket 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 // Map priorities to Discord colors
$priorityColors = [ $priorityColors = [

View File

@@ -234,12 +234,16 @@ $priorityColors = [
"4" => 6736490 // --priority-4: #66bb6a "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 = [ $discord_data = [
"content" => "", "content" => "",
"embeds" => [[ "embeds" => [[
"title" => "New Ticket Created: #" . $ticket_id, "title" => "New Ticket Created: #" . $ticket_id,
"description" => $title, "description" => $title,
"url" => "http://t.lotusguild.org/ticket/" . $ticket_id, "url" => $ticketUrl,
"color" => $priorityColors[$priority], "color" => $priorityColors[$priority],
"fields" => [ "fields" => [
["name" => "Priority", "value" => $priority, "inline" => true], ["name" => "Priority", "value" => $priority, "inline" => true],