diff --git a/bin/cron.php b/bin/cron.php index d4cbc24..90bc135 100755 --- a/bin/cron.php +++ b/bin/cron.php @@ -8,6 +8,14 @@ use ICT\Core\Service; use ICT\Core\Task; +use ICT\Core\Corelog; +use ICT\Core\User; +use ICT\Core\Tenant; +use ICT\Core\Transmission; +use ICT\Core\Message\Document; +use ICT\Core\Spool; +use ICT\Core\Password_Policy; +use ICT\Core\DB; // default include is /usr/ictcore/core chdir(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'core'); @@ -16,20 +24,106 @@ function cron_process() { - // process all pending retries - Task::process_all(); - - // execute reload method for all available services - // it will restart gateway in case there are new configurations - $listService = Service::load_all(); - foreach ($listService as $oService) { - $oService->config_update(); - } - - // execute email fetch script - // nothing special we just need to include it for execution - include_once('../bin/sendmail/gateway.php'); + // ----------------------------------------------- + // OLD ORIGINAL FUNCTIONS (UNTOUCHED) + // ----------------------------------------------- + + // process all pending retries + Task::process_all(); + + // counting user password expiration + $Password_Policy = new Password_Policy(); + $Password_Policy->password_exp_limit(); + + // reload method for all services + $listService = Service::load_all(); + foreach ($listService as $oService) { + $oService->config_update(); + } + + // execute email fetch script + include_once('../bin/sendmail/gateway.php'); + + date_default_timezone_set('UTC'); + $newDateTime = date('h:i A'); + if ($newDateTime == '12:00 AM') { + + Corelog::log('Reseting tenant limits at: ' . date('r'), Corelog::INFO); + + $listUser = User::search(); + foreach ($listUser as $aUser) { + $oUser = new User($aUser['user_id']); + $oUser->reset_daily_sent(); + if (date('d') == 1) { + $oUser->reset_monthly_sent(); + } + } + } + + // ----------------------------------------------- + // ⭐ ADDED: schedule.fax.cron.php functions + // ----------------------------------------------- + + Corelog::log("=== FAX CRON STARTED ===", Corelog::INFO); + Corelog::log("TIME: " . date('Y-m-d H:i:s'), Corelog::INFO); + + $currentTime = time(); + + $query = " + SELECT transmission_id + FROM transmission + WHERE status = '" . Transmission::STATUS_PENDING . "' + AND (schedule_time IS NULL OR schedule_time <= $currentTime) + ORDER BY transmission_id DESC + LIMIT 1 + "; + + $results = DB::query('transmission', $query); + + if (empty($results)) { + Corelog::log("No pending transmissions at " . date('Y-m-d H:i:s'), Corelog::INFO); + return; + } + + foreach ($results as $row) { + try { + $transmission = new Transmission($row['transmission_id']); + + if (!isset($transmission->transmission_id)) { + Corelog::log("Invalid transmission object — skipping", Corelog::WARNING); + continue; + } + + if (empty($transmission->contact_id)) { + Corelog::log("Contact missing — calling send() anyway", Corelog::WARNING); + } + + $success = $transmission->send(); + + // if ($success) { + // $transmission->status = Transmission::STATUS_COMPLETED; + // $transmission->response = "Sent successfully"; + // } else { + // $transmission->status = Transmission::STATUS_FAILED; + // $transmission->response = "Send failed"; + // } + + $transmission->last_run = time(); + + if (method_exists($transmission, 'save')) { + $transmission->save(); + } else { + Corelog::log("Save method missing — cannot update", Corelog::WARNING); + } + + } catch (Exception $e) { + Corelog::log("Error: " . $e->getMessage(), Corelog::ERROR); + } + } + + Corelog::log("=== FAX CRON FINISHED ===", Corelog::INFO); } +// run cron cron_process(); exit(); diff --git a/core/Api/KpiApi.php b/core/Api/KpiApi.php new file mode 100644 index 0000000..8e98f7c --- /dev/null +++ b/core/Api/KpiApi.php @@ -0,0 +1,359 @@ +computeRangeTimestamps($range, $startParam, $endParam); + + // build periods array: each period has label, start_ts, end_ts, key + $periods = $this->buildPeriodsForRange($range, $startTs, $endTs); + + // ✅ Dynamic groupBy depending on range + if ($range === 'monthly' || $range === 'daily' || $range === 'weekly') { + $groupBy = "DATE(FROM_UNIXTIME(date_created))";} + elseif ($range === 'yearly') { + $groupBy = "DATE_FORMAT(FROM_UNIXTIME(date_created), '%Y-%m')"; + } else { + $groupBy = "DATE(FROM_UNIXTIME(date_created))"; + } + + // ✅ SQL with flexible groupBy + $sql = "SELECT + {$groupBy} as period_key, + SUM(CASE WHEN direction='outbound' THEN 1 ELSE 0 END) as sent, + SUM(CASE WHEN direction='inbound' THEN 1 ELSE 0 END) as received, + SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as success, + SUM(CASE WHEN status IN ('failed','failed(dnc)','no_provider') THEN 1 ELSE 0 END) as failed + FROM transmission + WHERE is_deleted = 0 + AND date_created BETWEEN {$startTs} AND {$endTs} + GROUP BY period_key + ORDER BY period_key ASC"; + + $result = DB::query('transmission', $sql); + + // Map DB rows into proper keys depending on range + $map = []; + while ($row = mysqli_fetch_assoc($result)) { + $key = $row['period_key']; // Already formatted correctly by SQL + + if (!isset($map[$key])) { + $map[$key] = ['sent'=>0, 'received'=>0, 'success'=>0, 'failed'=>0]; + } + + $map[$key]['sent'] += (int)$row['sent']; + $map[$key]['received'] += (int)$row['received']; + $map[$key]['success'] += (int)$row['success']; + $map[$key]['failed'] += (int)$row['failed']; + } + + // Prepare final arrays + $labels = []; + $sentArr = []; + $receivedArr = []; + $successArr = []; + $failedArr = []; + $totals = ['sent'=>0,'received'=>0,'success'=>0,'failed'=>0]; + + foreach ($periods as $p) { + $labels[] = $p['label']; + $mapKey = $p['key']; // match against SQL period_key + + $data = $map[$mapKey] ?? ['sent'=>0,'received'=>0,'success'=>0,'failed'=>0]; + + $sentArr[] = $data['sent']; + $receivedArr[] = $data['received']; + $successArr[] = $data['success']; + $failedArr[] = $data['failed']; + + $totals['sent'] += $data['sent']; + $totals['received'] += $data['received']; + $totals['success'] += $data['success']; + $totals['failed'] += $data['failed']; + } + + $totalAll = $totals['sent'] + $totals['received']; + $successRate = $totalAll > 0 ? round(($totals['success'] / $totalAll) * 100, 2) : 0; + $failureRate = $totalAll > 0 ? round(($totals['failed'] / $totalAll) * 100, 2) : 0; + + return [ + 'success' => true, + 'range' => $range, + 'labels' => $labels, + 'sent' => $sentArr, + 'received' => $receivedArr, + 'success' => $successArr, + 'failed' => $failedArr, + 'totals' => array_merge($totals, [ + 'success_rate' => $successRate, + 'failure_rate' => $failureRate + ]), + 'periods_meta' => array_map(function($p) { + return ['label'=>$p['label'],'start'=>$p['start'],'end'=>$p['end'],'key'=>$p['key']]; + }, $periods) + ]; + + } catch (\Exception $e) { + error_log("getFaxStatsV2 error: ".$e->getMessage()); + return ['success'=>false,'error'=>$e->getMessage()]; + } +} + + + +/** + * @url GET /users/kpi/fax-details + * Params: + * key = period_key (required) -- this is the 'key' returned in periods_meta (format: YYYY-MM-DD or YYYY-MM) + * status = completed|failed|all (optional, default all) + * direction = outbound|inbound|all (optional) + * limit, offset (optional) + * + * Returns list of transmissions for that period and filter. + */ +public function getFaxDetails($query = []) +{ + try { + if (empty($query['key'])) { + return ['success'=>false,'error'=>'period key required']; + } + $periodKey = $query['key']; + $status = $query['status'] ?? 'all'; + $direction = $query['direction'] ?? 'all'; + $limit = isset($query['limit']) ? (int)$query['limit'] : 200; + $offset = isset($query['offset']) ? (int)$query['offset'] : 0; + + // periodKey format: YYYY-MM-DD (day) or YYYY-MM (month) or Week key like YYYY-MM-DD__W + // We'll detect YYYY-MM-DD -> day range; YYYY-MM -> full month; YYYY -> year + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $periodKey)) { + $start = strtotime($periodKey . ' 00:00:00'); + $end = strtotime($periodKey . ' 23:59:59'); + } elseif (preg_match('/^\d{4}-\d{2}$/', $periodKey)) { + $start = strtotime($periodKey . '-01 00:00:00'); + $end = strtotime(date('Y-m-t', $start) . ' 23:59:59'); // t = last day of month + } elseif (preg_match('/^\d{4}$/', $periodKey)) { + $start = strtotime($periodKey . '-01-01 00:00:00'); + $end = strtotime($periodKey . '-12-31 23:59:59'); + } else { + // fallback: if we get a custom key that contains start__end + if (strpos($periodKey, '__') !== false) { + list($s, $e) = explode('__', $periodKey, 2); + $start = (int)$s; + $end = (int)$e; + } else { + return ['success'=>false,'error'=>'invalid period key format']; + } + } + + $wheres = []; + $wheres[] = "is_deleted = 0"; + $wheres[] = "date_created BETWEEN {$start} AND {$end}"; + + if ($status === 'completed') { + $wheres[] = "status = 'completed'"; + } elseif ($status === 'failed') { + $wheres[] = "status IN ('failed','failed(dnc)','no_provider')"; + } // else 'all' -> no filter + + if ($direction === 'outbound') { + $wheres[] = "direction = 'outbound'"; + } elseif ($direction === 'inbound') { + $wheres[] = "direction = 'inbound'"; + } + + $whereSql = implode(' AND ', $wheres); + + $sql = "SELECT transmission_id, contact_id, direction, status, response, date_created, pages, file_path + FROM transmission + WHERE {$whereSql} + ORDER BY date_created DESC + LIMIT {$offset}, {$limit}"; + + $res = DB::query('transmission', $sql); + + $rows = []; + while ($r = mysqli_fetch_assoc($res)) { + $rows[] = [ + 'id' => $r['transmission_id'], + 'contact_id' => $r['contact_id'], + 'direction' => $r['direction'], + 'status' => $r['status'], + 'response' => $r['response'], + 'date_created' => (int)$r['date_created'], + 'pages' => (int)$r['pages'], + 'file' => $r['file_path'] + ]; + } + + // also return totals for this period + $countSql = "SELECT + SUM(CASE WHEN direction='outbound' THEN 1 ELSE 0 END) as sent, + SUM(CASE WHEN direction='inbound' THEN 1 ELSE 0 END) as received, + SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as success, + SUM(CASE WHEN status IN ('failed','failed(dnc)','no_provider') THEN 1 ELSE 0 END) as failed + FROM transmission + WHERE {$whereSql}"; + $countRes = DB::query('transmission', $countSql); + $tot = mysqli_fetch_assoc($countRes); + + return [ + 'success' => true, + 'period_key' => $periodKey, + 'range_start' => $start, + 'range_end' => $end, + 'totals' => [ + 'sent' => (int)$tot['sent'], + 'received' => (int)$tot['received'], + 'success' => (int)$tot['success'], + 'failed' => (int)$tot['failed'] + ], + 'rows' => $rows + ]; + + } catch (\Exception $e) { + error_log("getFaxDetails error: ".$e->getMessage()); + return ['success'=>false,'error'=>$e->getMessage()]; + } +} + +/** + * Helper: compute start & end timestamps based on range + */ +private function computeRangeTimestamps($range, $startParam = null, $endParam = null) +{ + $now = time(); + if ($range === 'custom' && $startParam && $endParam) { + $start = strtotime($startParam . ' 00:00:00'); + $end = strtotime($endParam . ' 23:59:59'); + return [$start, $end]; + } + + switch ($range) { + case 'today': + $start = strtotime('today 00:00:00'); + $end = strtotime('today 23:59:59'); + break; + case 'weekly': + // if user passed a start anchor (YYYY-MM-DD), use that week (7 days from that date) + if ($startParam) { + $anchor = strtotime($startParam . ' 00:00:00'); + $start = strtotime(date('Y-m-d', $anchor) . ' 00:00:00'); + $end = strtotime('+6 days', $start) + 86399; + } else { + // last 7 days including today + $start = strtotime('today -6 days 00:00:00'); + $end = strtotime('today 23:59:59'); + } + break; + case 'monthly': + if ($startParam) { + $start = strtotime(date('Y-m-01', strtotime($startParam)) . ' 00:00:00'); + } else { + $start = strtotime(date('Y-m-01', $now) . ' 00:00:00'); + } + $end = strtotime(date('Y-m-t', $start) . ' 23:59:59'); + break; + case 'yearly': + if ($startParam && preg_match('/^\d{4}$/', $startParam)) { + $year = $startParam; + } else { + $year = date('Y', $now); + } + $start = strtotime($year . '-01-01 00:00:00'); + $end = strtotime($year . '-12-31 23:59:59'); + break; + default: // daily (last N days) + // default show last 7 days + $start = strtotime('today -6 days 00:00:00'); + $end = strtotime('today 23:59:59'); + break; + } + + return [$start, $end]; +} + +/** + * Helper: build periods array for range. + * Each period: ['label'=>..., 'start'=>ts, 'end'=>ts, 'map_key'=>YYYY-MM-DD, 'key'=>period_key] + */ +private function buildPeriodsForRange($range, $startTs, $endTs) +{ + $periods = []; + + if ($range === 'yearly') { + // months Jan..Dec + $year = date('Y', $startTs); + for ($m = 1; $m <= 12; $m++) { + $s = strtotime("$year-$m-01 00:00:00"); + $e = strtotime(date('Y-m-t', $s) . ' 23:59:59'); + $periods[] = [ + 'label' => date('M', $s), + 'start' => $s, + 'end' => $e, + 'map_key' => date('Y-m', $s), + 'key' => date('Y-m', $s) + ]; + } + } elseif ($range === 'monthly') { + // build *daily* buckets for the given month + $cur = strtotime(date('Y-m-01 00:00:00', $startTs)); + $endDay = strtotime(date('Y-m-t 00:00:00', $startTs)); + while ($cur <= $endDay) { + $s = $cur; + $e = $cur + 86399; + $label = date('d M', $cur); // e.g., 01 Sep, 02 Sep + $periods[] = [ + 'label' => $label, + 'start' => $s, + 'end' => $e, + 'map_key' => date('Y-m-d', $cur), // daily key + 'key' => date('Y-m-d', $cur) + ]; + $cur = strtotime('+1 day', $cur); + } + } else { + // daily/weekly/custom + $cur = strtotime(date('Y-m-d 00:00:00', $startTs)); + $endDay = strtotime(date('Y-m-d 00:00:00', $endTs)); + while ($cur <= $endDay) { + $s = $cur; + $e = $cur + 86399; + $label = date('Y-m-d', $cur); + $periods[] = [ + 'label' => $label, + 'start' => $s, + 'end' => $e, + 'map_key' => date('Y-m-d', $cur), + 'key' => date('Y-m-d', $cur) + ]; + $cur = strtotime('+1 day', $cur); + } + } + + return $periods; +} + + + +} \ No newline at end of file diff --git a/core/Api/ServerHealthApi.php b/core/Api/ServerHealthApi.php new file mode 100644 index 0000000..e4229c0 --- /dev/null +++ b/core/Api/ServerHealthApi.php @@ -0,0 +1,126 @@ + "success", + "servers" => $healthData, + "total_servers" => count($healthData), + "message" => "Server health data retrieved successfully" + ]; + } catch (\Throwable $e) { + throw new CoreException( + 500, + 'Failed to retrieve server health: ' . $e->getMessage() + ); + } + } + + + /** + * Check and update health for ALL servers/nodes + * + * @url POST /users/health/check + */ + public function check_all_servers_health() + { + + + try { + $results = ServerHealth::check_all_nodes_health(); + + return [ + "status" => "success", + "servers_checked" => count($results), + "results" => $results, + "message" => "Health check completed for all servers" + ]; + } catch (\Throwable $e) { + throw new CoreException( + 500, + 'Failed to Fetch Servers ', + $e + ); + } + } + + /** + * Get health history for a specific server + * + * @url GET /users/server/$node_id/health/history + */ + public function get_server_health_history($node_id, $query = array()) + { + + + try { + $filter = array_merge((array)$query, ['node_id' => $node_id]); + $history = ServerHealth::search($filter); + + return [ + "status" => "success", + "node_id" => $node_id, + "history" => $history, + "total_records" => count($history), + "message" => "Health history retrieved successfully" + ]; + } catch (\Throwable $e) { + throw new CoreException( + 500, + 'Failed to load Serverhealth history', + $e + ); + } + } + + + + /** + * @url GET /users/kpi/fax-stats-v2 + * Params: range=daily|weekly|monthly|yearly|custom + * start=YYYY-MM-DD (optional for custom or for anchoring) + * end=YYYY-MM-DD (optional for custom) + */ +public function getFaxStatsV2($query = array()) +{ + try { + return ServerHealth::faxStatsV2($query); + } catch (\Throwable $e) { + throw new CoreException( + 500, + 'Failed to load fax KPI stats', + $e + ); + } +} + + + +} + diff --git a/core/Api/TransmissionApi.php b/core/Api/TransmissionApi.php index 1d8f4ee..47d37fd 100755 --- a/core/Api/TransmissionApi.php +++ b/core/Api/TransmissionApi.php @@ -89,6 +89,13 @@ public function create($data = array()) $oTransmission = $oProgram->transmission_create($contact_id, $account_id, $direction); $this->set($oTransmission, $data); +if (!empty($data['scheduled']) && !empty($data['scheduled_time'])) { + $oTransmission->schedule_time = strtotime($data['scheduled_time']); // convert to UNIX timestamp + $oTransmission->status = Transmission::STATUS_PENDING; // scheduled + $oTransmission->last_run = $oTransmission->schedule_time; // match last_run +} + + if ($oTransmission->save()) { return $oTransmission->transmission_id; } else { diff --git a/core/ServerHealth.php b/core/ServerHealth.php new file mode 100644 index 0000000..7a05cc2 --- /dev/null +++ b/core/ServerHealth.php @@ -0,0 +1,515 @@ + 0) { + $this->id = $id; + $this->load(); + } + } + + public static function search($aFilter = array()) + { + $aHealth = array(); + $from_str = self::$table; + $aWhere = array(); + + foreach ($aFilter as $search_field => $search_value) { + switch ($search_field) { + case 'id': + case 'node_id': + $aWhere[] = "$search_field = $search_value"; + break; + case 'server': + case 'status': + $aWhere[] = "$search_field LIKE '%$search_value%'"; + break; + case 'before': + $aWhere[] = "checked_at <= '$search_value'"; + break; + case 'after': + $aWhere[] = "checked_at >= '$search_value'"; + break; + } + } + + if (!empty($aWhere)) { + $from_str .= ' WHERE ' . implode(' AND ', $aWhere); + } + + $query = "SELECT * FROM " . $from_str . " ORDER BY checked_at DESC"; + Corelog::log("Server health search with $query", Corelog::DEBUG, array('aFilter' => $aFilter)); + + $result = DB::query(self::$table, $query); + while ($data = mysqli_fetch_assoc($result)) { + $aHealth[] = $data; + } + + return $aHealth; + } + + private function load() + { + $query = "SELECT * FROM " . self::$table . " WHERE id='%id%'"; + $result = DB::query(self::$table, $query, array('id' => $this->id)); + $data = mysqli_fetch_assoc($result); + + if ($data) { + $this->id = $data['id']; + $this->node_id = $data['node_id']; + $this->server = $data['server']; + $this->cpu = $data['cpu']; + $this->ram = $data['ram']; + $this->disk = $data['disk']; + $this->status = $data['status']; + $this->checked_at = $data['checked_at']; + + Corelog::log("Server health loaded: $this->id", Corelog::CRUD); + } else { + throw new CoreException('404', 'Server health record not found'); + } + } + + public function delete() + { + Corelog::log("Server health delete: $this->id", Corelog::CRUD); + return DB::delete(self::$table, 'id', $this->id); + } + + public function save() +{ + $data = array( + 'id' => $this->id, + 'node_id' => $this->node_id, + 'server' => $this->server, + 'cpu' => $this->cpu, + 'ram' => $this->ram, + 'disk' => $this->disk, + 'status' => $this->status, + 'checked_at' => gmdate('Y-m-d H:i:s') + ); + + // Check if record already exists for this node_id + $query = "SELECT id FROM " . self::$table . " WHERE node_id = '{$this->node_id}' LIMIT 1"; + $result = DB::query(self::$table, $query); + + if ($row = mysqli_fetch_assoc($result)) { + // Update existing row + $this->id = $row['id']; + $data['id'] = $this->id; + $result = DB::update(self::$table, $data, 'id'); + Corelog::log("Server health updated: $this->id (node_id {$this->node_id})", Corelog::CRUD); + } else { + // Insert new row + $result = DB::update(self::$table, $data, false); + $this->id = $data['id']; + Corelog::log("New server health record created: $this->id", Corelog::CRUD); + } + + return $result; +} + + + /** + * Get all active nodes from the node table + */ + public static function get_active_nodes() + { + $query = "SELECT node_id, name, api_host, api_port, api_user, api_pass, + ssh_host, ssh_port, ssh_user, ssh_pass + FROM node + WHERE active = 1"; + + $result = DB::query('node', $query); + $nodes = array(); + + while ($data = mysqli_fetch_assoc($result)) { + $nodes[] = $data; + } + + return $nodes; + } + + /** + * Check health of all nodes/servers + */ + public static function check_all_nodes_health() + { + $nodes = self::get_active_nodes(); + $results = array(); + + foreach ($nodes as $node) { + if ($node['ssh_host'] === 'localhost' || $node['ssh_host'] === '127.0.0.1') { + // Local server - use direct system calls + $healthData = self::check_local_health($node); + } else { + // Remote server - use SSH + $healthData = self::check_remote_health($node); + } + + // Save to database + $oHealth = new ServerHealth(); + $oHealth->node_id = $node['node_id']; + $oHealth->server = $node['name']; + $oHealth->cpu = $healthData['cpu']; + $oHealth->ram = $healthData['ram']; + $oHealth->disk = $healthData['disk']; + $oHealth->status = $healthData['status']; + +if (isset($data['id']) && !empty($data['id'])) { + $result = DB::update(self::$table, $data, 'id'); +} else { + $result = DB::update(self::$table, $data, false); // inserts new row +} + + $oHealth->save(); + + $results[] = $healthData; + } + + return $results; + } + + /** + * Check health of local server + */ + private static function check_local_health($node) + { + $healthData = array( + 'node_id' => $node['node_id'], + 'server' => $node['name'], + 'status' => 'online', + 'cpu' => 'N/A', + 'ram' => 'N/A', + 'disk' => 'N/A' + ); + + try { + // CPU usage + $cpuLoad = sys_getloadavg(); + $cpu = round($cpuLoad[0], 2); + $healthData['cpu'] = $cpu . " %"; + + // RAM usage + $free = shell_exec('free -m'); + if ($free) { + $free = explode("\n", trim($free)); + $mem = explode(" ", preg_replace("!\s+!", " ", $free[1])); + $ramTotal = $mem[1] ?? 0; + $ramUsed = $mem[2] ?? 0; + $ramUsage = $ramTotal > 0 ? round(($ramUsed / $ramTotal) * 100, 2) : 0; + $healthData['ram'] = "{$ramUsed} MB / {$ramTotal} MB ({$ramUsage}%)"; + } + + // Disk usage + $diskTotal = round(disk_total_space("/") / 1024 / 1024 / 1024, 2); + $diskFree = round(disk_free_space("/") / 1024 / 1024 / 1024, 2); + $diskUsed = $diskTotal - $diskFree; + $diskUsage = $diskTotal > 0 ? round(($diskUsed / $diskTotal) * 100, 2) : 0; + $healthData['disk'] = "{$diskUsed} GB / {$diskTotal} GB ({$diskUsage}%)"; + + } catch (\Exception $e) { + $healthData['status'] = 'error'; + Corelog::log("Local health check failed: " . $e->getMessage(), Corelog::ERROR); + } + + return $healthData; + } + + /** + * Check health of remote server via SSH + */ + private static function check_remote_health($node) + { + $healthData = array( + 'node_id' => $node['node_id'], + 'server' => $node['name'], + 'status' => 'offline', + 'cpu' => 'N/A', + 'ram' => 'N/A', + 'disk' => 'N/A' + ); + + try { + $ssh_host = $node['ssh_host']; + $ssh_port = $node['ssh_port'] ?? 22; + $ssh_user = $node['ssh_user']; + $ssh_pass = $node['ssh_pass']; + + // Create SSH connection + $connection = @ssh2_connect($ssh_host, $ssh_port); + if ($connection && @ssh2_auth_password($connection, $ssh_user, $ssh_pass)) { + $healthData['status'] = 'online'; + + // Get CPU load + $stream = ssh2_exec($connection, "cat /proc/loadavg | awk '{print \$1}'"); + stream_set_blocking($stream, true); + $cpuLoad = stream_get_contents($stream); + $healthData['cpu'] = trim($cpuLoad) . " %"; + + // Get RAM usage + $stream = ssh2_exec($connection, "free -m | awk 'NR==2{printf \"%s MB / %s MB (%.2f%%)\", \$3, \$2, \$3*100/\$2}'"); + stream_set_blocking($stream, true); + $ramUsage = stream_get_contents($stream); + $healthData['ram'] = trim($ramUsage); + + // Get Disk usage + $stream = ssh2_exec($connection, "df -h / | awk 'NR==2{printf \"%s / %s (%s)\", \$3, \$2, \$5}'"); + stream_set_blocking($stream, true); + $diskUsage = stream_get_contents($stream); + $healthData['disk'] = trim($diskUsage); + + ssh2_disconnect($connection); + } + } catch (\Exception $e) { + Corelog::log("SSH connection failed for node {$node['node_id']}: " . $e->getMessage(), Corelog::ERROR); + } + + return $healthData; + } + + /** + * Get latest health for all servers + */ + public static function get_all_servers_health() + { + $query = "SELECT sh.*, n.name as node_name, n.ssh_host + FROM server_health sh + INNER JOIN node n ON sh.node_id = n.node_id + WHERE sh.checked_at = ( + SELECT MAX(checked_at) + FROM server_health + WHERE node_id = sh.node_id + ) + ORDER BY n.name"; + + $result = DB::query(self::$table, $query); + $healthData = array(); + + while ($data = mysqli_fetch_assoc($result)) { + $healthData[] = $data; + } + + return $healthData; + } + + + + /** + * Main KPI handler + */ + public static function faxStatsV2(array $query) + { + $range = $query['range'] ?? 'daily'; + $startParam = $query['start'] ?? null; + $endParam = $query['end'] ?? null; + + list($startTs, $endTs) = self::computeRangeTimestamps( + $range, + $startParam, + $endParam + ); + + $periods = self::buildPeriodsForRange( + $range, + $startTs, + $endTs + ); + + $rows = self::fetchTransmissionStats( + $range, + $startTs, + $endTs + ); + + return self::formatChartResponse( + $range, + $periods, + $rows + ); + } + + /** + * Fetch grouped transmission stats from DB + */ + private static function fetchTransmissionStats($range, $startTs, $endTs) + { + if ($range === 'yearly') { + $groupBy = "DATE_FORMAT(FROM_UNIXTIME(date_created), '%Y-%m')"; + } else { + $groupBy = "DATE(FROM_UNIXTIME(date_created))"; + } + + $sql = " + SELECT + {$groupBy} AS period_key, + SUM(CASE WHEN direction='outbound' THEN 1 ELSE 0 END) AS sent, + SUM(CASE WHEN direction='inbound' THEN 1 ELSE 0 END) AS received, + SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) AS success, + SUM(CASE WHEN status IN ('failed','failed(dnc)','no_provider') THEN 1 ELSE 0 END) AS failed + FROM transmission + WHERE is_deleted = 0 + AND date_created BETWEEN {$startTs} AND {$endTs} + GROUP BY period_key + ORDER BY period_key ASC + "; + + $res = DB::query('transmission', $sql); + $map = []; + + while ($r = mysqli_fetch_assoc($res)) { + $map[$r['period_key']] = [ + 'sent' => (int)$r['sent'], + 'received' => (int)$r['received'], + 'success' => (int)$r['success'], + 'failed' => (int)$r['failed'] + ]; + } + + return $map; + } + + /** + * Build final chart response + */ + private static function formatChartResponse($range, $periods, $map) + { + $labels = $sent = $received = $success = $failed = []; + $totals = ['sent'=>0,'received'=>0,'success'=>0,'failed'=>0]; + + foreach ($periods as $p) { + $labels[] = $p['label']; + $data = $map[$p['key']] ?? ['sent'=>0,'received'=>0,'success'=>0,'failed'=>0]; + + foreach ($data as $k => $v) { + $totals[$k] += $v; + } + + $sent[] = $data['sent']; + $received[] = $data['received']; + $success[] = $data['success']; + $failed[] = $data['failed']; + } + + $totalAll = $totals['sent'] + $totals['received']; + + return [ + 'ok' => true, + 'range' => $range, + 'labels' => $labels, + 'sent' => $sent, + 'received' => $received, + 'success' => $success, + 'failed' => $failed, + 'totals' => array_merge($totals, [ + 'success_rate' => $totalAll ? round(($totals['success']/$totalAll)*100,2) : 0, + 'failure_rate' => $totalAll ? round(($totals['failed']/$totalAll)*100,2) : 0 + ]), + 'periods_meta' => $periods +]; + } + + /** + * Date range resolver + */ + private static function computeRangeTimestamps($range, $start, $end) + { + if ($range === 'custom' && $start && $end) { + return [ + strtotime("$start 00:00:00"), + strtotime("$end 23:59:59") + ]; + } + + switch ($range) { + case 'monthly': + $s = strtotime(date('Y-m-01')); + $e = strtotime(date('Y-m-t')) + 86399; + break; + case 'yearly': + $y = date('Y'); + $s = strtotime("$y-01-01"); + $e = strtotime("$y-12-31 23:59:59"); + break; + default: + $s = strtotime('today -6 days'); + $e = strtotime('today 23:59:59'); + } + + return [$s, $e]; + } + + /** + * Build periods array + */ + private static function buildPeriodsForRange($range, $start, $end) + { + $periods = []; + + if ($range === 'yearly') { + $year = date('Y', $start); + for ($m=1;$m<=12;$m++) { + $key = sprintf('%04d-%02d', $year, $m); + $periods[] = [ + 'label' => date('M', strtotime("$key-01")), + 'key' => $key + ]; + } + return $periods; + } + + for ($d=$start; $d<=$end; $d+=86400) { + $periods[] = [ + 'label' => date('Y-m-d', $d), + 'key' => date('Y-m-d', $d) + ]; + } + + return $periods; + } + + + + + + +} \ No newline at end of file diff --git a/core/Transmission.php b/core/Transmission.php index d941ec2..d13145f 100644 --- a/core/Transmission.php +++ b/core/Transmission.php @@ -22,10 +22,10 @@ class Transmission const STATUS_PROCESSING = 'processing'; const STATUS_DONE = 'done'; // when transmission is done, and we don't know if it was completed or failed const STATUS_COMPLETED = 'completed'; + const STATUS_FAILED_PROVIDER = 'no rpovider'; const STATUS_FAILED = 'failed'; const FAILED_STATUS = 'failed(dnc)'; const STATUS_INVALID = 'invalid'; - const FAILED_STATUS = 'failed(dnc)'; const INTERNAL = 'internal'; // currently not in use const INBOUND = 'inbound'; const OUTBOUND = 'outbound'; @@ -65,6 +65,7 @@ class Transmission 'try_allowed', 'try_done', 'last_run', + 'schedule_time', 'is_deleted', 'campaign_id' ); @@ -146,6 +147,15 @@ class Transmission */ public $last_run = NULL; + + + /** + * @property integer $schedule_time + * @var integer + */ + public $schedule_time = NULL; + + /** * @property-read integer $is_deleted * 0 = no deleted, 1 = deleted @@ -213,8 +223,8 @@ public static function search($aFilter = array()) $from_str .= ' LEFT JOIN ' . self::$table_account . ' a ON t.account_id=a.account_id'; $from_str .= ' LEFT JOIN ' . self::$table_contact . ' c ON t.contact_id=c.contact_id'; $from_str .= ' LEFT JOIN ' . self::$table_user . ' u ON t.created_by=u.usr_id'; - $from_str .= ' LEFT JOIN ' . self::$table_spool . ' sp ON sp.transmission_id = t.transmission_id '; - + //$from_str .= ' LEFT JOIN ' . self::$table_spool . ' sp ON sp.transmission_id = t.transmission_id '; + $from_str .= ' LEFT JOIN ( SELECT s1.* FROM ' . self::$table_spool . ' s1 INNER JOIN (SELECT transmission_id, MAX(spool_id) AS max_id FROM spool GROUP BY transmission_id) s2 ON s1.spool_id = s2.max_id) sp ON sp.transmission_id = t.transmission_id'; $from_str_in = $from_str; $from_str .= ' LEFT JOIN ' . self::$table_program_resource . ' pr ON t.program_id = pr.program_id '; @@ -470,6 +480,7 @@ public function save() 'try_done' => $this->try_done, 'last_run' => $this->last_run, 'is_deleted' => $this->is_deleted, + 'schedule_time' => $this->schedule_time, 'campaign_id' => $this->campaign_id );