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:
14
Claude.md
14
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
|
- 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
|
||||||
|
|
||||||
|
|||||||
21
README.md
21
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 |
|
| `/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
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user