[FEAT] 實作機台智慧連線監控與儀表板告警同步優化

1. [Machine Model] 實作智慧抖動偵測邏輯 (Jitter Detection),判定 1 小時內斷線 3 次以上為不穩定機台。
2. [Machine Model] 優化 calculated_status,機台在線時自動忽略歷史斷線警告,並修正 scopeHasAnyAlert 排除連線類告警。
3. [ProcessStatus Job] 實作自動消警機制 (Auto-resolve),機台恢復連線時自動處理舊的斷線告警。
4. [Dashboard] 同步儀表板「待處理告警」統計,確保與機台列表狀態一致。
5. [UI] 於機台列表與儀表板同步新增琥珀色閃電圖示 () 與不穩定提示 Tooltip。
6. [i18n] 新增連線不穩定相關之繁體中文翻譯詞條。
This commit is contained in:
sky121113 2026-05-14 16:20:59 +08:00
parent 61b5e422ee
commit 3d1ddd5b91
6 changed files with 292 additions and 85 deletions

View File

@ -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();
// 交易營收統計 (真實數據)

View File

@ -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);

View File

@ -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);

View File

@ -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": "保固結束",

View File

@ -193,6 +193,7 @@
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Sub-machine Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Signal') }}</th>
@ -217,25 +218,63 @@
</td>
<td class="px-6 py-6 text-center">
@php $cStatus = $machine->calculated_status; @endphp
@if($cStatus === 'online')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
<div class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
<div class="flex flex-col items-center gap-0.5">
@if($cStatus === 'online')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 tooltip"
title="{{ $machine->is_unstable ? __('Connection is unstable (disconnected :count times in the last hour)', ['count' => $machine->recent_disconnection_count]) : __('Machine connection is normal') }}">
<div class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</div>
<span class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{ __('Online') }}</span>
@if($machine->is_unstable)
<div class="ml-1 text-amber-500 animate-pulse">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
@endif
</div>
<span class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{ __('Online') }}</span>
@elseif($cStatus === 'warning')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20">
<div class="h-2 w-2 rounded-full bg-amber-500 animate-pulse"></div>
<span class="text-[10px] font-black text-amber-600 dark:text-amber-400 tracking-widest uppercase">{{ __('Warning') }}</span>
</div>
@elseif($cStatus === 'error')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
<span class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{ __('Error') }}</span>
</div>
@else
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
<span class="text-[10px] font-black text-slate-600 dark:text-slate-400 tracking-widest uppercase">{{ __('Offline') }}</span>
</div>
@endif
@if($machine->latest_status_log_time && in_array($cStatus, ['error', 'warning']))
<span class="text-[9px] font-bold text-slate-400">{{ $machine->latest_status_log_time->diffForHumans() }}</span>
@endif
</div>
@elseif($cStatus === 'error')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
<span class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{ __('Error') }}</span>
</td>
<td class="px-6 py-6 text-center">
@php $hStatus = $machine->hardware_status; @endphp
<div class="flex flex-col items-center gap-0.5">
<div class="flex items-center justify-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full {{
$hStatus === 'error' ? 'bg-rose-500 animate-pulse' :
($hStatus === 'warning' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500/50')
}}"></span>
<span class="text-[10px] font-black {{
$hStatus === 'error' ? 'text-rose-500' :
($hStatus === 'warning' ? 'text-amber-500' : 'text-slate-400')
}} uppercase tracking-widest">
{{ $hStatus === 'error' ? __('Error') : ($hStatus === 'warning' ? __('Warning') : __('Normal')) }}
</span>
</div>
@if($machine->latest_hardware_log_time && $hStatus !== 'normal')
<span class="text-[9px] font-bold text-slate-400">{{ $machine->latest_hardware_log_time->diffForHumans() }}</span>
@endif
</div>
@else
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
<span class="text-[10px] font-black text-slate-600 dark:text-slate-400 tracking-widest uppercase">{{ __('Offline') }}</span>
</div>
@endif
</td>
<td class="px-6 py-6 text-center">
<span class="text-base font-extrabold text-slate-900 dark:text-slate-100">$ {{ number_format($machine->today_sales_sum ?? 0, 0) }}</span>
@ -309,6 +348,13 @@
@php $cStatus = $machine->calculated_status; @endphp
<div class="flex items-center">
@if($cStatus === 'online')
@if($machine->is_unstable)
<div class="text-amber-500 animate-pulse mr-1">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
@endif
<div class="flex h-2.5 w-2.5 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
@ -329,15 +375,40 @@
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Status') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 capitalize">
@if($cStatus === 'online')
<span class="text-emerald-500">{{ __('Online') }}</span>
@elseif($cStatus === 'error')
<span class="text-rose-500">{{ __('Error') }}</span>
@else
<span class="text-slate-400">{{ __('Offline') }}</span>
<div class="flex flex-col">
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 capitalize">
@if($cStatus === 'online')
<span class="text-emerald-500">{{ __('Online') }}</span>
@elseif($cStatus === 'warning')
<span class="text-amber-500">{{ __('Warning') }}</span>
@elseif($cStatus === 'error')
<span class="text-rose-500">{{ __('Error') }}</span>
@else
<span class="text-slate-400">{{ __('Offline') }}</span>
@endif
</p>
@if($machine->latest_status_log_time && in_array($cStatus, ['error', 'warning']))
<span class="text-[9px] font-bold text-slate-400">{{ $machine->latest_status_log_time->diffForHumans() }}</span>
@endif
</p>
</div>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Sub-machine Status') }}</p>
@php $hStatus = $machine->hardware_status; @endphp
<div class="flex flex-col">
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 capitalize">
@if($hStatus === 'error')
<span class="text-rose-500">{{ __('Error') }}</span>
@elseif($hStatus === 'warning')
<span class="text-amber-500">{{ __('Warning') }}</span>
@else
<span class="text-emerald-500">{{ __('Normal') }}</span>
@endif
</p>
@if($machine->latest_hardware_log_time && $hStatus !== 'normal')
<span class="text-[9px] font-bold text-slate-400">{{ $machine->latest_hardware_log_time->diffForHumans() }}</span>
@endif
</div>
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Last Signal') }}</p>

