diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index d55ac9c..144cace 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -20,7 +20,7 @@ class DashboardController extends Controller // 從資料庫獲取真實統計數據 $activeMachines = Machine::online()->count(); $offlineMachines = Machine::offline()->count(); - $alertsPending = Machine::hasError()->count(); + $alertsPending = Machine::hasAnyAlert()->count(); $memberCount = \App\Models\Member\Member::count(); // 交易營收統計 (真實數據) diff --git a/app/Jobs/Machine/ProcessStatus.php b/app/Jobs/Machine/ProcessStatus.php index f82d1fc..3a7baea 100644 --- a/app/Jobs/Machine/ProcessStatus.php +++ b/app/Jobs/Machine/ProcessStatus.php @@ -52,6 +52,14 @@ class ProcessStatus implements ShouldQueue $updateData = ['status' => $status]; if ($status === 'online') { $updateData['last_heartbeat_at'] = now(); + + // 自動消警:當機台恢復連線時,自動標記之前的「斷線 (LWT)」警告為已解決 + $machine->logs() + ->where('type', 'status') + ->where('level', 'warning') + ->where('message', 'Connection lost (LWT)') + ->where('is_resolved', false) + ->update(['is_resolved' => true]); } $machine->update($updateData); diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index f5949dd..a6b4a85 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -92,10 +92,10 @@ class Machine extends Model 'cash_module_enabled', ]; - protected $appends = ['image_urls', 'calculated_status']; + protected $appends = ['image_urls', 'calculated_status', 'hardware_status', 'latest_status_log_time', 'latest_hardware_log_time', 'is_unstable', 'recent_disconnection_count']; /** - * 動態計算機台當前狀態 + * 動態計算機台當前狀態 (僅限連線與系統層級) * 優先讀取資料庫 status 欄位 * 若在線,則進一步檢查過去 15 分鐘是否為異常 */ @@ -106,29 +106,92 @@ class Machine extends Model return 'offline'; } - // 判定異常 (檢查過去 15 分鐘內是否有 未解決的 error 日誌) + // 判定異常 (僅檢查 type = 'status' 的未解決日誌) $hasRecentErrors = $this->logs() + ->where('type', 'status') ->where('level', 'error') ->where('is_resolved', false) - ->where('created_at', '>=', now()->subMinutes(15)) ->exists(); if ($hasRecentErrors) { return 'error'; } - // 判定警告 (檢查過去 15 分鐘內是否有 未解決的 warning 日誌) + // 判定警告 (僅檢查 type = 'status' 的未解決日誌) + // 優化:當機台在線時,自動忽略「斷線 (LWT)」警告,因為狀態已恢復 $hasRecentWarnings = $this->logs() + ->where('type', 'status') ->where('level', 'warning') ->where('is_resolved', false) - ->where('created_at', '>=', now()->subMinutes(15)) + ->where('message', '!=', 'Connection lost (LWT)') ->exists(); if ($hasRecentWarnings) { return 'warning'; } - return 'online'; + return $this->status ?? 'online'; + } + + /** + * 下位機/硬體健康狀態 (基於 type = 'submachine' 日誌) + */ + public function getHardwareStatusAttribute(): string + { + // 判定異常 + $hasErrors = $this->logs() + ->where('type', 'submachine') + ->where('level', 'error') + ->where('is_resolved', false) + ->exists(); + + if ($hasErrors) { + return 'error'; + } + + // 判定警告 + $hasWarnings = $this->logs() + ->where('type', 'submachine') + ->where('level', 'warning') + ->where('is_resolved', false) + ->exists(); + + if ($hasWarnings) { + return 'warning'; + } + + return 'normal'; + } + + /** + * 取得最新一筆未解決的系統狀態日誌時間 + */ + public function getLatestStatusLogTimeAttribute() + { + $query = $this->logs() + ->where('type', 'status') + ->whereIn('level', ['error', 'warning']) + ->where('is_resolved', false); + + // 優化:若在線,不顯示已恢復的斷線時間 + if ($this->status === 'online') { + $query->where('message', '!=', 'Connection lost (LWT)'); + } + + return $query->latest()->first()?->created_at; + } + + /** + * 取得最新一筆未解決的下位機硬體日誌時間 + */ + public function getLatestHardwareLogTimeAttribute() + { + return $this->logs() + ->where('type', 'submachine') + ->whereIn('level', ['error', 'warning']) + ->where('is_resolved', false) + ->latest() + ->first()?->created_at; } /** @@ -147,33 +210,57 @@ class Machine extends Model return $query->where('status', 'offline'); } - /** - * Scope: 判定異常 (過去 15 分鐘內有錯誤或警告日誌) - */ public function scopeHasError($query) { - return $query->whereExists(function ($q) { - $q->select(\Illuminate\Support\Facades\DB::raw(1)) - ->from('machine_logs') - ->whereColumn('machine_logs.machine_id', 'machines.id') - ->whereIn('level', ['error', 'warning']) - ->where('is_resolved', false) - ->where('created_at', '>=', now()->subMinutes(15)); + return $query->whereHas('logs', function ($q) { + $q->where('is_resolved', false) + ->where('level', 'error'); }); } /** - * Scope: 判定運行中 (在線且無近期異常) + * Scope: 判定是否有任何待處理的異常 (排除掉單純的連線中斷) + * 待處理告警應專注於「功能性異常」(如硬體錯誤、APP 報錯),而非連線狀態。 + */ + public function scopeHasAnyAlert($query) + { + return $query->whereHas('logs', function ($q) { + $q->where(function($sub) { + $sub->where('is_resolved', false) + ->orWhereNull('is_resolved'); + }) + ->whereIn('level', ['error', 'warning']) + ->where('message', '!=', 'Connection lost (LWT)'); + }); + } + + public function scopeHasStatusError($query) + { + return $query->whereHas('logs', function ($q) { + $q->where('is_resolved', false) + ->where('type', 'status') + ->where('level', 'error'); + }); + } + + public function scopeHasHardwareError($query) + { + return $query->whereHas('logs', function ($q) { + $q->where('is_resolved', false) + ->where('type', 'submachine') + ->where('level', 'error'); + }); + } + + /** + * Scope: 判定運行中 (在線且無近期系統異常) */ public function scopeRunning($query) { - return $query->online()->whereNotExists(function ($q) { - $q->select(\Illuminate\Support\Facades\DB::raw(1)) - ->from('machine_logs') - ->whereColumn('machine_logs.machine_id', 'machines.id') - ->whereIn('level', ['error', 'warning']) - ->where('is_resolved', false) - ->where('created_at', '>=', now()->subMinutes(15)); + return $query->online()->whereDoesntHave('logs', function ($q) { + $q->where('is_resolved', false) + ->where('type', 'status') + ->whereIn('level', ['error', 'warning']); }); } @@ -283,6 +370,21 @@ class Machine extends Model return __($label); } + public function getIsUnstableAttribute(): bool + { + // 抖動偵測:1 小時內斷線次數 >= 3 + return $this->recent_disconnection_count >= 3; + } + + public function getRecentDisconnectionCountAttribute(): int + { + return $this->logs() + ->where('type', 'status') + ->where('message', 'Connection lost (LWT)') + ->where('created_at', '>=', now()->subHour()) + ->count(); + } + public function users() { return $this->belongsToMany(\App\Models\System\User::class); diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 6a92804..8cb70f0 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -303,6 +303,7 @@ "Connected": "通訊中", "Connecting...": "連線中", "Connection error": "連線錯誤", + "Connection is unstable (disconnected :count times in the last hour)": "連線不穩定(過去 1 小時內中斷 :count 次)", "Connection lost (LWT)": "連線中斷", "Connection restored": "連線恢復", "Connectivity Status": "連線狀態概況", @@ -775,7 +776,6 @@ "Loading Data": "載入資料中", "Loading failed": "載入失敗", "Loading machines...": "正在載入機台...", - "Loading...": "載入中...", "Location": "位置", "Location found!": "已成功獲取座標!", "Location not found": "找不到位置,您可以嘗試使用地標名稱(例如:花蓮縣政府)", @@ -1043,6 +1043,7 @@ "Normal": "正常", "Not Used": "不使用", "Not Used Description": "不使用第三方支付介接", + "Normal": "正常", "Note": "備註", "Note (optional)": "備註 (選填)", "Notes": "備註", @@ -1580,6 +1581,7 @@ "Sub Account Roles": "子帳號角色", "Sub Accounts": "子帳號", "Sub-actions": "子項目", + "Sub-machine Status": "下位機狀態", "Sub-machine Status Request": "下位機狀態回傳", "Submit Record": "提交紀錄", "Subtotal": "小計", @@ -1806,7 +1808,7 @@ "Warehouse to warehouse transfers": "倉庫對倉庫調撥", "Warehouse updated successfully": "倉庫更新成功", "Warehouse updated successfully.": "倉庫已成功更新。", - "Warning": "即將過期", + "Warning": "警告", "Warning: You are editing your own role!": "警告:您正在編輯目前使用的角色!", "Warranty": "保固", "Warranty End": "保固結束", diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index c03c76c..909a60d 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -193,6 +193,7 @@ {{ __('Machine Info') }} {{ __('Running Status') }} + {{ __('Sub-machine Status') }} {{ __('Today Cumulative Sales') }} {{ __('Current Stock') }} {{ __('Last Signal') }} @@ -217,25 +218,63 @@ @php $cStatus = $machine->calculated_status; @endphp - @if($cStatus === 'online') -
-
- - +
+ @if($cStatus === 'online') +
+
+ + +
+ {{ __('Online') }} + @if($machine->is_unstable) +
+ + + +
+ @endif
- {{ __('Online') }} + @elseif($cStatus === 'warning') +
+
+ {{ __('Warning') }} +
+ @elseif($cStatus === 'error') +
+
+ {{ __('Error') }} +
+ @else +
+
+ {{ __('Offline') }} +
+ @endif + @if($machine->latest_status_log_time && in_array($cStatus, ['error', 'warning'])) + {{ $machine->latest_status_log_time->diffForHumans() }} + @endif
- @elseif($cStatus === 'error') -
-
- {{ __('Error') }} + + + @php $hStatus = $machine->hardware_status; @endphp +
+
+ + + {{ $hStatus === 'error' ? __('Error') : ($hStatus === 'warning' ? __('Warning') : __('Normal')) }} + +
+ @if($machine->latest_hardware_log_time && $hStatus !== 'normal') + {{ $machine->latest_hardware_log_time->diffForHumans() }} + @endif
- @else -
-
- {{ __('Offline') }} -
- @endif $ {{ number_format($machine->today_sales_sum ?? 0, 0) }} @@ -309,6 +348,13 @@ @php $cStatus = $machine->calculated_status; @endphp
@if($cStatus === 'online') + @if($machine->is_unstable) +
+ + + +
+ @endif
@@ -329,15 +375,40 @@

