Add performance, security, and reliability improvements
- Consolidate all 20 API files to use centralized Database helper - Add optimistic locking to ticket updates to prevent concurrent conflicts - Add caching to StatsModel (60s TTL) for dashboard performance - Add health check endpoint (api/health.php) for monitoring - Improve rate limit cleanup with cron script and efficient DirectoryIterator - Enable rate limit response headers (X-RateLimit-*) - Add audit logging for workflow transitions - Log Discord webhook failures instead of silencing - Fix visibility check on export_tickets.php - Add database migration system with performance indexes - Fix cron recurring tickets to use assignTicket method Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,20 +2,29 @@
|
||||
/**
|
||||
* StatsModel - Dashboard statistics and metrics
|
||||
*
|
||||
* Provides various ticket statistics for dashboard widgets
|
||||
* Provides various ticket statistics for dashboard widgets.
|
||||
* Uses caching to reduce database load for frequently accessed stats.
|
||||
*/
|
||||
|
||||
class StatsModel {
|
||||
private $conn;
|
||||
require_once dirname(__DIR__) . '/helpers/CacheHelper.php';
|
||||
|
||||
public function __construct($conn) {
|
||||
class StatsModel {
|
||||
private mysqli $conn;
|
||||
|
||||
/** Cache TTL for dashboard stats in seconds */
|
||||
private const STATS_CACHE_TTL = 60;
|
||||
|
||||
/** Cache prefix for stats */
|
||||
private const CACHE_PREFIX = 'stats';
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of open tickets
|
||||
*/
|
||||
public function getOpenTicketCount() {
|
||||
public function getOpenTicketCount(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE status IN ('Open', 'Pending', 'In Progress')";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -25,7 +34,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get count of closed tickets
|
||||
*/
|
||||
public function getClosedTicketCount() {
|
||||
public function getClosedTicketCount(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE status = 'Closed'";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -35,7 +44,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get tickets grouped by priority
|
||||
*/
|
||||
public function getTicketsByPriority() {
|
||||
public function getTicketsByPriority(): array {
|
||||
$sql = "SELECT priority, COUNT(*) as count FROM tickets WHERE status != 'Closed' GROUP BY priority ORDER BY priority";
|
||||
$result = $this->conn->query($sql);
|
||||
$data = [];
|
||||
@@ -48,7 +57,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get tickets grouped by status
|
||||
*/
|
||||
public function getTicketsByStatus() {
|
||||
public function getTicketsByStatus(): array {
|
||||
$sql = "SELECT status, COUNT(*) as count FROM tickets GROUP BY status ORDER BY FIELD(status, 'Open', 'Pending', 'In Progress', 'Closed')";
|
||||
$result = $this->conn->query($sql);
|
||||
$data = [];
|
||||
@@ -61,7 +70,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get tickets grouped by category
|
||||
*/
|
||||
public function getTicketsByCategory() {
|
||||
public function getTicketsByCategory(): array {
|
||||
$sql = "SELECT category, COUNT(*) as count FROM tickets WHERE status != 'Closed' GROUP BY category ORDER BY count DESC";
|
||||
$result = $this->conn->query($sql);
|
||||
$data = [];
|
||||
@@ -74,7 +83,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get average resolution time in hours
|
||||
*/
|
||||
public function getAverageResolutionTime() {
|
||||
public function getAverageResolutionTime(): float {
|
||||
$sql = "SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) as avg_hours
|
||||
FROM tickets
|
||||
WHERE status = 'Closed'
|
||||
@@ -89,7 +98,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get count of tickets created today
|
||||
*/
|
||||
public function getTicketsCreatedToday() {
|
||||
public function getTicketsCreatedToday(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE DATE(created_at) = CURDATE()";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -99,7 +108,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get count of tickets created this week
|
||||
*/
|
||||
public function getTicketsCreatedThisWeek() {
|
||||
public function getTicketsCreatedThisWeek(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE YEARWEEK(created_at, 1) = YEARWEEK(CURDATE(), 1)";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -109,7 +118,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get count of tickets closed today
|
||||
*/
|
||||
public function getTicketsClosedToday() {
|
||||
public function getTicketsClosedToday(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE status = 'Closed' AND DATE(updated_at) = CURDATE()";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -119,7 +128,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get tickets by assignee (top 5)
|
||||
*/
|
||||
public function getTicketsByAssignee($limit = 5) {
|
||||
public function getTicketsByAssignee(int $limit = 5): array {
|
||||
$sql = "SELECT
|
||||
u.display_name,
|
||||
u.username,
|
||||
@@ -146,7 +155,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get unassigned ticket count
|
||||
*/
|
||||
public function getUnassignedTicketCount() {
|
||||
public function getUnassignedTicketCount(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE assigned_to IS NULL AND status != 'Closed'";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -156,7 +165,7 @@ class StatsModel {
|
||||
/**
|
||||
* Get critical (P1) ticket count
|
||||
*/
|
||||
public function getCriticalTicketCount() {
|
||||
public function getCriticalTicketCount(): int {
|
||||
$sql = "SELECT COUNT(*) as count FROM tickets WHERE priority = 1 AND status != 'Closed'";
|
||||
$result = $this->conn->query($sql);
|
||||
$row = $result->fetch_assoc();
|
||||
@@ -165,8 +174,35 @@ class StatsModel {
|
||||
|
||||
/**
|
||||
* Get all stats as a single array
|
||||
*
|
||||
* Uses caching to reduce database load. Stats are cached for STATS_CACHE_TTL seconds.
|
||||
*
|
||||
* @param bool $forceRefresh Force a cache refresh
|
||||
* @return array All dashboard statistics
|
||||
*/
|
||||
public function getAllStats() {
|
||||
public function getAllStats(bool $forceRefresh = false): array {
|
||||
$cacheKey = 'dashboard_all';
|
||||
|
||||
if ($forceRefresh) {
|
||||
CacheHelper::delete(self::CACHE_PREFIX, $cacheKey);
|
||||
}
|
||||
|
||||
return CacheHelper::remember(
|
||||
self::CACHE_PREFIX,
|
||||
$cacheKey,
|
||||
function() {
|
||||
return $this->fetchAllStats();
|
||||
},
|
||||
self::STATS_CACHE_TTL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all stats from database (uncached)
|
||||
*
|
||||
* @return array All dashboard statistics
|
||||
*/
|
||||
private function fetchAllStats(): array {
|
||||
return [
|
||||
'open_tickets' => $this->getOpenTicketCount(),
|
||||
'closed_tickets' => $this->getClosedTicketCount(),
|
||||
@@ -182,5 +218,13 @@ class StatsModel {
|
||||
'by_assignee' => $this->getTicketsByAssignee()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cached stats
|
||||
*
|
||||
* Call this method when ticket data changes to ensure fresh stats.
|
||||
*/
|
||||
public function invalidateCache(): void {
|
||||
CacheHelper::delete(self::CACHE_PREFIX, null);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user