[FIX] 修正遠端指令中心機台列表顯示與規格文件更新

1. 修正 `tab-machines-index.blade.php` 最後通訊時間的 HTML `<template>` 渲染錯誤,確保正常顯示。
2. 更新 `index.blade.php` 的 `formatTime` 邏輯,超過 2 小時的通訊時間將顯示完整日期與時間,與庫存頁面格式統一。
3. 更新 `tab-machines.blade.php` 與語系檔,將庫存警示相關文案精準化為「庫存警示 (Inventory Alerts)」與「庫存穩定 (Inventory Stable)」。
4. 更新 `api-technical-specs/SKILL.md` 中對於 B017 API 的描述,強調其為機台端主動拉取資料,而非單純依賴 MQTT 觸發。
5. 修正 `iot-communication/SKILL.md` 中 Redis Key 前綴的說明,對齊實際讀取 `.env` (`MQTT_REDIS_PREFIX`) 的設定。
This commit is contained in:
sky121113 2026-04-22 12:33:01 +08:00
parent 1d5aff2f2e
commit b21ea670bf
8 changed files with 53 additions and 196 deletions

View File

@ -4,193 +4,46 @@ trigger: always_on
# 開發框架規範說明書Cloud 後台管理系統 (star-cloud)
## 1. 專案概述
* **目標**打造一個強大且穩定的智能販賣機後台管理系統Cloud 平台),負責管理機台、商品、銷售數據以及提供給端點機台串接的 API。
* **核心架構**:採用 **Monorepo 單體式架構**,以 Laravel 為核心進行伺服器端渲染 (SSR) 與 API 服務,並搭配 **Go MQTT Gateway** 作為高併發 IoT 通訊的前置接收層。兩者透過 **Redis** 進行異步橋接,確保職責分離與系統穩定性。
* **工作流程**:後端處理業務邏輯與資料庫存取,並透過 Blade 引擎渲染包含 Tailwind CSS 類別的 HTML。前端互動行為由輕量級 Alpine.js 負責UI 元件以 Preline UI 為主體。
## 1. 專案概述 & 技術棧
* **目標**:智能販賣機後台管理系統,負責機台、商品、銷售數據與 IoT API。
* **核心架構**Monorepo 單體式架構。
* **後端核心**PHP 8.5 / Laravel 12, MySQL 8.0, Redis快取與異步橋接
* **IoT 通訊層**EMQX 5 (MQTT Broker) + Go MQTT Gateway。
* **前端視圖**Laravel Blade (SSR) + Alpine.js (互動) + Tailwind CSS + Preline UI (組件)。
* **開發環境**Laravel Sail (Docker / WSL2)
## 2. 技術棧 (Tech Stack)
## 2. 系統架構與資料流向
### 2.1 後端核心 (Laravel)
* **後端框架**PHP 8.5 / Laravel 12
* **核心組件**Redis (用於高併發 IoT 隊列、MQTT 橋接與快取,為系統穩定之必要條件)
* **資料庫**MySQL 8.0
* **開發環境**Laravel Sail (Docker / WSL2)
本系統採用 **MQTT 與 HTTP 雙軌並行** 策略:
### 2.2 IoT 通訊層 (MQTT Gateway)
* **MQTT Broker**EMQX 5 (負責維持機台長連線與訊息路由)
* **Gateway 語言**Go (負責訂閱 MQTT Topic、預處理訊息、轉發至 Redis)
* **橋接機制**Redis List (`mqtt_incoming_jobs`),由 Laravel 常駐指令 (`mqtt:listen`) 消費
### 2.3 前端
* **前端視圖 (View)**Laravel Blade
* **前端互動 (JS)**Alpine.js (專注於行為,不負責渲染)
* **介面與樣式 (CSS)**Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)。
* **重要規範**Preline UI 僅作為「原子組件」與「JS 互動邏輯」的參考庫。整體的「佈局」與「美學」必須嚴格遵守「極簡奢華風 UI 實作規範 (SKILL.md)」。
* **前端建置工具**Vite
## 3. 目錄結構與慣例
### 3.1 後端 (Laravel)
與標準 Laravel 結構保持一致,無過度拆分的模組化(與 ERP 的 Modular Monolith 區別):
* **Controllers**`app/Http/Controllers/`,負責接收請求並回傳 `view()` 或 JSON。
* **Models**`app/Models/{Domain}/`,按領域分群 (例如 `Machine`, `Member`, `System`)。
* **Routes**`routes/web.php` 用於後台管理介面;`routes/api.php` 提供外部或機台調用介面 (需 V1 版本化)。
* **Services** (建議)`app/Services/{Domain}/`,將商業邏輯與資料異動封裝於 Service 中。
* **Traits**`app/Traits/ApiResponse.php` 用於統一 API JSON 回傳格式。
* **Jobs**`app/Jobs/{Domain}/`,用於異步處理 IoT 資料寫入、通知發送等背景任務。
* **Console Commands**`app/Console/Commands/`,包含 MQTT 橋接守護進程 (`mqtt:listen`) 等常駐指令。
### 3.2 前端 (Blade / Tailwind / Alpine)
* **Views (頁面)**:位於 `resources/views/`。通常依功能建立資料夾(如 `resources/views/admin/machines/index.blade.php`)。
* **Layouts (版面)**:位於 `resources/views/layouts/`。定義全站的共用版面結構(如 header, sidebar, footer
* **Components (組件)**:位於 `resources/views/components/`。封裝可重用的 Blade 元件(如 Button, Modal, Table支援透過 `<x-button>` 語法呼叫。
### 3.3 MQTT Gateway (Go)
Go 專案以 Monorepo 形式置於專案根目錄下的 `mqtt-gateway/` 資料夾,獨立於 Laravel 程式碼:
* **進入點**`mqtt-gateway/main.go`
* **模組管理**`mqtt-gateway/go.mod` / `go.sum`
* **內部分層**
* `mqtt-gateway/internal/handler/` — 各 Topic 的訊息處理邏輯(如 heartbeat、transaction、error
* `mqtt-gateway/internal/bridge/` — Redis 橋接層,負責將處理後的 JSON 推入 `mqtt_incoming_jobs` List。
* `mqtt-gateway/config/` — 環境變數與 EMQX / Redis 連線設定。
> [!CAUTION]
> Go Gateway 的職責僅限於「接收、驗證、轉發」。**嚴禁**在 Go 中實作任何商業邏輯(如庫存扣減、通知發送),所有業務處理必須統一在 Laravel Service 層完成。
## 4. IoT 通訊架構 (MQTT + HTTP 雙軌制)
本系統的機台通訊採用 **MQTT 與 HTTP 雙軌並行** 的策略,依據通訊特性選擇最適合的協議。
### 4.1 整體資料流向
```
```text
機台 (Android APP)
├─ [高頻/即時] MQTT 長連線 ──→ EMQX Broker ──→ Go Gateway ──→ Redis List ──→ Laravel mqtt:listen ──→ Job ──→ MySQL
├─ [高頻/即時] MQTT ──→ EMQX Broker ──→ Go Gateway ──→ Redis List ──→ Laravel Job ──→ MySQL
└─ [低頻/大檔] HTTP REST ──→ Laravel API Controller ──→ (必要時) Job ──→ MySQL
└─ [低頻/大檔] HTTP REST ──→ Laravel API Controller ──→ MySQL
```
### 4.2 MQTT 通訊端點 (高頻與事件驅動)
以下端點因高頻率或即時性需求,採用 MQTT 協議通訊:
* **Go Gateway 職責**:僅限「接收、驗證、轉發」。**嚴禁**在 Go 中實作商業邏輯,統一交由 Laravel Service/Job 處理。
* **橋接機制**Go 收到 MQTT 訊息後轉為 JSON推入 Redis ListLaravel 透過常駐指令 (`mqtt:listen`) 消費並分派至對應的 Job 處理。
| API 代碼 | Topic 格式 | 用途 | QoS |
| :--- | :--- | :--- | :--- |
| B010 | `machine/{serial_no}/heartbeat` | 心跳上報 (每 10 秒) | 0 |
| B013 | `machine/{serial_no}/error` | 故障與異常狀態上報 | 1 |
| B600 | `machine/{serial_no}/transaction` | 交易紀錄回傳 | 1 |
## 3. 核心目錄結構特徵
除標準 Laravel 結構外,本專案的特殊慣例:
* **`mqtt-gateway/`**Go 獨立專案目錄,負責 MQTT 訊息接收與轉發。
* **`app/Jobs/`**:用於處理 Go 轉發過來的 IoT 非同步資料寫入與背景邏輯。
* **`app/Console/Commands/`**:包含常駐守護進程 (如 `mqtt:listen`)。
* **`resources/views/components/`**:全站共用的 Blade UI 元件(如 `<x-button>`),支援 x- 前綴呼叫。
**雲端→機台指令下發**:透過 `machine/{serial_no}/command` Topic 推送,取代原本 B010 Response 中的 `status` 欄位輪詢機制,實現毫秒級即時指令。
## 4. 前端與 UI 開發規範
* **樣式與組件**:全面使用 Tailwind CSS utility classes遵循 Preline UI 的 HTML 結構進行開發。**避免撰寫自訂 CSS**。
* **互動邏輯**:優先使用輕量級 **Alpine.js** (`x-data`, `x-show`, `@click`) 於 Blade 內完成,**禁用 React / Vue / Inertia.js**。
* **【警告Preline 冗餘】**Preline 官方範例常有多餘控制項。必須確保介面佈局極簡,嚴格遵守「極簡奢華風 UI 實作規範 (SKILL.md)」。
* **多語系 (I18n)**:所有 UI 可見文字必須使用 `@lang('key')``__('key')` 處理。Key 統一採用**英文原始詞彙**,並同步更新 `zh_TW.json`, `en.json`, `ja.json`
### 4.3 HTTP 通訊端點 (資料拉取與敏感操作)
以下端點因資料量大、安全性要求高或為 Request/Response 模式,維持使用 HTTP REST API
| API 代碼 | 用途 | 維持 HTTP 的原因 |
| :--- | :--- | :--- |
| B000 | 維運人員登入 | 無狀態認證HTTP 更自然 |
| B012 | 商品配置同步 | 大量資料的 GET 拉取 |
| B014 | 金鑰與參數下載 | 高安全性敏感操作,需嚴格 RBAC |
| B009 | 貨道庫存回報 | 低頻操作,由維運人員觸發 |
### 4.4 Redis 橋接機制 (Go ↔ Laravel)
Go Gateway 與 Laravel 之間透過 Redis List 進行單向異步橋接:
* **Redis Key (邏輯名)**`mqtt_incoming_jobs`
* **Redis Key (實際名)**`starcloud_database_mqtt_incoming_jobs` (含 Laravel 前綴)
* **Go 端 (生產者)**:將 MQTT 收到的 Payload 包裝成標準 JSON 後,執行 `RPUSH starcloud_database_mqtt_incoming_jobs {json}`。Go 程式碼中透過 `"starcloud_database_" + cfg.IncomingQueueKey` 手動加入前綴。
* **Laravel 端 (消費者)**:常駐指令 `php artisan mqtt:listen` 持續執行 `BLPOP mqtt_incoming_jobs`Laravel Redis 驅動自動加上前綴),取得 JSON 後解碼並分派至對應的 Laravel Job (如 `ProcessHeartbeat`, `ProcessTransaction`)。
> [!CAUTION]
> **Redis 前綴一致性**Laravel 的 Redis 驅動會自動在 Key 前加上 `config('database.redis.options.prefix')` (預設 `starcloud_database_`)。Go 端不使用 Laravel 的 Redis 客戶端,因此**必須在程式碼中手動加上此前綴**,否則雙方會操作不同的 Key導致訊息永遠無法傳達。
**JSON 橋接格式規範**
```json
{
"type": "heartbeat",
"serial_no": "M-001",
"received_at": "2026-04-14T09:00:00+08:00",
"payload": {
"current_page": 1,
"firmware_version": "1.0.5",
"temperature": 25.5
}
}
```
> [!IMPORTANT]
> **為何不讓 Go 直接寫入 Laravel Queue** 因為 Laravel Queue 的 Payload 包含 PHP 序列化物件字串 (`serialize()`)Go 無法安全產生此格式。透過獨立的 Redis List + 純 JSON可徹底解耦兩端的技術依賴。
### 4.5 MQTT 連線認證
機台連線 EMQX 時,使用 `serial_no` 作為 Username、`api_token` (SHA256) 作為 Password。驗證流程
1. **Laravel 端 (Token 派發時)**B014 下發 `api_token` 時,同步執行 `Redis::hset("machine_auth:{serial_no}", "password", hash('sha256', $api_token))`
2. **EMQX 端 (連線驗證時)**:配置 Redis Auth Plugin使用 `HMGET machine_auth:${username} password` 查詢 Redis Hash 進行極速驗證 (毫秒級),不經過 MySQL。
3. **Token 更新/撤銷時**Laravel 更新或刪除機台 Token 時,必須同步更新或刪除 Redis 中的對應 Hash。
4. **Go Gateway 認證**Gateway 使用固定帳號 `star-cloud-gateway` 連線 EMQX其認證資料同樣以 Hash 結構存放於 Redis (`machine_auth:star-cloud-gateway`)。
> [!CAUTION]
> **認證資料結構為 Redis Hash (HSET)**,非 String (SET)。EMQX 5.x 預設的 Redis 認證插件使用 `HMGET` 指令讀取,若誤用 `SET` 儲存為 String 類型,認證將永遠失敗。
> 批量同步指令:`php artisan mqtt:sync-auth`。
## 5. 開發標準 (Coding Standards)
* **命名規範**
* Controllers: `PascalCaseController.php` (例如 `MachineController.php`)
* Models: `PascalCase.php` (例如 `Machine.php`)
* Blade Views: `kebab-case.blade.php` 或按資源名稱 (例如 `index.blade.php`, `create.blade.php`)
* Routes uri: `kebab-case` (例如 `/machine-logs`)
* Go 檔案: `snake_case.go` (例如 `heartbeat_handler.go`)
* **回傳格式**
* Web 路由:回傳 `view()`,表單驗證失敗時直接使用 Laravel 內建的 redirect with errors。
* API 路由:回傳標準 JSON 格式的 `JsonResponse`
## 6. UI 與前端開發指南
* **樣式撰寫**:全面使用 Tailwind CSS utility classes**避免撰寫自訂 CSS**(除非少數特定動畫或覆寫)。
* **UI 元件庫**:遵循 **Preline UI** 的類別與 HTML 結構進行開發。
* **前端腳本**
* 優先使用 **Alpine.js** (`x-data`, `x-show`, `@click` 等) 在 HTML 標籤內完成簡單的 DOM 狀態切換與互動邏輯。
* 避免在 Blade 內撰寫冗長的 `<script>` Vanilla JS若邏輯過於複雜可將 Alpine state 獨立成 js 檔案再於 Vite 引入,但原則上保持輕量。
## 7. 多語系 I18n 規範 (Multi-language Standards)
* **視圖開發**:所有使用者可見的文字、按鈕、提示訊息,必須使用 Laravel 的 `@lang('key')``__('key')` 函式包裹。
* **語系 Key 命名**:語系 Key 必須採用 **英文原始詞彙 (English phrases)** 作為 Key 名稱為原則,以提高代碼可讀性並作為預設回退(除非該字串過長,才建議使用點號分隔的 key
* 範例:使用 `__('Account Settings')`
* **翻譯檔維護**
* 主語系檔案位於 `lang/` 目錄。
* 開發新功能時,必須同步更新以下三個 JSON 翻譯檔:`zh_TW.json` (主要)、`en.json` (預設)、`ja.json` (日文)。
## 8. AI 協作規則 (給 Antigravity AI)
* **角色設定**:你是一位專業的全端開發工程師助手。
* **代碼生成指令**
* 所有的解釋說明請使用 **繁體中文**。
* **【警告Preline 冗餘】** Preline UI 的官方範例常包含多餘的控制項(如頂部筆數切換)。**嚴禁**照抄其佈局必須確保頂部工具列Header/Toolbar維持極簡重複功能一律收納至底部。
* **【警告】** 此專案前端禁用 React / Vue / Inertia.js。所有的前端頁面生成必須使用 **Blade 模板** 結合 **Tailwind CSS****Alpine.js**
* **【多語系強制要求】** 任何新增的 Blade UI 區塊,禁止硬編碼 (Hard-coded) 中文或英文。必須使用 `__('...')` 並同步在 `lang/*.json` 補上翻譯。
* 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。
* 開發新功能時,請建立標準的 Controller 搭配對應的 `resources/views/.../` 目錄。
* **【Go Gateway 開發】** 修改 `mqtt-gateway/` 內的 Go 程式碼時嚴禁加入商業邏輯。Go 僅負責訊息接收、格式轉換與 Redis 轉發。
## 9. 運行機制 (Docker / Sail)
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
* **啟動環境**`./vendor/bin/sail up -d`(將同時啟動 Laravel、MySQL、Redis、EMQX、Go Gateway
* **執行 PHP 指令**`./vendor/bin/sail php -v`
* **執行 Artisan 指令**`./vendor/bin/sail artisan route:list`
* **執行 Composer**`./vendor/bin/sail composer install`
* **執行 Node/NPM**`./vendor/bin/sail npm run dev`
## 10. 部署與查修環境 (CI/CD)
* **自動化部署**:專案具備基於 Gitea Actions 的 CI/CD 自動化部署流程 (`.gitea/workflows/`)。
* **Demo 環境 (對應 `demo` 分支)**
* 透過 `deploy-demo.yaml`,合併或推送到 `demo` 分支會自動部署至 `demo-cloud.setdomains.work`
* 登入伺服器查修:`ssh demo-sc-103`,路徑為 `/workspace/mama/star-cloud`
## 11. 瀏覽器測試規範 (Browser Testing)
當需要進行瀏覽器自動化測試或手動驗證時,必須遵守以下連線資訊:
* **本地測試網址**`http://localhost:8090/` (注意:非 8000 或 8080)
* **預設管理員帳號**`admin`
* **預設管理員密碼**`Star82779061`
> [!IMPORTANT]
> 在執行 `open_browser_url` 或進行 E2E 測試時,請務必優先確認 Port 是否為 `8090`,以避免連線至錯誤的服務環境。
## 5. 運行與測試機制
* **啟動環境**`./vendor/bin/sail up -d`
* **執行指令**:透過 `./vendor/bin/sail php` / `artisan` / `composer` / `npm`
* **CI/CD 部署**:具備 Gitea Actions 自動化部署 (`demo` 分支會自動部署至 `demo-cloud.setdomains.work`)。
* **瀏覽器測試**
* 本地測試網址:`http://localhost:8090/` (注意 Port 為 **8090**)
* 預設管理員帳號 / 密碼:`admin` / `Star82779061`

View File

@ -205,7 +205,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
---
### 3.8 B017: 貨道庫存同步 (Slot Synchronization)
用於機台端獲取目前所有貨道的最新庫存、效期與狀態。通常由 **MQTT 指令 (`reload_stock`)** 觸發
用於機台端**主動**向遠端後台拉取目前所有貨道的最新庫存、效期與配置狀態。通常發生於機台開機初始化、補貨/維護模式結束,或是機台需要確保本地端庫存資料與雲端一致時主動請求
- **URL**: GET /api/v1/app/machine/reload_msg/B017
- **Authentication**: Bearer Token (Header)
@ -231,9 +231,6 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| capacity | Integer | 貨道容量上限 | 15 |
| status | String | 貨道狀態 ("1": 啟用, "0": 停用) | "1" |
> [!IMPORTANT]
> **同步機制**B017 為全量同步。App 收到回應後應更新本地資料庫對應貨道的數值。成功請求後,雲端會自動將相關的 `reload_stock` 指令標記為 `success`
---
### 3.9 B024: 取貨碼/通行碼驗證與消耗回報 (Access Code Verify & Report)

View File

@ -29,7 +29,7 @@ description: 規範智能販賣機與 Cloud 平台間的高頻通訊處理流程
> 兩條管線的 **Job / Service / Model 層完全共用**,差異僅在「進入點」。這確保了業務邏輯不會因為通訊協議不同而分裂。
> [!CAUTION]
> **Redis Key 前綴一致性**Laravel 的 Redis 驅動會自動在 Key 前加上 `starcloud_database_` 前綴。Go Gateway 不使用 Laravel Redis 客戶端,因此必須在程式碼中手動加上此前綴。另外EMQX 認證使用 Redis Hash (`HSET`),嚴禁使用 String (`SET`)。
> **Redis Key 前綴一致性**Laravel 的 Redis 驅動會自動在 Key 前加上 `REDIS_PREFIX` (預設為 `star_cloud_database_`)。Go Gateway 不使用 Laravel Redis 客戶端,因此已透過讀取 `.env` 中的 `MQTT_REDIS_PREFIX` 來確保雙方前綴一致。另外EMQX 認證使用 Redis Hash (`HSET`),嚴禁使用 String (`SET`)。
> [!IMPORTANT]
> **嚴禁**在 API Controller 直接進行資料庫寫入操作(針對機台發訊端點)。

View File

@ -67,6 +67,8 @@
"All Machines": "All Machines",
"All Stable": "All Stable",
"All Times System Timezone": "All times are in system timezone",
"Inventory Alerts": "Inventory Alerts",
"Inventory Stable": "Inventory Stable",
"Amount": "Amount",
"An error occurred while saving.": "An error occurred while saving.",
"analysis": "Analysis Management",

View File

@ -397,6 +397,8 @@
"Initial Admin Account": "初始管理帳號",
"Initial Role": "初始角色",
"Installation": "裝機",
"Inventory Alerts": "庫存警示",
"Inventory Stable": "庫存穩定",
"Invoice Status": "發票開立狀態",
"Items": "筆",
"items": "筆項目",

View File

@ -402,7 +402,11 @@
if (diff < 3600) return Math.floor(diff / 60) + ' ' + this.translations['mins ago'];
if (diff < 7200) return '1 ' + this.translations['hours ago'];
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
// 超過 2 小時顯示完整日期與時間 (HH:mm),與庫存頁面格式一致
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0') + ' ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
},
getCommandBadgeClass(status) {

View File

@ -119,15 +119,14 @@
</div>
</td>
<td class="px-6 py-6 text-center">
<template x-data="{ heartbeat: {{ Js::from($machine->last_heartbeat_at) }} }">
<div class="flex flex-col items-center">
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
x-text="formatTime(heartbeat)"></span>
<span
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5"
x-text="heartbeat ? heartbeat.split('T')[0] : '--'"></span>
</div>
</template>
<div class="flex flex-col items-center">
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
x-text="formatTime({{ Js::from($machine->last_heartbeat_at) }})"></span>
<span
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
{{ $machine->last_heartbeat_at ? \Illuminate\Support\Carbon::parse($machine->last_heartbeat_at)->format('Y-m-d') : '--' }}
</span>
</div>
</td>
<td class="px-6 py-6 text-right">
<button @click="selectMachine({{ Js::from($machine) }})"

View File

@ -7,7 +7,7 @@
<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">
{{ __('Status') }}</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">
{{ __('Alerts') }}</th>
{{ __('Inventory Alerts') }}</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 Sync') }}</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-right">
@ -77,7 +77,7 @@
</span>
@endif
@if(!$machine->low_stock_count && !$machine->expiring_soon_count)
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-600 uppercase tracking-[0.1em]">{{ __('All Stable') }}</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-600 uppercase tracking-[0.1em]">{{ __('Inventory Stable') }}</span>
@endif
</div>
</td>