{{ __('Status') }}

-

- @if($cStatus === 'online') - {{ __('Online') }} - @elseif($cStatus === 'error') - {{ __('Error') }} - @else - {{ __('Offline') }} +

+

+ @if($cStatus === 'online') + {{ __('Online') }} + @elseif($cStatus === 'warning') + {{ __('Warning') }} + @elseif($cStatus === 'error') + {{ __('Error') }} + @else + {{ __('Offline') }} + @endif +

+ @if($machine->latest_status_log_time && in_array($cStatus, ['error', 'warning'])) + {{ $machine->latest_status_log_time->diffForHumans() }} @endif -

+
+
+
+

{{ __('Sub-machine Status') }}

+ @php $hStatus = $machine->hardware_status; @endphp +
+

+ @if($hStatus === 'error') + {{ __('Error') }} + @elseif($hStatus === 'warning') + {{ __('Warning') }} + @else + {{ __('Normal') }} + @endif +

+ @if($machine->latest_hardware_log_time && $hStatus !== 'normal') + {{ $machine->latest_hardware_log_time->diffForHumans() }} + @endif +

{{ __('Last Signal') }}

diff --git a/resources/views/admin/machines/index.blade.php b/resources/views/admin/machines/index.blade.php index 6f287e2..311cf32 100644 --- a/resources/views/admin/machines/index.blade.php +++ b/resources/views/admin/machines/index.blade.php @@ -230,9 +230,6 @@ {{ __('Sub / Card / Scan') }} - - {{ __('Last Page') }} {{ __('APP Version') }} @@ -284,7 +281,7 @@ @if($cStatus === 'online')
+ title="{{ $machine->is_unstable ? __('Connection is unstable (disconnected :count times in the last hour)', ['count' => $machine->recent_disconnection_count]) : __('Machine connection is normal') }}">
@@ -293,22 +290,40 @@ {{ __('Online') }} + + @if($machine->is_unstable) +
+ + + +
+ @endif
@elseif($cStatus === 'warning') -
-
- {{ - __('Warning') }} +
+
+
+ {{ + __('Warning') }} +
+ @if($machine->latest_status_log_time) + {{ $machine->latest_status_log_time->diffForHumans() }} + @endif
@elseif($cStatus === 'error') -
-
- {{ - __('Error') }} +
+
+
+ {{ + __('Error') }} +
+ @if($machine->latest_status_log_time) + {{ $machine->latest_status_log_time->diffForHumans() }} + @endif
@else
temperature ?? '--' }}°C + @php + $hStatus = $machine->hardware_status; + @endphp
-
+ {{-- 下位機 --}} +
+ + + {{ $hStatus === 'error' ? __('Error') : ($hStatus === 'warning' ? __('Warning') : __('Normal')) }} + @if($machine->latest_hardware_log_time && $hStatus !== 'normal') + {{ $machine->latest_hardware_log_time->diffForHumans() }} + @endif + +
+ {{-- 刷卡機 --}} +
{{ __('Normal') }}
-
- - {{ - __('Normal') }} -
-
+ {{-- 掃碼機 --}} +
{{ __('Normal') }}
- - {{ $machine->current_page_label ?: '-' }} - {{ $machine->firmware_version ?: '-' }} @@ -454,13 +482,6 @@

{{ $machine->firmware_version ?: '-' }}

-
-

- {{ __('Last Page') }}

-

{{ - $machine->current_page_label ?: '-' }}

-

@@ -469,6 +490,9 @@ + @if($machine->latest_status_log_time && in_array($cStatus, ['error', 'warning'])) +

{{ $machine->latest_status_log_time->diffForHumans() }}

+ @endif