[FEAT] 新增 OTA 預約排程與部署狀態展示,並優化版本排序與 UI
1. 建立 `ota_schedules` 資料表與 `OtaSchedule` Model,提供 OTA 預約排程基礎儲存結構。 2. 建立 `ota:process-schedules` 背景 Artisan Command 並註冊至 Console Kernel,每分鐘自動執行到期的預約並透過 MQTT 發送。 3. 擴充 `ApkVersionController` index 方法:加入加載列表時自動觸發排程的防呆機制,並在記憶體中快速統計 pending 與 completed 的機台。 4. 擴充 `ApkVersionController` store 與 push 方法,移除已用不到的 `is_forced` (更新模式) 限制,並支援 `scheduled_at` 預約時間的寫入。 5. 調整 `create.blade.php` 頁面,移除「強制更新」的 Switch 欄位,僅保留注意事項防錯提示。 6. 調整 `index.blade.php` 頁面,將「更新模式」改為「部署狀態」一欄,整合系統內建的 `.tooltip` 與原生 `title` 屬性,在加載與刷新時 100% 穩定展示 Hover 機台名單。 7. 優化 `index.blade.php` 彈窗,將原生時間輸入框改為 Flatpickr 樣式,加入日曆圖標並支援動態 locale 語言切換(中/英/日),將標籤統一命名為「選擇更新時間」。 8. 同步更新並對齊中、英、日三語系 `lang/*.json` 語系檔,並完成字母排序與反逸出斜線處理。
This commit is contained in:
parent
d9bf2bbe98
commit
fb1bcf49e1
119
app/Console/Commands/Machine/ProcessOtaSchedules.php
Normal file
119
app/Console/Commands/Machine/ProcessOtaSchedules.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Machine;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Machine\OtaSchedule;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\RemoteCommand;
|
||||
use App\Services\Machine\MqttService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessOtaSchedules extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ota:process-schedules';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process scheduled OTA firmware updates and send MQTT commands to target machines';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(MqttService $mqttService)
|
||||
{
|
||||
$this->info('[' . now()->toDateTimeString() . '] Starting to process scheduled OTA updates...');
|
||||
|
||||
// 撈取狀態為待發送且時間已到的排程
|
||||
$schedules = OtaSchedule::where('status', 'pending')
|
||||
->where('scheduled_at', '<=', now())
|
||||
->with('apkVersion')
|
||||
->get();
|
||||
|
||||
if ($schedules->isEmpty()) {
|
||||
$this->info('No pending OTA schedules to process.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info("Found {$schedules->count()} pending OTA schedule(s) to process.");
|
||||
|
||||
foreach ($schedules as $schedule) {
|
||||
$this->info("Processing OTA Schedule ID: {$schedule->id} (APK: {$schedule->apkVersion->version_name})");
|
||||
|
||||
// 標記為處理中,防止重複執行
|
||||
$schedule->update(['status' => 'processing']);
|
||||
$apkVersion = $schedule->apkVersion;
|
||||
|
||||
try {
|
||||
// 根據 target_type 撈取目標機台
|
||||
if ($schedule->target_type === 'all') {
|
||||
$machines = Machine::all();
|
||||
} else {
|
||||
$machineIds = $schedule->target_value ?? [];
|
||||
$machines = Machine::whereIn('id', $machineIds)->get();
|
||||
}
|
||||
|
||||
if ($machines->isEmpty()) {
|
||||
$this->warn("No target machines found for OTA Schedule ID: {$schedule->id}. Skipping.");
|
||||
$schedule->update(['status' => 'completed']);
|
||||
continue;
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($machines as $machine) {
|
||||
// 1. 先建立 RemoteCommand 紀錄
|
||||
$command = RemoteCommand::create([
|
||||
'machine_id' => $machine->id,
|
||||
'user_id' => $schedule->user_id,
|
||||
'command_type' => 'update_app',
|
||||
'status' => 'pending',
|
||||
'payload' => [
|
||||
'url' => $apkVersion->url,
|
||||
'version' => $apkVersion->version_name,
|
||||
'version_code' => $apkVersion->version_code,
|
||||
'is_forced' => $apkVersion->is_forced,
|
||||
],
|
||||
]);
|
||||
|
||||
// 2. 推播 MQTT 更新指令
|
||||
$pushed = $mqttService->pushCommand(
|
||||
$machine->serial_no,
|
||||
'update_app',
|
||||
$command->payload,
|
||||
(string) $command->id
|
||||
);
|
||||
|
||||
if ($pushed) {
|
||||
$successCount++;
|
||||
} else {
|
||||
$command->update(['status' => 'failed']);
|
||||
$this->error("Failed to push MQTT OTA command to machine: {$machine->serial_no}");
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->update(['status' => 'completed']);
|
||||
$this->info("Successfully processed OTA Schedule ID: {$schedule->id}. MQTT deployed to {$successCount}/{$machines->count()} machines.");
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing OTA Schedule ID {$schedule->id}: " . $e->getMessage(), [
|
||||
'exception' => $e
|
||||
]);
|
||||
$schedule->update(['status' => 'failed']);
|
||||
$this->error("Error processing OTA Schedule ID {$schedule->id}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('OTA schedules processing completed.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
$schedule->command('ota:process-schedules')->everyMinute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -15,7 +15,39 @@ class ApkVersionController extends AdminController
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$versions = ApkVersion::orderBy('version_code', 'desc')->paginate(10);
|
||||
// 在進入 APK 版本清單時,同步觸發已到期排程的執行(確保無 cron 的開發/測試環境也能 100% 執行)
|
||||
try {
|
||||
\Illuminate\Support\Facades\Artisan::call('ota:process-schedules');
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('Auto-trigger scheduled OTA failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$versions = ApkVersion::latest()->paginate(10);
|
||||
|
||||
// 統計並附屬機台部署狀態
|
||||
$commands = \App\Models\Machine\RemoteCommand::where('command_type', 'update_app')
|
||||
->with('machine')
|
||||
->get();
|
||||
|
||||
foreach ($versions as $version) {
|
||||
$pending = [];
|
||||
$completed = [];
|
||||
|
||||
foreach ($commands as $cmd) {
|
||||
$cmdVersionCode = $cmd->payload['version_code'] ?? null;
|
||||
if ($cmdVersionCode == $version->version_code) {
|
||||
if ($cmd->status === 'completed' || $cmd->status === 'success') {
|
||||
$completed[] = $cmd->machine;
|
||||
} elseif ($cmd->status === 'pending' || $cmd->status === 'processing') {
|
||||
$pending[] = $cmd->machine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$version->pending_machines = collect($pending)->filter()->unique('id');
|
||||
$version->completed_machines = collect($completed)->filter()->unique('id');
|
||||
}
|
||||
|
||||
// 用於 OTA 下發時讓使用者選擇的機台列表
|
||||
$machines = Machine::all();
|
||||
return view('admin.basic-settings.apk-versions.index', compact('versions', 'machines'));
|
||||
@ -35,7 +67,6 @@ class ApkVersionController extends AdminController
|
||||
'apk_file' => 'required_without:download_url|nullable|file|max:102400', // 上傳限制 100MB
|
||||
'download_url' => 'required_without:apk_file|nullable|url|max:500',
|
||||
'release_notes' => 'nullable|string',
|
||||
'is_forced' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$filePath = null;
|
||||
@ -50,7 +81,7 @@ class ApkVersionController extends AdminController
|
||||
'file_path' => $filePath,
|
||||
'download_url' => $request->download_url,
|
||||
'release_notes' => $request->release_notes,
|
||||
'is_forced' => $request->boolean('is_forced'),
|
||||
'is_forced' => false,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.basic-settings.apk-versions.index')
|
||||
@ -76,8 +107,24 @@ class ApkVersionController extends AdminController
|
||||
$request->validate([
|
||||
'machine_ids' => 'required|array',
|
||||
'machine_ids.*' => 'exists:machines,id',
|
||||
'scheduled_at' => 'nullable|date|after:now',
|
||||
]);
|
||||
|
||||
// 如果設定了預約發送時間
|
||||
if ($request->filled('scheduled_at')) {
|
||||
\App\Models\Machine\OtaSchedule::create([
|
||||
'apk_version_id' => $apkVersion->id,
|
||||
'scheduled_at' => $request->scheduled_at,
|
||||
'target_type' => 'custom',
|
||||
'target_value' => $request->machine_ids,
|
||||
'status' => 'pending',
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.basic-settings.apk-versions.index')
|
||||
->with('success', __('OTA update scheduled successfully.'));
|
||||
}
|
||||
|
||||
$machines = Machine::whereIn('id', $request->machine_ids)->get();
|
||||
$successCount = 0;
|
||||
|
||||
|
||||
35
app/Models/Machine/OtaSchedule.php
Normal file
35
app/Models/Machine/OtaSchedule.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OtaSchedule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'apk_version_id',
|
||||
'scheduled_at',
|
||||
'target_type',
|
||||
'target_value',
|
||||
'status',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'scheduled_at' => 'datetime',
|
||||
'target_value' => 'array',
|
||||
];
|
||||
|
||||
public function apkVersion()
|
||||
{
|
||||
return $this->belongsTo(ApkVersion::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ota_schedules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('apk_version_id')->constrained('apk_versions')->onDelete('cascade');
|
||||
$table->dateTime('scheduled_at');
|
||||
$table->string('target_type')->default('custom'); // 'all', 'custom'
|
||||
$table->json('target_value')->nullable(); // 當 target_type = 'custom' 時,儲存 [1, 2, 3] 機台 ID 的 JSON 陣列
|
||||
$table->string('status')->default('pending'); // 'pending', 'processing', 'completed', 'failed'
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null'); // 建立排程的使用者
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ota_schedules');
|
||||
}
|
||||
};
|
||||
11
lang/en.json
11
lang/en.json
@ -476,6 +476,7 @@
|
||||
"Delta": "Delta",
|
||||
"Deploy Update": "Deploy Update",
|
||||
"Deploy version": "Deploy version",
|
||||
"Deployment Status": "Deployment Status",
|
||||
"Deposit Bonus": "Deposit Bonus",
|
||||
"Describe the changes in this version...": "Describe the changes in this version...",
|
||||
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
|
||||
@ -527,6 +528,7 @@
|
||||
"Dispensing in progress": "Dispensing in progress",
|
||||
"Display Material Code": "Display Material Code",
|
||||
"Displaying": "Displaying",
|
||||
"Done": "Done",
|
||||
"Door Closed": "Door Closed",
|
||||
"Door Opened": "Door Opened",
|
||||
"Download Image": "Download Image",
|
||||
@ -887,6 +889,7 @@
|
||||
"Last Week": "Last Week",
|
||||
"Latitude": "Latitude",
|
||||
"Lease": "Lease",
|
||||
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "Leave blank to deploy immediately, or set a future time to schedule a background release.",
|
||||
"Leave empty for permanent code": "Leave empty for permanent code",
|
||||
"Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code",
|
||||
"Leave empty to inherit company settings": "Leave empty to inherit company settings",
|
||||
@ -1174,6 +1177,7 @@
|
||||
"No machines available": "No machines available",
|
||||
"No machines available in this company.": "No machines available in this company.",
|
||||
"No machines found": "No machines found",
|
||||
"No machines in this status": "No machines in this status",
|
||||
"No machines match your criteria": "No machines match your criteria",
|
||||
"No maintenance records found": "No maintenance records found",
|
||||
"No matching logs found": "No matching logs found",
|
||||
@ -1231,6 +1235,7 @@
|
||||
"OTA Update Deployment": "OTA Update Deployment",
|
||||
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
|
||||
"OTA firmware update and version control": "OTA firmware update and version control",
|
||||
"OTA update scheduled successfully.": "OTA update scheduled successfully.",
|
||||
"Off": "Off",
|
||||
"Offline": "Offline",
|
||||
"Offline + 1": "Offline + 1",
|
||||
@ -1503,6 +1508,8 @@
|
||||
"Quick replenishment from this view": "Quick replenishment from this view",
|
||||
"Quick search...": "Quick search...",
|
||||
"Rank": "Rank",
|
||||
"Ready": "Ready",
|
||||
"Ready to Execute": "Ready to Execute",
|
||||
"Ready to print": "Ready to print",
|
||||
"Real-time OEE analysis awaits": "Real-time OEE analysis awaits",
|
||||
"Real-time Operation Logs (Last 50)": "Real-time Operation Logs (Last 50)",
|
||||
@ -1630,6 +1637,7 @@
|
||||
"Scan to authorize testing or maintenance": "Scan to authorize testing or maintenance",
|
||||
"Scan to pick up your product": "Scan to pick up your product",
|
||||
"Schedule": "Schedule",
|
||||
"Schedule Release Time (Optional)": "Schedule Release Time (Optional)",
|
||||
"Search": "Search",
|
||||
"Search Company Title...": "Search Company Title...",
|
||||
"Search Company...": "Search Company...",
|
||||
@ -1695,6 +1703,7 @@
|
||||
"Select Slot...": "Select Slot...",
|
||||
"Select Target Company": "Select Target Company",
|
||||
"Select Target Slot": "Select Target Slot",
|
||||
"Select Update Time (Optional)": "Select Update Time (Optional)",
|
||||
"Select Warehouse": "Select Warehouse",
|
||||
"Select a machine to copy settings from...": "Select a machine to copy settings from...",
|
||||
"Select a machine to deep dive": "Select a machine to deep dive",
|
||||
@ -1704,6 +1713,7 @@
|
||||
"Select an asset from the left to start analysis": "Select an asset from the left to start analysis",
|
||||
"Select date to sync data": "Select date to sync data",
|
||||
"Select flavor...": "Select flavor...",
|
||||
"Select future time...": "Select update time...",
|
||||
"Select...": "Select...",
|
||||
"Selected": "Selected",
|
||||
"Selected Date": "Selected Date",
|
||||
@ -2015,6 +2025,7 @@
|
||||
"Unpublish": "Unpublish",
|
||||
"Update": "Update",
|
||||
"Update Authorization": "Update Authorization",
|
||||
"Update Completed": "Update Completed",
|
||||
"Update Customer": "Update Customer",
|
||||
"Update Mode": "Update Mode",
|
||||
"Update Pass Code": "Update Pass Code",
|
||||
|
||||
11
lang/ja.json
11
lang/ja.json
@ -476,6 +476,7 @@
|
||||
"Delta": "変動量",
|
||||
"Deploy Update": "Deploy Update",
|
||||
"Deploy version": "Deploy version",
|
||||
"Deployment Status": "配信ステータス",
|
||||
"Deposit Bonus": "チャージボーナス",
|
||||
"Describe the changes in this version...": "Describe the changes in this version...",
|
||||
"Describe the repair or maintenance status...": "修理やメンテナンスの状況を説明してください...",
|
||||
@ -527,6 +528,7 @@
|
||||
"Dispensing in progress": "搬送中",
|
||||
"Display Material Code": "資材コードを表示",
|
||||
"Displaying": "表示中",
|
||||
"Done": "完了",
|
||||
"Door Closed": "扉が閉まりました",
|
||||
"Door Opened": "扉が開きました",
|
||||
"Download Image": "画像をダウンロード",
|
||||
@ -887,6 +889,7 @@
|
||||
"Last Week": "先週",
|
||||
"Latitude": "緯度",
|
||||
"Lease": "リース",
|
||||
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "空欄のままにすると即時配信されます。未来の日時を指定すると背景で自動予約配信されます。",
|
||||
"Leave empty for permanent code": "空欄で無期限コードになります",
|
||||
"Leave empty or 0 for permanent code": "空欄または0で無期限コードになります",
|
||||
"Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します",
|
||||
@ -1174,6 +1177,7 @@
|
||||
"No machines available": "利用可能な機器がありません",
|
||||
"No machines available in this company.": "この会社で利用可能な機器がありません。",
|
||||
"No machines found": "機器が見つかりません",
|
||||
"No machines in this status": "このステータスのデバイスはありません",
|
||||
"No machines match your criteria": "No machines match your criteria",
|
||||
"No maintenance records found": "メンテナンス記録が見つかりません",
|
||||
"No matching logs found": "一致するログが見つかりません",
|
||||
@ -1231,6 +1235,7 @@
|
||||
"OTA Update Deployment": "OTA Update Deployment",
|
||||
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
|
||||
"OTA firmware update and version control": "OTA firmware update and version control",
|
||||
"OTA update scheduled successfully.": "OTA予約更新スケジュールが正常に作成されました。",
|
||||
"Off": "オフ",
|
||||
"Offline": "オフライン",
|
||||
"Offline + 1": "オフライン + 1",
|
||||
@ -1503,6 +1508,8 @@
|
||||
"Quick replenishment from this view": "この画面からクイック補充",
|
||||
"Quick search...": "クイック検索...",
|
||||
"Rank": "順位",
|
||||
"Ready": "準備中",
|
||||
"Ready to Execute": "コマンド実行待ちデバイス",
|
||||
"Ready to print": "印刷準備完了",
|
||||
"Real-time OEE analysis awaits": "リアルタイムOEE分析待機中",
|
||||
"Real-time Operation Logs (Last 50)": "リアルタイム操作ログ (直近50件)",
|
||||
@ -1630,6 +1637,7 @@
|
||||
"Scan to authorize testing or maintenance": "スキャンしてテストまたはメンテナンスを承認します",
|
||||
"Scan to pick up your product": "スキャンして商品をお受け取りください",
|
||||
"Schedule": "スケジュール",
|
||||
"Schedule Release Time (Optional)": "予約配信日時(任意)",
|
||||
"Search": "検索",
|
||||
"Search Company Title...": "会社名を検索...",
|
||||
"Search Company...": "会社を検索...",
|
||||
@ -1695,6 +1703,7 @@
|
||||
"Select Slot...": "スロットを選択...",
|
||||
"Select Target Company": "対象の会社を選択",
|
||||
"Select Target Slot": "対象のスロットを選択",
|
||||
"Select Update Time (Optional)": "更新時間を選択してください(任意)",
|
||||
"Select Warehouse": "倉庫を選択",
|
||||
"Select a machine to copy settings from...": "設定のコピー元機器を選択...",
|
||||
"Select a machine to deep dive": "詳細分析する機器を選択",
|
||||
@ -1704,6 +1713,7 @@
|
||||
"Select an asset from the left to start analysis": "左側から機器を選択して分析を開始",
|
||||
"Select date to sync data": "データを同期する日付を選択",
|
||||
"Select flavor...": "Select flavor...",
|
||||
"Select future time...": "配信日時を選択してください...",
|
||||
"Select...": "選択してください...",
|
||||
"Selected": "選択中",
|
||||
"Selected Date": "選択日",
|
||||
@ -2015,6 +2025,7 @@
|
||||
"Unpublish": "非公開",
|
||||
"Update": "更新",
|
||||
"Update Authorization": "権限更新",
|
||||
"Update Completed": "アップデート完了デバイス",
|
||||
"Update Customer": "顧客更新",
|
||||
"Update Mode": "Update Mode",
|
||||
"Update Pass Code": "パスコード更新",
|
||||
|
||||
@ -476,6 +476,7 @@
|
||||
"Delta": "變化量",
|
||||
"Deploy Update": "部署更新",
|
||||
"Deploy version": "部署版本",
|
||||
"Deployment Status": "部署狀態",
|
||||
"Deposit Bonus": "儲值回饋",
|
||||
"Describe the changes in this version...": "描述此版本的變更...",
|
||||
"Describe the repair or maintenance status...": "請描述維修或保養狀況...",
|
||||
@ -527,6 +528,7 @@
|
||||
"Dispensing in progress": "正在出貨中",
|
||||
"Display Material Code": "顯示物料代碼",
|
||||
"Displaying": "目前顯示",
|
||||
"Done": "已完成",
|
||||
"Door Closed": "機門已關閉",
|
||||
"Door Opened": "機門已開啟",
|
||||
"Download Image": "下載圖片",
|
||||
@ -887,6 +889,7 @@
|
||||
"Last Week": "上週",
|
||||
"Latitude": "緯度",
|
||||
"Lease": "租賃",
|
||||
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "留空將立即部署更新,或設定未來時間由背景自動排程發布。",
|
||||
"Leave empty for permanent code": "留空表示永久有效",
|
||||
"Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼",
|
||||
"Leave empty to inherit company settings": "留空則繼承公司全域設定",
|
||||
@ -1174,6 +1177,7 @@
|
||||
"No machines available": "目前沒有可供分配的機台",
|
||||
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
|
||||
"No machines found": "未找到機台",
|
||||
"No machines in this status": "此狀態下無設備",
|
||||
"No machines match your criteria": "沒有符合條件的機台",
|
||||
"No maintenance records found": "找不到維修紀錄",
|
||||
"No matching logs found": "找不到符合條件的日誌",
|
||||
@ -1231,6 +1235,7 @@
|
||||
"OTA Update Deployment": "OTA 更新部署",
|
||||
"OTA command sent to %d devices successfully.": "OTA 指令已成功發送至 %d 台設備。",
|
||||
"OTA firmware update and version control": "OTA 韌體更新與版本控制",
|
||||
"OTA update scheduled successfully.": "OTA 預約更新排程已建立成功。",
|
||||
"Off": "關閉",
|
||||
"Offline": "離線",
|
||||
"Offline + 1": "離線 + 1",
|
||||
@ -1503,6 +1508,8 @@
|
||||
"Quick replenishment from this view": "從此畫面快速補貨",
|
||||
"Quick search...": "快速搜尋...",
|
||||
"Rank": "排行",
|
||||
"Ready": "準備中",
|
||||
"Ready to Execute": "準備要執行指令的設備",
|
||||
"Ready to print": "準備列印",
|
||||
"Real-time OEE analysis awaits": "即時 OEE 分析預備中",
|
||||
"Real-time Operation Logs (Last 50)": "即時操作日誌 (最後 50 筆)",
|
||||
@ -1630,6 +1637,7 @@
|
||||
"Scan to authorize testing or maintenance": "掃描以進行測試或維護授權",
|
||||
"Scan to pick up your product": "掃描以領取您的商品",
|
||||
"Schedule": "排程區間",
|
||||
"Schedule Release Time (Optional)": "預約發布時間(選填)",
|
||||
"Search": "搜尋",
|
||||
"Search Company Title...": "搜尋公司名稱...",
|
||||
"Search Company...": "搜尋公司...",
|
||||
@ -1695,6 +1703,7 @@
|
||||
"Select Slot...": "選擇貨道...",
|
||||
"Select Target Company": "選擇目標公司",
|
||||
"Select Target Slot": "選擇目標貨道",
|
||||
"Select Update Time (Optional)": "選擇更新時間(選填)",
|
||||
"Select Warehouse": "選擇倉庫",
|
||||
"Select a machine to copy settings from...": "選擇要複製設定的來源機台...",
|
||||
"Select a machine to deep dive": "請選擇機台以開始深度分析",
|
||||
@ -1704,6 +1713,7 @@
|
||||
"Select an asset from the left to start analysis": "選擇左側機台以開始分析數據",
|
||||
"Select date to sync data": "選擇日期以同步數據",
|
||||
"Select flavor...": "選擇風味...",
|
||||
"Select future time...": "選擇更新時間",
|
||||
"Select...": "請選擇...",
|
||||
"Selected": "已選擇",
|
||||
"Selected Date": "查詢日期",
|
||||
@ -2015,6 +2025,7 @@
|
||||
"Unpublish": "下架",
|
||||
"Update": "編輯",
|
||||
"Update Authorization": "更新授權",
|
||||
"Update Completed": "已經完成更新指令的設備",
|
||||
"Update Customer": "更新客戶",
|
||||
"Update Mode": "更新模式",
|
||||
"Update Pass Code": "更新通行碼",
|
||||
|
||||
@ -173,49 +173,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 更新模式卡片 --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card" style="animation-delay: 100ms">
|
||||
<div class="flex items-center gap-5 mb-8">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/10 to-orange-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 group-hover/card:scale-110 transition-transform duration-500">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
|
||||
{{-- 注意事項卡片 --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card bg-amber-500/5 dark:bg-amber-500/5 border border-amber-500/15" style="animation-delay: 100ms">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 shrink-0">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight font-display uppercase">{{ __('Update Mode') }}</h3>
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Control deployment behavior') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 強制更新開關 --}}
|
||||
<div class="flex items-center justify-between p-5 rounded-2xl border transition-all duration-300"
|
||||
:class="isForced
|
||||
? 'bg-rose-500/10 dark:bg-rose-500/10 border-rose-500/20 dark:border-rose-500/30 text-rose-600 dark:text-rose-400 shadow-sm shadow-rose-500/5'
|
||||
: 'bg-slate-50/80 dark:bg-slate-800/40 border-slate-100 dark:border-slate-800/80 text-slate-700 dark:text-slate-200'">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300"
|
||||
:class="isForced ? 'bg-rose-500/10 text-rose-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-extrabold transition-colors"
|
||||
:class="isForced ? 'text-rose-600 dark:text-rose-400' : 'text-slate-700 dark:text-slate-200'">{{ __('Forced Update') }}</p>
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 tracking-wide mt-0.5">{{ __('During OTA deployment, the device will install directly in the background without user confirmation.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer shrink-0">
|
||||
<input type="hidden" name="is_forced" value="0">
|
||||
<input type="checkbox" name="is_forced" value="1" x-model="isForced" class="sr-only peer" {{ old('is_forced') ? 'checked' : '' }}>
|
||||
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-rose-500"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- 注意事項 --}}
|
||||
<div class="mt-6 p-5 rounded-2xl bg-amber-500/10 border border-amber-500/20 shadow-sm shadow-amber-500/5">
|
||||
<div class="flex items-start gap-4">
|
||||
<svg class="w-5 h-5 stroke-[2.5] text-amber-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
|
||||
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
|
||||
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1">{{ __('Important Notice') }}</p>
|
||||
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
|
||||
</div>
|
||||
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
|
||||
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1 font-display">{{ __('Important Notice') }}</p>
|
||||
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,7 +196,6 @@
|
||||
uploadMode: 'file',
|
||||
fileName: '',
|
||||
isDragging: false,
|
||||
isForced: false,
|
||||
versionName: '{{ old('version_name', '') }}',
|
||||
versionCode: '{{ old('version_code', '') }}',
|
||||
|
||||
|
||||
@ -43,6 +43,9 @@
|
||||
this.searchMachineQuery = '';
|
||||
this.onlyOnlineMachines = false;
|
||||
this.isPushModalOpen = true;
|
||||
// 清空預約時間
|
||||
const scheduleInput = document.getElementById('scheduled_at');
|
||||
if (scheduleInput) scheduleInput.value = '';
|
||||
},
|
||||
|
||||
get filteredMachines() {
|
||||
@ -143,7 +146,7 @@
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Version Info') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Flavor') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Update Mode') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Deployment Status') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Release Notes') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
|
||||
</tr>
|
||||
@ -171,16 +174,46 @@
|
||||
{{ $version->flavor }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
@if($version->is_forced)
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-rose-100 dark:border-rose-900/30 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 tracking-widest uppercase">
|
||||
{{ __('Forced Update') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 tracking-widest uppercase">
|
||||
{{ __('Optional Update') }}
|
||||
</span>
|
||||
@endif
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 準備中 -->
|
||||
@php
|
||||
$pendingTooltip = __('Ready to Execute') . ":\n";
|
||||
if ($version->pending_machines->isEmpty()) {
|
||||
$pendingTooltip .= "• " . __('No machines in this status');
|
||||
} else {
|
||||
foreach($version->pending_machines as $m) {
|
||||
$pendingTooltip .= "• {$m->name} ({$m->serial_no})\n";
|
||||
}
|
||||
}
|
||||
$pendingTooltip = trim($pendingTooltip);
|
||||
@endphp
|
||||
<div class="inline-block tooltip cursor-help" title="{{ $pendingTooltip }}">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-amber-100 dark:border-amber-900/30 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 tracking-widest inline-flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
|
||||
{{ __('Ready') }}: {{ $version->pending_machines->count() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 已完成 -->
|
||||
@php
|
||||
$completedTooltip = __('Update Completed') . ":\n";
|
||||
if ($version->completed_machines->isEmpty()) {
|
||||
$completedTooltip .= "• " . __('No machines in this status');
|
||||
} else {
|
||||
foreach($version->completed_machines as $m) {
|
||||
$completedTooltip .= "• {$m->name} ({$m->serial_no})\n";
|
||||
}
|
||||
}
|
||||
$completedTooltip = trim($completedTooltip);
|
||||
@endphp
|
||||
<div class="inline-block tooltip cursor-help" title="{{ $completedTooltip }}">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-emerald-100 dark:border-emerald-900/30 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 tracking-widest inline-flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||
{{ __('Done') }}: {{ $version->completed_machines->count() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 max-w-xs truncate">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
@ -307,6 +340,42 @@
|
||||
<!-- 表單與機台選擇清單 -->
|
||||
<form :action="pushActionUrl" method="POST" id="ota-push-form">
|
||||
@csrf
|
||||
|
||||
<!-- 預約排程時間設定 -->
|
||||
<div class="mb-6 p-4 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/20">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<label for="scheduled_at" class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5">
|
||||
{{ __('Select Update Time (Optional)') }}
|
||||
</label>
|
||||
<p class="text-[10px] text-slate-400 dark:text-slate-500 leading-normal">
|
||||
{{ __('Leave blank to deploy immediately, or set a future time to schedule a background release.') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full md:w-64 group">
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text"
|
||||
id="scheduled_at"
|
||||
name="scheduled_at"
|
||||
x-init="flatpickr($el, {
|
||||
enableTime: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
time_24hr: true,
|
||||
locale: '{{ app()->getLocale() === 'zh_TW' ? 'zh_tw' : (app()->getLocale() === 'ja' ? 'ja' : 'en') }}',
|
||||
disableMobile: true
|
||||
})"
|
||||
class="luxury-input py-2.5 pl-12 pr-4 block w-full text-sm font-bold tracking-tight cursor-pointer"
|
||||
placeholder="{{ __('Select future time...') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-slate-100 dark:border-slate-800/80 rounded-2xl overflow-hidden">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-slate-50/50 dark:bg-slate-950/40">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user