View File

@ -230,9 +230,6 @@
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Sub / Card / Scan') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Last Page') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('APP Version') }}</th>
@ -284,7 +281,7 @@
@if($cStatus === 'online')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 tooltip mx-auto w-fit"
title="{{ __('Machine is heartbeat normal') }}">
title="{{ $machine->is_unstable ? __('Connection is unstable (disconnected :count times in the last hour)', ['count' => $machine->recent_disconnection_count]) : __('Machine connection is normal') }}">
<div class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
@ -293,22 +290,40 @@
<span
class="text-xs font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
__('Online') }}</span>
@if($machine->is_unstable)
<div class="ml-1 text-amber-500 animate-pulse">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
@endif
</div>
@elseif($cStatus === 'warning')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 tooltip mx-auto w-fit"
title="{{ __('Recently reported warnings in logs') }}">
<div class="h-2 w-2 rounded-full bg-amber-500 animate-pulse"></div>
<span
class="text-xs font-black text-amber-600 dark:text-amber-400 tracking-widest uppercase">{{
__('Warning') }}</span>
<div class="flex flex-col items-center gap-0.5 px-3 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 tooltip mx-auto w-fit"
title="{{ __('Connection warnings') }}">
<div class="flex items-center gap-1.5">
<div class="h-2 w-2 rounded-full bg-amber-500 animate-pulse"></div>
<span
class="text-xs font-black text-amber-600 dark:text-amber-400 tracking-widest uppercase">{{
__('Warning') }}</span>
</div>
@if($machine->latest_status_log_time)
<span class="text-[9px] font-bold text-amber-500/80 block leading-none">{{ $machine->latest_status_log_time->diffForHumans() }}</span>
@endif
</div>
@elseif($cStatus === 'error')
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20 tooltip mx-auto w-fit"
title="{{ __('Recently reported errors or warnings in logs') }}">
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
<span
class="text-xs font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
__('Error') }}</span>
<div class="flex flex-col items-center gap-0.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20 tooltip mx-auto w-fit"
title="{{ __('Connection or system errors') }}">
<div class="flex items-center gap-1.5">
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
<span
class="text-xs font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
__('Error') }}</span>
</div>
@if($machine->latest_status_log_time)
<span class="text-[9px] font-bold text-rose-500/80 block leading-none">{{ $machine->latest_status_log_time->diffForHumans() }}</span>
@endif
</div>
@else
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20 tooltip mx-auto w-fit"
@ -325,27 +340,40 @@
$machine->temperature ?? '--' }}°C</span>
</td>
<td class="px-6 py-6 text-center">
@php
$hStatus = $machine->hardware_status;
@endphp
<div class="flex items-center justify-center gap-4">
<div class="flex items-center gap-1.5">
{{-- 下位機 --}}
<div class="flex items-center gap-1.5 tooltip" title="{{ __('Sub-machine Status') }}">
<span class="w-1.5 h-1.5 rounded-full {{
$hStatus === 'error' ? 'bg-rose-500 animate-pulse' :
($hStatus === 'warning' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500/50')
}}"></span>
<span class="text-xs font-black {{
$hStatus === 'error' ? 'text-rose-500' :
($hStatus === 'warning' ? 'text-amber-500' : 'text-slate-400')
}} uppercase tracking-widest">
{{ $hStatus === 'error' ? __('Error') : ($hStatus === 'warning' ? __('Warning') : __('Normal')) }}
@if($machine->latest_hardware_log_time && $hStatus !== 'normal')
<span class="text-[9px] lowercase opacity-70 block -mt-0.5 ml-3 font-bold">{{ $machine->latest_hardware_log_time->diffForHumans() }}</span>
@endif
</span>
</div>
{{-- 刷卡機 --}}
<div class="flex items-center gap-1.5 opacity-50">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
__('Normal') }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
__('Normal') }}</span>
</div>
<div class="flex items-center gap-1.5">
{{-- 掃碼機 --}}
<div class="flex items-center gap-1.5 opacity-50">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
__('Normal') }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 font-bold text-slate-600 dark:text-slate-400 text-center">
{{ $machine->current_page_label ?: '-' }}
</td>
<td
class="px-6 py-6 font-mono text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest text-center">
{{ $machine->firmware_version ?: '-' }}
@ -454,13 +482,6 @@
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{
$machine->firmware_version ?: '-' }}</p>
</div>
<div>
<p
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('Last Page') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{
$machine->current_page_label ?: '-' }}</p>
</div>
<div>
<p
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
@ -469,6 +490,9 @@
<x-status-badge
:status="$cStatus === 'online' ? 'online' : ($cStatus === 'error' ? 'error' : 'offline')"
size="sm" />
@if($machine->latest_status_log_time && in_array($cStatus, ['error', 'warning']))
<p class="text-[9px] font-bold text-slate-400 mt-1">{{ $machine->latest_status_log_time->diffForHumans() }}</p>
@endif
</div>
<div class="col-span-2">
<p